diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index b566b352a..190ef76ef 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -207,6 +207,14 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = 0, ) + /** + * Indicates if the current legacy wallpaper should be migrated. + */ + var shouldMigrateLegacyWallpaper by booleanPreference( + key = appContext.getPreferenceKey(R.string.pref_key_should_migrate_wallpaper), + default = true, + ) + /** * Indicates if the wallpaper onboarding dialog should be shown. */ diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/LegacyWallpaperMigration.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/LegacyWallpaperMigration.kt new file mode 100644 index 000000000..993e26b72 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/LegacyWallpaperMigration.kt @@ -0,0 +1,71 @@ +/* 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.Dispatchers +import kotlinx.coroutines.withContext +import mozilla.components.support.base.log.logger.Logger +import java.io.File +import java.io.IOException + +/** + * Manages the migration of legacy wallpapers to the new paths + * + * @property storageRootDirectory The top level app-local storage directory. + */ +class LegacyWallpaperMigration( + private val storageRootDirectory: File, +) { + /** + * Migrate the legacy wallpaper to the new path and delete the remaining legacy files. + * + * @param wallpaperName Name of the wallpaper to be migrated. + */ + suspend fun migrateLegacyWallpaper( + wallpaperName: String, + ) = withContext(Dispatchers.IO) { + val legacyPortraitFile = + File(storageRootDirectory, "wallpapers/portrait/light/$wallpaperName.png") + val legacyLandscapeFile = + File(storageRootDirectory, "wallpapers/landscape/light/$wallpaperName.png") + // If any of portrait or landscape files of the wallpaper are missing, then we shouldn't + // migrate it + if (!legacyLandscapeFile.exists() || !legacyPortraitFile.exists()) { + return@withContext + } + // Directory where the legacy wallpaper files should be migrated + val targetDirectory = "wallpapers/${wallpaperName.lowercase()}" + + try { + // Use the portrait file as thumbnail + legacyPortraitFile.copyTo( + File( + storageRootDirectory, + "$targetDirectory/thumbnail.png", + ), + ) + // Copy the portrait file + legacyPortraitFile.copyTo( + File( + storageRootDirectory, + "$targetDirectory/portrait.png", + ), + ) + // Copy the landscape file + legacyLandscapeFile.copyTo( + File( + storageRootDirectory, + "$targetDirectory/landscape.png", + ), + ) + } catch (e: IOException) { + Logger.error("Failed to migrate legacy wallpaper", e) + } + + // Delete the remaining legacy files + File(storageRootDirectory, "wallpapers/portrait").deleteRecursively() + File(storageRootDirectory, "wallpapers/landscape").deleteRecursively() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt index 6e5cd7ef4..74751736c 100644 --- a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt @@ -47,11 +47,13 @@ class WallpapersUseCases( val initialize: InitializeWallpapersUseCase by lazy { if (FeatureFlags.wallpaperV2Enabled) { val metadataFetcher = WallpaperMetadataFetcher(client) + val migrationHelper = LegacyWallpaperMigration(storageRootDirectory) DefaultInitializeWallpaperUseCase( store = store, downloader = downloader, fileManager = fileManager, metadataFetcher = metadataFetcher, + migrationHelper = migrationHelper, settings = context.settings(), currentLocale = currentLocale, ) @@ -222,12 +224,14 @@ class WallpapersUseCases( } } + @Suppress("LongParameterList") @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal class DefaultInitializeWallpaperUseCase( private val store: 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 { @@ -236,6 +240,10 @@ class WallpapersUseCases( store.dispatch(AppAction.WallpaperAction.UpdateCurrentWallpaper(it)) } val currentWallpaperName = withContext(Dispatchers.IO) { settings.currentWallpaperName } + if (settings.shouldMigrateLegacyWallpaper) { + migrationHelper.migrateLegacyWallpaper(currentWallpaperName) + settings.shouldMigrateLegacyWallpaper = false + } val possibleWallpapers = metadataFetcher.downloadWallpaperList().filter { !it.isExpired() && it.isAvailableInLocale() } diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index 09d692a9e..cc96aead2 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -207,6 +207,7 @@ pref_key_current_wallpaper_text_color pref_key_current_wallpaper_card_color pref_key_wallpapers_onboarding + pref_key_should_migrate_wallpaper pref_key_encryption_key_generated diff --git a/app/src/test/java/org/mozilla/fenix/wallpapers/LegacyWallpaperMigrationTest.kt b/app/src/test/java/org/mozilla/fenix/wallpapers/LegacyWallpaperMigrationTest.kt new file mode 100644 index 000000000..8d7dab271 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/wallpapers/LegacyWallpaperMigrationTest.kt @@ -0,0 +1,117 @@ +package org.mozilla.fenix.wallpapers + +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertFalse +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 LegacyWallpaperMigrationTest { + @Rule + @JvmField + val tempFolder = TemporaryFolder() + private lateinit var wallpapersFolder: File + private lateinit var migrationHelper: LegacyWallpaperMigration + private lateinit var portraitLightFolder: File + private lateinit var portraitDarkFolder: File + private lateinit var landscapeLightFolder: File + private lateinit var landscapeDarkFolder: File + + @Before + fun setup() { + wallpapersFolder = File(tempFolder.root, "wallpapers") + migrationHelper = LegacyWallpaperMigration( + storageRootDirectory = tempFolder.root, + ) + } + + @Test + fun `WHEN the legacy wallpaper is migrated THEN the legacy wallpapers are deleted`() = runTest { + val wallpaperName = "wallpaper1" + + createAllLegacyFiles(wallpaperName) + + migrationHelper.migrateLegacyWallpaper(wallpaperName) + + assertTrue(getAllFiles(wallpaperName).all { it.exists() }) + assertFalse(File(portraitLightFolder, "$wallpaperName.png").exists()) + assertFalse(File(portraitDarkFolder, "$wallpaperName.png").exists()) + assertFalse(File(landscapeLightFolder, "$wallpaperName.png").exists()) + assertFalse(File(landscapeDarkFolder, "$wallpaperName.png").exists()) + } + + @Test + fun `GIVEN landscape legacy wallpaper is missing WHEN the wallpapers are migrated THEN the wallpaper is not migrated`() = + runTest { + val portraitOnlyWallpaperName = "portraitOnly" + val completeWallpaperName = "legacy" + createAllLegacyFiles(completeWallpaperName) + File(landscapeLightFolder, "$portraitOnlyWallpaperName.png").apply { + createNewFile() + } + File(landscapeDarkFolder, "$portraitOnlyWallpaperName.png").apply { + createNewFile() + } + + migrationHelper.migrateLegacyWallpaper(portraitOnlyWallpaperName) + migrationHelper.migrateLegacyWallpaper(completeWallpaperName) + + assertTrue(getAllFiles(completeWallpaperName).all { it.exists() }) + assertFalse(getAllFiles(portraitOnlyWallpaperName).any { it.exists() }) + } + + @Test + fun `GIVEN portrait legacy wallpaper is missing WHEN the wallpapers are migrated THEN the wallpaper is not migrated`() = + runTest { + val landscapeOnlyWallpaperName = "portraitOnly" + val completeWallpaperName = "legacy" + createAllLegacyFiles(completeWallpaperName) + File(portraitLightFolder, "$landscapeOnlyWallpaperName.png").apply { + createNewFile() + } + File(portraitDarkFolder, "$landscapeOnlyWallpaperName.png").apply { + createNewFile() + } + + migrationHelper.migrateLegacyWallpaper(landscapeOnlyWallpaperName) + migrationHelper.migrateLegacyWallpaper(completeWallpaperName) + + assertTrue(getAllFiles(completeWallpaperName).all { it.exists() }) + assertFalse(getAllFiles(landscapeOnlyWallpaperName).any { it.exists() }) + } + + private fun createAllLegacyFiles(name: String) { + if (!this::portraitLightFolder.isInitialized) { + portraitLightFolder = tempFolder.newFolder("wallpapers", "portrait", "light") + portraitDarkFolder = tempFolder.newFolder("wallpapers", "portrait", "dark") + landscapeLightFolder = tempFolder.newFolder("wallpapers", "landscape", "light") + landscapeDarkFolder = tempFolder.newFolder("wallpapers", "landscape", "dark") + } + + File(portraitLightFolder, "$name.png").apply { + createNewFile() + } + File(landscapeLightFolder, "$name.png").apply { + createNewFile() + } + File(portraitDarkFolder, "$name.png").apply { + createNewFile() + } + File(landscapeDarkFolder, "$name.png").apply { + createNewFile() + } + } + + private fun getAllFiles(name: String): List { + val folder = File(wallpapersFolder, name) + return listOf( + folder, + File(folder, "portrait.png"), + File(folder, "landscape.png"), + File(folder, "thumbnail.png"), + ) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/wallpapers/WallpapersUseCasesTest.kt b/app/src/test/java/org/mozilla/fenix/wallpapers/WallpapersUseCasesTest.kt index 7308ca0f8..e9ef700f3 100644 --- a/app/src/test/java/org/mozilla/fenix/wallpapers/WallpapersUseCasesTest.kt +++ b/app/src/test/java/org/mozilla/fenix/wallpapers/WallpapersUseCasesTest.kt @@ -42,11 +42,16 @@ class WallpapersUseCasesTest { every { currentWallpaperTextColor = any() } just Runs every { currentWallpaperCardColor } returns 0L every { currentWallpaperCardColor = any() } just Runs + every { shouldMigrateLegacyWallpaper } returns false + every { shouldMigrateLegacyWallpaper = any() } just Runs } private val mockLegacyDownloader = mockk(relaxed = true) private val mockLegacyFileManager = mockk { every { clean(any(), any()) } just runs } + private val mockMigrationHelper = mockk { + coEvery { migrateLegacyWallpaper(any()) } just runs + } private val mockMetadataFetcher = mockk() private val mockDownloader = mockk { @@ -252,6 +257,7 @@ class WallpapersUseCasesTest { mockDownloader, mockFileManager, mockMetadataFetcher, + mockMigrationHelper, mockSettings, "en-US", ).invoke() @@ -275,6 +281,7 @@ class WallpapersUseCasesTest { mockDownloader, mockFileManager, mockMetadataFetcher, + mockMigrationHelper, mockSettings, "en-US", ).invoke() @@ -302,6 +309,7 @@ class WallpapersUseCasesTest { mockDownloader, mockFileManager, mockMetadataFetcher, + mockMigrationHelper, mockSettings, "en-US", ).invoke() @@ -329,6 +337,7 @@ class WallpapersUseCasesTest { mockDownloader, mockFileManager, mockMetadataFetcher, + mockMigrationHelper, mockSettings, "en-US", ).invoke() @@ -356,6 +365,7 @@ class WallpapersUseCasesTest { mockDownloader, mockFileManager, mockMetadataFetcher, + mockMigrationHelper, mockSettings, locale, ).invoke() @@ -380,6 +390,7 @@ class WallpapersUseCasesTest { mockDownloader, mockFileManager, mockMetadataFetcher, + mockMigrationHelper, mockSettings, "en-US", ).invoke() @@ -404,6 +415,7 @@ class WallpapersUseCasesTest { mockDownloader, mockFileManager, mockMetadataFetcher, + mockMigrationHelper, mockSettings, "en-US", ).invoke() @@ -436,6 +448,7 @@ class WallpapersUseCasesTest { mockDownloader, mockFileManager, mockMetadataFetcher, + mockMigrationHelper, mockSettings, "en-US", ).invoke() @@ -460,6 +473,7 @@ class WallpapersUseCasesTest { mockDownloader, mockFileManager, mockMetadataFetcher, + mockMigrationHelper, mockSettings, "en-US", ).invoke() @@ -485,6 +499,7 @@ class WallpapersUseCasesTest { mockDownloader, mockFileManager, mockMetadataFetcher, + mockMigrationHelper, mockSettings, "en-US", ).invoke()