diff --git a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt index 481c355af..302ae0828 100644 --- a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt +++ b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt @@ -16,6 +16,7 @@ import androidx.annotation.IdRes enum class BrowserDirection(@IdRes val fragmentId: Int) { FromGlobal(0), FromHome(R.id.homeFragment), + FromWallpaper(R.id.wallpaperSettingsFragment), FromSearchDialog(R.id.searchDialogFragment), FromSettings(R.id.settingsFragment), FromBookmarks(R.id.bookmarkFragment), diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 46c6bcd5d..fc70fd044 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -121,6 +121,7 @@ import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirecti import org.mozilla.fenix.settings.search.AddSearchEngineFragmentDirections import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirections import org.mozilla.fenix.settings.studies.StudiesFragmentDirections +import org.mozilla.fenix.settings.wallpaper.WallpaperSettingsFragmentDirections import org.mozilla.fenix.share.AddNewDeviceFragmentDirections import org.mozilla.fenix.tabstray.TabsTrayFragment import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections @@ -811,6 +812,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { NavGraphDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromHome -> HomeFragmentDirections.actionGlobalBrowser(customTabSessionId) + BrowserDirection.FromWallpaper -> + WallpaperSettingsFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromSearchDialog -> SearchDialogFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromSettings -> diff --git a/app/src/main/java/org/mozilla/fenix/settings/wallpaper/Extensions.kt b/app/src/main/java/org/mozilla/fenix/settings/wallpaper/Extensions.kt new file mode 100644 index 000000000..724ba8e60 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/wallpaper/Extensions.kt @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.wallpaper + +import org.mozilla.fenix.wallpapers.Wallpaper + +/** + * The extension function to group wallpapers according to their name. + **/ +fun List.groupByDisplayableCollection(): Map> = groupBy { + it.collection +}.filter { + it.key.name != "default" +}.map { + val wallpapers = it.value.filter { wallpaper -> + wallpaper.thumbnailFileState == Wallpaper.ImageFileState.Downloaded + } + if (it.key.name == "classic-firefox") { + it.key to listOf(Wallpaper.Default) + wallpapers + } else { + it.key to wallpapers + } +}.toMap().takeIf { + it.isNotEmpty() +} ?: mapOf(Wallpaper.DefaultCollection to listOf(Wallpaper.Default)) diff --git a/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettings.kt b/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettings.kt index 17097c351..423d41edb 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettings.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettings.kt @@ -16,8 +16,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState @@ -25,6 +27,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Surface +import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -37,9 +40,11 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.mozilla.fenix.R +import org.mozilla.fenix.compose.ClickableSubstringLink import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.Theme import org.mozilla.fenix.wallpapers.Wallpaper @@ -48,34 +53,110 @@ import org.mozilla.fenix.wallpapers.Wallpaper * The screen for controlling settings around Wallpapers. When a new wallpaper is selected, * a snackbar will be displayed. * - * @param wallpapers Wallpapers to add to grid. + * @param wallpaperGroups Wallpapers groups to add to grid. * @param selectedWallpaper The currently selected wallpaper. * @param defaultWallpaper The default wallpaper. * @param loadWallpaperResource Callback to handle loading a wallpaper bitmap. Only optional in the default case. * @param onSelectWallpaper Callback for when a new wallpaper is selected. + * @param onLearnMoreClick Callback for when the learn more action is clicked from the group description. */ @SuppressLint("UnusedMaterialScaffoldPaddingParameter") @Composable @Suppress("LongParameterList") fun WallpaperSettings( - wallpapers: List, + wallpaperGroups: Map>, defaultWallpaper: Wallpaper, loadWallpaperResource: suspend (Wallpaper) -> Bitmap?, selectedWallpaper: Wallpaper, onSelectWallpaper: (Wallpaper) -> Unit, + onLearnMoreClick: (String) -> Unit, ) { Column( modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() .verticalScroll(rememberScrollState()) - .background(color = FirefoxTheme.colors.layer1), + .background(color = FirefoxTheme.colors.layer1) + .padding( + end = 16.dp, + start = 16.dp, + top = 16.dp, + ), ) { - WallpaperThumbnails( - wallpapers = wallpapers, - defaultWallpaper = defaultWallpaper, - loadWallpaperResource = loadWallpaperResource, - selectedWallpaper = selectedWallpaper, - onSelectWallpaper = { updatedWallpaper -> onSelectWallpaper(updatedWallpaper) }, + wallpaperGroups.forEach { (collection, wallpapers) -> + if (wallpapers.isNotEmpty()) { + WallpaperGroupHeading( + collection = collection, + onLearnMoreClick = onLearnMoreClick, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + WallpaperThumbnails( + wallpapers = wallpapers, + defaultWallpaper = defaultWallpaper, + loadWallpaperResource = loadWallpaperResource, + selectedWallpaper = selectedWallpaper, + onSelectWallpaper = onSelectWallpaper, + ) + + Spacer(modifier = Modifier.height(32.dp)) + } + } + } +} + +@Composable +private fun WallpaperGroupHeading( + collection: Wallpaper.Collection, + onLearnMoreClick: (String) -> Unit, +) { + // Since the last new collection of wallpapers was tied directly to an MR release, + // it was decided that we should use string resources for these titles + // and descriptions so they could be localized. + // In the future, we may want to either use the dynamic wallpaper properties with localized fallbacks + // or invest in a method of localizing the remote strings themselves. + if (collection.name == "classic-firefox") { + Text( + text = stringResource(R.string.wallpaper_classic_title), + color = FirefoxTheme.colors.textSecondary, + style = FirefoxTheme.typography.subtitle2, ) + } else { + Column { + Text( + text = stringResource(R.string.wallpaper_limited_edition_title), + color = FirefoxTheme.colors.textSecondary, + style = FirefoxTheme.typography.subtitle2, + ) + + Spacer(modifier = Modifier.height(2.dp)) + + if (collection.learnMoreUrl.isNullOrEmpty()) { + val text = stringResource(R.string.wallpaper_limited_edition_description) + Text( + text = text, + color = FirefoxTheme.colors.textSecondary, + style = FirefoxTheme.typography.caption, + ) + } else { + val link = stringResource(R.string.wallpaper_learn_more) + val text = stringResource(R.string.wallpaper_limited_edition_description_with_learn_more, link) + val linkStartIndex = text.indexOf(link) + val linkEndIndex = linkStartIndex + link.length + + ClickableSubstringLink( + text = text, + textColor = FirefoxTheme.colors.textSecondary, + linkTextColor = FirefoxTheme.colors.textSecondary, + linkTextDecoration = TextDecoration.Underline, + clickableStartIndex = linkStartIndex, + clickableEndIndex = linkEndIndex, + ) { + onLearnMoreClick(collection.learnMoreUrl) + } + } + } } } @@ -88,8 +169,6 @@ fun WallpaperSettings( * @param selectedWallpaper The currently selected wallpaper. * @param numColumns The number of columns that will occupy the grid. * @param onSelectWallpaper Action to take when a new wallpaper is selected. - * @param verticalPadding Vertical content padding inside the block. - * @param horizontalPadding Horizontal content padding inside the block. */ @Composable @Suppress("LongParameterList") @@ -100,39 +179,30 @@ fun WallpaperThumbnails( loadWallpaperResource: suspend (Wallpaper) -> Bitmap?, onSelectWallpaper: (Wallpaper) -> Unit, numColumns: Int = 3, - verticalPadding: Int = 30, - horizontalPadding: Int = 20, ) { - Column( - modifier = Modifier.padding( - vertical = verticalPadding.dp, - horizontal = horizontalPadding.dp, - ), - ) { - val numRows = (wallpapers.size + numColumns - 1) / numColumns - for (rowIndex in 0 until numRows) { - Row { - for (columnIndex in 0 until numColumns) { - val itemIndex = rowIndex * numColumns + columnIndex - if (itemIndex < wallpapers.size) { - val wallpaper = wallpapers[itemIndex] - Box( - modifier = Modifier - .weight(1f, fill = true) - .padding(4.dp), - ) { - WallpaperThumbnailItem( - wallpaper = wallpaper, - defaultWallpaper = defaultWallpaper, - loadWallpaperResource = loadWallpaperResource, - isSelected = selectedWallpaper.name == wallpaper.name, - isLoading = wallpaper.assetsFileState == Wallpaper.ImageFileState.Downloading, - onSelect = onSelectWallpaper, - ) - } - } else { - Spacer(Modifier.weight(1f)) + val numRows = (wallpapers.size + numColumns - 1) / numColumns + for (rowIndex in 0 until numRows) { + Row { + for (columnIndex in 0 until numColumns) { + val itemIndex = rowIndex * numColumns + columnIndex + if (itemIndex < wallpapers.size) { + val wallpaper = wallpapers[itemIndex] + Box( + modifier = Modifier + .weight(1f, fill = true) + .padding(4.dp), + ) { + WallpaperThumbnailItem( + wallpaper = wallpaper, + defaultWallpaper = defaultWallpaper, + loadWallpaperResource = loadWallpaperResource, + isSelected = selectedWallpaper.name == wallpaper.name, + isLoading = wallpaper.assetsFileState == Wallpaper.ImageFileState.Downloading, + onSelect = onSelectWallpaper, + ) } + } else { + Spacer(Modifier.weight(1f)) } } } @@ -229,9 +299,10 @@ private fun WallpaperThumbnailsPreview() { WallpaperSettings( defaultWallpaper = Wallpaper.Default, loadWallpaperResource = { null }, - wallpapers = listOf(Wallpaper.Default), + wallpaperGroups = mapOf(Wallpaper.DefaultCollection to listOf(Wallpaper.Default)), selectedWallpaper = Wallpaper.Default, onSelectWallpaper = {}, + onLearnMoreClick = {}, ) } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettingsFragment.kt index 173c7b9a8..09f185b3e 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettingsFragment.kt @@ -17,7 +17,9 @@ import androidx.navigation.fragment.findNavController import kotlinx.coroutines.launch import mozilla.components.lib.state.ext.observeAsComposableState import mozilla.components.service.glean.private.NoExtras +import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.GleanMetrics.Wallpapers +import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.ext.requireComponents @@ -54,7 +56,7 @@ class WallpaperSettingsFragment : Fragment() { val coroutineScope = rememberCoroutineScope() WallpaperSettings( - wallpapers = wallpapers, + wallpaperGroups = wallpapers.groupByDisplayableCollection(), defaultWallpaper = Wallpaper.Default, selectedWallpaper = currentWallpaper, loadWallpaperResource = { @@ -66,6 +68,13 @@ class WallpaperSettingsFragment : Fragment() { onWallpaperSelected(it, result, this@apply) } }, + onLearnMoreClick = { url -> + (activity as HomeActivity).openToBrowserAndLoad( + searchTermOrURL = url, + newTab = true, + from = BrowserDirection.FromWallpaper, + ) + }, ) } } diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperOnboarding.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperOnboarding.kt index 9fd370af6..2235ba1fe 100644 --- a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperOnboarding.kt +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperOnboarding.kt @@ -91,16 +91,18 @@ fun WallpaperOnboarding( style = FirefoxTheme.typography.caption, ) + Spacer(modifier = Modifier.height(16.dp)) + WallpaperThumbnails( wallpapers = wallpapers, defaultWallpaper = Wallpaper.Default, selectedWallpaper = currentWallpaper, loadWallpaperResource = { loadWallpaperResource(it) }, onSelectWallpaper = { onSelectWallpaper(it) }, - verticalPadding = 16, - horizontalPadding = 0, ) + Spacer(modifier = Modifier.height(16.dp)) + TextButton( modifier = Modifier .align(Alignment.CenterHorizontally) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3e2f66101..d1ee0ffd6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -459,12 +459,21 @@ Try again Couldn’t change wallpaper - + + Learn more Change wallpaper by tapping Firefox homepage logo Firefox logo - change the wallpaper, button + + Classic Firefox + + Limited Edition + + The new Independent Voices collection. %s + + The new Independent Voices collection. Try a splash of color diff --git a/app/src/test/java/org/mozilla/fenix/settings/wallpaper/ExtensionsTest.kt b/app/src/test/java/org/mozilla/fenix/settings/wallpaper/ExtensionsTest.kt new file mode 100644 index 000000000..f370f3362 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/settings/wallpaper/ExtensionsTest.kt @@ -0,0 +1,88 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.wallpaper + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.mozilla.fenix.wallpapers.Wallpaper + +class ExtensionsTest { + private val classicCollection = getSeasonalCollection("classic-firefox") + + @Test + fun `GIVEN wallpapers that include the default WHEN grouped by collection THEN default will be added to classic firefox`() { + val seasonalCollection = getSeasonalCollection("finally fall") + val classicFirefoxWallpapers = (0..5).map { generateClassicFirefoxWallpaper("firefox$it") } + val seasonalWallpapers = (0..5).map { generateSeasonalWallpaperCollection("${seasonalCollection.name}$it", seasonalCollection.name) } + val allWallpapers = listOf(Wallpaper.Default) + classicFirefoxWallpapers + seasonalWallpapers + + val result = allWallpapers.groupByDisplayableCollection() + + assertEquals(2, result.size) + assertEquals(listOf(Wallpaper.Default) + classicFirefoxWallpapers, result[classicCollection]) + assertEquals(seasonalWallpapers, result[seasonalCollection]) + } + + @Test + fun `GIVEN no wallpapers but the default WHEN grouped by collection THEN the default will still be present`() { + val result = listOf(Wallpaper.Default).groupByDisplayableCollection() + + assertEquals(1, result.size) + assertEquals(listOf(Wallpaper.Default), result[Wallpaper.DefaultCollection]) + } + + @Test + fun `GIVEN wallpapers with thumbnails that have not downloaded WHEN grouped by collection THEN wallpapers without thumbnails will not be included`() { + val seasonalCollection = getSeasonalCollection("finally fall") + val classicFirefoxWallpapers = (0..5).map { generateClassicFirefoxWallpaper("firefox$it") } + val downloadedSeasonalWallpapers = (0..5).map { generateSeasonalWallpaperCollection("${seasonalCollection.name}$it", seasonalCollection.name) } + val nonDownloadedSeasonalWallpapers = (0..5).map { + generateSeasonalWallpaperCollection( + "${seasonalCollection.name}$it", + seasonalCollection.name, + Wallpaper.ImageFileState.Error, + ) + } + val allWallpapers = listOf(Wallpaper.Default) + classicFirefoxWallpapers + downloadedSeasonalWallpapers + nonDownloadedSeasonalWallpapers + + val result = allWallpapers.groupByDisplayableCollection() + + assertEquals(2, result.size) + assertEquals(listOf(Wallpaper.Default) + classicFirefoxWallpapers, result[classicCollection]) + assertEquals(downloadedSeasonalWallpapers, result[seasonalCollection]) + } + + private fun generateClassicFirefoxWallpaper(name: String) = Wallpaper( + name = name, + textColor = 0L, + cardColor = 0L, + thumbnailFileState = Wallpaper.ImageFileState.Downloaded, + assetsFileState = Wallpaper.ImageFileState.Downloaded, + collection = classicCollection, + ) + + private fun getSeasonalCollection(name: String) = Wallpaper.Collection( + name = name, + heading = null, + description = null, + learnMoreUrl = null, + availableLocales = null, + startDate = null, + endDate = null, + ) + + private fun generateSeasonalWallpaperCollection( + wallpaperName: String, + collectionName: String, + thumbnailState: Wallpaper.ImageFileState = Wallpaper.ImageFileState.Downloaded, + ) = Wallpaper( + name = wallpaperName, + textColor = 0L, + cardColor = 0L, + thumbnailFileState = thumbnailState, + assetsFileState = Wallpaper.ImageFileState.Downloaded, + collection = getSeasonalCollection(collectionName), + ) +}