diff --git a/app/src/main/java/org/mozilla/fenix/components/Components.kt b/app/src/main/java/org/mozilla/fenix/components/Components.kt index 2ea88a5b3..49446aeb2 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -36,6 +36,7 @@ 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.WallpaperFileManager import org.mozilla.fenix.wallpapers.WallpaperDownloader import org.mozilla.fenix.wallpapers.WallpaperManager import org.mozilla.fenix.wifi.WifiConnectionMonitor @@ -148,6 +149,7 @@ class Components(private val context: Context) { WallpaperManager( settings, WallpaperDownloader(context, core.client, analytics.crashReporter), + WallpaperFileManager(context.filesDir), analytics.crashReporter, ) } diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt index 13c835e49..05cbba079 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt @@ -953,7 +953,7 @@ private val Event.wrapper: EventWrapper<*>? Wallpapers.wallpaperSelected.record( Wallpapers.WallpaperSelectedExtra( 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.WallpaperSwitchedExtra( name = this.wallpaper.name, - themeCollection = this.wallpaper.themeCollection::class.simpleName, + themeCollection = this.wallpaper::class.simpleName, ), ) } diff --git a/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettings.kt b/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettings.kt index 9d711d4d9..e0cfa09b1 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettings.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettings.kt @@ -87,7 +87,7 @@ fun WallpaperSettings( SnackbarHost(hostState = hostState) { WallpaperSnackbar(onViewWallpaper) } - } + }, ) { Column { WallpaperThumbnails( @@ -292,7 +292,7 @@ private fun WallpaperThumbnailsPreview() { loadWallpaperResource = { wallpaperManager.loadSavedWallpaper(context, it) }, - wallpapers = wallpaperManager.availableWallpapers, + wallpapers = wallpaperManager.wallpapers, selectedWallpaper = wallpaperManager.currentWallpaper, onSelectWallpaper = {}, onViewWallpaper = {}, diff --git a/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettingsFragment.kt index c2f564800..6f892249a 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettingsFragment.kt @@ -49,7 +49,7 @@ class WallpaperSettingsFragment : Fragment() { var currentWallpaper by remember { mutableStateOf(wallpaperManager.currentWallpaper) } var wallpapersSwitchedByLogo by remember { mutableStateOf(settings.wallpapersSwitchedByLogoTap) } WallpaperSettings( - wallpapers = wallpaperManager.availableWallpapers, + wallpapers = wallpaperManager.wallpapers, defaultWallpaper = WallpaperManager.defaultWallpaper, loadWallpaperResource = { wallpaperManager.loadSavedWallpaper(requireContext(), it) diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/Wallpaper.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/Wallpaper.kt index f9ea1f1e8..1e7ab2e51 100644 --- a/app/src/main/java/org/mozilla/fenix/wallpapers/Wallpaper.kt +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/Wallpaper.kt @@ -4,79 +4,62 @@ package org.mozilla.fenix.wallpapers -import android.content.Context -import android.content.res.Configuration -import org.mozilla.fenix.R +import androidx.annotation.DrawableRes +import java.util.Date /** - * A class that represents an available wallpaper and its state. - * @property name Indicates the name of this 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. + * Type hierarchy defining the various wallpapers that are available as home screen backgrounds. + * @property name The name of the wallpaper. */ -data class Wallpaper( - val name: String, - val themeCollection: WallpaperThemeCollection, -) +sealed class Wallpaper { + abstract val name: String -/** - * A type hierarchy representing the different theme collections [Wallpaper]s belong to. - */ -enum class WallpaperThemeCollection(val origin: WallpaperOrigin) { - NONE(WallpaperOrigin.LOCAL), - FIREFOX(WallpaperOrigin.LOCAL), - FOCUS(WallpaperOrigin.REMOTE), -} - -/** - * The parent directory name of a wallpaper. Since wallpapers that are [WallpaperOrigin.LOCAL] are - * stored in drawables, this extension is not applicable to them. - */ -val WallpaperThemeCollection.directoryName: String get() = when (this) { - WallpaperThemeCollection.NONE, - WallpaperThemeCollection.FIREFOX -> "" - WallpaperThemeCollection.FOCUS -> "focus" -} - -/** - * Types defining whether a [Wallpaper] is delivered through a remote source or is included locally - * in the APK. - */ -enum class WallpaperOrigin { - LOCAL, - REMOTE, -} - -val Wallpaper.drawableId: Int get() = when (name) { - "amethyst" -> R.drawable.amethyst - "cerulean" -> R.drawable.cerulean - "sunrise" -> R.drawable.sunrise - else -> -1 -} - -/** - * 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 { - return resources.configuration.uiMode and - Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + /** + * The default wallpaper. This uses the standard color resource to as a background, instead of + * loading a bitmap. + */ + object Default : Wallpaper() { + override val name = "default" + } + + /** + * Wallpapers that are included directly in the shipped APK. + * + * @property drawableId The drawable bitmap used as the background. + */ + sealed class Local : Wallpaper() { + abstract val drawableId: Int + data class Firefox(override val name: String, @DrawableRes override val drawableId: Int) : Local() + } + + /** + * Wallpapers that need to be fetched from a network resource. + * + * @property expirationDate Optional date at which this wallpaper should no longer be available. + */ + sealed class Remote : Wallpaper() { + abstract val expirationDate: Date? + data class Focus(override val name: String, override val expirationDate: Date? = null) : Remote() + + /** + * If a user had previously selected a wallpaper, they are allowed to retain it even if + * the wallpaper is otherwise expired. This type exists as a wrapper around that current + * wallpaper. + */ + data class Expired(override val name: String) : Remote() { + override val expirationDate: Date? = null + } + } + + companion object { + /** + * 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" + } } diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperDownloader.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperDownloader.kt index 2f2225cfe..755e7f632 100644 --- a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperDownloader.kt +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperDownloader.kt @@ -35,8 +35,8 @@ class WallpaperDownloader( * found at a remote path in the form: * /////.png */ - suspend fun downloadWallpaper(wallpaper: Wallpaper) = withContext(Dispatchers.IO) { - for (metadata in wallpaper.toMetadata()) { + suspend fun downloadWallpaper(wallpaper: Wallpaper.Remote) = withContext(Dispatchers.IO) { + for (metadata in wallpaper.toMetadata(context)) { val localFile = File(context.filesDir.absolutePath, metadata.localPath) if (localFile.exists()) continue val request = Request( @@ -62,18 +62,15 @@ class WallpaperDownloader( private data class WallpaperMetadata(val remotePath: String, val localPath: String) - private fun Wallpaper.toMetadata(): List = when (themeCollection.origin) { - WallpaperOrigin.LOCAL -> listOf() - WallpaperOrigin.REMOTE -> { - listOf("landscape", "portrait").flatMap { orientation -> - listOf("light", "dark").map { theme -> - val basePath = getLocalPath(orientation, theme) - val remotePath = "${context.resolutionSegment()}/$basePath" - WallpaperMetadata(remotePath, basePath) - } + private fun Wallpaper.Remote.toMetadata(context: Context): List = + listOf("landscape", "portrait").flatMap { orientation -> + listOf("light", "dark").map { theme -> + val remoteParent = this::class.simpleName!!.lowercase() + val localPath = "wallpapers/$orientation/$theme/$name.png" + val remotePath = "${context.resolutionSegment()}/$orientation/$theme/$remoteParent$name.png" + WallpaperMetadata(remotePath, localPath) } } - } @Suppress("MagicNumber") private fun Context.resolutionSegment(): String = when (resources.displayMetrics.densityDpi) { diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperFileManager.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperFileManager.kt new file mode 100644 index 000000000..034ae5d73 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperFileManager.kt @@ -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 = + 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) { + val wallpapersToKeep = (listOf(currentWallpaper) + availableWallpapers).map { it.name } + cleanChildren(portraitDirectory, wallpapersToKeep) + cleanChildren(landscapeDirectory, wallpapersToKeep) + } + + private fun cleanChildren(dir: File, wallpapersToKeep: List) { + for (file in dir.walkTopDown()) { + if (file.isDirectory || file.nameWithoutExtension in wallpapersToKeep) continue + scope.launch { + file.delete() + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperManager.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperManager.kt index b0e0193f7..495879b24 100644 --- a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperManager.kt +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperManager.kt @@ -7,6 +7,7 @@ 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.drawable.BitmapDrawable @@ -22,6 +23,7 @@ import org.mozilla.fenix.R 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. @@ -30,17 +32,13 @@ import java.io.File class WallpaperManager( private val settings: Settings, private val downloader: WallpaperDownloader, + private val fileManager: WallpaperFileManager, private val crashReporter: CrashReporter, + allWallpapers: List = availableWallpapers ) { val logger = Logger("WallpaperManager") - private val remoteWallpapers = listOf( - Wallpaper( - "focus", - themeCollection = WallpaperThemeCollection.FOCUS - ), - ) - var availableWallpapers: List = localWallpapers + remoteWallpapers - private set + + val wallpapers = allWallpapers.filter(::filterExpiredRemoteWallpapers) var currentWallpaper: Wallpaper = getCurrentWallpaperFromSettings() set(value) { @@ -48,6 +46,10 @@ class WallpaperManager( field = value } + init { + fileManager.clean(currentWallpaper, wallpapers.filterIsInstance()) + } + /** * Apply the [newWallpaper] into the [wallpaperContainer] and update the [currentWallpaper]. */ @@ -75,7 +77,7 @@ class WallpaperManager( * Download all known remote wallpapers. */ suspend fun downloadAllRemoteWallpapers() { - for (wallpaper in remoteWallpapers) { + for (wallpaper in wallpapers.filterIsInstance()) { downloader.downloadWallpaper(wallpaper) } } @@ -85,7 +87,7 @@ class WallpaperManager( * the first available [Wallpaper] will be returned. */ fun switchToNextWallpaper(): Wallpaper { - val values = availableWallpapers + val values = wallpapers val index = values.indexOf(currentWallpaper) + 1 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 { val currentWallpaper = settings.currentWallpaper return if (currentWallpaper.isEmpty()) { defaultWallpaper } 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. */ fun loadSavedWallpaper(context: Context, wallpaper: Wallpaper): Bitmap? = - if (wallpaper.themeCollection.origin == WallpaperOrigin.LOCAL) { - loadWallpaperFromDrawables(context, wallpaper) - } else { - loadWallpaperFromDisk(context, wallpaper) + when (wallpaper) { + is Wallpaper.Local -> loadWallpaperFromDrawables(context, wallpaper) + is Wallpaper.Remote -> 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) }.getOrNull() /** * 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) runBlockingIncrement { withContext(Dispatchers.IO) { @@ -131,6 +143,25 @@ class WallpaperManager( } }.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. * After animating the first time, the [Settings.shouldAnimateFirefoxLogo] setting @@ -166,16 +197,18 @@ class WallpaperManager( companion object { const val DEFAULT_RESOURCE = R.attr.homeBackground - val defaultWallpaper = Wallpaper( - name = "default", - themeCollection = WallpaperThemeCollection.NONE + val defaultWallpaper = Wallpaper.Default + private val localWallpapers: List = listOf( + Wallpaper.Local.Firefox("amethyst", R.drawable.amethyst), + Wallpaper.Local.Firefox("cerulean", R.drawable.cerulean), + Wallpaper.Local.Firefox("sunrise", R.drawable.sunrise), ) - val localWallpapers = listOf( - defaultWallpaper, - Wallpaper("amethyst", themeCollection = WallpaperThemeCollection.FIREFOX), - Wallpaper("cerulean", themeCollection = WallpaperThemeCollection.FIREFOX), - Wallpaper("sunrise", themeCollection = WallpaperThemeCollection.FIREFOX), + private val remoteWallpapers: List = listOf( + Wallpaper.Remote.Focus( + "focus", + ), ) + private val availableWallpapers = listOf(defaultWallpaper) + localWallpapers + remoteWallpapers private const val ANIMATION_DELAY_MS = 1500L } } diff --git a/app/src/test/java/org/mozilla/fenix/wallpapers/WallpaperFileManagerTest.kt b/app/src/test/java/org/mozilla/fenix/wallpapers/WallpaperFileManagerTest.kt new file mode 100644 index 000000000..4e2a5d374 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/wallpapers/WallpaperFileManagerTest.kt @@ -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 { + return listOf( + File(portraitLightFolder, "$name.png"), + File(portraitDarkFolder, "$name.png"), + File(landscapeLightFolder, "$name.png"), + File(landscapeDarkFolder, "$name.png"), + ) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/wallpapers/WallpaperManagerTest.kt b/app/src/test/java/org/mozilla/fenix/wallpapers/WallpaperManagerTest.kt index 29c670f3f..b74f12c88 100644 --- a/app/src/test/java/org/mozilla/fenix/wallpapers/WallpaperManagerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/wallpapers/WallpaperManagerTest.kt @@ -1,32 +1,150 @@ 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.runBlockingTest import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test import org.mozilla.fenix.components.metrics.MetricController 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 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 fun `WHEN wallpaper set THEN current wallpaper updated in settings`() { every { mockMetrics.track(any()) } just runs val currentCaptureSlot = slot() - every { mockSettings.currentWallpaper } returns "a different name" + every { mockSettings.currentWallpaper } returns "" every { mockSettings.currentWallpaper = capture(currentCaptureSlot) } just runs - val updatedWallpaper = WallpaperManager.defaultWallpaper - val wallpaperManager = WallpaperManager(mockSettings, mockk(), mockk()) + val updatedName = "new name" + val updatedWallpaper = Wallpaper.Local.Firefox(updatedName, drawableId = 0) + val wallpaperManager = WallpaperManager(mockSettings, mockk(), mockFileManager, mockk(), listOf()) wallpaperManager.currentWallpaper = updatedWallpaper 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) + } }