[fenix] For https://github.com/mozilla-mobile/fenix/issues/26423: simplify wallpaper types to single data class

pull/600/head
MatthewTighe 2 years ago committed by mergify[bot]
parent c6a17a7883
commit 702aeda47b

@ -931,7 +931,7 @@ class HomeFragment : Fragment() {
// We only want to update the wallpaper when it's different from the default one // We only want to update the wallpaper when it's different from the default one
// as the default is applied already on xml by default. // as the default is applied already on xml by default.
when (val currentWallpaper = state.wallpaperState.currentWallpaper) { when (val currentWallpaper = state.wallpaperState.currentWallpaper) {
is Wallpaper.Default -> { Wallpaper.Default -> {
binding.wallpaperImageView.visibility = View.GONE binding.wallpaperImageView.visibility = View.GONE
} }
else -> { else -> {

@ -4,88 +4,43 @@
package org.mozilla.fenix.wallpapers package org.mozilla.fenix.wallpapers
import androidx.annotation.DrawableRes
import java.util.Calendar
import java.util.Date import java.util.Date
/** /**
* Type hierarchy defining the various wallpapers that are available as home screen backgrounds. * Type that represents wallpapers.
*
* @property name The name of the wallpaper. * @property name The name of the wallpaper.
* @property collectionName The name of the collection the wallpaper belongs to.
* @property availableLocales The locales that this wallpaper is restricted to. If null, the wallpaper
* is not restricted.
* @property startDate The date the wallpaper becomes available in a promotion. If null, it is available
* from any date.
* @property endDate The date the wallpaper stops being available in a promotion. If null,
* the wallpaper will be available to any date.
*/ */
sealed class Wallpaper { data class Wallpaper(
abstract val name: String val name: String,
val collectionName: String,
/** val availableLocales: List<String>?,
* The default wallpaper. This uses the standard color resource to as a background, instead of val startDate: Date?,
* loading a bitmap. val endDate: Date?
*/ ) {
object Default : Wallpaper() {
override val name = "default"
}
/**
* 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) : Wallpaper()
/**
* 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?
abstract val remoteParentDirName: String
@Suppress("MagicNumber")
/**
* A promotional partnered wallpaper.
*/
data class House(
override val name: String,
override val expirationDate: Date? = Calendar.getInstance().run {
set(2022, Calendar.APRIL, 30)
time
}
) : Remote(), Promotional {
override val remoteParentDirName: String = "house"
override fun isAvailableInLocale(locale: String): Boolean =
listOf("en-US", "es-US").contains(locale)
}
/**
* Wallpapers that are original Firefox creations.
*/
data class Firefox(
override val name: String,
) : Remote() {
override val expirationDate: Date? = null
override val remoteParentDirName: String = "firefox"
}
}
/**
* Designates whether a wallpaper is part of a promotion that is locale-restricted.
*/
interface Promotional {
/**
* Returns whether the wallpaper is available in [locale] or not.
*/
fun isAvailableInLocale(locale: String): Boolean
}
companion object { companion object {
const val amethystName = "amethyst"
const val ceruleanName = "cerulean"
const val sunriseName = "sunrise"
const val twilightHillsName = "twilight-hills"
const val beachVibeName = "beach-vibe"
const val firefoxCollectionName = "firefox"
const val defaultName = "default"
val Default = Wallpaper(
name = defaultName,
collectionName = defaultName,
availableLocales = null,
startDate = null,
endDate = null,
)
/** /**
* Defines the standard path at which a wallpaper resource is kept on disk. * Defines the standard path at which a wallpaper resource is kept on disk.
* *

@ -33,7 +33,7 @@ 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.Remote) = withContext(Dispatchers.IO) { suspend fun downloadWallpaper(wallpaper: Wallpaper) = withContext(Dispatchers.IO) {
if (remoteHost.isNullOrEmpty()) { if (remoteHost.isNullOrEmpty()) {
return@withContext return@withContext
} }
@ -71,14 +71,14 @@ 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.Remote.toMetadata(context: Context): List<WallpaperMetadata> = private fun Wallpaper.toMetadata(context: Context): List<WallpaperMetadata> =
listOf("landscape", "portrait").flatMap { orientation -> listOf("landscape", "portrait").flatMap { orientation ->
listOf("light", "dark").map { theme -> listOf("light", "dark").map { theme ->
val localPath = "wallpapers/$orientation/$theme/$name.png" val localPath = "wallpapers/$orientation/$theme/$name.png"
val remotePath = "${context.resolutionSegment()}/" + val remotePath = "${context.resolutionSegment()}/" +
"$orientation/" + "$orientation/" +
"$theme/" + "$theme/" +
"$remoteParentDirName/" + "$collectionName/" +
"$name.png" "$name.png"
WallpaperMetadata(remotePath, localPath) WallpaperMetadata(remotePath, localPath)
} }

@ -24,9 +24,15 @@ class WallpaperFileManager(
* files for each of the following orientation and theme combinations: * files for each of the following orientation and theme combinations:
* light/portrait - light/landscape - dark/portrait - dark/landscape * light/portrait - light/landscape - dark/portrait - dark/landscape
*/ */
suspend fun lookupExpiredWallpaper(name: String): Wallpaper.Expired? = withContext(Dispatchers.IO) { suspend fun lookupExpiredWallpaper(name: String): Wallpaper? = withContext(Dispatchers.IO) {
if (getAllLocalWallpaperPaths(name).all { File(rootDirectory, it).exists() }) { if (getAllLocalWallpaperPaths(name).all { File(rootDirectory, it).exists() }) {
Wallpaper.Expired(name) Wallpaper(
name = name,
collectionName = "",
availableLocales = null,
startDate = null,
endDate = null,
)
} else null } else null
} }
@ -40,7 +46,7 @@ class WallpaperFileManager(
/** /**
* Remove all wallpapers that are not the [currentWallpaper] or in [availableWallpapers]. * Remove all wallpapers that are not the [currentWallpaper] or in [availableWallpapers].
*/ */
fun clean(currentWallpaper: Wallpaper, availableWallpapers: List<Wallpaper.Remote>) { fun clean(currentWallpaper: Wallpaper, availableWallpapers: List<Wallpaper>) {
scope.launch { scope.launch {
val wallpapersToKeep = (listOf(currentWallpaper) + availableWallpapers).map { it.name } val wallpapersToKeep = (listOf(currentWallpaper) + availableWallpapers).map { it.name }
cleanChildren(portraitDirectory, wallpapersToKeep) cleanChildren(portraitDirectory, wallpapersToKeep)

@ -96,7 +96,7 @@ class WallpapersUseCases(
// This should be cleaned up as improvements are made to the storage, file management, // This should be cleaned up as improvements are made to the storage, file management,
// and download utilities. // and download utilities.
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val availableWallpapers = getAvailableWallpapers() val availableWallpapers = possibleWallpapers.getAvailableWallpapers()
val currentWallpaperName = settings.currentWallpaper val currentWallpaperName = settings.currentWallpaper
val currentWallpaper = possibleWallpapers.find { it.name == currentWallpaperName } val currentWallpaper = possibleWallpapers.find { it.name == currentWallpaperName }
?: fileManager.lookupExpiredWallpaper(currentWallpaperName) ?: fileManager.lookupExpiredWallpaper(currentWallpaperName)
@ -104,7 +104,7 @@ class WallpapersUseCases(
fileManager.clean( fileManager.clean(
currentWallpaper, currentWallpaper,
possibleWallpapers.filterIsInstance<Wallpaper.Remote>() possibleWallpapers
) )
downloadAllRemoteWallpapers(availableWallpapers) downloadAllRemoteWallpapers(availableWallpapers)
store.dispatch(AppAction.WallpaperAction.UpdateAvailableWallpapers(availableWallpapers)) store.dispatch(AppAction.WallpaperAction.UpdateAvailableWallpapers(availableWallpapers))
@ -112,42 +112,63 @@ class WallpapersUseCases(
} }
} }
private fun getAvailableWallpapers() = possibleWallpapers private fun List<Wallpaper>.getAvailableWallpapers() =
.filter { !it.isExpired() && it.isAvailableInLocale() } this.filter { !it.isExpired() && it.isAvailableInLocale() }
private suspend fun downloadAllRemoteWallpapers(availableWallpapers: List<Wallpaper>) { private suspend fun downloadAllRemoteWallpapers(availableWallpapers: List<Wallpaper>) {
for (wallpaper in availableWallpapers.filterIsInstance<Wallpaper.Remote>()) { for (wallpaper in availableWallpapers) {
downloader.downloadWallpaper(wallpaper) if (wallpaper != Wallpaper.Default) {
downloader.downloadWallpaper(wallpaper)
}
} }
} }
private fun Wallpaper.isExpired(): Boolean = when (this) { private fun Wallpaper.isExpired(): Boolean {
is Wallpaper.Remote -> { val expired = this.endDate?.let { Date().after(it) } ?: false
val expired = this.expirationDate?.let { Date().after(it) } ?: false return expired && this.name != settings.currentWallpaper
expired && this.name != settings.currentWallpaper
}
else -> false
} }
private fun Wallpaper.isAvailableInLocale(): Boolean = private fun Wallpaper.isAvailableInLocale(): Boolean =
if (this is Wallpaper.Promotional) { this.availableLocales?.contains(currentLocale) ?: true
this.isAvailableInLocale(currentLocale)
} else {
true
}
companion object { companion object {
private val localWallpapers: List<Wallpaper.Local> = listOf( private val localWallpapers: List<Wallpaper> = listOf(
Wallpaper.Local.Firefox("amethyst", R.drawable.amethyst), Wallpaper(
Wallpaper.Local.Firefox("cerulean", R.drawable.cerulean), name = Wallpaper.amethystName,
Wallpaper.Local.Firefox("sunrise", R.drawable.sunrise), collectionName = Wallpaper.firefoxCollectionName,
availableLocales = null,
startDate = null,
endDate = null,
),
Wallpaper(
name = Wallpaper.ceruleanName,
collectionName = Wallpaper.firefoxCollectionName,
availableLocales = null,
startDate = null,
endDate = null,
),
Wallpaper(
name = Wallpaper.sunriseName,
collectionName = Wallpaper.firefoxCollectionName,
availableLocales = null,
startDate = null,
endDate = null,
),
) )
private val remoteWallpapers: List<Wallpaper.Remote> = listOf( private val remoteWallpapers: List<Wallpaper> = listOf(
Wallpaper.Remote.Firefox( Wallpaper(
"twilight-hills" name = Wallpaper.twilightHillsName,
collectionName = Wallpaper.firefoxCollectionName,
availableLocales = null,
startDate = null,
endDate = null,
), ),
Wallpaper.Remote.Firefox( Wallpaper(
"beach-vibe" name = Wallpaper.beachVibeName,
collectionName = Wallpaper.firefoxCollectionName,
availableLocales = null,
startDate = null,
endDate = null,
), ),
) )
val allWallpapers = listOf(Wallpaper.Default) + localWallpapers + remoteWallpapers val allWallpapers = listOf(Wallpaper.Default) + localWallpapers + remoteWallpapers
@ -173,24 +194,34 @@ class WallpapersUseCases(
* *
* @param wallpaper The wallpaper to load a bitmap for. * @param wallpaper The wallpaper to load a bitmap for.
*/ */
override suspend operator fun invoke(wallpaper: Wallpaper): Bitmap? = when (wallpaper) { override suspend operator fun invoke(wallpaper: Wallpaper): Bitmap? = when (wallpaper.name) {
is Wallpaper.Local -> loadWallpaperFromDrawable(context, wallpaper) Wallpaper.amethystName, Wallpaper.ceruleanName, Wallpaper.sunriseName -> {
is Wallpaper.Remote -> loadWallpaperFromDisk(context, wallpaper) loadWallpaperFromDrawable(context, wallpaper)
}
Wallpaper.twilightHillsName, Wallpaper.beachVibeName -> {
loadWallpaperFromDisk(context, wallpaper)
}
else -> null else -> null
} }
private suspend fun loadWallpaperFromDrawable( private suspend fun loadWallpaperFromDrawable(
context: Context, context: Context,
wallpaper: Wallpaper.Local wallpaper: Wallpaper
): Bitmap? = Result.runCatching { ): Bitmap? = Result.runCatching {
val drawableRes = when (wallpaper.name) {
Wallpaper.amethystName -> R.drawable.amethyst
Wallpaper.ceruleanName -> R.drawable.cerulean
Wallpaper.sunriseName -> R.drawable.sunrise
else -> return@runCatching null
}
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
BitmapFactory.decodeResource(context.resources, wallpaper.drawableId) BitmapFactory.decodeResource(context.resources, drawableRes)
} }
}.getOrNull() }.getOrNull()
private suspend fun loadWallpaperFromDisk( private suspend fun loadWallpaperFromDisk(
context: Context, context: Context,
wallpaper: Wallpaper.Remote wallpaper: Wallpaper
): Bitmap? = Result.runCatching { ): Bitmap? = Result.runCatching {
val path = wallpaper.getLocalPathFromContext(context) val path = wallpaper.getLocalPathFromContext(context)
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -203,7 +234,7 @@ class WallpapersUseCases(
* Get the expected local path on disk for a wallpaper. This will differ depending * Get the expected local path on disk for a wallpaper. This will differ depending
* on orientation and app theme. * on orientation and app theme.
*/ */
private fun Wallpaper.Remote.getLocalPathFromContext(context: Context): String { private fun Wallpaper.getLocalPathFromContext(context: Context): String {
val orientation = if (context.isLandscape()) "landscape" else "portrait" val orientation = if (context.isLandscape()) "landscape" else "portrait"
val theme = if (context.isDark()) "dark" else "light" val theme = if (context.isDark()) "dark" else "light"
return Wallpaper.getBaseLocalPath(orientation, theme, name) return Wallpaper.getBaseLocalPath(orientation, theme, name)
@ -247,7 +278,7 @@ class WallpapersUseCases(
Wallpapers.wallpaperSelected.record( Wallpapers.wallpaperSelected.record(
Wallpapers.WallpaperSelectedExtra( Wallpapers.WallpaperSelectedExtra(
name = wallpaper.name, name = wallpaper.name,
themeCollection = wallpaper::class.simpleName themeCollection = wallpaper.collectionName
) )
) )
} }

@ -46,7 +46,7 @@ class WallpaperFileManagerTest {
val result = fileManager.lookupExpiredWallpaper(wallpaperName) val result = fileManager.lookupExpiredWallpaper(wallpaperName)
val expected = Wallpaper.Expired(name = wallpaperName) val expected = generateWallpaper(name = wallpaperName)
assertEquals(expected, result) assertEquals(expected, result)
} }
@ -64,9 +64,9 @@ class WallpaperFileManagerTest {
@Test @Test
fun `WHEN cleaned THEN current wallpaper and available wallpapers kept`() { fun `WHEN cleaned THEN current wallpaper and available wallpapers kept`() {
val currentName = "current" val currentName = "current"
val currentWallpaper = Wallpaper.Expired(currentName) val currentWallpaper = generateWallpaper(name = currentName)
val availableName = "available" val availableName = "available"
val available = Wallpaper.Remote.House(name = availableName) val available = generateWallpaper(name = availableName)
val unavailableName = "unavailable" val unavailableName = "unavailable"
createAllFiles(currentName) createAllFiles(currentName)
createAllFiles(availableName) createAllFiles(availableName)
@ -93,4 +93,12 @@ class WallpaperFileManagerTest {
File(landscapeDarkFolder, "$name.png"), File(landscapeDarkFolder, "$name.png"),
) )
} }
private fun generateWallpaper(name: String) = Wallpaper(
name = name,
collectionName = "",
availableLocales = null,
startDate = null,
endDate = null
)
} }

@ -42,6 +42,48 @@ class WallpapersUseCasesTest {
every { clean(any(), any()) } just runs every { clean(any(), any()) } just runs
} }
@Test
fun `WHEN initializing THEN the default wallpaper is not 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 = listOf(Wallpaper.Default) + fakeRemoteWallpapers
).invoke()
appStore.waitUntilIdle()
coVerify(exactly = 0) { mockDownloader.downloadWallpaper(Wallpaper.Default) }
}
@Test
fun `WHEN initializing THEN default wallpaper is included in available wallpapers`() = 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 = listOf(Wallpaper.Default) + fakeRemoteWallpapers
).invoke()
appStore.waitUntilIdle()
assertTrue(appStore.state.wallpaperState.availableWallpapers.contains(Wallpaper.Default))
}
@Test @Test
fun `GIVEN wallpapers that expired WHEN invoking initialize use case THEN expired wallpapers are filtered out and cleaned up`() = runTest { 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 -> val fakeRemoteWallpapers = listOf("first", "second", "third").map { name ->
@ -154,7 +196,7 @@ class WallpapersUseCasesTest {
).invoke() ).invoke()
appStore.waitUntilIdle() appStore.waitUntilIdle()
assertTrue(appStore.state.wallpaperState.currentWallpaper is Wallpaper.Default) assertTrue(appStore.state.wallpaperState.currentWallpaper == Wallpaper.Default)
} }
@Test @Test
@ -213,7 +255,7 @@ class WallpapersUseCasesTest {
timeRelation: TimeRelation, timeRelation: TimeRelation,
name: String = "name", name: String = "name",
isInPromo: Boolean = true isInPromo: Boolean = true
): Wallpaper.Remote { ): Wallpaper {
fakeCalendar.time = baseFakeDate fakeCalendar.time = baseFakeDate
when (timeRelation) { when (timeRelation) {
TimeRelation.BEFORE -> fakeCalendar.add(Calendar.DATE, -5) TimeRelation.BEFORE -> fakeCalendar.add(Calendar.DATE, -5)
@ -222,9 +264,21 @@ class WallpapersUseCasesTest {
} }
val relativeTime = fakeCalendar.time val relativeTime = fakeCalendar.time
return if (isInPromo) { return if (isInPromo) {
Wallpaper.Remote.House(name = name, expirationDate = relativeTime) Wallpaper(
name = name,
collectionName = "",
availableLocales = listOf("en-US"),
startDate = null,
endDate = relativeTime
)
} else { } else {
Wallpaper.Remote.Firefox(name = name) Wallpaper(
name = name,
collectionName = "",
availableLocales = null,
startDate = null,
endDate = relativeTime
)
} }
} }
} }

Loading…
Cancel
Save