fenix/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperManager.kt

267 lines
9.7 KiB
Kotlin

/* 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.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.ImageView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.R
import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.utils.Settings
import java.io.File
import java.util.Date
/**
* Provides access to available wallpapers and manages their states.
*/
@Suppress("TooManyFunctions")
class WallpaperManager(
private val settings: Settings,
private val downloader: WallpaperDownloader,
private val fileManager: WallpaperFileManager,
private val currentLocale: String,
allWallpapers: List<Wallpaper> = availableWallpapers
) {
val logger = Logger("WallpaperManager")
val wallpapers = allWallpapers
.filter(::filterExpiredRemoteWallpapers)
.filter(::filterPromotionalWallpapers)
var currentWallpaper: Wallpaper = getCurrentWallpaperFromSettings()
set(value) {
settings.currentWallpaper = value.name
field = value
}
init {
fileManager.clean(currentWallpaper, wallpapers.filterIsInstance<Wallpaper.Remote>())
}
/**
* Apply the [newWallpaper] into the [wallpaperContainer] and update the [currentWallpaper].
*/
fun updateWallpaper(wallpaperContainer: ImageView, newWallpaper: Wallpaper) {
val context = wallpaperContainer.context
if (newWallpaper == defaultWallpaper) {
wallpaperContainer.visibility = View.GONE
logger.info("Wallpaper update to default background")
} else {
val bitmap = loadSavedWallpaper(context, newWallpaper)
if (bitmap == null) {
val message = "Could not load wallpaper bitmap. Resetting to default."
logger.error(message)
currentWallpaper = defaultWallpaper
wallpaperContainer.visibility = View.GONE
return
} else {
wallpaperContainer.visibility = View.VISIBLE
scaleBitmapToBottom(bitmap, wallpaperContainer)
}
}
currentWallpaper = newWallpaper
}
/**
* Download all known remote wallpapers.
*/
suspend fun downloadAllRemoteWallpapers() {
for (wallpaper in wallpapers.filterIsInstance<Wallpaper.Remote>()) {
downloader.downloadWallpaper(wallpaper)
}
}
/**
* Returns the next available [Wallpaper], the [currentWallpaper] is the last one then
* the first available [Wallpaper] will be returned.
*/
fun switchToNextWallpaper(): Wallpaper {
val values = wallpapers
val index = values.indexOf(currentWallpaper) + 1
return if (index >= values.size) {
values.first()
} else {
values[index]
}
}
private fun filterExpiredRemoteWallpapers(wallpaper: Wallpaper): Boolean = when (wallpaper) {
is Wallpaper.Remote -> {
val notExpired = wallpaper.expirationDate?.let { Date().before(it) } ?: true
notExpired || wallpaper.name == settings.currentWallpaper
}
else -> true
}
private fun filterPromotionalWallpapers(wallpaper: Wallpaper): Boolean =
if (wallpaper is Wallpaper.Promotional) {
wallpaper.isAvailableInLocale(currentLocale)
} else {
true
}
private fun getCurrentWallpaperFromSettings(): Wallpaper {
val currentWallpaper = settings.currentWallpaper
return if (currentWallpaper.isEmpty()) {
defaultWallpaper
} else {
wallpapers.find { it.name == currentWallpaper }
?: fileManager.lookupExpiredWallpaper(currentWallpaper)
?: defaultWallpaper
}
}
/**
* Load a wallpaper that is saved locally.
*/
fun loadSavedWallpaper(context: Context, wallpaper: Wallpaper): Bitmap? =
when (wallpaper) {
is Wallpaper.Local -> loadWallpaperFromDrawables(context, wallpaper)
is Wallpaper.Remote -> loadWallpaperFromDisk(context, wallpaper)
else -> null
}
private fun loadWallpaperFromDrawables(context: Context, wallpaper: Wallpaper.Local): Bitmap? = Result.runCatching {
BitmapFactory.decodeResource(context.resources, wallpaper.drawableId)
}.getOrNull()
/**
* Load a wallpaper from app-specific storage.
*/
private fun loadWallpaperFromDisk(context: Context, wallpaper: Wallpaper.Remote): 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 scaleBitmapToBottom(bitmap: Bitmap, view: ImageView) {
view.setImageBitmap(bitmap)
view.scaleType = ImageView.ScaleType.MATRIX
val matrix = Matrix()
view.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
override fun onLayoutChange(
v: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
val viewWidth: Float = view.width.toFloat()
val viewHeight: Float = view.height.toFloat()
val bitmapWidth = bitmap.width
val bitmapHeight = bitmap.height
val widthScale = viewWidth / bitmapWidth
val heightScale = viewHeight / bitmapHeight
val scale = widthScale.coerceAtLeast(heightScale)
matrix.postScale(scale, scale)
// The image is translated to its bottom such that any pertinent information is
// guaranteed to be shown.
// Majority of this math borrowed from // https://medium.com/@tokudu/how-to-whitelist-strictmode-violations-on-android-based-on-stacktrace-eb0018e909aa
// except that there is no need to translate horizontally in our case.
matrix.postTranslate(0f, (viewHeight - bitmapHeight * scale))
view.imageMatrix = matrix
view.removeOnLayoutChangeListener(this)
}
})
}
/**
* Get the expected local path on disk for a wallpaper. This will differ depending
* on orientation and app theme.
*/
private fun Wallpaper.Remote.getLocalPathFromContext(context: Context): String {
val orientation = if (context.isLandscape()) "landscape" else "portrait"
val theme = if (context.isDark()) "dark" else "light"
return Wallpaper.getBaseLocalPath(orientation, theme, name)
}
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
}
/**
* Animates the Firefox logo, if it hasn't been animated before, otherwise nothing will happen.
* After animating the first time, the [Settings.shouldAnimateFirefoxLogo] setting
* will be updated.
*/
@Suppress("MagicNumber")
fun animateLogoIfNeeded(logo: View) {
if (!settings.shouldAnimateFirefoxLogo) {
return
}
Handler(Looper.getMainLooper()).postDelayed(
{
val animator1 = ObjectAnimator.ofFloat(logo, "rotation", 0f, 10f)
val animator2 = ObjectAnimator.ofFloat(logo, "rotation", 10f, 0f)
val animator3 = ObjectAnimator.ofFloat(logo, "rotation", 0f, 10f)
val animator4 = ObjectAnimator.ofFloat(logo, "rotation", 10f, 0f)
animator1.duration = 200
animator2.duration = 200
animator3.duration = 200
animator4.duration = 200
val set = AnimatorSet()
set.play(animator1).before(animator2).after(animator3).before(animator4)
set.start()
settings.shouldAnimateFirefoxLogo = false
},
ANIMATION_DELAY_MS
)
}
companion object {
val defaultWallpaper = Wallpaper.Default
private val localWallpapers: List<Wallpaper.Local> = listOf(
Wallpaper.Local.Firefox("amethyst", R.drawable.amethyst),
Wallpaper.Local.Firefox("cerulean", R.drawable.cerulean),
Wallpaper.Local.Firefox("sunrise", R.drawable.sunrise),
)
private val remoteWallpapers: List<Wallpaper.Remote> = listOf(
Wallpaper.Remote.House(
"panda",
),
Wallpaper.Remote.House(
"mei",
),
Wallpaper.Remote.Firefox(
"twilight-hills"
),
Wallpaper.Remote.Firefox(
"beach-vibe"
),
)
private val availableWallpapers = listOf(defaultWallpaper) + localWallpapers + remoteWallpapers
private const val ANIMATION_DELAY_MS = 1500L
}
}