Store app icons in separate file

so they can be shown when selecting apps for restore which is before we have downloaded the APK files to extract icons from
This commit is contained in:
Torsten Grote 2024-05-21 11:51:05 -03:00
parent 905340770c
commit 5a2f1187a8
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
11 changed files with 257 additions and 13 deletions

View file

@ -46,7 +46,19 @@ class KoinInstrumentationTestApp : App() {
viewModel { viewModel {
currentRestoreViewModel = currentRestoreViewModel =
spyk(RestoreViewModel(context, get(), get(), get(), get(), get(), get(), get())) spyk(
RestoreViewModel(
app = context,
settingsManager = get(),
keyManager = get(),
backupManager = get(),
restoreCoordinator = get(),
apkRestore = get(),
iconManager = get(),
storageBackup = get(),
pluginManager = get(),
)
)
currentRestoreViewModel!! currentRestoreViewModel!!
} }

View file

@ -95,7 +95,19 @@ open class App : Application() {
) )
} }
viewModel { RestoreStorageViewModel(this@App, get(), get(), get(), get()) } viewModel { RestoreStorageViewModel(this@App, get(), get(), get(), get()) }
viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get(), get()) } viewModel {
RestoreViewModel(
app = this@App,
settingsManager = get(),
keyManager = get(),
backupManager = get(),
restoreCoordinator = get(),
apkRestore = get(),
iconManager = get(),
storageBackup = get(),
pluginManager = get(),
)
}
viewModel { FileSelectionViewModel(this@App, get()) } viewModel { FileSelectionViewModel(this@App, get()) }
} }

View file

@ -1,5 +1,6 @@
package com.stevesoltys.seedvault.restore package com.stevesoltys.seedvault.restore
import android.graphics.Bitmap
import android.text.format.DateUtils import android.text.format.DateUtils
import android.text.format.Formatter import android.text.format.Formatter
import android.view.LayoutInflater import android.view.LayoutInflater
@ -14,16 +15,22 @@ import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.metadata.PackageMetadata import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.restore.AppSelectionAdapter.AppSelectionViewHolder import com.stevesoltys.seedvault.restore.AppSelectionAdapter.AppSelectionViewHolder
import com.stevesoltys.seedvault.ui.AppViewHolder import com.stevesoltys.seedvault.ui.AppViewHolder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
internal data class SelectableAppItem( internal data class SelectableAppItem(
val packageName: String, val packageName: String,
val metadata: PackageMetadata, val metadata: PackageMetadata,
val selected: Boolean, val selected: Boolean,
val hasIcon: Boolean? = null,
) { ) {
val name: String get() = packageName val name: String get() = packageName
} }
internal class AppSelectionAdapter( internal class AppSelectionAdapter(
val scope: CoroutineScope,
val iconLoader: suspend (String, (Bitmap) -> Unit) -> Unit,
val listener: (SelectableAppItem) -> Unit, val listener: (SelectableAppItem) -> Unit,
) : Adapter<AppSelectionViewHolder>() { ) : Adapter<AppSelectionViewHolder>() {
@ -37,7 +44,7 @@ internal class AppSelectionAdapter(
old: SelectableAppItem, old: SelectableAppItem,
new: SelectableAppItem, new: SelectableAppItem,
): Boolean { ): Boolean {
return old.selected == new.selected return old.selected == new.selected && old.hasIcon == new.hasIcon
} }
} }
private val differ = AsyncListDiffer(this, diffCallback) private val differ = AsyncListDiffer(this, diffCallback)
@ -64,7 +71,14 @@ internal class AppSelectionAdapter(
differ.submitList(items) differ.submitList(items)
} }
override fun onViewRecycled(holder: AppSelectionViewHolder) {
holder.iconJob?.cancel()
}
internal inner class AppSelectionViewHolder(v: View) : AppViewHolder(v) { internal inner class AppSelectionViewHolder(v: View) : AppViewHolder(v) {
var iconJob: Job? = null
fun bind(item: SelectableAppItem) { fun bind(item: SelectableAppItem) {
v.background = clickableBackground v.background = clickableBackground
v.setOnClickListener { v.setOnClickListener {
@ -76,9 +90,23 @@ internal class AppSelectionAdapter(
checkBox.setOnCheckedChangeListener { _, _ -> checkBox.setOnCheckedChangeListener { _, _ ->
listener(item) listener(item)
} }
checkBox.visibility = VISIBLE checkBox.visibility = if (item.hasIcon == null) INVISIBLE else VISIBLE
progressBar.visibility = INVISIBLE progressBar.visibility = if (item.hasIcon == null) VISIBLE else INVISIBLE
appIcon.setImageResource(R.drawable.ic_launcher_default)
if (item.hasIcon == null) {
appIcon.alpha = 0.5f
} else if (item.hasIcon) {
appIcon.alpha = 0.5f
iconJob = scope.launch {
iconLoader(item.packageName) { bitmap ->
appIcon.setImageBitmap(bitmap)
appIcon.alpha = 1f
}
}
} else {
appIcon.alpha = 1f
}
appIcon.setImageResource(R.drawable.ic_launcher_default) appIcon.setImageResource(R.drawable.ic_launcher_default)
appName.text = item.packageName appName.text = item.packageName
val time = if (item.metadata.time > 0) DateUtils.getRelativeTimeSpanString( val time = if (item.metadata.time > 0) DateUtils.getRelativeTimeSpanString(

View file

@ -1,5 +1,6 @@
package com.stevesoltys.seedvault.restore package com.stevesoltys.seedvault.restore
import android.graphics.Bitmap
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -7,6 +8,7 @@ import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.TextView import android.widget.TextView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.checkbox.MaterialCheckBox
@ -18,7 +20,7 @@ class AppSelectionFragment : Fragment() {
private val viewModel: RestoreViewModel by sharedViewModel() private val viewModel: RestoreViewModel by sharedViewModel()
private val layoutManager = LinearLayoutManager(context) private val layoutManager = LinearLayoutManager(context)
private val adapter = AppSelectionAdapter { item -> private val adapter = AppSelectionAdapter(lifecycleScope, this::loadIcon) { item ->
viewModel.onAppSelected(item) viewModel.onAppSelected(item)
} }
@ -64,7 +66,15 @@ class AppSelectionFragment : Fragment() {
viewModel.selectedApps.observe(viewLifecycleOwner) { state -> viewModel.selectedApps.observe(viewLifecycleOwner) { state ->
adapter.submitList(state.apps) adapter.submitList(state.apps)
toggleAllView.isChecked = state.allSelected toggleAllView.isChecked = state.allSelected
// enable toggle all views only after icons have loaded
toggleAllView.isEnabled = state.iconsLoaded
toggleAllTextView.isEnabled = state.iconsLoaded
button.isEnabled = state.iconsLoaded
} }
} }
private suspend fun loadIcon(packageName: String, callback: (Bitmap) -> Unit) {
viewModel.loadIcon(packageName, callback)
}
} }

View file

@ -13,6 +13,7 @@ import android.app.backup.IRestoreObserver
import android.app.backup.IRestoreSession import android.app.backup.IRestoreSession
import android.app.backup.RestoreSet import android.app.backup.RestoreSet
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.os.RemoteException import android.os.RemoteException
import android.os.UserHandle import android.os.UserHandle
import android.util.Log import android.util.Log
@ -61,6 +62,8 @@ import com.stevesoltys.seedvault.ui.LiveEvent
import com.stevesoltys.seedvault.ui.MutableLiveEvent import com.stevesoltys.seedvault.ui.MutableLiveEvent
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
import com.stevesoltys.seedvault.ui.notification.getAppName import com.stevesoltys.seedvault.ui.notification.getAppName
import com.stevesoltys.seedvault.worker.FILE_BACKUP_ICONS
import com.stevesoltys.seedvault.worker.IconManager
import com.stevesoltys.seedvault.worker.NUM_PACKAGES_PER_TRANSACTION import com.stevesoltys.seedvault.worker.NUM_PACKAGES_PER_TRANSACTION
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -83,6 +86,7 @@ internal const val PACKAGES_PER_CHUNK = NUM_PACKAGES_PER_TRANSACTION
internal class SelectedAppsState( internal class SelectedAppsState(
val apps: List<SelectableAppItem>, val apps: List<SelectableAppItem>,
val allSelected: Boolean, val allSelected: Boolean,
val iconsLoaded: Boolean,
) )
internal class RestoreViewModel( internal class RestoreViewModel(
@ -92,6 +96,7 @@ internal class RestoreViewModel(
private val backupManager: IBackupManager, private val backupManager: IBackupManager,
private val restoreCoordinator: RestoreCoordinator, private val restoreCoordinator: RestoreCoordinator,
private val apkRestore: ApkRestore, private val apkRestore: ApkRestore,
private val iconManager: IconManager,
storageBackup: StorageBackup, storageBackup: StorageBackup,
pluginManager: StoragePluginManager, pluginManager: StoragePluginManager,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
@ -175,6 +180,7 @@ internal class RestoreViewModel(
override fun onRestorableBackupClicked(restorableBackup: RestorableBackup) { override fun onRestorableBackupClicked(restorableBackup: RestorableBackup) {
mChosenRestorableBackup.value = restorableBackup mChosenRestorableBackup.value = restorableBackup
// filter and sort app items for display
val items = restorableBackup.packageMetadataMap.mapNotNull { (packageName, metadata) -> val items = restorableBackup.packageMetadataMap.mapNotNull { (packageName, metadata) ->
if (metadata.time == 0L && !metadata.hasApk()) null if (metadata.time == 0L && !metadata.hasApk()) null
else if (packageName == MAGIC_PACKAGE_MANAGER) null else if (packageName == MAGIC_PACKAGE_MANAGER) null
@ -183,21 +189,46 @@ internal class RestoreViewModel(
if (i1.metadata.system == i2.metadata.system) i1.name.compareTo(i2.name, true) if (i1.metadata.system == i2.metadata.system) i1.name.compareTo(i2.name, true)
else i1.metadata.system.compareTo(i2.metadata.system) else i1.metadata.system.compareTo(i2.metadata.system)
} }
mSelectedApps.value = SelectedAppsState(items, true) mSelectedApps.value =
SelectedAppsState(apps = items, allSelected = true, iconsLoaded = false)
// download icons
viewModelScope.launch(Dispatchers.IO) {
val plugin = pluginManager.appPlugin
val token = restorableBackup.token
val packagesWithIcons = try {
plugin.getInputStream(token, FILE_BACKUP_ICONS).use {
iconManager.downloadIcons(it)
}
} catch (e: Exception) {
Log.e(TAG, "Error loading icons:", e)
emptySet()
}
// update state, so it knows that icons have loaded
val updatedItems = items.map { item ->
item.copy(hasIcon = item.packageName in packagesWithIcons)
}
val newState =
SelectedAppsState(updatedItems, allSelected = true, iconsLoaded = true)
mSelectedApps.postValue(newState)
}
mDisplayFragment.setEvent(SELECT_APPS) mDisplayFragment.setEvent(SELECT_APPS)
} }
suspend fun loadIcon(packageName: String, callback: (Bitmap) -> Unit) {
iconManager.loadIcon(packageName, callback)
}
fun onCheckAllAppsClicked() { fun onCheckAllAppsClicked() {
val apps = selectedApps.value?.apps ?: return val apps = selectedApps.value?.apps ?: return
val allSelected = apps.all { it.selected } val allSelected = apps.all { it.selected }
if (allSelected) { if (allSelected) {
// unselect all // unselect all
val newApps = apps.map { if (it.selected) it.copy(selected = false) else it } val newApps = apps.map { if (it.selected) it.copy(selected = false) else it }
mSelectedApps.value = SelectedAppsState(newApps, false) mSelectedApps.value = SelectedAppsState(newApps, false, iconsLoaded = true)
} else { } else {
// select all // select all
val newApps = apps.map { if (!it.selected) it.copy(selected = true) else it } val newApps = apps.map { if (!it.selected) it.copy(selected = true) else it }
mSelectedApps.value = SelectedAppsState(newApps, true) mSelectedApps.value = SelectedAppsState(newApps, true, iconsLoaded = true)
} }
} }
@ -214,7 +245,7 @@ internal class RestoreViewModel(
allSelected = allSelected && app.selected allSelected = allSelected && app.selected
} }
} }
mSelectedApps.value = SelectedAppsState(apps, allSelected) mSelectedApps.value = SelectedAppsState(apps, allSelected, iconsLoaded = true)
} }
internal fun onNextClickedAfterSelectingApps() { internal fun onNextClickedAfterSelectingApps() {

View file

@ -69,7 +69,7 @@ internal class SettingsViewModel(
app: Application, app: Application,
settingsManager: SettingsManager, settingsManager: SettingsManager,
keyManager: KeyManager, keyManager: KeyManager,
private val pluginManager: StoragePluginManager, pluginManager: StoragePluginManager,
private val metadataManager: MetadataManager, private val metadataManager: MetadataManager,
private val appListRetriever: AppListRetriever, private val appListRetriever: AppListRetriever,
private val storageBackup: StorageBackup, private val storageBackup: StorageBackup,

View file

@ -15,7 +15,7 @@ abstract class RequireProvisioningViewModel(
protected val app: Application, protected val app: Application,
protected val settingsManager: SettingsManager, protected val settingsManager: SettingsManager,
protected val keyManager: KeyManager, protected val keyManager: KeyManager,
private val pluginManager: StoragePluginManager, protected val pluginManager: StoragePluginManager,
) : AndroidViewModel(app) { ) : AndroidViewModel(app) {
abstract val isRestoreOperation: Boolean abstract val isRestoreOperation: Boolean

View file

@ -29,6 +29,7 @@ internal class ApkBackupManager(
private val settingsManager: SettingsManager, private val settingsManager: SettingsManager,
private val metadataManager: MetadataManager, private val metadataManager: MetadataManager,
private val packageService: PackageService, private val packageService: PackageService,
private val iconManager: IconManager,
private val apkBackup: ApkBackup, private val apkBackup: ApkBackup,
private val pluginManager: StoragePluginManager, private val pluginManager: StoragePluginManager,
private val nm: BackupNotificationManager, private val nm: BackupNotificationManager,
@ -44,6 +45,8 @@ internal class ApkBackupManager(
// Since an APK backup does not change the [packageState], we first record it for all // Since an APK backup does not change the [packageState], we first record it for all
// packages that don't get backed up. // packages that don't get backed up.
recordNotBackedUpPackages() recordNotBackedUpPackages()
// Upload current icons, so we can show them to user before restore
uploadIcons()
// Now, if APK backups are enabled by the user, we back those up. // Now, if APK backups are enabled by the user, we back those up.
if (settingsManager.backupApks()) { if (settingsManager.backupApks()) {
backUpApks() backUpApks()
@ -94,6 +97,17 @@ internal class ApkBackupManager(
} }
} }
private suspend fun uploadIcons() {
try {
val token = settingsManager.getToken() ?: throw IOException("no current token")
pluginManager.appPlugin.getOutputStream(token, FILE_BACKUP_ICONS).use {
iconManager.uploadIcons(it)
}
} catch (e: IOException) {
Log.e(TAG, "Error uploading icons: ", e)
}
}
/** /**
* Backs up an APK for the given [PackageInfo]. * Backs up an APK for the given [PackageInfo].
* *

View file

@ -0,0 +1,111 @@
/*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.worker
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Bitmap.CompressFormat.WEBP_LOSSY
import android.graphics.BitmapFactory
import android.util.Log
import androidx.appcompat.content.res.AppCompatResources.getDrawable
import androidx.core.graphics.drawable.toBitmap
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.transport.backup.PackageService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.util.zip.Deflater.BEST_SPEED
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
internal const val FILE_BACKUP_ICONS = ".backup.icons"
private const val ICON_SIZE = 128
private const val ICON_QUALITY = 75
private const val CACHE_FOLDER = "restore-icons"
private val TAG = IconManager::class.simpleName
internal class IconManager(
private val context: Context,
private val packageService: PackageService,
) {
@Throws(IOException::class)
fun uploadIcons(outputStream: OutputStream) {
Log.d(TAG, "Start uploading icons")
val packageManager = context.packageManager
ZipOutputStream(outputStream).use { zip ->
zip.setLevel(BEST_SPEED)
packageService.allUserPackages.forEach {
val drawable = packageManager.getApplicationIcon(it.applicationInfo)
if (packageManager.isDefaultApplicationIcon(drawable)) return@forEach
val entry = ZipEntry(it.packageName)
zip.putNextEntry(entry)
drawable.toBitmap(ICON_SIZE, ICON_SIZE).compress(WEBP_LOSSY, ICON_QUALITY, zip)
zip.closeEntry()
}
}
Log.d(TAG, "Finished uploading icons")
}
/**
* Downloads icons file from given [inputStream].
* @return a set of package names for which icons were found
*/
@Throws(IOException::class, SecurityException::class)
fun downloadIcons(inputStream: InputStream): Set<String> {
Log.d(TAG, "Start downloading icons")
val folder = File(context.cacheDir, CACHE_FOLDER)
if (!folder.isDirectory && !folder.mkdirs())
throw IOException("Can't create cache folder for icons")
val set = mutableSetOf<String>()
ZipInputStream(inputStream).use { zip ->
var entry = zip.nextEntry
while (entry != null) {
File(folder, entry.name).outputStream().use { outputStream ->
zip.copyTo(outputStream)
}
set.add(entry.name)
entry = zip.nextEntry
}
}
Log.d(TAG, "Finished downloading icons")
return set
}
private val defaultIcon by lazy {
getDrawable(context, R.drawable.ic_launcher_default)!!.toBitmap()
}
/**
* Tries to load the icons for the given [packageName]
* that was downloaded before with [downloadIcons].
* Calls [callback] on the UiThread with the loaded [Bitmap] or the default icon.
*/
suspend fun loadIcon(packageName: String, callback: (Bitmap) -> Unit) {
try {
withContext(Dispatchers.IO) {
val folder = File(context.cacheDir, CACHE_FOLDER)
val file = File(folder, packageName)
file.inputStream().use { inputStream ->
val bitmap = BitmapFactory.decodeStream(inputStream)
withContext(Dispatchers.Main) {
callback(bitmap)
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Error loading icon for $packageName", e)
withContext(Dispatchers.Main) {
callback(defaultIcon)
}
}
}
}

View file

@ -16,6 +16,12 @@ val workerModule = module {
packageService = get(), packageService = get(),
) )
} }
factory {
IconManager(
context = androidContext(),
packageService = get(),
)
}
single { single {
ApkBackup( ApkBackup(
pm = androidContext().packageManager, pm = androidContext().packageManager,
@ -31,6 +37,7 @@ val workerModule = module {
metadataManager = get(), metadataManager = get(),
packageService = get(), packageService = get(),
apkBackup = get(), apkBackup = get(),
iconManager = get(),
pluginManager = get(), pluginManager = get(),
nm = get() nm = get()
) )

View file

@ -31,6 +31,7 @@ import io.mockk.verify
import io.mockk.verifyAll import io.mockk.verifyAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
@ -38,6 +39,7 @@ internal class ApkBackupManagerTest : TransportTest() {
private val packageService: PackageService = mockk() private val packageService: PackageService = mockk()
private val apkBackup: ApkBackup = mockk() private val apkBackup: ApkBackup = mockk()
private val iconManager: IconManager = mockk()
private val storagePluginManager: StoragePluginManager = mockk() private val storagePluginManager: StoragePluginManager = mockk()
private val plugin: StoragePlugin<*> = mockk() private val plugin: StoragePlugin<*> = mockk()
private val nm: BackupNotificationManager = mockk() private val nm: BackupNotificationManager = mockk()
@ -48,6 +50,7 @@ internal class ApkBackupManagerTest : TransportTest() {
metadataManager = metadataManager, metadataManager = metadataManager,
packageService = packageService, packageService = packageService,
apkBackup = apkBackup, apkBackup = apkBackup,
iconManager = iconManager,
pluginManager = storagePluginManager, pluginManager = storagePluginManager,
nm = nm, nm = nm,
) )
@ -64,6 +67,8 @@ internal class ApkBackupManagerTest : TransportTest() {
every { nm.onAppsNotBackedUp() } just Runs every { nm.onAppsNotBackedUp() } just Runs
every { packageService.notBackedUpPackages } returns listOf(packageInfo) every { packageService.notBackedUpPackages } returns listOf(packageInfo)
expectUploadIcons()
every { every {
metadataManager.getPackageMetadata(packageInfo.packageName) metadataManager.getPackageMetadata(packageInfo.packageName)
} returns packageMetadata } returns packageMetadata
@ -87,6 +92,8 @@ internal class ApkBackupManagerTest : TransportTest() {
every { nm.onAppsNotBackedUp() } just Runs every { nm.onAppsNotBackedUp() } just Runs
every { packageService.notBackedUpPackages } returns listOf(packageInfo) every { packageService.notBackedUpPackages } returns listOf(packageInfo)
expectUploadIcons()
every { every {
metadataManager.getPackageMetadata(packageInfo.packageName) metadataManager.getPackageMetadata(packageInfo.packageName)
} returns null } returns null
@ -116,6 +123,8 @@ internal class ApkBackupManagerTest : TransportTest() {
every { nm.onAppsNotBackedUp() } just Runs every { nm.onAppsNotBackedUp() } just Runs
every { packageService.notBackedUpPackages } returns listOf(packageInfo) every { packageService.notBackedUpPackages } returns listOf(packageInfo)
expectUploadIcons()
every { every {
metadataManager.getPackageMetadata(packageInfo.packageName) metadataManager.getPackageMetadata(packageInfo.packageName)
} returns packageMetadata } returns packageMetadata
@ -139,6 +148,8 @@ internal class ApkBackupManagerTest : TransportTest() {
every { nm.onAppsNotBackedUp() } just Runs every { nm.onAppsNotBackedUp() } just Runs
every { packageService.notBackedUpPackages } returns listOf(packageInfo) every { packageService.notBackedUpPackages } returns listOf(packageInfo)
expectUploadIcons()
every { every {
metadataManager.getPackageMetadata(packageInfo.packageName) metadataManager.getPackageMetadata(packageInfo.packageName)
} returns packageMetadata } returns packageMetadata
@ -167,7 +178,7 @@ internal class ApkBackupManagerTest : TransportTest() {
} }
} }
) )
expectUploadIcons()
expectAllAppsWillGetBackedUp() expectAllAppsWillGetBackedUp()
every { settingsManager.backupApks() } returns true every { settingsManager.backupApks() } returns true
@ -207,6 +218,8 @@ internal class ApkBackupManagerTest : TransportTest() {
every { nm.onAppsNotBackedUp() } just Runs every { nm.onAppsNotBackedUp() } just Runs
every { packageService.notBackedUpPackages } returns listOf(packageInfo) every { packageService.notBackedUpPackages } returns listOf(packageInfo)
expectUploadIcons()
every { every {
metadataManager.getPackageMetadata(packageInfo.packageName) metadataManager.getPackageMetadata(packageInfo.packageName)
} returns packageMetadata } returns packageMetadata
@ -233,6 +246,12 @@ internal class ApkBackupManagerTest : TransportTest() {
} }
} }
private suspend fun expectUploadIcons() {
val stream = ByteArrayOutputStream()
coEvery { plugin.getOutputStream(token, FILE_BACKUP_ICONS) } returns stream
every { iconManager.uploadIcons(stream) } just Runs
}
private fun expectAllAppsWillGetBackedUp() { private fun expectAllAppsWillGetBackedUp() {
every { nm.onAppsNotBackedUp() } just Runs every { nm.onAppsNotBackedUp() } just Runs
every { packageService.notBackedUpPackages } returns emptyList() every { packageService.notBackedUpPackages } returns emptyList()