closes #23565: expire remote wallpapers but let users keep selected

upstream-sync
Matt Tighe 2 years ago committed by mergify[bot]
parent e290bc1bd6
commit 9b9d08c7d1

@ -36,6 +36,7 @@ import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.ClipboardHandler import org.mozilla.fenix.utils.ClipboardHandler
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.wallpapers.WallpaperFileManager
import org.mozilla.fenix.wallpapers.WallpaperDownloader import org.mozilla.fenix.wallpapers.WallpaperDownloader
import org.mozilla.fenix.wallpapers.WallpaperManager import org.mozilla.fenix.wallpapers.WallpaperManager
import org.mozilla.fenix.wifi.WifiConnectionMonitor import org.mozilla.fenix.wifi.WifiConnectionMonitor
@ -148,6 +149,7 @@ class Components(private val context: Context) {
WallpaperManager( WallpaperManager(
settings, settings,
WallpaperDownloader(context, core.client, analytics.crashReporter), WallpaperDownloader(context, core.client, analytics.crashReporter),
WallpaperFileManager(context.filesDir),
analytics.crashReporter, analytics.crashReporter,
) )
} }

@ -953,7 +953,7 @@ private val Event.wrapper: EventWrapper<*>?
Wallpapers.wallpaperSelected.record( Wallpapers.wallpaperSelected.record(
Wallpapers.WallpaperSelectedExtra( Wallpapers.WallpaperSelectedExtra(
name = this.wallpaper.name, name = this.wallpaper.name,
themeCollection = this.wallpaper.themeCollection::class.simpleName, themeCollection = this.wallpaper::class.simpleName,
), ),
) )
} }
@ -963,7 +963,7 @@ private val Event.wrapper: EventWrapper<*>?
Wallpapers.wallpaperSwitched.record( Wallpapers.wallpaperSwitched.record(
Wallpapers.WallpaperSwitchedExtra( Wallpapers.WallpaperSwitchedExtra(
name = this.wallpaper.name, name = this.wallpaper.name,
themeCollection = this.wallpaper.themeCollection::class.simpleName, themeCollection = this.wallpaper::class.simpleName,
), ),
) )
} }

@ -87,7 +87,7 @@ fun WallpaperSettings(
SnackbarHost(hostState = hostState) { SnackbarHost(hostState = hostState) {
WallpaperSnackbar(onViewWallpaper) WallpaperSnackbar(onViewWallpaper)
} }
} },
) { ) {
Column { Column {
WallpaperThumbnails( WallpaperThumbnails(
@ -292,7 +292,7 @@ private fun WallpaperThumbnailsPreview() {
loadWallpaperResource = { loadWallpaperResource = {
wallpaperManager.loadSavedWallpaper(context, it) wallpaperManager.loadSavedWallpaper(context, it)
}, },
wallpapers = wallpaperManager.availableWallpapers, wallpapers = wallpaperManager.wallpapers,
selectedWallpaper = wallpaperManager.currentWallpaper, selectedWallpaper = wallpaperManager.currentWallpaper,
onSelectWallpaper = {}, onSelectWallpaper = {},
onViewWallpaper = {}, onViewWallpaper = {},

@ -49,7 +49,7 @@ class WallpaperSettingsFragment : Fragment() {
var currentWallpaper by remember { mutableStateOf(wallpaperManager.currentWallpaper) } var currentWallpaper by remember { mutableStateOf(wallpaperManager.currentWallpaper) }
var wallpapersSwitchedByLogo by remember { mutableStateOf(settings.wallpapersSwitchedByLogoTap) } var wallpapersSwitchedByLogo by remember { mutableStateOf(settings.wallpapersSwitchedByLogoTap) }
WallpaperSettings( WallpaperSettings(
wallpapers = wallpaperManager.availableWallpapers, wallpapers = wallpaperManager.wallpapers,
defaultWallpaper = WallpaperManager.defaultWallpaper, defaultWallpaper = WallpaperManager.defaultWallpaper,
loadWallpaperResource = { loadWallpaperResource = {
wallpaperManager.loadSavedWallpaper(requireContext(), it) wallpaperManager.loadSavedWallpaper(requireContext(), it)

@ -4,79 +4,62 @@
package org.mozilla.fenix.wallpapers package org.mozilla.fenix.wallpapers
import android.content.Context import androidx.annotation.DrawableRes
import android.content.res.Configuration import java.util.Date
import org.mozilla.fenix.R
/** /**
* A class that represents an available wallpaper and its state. * Type hierarchy defining the various wallpapers that are available as home screen backgrounds.
* @property name Indicates the name of this wallpaper. * @property name The name of the wallpaper.
* @property portraitPath A file path for the portrait version of this wallpaper.
* @property landscapePath A file path for the landscape version of this wallpaper.
* @property isDark Indicates if the most predominant color on the wallpaper is dark.
* @property themeCollection The theme collection this wallpaper belongs to.
*/ */
data class Wallpaper( sealed class Wallpaper {
val name: String, abstract val name: String
val themeCollection: WallpaperThemeCollection,
)
/** /**
* A type hierarchy representing the different theme collections [Wallpaper]s belong to. * The default wallpaper. This uses the standard color resource to as a background, instead of
*/ * loading a bitmap.
enum class WallpaperThemeCollection(val origin: WallpaperOrigin) { */
NONE(WallpaperOrigin.LOCAL), object Default : Wallpaper() {
FIREFOX(WallpaperOrigin.LOCAL), override val name = "default"
FOCUS(WallpaperOrigin.REMOTE), }
}
/** /**
* The parent directory name of a wallpaper. Since wallpapers that are [WallpaperOrigin.LOCAL] are * Wallpapers that are included directly in the shipped APK.
* stored in drawables, this extension is not applicable to them. *
*/ * @property drawableId The drawable bitmap used as the background.
val WallpaperThemeCollection.directoryName: String get() = when (this) { */
WallpaperThemeCollection.NONE, sealed class Local : Wallpaper() {
WallpaperThemeCollection.FIREFOX -> "" abstract val drawableId: Int
WallpaperThemeCollection.FOCUS -> "focus" data class Firefox(override val name: String, @DrawableRes override val drawableId: Int) : Local()
} }
/** /**
* Types defining whether a [Wallpaper] is delivered through a remote source or is included locally * Wallpapers that need to be fetched from a network resource.
* in the APK. *
*/ * @property expirationDate Optional date at which this wallpaper should no longer be available.
enum class WallpaperOrigin { */
LOCAL, sealed class Remote : Wallpaper() {
REMOTE, abstract val expirationDate: Date?
} data class Focus(override val name: String, override val expirationDate: Date? = null) : Remote()
val Wallpaper.drawableId: Int get() = when (name) { /**
"amethyst" -> R.drawable.amethyst * If a user had previously selected a wallpaper, they are allowed to retain it even if
"cerulean" -> R.drawable.cerulean * the wallpaper is otherwise expired. This type exists as a wrapper around that current
"sunrise" -> R.drawable.sunrise * wallpaper.
else -> -1 */
} data class Expired(override val name: String) : Remote() {
override val expirationDate: Date? = null
/** }
* Get the expected local path on disk for a wallpaper. This will differ depending }
* on orientation and app theme.
*/
fun Wallpaper.getLocalPathFromContext(context: Context): String {
val orientation = if (context.isLandscape()) "landscape" else "portrait"
val theme = if (context.isDark()) "dark" else "light"
return getLocalPath(orientation, theme)
}
/**
* Get the expected local path on disk for a wallpaper if orientation and app theme are known.
*/
fun Wallpaper.getLocalPath(orientation: String, theme: String): String =
"$orientation/$theme/${themeCollection.directoryName}/$name.png"
private fun Context.isLandscape(): Boolean {
return resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
}
private fun Context.isDark(): Boolean { companion object {
return resources.configuration.uiMode and /**
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES * Defines the standard path at which a wallpaper resource is kept on disk.
*
* @param orientation One of landscape/portrait.
* @param theme One of dark/light.
* @param name The name of the wallpaper.
*/
fun getBaseLocalPath(orientation: String, theme: String, name: String): String =
"wallpapers/$orientation/$theme/$name.png"
}
} }

@ -35,8 +35,8 @@ class WallpaperDownloader(
* found at a remote path in the form: * found at a remote path in the form:
* <WALLPAPER_URL>/<resolution>/<orientation>/<app theme>/<wallpaper theme>/<wallpaper name>.png * <WALLPAPER_URL>/<resolution>/<orientation>/<app theme>/<wallpaper theme>/<wallpaper name>.png
*/ */
suspend fun downloadWallpaper(wallpaper: Wallpaper) = withContext(Dispatchers.IO) { suspend fun downloadWallpaper(wallpaper: Wallpaper.Remote) = withContext(Dispatchers.IO) {
for (metadata in wallpaper.toMetadata()) { for (metadata in wallpaper.toMetadata(context)) {
val localFile = File(context.filesDir.absolutePath, metadata.localPath) val localFile = File(context.filesDir.absolutePath, metadata.localPath)
if (localFile.exists()) continue if (localFile.exists()) continue
val request = Request( val request = Request(
@ -62,18 +62,15 @@ class WallpaperDownloader(
private data class WallpaperMetadata(val remotePath: String, val localPath: String) private data class WallpaperMetadata(val remotePath: String, val localPath: String)
private fun Wallpaper.toMetadata(): List<WallpaperMetadata> = when (themeCollection.origin) { private fun Wallpaper.Remote.toMetadata(context: Context): List<WallpaperMetadata> =
WallpaperOrigin.LOCAL -> listOf() listOf("landscape", "portrait").flatMap { orientation ->
WallpaperOrigin.REMOTE -> { listOf("light", "dark").map { theme ->
listOf("landscape", "portrait").flatMap { orientation -> val remoteParent = this::class.simpleName!!.lowercase()
listOf("light", "dark").map { theme -> val localPath = "wallpapers/$orientation/$theme/$name.png"
val basePath = getLocalPath(orientation, theme) val remotePath = "${context.resolutionSegment()}/$orientation/$theme/$remoteParent$name.png"
val remotePath = "${context.resolutionSegment()}/$basePath" WallpaperMetadata(remotePath, localPath)
WallpaperMetadata(remotePath, basePath)
}
} }
} }
}
@Suppress("MagicNumber") @Suppress("MagicNumber")
private fun Context.resolutionSegment(): String = when (resources.displayMetrics.densityDpi) { private fun Context.resolutionSegment(): String = when (resources.displayMetrics.densityDpi) {

@ -0,0 +1,56 @@
/* 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 kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
class WallpaperFileManager(
private val rootDirectory: File,
coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
private val scope = CoroutineScope(coroutineDispatcher)
private val portraitDirectory = File(rootDirectory, "wallpapers/portrait")
private val landscapeDirectory = File(rootDirectory, "wallpapers/landscape")
/**
* Lookup all the files for a wallpaper name. This lookup will fail if there are not
* files for each of the following orientation and theme combinations:
* light/portrait - light/landscape - dark/portrait - dark/landscape
*/
fun lookupExpiredWallpaper(name: String): Wallpaper.Remote.Expired? {
return if (getAllLocalWallpaperPaths(name).all { File(rootDirectory, it).exists() }) {
Wallpaper.Remote.Expired(name)
} else null
}
private fun getAllLocalWallpaperPaths(name: String): List<String> =
listOf("landscape", "portrait").flatMap { orientation ->
listOf("light", "dark").map { theme ->
Wallpaper.getBaseLocalPath(orientation, theme, name)
}
}
/**
* Remove all wallpapers that are not the [currentWallpaper] or in [availableWallpapers].
*/
fun clean(currentWallpaper: Wallpaper, availableWallpapers: List<Wallpaper.Remote>) {
val wallpapersToKeep = (listOf(currentWallpaper) + availableWallpapers).map { it.name }
cleanChildren(portraitDirectory, wallpapersToKeep)
cleanChildren(landscapeDirectory, wallpapersToKeep)
}
private fun cleanChildren(dir: File, wallpapersToKeep: List<String>) {
for (file in dir.walkTopDown()) {
if (file.isDirectory || file.nameWithoutExtension in wallpapersToKeep) continue
scope.launch {
file.delete()
}
}
}
}

@ -7,6 +7,7 @@ package org.mozilla.fenix.wallpapers
import android.animation.AnimatorSet import android.animation.AnimatorSet
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.content.Context import android.content.Context
import android.content.res.Configuration
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
@ -22,6 +23,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.perf.runBlockingIncrement import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
import java.io.File import java.io.File
import java.util.Date
/** /**
* Provides access to available wallpapers and manages their states. * Provides access to available wallpapers and manages their states.
@ -30,17 +32,13 @@ import java.io.File
class WallpaperManager( class WallpaperManager(
private val settings: Settings, private val settings: Settings,
private val downloader: WallpaperDownloader, private val downloader: WallpaperDownloader,
private val fileManager: WallpaperFileManager,
private val crashReporter: CrashReporter, private val crashReporter: CrashReporter,
allWallpapers: List<Wallpaper> = availableWallpapers
) { ) {
val logger = Logger("WallpaperManager") val logger = Logger("WallpaperManager")
private val remoteWallpapers = listOf(
Wallpaper( val wallpapers = allWallpapers.filter(::filterExpiredRemoteWallpapers)
"focus",
themeCollection = WallpaperThemeCollection.FOCUS
),
)
var availableWallpapers: List<Wallpaper> = localWallpapers + remoteWallpapers
private set
var currentWallpaper: Wallpaper = getCurrentWallpaperFromSettings() var currentWallpaper: Wallpaper = getCurrentWallpaperFromSettings()
set(value) { set(value) {
@ -48,6 +46,10 @@ class WallpaperManager(
field = value field = value
} }
init {
fileManager.clean(currentWallpaper, wallpapers.filterIsInstance<Wallpaper.Remote>())
}
/** /**
* Apply the [newWallpaper] into the [wallpaperContainer] and update the [currentWallpaper]. * Apply the [newWallpaper] into the [wallpaperContainer] and update the [currentWallpaper].
*/ */
@ -75,7 +77,7 @@ class WallpaperManager(
* Download all known remote wallpapers. * Download all known remote wallpapers.
*/ */
suspend fun downloadAllRemoteWallpapers() { suspend fun downloadAllRemoteWallpapers() {
for (wallpaper in remoteWallpapers) { for (wallpaper in wallpapers.filterIsInstance<Wallpaper.Remote>()) {
downloader.downloadWallpaper(wallpaper) downloader.downloadWallpaper(wallpaper)
} }
} }
@ -85,7 +87,7 @@ class WallpaperManager(
* the first available [Wallpaper] will be returned. * the first available [Wallpaper] will be returned.
*/ */
fun switchToNextWallpaper(): Wallpaper { fun switchToNextWallpaper(): Wallpaper {
val values = availableWallpapers val values = wallpapers
val index = values.indexOf(currentWallpaper) + 1 val index = values.indexOf(currentWallpaper) + 1
return if (index >= values.size) { return if (index >= values.size) {
@ -95,12 +97,22 @@ class WallpaperManager(
} }
} }
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 getCurrentWallpaperFromSettings(): Wallpaper { private fun getCurrentWallpaperFromSettings(): Wallpaper {
val currentWallpaper = settings.currentWallpaper val currentWallpaper = settings.currentWallpaper
return if (currentWallpaper.isEmpty()) { return if (currentWallpaper.isEmpty()) {
defaultWallpaper defaultWallpaper
} else { } else {
availableWallpapers.find { it.name == currentWallpaper } ?: defaultWallpaper wallpapers.find { it.name == currentWallpaper }
?: fileManager.lookupExpiredWallpaper(currentWallpaper)
?: defaultWallpaper
} }
} }
@ -108,20 +120,20 @@ class WallpaperManager(
* Load a wallpaper that is saved locally. * Load a wallpaper that is saved locally.
*/ */
fun loadSavedWallpaper(context: Context, wallpaper: Wallpaper): Bitmap? = fun loadSavedWallpaper(context: Context, wallpaper: Wallpaper): Bitmap? =
if (wallpaper.themeCollection.origin == WallpaperOrigin.LOCAL) { when (wallpaper) {
loadWallpaperFromDrawables(context, wallpaper) is Wallpaper.Local -> loadWallpaperFromDrawables(context, wallpaper)
} else { is Wallpaper.Remote -> loadWallpaperFromDisk(context, wallpaper)
loadWallpaperFromDisk(context, wallpaper) else -> null
} }
private fun loadWallpaperFromDrawables(context: Context, wallpaper: Wallpaper): Bitmap? = Result.runCatching { private fun loadWallpaperFromDrawables(context: Context, wallpaper: Wallpaper.Local): Bitmap? = Result.runCatching {
BitmapFactory.decodeResource(context.resources, wallpaper.drawableId) BitmapFactory.decodeResource(context.resources, wallpaper.drawableId)
}.getOrNull() }.getOrNull()
/** /**
* Load a wallpaper from app-specific storage. * Load a wallpaper from app-specific storage.
*/ */
private fun loadWallpaperFromDisk(context: Context, wallpaper: Wallpaper): Bitmap? = Result.runCatching { private fun loadWallpaperFromDisk(context: Context, wallpaper: Wallpaper.Remote): Bitmap? = Result.runCatching {
val path = wallpaper.getLocalPathFromContext(context) val path = wallpaper.getLocalPathFromContext(context)
runBlockingIncrement { runBlockingIncrement {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -131,6 +143,25 @@ class WallpaperManager(
} }
}.getOrNull() }.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
}
/** /**
* Animates the Firefox logo, if it hasn't been animated before, otherwise nothing will happen. * Animates the Firefox logo, if it hasn't been animated before, otherwise nothing will happen.
* After animating the first time, the [Settings.shouldAnimateFirefoxLogo] setting * After animating the first time, the [Settings.shouldAnimateFirefoxLogo] setting
@ -166,16 +197,18 @@ class WallpaperManager(
companion object { companion object {
const val DEFAULT_RESOURCE = R.attr.homeBackground const val DEFAULT_RESOURCE = R.attr.homeBackground
val defaultWallpaper = Wallpaper( val defaultWallpaper = Wallpaper.Default
name = "default", private val localWallpapers: List<Wallpaper.Local> = listOf(
themeCollection = WallpaperThemeCollection.NONE Wallpaper.Local.Firefox("amethyst", R.drawable.amethyst),
Wallpaper.Local.Firefox("cerulean", R.drawable.cerulean),
Wallpaper.Local.Firefox("sunrise", R.drawable.sunrise),
) )
val localWallpapers = listOf( private val remoteWallpapers: List<Wallpaper.Remote> = listOf(
defaultWallpaper, Wallpaper.Remote.Focus(
Wallpaper("amethyst", themeCollection = WallpaperThemeCollection.FIREFOX), "focus",
Wallpaper("cerulean", themeCollection = WallpaperThemeCollection.FIREFOX), ),
Wallpaper("sunrise", themeCollection = WallpaperThemeCollection.FIREFOX),
) )
private val availableWallpapers = listOf(defaultWallpaper) + localWallpapers + remoteWallpapers
private const val ANIMATION_DELAY_MS = 1500L private const val ANIMATION_DELAY_MS = 1500L
} }
} }

@ -0,0 +1,91 @@
/* 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 kotlinx.coroutines.test.TestCoroutineDispatcher
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File
class WallpaperFileManagerTest {
@Rule
@JvmField
val tempFolder = TemporaryFolder()
private lateinit var portraitLightFolder: File
private lateinit var portraitDarkFolder: File
private lateinit var landscapeLightFolder: File
private lateinit var landscapeDarkFolder: File
private val dispatcher = TestCoroutineDispatcher()
private lateinit var fileManager: WallpaperFileManager
@Before
fun setup() {
portraitLightFolder = tempFolder.newFolder("wallpapers", "portrait", "light")
portraitDarkFolder = tempFolder.newFolder("wallpapers", "portrait", "dark")
landscapeLightFolder = tempFolder.newFolder("wallpapers", "landscape", "light")
landscapeDarkFolder = tempFolder.newFolder("wallpapers", "landscape", "dark")
fileManager = WallpaperFileManager(
rootDirectory = tempFolder.root,
coroutineDispatcher = dispatcher,
)
}
@Test
fun `GIVEN files exist in all directories WHEN expired wallpaper looked up THEN expired wallpaper returned`() {
val wallpaperName = "name"
createAllFiles(wallpaperName)
val expected = Wallpaper.Remote.Expired(name = wallpaperName)
assertEquals(expected, fileManager.lookupExpiredWallpaper(wallpaperName))
}
@Test
fun `GIVEN any missing file in directories WHEN expired wallpaper looked up THEN null returned`() {
val wallpaperName = "name"
File(landscapeLightFolder, "$wallpaperName.png").createNewFile()
File(landscapeDarkFolder, "$wallpaperName.png").createNewFile()
assertEquals(null, fileManager.lookupExpiredWallpaper(wallpaperName))
}
@Test
fun `WHEN cleaned THEN current wallpaper and available wallpapers kept`() {
val currentName = "current"
val currentWallpaper = Wallpaper.Remote.Expired(currentName)
val availableName = "available"
val available = Wallpaper.Remote.Focus(name = availableName)
val unavailableName = "unavailable"
createAllFiles(currentName)
createAllFiles(availableName)
createAllFiles(unavailableName)
fileManager.clean(currentWallpaper, listOf(available))
assertTrue(getAllFiles(currentName).all { it.exists() })
assertTrue(getAllFiles(availableName).all { it.exists() })
assertTrue(getAllFiles(unavailableName).none { it.exists() })
}
private fun createAllFiles(name: String) {
for (file in getAllFiles(name)) {
file.createNewFile()
}
}
private fun getAllFiles(name: String): List<File> {
return listOf(
File(portraitLightFolder, "$name.png"),
File(portraitDarkFolder, "$name.png"),
File(landscapeLightFolder, "$name.png"),
File(landscapeDarkFolder, "$name.png"),
)
}
}

@ -1,32 +1,150 @@
package org.mozilla.fenix.wallpapers package org.mozilla.fenix.wallpapers
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.runs import io.mockk.runs
import io.mockk.slot import io.mockk.slot
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
import java.util.Calendar
import java.util.Date
class WallpaperManagerTest { 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 mockSettings: Settings = mockk()
private val mockMetrics: MetricController = mockk() private val mockMetrics: MetricController = mockk()
private val mockDownloader: WallpaperDownloader = mockk {
coEvery { downloadWallpaper(any()) } just runs
}
private val mockFileManager: WallpaperFileManager = mockk {
every { clean(any(), any()) } just runs
}
@Test @Test
fun `WHEN wallpaper set THEN current wallpaper updated in settings`() { fun `WHEN wallpaper set THEN current wallpaper updated in settings`() {
every { mockMetrics.track(any()) } just runs every { mockMetrics.track(any()) } just runs
val currentCaptureSlot = slot<String>() val currentCaptureSlot = slot<String>()
every { mockSettings.currentWallpaper } returns "a different name" every { mockSettings.currentWallpaper } returns ""
every { mockSettings.currentWallpaper = capture(currentCaptureSlot) } just runs every { mockSettings.currentWallpaper = capture(currentCaptureSlot) } just runs
val updatedWallpaper = WallpaperManager.defaultWallpaper val updatedName = "new name"
val wallpaperManager = WallpaperManager(mockSettings, mockk(), mockk()) val updatedWallpaper = Wallpaper.Local.Firefox(updatedName, drawableId = 0)
val wallpaperManager = WallpaperManager(mockSettings, mockk(), mockFileManager, mockk(), listOf())
wallpaperManager.currentWallpaper = updatedWallpaper wallpaperManager.currentWallpaper = updatedWallpaper
assertEquals(updatedWallpaper.name, currentCaptureSlot.captured) assertEquals(updatedWallpaper.name, currentCaptureSlot.captured)
} }
@Test
fun `GIVEN no remote wallpapers expired WHEN downloading remote wallpapers THEN all downloaded`() = runBlockingTest {
every { mockSettings.currentWallpaper } returns ""
val fakeRemoteWallpapers = listOf("first", "second", "third").map { name ->
makeFakeRemoteWallpaper(TimeRelation.LATER, name)
}
val wallpaperManager = WallpaperManager(
mockSettings,
mockDownloader,
mockFileManager,
mockk(),
allWallpapers = fakeRemoteWallpapers
)
wallpaperManager.downloadAllRemoteWallpapers()
for (fakeRemoteWallpaper in fakeRemoteWallpapers) {
coVerify { mockDownloader.downloadWallpaper(fakeRemoteWallpaper) }
}
}
@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,
mockDownloader,
mockFileManager,
mockk(),
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.Remote.Expired(currentWallpaperName)
every { mockFileManager.lookupExpiredWallpaper(currentWallpaperName) } returns expected
val wallpaperManager = WallpaperManager(
mockSettings,
mockDownloader,
mockFileManager,
mockk(),
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.Remote.Expired(currentWallpaperName)
every { mockFileManager.lookupExpiredWallpaper(currentWallpaperName) } returns expected
val wallpaperManager = WallpaperManager(
mockSettings,
mockDownloader,
mockFileManager,
mockk(),
allWallpapers = listOf()
)
assertEquals(expected, wallpaperManager.currentWallpaper)
}
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"
): 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 Wallpaper.Remote.Focus(name = name, expirationDate = relativeTime)
}
} }

Loading…
Cancel
Save