/* 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.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. */ @Suppress("TooManyFunctions") class WallpaperManager( private val settings: Settings, private val wallpaperStorage: WallpaperStorage, private val downloader: WallpaperDownloader, private val crashReporter: CrashReporter, ) { val logger = Logger("WallpaperManager") private val remoteWallpapers = listOf( Wallpaper( "focus", portraitPath = "", landscapePath = "", isDark = false, themeCollection = WallpaperThemeCollection.FOCUS ), ) var availableWallpapers: List = loadWallpapers() + remoteWallpapers private set var currentWallpaper: Wallpaper = getCurrentWallpaperFromSettings() set(value) { settings.currentWallpaper = value.name field = value } /** * Apply the [newWallpaper] into the [wallpaperContainer] and update the [currentWallpaper]. */ fun updateWallpaper(wallpaperContainer: View, newWallpaper: Wallpaper) { val context = wallpaperContainer.context if (newWallpaper == defaultWallpaper) { wallpaperContainer.setBackgroundColor(context.getColorFromAttr(DEFAULT_RESOURCE)) 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) 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. */ fun switchToNextWallpaper(): Wallpaper { val values = availableWallpapers val index = values.indexOf(currentWallpaper) + 1 return if (index >= values.size) { values.first() } else { values[index] } } private fun getCurrentWallpaperFromSettings(): Wallpaper { val currentWallpaper = settings.currentWallpaper return if (currentWallpaper.isEmpty()) { defaultWallpaper } else { availableWallpapers.find { it.name == currentWallpaper } ?: defaultWallpaper } } /** * 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 } 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 } private fun loadWallpapers(): List { val wallpapersFromStorage = wallpaperStorage.loadAll() return if (wallpapersFromStorage.isNotEmpty()) { listOf(defaultWallpaper) + wallpapersFromStorage } else { listOf(defaultWallpaper) } } /** * 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 { const val DEFAULT_RESOURCE = R.attr.homeBackground val defaultWallpaper = Wallpaper( name = "default_wallpaper", portraitPath = "", landscapePath = "", isDark = false, themeCollection = WallpaperThemeCollection.NONE ) private const val ANIMATION_DELAY_MS = 1500L } }