* closes #23504: download focus wallpapers at runtime * address pr feedback * only download wallpapers if feature flag is set
This commit is contained in:
parent
9b9fab40da
commit
9dc0506ec2
|
@ -84,6 +84,7 @@ gen-external-apklibs
|
|||
.sentry_token
|
||||
.mls_token
|
||||
.nimbus
|
||||
.wallpaper_url
|
||||
|
||||
# Python Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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([{
|
||||
|
|
Loading…
Reference in New Issue