2
0
mirror of https://github.com/fork-maintainers/iceraven-browser synced 2024-11-19 09:25:34 +00:00

[fenix] Closes https://github.com/mozilla-mobile/fenix/issues/26211: Download wallpapers when thumbnails clicked

This commit is contained in:
MatthewTighe 2022-08-19 15:47:55 -07:00 committed by mergify[bot]
parent a3f055a36e
commit e70f6d19af
14 changed files with 283 additions and 76 deletions

View File

@ -5,6 +5,7 @@
package org.mozilla.fenix.components package org.mozilla.fenix.components
import android.content.Context import android.content.Context
import android.os.StrictMode
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.Engine
import mozilla.components.concept.fetch.Client import mozilla.components.concept.fetch.Client
@ -23,6 +24,7 @@ import mozilla.components.feature.tabs.CustomTabsUseCases
import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.feature.top.sites.TopSitesStorage import mozilla.components.feature.top.sites.TopSitesStorage
import mozilla.components.feature.top.sites.TopSitesUseCases import mozilla.components.feature.top.sites.TopSitesUseCases
import mozilla.components.support.locale.LocaleManager
import mozilla.components.support.locale.LocaleUseCases import mozilla.components.support.locale.LocaleUseCases
import org.mozilla.fenix.components.bookmarks.BookmarksUseCase import org.mozilla.fenix.components.bookmarks.BookmarksUseCase
import org.mozilla.fenix.perf.StrictModeManager import org.mozilla.fenix.perf.StrictModeManager
@ -107,6 +109,13 @@ class UseCases(
val bookmarksUseCases by lazyMonitored { BookmarksUseCase(bookmarksStorage, historyStorage) } val bookmarksUseCases by lazyMonitored { BookmarksUseCase(bookmarksStorage, historyStorage) }
val wallpaperUseCases by lazyMonitored { val wallpaperUseCases by lazyMonitored {
WallpapersUseCases(context, appStore, client, strictMode) // Required to even access context.filesDir property and to retrieve current locale
val (rootStorageDirectory, currentLocale) = strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
val rootStorageDirectory = context.filesDir
val currentLocale = LocaleManager.getCurrentLocale(context)?.toLanguageTag()
?: LocaleManager.getSystemDefault().toLanguageTag()
rootStorageDirectory to currentLocale
}
WallpapersUseCases(context, appStore, client, rootStorageDirectory, currentLocale)
} }
} }

View File

@ -169,5 +169,17 @@ sealed class AppAction : Action {
* Indicates that the list of potential wallpapers has changed. * Indicates that the list of potential wallpapers has changed.
*/ */
data class UpdateAvailableWallpapers(val wallpapers: List<Wallpaper>) : WallpaperAction() data class UpdateAvailableWallpapers(val wallpapers: List<Wallpaper>) : WallpaperAction()
/**
* Indicates a change in the download state of a wallpaper. Note that this is meant to be
* used for full size images, not thumbnails.
*
* @property wallpaper The wallpaper that is being updated.
* @property imageState The updated image state for the wallpaper.
*/
data class UpdateWallpaperDownloadState(
val wallpaper: Wallpaper,
val imageState: Wallpaper.ImageFileState
) : WallpaperAction()
} }
} }

View File

@ -209,6 +209,17 @@ internal object AppStoreReducer {
state.copy( state.copy(
wallpaperState = state.wallpaperState.copy(availableWallpapers = action.wallpapers) wallpaperState = state.wallpaperState.copy(availableWallpapers = action.wallpapers)
) )
is AppAction.WallpaperAction.UpdateWallpaperDownloadState -> {
val wallpapers = state.wallpaperState.availableWallpapers.map {
if (it == action.wallpaper) {
it.copy(assetsFileState = action.imageState)
} else {
it
}
}
val wallpaperState = state.wallpaperState.copy(availableWallpapers = wallpapers)
state.copy(wallpaperState = wallpaperState)
}
} }
} }

View File

@ -8,10 +8,12 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.launch
import mozilla.components.lib.state.ext.observeAsComposableState import mozilla.components.lib.state.ext.observeAsComposableState
import mozilla.components.service.glean.private.NoExtras import mozilla.components.service.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.Wallpapers import org.mozilla.fenix.GleanMetrics.Wallpapers
@ -47,12 +49,18 @@ class WallpaperSettingsFragment : Fragment() {
state.wallpaperState.currentWallpaper state.wallpaperState.currentWallpaper
}.value ?: Wallpaper.Default }.value ?: Wallpaper.Default
var coroutineScope = rememberCoroutineScope()
WallpaperSettings( WallpaperSettings(
wallpapers = wallpapers, wallpapers = wallpapers,
defaultWallpaper = Wallpaper.Default, defaultWallpaper = Wallpaper.Default,
loadWallpaperResource = { wallpaperUseCases.loadBitmap(it) },
selectedWallpaper = currentWallpaper, selectedWallpaper = currentWallpaper,
onSelectWallpaper = { wallpaperUseCases.selectWallpaper(it) }, loadWallpaperResource = {
wallpaperUseCases.loadThumbnail(it)
},
onSelectWallpaper = {
coroutineScope.launch { wallpaperUseCases.selectWallpaper(it) }
},
onViewWallpaper = { findNavController().navigate(R.id.homeFragment) }, onViewWallpaper = { findNavController().navigate(R.id.homeFragment) },
) )
} }

View File

@ -38,7 +38,8 @@ class LegacyWallpaperFileManager(
collection = Wallpaper.DefaultCollection, collection = Wallpaper.DefaultCollection,
textColor = null, textColor = null,
cardColor = null, cardColor = null,
thumbnailFileState = Wallpaper.ImageFileState.NotAvailable, thumbnailFileState = Wallpaper.ImageFileState.Unavailable,
assetsFileState = Wallpaper.ImageFileState.Downloaded,
) )
} else null } else null
} }

View File

@ -22,6 +22,7 @@ data class Wallpaper(
val textColor: Long?, val textColor: Long?,
val cardColor: Long?, val cardColor: Long?,
val thumbnailFileState: ImageFileState, val thumbnailFileState: ImageFileState,
val assetsFileState: ImageFileState,
) { ) {
/** /**
* Type that represents a collection that a [Wallpaper] belongs to. * Type that represents a collection that a [Wallpaper] belongs to.
@ -68,6 +69,7 @@ data class Wallpaper(
textColor = null, textColor = null,
cardColor = null, cardColor = null,
thumbnailFileState = ImageFileState.Downloaded, thumbnailFileState = ImageFileState.Downloaded,
assetsFileState = ImageFileState.Downloaded,
) )
/** /**
@ -104,6 +106,7 @@ data class Wallpaper(
cardColor = cardColor, cardColor = cardColor,
collection = DefaultCollection, collection = DefaultCollection,
thumbnailFileState = ImageFileState.Downloaded, thumbnailFileState = ImageFileState.Downloaded,
assetsFileState = ImageFileState.Downloaded,
) )
} else null } else null
} }
@ -127,9 +130,21 @@ data class Wallpaper(
* Defines the download state of wallpaper asset. * Defines the download state of wallpaper asset.
*/ */
enum class ImageFileState { enum class ImageFileState {
NotAvailable, Unavailable,
Downloading, Downloading,
Downloaded, Downloaded,
Error, 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

@ -37,30 +37,39 @@ class WallpaperDownloader(
* and will be stored in the local path: * and will be stored in the local path:
* wallpapers/<wallpaper name>/<orientation>.png * wallpapers/<wallpaper name>/<orientation>.png
*/ */
suspend fun downloadWallpaper(wallpaper: Wallpaper) = withContext(dispatcher) { suspend fun downloadWallpaper(wallpaper: Wallpaper): Wallpaper.ImageFileState = withContext(dispatcher) {
listOf(Wallpaper.ImageType.Portrait, Wallpaper.ImageType.Landscape).map { imageType -> val portraitResult = downloadAsset(wallpaper, Wallpaper.ImageType.Portrait)
wallpaper.downloadAsset(imageType) val landscapeResult = downloadAsset(wallpaper, Wallpaper.ImageType.Landscape)
return@withContext if (portraitResult == Wallpaper.ImageFileState.Downloaded &&
landscapeResult == Wallpaper.ImageFileState.Downloaded
) {
Wallpaper.ImageFileState.Downloaded
} else {
Wallpaper.ImageFileState.Error
} }
} }
/** /**
* Downloads a thumbnail for a wallpaper from the network. This is expected to be found remotely * Downloads a thumbnail for a wallpaper from the network. This is expected to be found remotely
* at: * at:
* <WALLPAPER_URL>/<collection name>/<wallpaper name>/<orientation>.png * <WALLPAPER_URL>/<collection name>/<wallpaper name>/thumbnail.png
* and stored locally at: * and stored locally at:
* wallpapers/<wallpaper name>/<orientation>.png * wallpapers/<wallpaper name>/thumbnail.png
*/ */
suspend fun downloadThumbnail(wallpaper: Wallpaper): Wallpaper.ImageFileState = withContext(dispatcher) { suspend fun downloadThumbnail(wallpaper: Wallpaper): Wallpaper.ImageFileState = withContext(dispatcher) {
wallpaper.downloadAsset(Wallpaper.ImageType.Thumbnail) downloadAsset(wallpaper, Wallpaper.ImageType.Thumbnail)
} }
private suspend fun Wallpaper.downloadAsset( private suspend fun downloadAsset(
wallpaper: Wallpaper,
imageType: Wallpaper.ImageType imageType: Wallpaper.ImageType
): Wallpaper.ImageFileState = withContext(dispatcher) { ): Wallpaper.ImageFileState = withContext(dispatcher) {
val localFile = File(storageRootDirectory, getLocalPath(name, imageType)) val localFile = File(storageRootDirectory, getLocalPath(wallpaper.name, imageType))
if (localFile.exists()) return@withContext Wallpaper.ImageFileState.Downloaded if (localFile.exists()) {
return@withContext Wallpaper.ImageFileState.Downloaded
}
val remotePath = "${collection.name}/${name}/${imageType.lowercase()}.png" val remotePath = "${wallpaper.collection.name}/${wallpaper.name}/${imageType.lowercase()}.png"
val request = Request( val request = Request(
url = "$remoteHost/$remotePath", url = "$remoteHost/$remotePath",
method = Request.Method.GET method = Request.Method.GET
@ -83,7 +92,7 @@ class WallpaperDownloader(
localFile.delete() localFile.delete()
} }
} }
Wallpaper.ImageFileState.Downloaded Wallpaper.ImageFileState.Error
} }
} }
} }

View File

@ -31,20 +31,21 @@ class WallpaperFileManager(
* files for each of a portrait and landscape orientation as well as a thumbnail. * files for each of a portrait and landscape orientation as well as a thumbnail.
*/ */
suspend fun lookupExpiredWallpaper(name: String): Wallpaper? = withContext(Dispatchers.IO) { suspend fun lookupExpiredWallpaper(name: String): Wallpaper? = withContext(Dispatchers.IO) {
if (getAllLocalWallpaperPaths(name).all { File(storageRootDirectory, it).exists() }) { if (allAssetsExist(name)) {
Wallpaper( Wallpaper(
name = name, name = name,
collection = Wallpaper.DefaultCollection, collection = Wallpaper.DefaultCollection,
textColor = null, textColor = null,
cardColor = null, cardColor = null,
thumbnailFileState = Wallpaper.ImageFileState.Downloaded, thumbnailFileState = Wallpaper.ImageFileState.Downloaded,
assetsFileState = Wallpaper.ImageFileState.Downloaded,
) )
} else null } else null
} }
private fun getAllLocalWallpaperPaths(name: String): List<String> = private fun allAssetsExist(name: String): Boolean =
Wallpaper.ImageType.values().map { orientation -> Wallpaper.ImageType.values().all { type ->
getLocalPath(name, orientation) File(storageRootDirectory, getLocalPath(name, type)).exists()
} }
/** /**
@ -60,4 +61,11 @@ class WallpaperFileManager(
} }
} }
} }
/**
* Checks whether all the assets for a wallpaper exist on the file system.
*/
suspend fun wallpaperImagesExist(wallpaper: Wallpaper): Boolean = withContext(Dispatchers.IO) {
return@withContext allAssetsExist(wallpaper.name)
}
} }

View File

@ -83,7 +83,8 @@ class WallpaperMetadataFetcher(
textColor = getArgbValueAsLong("text-color"), textColor = getArgbValueAsLong("text-color"),
cardColor = getArgbValueAsLong("card-color"), cardColor = getArgbValueAsLong("card-color"),
collection = collection, collection = collection,
thumbnailFileState = Wallpaper.ImageFileState.NotAvailable, thumbnailFileState = Wallpaper.ImageFileState.Unavailable,
assetsFileState = Wallpaper.ImageFileState.Unavailable,
) )
} }
} }

View File

@ -8,19 +8,16 @@ import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.os.StrictMode
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mozilla.components.concept.fetch.Client import mozilla.components.concept.fetch.Client
import mozilla.components.support.locale.LocaleManager
import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.GleanMetrics.Wallpapers import org.mozilla.fenix.GleanMetrics.Wallpapers
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
import java.io.File import java.io.File
import java.util.Date import java.util.Date
@ -31,7 +28,8 @@ import java.util.Date
* @param context Used for various file and configuration checks. * @param context Used for various file and configuration checks.
* @param store Will receive dispatches of metadata updates like the currently selected wallpaper. * @param store Will receive dispatches of metadata updates like the currently selected wallpaper.
* @param client Handles downloading wallpapers and their metadata. * @param client Handles downloading wallpapers and their metadata.
* @param strictMode Required for determining some device state like current locale and file paths. * @param storageRootDirectory The top level app-local storage directory.
* @param currentLocale The locale currently being used on the device.
* *
* @property initialize Usecase for initializing wallpaper feature. Should usually be called early * @property initialize Usecase for initializing wallpaper feature. Should usually be called early
* in the app's lifetime to ensure that any potential long-running tasks can complete quickly. * in the app's lifetime to ensure that any potential long-running tasks can complete quickly.
@ -42,20 +40,13 @@ class WallpapersUseCases(
context: Context, context: Context,
store: AppStore, store: AppStore,
client: Client, client: Client,
strictMode: StrictModeManager, storageRootDirectory: File,
filesDir: File, currentLocale: String,
) { ) {
private val downloader = WallpaperDownloader(storageRootDirectory, client)
private val fileManager = WallpaperFileManager(storageRootDirectory)
val initialize: InitializeWallpapersUseCase by lazy { val initialize: InitializeWallpapersUseCase by lazy {
if (FeatureFlags.wallpaperV2Enabled) { if (FeatureFlags.wallpaperV2Enabled) {
// Required to even access context.filesDir property and to retrieve current locale
val (storageRootDirectory, currentLocale) = strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
val storageRootDirectory = context.filesDir
val currentLocale = LocaleManager.getCurrentLocale(context)?.toLanguageTag()
?: LocaleManager.getSystemDefault().toLanguageTag()
storageRootDirectory to currentLocale
}
val downloader = WallpaperDownloader(storageRootDirectory, client)
val fileManager = WallpaperFileManager(storageRootDirectory)
val metadataFetcher = WallpaperMetadataFetcher(client) val metadataFetcher = WallpaperMetadataFetcher(client)
DefaultInitializeWallpaperUseCase( DefaultInitializeWallpaperUseCase(
store = store, store = store,
@ -66,13 +57,7 @@ class WallpapersUseCases(
currentLocale = currentLocale currentLocale = currentLocale
) )
} else { } else {
// Required to even access context.filesDir property and to retrieve current locale val fileManager = LegacyWallpaperFileManager(storageRootDirectory)
val (fileManager, currentLocale) = strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
val fileManager = LegacyWallpaperFileManager(context.filesDir)
val currentLocale = LocaleManager.getCurrentLocale(context)?.toLanguageTag()
?: LocaleManager.getSystemDefault().toLanguageTag()
fileManager to currentLocale
}
val downloader = LegacyWallpaperDownloader(context, client) val downloader = LegacyWallpaperDownloader(context, client)
LegacyInitializeWallpaperUseCase( LegacyInitializeWallpaperUseCase(
store = store, store = store,
@ -92,12 +77,18 @@ class WallpapersUseCases(
} }
val loadThumbnail: LoadThumbnailUseCase by lazy { val loadThumbnail: LoadThumbnailUseCase by lazy {
if (FeatureFlags.wallpaperV2Enabled) { if (FeatureFlags.wallpaperV2Enabled) {
DefaultLoadThumbnailUseCase(filesDir) DefaultLoadThumbnailUseCase(storageRootDirectory)
} else { } else {
LegacyLoadThumbnailUseCase(context) LegacyLoadThumbnailUseCase(context)
} }
} }
val selectWallpaper: SelectWallpaperUseCase by lazy { DefaultSelectWallpaperUseCase(context.settings(), store) } val selectWallpaper: SelectWallpaperUseCase by lazy {
if (FeatureFlags.wallpaperV2Enabled) {
DefaultSelectWallpaperUseCase(context.settings(), store, fileManager, downloader)
} else {
LegacySelectWallpaperUseCase(context.settings(), store)
}
}
/** /**
* Contract for usecases that initialize the wallpaper feature. * Contract for usecases that initialize the wallpaper feature.
@ -190,21 +181,24 @@ class WallpapersUseCases(
collection = firefoxClassicCollection, collection = firefoxClassicCollection,
textColor = null, textColor = null,
cardColor = null, cardColor = null,
thumbnailFileState = Wallpaper.ImageFileState.NotAvailable, thumbnailFileState = Wallpaper.ImageFileState.Unavailable,
assetsFileState = Wallpaper.ImageFileState.Downloaded,
), ),
Wallpaper( Wallpaper(
name = Wallpaper.ceruleanName, name = Wallpaper.ceruleanName,
collection = firefoxClassicCollection, collection = firefoxClassicCollection,
textColor = null, textColor = null,
cardColor = null, cardColor = null,
thumbnailFileState = Wallpaper.ImageFileState.NotAvailable, thumbnailFileState = Wallpaper.ImageFileState.Unavailable,
assetsFileState = Wallpaper.ImageFileState.Downloaded,
), ),
Wallpaper( Wallpaper(
name = Wallpaper.sunriseName, name = Wallpaper.sunriseName,
collection = firefoxClassicCollection, collection = firefoxClassicCollection,
textColor = null, textColor = null,
cardColor = null, cardColor = null,
thumbnailFileState = Wallpaper.ImageFileState.NotAvailable, thumbnailFileState = Wallpaper.ImageFileState.Unavailable,
assetsFileState = Wallpaper.ImageFileState.Downloaded,
), ),
) )
private val remoteWallpapers: List<Wallpaper> = listOf( private val remoteWallpapers: List<Wallpaper> = listOf(
@ -213,14 +207,16 @@ class WallpapersUseCases(
collection = firefoxClassicCollection, collection = firefoxClassicCollection,
textColor = null, textColor = null,
cardColor = null, cardColor = null,
thumbnailFileState = Wallpaper.ImageFileState.NotAvailable, thumbnailFileState = Wallpaper.ImageFileState.Unavailable,
assetsFileState = Wallpaper.ImageFileState.Downloaded,
), ),
Wallpaper( Wallpaper(
name = Wallpaper.beachVibeName, name = Wallpaper.beachVibeName,
collection = firefoxClassicCollection, collection = firefoxClassicCollection,
textColor = null, textColor = null,
cardColor = null, cardColor = null,
thumbnailFileState = Wallpaper.ImageFileState.NotAvailable, thumbnailFileState = Wallpaper.ImageFileState.Unavailable,
assetsFileState = Wallpaper.ImageFileState.Downloaded,
), ),
) )
val allWallpapers = listOf(Wallpaper.Default) + localWallpapers + remoteWallpapers val allWallpapers = listOf(Wallpaper.Default) + localWallpapers + remoteWallpapers
@ -257,8 +253,6 @@ class WallpapersUseCases(
possibleWallpapers possibleWallpapers
) )
possibleWallpapers.forEach { downloader.downloadWallpaper(it) }
val wallpapersWithUpdatedThumbnailState = possibleWallpapers.map { wallpaper -> val wallpapersWithUpdatedThumbnailState = possibleWallpapers.map { wallpaper ->
val result = downloader.downloadThumbnail(wallpaper) val result = downloader.downloadThumbnail(wallpaper)
wallpaper.copy(thumbnailFileState = result) wallpaper.copy(thumbnailFileState = result)
@ -401,13 +395,13 @@ class WallpapersUseCases(
} }
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal class LegacyLoadThumbnailUseCase(private val context: Context): LoadThumbnailUseCase { internal class LegacyLoadThumbnailUseCase(private val context: Context) : LoadThumbnailUseCase {
override suspend fun invoke(wallpaper: Wallpaper): Bitmap? = override suspend fun invoke(wallpaper: Wallpaper): Bitmap? =
LegacyLoadBitmapUseCase(context).invoke(wallpaper) LegacyLoadBitmapUseCase(context).invoke(wallpaper)
} }
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal class DefaultLoadThumbnailUseCase(private val filesDir: File): LoadThumbnailUseCase { internal class DefaultLoadThumbnailUseCase(private val filesDir: File) : LoadThumbnailUseCase {
override suspend fun invoke(wallpaper: Wallpaper): Bitmap? = withContext(Dispatchers.IO) { override suspend fun invoke(wallpaper: Wallpaper): Bitmap? = withContext(Dispatchers.IO) {
Result.runCatching { Result.runCatching {
val path = Wallpaper.getLocalPath(wallpaper.name, Wallpaper.ImageType.Thumbnail) val path = Wallpaper.getLocalPath(wallpaper.name, Wallpaper.ImageType.Thumbnail)
@ -428,11 +422,11 @@ class WallpapersUseCases(
* *
* @param wallpaper The selected wallpaper. * @param wallpaper The selected wallpaper.
*/ */
operator fun invoke(wallpaper: Wallpaper) suspend operator fun invoke(wallpaper: Wallpaper)
} }
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal class DefaultSelectWallpaperUseCase( internal class LegacySelectWallpaperUseCase(
private val settings: Settings, private val settings: Settings,
private val store: AppStore, private val store: AppStore,
) : SelectWallpaperUseCase { ) : SelectWallpaperUseCase {
@ -441,7 +435,7 @@ class WallpapersUseCases(
* *
* @param wallpaper The selected wallpaper. * @param wallpaper The selected wallpaper.
*/ */
override fun invoke(wallpaper: Wallpaper) { override suspend fun invoke(wallpaper: Wallpaper) {
settings.currentWallpaperName = wallpaper.name settings.currentWallpaperName = wallpaper.name
settings.currentWallpaperTextColor = wallpaper.textColor ?: 0 settings.currentWallpaperTextColor = wallpaper.textColor ?: 0
settings.currentWallpaperCardColor = wallpaper.cardColor ?: 0 settings.currentWallpaperCardColor = wallpaper.cardColor ?: 0
@ -454,4 +448,46 @@ class WallpapersUseCases(
) )
} }
} }
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal class DefaultSelectWallpaperUseCase(
private val settings: Settings,
private val store: AppStore,
private val fileManager: WallpaperFileManager,
private val downloader: WallpaperDownloader,
) : SelectWallpaperUseCase {
/**
* Select a new wallpaper. Storage and the store will be updated appropriately.
*
* @param wallpaper The selected wallpaper.
*/
override suspend fun invoke(wallpaper: Wallpaper) {
if (wallpaper == Wallpaper.Default || fileManager.wallpaperImagesExist(wallpaper)) {
selectWallpaper(wallpaper)
dispatchDownloadState(wallpaper, Wallpaper.ImageFileState.Downloaded)
} else {
dispatchDownloadState(wallpaper, Wallpaper.ImageFileState.Downloading)
val result = downloader.downloadWallpaper(wallpaper)
dispatchDownloadState(wallpaper, result)
if (result == Wallpaper.ImageFileState.Downloaded) {
selectWallpaper(wallpaper)
}
}
}
private fun selectWallpaper(wallpaper: Wallpaper) {
settings.currentWallpaperName = wallpaper.name
store.dispatch(AppAction.WallpaperAction.UpdateCurrentWallpaper(wallpaper))
Wallpapers.wallpaperSelected.record(
Wallpapers.WallpaperSelectedExtra(
name = wallpaper.name,
themeCollection = wallpaper.collection.name
)
)
}
private fun dispatchDownloadState(wallpaper: Wallpaper, downloadState: Wallpaper.ImageFileState) {
store.dispatch(AppAction.WallpaperAction.UpdateWallpaperDownloadState(wallpaper, downloadState))
}
}
} }

View File

@ -98,7 +98,8 @@ class LegacyWallpaperFileManagerTest {
name = name, name = name,
textColor = null, textColor = null,
cardColor = null, cardColor = null,
thumbnailFileState = Wallpaper.ImageFileState.NotAvailable, thumbnailFileState = Wallpaper.ImageFileState.Unavailable,
assetsFileState = Wallpaper.ImageFileState.Downloaded,
collection = Wallpaper.Collection( collection = Wallpaper.Collection(
name = Wallpaper.defaultName, name = Wallpaper.defaultName,
heading = null, heading = null,

View File

@ -74,10 +74,7 @@ class WallpaperDownloaderTest {
@Test @Test
fun `GIVEN that thumbnail request is successful WHEN downloading THEN file is created in expected location`() = runTest { fun `GIVEN that thumbnail request is successful WHEN downloading THEN file is created in expected location`() = runTest {
val wallpaper = generateWallpaper() val wallpaper = generateWallpaper()
val thumbnailRequest = Request( val thumbnailRequest = wallpaper.generateRequest("thumbnail")
url = "$remoteHost/${wallpaper.collection.name}/${wallpaper.name}/thumbnail.png",
method = Request.Method.GET
)
val mockThumbnailResponse = mockk<Response>() val mockThumbnailResponse = mockk<Response>()
every { mockThumbnailResponse.status } returns 200 every { mockThumbnailResponse.status } returns 200
every { mockThumbnailResponse.body } returns Response.Body(wallpaperBytes.byteInputStream()) every { mockThumbnailResponse.body } returns Response.Body(wallpaperBytes.byteInputStream())
@ -132,7 +129,8 @@ class WallpaperDownloaderTest {
collection = wallpaperCollection, collection = wallpaperCollection,
textColor = null, textColor = null,
cardColor = null, cardColor = null,
thumbnailFileState = Wallpaper.ImageFileState.NotAvailable thumbnailFileState = Wallpaper.ImageFileState.Unavailable,
assetsFileState = Wallpaper.ImageFileState.Unavailable,
) )
private fun Wallpaper.generateRequest(type: String) = Request( private fun Wallpaper.generateRequest(type: String) = Request(

View File

@ -3,6 +3,7 @@ package org.mozilla.fenix.wallpapers
import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
@ -109,6 +110,30 @@ class WallpaperFileManagerTest {
assertTrue(getAllFiles(unavailableName).none { it.exists() }) assertTrue(getAllFiles(unavailableName).none { it.exists() })
} }
@Test
fun `WHEN both wallpaper assets exist THEN the file lookup will succeed`() = runTest {
val wallpaper = generateWallpaper("name")
createAllFiles(wallpaper.name)
val result = fileManager.wallpaperImagesExist(wallpaper)
assertTrue(result)
}
@Test
fun `WHEN at least one wallpaper asset does not exist THEN the file lookup will fail`() = runTest {
val wallpaper = generateWallpaper("name")
val allFiles = getAllFiles(wallpaper.name)
(0 until (allFiles.size - 1)).forEach {
allFiles[it].mkdirs()
allFiles[it].createNewFile()
}
val result = fileManager.wallpaperImagesExist(wallpaper)
assertFalse(result)
}
private fun createAllFiles(name: String) { private fun createAllFiles(name: String) {
for (file in getAllFiles(name)) { for (file in getAllFiles(name)) {
file.mkdirs() file.mkdirs()
@ -131,6 +156,7 @@ class WallpaperFileManagerTest {
textColor = null, textColor = null,
cardColor = null, cardColor = null,
thumbnailFileState = Wallpaper.ImageFileState.Downloaded, thumbnailFileState = Wallpaper.ImageFileState.Downloaded,
collection = Wallpaper.DefaultCollection assetsFileState = Wallpaper.ImageFileState.Downloaded,
collection = Wallpaper.DefaultCollection,
) )
} }

View File

@ -22,6 +22,7 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mozilla.fenix.GleanMetrics.Wallpapers import org.mozilla.fenix.GleanMetrics.Wallpapers
import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
import java.util.Calendar import java.util.Calendar
import java.util.Date import java.util.Date
@ -319,11 +320,11 @@ class WallpapersUseCasesTest {
} }
val expiredWallpaper = makeFakeRemoteWallpaper(TimeRelation.BEFORE, "expired") val expiredWallpaper = makeFakeRemoteWallpaper(TimeRelation.BEFORE, "expired")
val allWallpapers = listOf(expiredWallpaper) + fakeRemoteWallpapers val allWallpapers = listOf(expiredWallpaper) + fakeRemoteWallpapers
every { mockSettings.currentWallpaperName } returns "expired"
coEvery { mockFileManager.lookupExpiredWallpaper(any()) } returns expiredWallpaper coEvery { mockFileManager.lookupExpiredWallpaper(any()) } returns expiredWallpaper
coEvery { mockMetadataFetcher.downloadWallpaperList() } returns allWallpapers coEvery { mockMetadataFetcher.downloadWallpaperList() } returns allWallpapers
coEvery { mockDownloader.downloadThumbnail(any()) } returns Wallpaper.ImageFileState.Downloaded coEvery { mockDownloader.downloadThumbnail(any()) } returns Wallpaper.ImageFileState.Downloaded
WallpapersUseCases.DefaultInitializeWallpaperUseCase( WallpapersUseCases.DefaultInitializeWallpaperUseCase(
appStore, appStore,
mockDownloader, mockDownloader,
@ -366,7 +367,7 @@ class WallpapersUseCasesTest {
} }
@Test @Test
fun `GIVEN available wallpapers WHEN invoking initialize use case THEN available wallpapers downloaded`() = runTest { fun `GIVEN available wallpapers WHEN invoking initialize use case THEN available wallpaper thumbnails downloaded`() = runTest {
val fakeRemoteWallpapers = listOf("first", "second", "third").map { name -> val fakeRemoteWallpapers = listOf("first", "second", "third").map { name ->
makeFakeRemoteWallpaper(TimeRelation.LATER, name) makeFakeRemoteWallpaper(TimeRelation.LATER, name)
} }
@ -385,12 +386,12 @@ class WallpapersUseCasesTest {
).invoke() ).invoke()
for (fakeRemoteWallpaper in fakeRemoteWallpapers) { for (fakeRemoteWallpaper in fakeRemoteWallpapers) {
coVerify { mockDownloader.downloadWallpaper(fakeRemoteWallpaper) } coVerify { mockDownloader.downloadThumbnail(fakeRemoteWallpaper) }
} }
} }
@Test @Test
fun `GIVEN available wallpapers WHEN invoking initialize use case THEN thumbnails downloaded and store state reflects that`() = runTest { fun `GIVEN available wallpapers WHEN invoking initialize use case THEN thumbnails downloaded and the store state is updated to reflect that`() = runTest {
val fakeRemoteWallpapers = listOf("first", "second", "third").map { name -> val fakeRemoteWallpapers = listOf("first", "second", "third").map { name ->
makeFakeRemoteWallpaper(TimeRelation.LATER, name) makeFakeRemoteWallpaper(TimeRelation.LATER, name)
} }
@ -412,13 +413,15 @@ class WallpapersUseCasesTest {
coVerify { mockDownloader.downloadThumbnail(fakeRemoteWallpaper) } coVerify { mockDownloader.downloadThumbnail(fakeRemoteWallpaper) }
} }
appStore.waitUntilIdle() appStore.waitUntilIdle()
assertTrue(appStore.state.wallpaperState.availableWallpapers.all { assertTrue(
it.thumbnailFileState == Wallpaper.ImageFileState.Downloaded appStore.state.wallpaperState.availableWallpapers.all {
}) it.thumbnailFileState == Wallpaper.ImageFileState.Downloaded
}
)
} }
@Test @Test
fun `GIVEN thumbnail download fails WHEN invoking initialize use case THEN store state reflects that`() = runTest { fun `GIVEN thumbnail download fails WHEN invoking initialize use case THEN the store state is updated to reflect that`() = runTest {
val fakeRemoteWallpapers = listOf("first", "second", "third").map { name -> val fakeRemoteWallpapers = listOf("first", "second", "third").map { name ->
makeFakeRemoteWallpaper(TimeRelation.LATER, name) makeFakeRemoteWallpaper(TimeRelation.LATER, name)
} }
@ -496,14 +499,14 @@ class WallpapersUseCasesTest {
} }
@Test @Test
fun `WHEN selected wallpaper usecase invoked THEN storage updated and store receives dispatch`() { fun `WHEN legacy selected wallpaper usecase invoked THEN storage updated and store receives dispatch`() = runTest {
val selectedWallpaper = makeFakeRemoteWallpaper(TimeRelation.LATER, "selected") val selectedWallpaper = makeFakeRemoteWallpaper(TimeRelation.LATER, "selected")
val slot = slot<String>() val slot = slot<String>()
coEvery { mockFileManager.lookupExpiredWallpaper(any()) } returns null coEvery { mockFileManager.lookupExpiredWallpaper(any()) } returns null
every { mockSettings.currentWallpaperName } returns "" every { mockSettings.currentWallpaperName } returns ""
every { mockSettings.currentWallpaperName = capture(slot) } just runs every { mockSettings.currentWallpaperName = capture(slot) } just runs
WallpapersUseCases.DefaultSelectWallpaperUseCase( WallpapersUseCases.LegacySelectWallpaperUseCase(
mockSettings, mockSettings,
appStore appStore
).invoke(selectedWallpaper) ).invoke(selectedWallpaper)
@ -514,6 +517,73 @@ class WallpapersUseCasesTest {
assertEquals(selectedWallpaper.name, Wallpapers.wallpaperSelected.testGetValue()?.first()?.extra?.get("name")!!) assertEquals(selectedWallpaper.name, Wallpapers.wallpaperSelected.testGetValue()?.first()?.extra?.get("name")!!)
} }
@Test
fun `GIVEN wallpaper downloaded WHEN selecting a wallpaper THEN storage updated and store receives dispatch`() = runTest {
val selectedWallpaper = makeFakeRemoteWallpaper(TimeRelation.LATER, "selected")
val slot = slot<String>()
coEvery { mockFileManager.lookupExpiredWallpaper(any()) } returns null
every { mockSettings.currentWallpaperName } returns ""
every { mockSettings.currentWallpaperName = capture(slot) } just runs
coEvery { mockFileManager.wallpaperImagesExist(selectedWallpaper) } returns true
WallpapersUseCases.DefaultSelectWallpaperUseCase(
mockSettings,
appStore,
mockFileManager,
mockDownloader,
).invoke(selectedWallpaper)
appStore.waitUntilIdle()
assertEquals(selectedWallpaper.name, slot.captured)
assertEquals(selectedWallpaper, appStore.state.wallpaperState.currentWallpaper)
assertEquals(selectedWallpaper.name, Wallpapers.wallpaperSelected.testGetValue()?.first()?.extra?.get("name")!!)
}
@Test
fun `GIVEN wallpaper is not downloaded WHEN selecting a wallpaper and download succeeds THEN storage updated and store receives dispatch`() = runTest {
val selectedWallpaper = makeFakeRemoteWallpaper(TimeRelation.LATER, "selected")
val slot = slot<String>()
val mockStore = mockk<AppStore>(relaxed = true)
coEvery { mockFileManager.lookupExpiredWallpaper(any()) } returns null
every { mockSettings.currentWallpaperName } returns ""
every { mockSettings.currentWallpaperName = capture(slot) } just runs
coEvery { mockFileManager.wallpaperImagesExist(selectedWallpaper) } returns false
coEvery { mockDownloader.downloadWallpaper(selectedWallpaper) } returns Wallpaper.ImageFileState.Downloaded
WallpapersUseCases.DefaultSelectWallpaperUseCase(
mockSettings,
mockStore,
mockFileManager,
mockDownloader,
).invoke(selectedWallpaper)
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)) }
}
@Test
fun `GIVEN wallpaper is not downloaded WHEN selecting a wallpaper and any download fails THEN wallpaper not set and store receives dispatch`() = runTest {
val selectedWallpaper = makeFakeRemoteWallpaper(TimeRelation.LATER, "selected")
val slot = slot<String>()
val mockStore = mockk<AppStore>(relaxed = true)
coEvery { mockFileManager.lookupExpiredWallpaper(any()) } returns null
every { mockSettings.currentWallpaperName } returns ""
every { mockSettings.currentWallpaperName = capture(slot) } just runs
coEvery { mockFileManager.wallpaperImagesExist(selectedWallpaper) } returns false
coEvery { mockDownloader.downloadWallpaper(selectedWallpaper) } returns Wallpaper.ImageFileState.Error
WallpapersUseCases.DefaultSelectWallpaperUseCase(
mockSettings,
mockStore,
mockFileManager,
mockDownloader,
).invoke(selectedWallpaper)
verify { mockStore.dispatch(AppAction.WallpaperAction.UpdateWallpaperDownloadState(selectedWallpaper, Wallpaper.ImageFileState.Downloading)) }
verify { mockStore.dispatch(AppAction.WallpaperAction.UpdateWallpaperDownloadState(selectedWallpaper, Wallpaper.ImageFileState.Error)) }
}
private enum class TimeRelation { private enum class TimeRelation {
BEFORE, BEFORE,
NOW, NOW,
@ -549,7 +619,8 @@ class WallpapersUseCasesTest {
), ),
textColor = null, textColor = null,
cardColor = null, cardColor = null,
thumbnailFileState = Wallpaper.ImageFileState.NotAvailable, thumbnailFileState = Wallpaper.ImageFileState.Unavailable,
assetsFileState = Wallpaper.ImageFileState.Unavailable,
) )
} else { } else {
Wallpaper( Wallpaper(
@ -565,7 +636,8 @@ class WallpapersUseCasesTest {
), ),
textColor = null, textColor = null,
cardColor = null, cardColor = null,
thumbnailFileState = Wallpaper.ImageFileState.NotAvailable, thumbnailFileState = Wallpaper.ImageFileState.Unavailable,
assetsFileState = Wallpaper.ImageFileState.Unavailable,
) )
} }
} }