Skip to content

Commit

Permalink
For mozilla-mobile#26424 - Create wallpaper file migration helper
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexandru2909 committed Aug 29, 2022
1 parent 8bc9c9b commit 2c787be
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,36 @@ class WallpaperDownloader(
}
}

/**
* Attempts to download the thumbnail for a wallpaper.
*
* @param wallpaper The wallpaper for which we want to download the thumbnail.
* @param onFailure onFailure callback invoked when the thumbnail fails to download.
*/
fun downloadThumbnail(wallpaper: Wallpaper, onFailure: (Throwable) -> Unit) {
val localFile = File(
storageRootDirectory.absolutePath,
getLocalPath(wallpaper.name, Wallpaper.ImageType.Thumbnail)
)
val remotePath =
"${wallpaper.collection.name}/${wallpaper.name}/${Wallpaper.ImageType.Thumbnail.lowercase()}.png"

val request = Request(
url = "$remoteHost/$remotePath",
method = Request.Method.GET
)
Result.runCatching {
val response = client.fetch(request)
if (!response.isSuccess) {
return
}
File(localFile.path.substringBeforeLast("/")).mkdirs()
response.body.useStream { input ->
input.copyTo(localFile.outputStream())
}
}.onFailure(onFailure)
}

private data class WallpaperMetadata(val remotePath: String, val localPath: String)

private fun Wallpaper.toMetadata(): List<WallpaperMetadata> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ class WallpaperFileManager(
coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
private val scope = CoroutineScope(coroutineDispatcher)
private val wallpapersDirectory = File(storageRootDirectory, "wallpapers")

/**
* Lookup all the files for a wallpaper name. This lookup will fail if there are not
Expand All @@ -47,16 +46,99 @@ class WallpaperFileManager(
}

/**
* Remove all wallpapers that are not the [currentWallpaper] or in [availableWallpapers].
* Migrate the legacy wallpapers to the new path and delete the remaining legacy files.
*
* @param availableWallpapers List of wallpapers available remote.
* @param downloadThumbnail Function used to download the thumbnail for a wallpaper available
* remote.
* @return List of migrated wallpapers.
*/
suspend fun clean(currentWallpaper: Wallpaper, availableWallpapers: List<Wallpaper>) = withContext(Dispatchers.IO) {
scope.launch {
val wallpapersToKeep = (listOf(currentWallpaper) + availableWallpapers).map { it.name }
wallpapersDirectory.listFiles()?.forEach { file ->
if (file.isDirectory && !wallpapersToKeep.contains(file.name)) {
file.deleteRecursively()
suspend fun migrateAndCleanLegacyWallpapers(
availableWallpapers: List<Wallpaper>,
downloadThumbnail: (Wallpaper, (Throwable) -> Unit) -> Unit
): List<Wallpaper> = withContext(Dispatchers.IO) {
val legacyPortraitDirectory = File(storageRootDirectory, "wallpapers/portrait/light")
val legacyLandscapeDirectory = File(storageRootDirectory, "wallpapers/landscape/light")

val legacyWallpapers = legacyPortraitDirectory.listFiles()
?.filter { file -> !file.isDirectory }
?.mapNotNull { portraitFile ->
val landscapeFile = File(legacyLandscapeDirectory, portraitFile.name)
if (landscapeFile.exists()) {
Pair(portraitFile, landscapeFile)
} else {
null
}
}

val migratedWallpapers = legacyWallpapers?.map { pair ->
val name = pair.first.nameWithoutExtension
availableWallpapers.firstOrNull { it.name == name } ?: Wallpaper(
name,
Wallpaper.DefaultCollection,
null,
null
)
}

legacyWallpapers?.let {
migrateLegacyWallpapers(availableWallpapers, it, downloadThumbnail)
}
migratedWallpapers ?: listOf()
}

private fun migrateLegacyWallpapers(
availableWallpapers: List<Wallpaper>,
legacyWallpapers: List<Pair<File, File>>,
downloadThumbnail: (Wallpaper, (Throwable) -> Unit) -> Unit
) = scope.launch {
legacyWallpapers.map { pair ->
val portraitFile = pair.first
val landscapeFile = pair.second
val name = portraitFile.nameWithoutExtension
val remoteWallpaper = availableWallpapers.firstOrNull { it.name == name }

// If there is a thumbnail available for the current wallpaper,
// attempt to download it
if (remoteWallpaper != null) {
downloadThumbnail(
remoteWallpaper
) {
// If the thumbnail failed to download, use the portrait as thumbnail
portraitFile.copyTo(
File(
storageRootDirectory,
"wallpapers/${name.lowercase()}/thumbnail.png"
)
)
}
} else {
// If there is no thumbnail available for the wallpaper,
// use the portrait as thumbnail
portraitFile.copyTo(
File(
storageRootDirectory,
"wallpapers/${name.lowercase()}/thumbnail.png"
)
)
}
// Copy the portrait file
portraitFile.copyTo(
File(
storageRootDirectory,
"wallpapers/${name.lowercase()}/portrait.png"
)
)
// Copy the landscape file
landscapeFile.copyTo(
File(
storageRootDirectory,
"wallpapers/${name.lowercase()}/landscape.png"
)
)
}
// Delete the remaining legacy files
File(storageRootDirectory, "wallpapers/portrait").deleteRecursively()
File(storageRootDirectory, "wallpapers/landscape").deleteRecursively()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,8 @@ class WallpapersUseCases(
) : InitializeWallpapersUseCase {
override suspend fun invoke() {
val currentWallpaperName = withContext(Dispatchers.IO) { settings.currentWallpaperName }
val possibleWallpapers = metadataFetcher.downloadWallpaperList().filter {
val wallpaperList = metadataFetcher.downloadWallpaperList()
val possibleWallpapers = wallpaperList.filter {
!it.isExpired() && it.isAvailableInLocale()
}
val currentWallpaper = possibleWallpapers.find { it.name == currentWallpaperName }
Expand All @@ -228,14 +229,14 @@ class WallpapersUseCases(
// Dispatching this early will make it accessible to the home screen ASAP
store.dispatch(AppAction.WallpaperAction.UpdateCurrentWallpaper(currentWallpaper))

fileManager.clean(
currentWallpaper,
possibleWallpapers
val migratedWallpapers = fileManager.migrateAndCleanLegacyWallpapers(
wallpaperList,
downloader::downloadThumbnail
)

possibleWallpapers.forEach { downloader.downloadWallpaper(it) }

val defaultIncluded = listOf(Wallpaper.Default) + possibleWallpapers
val defaultIncluded = listOf(Wallpaper.Default) + possibleWallpapers + migratedWallpapers
store.dispatch(AppAction.WallpaperAction.UpdateAvailableWallpapers(defaultIncluded))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.mozilla.fenix.wallpapers
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
Expand All @@ -19,6 +20,10 @@ class WallpaperFileManagerTest {
private val dispatcher = UnconfinedTestDispatcher()

private lateinit var fileManager: WallpaperFileManager
private lateinit var portraitLightFolder: File
private lateinit var portraitDarkFolder: File
private lateinit var landscapeLightFolder: File
private lateinit var landscapeDarkFolder: File

@Before
fun setup() {
Expand All @@ -27,6 +32,10 @@ class WallpaperFileManagerTest {
storageRootDirectory = tempFolder.root,
coroutineDispatcher = dispatcher,
)
portraitLightFolder = tempFolder.newFolder("wallpapers", "portrait", "light")
portraitDarkFolder = tempFolder.newFolder("wallpapers", "portrait", "dark")
landscapeLightFolder = tempFolder.newFolder("wallpapers", "landscape", "light")
landscapeDarkFolder = tempFolder.newFolder("wallpapers", "landscape", "dark")
}

@Test
Expand Down Expand Up @@ -92,21 +101,77 @@ class WallpaperFileManagerTest {
}

@Test
fun `WHEN cleaned THEN current wallpaper and available wallpapers kept`() = runTest {
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() })
fun `WHEN legacy wallpapers are migrated THEN the legacy wallpapers are deleted`() = runTest {
val firstWallpaper = "wallpaper1"
val secondWallpaper = "wallpaper2"
createAllLegacyFiles(firstWallpaper)
createAllLegacyFiles(secondWallpaper)

val migratedWallpapers = fileManager.migrateAndCleanLegacyWallpapers(listOf()) { _, _ -> }

assertEquals(2, migratedWallpapers.size)
val wallpaperNames = migratedWallpapers.map { wallpaper -> wallpaper.name }
assertTrue(wallpaperNames.contains(firstWallpaper))
assertTrue(wallpaperNames.contains(secondWallpaper))
assertTrue(getAllFiles(firstWallpaper).all { it.exists() })
assertTrue(getAllFiles(secondWallpaper).all { it.exists() })
assertFalse(File(portraitLightFolder, "$firstWallpaper.png").exists())
assertFalse(File(portraitDarkFolder, "$firstWallpaper.png").exists())
assertFalse(File(landscapeLightFolder, "$firstWallpaper.png").exists())
assertFalse(File(landscapeDarkFolder, "$firstWallpaper.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 allWallpaperName = "legacy"
File(landscapeLightFolder, "$portraitOnlyWallpaperName.png").apply {
createNewFile()
}
File(landscapeDarkFolder, "$portraitOnlyWallpaperName.png").apply {
createNewFile()
}
createAllLegacyFiles(allWallpaperName)

fileManager.migrateAndCleanLegacyWallpapers(listOf()) { _, _ -> }

assertTrue(getAllFiles(allWallpaperName).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 allWallpaperName = "legacy"
File(portraitLightFolder, "$landscapeOnlyWallpaperName.png").apply {
createNewFile()
}
File(portraitDarkFolder, "$landscapeOnlyWallpaperName.png").apply {
createNewFile()
}
createAllLegacyFiles(allWallpaperName)

fileManager.migrateAndCleanLegacyWallpapers(listOf()) { _, _ -> }

assertTrue(getAllFiles(allWallpaperName).all { it.exists() })
assertFalse(getAllFiles(landscapeOnlyWallpaperName).any { it.exists() })
}

private fun createAllLegacyFiles(name: String) {
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 createAllFiles(name: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class WallpapersUseCasesTest {
private val mockMetadataFetcher = mockk<WallpaperMetadataFetcher>()
private val mockDownloader = mockk<WallpaperDownloader>(relaxed = true)
private val mockFileManager = mockk<WallpaperFileManager> {
coEvery { clean(any(), any()) } returns mockk()
coEvery { migrateAndCleanLegacyWallpapers(any(), any()) } returns listOf()
}

@Test
Expand Down Expand Up @@ -298,7 +298,6 @@ class WallpapersUseCasesTest {
val expectedFilteredWallpaper = fakeExpiredRemoteWallpapers[0]
appStore.waitUntilIdle()
assertFalse(appStore.state.wallpaperState.availableWallpapers.contains(expectedFilteredWallpaper))
coVerify { mockFileManager.clean(Wallpaper.Default, fakeRemoteWallpapers) }
}

@Test
Expand Down

0 comments on commit 2c787be

Please sign in to comment.