closes #23504: download focus wallpapers at runtime (#23505)

* closes #23504: download focus wallpapers at runtime

* address pr feedback

* only download wallpapers if feature flag is set
This commit is contained in:
Matthew Tighe 2022-02-01 13:45:55 -08:00 committed by GitHub
parent 9b9fab40da
commit 9dc0506ec2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 249 additions and 28 deletions

1
.gitignore vendored
View File

@ -84,6 +84,7 @@ gen-external-apklibs
.sentry_token
.mls_token
.nimbus
.wallpaper_url
# Python Byte-compiled / optimized / DLL files
__pycache__/

View File

@ -376,6 +376,21 @@ android.applicationVariants.all { variant ->
} else {
buildConfigField 'Boolean', 'MOZILLA_OFFICIAL', 'false'
}
// -------------------------------------------------------------------------------------------------
// BuildConfig: Set remote wallpaper URL using local file if it exists
// -------------------------------------------------------------------------------------------------
print("Wallpaper URL: ")
try {
def token = new File("${rootDir}/.wallpaper_url").text.trim()
buildConfigField 'String', 'WALLPAPER_URL', '"' + token + '"'
println "(Added from .wallpaper_url file)"
} catch (FileNotFoundException ignored) {
buildConfigField 'String', 'WALLPAPER_URL', '""'
println("--")
}
}
// Generate Kotlin code for the Fenix Glean metrics.

View File

@ -127,6 +127,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
setupInMainProcessOnly()
downloadWallpapers()
// DO NOT MOVE ANYTHING BELOW THIS stop CALL.
PerfStartup.applicationOnCreate.stopAndAccumulate(completeMethodDurationTimerId)
}
@ -781,4 +782,13 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
}
override fun getWorkManagerConfiguration() = Builder().setMinimumLoggingLevel(INFO).build()
@OptIn(DelicateCoroutinesApi::class)
private fun downloadWallpapers() {
if (FeatureFlags.showWallpapers) {
GlobalScope.launch {
components.wallpaperManager.downloadAllRemoteWallpapers()
}
}
}
}

View File

@ -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.WallpaperDownloader
import org.mozilla.fenix.wallpapers.WallpaperManager
import org.mozilla.fenix.wallpapers.WallpapersAssetsStorage
import org.mozilla.fenix.wifi.WifiConnectionMonitor
@ -145,7 +146,12 @@ class Components(private val context: Context) {
}
val wallpaperManager by lazyMonitored {
WallpaperManager(settings, WallpapersAssetsStorage(context))
WallpaperManager(
settings,
WallpapersAssetsStorage(context),
WallpaperDownloader(context, core.client, analytics.crashReporter),
analytics.crashReporter,
)
}
val analytics by lazyMonitored { Analytics(context) }

View File

@ -70,7 +70,7 @@ import java.util.Locale
fun WallpaperSettings(
wallpapers: List<Wallpaper>,
defaultWallpaper: Wallpaper,
loadWallpaperResource: (Wallpaper) -> Bitmap,
loadWallpaperResource: (Wallpaper) -> Bitmap?,
selectedWallpaper: Wallpaper,
onSelectWallpaper: (Wallpaper) -> Unit,
onViewWallpaper: () -> Unit,
@ -163,7 +163,7 @@ private fun WallpaperSnackbar(
private fun WallpaperThumbnails(
wallpapers: List<Wallpaper>,
defaultWallpaper: Wallpaper,
loadWallpaperResource: (Wallpaper) -> Bitmap,
loadWallpaperResource: (Wallpaper) -> Bitmap?,
selectedWallpaper: Wallpaper,
numColumns: Int = 3,
onSelectWallpaper: (Wallpaper) -> Unit,
@ -199,7 +199,7 @@ private fun WallpaperThumbnails(
private fun WallpaperThumbnailItem(
wallpaper: Wallpaper,
defaultWallpaper: Wallpaper,
loadWallpaperResource: (Wallpaper) -> Bitmap,
loadWallpaperResource: (Wallpaper) -> Bitmap?,
isSelected: Boolean,
aspectRatio: Float = 1.1f,
onSelect: (Wallpaper) -> Unit
@ -214,6 +214,9 @@ private fun WallpaperThumbnailItem(
Modifier
}
val bitmap = loadWallpaperResource(wallpaper)
// Completely avoid drawing the item if a bitmap cannot be loaded and is required
if (bitmap == null && wallpaper != defaultWallpaper) return
Surface(
elevation = 4.dp,
shape = thumbnailShape,
@ -225,15 +228,14 @@ private fun WallpaperThumbnailItem(
.then(border)
.clickable { onSelect(wallpaper) }
) {
if (wallpaper != defaultWallpaper) {
val contentDescription = stringResource(
R.string.wallpapers_item_name_content_description, wallpaper.name
)
if (bitmap != null) {
Image(
bitmap = loadWallpaperResource(wallpaper).asImageBitmap(),
bitmap = bitmap.asImageBitmap(),
contentScale = ContentScale.FillBounds,
contentDescription = contentDescription,
modifier = Modifier.fillMaxSize()
contentDescription = stringResource(
R.string.wallpapers_item_name_content_description, wallpaper.name
),
modifier = Modifier.fillMaxSize(),
)
}
}
@ -288,7 +290,7 @@ private fun WallpaperThumbnailsPreview() {
WallpaperSettings(
defaultWallpaper = WallpaperManager.defaultWallpaper,
loadWallpaperResource = {
wallpaperManager.loadWallpaperFromAssets(it, context)
wallpaperManager.loadSavedWallpaper(context, it)
},
wallpapers = wallpaperManager.availableWallpapers,
selectedWallpaper = wallpaperManager.currentWallpaper,

View File

@ -52,7 +52,7 @@ class WallpaperSettingsFragment : Fragment() {
wallpapers = wallpaperManager.availableWallpapers,
defaultWallpaper = WallpaperManager.defaultWallpaper,
loadWallpaperResource = {
wallpaperManager.loadWallpaperFromAssets(it, requireContext())
wallpaperManager.loadSavedWallpaper(requireContext(), it)
},
selectedWallpaper = currentWallpaper,
onSelectWallpaper = { selectedWallpaper: Wallpaper ->

View File

@ -4,6 +4,9 @@
package org.mozilla.fenix.wallpapers
import android.content.Context
import android.content.res.Configuration
/**
* A class that represents an available wallpaper and its state.
* @property name Indicates the name of this wallpaper.
@ -23,7 +26,52 @@ data class Wallpaper(
/**
* A type hierarchy representing the different theme collections [Wallpaper]s belong to.
*/
sealed class WallpaperThemeCollection {
object None : WallpaperThemeCollection()
object Firefox : WallpaperThemeCollection()
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,
}
/**
* 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
}

View File

@ -0,0 +1,86 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.wallpapers
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import mozilla.components.concept.fetch.Client
import mozilla.components.concept.fetch.Request
import mozilla.components.concept.fetch.isSuccess
import mozilla.components.lib.crash.CrashReporter
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.BuildConfig
import java.io.File
/**
* Can download wallpapers from a remote host.
*
* @param context Required for writing files to local storage.
* @param client Required for fetching files from network.
*/
class WallpaperDownloader(
private val context: Context,
private val client: Client,
private val crashReporter: CrashReporter,
) {
private val logger = Logger("WallpaperDownloader")
private val remoteHost = BuildConfig.WALLPAPER_URL
/**
* Downloads a wallpaper from the network. Will try to fetch 4 versions of each wallpaper:
* portrait/light - portrait/dark - landscape/light - landscape/dark. These are expected to be
* found at a remote path in the form:
* <WALLPAPER_URL>/<resolution>/<orientation>/<app theme>/<wallpaper theme>/<wallpaper name>.png
*/
suspend fun downloadWallpaper(wallpaper: Wallpaper) = withContext(Dispatchers.IO) {
for (metadata in wallpaper.toMetadata()) {
val localFile = File(context.filesDir.absolutePath, metadata.localPath)
if (localFile.exists()) continue
val request = Request(
url = "$remoteHost/${metadata.remotePath}",
method = Request.Method.GET
)
Result.runCatching {
client.fetch(request)
}.onSuccess {
if (!it.isSuccess) {
logger.error("Download response failure code: ${it.status}")
return@withContext
}
File(localFile.path.substringBeforeLast("/")).mkdirs()
it.body.useStream { input ->
input.copyTo(localFile.outputStream())
}
}.onFailure {
logger.error(it.message ?: "Download failed: no throwable message included.", it)
crashReporter.submitCaughtException(it)
}
}
}
private data class WallpaperMetadata(val remotePath: String, val localPath: String)
private fun Wallpaper.toMetadata(): List<WallpaperMetadata> = 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)
}
}
}
}
@Suppress("MagicNumber")
private fun Context.resolutionSegment(): String = when (resources.displayMetrics.densityDpi) {
// targeting hdpi and greater density resolutions https://developer.android.com/training/multiscreen/screendensities
in 0..240 -> "low"
in 240..320 -> "medium"
else -> "high"
}
}

View File

@ -14,10 +14,15 @@ import android.graphics.drawable.BitmapDrawable
import android.os.Handler
import android.os.Looper
import android.view.View
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import mozilla.components.lib.crash.CrashReporter
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.android.content.getColorFromAttr
import org.mozilla.fenix.R
import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.utils.Settings
import java.io.File
/**
* Provides access to available wallpapers and manages their states.
@ -26,9 +31,20 @@ import org.mozilla.fenix.utils.Settings
class WallpaperManager(
private val settings: Settings,
private val wallpaperStorage: WallpaperStorage,
private val downloader: WallpaperDownloader,
private val crashReporter: CrashReporter,
) {
val logger = Logger("WallpaperManager")
var availableWallpapers: List<Wallpaper> = loadWallpapers()
private val remoteWallpapers = listOf(
Wallpaper(
"focus",
portraitPath = "",
landscapePath = "",
isDark = false,
themeCollection = WallpaperThemeCollection.FOCUS
),
)
var availableWallpapers: List<Wallpaper> = loadWallpapers() + remoteWallpapers
private set
var currentWallpaper: Wallpaper = getCurrentWallpaperFromSettings()
@ -46,13 +62,29 @@ class WallpaperManager(
wallpaperContainer.setBackgroundColor(context.getColorFromAttr(DEFAULT_RESOURCE))
logger.info("Wallpaper update to default background")
} else {
logger.info("Wallpaper update to ${newWallpaper.name}")
val bitmap = loadWallpaperFromAssets(newWallpaper, context)
wallpaperContainer.background = BitmapDrawable(context.resources, bitmap)
val bitmap = loadSavedWallpaper(context, newWallpaper)
if (bitmap == null) {
val message = "Could not load wallpaper bitmap. Resetting to default."
logger.error(message)
crashReporter.submitCaughtException(NullPointerException(message))
wallpaperContainer.setBackgroundColor(context.getColorFromAttr(DEFAULT_RESOURCE))
currentWallpaper = defaultWallpaper
} else {
wallpaperContainer.background = BitmapDrawable(context.resources, bitmap)
}
}
currentWallpaper = newWallpaper
}
/**
* Download all known remote wallpapers.
*/
suspend fun downloadAllRemoteWallpapers() {
for (wallpaper in remoteWallpapers) {
downloader.downloadWallpaper(wallpaper)
}
}
/**
* Returns the next available [Wallpaper], the [currentWallpaper] is the last one then
* the first available [Wallpaper] will be returned.
@ -77,16 +109,36 @@ class WallpaperManager(
}
}
fun loadWallpaperFromAssets(wallpaper: Wallpaper, context: Context): Bitmap {
/**
* Load a wallpaper that is saved locally.
*/
fun loadSavedWallpaper(context: Context, wallpaper: Wallpaper): Bitmap? =
if (wallpaper.themeCollection.origin == WallpaperOrigin.LOCAL) {
loadWallpaperFromAssets(context, wallpaper)
} else {
loadWallpaperFromDisk(context, wallpaper)
}
private fun loadWallpaperFromAssets(context: Context, wallpaper: Wallpaper): Bitmap? = Result.runCatching {
val path = if (isLandscape(context)) {
wallpaper.landscapePath
} else {
wallpaper.portraitPath
}
return context.assets.open(path).use {
context.assets.open(path).use {
BitmapFactory.decodeStream(it)
}
}
}.getOrNull()
private fun loadWallpaperFromDisk(context: Context, wallpaper: Wallpaper): Bitmap? = Result.runCatching {
val path = wallpaper.getLocalPathFromContext(context)
runBlockingIncrement {
withContext(Dispatchers.IO) {
val file = File(context.filesDir, path)
BitmapFactory.decodeStream(file.inputStream())
}
}
}.getOrNull()
private fun isLandscape(context: Context): Boolean {
return context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
@ -141,7 +193,7 @@ class WallpaperManager(
portraitPath = "",
landscapePath = "",
isDark = false,
themeCollection = WallpaperThemeCollection.None
themeCollection = WallpaperThemeCollection.NONE
)
private const val ANIMATION_DELAY_MS = 1500L
}

View File

@ -42,10 +42,10 @@ class WallpapersAssetsStorage(private val context: Context) : WallpaperStorage {
isDark = getBoolean("isDark"),
themeCollection = Result.runCatching {
when (getString("themeCollection")) {
"firefox" -> WallpaperThemeCollection.Firefox
else -> WallpaperThemeCollection.None
"firefox" -> WallpaperThemeCollection.FIREFOX
else -> WallpaperThemeCollection.NONE
}
}.getOrDefault(WallpaperThemeCollection.None)
}.getOrDefault(WallpaperThemeCollection.NONE)
)
} catch (e: JSONException) {
logger.error("unable to parse json for wallpaper $this", e)

View File

@ -27,7 +27,7 @@ class WallpaperManagerTest {
every { mockSettings.currentWallpaper = capture(currentCaptureSlot) } just runs
val updatedWallpaper = WallpaperManager.defaultWallpaper
val wallpaperManager = WallpaperManager(mockSettings, mockStorage)
val wallpaperManager = WallpaperManager(mockSettings, mockStorage, mockk(), mockk())
wallpaperManager.currentWallpaper = updatedWallpaper
assertEquals(updatedWallpaper.name, currentCaptureSlot.captured)

View File

@ -47,6 +47,7 @@ def add_shippable_secrets(config, tasks):
('sentry_dsn', '.sentry_token'),
('mls', '.mls_token'),
('nimbus_url', '.nimbus'),
('wallpaper_url', ".wallpaper_url")
)])
else:
dummy_secrets.extend([{