Fixes #26245: refactor the WallpaperManager as several WallpaperUseCases

This commit is contained in:
MatthewTighe 2022-08-02 16:37:23 -07:00 committed by mergify[bot]
parent d0c21c06aa
commit 72959901d8
16 changed files with 605 additions and 449 deletions

View File

@ -35,7 +35,7 @@ import org.mozilla.fenix.helpers.HomeActivityTestRule
*
* Say no to main thread IO! 🙅
*/
private const val EXPECTED_SUPPRESSION_COUNT = 19
private const val EXPECTED_SUPPRESSION_COUNT = 18
/**
* The number of times we call the `runBlocking` coroutine method on the main thread during this

View File

@ -850,7 +850,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
@OptIn(DelicateCoroutinesApi::class)
open fun downloadWallpapers() {
GlobalScope.launch {
components.wallpaperManager.downloadAllRemoteWallpapers()
components.useCases.wallpaperUseCases.initialize()
}
}
}

View File

@ -19,7 +19,6 @@ import mozilla.components.feature.addons.update.DefaultAddonUpdater
import mozilla.components.feature.autofill.AutofillConfiguration
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.base.worker.Frequency
import mozilla.components.support.locale.LocaleManager
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity
@ -45,8 +44,6 @@ import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.ClipboardHandler
import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.wallpapers.WallpaperDownloader
import org.mozilla.fenix.wallpapers.WallpaperFileManager
import org.mozilla.fenix.wallpapers.WallpaperManager
import org.mozilla.fenix.wifi.WifiConnectionMonitor
import java.util.concurrent.TimeUnit
@ -85,7 +82,10 @@ class Components(private val context: Context) {
core.webAppShortcutManager,
core.topSitesStorage,
core.bookmarksStorage,
core.historyStorage
core.historyStorage,
appStore,
core.client,
strictMode,
)
}
@ -161,17 +161,11 @@ class Components(private val context: Context) {
val strictMode by lazyMonitored { StrictModeManager(Config, this) }
val wallpaperManager by lazyMonitored {
val currentLocale = strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
LocaleManager.getCurrentLocale(context)?.toLanguageTag()
?: LocaleManager.getSystemDefault().toLanguageTag()
}
strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
WallpaperManager(
settings,
appStore,
WallpaperDownloader(context, core.client),
WallpaperFileManager(context.filesDir),
currentLocale
useCases.wallpaperUseCases.selectWallpaper,
)
}
}

View File

@ -7,6 +7,7 @@ package org.mozilla.fenix.components
import android.content.Context
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.fetch.Client
import mozilla.components.concept.storage.BookmarksStorage
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.feature.app.links.AppLinksUseCases
@ -24,7 +25,9 @@ import mozilla.components.feature.top.sites.TopSitesStorage
import mozilla.components.feature.top.sites.TopSitesUseCases
import mozilla.components.support.locale.LocaleUseCases
import org.mozilla.fenix.components.bookmarks.BookmarksUseCase
import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.wallpapers.WallpapersUseCases
/**
* Component group for all use cases. Use cases are provided by feature
@ -38,7 +41,10 @@ class UseCases(
private val shortcutManager: WebAppShortcutManager,
private val topSitesStorage: TopSitesStorage,
private val bookmarksStorage: BookmarksStorage,
private val historyStorage: HistoryStorage
private val historyStorage: HistoryStorage,
appStore: AppStore,
client: Client,
strictMode: StrictModeManager,
) {
/**
* Use cases that provide engine interactions for a given browser session.
@ -99,4 +105,8 @@ class UseCases(
* Use cases that provide bookmark management.
*/
val bookmarksUseCases by lazyMonitored { BookmarksUseCase(bookmarksStorage, historyStorage) }
val wallpaperUseCases by lazyMonitored {
WallpapersUseCases(context, appStore, client, strictMode)
}
}

View File

@ -0,0 +1,48 @@
package org.mozilla.fenix.ext
import android.graphics.Bitmap
import android.graphics.Matrix
import android.view.View
import android.widget.ImageView
/**
* This will scale the received [Bitmap] to the size of the [view]. It retains the bitmap's
* original aspect ratio, but will shrink or enlarge it to fit the viewport. If bitmap does not
* correctly fit the aspect ratio of the view, it will be shifted to prioritize the bottom-left
* of the bitmap.
*/
fun Bitmap.scaleToBottomOfView(view: ImageView) {
val bitmap = this
view.setImageBitmap(bitmap)
view.scaleType = ImageView.ScaleType.MATRIX
val matrix = Matrix()
view.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
override fun onLayoutChange(
v: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
val viewWidth: Float = view.width.toFloat()
val viewHeight: Float = view.height.toFloat()
val bitmapWidth = bitmap.width
val bitmapHeight = bitmap.height
val widthScale = viewWidth / bitmapWidth
val heightScale = viewHeight / bitmapHeight
val scale = widthScale.coerceAtLeast(heightScale)
matrix.postScale(scale, scale)
// The image is translated to its bottom such that any pertinent information is
// guaranteed to be shown.
// Majority of this math borrowed from // https://medium.com/@tokudu/how-to-whitelist-strictmode-violations-on-android-based-on-stacktrace-eb0018e909aa
// except that there is no need to translate horizontally in our case.
matrix.postTranslate(0f, (viewHeight - bitmapHeight * scale))
view.imageMatrix = matrix
view.removeOnLayoutChangeListener(this)
}
})
}

View File

@ -94,6 +94,7 @@ import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.scaleToBottomOfView
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.gleanplumb.DefaultMessageController
import org.mozilla.fenix.gleanplumb.MessagingFeature
@ -968,11 +969,11 @@ class HomeFragment : Fragment() {
binding.wallpaperImageView.visibility = View.GONE
}
else -> {
with(requireComponents.wallpaperManager) {
val bitmap = currentWallpaper.load(requireContext()) ?: return@onEach
bitmap.scaleBitmapToBottomOfView(binding.wallpaperImageView)
val bitmap = requireComponents.useCases.wallpaperUseCases.loadBitmap(currentWallpaper)
bitmap?.let {
it.scaleToBottomOfView(binding.wallpaperImageView)
binding.wallpaperImageView.visibility = View.VISIBLE
}
binding.wallpaperImageView.visibility = View.VISIBLE
}
}
}

View File

@ -33,7 +33,7 @@ import androidx.compose.material.SwitchDefaults
import androidx.compose.material.Text
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -43,7 +43,6 @@ 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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@ -53,11 +52,9 @@ import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.button.TextButton
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
import org.mozilla.fenix.wallpapers.Wallpaper
import org.mozilla.fenix.wallpapers.WallpaperManager
/**
* The screen for controlling settings around Wallpapers. When a new wallpaper is selected,
@ -77,7 +74,7 @@ import org.mozilla.fenix.wallpapers.WallpaperManager
fun WallpaperSettings(
wallpapers: List<Wallpaper>,
defaultWallpaper: Wallpaper,
loadWallpaperResource: (Wallpaper) -> Bitmap?,
loadWallpaperResource: suspend (Wallpaper) -> Bitmap?,
selectedWallpaper: Wallpaper,
onSelectWallpaper: (Wallpaper) -> Unit,
onViewWallpaper: () -> Unit,
@ -164,7 +161,7 @@ private fun WallpaperSnackbar(
private fun WallpaperThumbnails(
wallpapers: List<Wallpaper>,
defaultWallpaper: Wallpaper,
loadWallpaperResource: (Wallpaper) -> Bitmap?,
loadWallpaperResource: suspend (Wallpaper) -> Bitmap?,
selectedWallpaper: Wallpaper,
numColumns: Int = 3,
onSelectWallpaper: (Wallpaper) -> Unit,
@ -211,14 +208,14 @@ private fun WallpaperThumbnails(
private fun WallpaperThumbnailItem(
wallpaper: Wallpaper,
defaultWallpaper: Wallpaper,
loadWallpaperResource: (Wallpaper) -> Bitmap?,
loadWallpaperResource: suspend (Wallpaper) -> Bitmap?,
isSelected: Boolean,
aspectRatio: Float = 1.1f,
onSelect: (Wallpaper) -> Unit
) {
var bitmap by remember { mutableStateOf(loadWallpaperResource(wallpaper)) }
DisposableEffect(LocalConfiguration.current.orientation) {
onDispose { bitmap = loadWallpaperResource(wallpaper) }
var bitmap: Bitmap? by remember { mutableStateOf(null) }
LaunchedEffect(LocalConfiguration.current.orientation) {
bitmap = loadWallpaperResource(wallpaper)
}
val thumbnailShape = RoundedCornerShape(8.dp)
val border = if (isSelected) {
@ -292,16 +289,12 @@ private fun WallpaperLogoSwitch(
@Composable
private fun WallpaperThumbnailsPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
val context = LocalContext.current
val wallpaperManager = context.components.wallpaperManager
WallpaperSettings(
defaultWallpaper = WallpaperManager.defaultWallpaper,
loadWallpaperResource = { wallpaper ->
with(wallpaperManager) { wallpaper.load(context) }
},
wallpapers = wallpaperManager.wallpapers,
selectedWallpaper = wallpaperManager.currentWallpaper,
defaultWallpaper = Wallpaper.Default,
loadWallpaperResource = { null },
wallpapers = listOf(Wallpaper.Default),
selectedWallpaper = Wallpaper.Default,
onSelectWallpaper = {},
onViewWallpaper = {},
tapLogoSwitchChecked = false,

View File

@ -16,6 +16,7 @@ import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
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
@ -23,11 +24,14 @@ import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.wallpapers.Wallpaper
import org.mozilla.fenix.wallpapers.WallpaperManager
class WallpaperSettingsFragment : Fragment() {
private val wallpaperManager by lazy {
requireComponents.wallpaperManager
private val appStore by lazy {
requireComponents.appStore
}
private val wallpaperUseCases by lazy {
requireComponents.useCases.wallpaperUseCases
}
private val settings by lazy {
@ -44,25 +48,20 @@ class WallpaperSettingsFragment : Fragment() {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
FirefoxTheme {
var currentWallpaper by remember { mutableStateOf(wallpaperManager.currentWallpaper) }
val wallpapers = appStore.observeAsComposableState { state ->
state.wallpaperState.availableWallpapers
}.value ?: listOf()
val currentWallpaper = appStore.observeAsComposableState { state ->
state.wallpaperState.currentWallpaper
}.value ?: Wallpaper.Default
var wallpapersSwitchedByLogo by remember { mutableStateOf(settings.wallpapersSwitchedByLogoTap) }
WallpaperSettings(
wallpapers = wallpaperManager.wallpapers,
defaultWallpaper = WallpaperManager.defaultWallpaper,
loadWallpaperResource = { wallpaper ->
with(wallpaperManager) { wallpaper.load(context) }
},
wallpapers = wallpapers,
defaultWallpaper = Wallpaper.Default,
loadWallpaperResource = { wallpaperUseCases.loadBitmap(it) },
selectedWallpaper = currentWallpaper,
onSelectWallpaper = { selectedWallpaper: Wallpaper ->
currentWallpaper = selectedWallpaper
wallpaperManager.currentWallpaper = selectedWallpaper
Wallpapers.wallpaperSelected.record(
Wallpapers.WallpaperSelectedExtra(
name = selectedWallpaper.name,
themeCollection = selectedWallpaper::class.simpleName
)
)
},
onSelectWallpaper = { wallpaperUseCases.selectWallpaper(it) },
onViewWallpaper = { findNavController().navigate(R.id.homeFragment) },
tapLogoSwitchChecked = wallpapersSwitchedByLogo,
onTapLogoSwitchCheckedChange = {

View File

@ -51,7 +51,7 @@ import org.mozilla.fenix.settings.logins.SortingStrategy
import org.mozilla.fenix.settings.registerOnSharedPreferenceChangeListener
import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_BLOCK_ALL
import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_BLOCK_AUDIBLE
import org.mozilla.fenix.wallpapers.WallpaperManager
import org.mozilla.fenix.wallpapers.Wallpaper
import java.security.InvalidParameterException
import java.util.UUID
@ -187,7 +187,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
var currentWallpaper by stringPreference(
appContext.getPreferenceKey(R.string.pref_key_current_wallpaper),
default = WallpaperManager.defaultWallpaper.name
default = Wallpaper.Default.name
)
var wallpapersSwitchedByLogoTap by booleanPreference(

View File

@ -8,6 +8,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class WallpaperFileManager(
@ -23,8 +24,8 @@ class WallpaperFileManager(
* files for each of the following orientation and theme combinations:
* light/portrait - light/landscape - dark/portrait - dark/landscape
*/
fun lookupExpiredWallpaper(name: String): Wallpaper.Expired? {
return if (getAllLocalWallpaperPaths(name).all { File(rootDirectory, it).exists() }) {
suspend fun lookupExpiredWallpaper(name: String): Wallpaper.Expired? = withContext(Dispatchers.IO) {
if (getAllLocalWallpaperPaths(name).all { File(rootDirectory, it).exists() }) {
Wallpaper.Expired(name)
} else null
}

View File

@ -6,25 +6,12 @@ package org.mozilla.fenix.wallpapers
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.ImageView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.R
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.utils.Settings
import java.io.File
import java.util.Date
/**
* Provides access to available wallpapers and manages their states.
@ -33,39 +20,12 @@ import java.util.Date
class WallpaperManager(
private val settings: Settings,
private val appStore: AppStore,
private val downloader: WallpaperDownloader,
private val fileManager: WallpaperFileManager,
private val currentLocale: String,
allWallpapers: List<Wallpaper> = availableWallpapers
private val selectWallpaperUseCase: WallpapersUseCases.SelectWallpaperUseCase,
) {
val logger = Logger("WallpaperManager")
val wallpapers = allWallpapers
.filter(::filterExpiredRemoteWallpapers)
.filter(::filterPromotionalWallpapers)
.also {
appStore.dispatch(AppAction.WallpaperAction.UpdateAvailableWallpapers(it))
}
var currentWallpaper: Wallpaper = getCurrentWallpaperFromSettings()
set(value) {
settings.currentWallpaper = value.name
appStore.dispatch(AppAction.WallpaperAction.UpdateCurrentWallpaper(value))
field = value
}
init {
fileManager.clean(currentWallpaper, wallpapers.filterIsInstance<Wallpaper.Remote>())
}
/**
* Download all known remote wallpapers.
*/
suspend fun downloadAllRemoteWallpapers() {
for (wallpaper in wallpapers.filterIsInstance<Wallpaper.Remote>()) {
downloader.downloadWallpaper(wallpaper)
}
}
val wallpapers get() = appStore.state.wallpaperState.availableWallpapers
val currentWallpaper: Wallpaper get() = appStore.state.wallpaperState.currentWallpaper
/**
* Returns the next available [Wallpaper], the [currentWallpaper] is the last one then
@ -80,126 +40,10 @@ class WallpaperManager(
} else {
values[index]
}.also {
currentWallpaper = it
selectWallpaperUseCase(it)
}
}
private fun filterExpiredRemoteWallpapers(wallpaper: Wallpaper): Boolean = when (wallpaper) {
is Wallpaper.Remote -> {
val notExpired = wallpaper.expirationDate?.let { Date().before(it) } ?: true
notExpired || wallpaper.name == settings.currentWallpaper
}
else -> true
}
private fun filterPromotionalWallpapers(wallpaper: Wallpaper): Boolean =
if (wallpaper is Wallpaper.Promotional) {
wallpaper.isAvailableInLocale(currentLocale)
} else {
true
}
private fun getCurrentWallpaperFromSettings(): Wallpaper {
return if (isDefaultTheCurrentWallpaper(settings)) {
defaultWallpaper
} else {
val currentWallpaper = settings.currentWallpaper
wallpapers.find { it.name == currentWallpaper }
?: fileManager.lookupExpiredWallpaper(currentWallpaper)
?: defaultWallpaper
}.also {
appStore.dispatch(AppAction.WallpaperAction.UpdateCurrentWallpaper(it))
}
}
/**
* Load a wallpaper that is saved locally.
*/
fun Wallpaper.load(context: Context): Bitmap? =
when (this) {
is Wallpaper.Local -> loadWallpaperFromDrawables(context, this)
is Wallpaper.Remote -> loadWallpaperFromDisk(context, this)
else -> null
}
private fun loadWallpaperFromDrawables(context: Context, wallpaper: Wallpaper.Local): Bitmap? = Result.runCatching {
BitmapFactory.decodeResource(context.resources, wallpaper.drawableId)
}.getOrNull()
/**
* Load a wallpaper from app-specific storage.
*/
private fun loadWallpaperFromDisk(context: Context, wallpaper: Wallpaper.Remote): Bitmap? = Result.runCatching {
val path = wallpaper.getLocalPathFromContext(context)
runBlockingIncrement {
withContext(Dispatchers.IO) {
val file = File(context.filesDir, path)
BitmapFactory.decodeStream(file.inputStream())
}
}
}.getOrNull()
/**
* This will scale the received [Bitmap] to the size of the [view]. It retains the bitmap's
* original aspect ratio, but will shrink or enlarge it to fit the viewport. If bitmap does not
* correctly fit the aspect ratio of the view, it will be shifted to prioritize the bottom-left
* of the bitmap.
*/
fun Bitmap.scaleBitmapToBottomOfView(view: ImageView) {
val bitmap = this
view.setImageBitmap(bitmap)
view.scaleType = ImageView.ScaleType.MATRIX
val matrix = Matrix()
view.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
override fun onLayoutChange(
v: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
val viewWidth: Float = view.width.toFloat()
val viewHeight: Float = view.height.toFloat()
val bitmapWidth = bitmap.width
val bitmapHeight = bitmap.height
val widthScale = viewWidth / bitmapWidth
val heightScale = viewHeight / bitmapHeight
val scale = widthScale.coerceAtLeast(heightScale)
matrix.postScale(scale, scale)
// The image is translated to its bottom such that any pertinent information is
// guaranteed to be shown.
// Majority of this math borrowed from // https://medium.com/@tokudu/how-to-whitelist-strictmode-violations-on-android-based-on-stacktrace-eb0018e909aa
// except that there is no need to translate horizontally in our case.
matrix.postTranslate(0f, (viewHeight - bitmapHeight * scale))
view.imageMatrix = matrix
view.removeOnLayoutChangeListener(this)
}
})
}
/**
* Get the expected local path on disk for a wallpaper. This will differ depending
* on orientation and app theme.
*/
private fun Wallpaper.Remote.getLocalPathFromContext(context: Context): String {
val orientation = if (context.isLandscape()) "landscape" else "portrait"
val theme = if (context.isDark()) "dark" else "light"
return Wallpaper.getBaseLocalPath(orientation, theme, name)
}
private fun Context.isLandscape(): Boolean {
return resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
}
private fun Context.isDark(): Boolean {
return resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
}
/**
* Animates the Firefox logo, if it hasn't been animated before, otherwise nothing will happen.
* After animating the first time, the [Settings.shouldAnimateFirefoxLogo] setting
@ -242,20 +86,6 @@ class WallpaperManager(
}
val defaultWallpaper = Wallpaper.Default
private val localWallpapers: List<Wallpaper.Local> = listOf(
Wallpaper.Local.Firefox("amethyst", R.drawable.amethyst),
Wallpaper.Local.Firefox("cerulean", R.drawable.cerulean),
Wallpaper.Local.Firefox("sunrise", R.drawable.sunrise),
)
private val remoteWallpapers: List<Wallpaper.Remote> = listOf(
Wallpaper.Remote.Firefox(
"twilight-hills"
),
Wallpaper.Remote.Firefox(
"beach-vibe"
),
)
private val availableWallpapers = listOf(defaultWallpaper) + localWallpapers + remoteWallpapers
private const val ANIMATION_DELAY_MS = 1500L
}
}

View File

@ -0,0 +1,255 @@
/* 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.wallpapers
import android.content.Context
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.StrictMode
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import mozilla.components.concept.fetch.Client
import mozilla.components.support.locale.LocaleManager
import org.mozilla.fenix.GleanMetrics.Wallpapers
import org.mozilla.fenix.R
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.utils.Settings
import java.io.File
import java.util.Date
/**
* Contains use cases related to the wallpaper feature.
*
* @param context Used for various file and configuration checks.
* @param store Will receive dispatches of metadata updates like the currently selected wallpaper.
* @param client Handles downloading wallpapers and their metadata.
* @param strictMode Required for determining some device state like current locale and file paths.
*
* @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.
* @property loadBitmap Usecase for loading specific wallpaper bitmaps.
* @property selectWallpaper Usecase for selecting a new wallpaper.
*/
class WallpapersUseCases(
context: Context,
store: AppStore,
client: Client,
strictMode: StrictModeManager
) {
val initialize: InitializeWallpapersUseCase by lazy {
// Required to even access context.filesDir property and to retrieve current locale
val (fileManager, currentLocale) = strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
val fileManager = WallpaperFileManager(context.filesDir)
val currentLocale = LocaleManager.getCurrentLocale(context)?.toLanguageTag()
?: LocaleManager.getSystemDefault().toLanguageTag()
fileManager to currentLocale
}
val downloader = WallpaperDownloader(context, client)
DefaultInitializeWallpaperUseCase(
store = store,
downloader = downloader,
fileManager = fileManager,
settings = context.settings(),
currentLocale = currentLocale
)
}
val loadBitmap: LoadBitmapUseCase by lazy { DefaultLoadBitmapUseCase(context) }
val selectWallpaper: SelectWallpaperUseCase by lazy { DefaultSelectWallpaperUseCase(context.settings(), store) }
/**
* Contract for usecases that initialize the wallpaper feature.
*/
interface InitializeWallpapersUseCase {
/**
* Start operations that should be down during initialization, like remote metadata
* retrieval and determining the currently selected wallpaper.
*/
suspend operator fun invoke()
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal class DefaultInitializeWallpaperUseCase(
private val store: AppStore,
private val downloader: WallpaperDownloader,
private val fileManager: WallpaperFileManager,
private val settings: Settings,
private val currentLocale: String,
private val possibleWallpapers: List<Wallpaper> = allWallpapers,
) : InitializeWallpapersUseCase {
/**
* Downloads the currently available wallpaper metadata from a remote source.
* Updates the [store] with that metadata and with the selected wallpaper found in storage.
* Removes any unused promotional or time-limited assets from local storage.
* Should usually be called early the app's lifetime to ensure that metadata and thumbnails
* are available as soon as they are needed.
*/
override suspend operator fun invoke() {
// Quite a bit of code needs to be executed off the main thread in some of this setup.
// This should be cleaned up as improvements are made to the storage, file management,
// and download utilities.
withContext(Dispatchers.IO) {
val availableWallpapers = getAvailableWallpapers()
val currentWallpaperName = settings.currentWallpaper
val currentWallpaper = possibleWallpapers.find { it.name == currentWallpaperName }
?: fileManager.lookupExpiredWallpaper(currentWallpaperName)
?: Wallpaper.Default
fileManager.clean(
currentWallpaper,
possibleWallpapers.filterIsInstance<Wallpaper.Remote>()
)
downloadAllRemoteWallpapers(availableWallpapers)
store.dispatch(AppAction.WallpaperAction.UpdateAvailableWallpapers(availableWallpapers))
store.dispatch(AppAction.WallpaperAction.UpdateCurrentWallpaper(currentWallpaper))
}
}
private fun getAvailableWallpapers() = possibleWallpapers
.filter { !it.isExpired() && it.isAvailableInLocale() }
private suspend fun downloadAllRemoteWallpapers(availableWallpapers: List<Wallpaper>) {
for (wallpaper in availableWallpapers.filterIsInstance<Wallpaper.Remote>()) {
downloader.downloadWallpaper(wallpaper)
}
}
private fun Wallpaper.isExpired(): Boolean = when (this) {
is Wallpaper.Remote -> {
val expired = this.expirationDate?.let { Date().after(it) } ?: false
expired && this.name != settings.currentWallpaper
}
else -> false
}
private fun Wallpaper.isAvailableInLocale(): Boolean =
if (this is Wallpaper.Promotional) {
this.isAvailableInLocale(currentLocale)
} else {
true
}
companion object {
private val localWallpapers: List<Wallpaper.Local> = listOf(
Wallpaper.Local.Firefox("amethyst", R.drawable.amethyst),
Wallpaper.Local.Firefox("cerulean", R.drawable.cerulean),
Wallpaper.Local.Firefox("sunrise", R.drawable.sunrise),
)
private val remoteWallpapers: List<Wallpaper.Remote> = listOf(
Wallpaper.Remote.Firefox(
"twilight-hills"
),
Wallpaper.Remote.Firefox(
"beach-vibe"
),
)
val allWallpapers = listOf(Wallpaper.Default) + localWallpapers + remoteWallpapers
}
}
/**
* Contract for usecase for loading bitmaps related to a specific wallpaper.
*/
interface LoadBitmapUseCase {
/**
* Load the bitmap for a [wallpaper], if available.
*
* @param wallpaper The wallpaper to load a bitmap for.
*/
suspend operator fun invoke(wallpaper: Wallpaper): Bitmap?
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal class DefaultLoadBitmapUseCase(private val context: Context) : LoadBitmapUseCase {
/**
* Load the bitmap for a [wallpaper], if available.
*
* @param wallpaper The wallpaper to load a bitmap for.
*/
override suspend operator fun invoke(wallpaper: Wallpaper): Bitmap? = when (wallpaper) {
is Wallpaper.Local -> loadWallpaperFromDrawable(context, wallpaper)
is Wallpaper.Remote -> loadWallpaperFromDisk(context, wallpaper)
else -> null
}
private suspend fun loadWallpaperFromDrawable(
context: Context,
wallpaper: Wallpaper.Local
): Bitmap? = Result.runCatching {
withContext(Dispatchers.IO) {
BitmapFactory.decodeResource(context.resources, wallpaper.drawableId)
}
}.getOrNull()
private suspend fun loadWallpaperFromDisk(
context: Context,
wallpaper: Wallpaper.Remote
): Bitmap? = Result.runCatching {
val path = wallpaper.getLocalPathFromContext(context)
withContext(Dispatchers.IO) {
val file = File(context.filesDir, path)
BitmapFactory.decodeStream(file.inputStream())
}
}.getOrNull()
/**
* Get the expected local path on disk for a wallpaper. This will differ depending
* on orientation and app theme.
*/
private fun Wallpaper.Remote.getLocalPathFromContext(context: Context): String {
val orientation = if (context.isLandscape()) "landscape" else "portrait"
val theme = if (context.isDark()) "dark" else "light"
return Wallpaper.getBaseLocalPath(orientation, theme, name)
}
private fun Context.isLandscape(): Boolean {
return resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
}
private fun Context.isDark(): Boolean {
return resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
}
}
/**
* Contract for usecase of selecting a new wallpaper.
*/
interface SelectWallpaperUseCase {
/**
* Select a new wallpaper.
*
* @param wallpaper The selected wallpaper.
*/
operator fun invoke(wallpaper: Wallpaper)
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal class DefaultSelectWallpaperUseCase(
private val settings: Settings,
private val store: AppStore,
) : SelectWallpaperUseCase {
/**
* Select a new wallpaper. Storage and the store will be updated appropriately.
*
* @param wallpaper The selected wallpaper.
*/
override fun invoke(wallpaper: Wallpaper) {
settings.currentWallpaper = wallpaper.name
store.dispatch(AppAction.WallpaperAction.UpdateCurrentWallpaper(wallpaper))
Wallpapers.wallpaperSelected.record(
Wallpapers.WallpaperSelectedExtra(
name = wallpaper.name,
themeCollection = wallpaper::class.simpleName
)
)
}
}
}

View File

@ -5,6 +5,7 @@
package org.mozilla.fenix.perf
import androidx.lifecycle.Lifecycle
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
@ -22,7 +23,6 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.GleanMetrics.PerfStartup
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.perf.StartupPathProvider.StartupPath
import org.mozilla.fenix.perf.StartupStateProvider.StartupState
@ -35,7 +35,7 @@ private val validTelemetryLabels = run {
private val activityClass = HomeActivity::class.java
@RunWith(FenixRobolectricTestRunner::class)
@RunWith(AndroidJUnit4::class)
class StartupTypeTelemetryTest {
@get:Rule

View File

@ -5,6 +5,7 @@
package org.mozilla.fenix.wallpapers
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
@ -39,21 +40,25 @@ class WallpaperFileManagerTest {
}
@Test
fun `GIVEN files exist in all directories WHEN expired wallpaper looked up THEN expired wallpaper returned`() {
fun `GIVEN files exist in all directories WHEN expired wallpaper looked up THEN expired wallpaper returned`() = runTest {
val wallpaperName = "name"
createAllFiles(wallpaperName)
val result = fileManager.lookupExpiredWallpaper(wallpaperName)
val expected = Wallpaper.Expired(name = wallpaperName)
assertEquals(expected, fileManager.lookupExpiredWallpaper(wallpaperName))
assertEquals(expected, result)
}
@Test
fun `GIVEN any missing file in directories WHEN expired wallpaper looked up THEN null returned`() {
fun `GIVEN any missing file in directories WHEN expired wallpaper looked up THEN null returned`() = runTest {
val wallpaperName = "name"
File(landscapeLightFolder, "$wallpaperName.png").createNewFile()
File(landscapeDarkFolder, "$wallpaperName.png").createNewFile()
assertEquals(null, fileManager.lookupExpiredWallpaper(wallpaperName))
val result = fileManager.lookupExpiredWallpaper(wallpaperName)
assertEquals(null, result)
}
@Test

View File

@ -1,182 +1,15 @@
package org.mozilla.fenix.wallpapers
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.slot
import kotlinx.coroutines.test.runTest
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.utils.Settings
import java.util.Calendar
import java.util.Date
class WallpaperManagerTest {
// initialize this once, so it can be shared throughout tests
private val baseFakeDate = Date()
private val fakeCalendar = Calendar.getInstance()
private val mockSettings: Settings = mockk()
private val mockDownloader: WallpaperDownloader = mockk {
coEvery { downloadWallpaper(any()) } just runs
}
private val mockFileManager: WallpaperFileManager = mockk {
every { clean(any(), any()) } just runs
}
private val appStore = AppStore()
@Test
fun `WHEN wallpaper set THEN current wallpaper updated in settings and dispatched to store`() {
val currentCaptureSlot = slot<String>()
every { mockSettings.currentWallpaper } returns ""
every { mockSettings.currentWallpaper = capture(currentCaptureSlot) } just runs
val updatedName = "new name"
val updatedWallpaper = Wallpaper.Local.Firefox(updatedName, drawableId = 0)
val wallpaperManager = WallpaperManager(mockSettings, appStore, mockk(), mockFileManager, "en-US", listOf())
wallpaperManager.currentWallpaper = updatedWallpaper
assertEquals(updatedWallpaper.name, currentCaptureSlot.captured)
appStore.waitUntilIdle()
assertEquals(updatedWallpaper, appStore.state.wallpaperState.currentWallpaper)
}
@Test
fun `GIVEN no remote wallpapers expired and locale in promo WHEN downloading remote wallpapers THEN all downloaded`() = runTest {
every { mockSettings.currentWallpaper } returns ""
val fakeRemoteWallpapers = listOf("first", "second", "third").map { name ->
makeFakeRemoteWallpaper(TimeRelation.LATER, name)
}
val wallpaperManager = WallpaperManager(
mockSettings,
appStore,
mockDownloader,
mockFileManager,
"en-US",
allWallpapers = fakeRemoteWallpapers
)
wallpaperManager.downloadAllRemoteWallpapers()
for (fakeRemoteWallpaper in fakeRemoteWallpapers) {
coVerify { mockDownloader.downloadWallpaper(fakeRemoteWallpaper) }
}
}
@Test
fun `GIVEN no remote wallpapers expired and locale not in promo WHEN downloading remote wallpapers THEN none downloaded`() = runTest {
every { mockSettings.currentWallpaper } returns ""
val fakeRemoteWallpapers = listOf("first", "second", "third").map { name ->
makeFakeRemoteWallpaper(TimeRelation.LATER, name)
}
val wallpaperManager = WallpaperManager(
mockSettings,
appStore,
mockDownloader,
mockFileManager,
"en-CA",
allWallpapers = fakeRemoteWallpapers
)
wallpaperManager.downloadAllRemoteWallpapers()
for (fakeRemoteWallpaper in fakeRemoteWallpapers) {
coVerify(exactly = 0) { mockDownloader.downloadWallpaper(fakeRemoteWallpaper) }
}
}
@Test
fun `GIVEN no remote wallpapers expired and locale not in promo WHEN downloading remote wallpapers THEN non-promo wallpapers downloaded`() = runTest {
every { mockSettings.currentWallpaper } returns ""
val fakePromoWallpapers = listOf("first", "second", "third").map { name ->
makeFakeRemoteWallpaper(TimeRelation.LATER, name)
}
val fakeNonPromoWallpapers = listOf(makeFakeRemoteWallpaper(TimeRelation.LATER, "fourth", false))
val fakeRemoteWallpapers = fakePromoWallpapers + fakeNonPromoWallpapers
val wallpaperManager = WallpaperManager(
mockSettings,
appStore,
mockDownloader,
mockFileManager,
"en-CA",
allWallpapers = fakeRemoteWallpapers
)
wallpaperManager.downloadAllRemoteWallpapers()
for (wallpaper in fakePromoWallpapers) {
coVerify(exactly = 0) { mockDownloader.downloadWallpaper(wallpaper) }
}
for (wallpaper in fakeNonPromoWallpapers) {
coVerify { mockDownloader.downloadWallpaper(wallpaper) }
}
}
@Test
fun `GIVEN some expired wallpapers WHEN initialized THEN wallpapers are not available`() {
every { mockSettings.currentWallpaper } returns ""
val expiredRemoteWallpaper = makeFakeRemoteWallpaper(TimeRelation.BEFORE, "expired")
val activeRemoteWallpaper = makeFakeRemoteWallpaper(TimeRelation.LATER, "expired")
val wallpaperManager = WallpaperManager(
mockSettings,
appStore,
mockDownloader,
mockFileManager,
"en-US",
allWallpapers = listOf(expiredRemoteWallpaper, activeRemoteWallpaper)
)
assertFalse(wallpaperManager.wallpapers.contains(expiredRemoteWallpaper))
assertTrue(wallpaperManager.wallpapers.contains(activeRemoteWallpaper))
}
@Test
fun `GIVEN current wallpaper is expired THEN it is available as expired even when others are filtered`() {
val currentWallpaperName = "named"
val currentExpiredWallpaper = makeFakeRemoteWallpaper(TimeRelation.BEFORE, name = currentWallpaperName)
every { mockSettings.currentWallpaper } returns currentWallpaperName
val expiredRemoteWallpaper = makeFakeRemoteWallpaper(TimeRelation.BEFORE, "expired")
val expected = Wallpaper.Expired(currentWallpaperName)
every { mockFileManager.lookupExpiredWallpaper(currentWallpaperName) } returns expected
val wallpaperManager = WallpaperManager(
mockSettings,
appStore,
mockDownloader,
mockFileManager,
"en-US",
allWallpapers = listOf(expiredRemoteWallpaper)
)
assertFalse(wallpaperManager.wallpapers.contains(expiredRemoteWallpaper))
assertFalse(wallpaperManager.wallpapers.contains(currentExpiredWallpaper))
assertEquals(expected, wallpaperManager.currentWallpaper)
}
@Test
fun `GIVEN current wallpaper is expired THEN it is available even if not listed in initial parameter`() {
val currentWallpaperName = "named"
every { mockSettings.currentWallpaper } returns currentWallpaperName
val expected = Wallpaper.Expired(currentWallpaperName)
every { mockFileManager.lookupExpiredWallpaper(currentWallpaperName) } returns expected
val wallpaperManager = WallpaperManager(
mockSettings,
appStore,
mockDownloader,
mockFileManager,
"en-US",
allWallpapers = listOf()
)
assertEquals(expected, wallpaperManager.currentWallpaper)
}
@Test
fun `GIVEN no custom wallpaper set WHEN checking whether the current wallpaper should be default THEN return true`() {
@ -204,47 +37,4 @@ class WallpaperManagerTest {
assertFalse(result)
}
@Test
fun `WHEN manager initialized THEN available wallpapers are dispatched to store`() {
every { mockSettings.currentWallpaper } returns WallpaperManager.defaultWallpaper.name
val wallpaperManager = WallpaperManager(
mockSettings,
appStore,
mockDownloader,
mockFileManager,
"en-US",
)
appStore.waitUntilIdle()
assertEquals(wallpaperManager.wallpapers, appStore.state.wallpaperState.availableWallpapers)
}
private enum class TimeRelation {
BEFORE,
NOW,
LATER,
}
/**
* [timeRelation] should specify a time relative to the time the tests are run
*/
private fun makeFakeRemoteWallpaper(
timeRelation: TimeRelation,
name: String = "name",
isInPromo: Boolean = true
): Wallpaper.Remote {
fakeCalendar.time = baseFakeDate
when (timeRelation) {
TimeRelation.BEFORE -> fakeCalendar.add(Calendar.DATE, -5)
TimeRelation.NOW -> Unit
TimeRelation.LATER -> fakeCalendar.add(Calendar.DATE, 5)
}
val relativeTime = fakeCalendar.time
return if (isInPromo) {
Wallpaper.Remote.House(name = name, expirationDate = relativeTime)
} else {
Wallpaper.Remote.Firefox(name = name)
}
}
}

View File

@ -0,0 +1,230 @@
package org.mozilla.fenix.wallpapers
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.slot
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import mozilla.components.service.glean.testing.GleanTestRule
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.GleanMetrics.Wallpapers
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.utils.Settings
import java.util.Calendar
import java.util.Date
@RunWith(AndroidJUnit4::class)
class WallpapersUseCasesTest {
@get:Rule
val gleanTestRule = GleanTestRule(testContext)
// initialize this once, so it can be shared throughout tests
private val baseFakeDate = Date()
private val fakeCalendar = Calendar.getInstance()
private val appStore = AppStore()
private val mockSettings = mockk<Settings>()
private val mockDownloader = mockk<WallpaperDownloader>(relaxed = true)
private val mockFileManager = mockk<WallpaperFileManager> {
every { clean(any(), any()) } just runs
}
@Test
fun `GIVEN wallpapers that expired WHEN invoking initialize use case THEN expired wallpapers are filtered out and cleaned up`() = runTest {
val fakeRemoteWallpapers = listOf("first", "second", "third").map { name ->
makeFakeRemoteWallpaper(TimeRelation.LATER, name)
}
val fakeExpiredRemoteWallpapers = listOf("expired").map { name ->
makeFakeRemoteWallpaper(TimeRelation.BEFORE, name)
}
val possibleWallpapers = fakeRemoteWallpapers + fakeExpiredRemoteWallpapers
every { mockSettings.currentWallpaper } returns ""
coEvery { mockFileManager.lookupExpiredWallpaper(any()) } returns null
WallpapersUseCases.DefaultInitializeWallpaperUseCase(
appStore,
mockDownloader,
mockFileManager,
mockSettings,
"en-US",
possibleWallpapers = possibleWallpapers
).invoke()
val expectedFilteredWallpaper = fakeExpiredRemoteWallpapers[0]
appStore.waitUntilIdle()
assertFalse(appStore.state.wallpaperState.availableWallpapers.contains(expectedFilteredWallpaper))
verify { mockFileManager.clean(Wallpaper.Default, possibleWallpapers) }
}
@Test
fun `GIVEN wallpapers that expired and an expired one is selected WHEN invoking initialize use case THEN selected wallpaper is not filtered out`() = runTest {
val fakeRemoteWallpapers = listOf("first", "second", "third").map { name ->
makeFakeRemoteWallpaper(TimeRelation.LATER, name)
}
val expiredWallpaper = makeFakeRemoteWallpaper(TimeRelation.BEFORE, "expired")
every { mockSettings.currentWallpaper } returns expiredWallpaper.name
coEvery { mockFileManager.lookupExpiredWallpaper(any()) } returns null
WallpapersUseCases.DefaultInitializeWallpaperUseCase(
appStore,
mockDownloader,
mockFileManager,
mockSettings,
"en-US",
possibleWallpapers = fakeRemoteWallpapers + listOf(expiredWallpaper)
).invoke()
appStore.waitUntilIdle()
assertTrue(appStore.state.wallpaperState.availableWallpapers.contains(expiredWallpaper))
assertEquals(expiredWallpaper, appStore.state.wallpaperState.currentWallpaper)
}
@Test
fun `GIVEN wallpapers that are in promotions outside of locale WHEN invoking initialize use case THEN promotional wallpapers are filtered out`() = runTest {
val fakeRemoteWallpapers = listOf("first", "second", "third").map { name ->
makeFakeRemoteWallpaper(TimeRelation.LATER, name)
}
val locale = "en-CA"
every { mockSettings.currentWallpaper } returns ""
coEvery { mockFileManager.lookupExpiredWallpaper(any()) } returns null
WallpapersUseCases.DefaultInitializeWallpaperUseCase(
appStore,
mockDownloader,
mockFileManager,
mockSettings,
locale,
possibleWallpapers = fakeRemoteWallpapers
).invoke()
appStore.waitUntilIdle()
assertTrue(appStore.state.wallpaperState.availableWallpapers.isEmpty())
}
@Test
fun `GIVEN available wallpapers WHEN invoking initialize use case THEN available wallpapers downloaded`() = runTest {
val fakeRemoteWallpapers = listOf("first", "second", "third").map { name ->
makeFakeRemoteWallpaper(TimeRelation.LATER, name)
}
every { mockSettings.currentWallpaper } returns ""
coEvery { mockFileManager.lookupExpiredWallpaper(any()) } returns null
WallpapersUseCases.DefaultInitializeWallpaperUseCase(
appStore,
mockDownloader,
mockFileManager,
mockSettings,
"en-US",
possibleWallpapers = fakeRemoteWallpapers
).invoke()
for (fakeRemoteWallpaper in fakeRemoteWallpapers) {
coVerify { mockDownloader.downloadWallpaper(fakeRemoteWallpaper) }
}
}
@Test
fun `GIVEN a wallpaper has not been selected WHEN invoking initialize use case THEN store contains default`() = runTest {
val fakeRemoteWallpapers = listOf("first", "second", "third").map { name ->
makeFakeRemoteWallpaper(TimeRelation.LATER, name)
}
every { mockSettings.currentWallpaper } returns ""
coEvery { mockFileManager.lookupExpiredWallpaper(any()) } returns null
WallpapersUseCases.DefaultInitializeWallpaperUseCase(
appStore,
mockDownloader,
mockFileManager,
mockSettings,
"en-US",
possibleWallpapers = fakeRemoteWallpapers
).invoke()
appStore.waitUntilIdle()
assertTrue(appStore.state.wallpaperState.currentWallpaper is Wallpaper.Default)
}
@Test
fun `GIVEN a wallpaper is selected and there are available wallpapers WHEN invoking initialize use case THEN these are dispatched to the store`() = runTest {
val selectedWallpaper = makeFakeRemoteWallpaper(TimeRelation.LATER, "selected")
val fakeRemoteWallpapers = listOf("first", "second", "third").map { name ->
makeFakeRemoteWallpaper(TimeRelation.LATER, name)
}
val possibleWallpapers = listOf(selectedWallpaper) + fakeRemoteWallpapers
every { mockSettings.currentWallpaper } returns selectedWallpaper.name
coEvery { mockFileManager.lookupExpiredWallpaper(any()) } returns null
WallpapersUseCases.DefaultInitializeWallpaperUseCase(
appStore,
mockDownloader,
mockFileManager,
mockSettings,
"en-US",
possibleWallpapers = possibleWallpapers
).invoke()
appStore.waitUntilIdle()
assertEquals(selectedWallpaper, appStore.state.wallpaperState.currentWallpaper)
assertEquals(possibleWallpapers, appStore.state.wallpaperState.availableWallpapers)
}
@Test
fun `WHEN selected wallpaper usecase invoked THEN storage updated and store receives dispatch`() {
val selectedWallpaper = makeFakeRemoteWallpaper(TimeRelation.LATER, "selected")
val slot = slot<String>()
coEvery { mockFileManager.lookupExpiredWallpaper(any()) } returns null
every { mockSettings.currentWallpaper } returns ""
every { mockSettings.currentWallpaper = capture(slot) } just runs
WallpapersUseCases.DefaultSelectWallpaperUseCase(
mockSettings,
appStore
).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")!!)
}
private enum class TimeRelation {
BEFORE,
NOW,
LATER,
}
/**
* [timeRelation] should specify a time relative to the time the tests are run
*/
private fun makeFakeRemoteWallpaper(
timeRelation: TimeRelation,
name: String = "name",
isInPromo: Boolean = true
): Wallpaper.Remote {
fakeCalendar.time = baseFakeDate
when (timeRelation) {
TimeRelation.BEFORE -> fakeCalendar.add(Calendar.DATE, -5)
TimeRelation.NOW -> Unit
TimeRelation.LATER -> fakeCalendar.add(Calendar.DATE, 5)
}
val relativeTime = fakeCalendar.time
return if (isInPromo) {
Wallpaper.Remote.House(name = name, expirationDate = relativeTime)
} else {
Wallpaper.Remote.Firefox(name = name)
}
}
}