diff options
Diffstat (limited to 'mobile/android/fenix/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt')
-rw-r--r-- | mobile/android/fenix/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt | 279 |
1 files changed, 279 insertions, 0 deletions
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt new file mode 100644 index 0000000000..cdb70c1631 --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt @@ -0,0 +1,279 @@ +/* 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 android.content.res.Configuration +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import androidx.annotation.VisibleForTesting +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import mozilla.components.concept.fetch.Client +import org.mozilla.fenix.components.AppStore +import org.mozilla.fenix.components.appstate.AppAction +import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.utils.Settings +import java.io.File +import java.util.Date + +/** + * Contains use cases related to the wallpaper feature. + * + * @param context Used for various file and configuration checks. + * @param appStore Will receive dispatches of metadata updates like the currently selected wallpaper. + * @param client Handles downloading wallpapers and their metadata. + * @param storageRootDirectory The top level app-local storage directory. + * @param currentLocale The locale currently being used on the device. + */ +class WallpapersUseCases( + context: Context, + appStore: AppStore, + client: Client, + storageRootDirectory: File, + currentLocale: String, +) { + private val downloader = WallpaperDownloader(storageRootDirectory, client) + private val fileManager = WallpaperFileManager(storageRootDirectory) + + // Use case for initializing wallpaper feature. Should usually be called early + // in the app's lifetime to ensure that any potential long-running tasks can complete quickly. + val initialize: InitializeWallpapersUseCase by lazy { + val metadataFetcher = WallpaperMetadataFetcher(client) + val migrationHelper = LegacyWallpaperMigration( + storageRootDirectory = storageRootDirectory, + settings = context.settings(), + selectWallpaper::invoke, + ) + DefaultInitializeWallpaperUseCase( + appStore = appStore, + downloader = downloader, + fileManager = fileManager, + metadataFetcher = metadataFetcher, + migrationHelper = migrationHelper, + settings = context.settings(), + currentLocale = currentLocale, + ) + } + + // Use case for loading specific wallpaper bitmaps. + val loadBitmap: LoadBitmapUseCase by lazy { + DefaultLoadBitmapUseCase( + getFilesDir = { context.filesDir }, + ) + } + + val loadThumbnail: LoadThumbnailUseCase by lazy { + DefaultLoadThumbnailUseCase(storageRootDirectory) + } + + // Use case for selecting a new wallpaper. + val selectWallpaper: SelectWallpaperUseCase by lazy { + DefaultSelectWallpaperUseCase(context.settings(), appStore, fileManager, downloader) + } + + /** + * Contract for usecases that initialize the wallpaper feature. + */ + interface InitializeWallpapersUseCase { + /** + * Start operations that should be down during initialization, like remote metadata + * retrieval and determining the currently selected wallpaper. + */ + suspend operator fun invoke() + } + + @Suppress("LongParameterList") + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal class DefaultInitializeWallpaperUseCase( + private val appStore: AppStore, + private val downloader: WallpaperDownloader, + private val fileManager: WallpaperFileManager, + private val metadataFetcher: WallpaperMetadataFetcher, + private val migrationHelper: LegacyWallpaperMigration, + private val settings: Settings, + private val currentLocale: String, + ) : InitializeWallpapersUseCase { + override suspend fun invoke() { + Wallpaper.getCurrentWallpaperFromSettings(settings)?.let { + appStore.dispatch(AppAction.WallpaperAction.UpdateCurrentWallpaper(it)) + } + + val currentWallpaperName = if (settings.shouldMigrateLegacyWallpaper) { + val migratedWallpaperName = + migrationHelper.migrateLegacyWallpaper(settings.currentWallpaperName) + settings.currentWallpaperName = migratedWallpaperName + settings.shouldMigrateLegacyWallpaper = false + migratedWallpaperName + } else { + settings.currentWallpaperName + } + + if (settings.shouldMigrateLegacyWallpaperCardColors) { + migrationHelper.migrateExpiredWallpaperCardColors() + } + + val possibleWallpapers = metadataFetcher.downloadWallpaperList().filter { + !it.isExpired() && it.isAvailableInLocale() + } + val currentWallpaper = possibleWallpapers.find { it.name == currentWallpaperName } + ?: fileManager.lookupExpiredWallpaper(settings) + ?: Wallpaper.Default + + // Dispatching this early will make it accessible to the home screen ASAP. If it has been + // dispatched above, we may still need to update other metadata about it. + appStore.dispatch(AppAction.WallpaperAction.UpdateCurrentWallpaper(currentWallpaper)) + + fileManager.clean( + currentWallpaper, + possibleWallpapers, + ) + + val wallpapersWithUpdatedThumbnailState = possibleWallpapers.map { wallpaper -> + val result = downloader.downloadThumbnail(wallpaper) + wallpaper.copy(thumbnailFileState = result) + } + + val defaultIncluded = listOf(Wallpaper.Default) + wallpapersWithUpdatedThumbnailState + appStore.dispatch(AppAction.WallpaperAction.UpdateAvailableWallpapers(defaultIncluded)) + } + + private fun Wallpaper.isExpired(): Boolean = when (this) { + Wallpaper.Default -> false + else -> { + val expired = this.collection.endDate?.let { Date().after(it) } ?: false + expired && this.name != settings.currentWallpaperName + } + } + + private fun Wallpaper.isAvailableInLocale(): Boolean = + this.collection.availableLocales?.contains(currentLocale) ?: true + } + + /** + * Contract for usecase for loading bitmaps related to a specific wallpaper. + */ + interface LoadBitmapUseCase { + /** + * Load the bitmap for a [wallpaper], if available. + * + * @param wallpaper The wallpaper to load a bitmap for. + * @param orientation The orientation of wallpaper. + */ + suspend operator fun invoke(wallpaper: Wallpaper, orientation: Int): Bitmap? + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal class DefaultLoadBitmapUseCase( + private val getFilesDir: suspend () -> File, + ) : LoadBitmapUseCase { + override suspend fun invoke(wallpaper: Wallpaper, orientation: Int): Bitmap? = + loadWallpaperFromDisk(wallpaper, orientation) + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal suspend fun loadWallpaperFromDisk( + wallpaper: Wallpaper, + orientation: Int, + ): Bitmap? = Result.runCatching { + val path = wallpaper.getLocalPathFromContext(orientation) + withContext(Dispatchers.IO) { + val file = File(getFilesDir(), path) + BitmapFactory.decodeStream(file.inputStream()) + } + }.getOrNull() + + /** + * Get the expected local path on disk for a wallpaper. This will differ depending + * on orientation and app theme. + */ + private fun Wallpaper.getLocalPathFromContext(orientation: Int): String { + val orientationWallpaper = if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + Wallpaper.ImageType.Landscape + } else { + Wallpaper.ImageType.Portrait + } + return Wallpaper.getLocalPath(name, orientationWallpaper) + } + } + + /** + * Contract for usecase for loading thumbnail bitmaps related to a specific wallpaper. + */ + interface LoadThumbnailUseCase { + /** + * Load the bitmap for a [wallpaper] thumbnail, if available. + * + * @param wallpaper The wallpaper to load a thumbnail for. + */ + suspend operator fun invoke(wallpaper: Wallpaper): Bitmap? + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal class DefaultLoadThumbnailUseCase(private val filesDir: File) : LoadThumbnailUseCase { + override suspend fun invoke(wallpaper: Wallpaper): Bitmap? = withContext(Dispatchers.IO) { + Result.runCatching { + val path = Wallpaper.getLocalPath(wallpaper.name, Wallpaper.ImageType.Thumbnail) + withContext(Dispatchers.IO) { + val file = File(filesDir, path) + BitmapFactory.decodeStream(file.inputStream()) + } + }.getOrNull() + } + } + + /** + * Contract for usecase of selecting a new wallpaper. + */ + interface SelectWallpaperUseCase { + /** + * Select a new wallpaper. + * + * @param wallpaper The selected wallpaper. + */ + suspend operator fun invoke(wallpaper: Wallpaper): Wallpaper.ImageFileState + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal class DefaultSelectWallpaperUseCase( + private val settings: Settings, + private val appStore: AppStore, + private val fileManager: WallpaperFileManager, + private val downloader: WallpaperDownloader, + ) : SelectWallpaperUseCase { + /** + * Select a new wallpaper. Storage and the app store will be updated appropriately. + * + * @param wallpaper The selected wallpaper. + */ + override suspend fun invoke(wallpaper: Wallpaper): Wallpaper.ImageFileState { + return if (wallpaper == Wallpaper.Default || fileManager.wallpaperImagesExist(wallpaper)) { + selectWallpaper(wallpaper) + dispatchDownloadState(wallpaper, Wallpaper.ImageFileState.Downloaded) + Wallpaper.ImageFileState.Downloaded + } else { + dispatchDownloadState(wallpaper, Wallpaper.ImageFileState.Downloading) + val result = downloader.downloadWallpaper(wallpaper) + dispatchDownloadState(wallpaper, result) + if (result == Wallpaper.ImageFileState.Downloaded) { + selectWallpaper(wallpaper) + } + result + } + } + + @VisibleForTesting + internal fun selectWallpaper(wallpaper: Wallpaper) { + settings.currentWallpaperName = wallpaper.name + settings.currentWallpaperTextColor = wallpaper.textColor ?: 0L + settings.currentWallpaperCardColorLight = wallpaper.cardColorLight ?: 0L + settings.currentWallpaperCardColorDark = wallpaper.cardColorDark ?: 0L + appStore.dispatch(AppAction.WallpaperAction.UpdateCurrentWallpaper(wallpaper)) + } + + private fun dispatchDownloadState(wallpaper: Wallpaper, downloadState: Wallpaper.ImageFileState) { + appStore.dispatch(AppAction.WallpaperAction.UpdateWallpaperDownloadState(wallpaper, downloadState)) + } + } +} |