diff --git a/app/src/main/java/org/mozilla/fenix/components/appstate/AppStoreReducer.kt b/app/src/main/java/org/mozilla/fenix/components/appstate/AppStoreReducer.kt index 18f091a3e3b4..5355af13d5fc 100644 --- a/app/src/main/java/org/mozilla/fenix/components/appstate/AppStoreReducer.kt +++ b/app/src/main/java/org/mozilla/fenix/components/appstate/AppStoreReducer.kt @@ -211,7 +211,7 @@ internal object AppStoreReducer { ) is AppAction.WallpaperAction.UpdateWallpaperDownloadState -> { val wallpapers = state.wallpaperState.availableWallpapers.map { - if (it == action.wallpaper) { + if (it.name == action.wallpaper.name) { it.copy(assetsFileState = action.imageState) } else { it diff --git a/app/src/main/java/org/mozilla/fenix/home/WallpapersObserver.kt b/app/src/main/java/org/mozilla/fenix/home/WallpapersObserver.kt index 9e01e6faee3f..d9868c77b0cc 100644 --- a/app/src/main/java/org/mozilla/fenix/home/WallpapersObserver.kt +++ b/app/src/main/java/org/mozilla/fenix/home/WallpapersObserver.kt @@ -15,6 +15,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import mozilla.components.lib.state.Store +import org.mozilla.fenix.R +import org.mozilla.fenix.addons.showSnackBar import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.appstate.AppState @@ -92,6 +94,13 @@ class WallpapersObserver( bitmap?.let { it.scaleToBottomOfView(wallpaperImageView) wallpaperImageView.isVisible = true + } ?: run { + with(wallpaperImageView) { + showSnackBar( + view = this, + text = resources.getString(R.string.wallpaper_select_error_snackbar_message), + ) + } } } } 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 b74cf59e29c1..17097c351299 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 @@ -8,6 +8,7 @@ import android.annotation.SuppressLint import android.graphics.Bitmap import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -17,37 +18,28 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Scaffold -import androidx.compose.material.Snackbar -import androidx.compose.material.SnackbarDuration -import androidx.compose.material.SnackbarHost +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import kotlinx.coroutines.launch import org.mozilla.fenix.R -import org.mozilla.fenix.compose.button.TextButton import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.Theme import org.mozilla.fenix.wallpapers.Wallpaper @@ -58,10 +50,9 @@ import org.mozilla.fenix.wallpapers.Wallpaper * * @param wallpapers Wallpapers to add to grid. * @param selectedWallpaper The currently selected wallpaper. - * @param defaultWallpaper The default 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 onViewWallpaper Callback for when the view action is clicked from snackbar. */ @SuppressLint("UnusedMaterialScaffoldPaddingParameter") @Composable @@ -72,76 +63,27 @@ fun WallpaperSettings( loadWallpaperResource: suspend (Wallpaper) -> Bitmap?, selectedWallpaper: Wallpaper, onSelectWallpaper: (Wallpaper) -> Unit, - onViewWallpaper: () -> Unit, ) { - val coroutineScope = rememberCoroutineScope() - val scaffoldState = rememberScaffoldState() - - Scaffold( - backgroundColor = FirefoxTheme.colors.layer1, - scaffoldState = scaffoldState, - snackbarHost = { hostState -> - SnackbarHost(hostState = hostState) { - WallpaperSnackbar(onViewWallpaper) - } - }, + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .background(color = FirefoxTheme.colors.layer1), ) { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - WallpaperThumbnails( - wallpapers = wallpapers, - defaultWallpaper = defaultWallpaper, - selectedWallpaper = selectedWallpaper, - loadWallpaperResource = loadWallpaperResource, - onSelectWallpaper = { updatedWallpaper -> - coroutineScope.launch { - scaffoldState.snackbarHostState.showSnackbar( - message = "", // overwritten by WallpaperSnackbar - duration = SnackbarDuration.Short, - ) - } - onSelectWallpaper(updatedWallpaper) - }, - ) - } + WallpaperThumbnails( + wallpapers = wallpapers, + defaultWallpaper = defaultWallpaper, + loadWallpaperResource = loadWallpaperResource, + selectedWallpaper = selectedWallpaper, + onSelectWallpaper = { updatedWallpaper -> onSelectWallpaper(updatedWallpaper) }, + ) } } -@Composable -private fun WallpaperSnackbar( - onViewWallpaper: () -> Unit, -) { - Snackbar( - modifier = Modifier - .padding(8.dp) - .heightIn(min = 48.dp), - backgroundColor = FirefoxTheme.colors.actionPrimary, - content = { - Text( - modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp), - text = stringResource(R.string.wallpaper_updated_snackbar_message), - textAlign = TextAlign.Start, - color = FirefoxTheme.colors.textOnColorPrimary, - overflow = TextOverflow.Ellipsis, - maxLines = 2, - style = FirefoxTheme.typography.headline7, - ) - }, - action = { - TextButton( - text = stringResource(R.string.wallpaper_updated_snackbar_action), - onClick = onViewWallpaper, - modifier = Modifier.padding(all = 8.dp), - textColor = FirefoxTheme.colors.textOnColorPrimary, - ) - }, - ) -} - /** * A grid of selectable wallpaper thumbnails. * * @param wallpapers Wallpapers to add to grid. - * @param defaultWallpaper The default wallpaper + * @param defaultWallpaper The default wallpaper. * @param loadWallpaperResource Callback to handle loading a wallpaper bitmap. Only optional in the default case. * @param selectedWallpaper The currently selected wallpaper. * @param numColumns The number of columns that will occupy the grid. @@ -173,16 +115,18 @@ fun WallpaperThumbnails( 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 = wallpapers[itemIndex], + wallpaper = wallpaper, defaultWallpaper = defaultWallpaper, loadWallpaperResource = loadWallpaperResource, - isSelected = selectedWallpaper == wallpapers[itemIndex], + isSelected = selectedWallpaper.name == wallpaper.name, + isLoading = wallpaper.assetsFileState == Wallpaper.ImageFileState.Downloading, onSelect = onSelectWallpaper, ) } @@ -199,9 +143,14 @@ fun WallpaperThumbnails( * A single wallpaper thumbnail. * * @param wallpaper The wallpaper to display. + * @param defaultWallpaper The default wallpaper. + * @param loadWallpaperResource Callback to handle loading a wallpaper bitmap. * @param isSelected Whether the wallpaper is currently selected. + * @param isLoading Whether the wallpaper is currently downloading. * @param aspectRatio The ratio of height to width of the thumbnail. * @param onSelect Action to take when this wallpaper is selected. + * @param loadingOpacity Opacity of the currently downloading wallpaper. + * @param onSelect Action to take when a new wallpaper is selected. */ @Composable @Suppress("LongParameterList") @@ -210,7 +159,9 @@ private fun WallpaperThumbnailItem( defaultWallpaper: Wallpaper, loadWallpaperResource: suspend (Wallpaper) -> Bitmap?, isSelected: Boolean, + isLoading: Boolean, aspectRatio: Float = 1.1f, + loadingOpacity: Float = 0.5f, onSelect: (Wallpaper) -> Unit, ) { var bitmap: Bitmap? by remember { mutableStateOf(null) } @@ -220,7 +171,12 @@ private fun WallpaperThumbnailItem( val thumbnailShape = RoundedCornerShape(8.dp) val border = if (isSelected) { Modifier.border( - BorderStroke(width = 2.dp, color = FirefoxTheme.colors.borderAccent), + BorderStroke(width = 3.dp, color = FirefoxTheme.colors.borderAccent), + thumbnailShape, + ) + } else if (wallpaper.name == Wallpaper.defaultName) { + Modifier.border( + BorderStroke(width = 1.dp, color = FirefoxTheme.colors.borderPrimary), thumbnailShape, ) } else { @@ -249,8 +205,20 @@ private fun WallpaperThumbnailItem( wallpaper.name, ), modifier = Modifier.fillMaxSize(), + alpha = if (isLoading) loadingOpacity else 1.0f, ) } + if (isLoading) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(24.dp), + ) { + CircularProgressIndicator( + color = FirefoxTheme.colors.borderAccent, + ) + } + } } } @@ -264,17 +232,6 @@ private fun WallpaperThumbnailsPreview() { wallpapers = listOf(Wallpaper.Default), selectedWallpaper = Wallpaper.Default, onSelectWallpaper = {}, - onViewWallpaper = {}, - ) - } -} - -@Preview -@Composable -private fun WallpaperSnackbarPreview() { - FirefoxTheme(theme = Theme.getTheme()) { - WallpaperSnackbar( - onViewWallpaper = {}, ) } } 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 262f9cf19698..173c7b9a8209 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 @@ -12,12 +12,14 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope 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.GleanMetrics.Wallpapers import org.mozilla.fenix.R +import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.theme.FirefoxTheme @@ -36,7 +38,7 @@ class WallpaperSettingsFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, - ): View? { + ): View { Wallpapers.wallpaperSettingsOpened.record(NoExtras()) return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) @@ -49,7 +51,7 @@ class WallpaperSettingsFragment : Fragment() { state.wallpaperState.currentWallpaper }.value ?: Wallpaper.Default - var coroutineScope = rememberCoroutineScope() + val coroutineScope = rememberCoroutineScope() WallpaperSettings( wallpapers = wallpapers, @@ -59,14 +61,52 @@ class WallpaperSettingsFragment : Fragment() { wallpaperUseCases.loadThumbnail(it) }, onSelectWallpaper = { - coroutineScope.launch { wallpaperUseCases.selectWallpaper(it) } + coroutineScope.launch { + val result = wallpaperUseCases.selectWallpaper(it) + onWallpaperSelected(it, result, this@apply) + } }, - onViewWallpaper = { findNavController().navigate(R.id.homeFragment) }, ) } } } } + + private fun onWallpaperSelected( + wallpaper: Wallpaper, + result: Wallpaper.ImageFileState, + view: View, + ) { + when (result) { + Wallpaper.ImageFileState.Downloaded -> { + FenixSnackbar.make( + view = view, + isDisplayedWithBrowserToolbar = false, + ) + .setText(view.context.getString(R.string.wallpaper_updated_snackbar_message)) + .setAction(requireContext().getString(R.string.wallpaper_updated_snackbar_action)) { + findNavController().navigate(R.id.homeFragment) + } + .show() + } + Wallpaper.ImageFileState.Error -> { + FenixSnackbar.make( + view = view, + isDisplayedWithBrowserToolbar = false, + ) + .setText(view.context.getString(R.string.wallpaper_download_error_snackbar_message)) + .setAction(view.context.getString(R.string.wallpaper_download_error_snackbar_action)) { + viewLifecycleOwner.lifecycleScope.launch { + val retryResult = wallpaperUseCases.selectWallpaper(wallpaper) + onWallpaperSelected(wallpaper, retryResult, view) + } + } + .show() + } + else -> { /* noop */ } + } + } + override fun onResume() { super.onResume() showToolbar(getString(R.string.customize_wallpapers)) diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/Wallpaper.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/Wallpaper.kt index d1a45939a70c..89207c4a62c2 100644 --- a/app/src/main/java/org/mozilla/fenix/wallpapers/Wallpaper.kt +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/Wallpaper.kt @@ -138,16 +138,4 @@ data class Wallpaper( Downloaded, Error, } - - override fun hashCode(): Int { - return name.hashCode() - } - - override fun equals(other: Any?): Boolean { - return if (other is Wallpaper) { - this.name == other.name - } else { - false - } - } } diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt index 73da1bbc92b8..9c2a4631bcde 100644 --- a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt @@ -423,7 +423,7 @@ class WallpapersUseCases( * * @param wallpaper The selected wallpaper. */ - suspend operator fun invoke(wallpaper: Wallpaper) + suspend operator fun invoke(wallpaper: Wallpaper): Wallpaper.ImageFileState } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @@ -436,7 +436,7 @@ class WallpapersUseCases( * * @param wallpaper The selected wallpaper. */ - override suspend fun invoke(wallpaper: Wallpaper) { + override suspend fun invoke(wallpaper: Wallpaper): Wallpaper.ImageFileState { settings.currentWallpaperName = wallpaper.name settings.currentWallpaperTextColor = wallpaper.textColor ?: 0 settings.currentWallpaperCardColor = wallpaper.cardColor ?: 0 @@ -447,6 +447,7 @@ class WallpapersUseCases( themeCollection = wallpaper.collection.name, ), ) + return Wallpaper.ImageFileState.Downloaded } } @@ -462,10 +463,11 @@ class WallpapersUseCases( * * @param wallpaper The selected wallpaper. */ - override suspend fun invoke(wallpaper: Wallpaper) { - if (wallpaper == Wallpaper.Default || fileManager.wallpaperImagesExist(wallpaper)) { + override suspend fun invoke(wallpaper: Wallpaper): Wallpaper.ImageFileState { + return if (wallpaper == Wallpaper.Default || fileManager.wallpaperImagesExist(wallpaper)) { selectWallpaper(wallpaper) dispatchDownloadState(wallpaper, Wallpaper.ImageFileState.Downloaded) + Wallpaper.ImageFileState.Downloaded } else { dispatchDownloadState(wallpaper, Wallpaper.ImageFileState.Downloading) val result = downloader.downloadWallpaper(wallpaper) @@ -473,6 +475,7 @@ class WallpapersUseCases( if (result == Wallpaper.ImageFileState.Downloaded) { selectWallpaper(wallpaper) } + result } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c45940305785..3e2f66101494 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -453,6 +453,13 @@ Wallpaper updated! View + + Couldn’t download wallpaper + + Try again + + Couldn’t change wallpaper + Change wallpaper by tapping Firefox homepage logo