Merge branch 'releases_v114' into mergify/bp/releases_v114/pr-1906
@ -0,0 +1,74 @@
|
||||
package org.mozilla.fenix.components.metrics
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import mozilla.components.support.utils.ext.getPackageInfoCompat
|
||||
import org.mozilla.fenix.ext.metrics
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Worker that will send the User Activated event at the end of the first week.
|
||||
*/
|
||||
class GrowthDataWorker(
|
||||
context: Context,
|
||||
workerParameters: WorkerParameters,
|
||||
) : Worker(context, workerParameters) {
|
||||
|
||||
override fun doWork(): Result {
|
||||
val settings = applicationContext.settings()
|
||||
|
||||
if (!System.currentTimeMillis().isAfterFirstWeekFromInstall(applicationContext) ||
|
||||
settings.growthUserActivatedSent
|
||||
) {
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
applicationContext.metrics.track(Event.GrowthData.UserActivated(fromSearch = false))
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val GROWTH_USER_ACTIVATED_WORK_NAME = "org.mozilla.fenix.growth.work"
|
||||
private const val DAY_MILLIS: Long = 1000 * 60 * 60 * 24
|
||||
private const val FULL_WEEK_MILLIS: Long = DAY_MILLIS * 7
|
||||
|
||||
/**
|
||||
* Schedules the Activated User event if needed.
|
||||
*/
|
||||
fun sendActivatedSignalIfNeeded(context: Context) {
|
||||
val instanceWorkManager = WorkManager.getInstance(context)
|
||||
|
||||
if (context.settings().growthUserActivatedSent) {
|
||||
return
|
||||
}
|
||||
|
||||
val growthSignalWork = OneTimeWorkRequest.Builder(GrowthDataWorker::class.java)
|
||||
.setInitialDelay(FULL_WEEK_MILLIS, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
|
||||
instanceWorkManager.beginUniqueWork(
|
||||
GROWTH_USER_ACTIVATED_WORK_NAME,
|
||||
ExistingWorkPolicy.KEEP,
|
||||
growthSignalWork,
|
||||
).enqueue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [Boolean] value signaling if current time is after the first week after install.
|
||||
*/
|
||||
private fun Long.isAfterFirstWeekFromInstall(context: Context): Boolean {
|
||||
val timeDifference = this - getInstalledTime(context)
|
||||
return (FULL_WEEK_MILLIS <= timeDifference)
|
||||
}
|
||||
|
||||
private fun getInstalledTime(context: Context): Long = context.packageManager
|
||||
.getPackageInfoCompat(context.packageName, 0)
|
||||
.firstInstallTime
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
/* 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.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 LegacyWallpaperDownloader(
|
||||
private val context: Context,
|
||||
private val client: Client,
|
||||
) {
|
||||
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) {
|
||||
if (remoteHost.isNullOrEmpty()) {
|
||||
return@withContext
|
||||
}
|
||||
|
||||
for (metadata in wallpaper.toMetadata(context)) {
|
||||
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 {
|
||||
val response = client.fetch(request)
|
||||
if (!response.isSuccess) {
|
||||
logger.error("Download response failure code: ${response.status}")
|
||||
return@withContext
|
||||
}
|
||||
File(localFile.path.substringBeforeLast("/")).mkdirs()
|
||||
response.body.useStream { input ->
|
||||
input.copyTo(localFile.outputStream())
|
||||
}
|
||||
}.onFailure {
|
||||
Result.runCatching {
|
||||
if (localFile.exists()) {
|
||||
localFile.delete()
|
||||
}
|
||||
}.onFailure { e ->
|
||||
logger.error("Failed to delete stale wallpaper bitmaps while downloading", e)
|
||||
}
|
||||
|
||||
logger.error(it.message ?: "Download failed: no throwable message included.", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class WallpaperMetadata(val remotePath: String, val localPath: String)
|
||||
|
||||
private fun Wallpaper.toMetadata(context: Context): List<WallpaperMetadata> =
|
||||
listOf("landscape", "portrait").flatMap { orientation ->
|
||||
listOf("light", "dark").map { theme ->
|
||||
val localPath = "wallpapers/$orientation/$theme/$name.png"
|
||||
val remotePath = "${context.resolutionSegment()}/" +
|
||||
"$orientation/" +
|
||||
"$theme/" +
|
||||
"${collection.name}/" +
|
||||
"$name.png"
|
||||
WallpaperMetadata(remotePath, localPath)
|
||||
}
|
||||
}
|
||||
|
||||
@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"
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
/* 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 kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Manages various functions related to the locally-stored wallpaper assets.
|
||||
*
|
||||
* @property rootDirectory The top level app-local storage directory.
|
||||
* @param coroutineDispatcher Dispatcher used to execute suspending functions. Default parameter
|
||||
* should be likely be used except for when under test.
|
||||
*/
|
||||
class LegacyWallpaperFileManager(
|
||||
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
|
||||
*/
|
||||
suspend fun lookupExpiredWallpaper(name: String): Wallpaper? = withContext(Dispatchers.IO) {
|
||||
if (getAllLocalWallpaperPaths(name).all { File(rootDirectory, it).exists() }) {
|
||||
Wallpaper(
|
||||
name = name,
|
||||
collection = Wallpaper.DefaultCollection,
|
||||
textColor = null,
|
||||
cardColorLight = null,
|
||||
cardColorDark = null,
|
||||
thumbnailFileState = Wallpaper.ImageFileState.Unavailable,
|
||||
assetsFileState = Wallpaper.ImageFileState.Downloaded,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAllLocalWallpaperPaths(name: String): List<String> =
|
||||
listOf("landscape", "portrait").flatMap { orientation ->
|
||||
listOf("light", "dark").map { theme ->
|
||||
Wallpaper.legacyGetLocalPath(orientation, theme, name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all wallpapers that are not the [currentWallpaper] or in [availableWallpapers].
|
||||
*/
|
||||
fun clean(currentWallpaper: Wallpaper, availableWallpapers: List<Wallpaper>) {
|
||||
scope.launch {
|
||||
val wallpapersToKeep = (listOf(currentWallpaper) + availableWallpapers).map { it.name }
|
||||
cleanChildren(portraitDirectory, wallpapersToKeep)
|
||||
cleanChildren(landscapeDirectory, wallpapersToKeep)
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanChildren(dir: File, wallpapersToKeep: List<String>) {
|
||||
for (file in dir.walkTopDown()) {
|
||||
if (file.isDirectory || file.nameWithoutExtension in wallpapersToKeep) continue
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 93 KiB |
Before Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 91 KiB |
Before Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 122 KiB |
Before Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 162 KiB |
Before Width: | Height: | Size: 152 KiB |
Before Width: | Height: | Size: 124 KiB |
Before Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 130 KiB |
Before Width: | Height: | Size: 166 KiB |
Before Width: | Height: | Size: 153 KiB |
Before Width: | Height: | Size: 335 KiB |
Before Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 123 KiB |
Before Width: | Height: | Size: 91 KiB |
Before Width: | Height: | Size: 158 KiB |
Before Width: | Height: | Size: 151 KiB |
Before Width: | Height: | Size: 126 KiB |
Before Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 97 KiB |
Before Width: | Height: | Size: 161 KiB |
Before Width: | Height: | Size: 152 KiB |
Before Width: | Height: | Size: 269 KiB |
Before Width: | Height: | Size: 126 KiB |
Before Width: | Height: | Size: 161 KiB |
@ -1,114 +0,0 @@
|
||||
/* 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.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
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 LegacyWallpaperFileManagerTest {
|
||||
@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 = UnconfinedTestDispatcher()
|
||||
|
||||
private lateinit var fileManager: LegacyWallpaperFileManager
|
||||
|
||||
@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 = LegacyWallpaperFileManager(
|
||||
rootDirectory = tempFolder.root,
|
||||
coroutineDispatcher = dispatcher,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN files exist in all directories WHEN expired wallpaper looked up THEN expired wallpaper returned`() = runTest {
|
||||
val wallpaperName = "name"
|
||||
createAllFiles(wallpaperName)
|
||||
|
||||
val result = fileManager.lookupExpiredWallpaper(wallpaperName)
|
||||
|
||||
val expected = generateWallpaper(name = wallpaperName)
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN any missing file in directories WHEN expired wallpaper looked up THEN null returned`() = runTest {
|
||||
val wallpaperName = "name"
|
||||
File(landscapeLightFolder, "$wallpaperName.png").createNewFile()
|
||||
File(landscapeDarkFolder, "$wallpaperName.png").createNewFile()
|
||||
|
||||
val result = fileManager.lookupExpiredWallpaper(wallpaperName)
|
||||
|
||||
assertEquals(null, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN cleaned THEN current wallpaper and available wallpapers kept`() {
|
||||
val currentName = "current"
|
||||
val currentWallpaper = generateWallpaper(name = currentName)
|
||||
val availableName = "available"
|
||||
val available = generateWallpaper(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<File> {
|
||||
return listOf(
|
||||
File(portraitLightFolder, "$name.png"),
|
||||
File(portraitDarkFolder, "$name.png"),
|
||||
File(landscapeLightFolder, "$name.png"),
|
||||
File(landscapeDarkFolder, "$name.png"),
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateWallpaper(name: String) = Wallpaper(
|
||||
name = name,
|
||||
textColor = null,
|
||||
cardColorLight = null,
|
||||
cardColorDark = null,
|
||||
thumbnailFileState = Wallpaper.ImageFileState.Unavailable,
|
||||
assetsFileState = Wallpaper.ImageFileState.Downloaded,
|
||||
collection = Wallpaper.Collection(
|
||||
name = Wallpaper.defaultName,
|
||||
heading = null,
|
||||
description = null,
|
||||
availableLocales = null,
|
||||
startDate = null,
|
||||
endDate = null,
|
||||
learnMoreUrl = null,
|
||||
),
|
||||
)
|
||||
}
|