From ae26141b60c9316c0371dd17972a279337da8049 Mon Sep 17 00:00:00 2001 From: MatthewTighe Date: Tue, 16 Aug 2022 13:57:34 -0700 Subject: [PATCH] For #26423: add wallpaper metadata fetcher --- .../org/mozilla/fenix/wallpapers/Wallpaper.kt | 46 ++- .../fenix/wallpapers/WallpaperDownloader.kt | 2 +- .../fenix/wallpapers/WallpaperFileManager.kt | 7 +- .../wallpapers/WallpaperMetadataFetcher.kt | 97 ++++++ .../fenix/wallpapers/WallpapersUseCases.kt | 48 +-- .../wallpapers/WallpaperFileManagerTest.kt | 13 +- .../WallpaperMetadataFetcherTest.kt | 306 ++++++++++++++++++ .../wallpapers/WallpapersUseCasesTest.kt | 26 +- 8 files changed, 493 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperMetadataFetcher.kt create mode 100644 app/src/test/java/org/mozilla/fenix/wallpapers/WallpaperMetadataFetcherTest.kt 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 3cdb74e4a16d..f538a5959c1e 100644 --- a/app/src/main/java/org/mozilla/fenix/wallpapers/Wallpaper.kt +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/Wallpaper.kt @@ -10,21 +10,37 @@ import java.util.Date * Type that represents wallpapers. * * @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 + * @property collection The name of the collection the wallpaper belongs to. * 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. + * @property textColor The 8 digit hex code color that should be used for text overlaying the wallpaper. + * @property cardColor The 8 digit hex code color that should be used for cards overlaying the wallpaper. */ data class Wallpaper( val name: String, - val collectionName: String, - val availableLocales: List?, - val startDate: Date?, - val endDate: Date? + val collection: Collection, + val textColor: Long?, + val cardColor: Long?, ) { + /** + * Type that represents a collection that a [Wallpaper] belongs to. + * + * @property name The name of the collection the wallpaper belongs to. + * @property learnMoreUrl The URL that can be visited to learn more about a collection, if any. + * @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. + */ + data class Collection( + val name: String, + val learnMoreUrl: String?, + val availableLocales: List?, + val startDate: Date?, + val endDate: Date?, + ) + companion object { const val amethystName = "amethyst" const val ceruleanName = "cerulean" @@ -33,13 +49,19 @@ data class Wallpaper( const val beachVibeName = "beach-vibe" const val firefoxCollectionName = "firefox" const val defaultName = "default" - val Default = Wallpaper( + val DefaultCollection = Collection( name = defaultName, - collectionName = defaultName, + learnMoreUrl = null, availableLocales = null, startDate = null, endDate = null, ) + val Default = Wallpaper( + name = defaultName, + collection = DefaultCollection, + textColor = null, + cardColor = null, + ) /** * Defines the standard path at which a wallpaper resource is kept on disk. 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 0016f73f8a02..a38640f60088 100644 --- a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperDownloader.kt +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperDownloader.kt @@ -78,7 +78,7 @@ class WallpaperDownloader( val remotePath = "${context.resolutionSegment()}/" + "$orientation/" + "$theme/" + - "$collectionName/" + + "${collection.name}/" + "$name.png" WallpaperMetadata(remotePath, localPath) } diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperFileManager.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperFileManager.kt index 67e7dc663605..ec483e711045 100644 --- a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperFileManager.kt +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperFileManager.kt @@ -28,10 +28,9 @@ class WallpaperFileManager( if (getAllLocalWallpaperPaths(name).all { File(rootDirectory, it).exists() }) { Wallpaper( name = name, - collectionName = "", - availableLocales = null, - startDate = null, - endDate = null, + collection = Wallpaper.DefaultCollection, + textColor = null, + cardColor = null, ) } else null } diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperMetadataFetcher.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperMetadataFetcher.kt new file mode 100644 index 000000000000..c793327db789 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperMetadataFetcher.kt @@ -0,0 +1,97 @@ +/* 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.Dispatchers +import kotlinx.coroutines.withContext +import mozilla.components.concept.fetch.Client +import mozilla.components.concept.fetch.Request +import org.json.JSONArray +import org.json.JSONObject +import org.mozilla.fenix.BuildConfig +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * Utility class for downloading wallpaper metadata from the remote server. + * + * @property client The client that will be used to fetch metadata. + */ +class WallpaperMetadataFetcher( + private val client: Client +) { + private val metadataUrl = BuildConfig.WALLPAPER_URL.substringBefore("android") + + "metadata/v$currentJsonVersion/wallpapers.json" + + /** + * Downloads the list of wallpapers from the remote source. Failures will return an empty list. + */ + suspend fun downloadWallpaperList(): List = withContext(Dispatchers.IO) { + Result.runCatching { + val request = Request(url = metadataUrl, method = Request.Method.GET) + val response = client.fetch(request) + response.body.useBufferedReader { + val json = it.readText() + JSONObject(json).parseAsWallpapers() + } + }.getOrElse { listOf() } + } + + private fun JSONObject.parseAsWallpapers(): List = with(getJSONArray("collections")) { + (0 until length()).map { index -> + getJSONObject(index).toCollectionOfWallpapers() + }.flatten() + } + + private fun JSONObject.toCollectionOfWallpapers(): List { + val collectionId = getString("id") + val availableLocales = optJSONArray("available-locales")?.getAvailableLocales() + val availabilityRange = optJSONObject("availability-range")?.getAvailabilityRange() + val learnMoreUrl = optString("learn-more-url") + val collection = Wallpaper.Collection( + name = collectionId, + availableLocales = availableLocales, + startDate = availabilityRange?.first, + endDate = availabilityRange?.second, + learnMoreUrl = learnMoreUrl, + ) + return getJSONArray("wallpapers").toWallpaperList(collection) + } + + private fun JSONArray.getAvailableLocales(): List? = + (0 until length()).map { getString(it) } + + private fun JSONObject.getAvailabilityRange(): Pair? { + val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US) + return Result.runCatching { + formatter.parse(getString("start"))!! to formatter.parse(getString("end"))!! + }.getOrNull() + } + + private fun JSONArray.toWallpaperList(collection: Wallpaper.Collection): List = + (0 until length()).map { index -> + with(getJSONObject(index)) { + Wallpaper( + name = getString("id"), + textColor = getArgbValueAsLong("text-color"), + cardColor = getArgbValueAsLong("card-color"), + collection = collection, + ) + } + } + + /** + * The wallpaper metadata has 6 digit hex color codes for compatibility with iOS. Since Android + * expects 8 digit ARBG values, we prepend FF for the "fully visible" version of the color + * listed in the metadata. + */ + private fun JSONObject.getArgbValueAsLong(propName: String): Long = "FF${getString(propName)}" + .toLong(radix = 16) + + companion object { + internal const val currentJsonVersion = 1 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt index aab02182e2ec..6d7fcbb78650 100644 --- a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt @@ -124,51 +124,53 @@ class WallpapersUseCases( } private fun Wallpaper.isExpired(): Boolean { - val expired = this.endDate?.let { Date().after(it) } ?: false + val expired = this.collection.endDate?.let { Date().after(it) } ?: false return expired && this.name != settings.currentWallpaper } private fun Wallpaper.isAvailableInLocale(): Boolean = - this.availableLocales?.contains(currentLocale) ?: true + this.collection.availableLocales?.contains(currentLocale) ?: true companion object { + private val firefoxClassicCollection = Wallpaper.Collection( + name = Wallpaper.firefoxCollectionName, + availableLocales = null, + startDate = null, + endDate = null, + learnMoreUrl = null + ) private val localWallpapers: List = listOf( Wallpaper( name = Wallpaper.amethystName, - collectionName = Wallpaper.firefoxCollectionName, - availableLocales = null, - startDate = null, - endDate = null, + collection = firefoxClassicCollection, + textColor = null, + cardColor = null, ), Wallpaper( name = Wallpaper.ceruleanName, - collectionName = Wallpaper.firefoxCollectionName, - availableLocales = null, - startDate = null, - endDate = null, + collection = firefoxClassicCollection, + textColor = null, + cardColor = null, ), Wallpaper( name = Wallpaper.sunriseName, - collectionName = Wallpaper.firefoxCollectionName, - availableLocales = null, - startDate = null, - endDate = null, + collection = firefoxClassicCollection, + textColor = null, + cardColor = null, ), ) private val remoteWallpapers: List = listOf( Wallpaper( name = Wallpaper.twilightHillsName, - collectionName = Wallpaper.firefoxCollectionName, - availableLocales = null, - startDate = null, - endDate = null, + collection = firefoxClassicCollection, + textColor = null, + cardColor = null, ), Wallpaper( name = Wallpaper.beachVibeName, - collectionName = Wallpaper.firefoxCollectionName, - availableLocales = null, - startDate = null, - endDate = null, + collection = firefoxClassicCollection, + textColor = null, + cardColor = null, ), ) val allWallpapers = listOf(Wallpaper.Default) + localWallpapers + remoteWallpapers @@ -278,7 +280,7 @@ class WallpapersUseCases( Wallpapers.wallpaperSelected.record( Wallpapers.WallpaperSelectedExtra( name = wallpaper.name, - themeCollection = wallpaper.collectionName + themeCollection = wallpaper.collection.name ) ) } diff --git a/app/src/test/java/org/mozilla/fenix/wallpapers/WallpaperFileManagerTest.kt b/app/src/test/java/org/mozilla/fenix/wallpapers/WallpaperFileManagerTest.kt index f9ecbd7b9187..5971c0da718b 100644 --- a/app/src/test/java/org/mozilla/fenix/wallpapers/WallpaperFileManagerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/wallpapers/WallpaperFileManagerTest.kt @@ -96,9 +96,14 @@ class WallpaperFileManagerTest { private fun generateWallpaper(name: String) = Wallpaper( name = name, - collectionName = "", - availableLocales = null, - startDate = null, - endDate = null + textColor = null, + cardColor = null, + collection = Wallpaper.Collection( + name = Wallpaper.defaultName, + availableLocales = null, + startDate = null, + endDate = null, + learnMoreUrl = null + ), ) } diff --git a/app/src/test/java/org/mozilla/fenix/wallpapers/WallpaperMetadataFetcherTest.kt b/app/src/test/java/org/mozilla/fenix/wallpapers/WallpaperMetadataFetcherTest.kt new file mode 100644 index 000000000000..c704ea0df7eb --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/wallpapers/WallpaperMetadataFetcherTest.kt @@ -0,0 +1,306 @@ +package org.mozilla.fenix.wallpapers + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import mozilla.components.concept.fetch.Client +import mozilla.components.concept.fetch.Request +import mozilla.components.concept.fetch.Response +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.BuildConfig +import org.mozilla.fenix.wallpapers.WallpaperMetadataFetcher.Companion.currentJsonVersion +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale + +@RunWith(AndroidJUnit4::class) +class WallpaperMetadataFetcherTest { + + private val expectedRequest = Request( + url = BuildConfig.WALLPAPER_URL.substringBefore("android") + + "metadata/v$currentJsonVersion/wallpapers.json", + method = Request.Method.GET + ) + private val mockResponse = mockk() + private val mockClient = mockk { + every { fetch(expectedRequest) } returns mockResponse + } + + private lateinit var metadataFetcher: WallpaperMetadataFetcher + + @Before + fun setup() { + metadataFetcher = WallpaperMetadataFetcher(mockClient) + } + + @Test + fun `GIVEN wallpaper metadata WHEN parsed THEN wallpapers have correct ids, text and card colors`() = runTest { + val json = """ + { + "last-updated-date": "2022-01-01", + "collections": [ + { + "id": "classic-firefox", + "available-locales": null, + "availability-range": null, + "wallpapers": [ + { + "id": "beach-vibes", + "text-color": "FBFBFE", + "card-color": "15141A" + }, + { + "id": "sunrise", + "text-color": "15141A", + "card-color": "FBFBFE" + } + ] + } + ] + } + """.trimIndent() + every { mockResponse.body } returns Response.Body(json.byteInputStream()) + + val wallpapers = metadataFetcher.downloadWallpaperList() + + with(wallpapers[0]) { + assertEquals(0xFFFBFBFE, textColor) + assertEquals(0xFF15141A, cardColor) + } + with(wallpapers[1]) { + assertEquals(0xFF15141A, textColor) + assertEquals(0xFFFBFBFE, cardColor) + } + } + + @Test + fun `GIVEN wallpaper metadata is missing an id WHEN parsed THEN parsing fails`() = runTest { + val json = """ + { + "last-updated-date": "2022-01-01", + "collections": [ + { + "id": "classic-firefox", + "available-locales": null, + "availability-range": null, + "wallpapers": [ + { + "text-color": "FBFBFE", + "card-color": "15141A" + }, + { + "id": "sunrise", + "text-color": "15141A", + "card-color": "FBFBFE" + } + ] + } + ] + } + """.trimIndent() + every { mockResponse.body } returns Response.Body(json.byteInputStream()) + + val wallpapers = metadataFetcher.downloadWallpaperList() + + assertTrue(wallpapers.isEmpty()) + } + + @Test + fun `GIVEN wallpaper metadata is missing a text color WHEN parsed THEN parsing fails`() = runTest { + val json = """ + { + "last-updated-date": "2022-01-01", + "collections": [ + { + "id": "classic-firefox", + "available-locales": null, + "availability-range": null, + "wallpapers": [ + { + "id": "beach-vibes", + "card-color": "15141A" + }, + { + "id": "sunrise", + "text-color": "15141A", + "card-color": "FBFBFE" + } + ] + } + ] + } + """.trimIndent() + every { mockResponse.body } returns Response.Body(json.byteInputStream()) + + val wallpapers = metadataFetcher.downloadWallpaperList() + + assertTrue(wallpapers.isEmpty()) + } + + @Test + fun `GIVEN wallpaper metadata is missing a card color WHEN parsed THEN parsing fails`() = runTest { + val json = """ + { + "last-updated-date": "2022-01-01", + "collections": [ + { + "id": "classic-firefox", + "available-locales": null, + "availability-range": null, + "wallpapers": [ + { + "id": "beach-vibes", + "text-color": "FBFBFE", + }, + { + "id": "sunrise", + "text-color": "15141A", + "card-color": "FBFBFE" + } + ] + } + ] + } + """.trimIndent() + every { mockResponse.body } returns Response.Body(json.byteInputStream()) + + val wallpapers = metadataFetcher.downloadWallpaperList() + + assertTrue(wallpapers.isEmpty()) + } + + @Test + fun `GIVEN collection with specified locales WHEN parsed THEN wallpapers includes locales`() = runTest { + val locales = listOf("en-US", "es-US", "en-CA", "fr-CA") + val json = """ + { + "last-updated-date": "2022-01-01", + "collections": [ + { + "id": "classic-firefox", + "available-locales": ["en-US", "es-US", "en-CA", "fr-CA"], + "availability-range": null, + "wallpapers": [ + { + "id": "beach-vibes", + "text-color": "FBFBFE", + "card-color": "15141A" + }, + { + "id": "sunrise", + "text-color": "15141A", + "card-color": "FBFBFE" + } + ] + } + ] + } + """.trimIndent() + every { mockResponse.body } returns Response.Body(json.byteInputStream()) + + val wallpapers = metadataFetcher.downloadWallpaperList() + + assertTrue(wallpapers.isNotEmpty()) + assertTrue( + wallpapers.all { + it.collection.availableLocales == locales + } + ) + } + + @Test + fun `GIVEN collection with specified date range WHEN parsed THEN wallpapers includes dates`() = runTest { + val calendar = Calendar.getInstance() + val startDate = calendar.run { + set(2022, Calendar.JUNE, 27) + time + } + val endDate = calendar.run { + set(2022, Calendar.SEPTEMBER, 30) + time + } + val json = """ + { + "last-updated-date": "2022-01-01", + "collections": [ + { + "id": "classic-firefox", + "available-locales": null, + "availability-range": { + "start": "2022-06-27", + "end": "2022-09-30" + }, + "wallpapers": [ + { + "id": "beach-vibes", + "text-color": "FBFBFE", + "card-color": "15141A" + }, + { + "id": "sunrise", + "text-color": "15141A", + "card-color": "FBFBFE" + } + ] + } + ] + } + """.trimIndent() + every { mockResponse.body } returns Response.Body(json.byteInputStream()) + + val wallpapers = metadataFetcher.downloadWallpaperList() + + assertTrue(wallpapers.isNotEmpty()) + assertTrue( + wallpapers.all { + val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US) + formatter.format(startDate) == formatter.format(it.collection.startDate!!) && + formatter.format(endDate) == formatter.format(it.collection.endDate!!) + } + ) + } + + @Test + fun `GIVEN collection with specified learn more url WHEN parsed THEN wallpapers includes url`() = runTest { + val json = """ + { + "last-updated-date": "2022-01-01", + "collections": [ + { + "id": "classic-firefox", + "available-locales": null, + "availability-range": null, + "learn-more-url": "https://www.mozilla.org", + "wallpapers": [ + { + "id": "beach-vibes", + "text-color": "FBFBFE", + "card-color": "15141A" + }, + { + "id": "sunrise", + "text-color": "15141A", + "card-color": "FBFBFE" + } + ] + } + ] + } + """.trimIndent() + every { mockResponse.body } returns Response.Body(json.byteInputStream()) + + val wallpapers = metadataFetcher.downloadWallpaperList() + + assertTrue(wallpapers.isNotEmpty()) + assertTrue( + wallpapers.all { + it.collection.learnMoreUrl == "https://www.mozilla.org" + } + ) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/wallpapers/WallpapersUseCasesTest.kt b/app/src/test/java/org/mozilla/fenix/wallpapers/WallpapersUseCasesTest.kt index 4ca5788d4b34..ff985126e084 100644 --- a/app/src/test/java/org/mozilla/fenix/wallpapers/WallpapersUseCasesTest.kt +++ b/app/src/test/java/org/mozilla/fenix/wallpapers/WallpapersUseCasesTest.kt @@ -266,18 +266,28 @@ class WallpapersUseCasesTest { return if (isInPromo) { Wallpaper( name = name, - collectionName = "", - availableLocales = listOf("en-US"), - startDate = null, - endDate = relativeTime + collection = Wallpaper.Collection( + name = Wallpaper.firefoxCollectionName, + availableLocales = listOf("en-US"), + startDate = null, + endDate = relativeTime, + learnMoreUrl = null + ), + textColor = null, + cardColor = null, ) } else { Wallpaper( name = name, - collectionName = "", - availableLocales = null, - startDate = null, - endDate = relativeTime + collection = Wallpaper.Collection( + name = Wallpaper.firefoxCollectionName, + availableLocales = null, + startDate = null, + endDate = relativeTime, + learnMoreUrl = null + ), + textColor = null, + cardColor = null, ) } }