Closes #26212: Download wallpapers when a thumbnail is clicked

This commit is contained in:
mike a 2022-09-12 13:27:31 -07:00 committed by mergify[bot]
parent 67ba299f77
commit 3aff74b40a
8 changed files with 123 additions and 115 deletions

View File

@ -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

View File

@ -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),
)
}
}
}
}

View File

@ -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())) {
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)
},
)
}
}
}
@Composable
private fun WallpaperSnackbar(
onViewWallpaper: () -> Unit,
) {
Snackbar(
Column(
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,
)
},
)
.verticalScroll(rememberScrollState())
.background(color = FirefoxTheme.colors.layer1),
) {
WallpaperThumbnails(
wallpapers = wallpapers,
defaultWallpaper = defaultWallpaper,
loadWallpaperResource = loadWallpaperResource,
selectedWallpaper = selectedWallpaper,
onSelectWallpaper = { updatedWallpaper -> onSelectWallpaper(updatedWallpaper) },
)
}
}
/**
* 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 = {},
)
}
}

View File

@ -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))

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -453,6 +453,13 @@
<string name="wallpaper_updated_snackbar_message">Wallpaper updated!</string>
<!-- Snackbar label for action to view selected wallpaper -->
<string name="wallpaper_updated_snackbar_action">View</string>
<!-- Snackbar message for when wallpaper couldn't be downloaded -->
<string name="wallpaper_download_error_snackbar_message">Couldnt download wallpaper</string>
<!-- Snackbar label for action to retry downloading the wallpaper -->
<string name="wallpaper_download_error_snackbar_action">Try again</string>
<!-- Snackbar message for when wallpaper couldn't be selected because of the disk error -->
<string name="wallpaper_select_error_snackbar_message">Couldnt change wallpaper</string>
<!-- Label for switch which toggles the "tap-to-switch" behavior on home screen logo -->
<string name="wallpaper_tap_to_change_switch_label_1" moz:removedIn="105" tools:ignore="UnusedResources">Change wallpaper by tapping Firefox homepage logo</string>
<!-- This is the accessibility content description for the wallpapers functionality. Users are

View File

@ -506,7 +506,7 @@ class WallpapersUseCasesTest {
every { mockSettings.currentWallpaperName } returns ""
every { mockSettings.currentWallpaperName = capture(slot) } just runs
WallpapersUseCases.LegacySelectWallpaperUseCase(
val wallpaperFileState = WallpapersUseCases.LegacySelectWallpaperUseCase(
mockSettings,
appStore,
).invoke(selectedWallpaper)
@ -515,6 +515,7 @@ class WallpapersUseCasesTest {
assertEquals(selectedWallpaper.name, slot.captured)
assertEquals(selectedWallpaper, appStore.state.wallpaperState.currentWallpaper)
assertEquals(selectedWallpaper.name, Wallpapers.wallpaperSelected.testGetValue()?.first()?.extra?.get("name")!!)
assertEquals(wallpaperFileState, Wallpaper.ImageFileState.Downloaded)
}
@Test
@ -526,7 +527,7 @@ class WallpapersUseCasesTest {
every { mockSettings.currentWallpaperName = capture(slot) } just runs
coEvery { mockFileManager.wallpaperImagesExist(selectedWallpaper) } returns true
WallpapersUseCases.DefaultSelectWallpaperUseCase(
val wallpaperFileState = WallpapersUseCases.DefaultSelectWallpaperUseCase(
mockSettings,
appStore,
mockFileManager,
@ -537,6 +538,7 @@ class WallpapersUseCasesTest {
assertEquals(selectedWallpaper.name, slot.captured)
assertEquals(selectedWallpaper, appStore.state.wallpaperState.currentWallpaper)
assertEquals(selectedWallpaper.name, Wallpapers.wallpaperSelected.testGetValue()?.first()?.extra?.get("name")!!)
assertEquals(wallpaperFileState, Wallpaper.ImageFileState.Downloaded)
}
@Test
@ -550,7 +552,7 @@ class WallpapersUseCasesTest {
coEvery { mockFileManager.wallpaperImagesExist(selectedWallpaper) } returns false
coEvery { mockDownloader.downloadWallpaper(selectedWallpaper) } returns Wallpaper.ImageFileState.Downloaded
WallpapersUseCases.DefaultSelectWallpaperUseCase(
val wallpaperFileState = WallpapersUseCases.DefaultSelectWallpaperUseCase(
mockSettings,
mockStore,
mockFileManager,
@ -560,6 +562,7 @@ class WallpapersUseCasesTest {
verify { mockStore.dispatch(AppAction.WallpaperAction.UpdateWallpaperDownloadState(selectedWallpaper, Wallpaper.ImageFileState.Downloading)) }
verify { mockStore.dispatch(AppAction.WallpaperAction.UpdateWallpaperDownloadState(selectedWallpaper, Wallpaper.ImageFileState.Downloaded)) }
verify { mockStore.dispatch(AppAction.WallpaperAction.UpdateCurrentWallpaper(selectedWallpaper)) }
assertEquals(wallpaperFileState, Wallpaper.ImageFileState.Downloaded)
}
@Test
@ -573,7 +576,7 @@ class WallpapersUseCasesTest {
coEvery { mockFileManager.wallpaperImagesExist(selectedWallpaper) } returns false
coEvery { mockDownloader.downloadWallpaper(selectedWallpaper) } returns Wallpaper.ImageFileState.Error
WallpapersUseCases.DefaultSelectWallpaperUseCase(
val wallpaperFileState = WallpapersUseCases.DefaultSelectWallpaperUseCase(
mockSettings,
mockStore,
mockFileManager,
@ -582,6 +585,7 @@ class WallpapersUseCasesTest {
verify { mockStore.dispatch(AppAction.WallpaperAction.UpdateWallpaperDownloadState(selectedWallpaper, Wallpaper.ImageFileState.Downloading)) }
verify { mockStore.dispatch(AppAction.WallpaperAction.UpdateWallpaperDownloadState(selectedWallpaper, Wallpaper.ImageFileState.Error)) }
assertEquals(wallpaperFileState, Wallpaper.ImageFileState.Error)
}
private enum class TimeRelation {