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:
parent
905340770c
commit
5a2f1187a8
11 changed files with 257 additions and 13 deletions
|
@ -46,7 +46,19 @@ class KoinInstrumentationTestApp : App() {
|
|||
|
||||
viewModel {
|
||||
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!!
|
||||
}
|
||||
|
||||
|
|
|
@ -95,7 +95,19 @@ open class App : Application() {
|
|||
)
|
||||
}
|
||||
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()) }
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.stevesoltys.seedvault.restore
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.text.format.DateUtils
|
||||
import android.text.format.Formatter
|
||||
import android.view.LayoutInflater
|
||||
|
@ -14,16 +15,22 @@ import com.stevesoltys.seedvault.R
|
|||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.restore.AppSelectionAdapter.AppSelectionViewHolder
|
||||
import com.stevesoltys.seedvault.ui.AppViewHolder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal data class SelectableAppItem(
|
||||
val packageName: String,
|
||||
val metadata: PackageMetadata,
|
||||
val selected: Boolean,
|
||||
val hasIcon: Boolean? = null,
|
||||
) {
|
||||
val name: String get() = packageName
|
||||
}
|
||||
|
||||
internal class AppSelectionAdapter(
|
||||
val scope: CoroutineScope,
|
||||
val iconLoader: suspend (String, (Bitmap) -> Unit) -> Unit,
|
||||
val listener: (SelectableAppItem) -> Unit,
|
||||
) : Adapter<AppSelectionViewHolder>() {
|
||||
|
||||
|
@ -37,7 +44,7 @@ internal class AppSelectionAdapter(
|
|||
old: SelectableAppItem,
|
||||
new: SelectableAppItem,
|
||||
): Boolean {
|
||||
return old.selected == new.selected
|
||||
return old.selected == new.selected && old.hasIcon == new.hasIcon
|
||||
}
|
||||
}
|
||||
private val differ = AsyncListDiffer(this, diffCallback)
|
||||
|
@ -64,7 +71,14 @@ internal class AppSelectionAdapter(
|
|||
differ.submitList(items)
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: AppSelectionViewHolder) {
|
||||
holder.iconJob?.cancel()
|
||||
}
|
||||
|
||||
internal inner class AppSelectionViewHolder(v: View) : AppViewHolder(v) {
|
||||
|
||||
var iconJob: Job? = null
|
||||
|
||||
fun bind(item: SelectableAppItem) {
|
||||
v.background = clickableBackground
|
||||
v.setOnClickListener {
|
||||
|
@ -76,9 +90,23 @@ internal class AppSelectionAdapter(
|
|||
checkBox.setOnCheckedChangeListener { _, _ ->
|
||||
listener(item)
|
||||
}
|
||||
checkBox.visibility = VISIBLE
|
||||
progressBar.visibility = INVISIBLE
|
||||
checkBox.visibility = if (item.hasIcon == null) INVISIBLE else VISIBLE
|
||||
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)
|
||||
appName.text = item.packageName
|
||||
val time = if (item.metadata.time > 0) DateUtils.getRelativeTimeSpanString(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.stevesoltys.seedvault.restore
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
@ -7,6 +8,7 @@ import android.view.ViewGroup
|
|||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.checkbox.MaterialCheckBox
|
||||
|
@ -18,7 +20,7 @@ class AppSelectionFragment : Fragment() {
|
|||
private val viewModel: RestoreViewModel by sharedViewModel()
|
||||
|
||||
private val layoutManager = LinearLayoutManager(context)
|
||||
private val adapter = AppSelectionAdapter { item ->
|
||||
private val adapter = AppSelectionAdapter(lifecycleScope, this::loadIcon) { item ->
|
||||
viewModel.onAppSelected(item)
|
||||
}
|
||||
|
||||
|
@ -64,7 +66,15 @@ class AppSelectionFragment : Fragment() {
|
|||
viewModel.selectedApps.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(state.apps)
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import android.app.backup.IRestoreObserver
|
|||
import android.app.backup.IRestoreSession
|
||||
import android.app.backup.RestoreSet
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.os.RemoteException
|
||||
import android.os.UserHandle
|
||||
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.RequireProvisioningViewModel
|
||||
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 kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -83,6 +86,7 @@ internal const val PACKAGES_PER_CHUNK = NUM_PACKAGES_PER_TRANSACTION
|
|||
internal class SelectedAppsState(
|
||||
val apps: List<SelectableAppItem>,
|
||||
val allSelected: Boolean,
|
||||
val iconsLoaded: Boolean,
|
||||
)
|
||||
|
||||
internal class RestoreViewModel(
|
||||
|
@ -92,6 +96,7 @@ internal class RestoreViewModel(
|
|||
private val backupManager: IBackupManager,
|
||||
private val restoreCoordinator: RestoreCoordinator,
|
||||
private val apkRestore: ApkRestore,
|
||||
private val iconManager: IconManager,
|
||||
storageBackup: StorageBackup,
|
||||
pluginManager: StoragePluginManager,
|
||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
|
@ -175,6 +180,7 @@ internal class RestoreViewModel(
|
|||
|
||||
override fun onRestorableBackupClicked(restorableBackup: RestorableBackup) {
|
||||
mChosenRestorableBackup.value = restorableBackup
|
||||
// filter and sort app items for display
|
||||
val items = restorableBackup.packageMetadataMap.mapNotNull { (packageName, metadata) ->
|
||||
if (metadata.time == 0L && !metadata.hasApk()) 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)
|
||||
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)
|
||||
}
|
||||
|
||||
suspend fun loadIcon(packageName: String, callback: (Bitmap) -> Unit) {
|
||||
iconManager.loadIcon(packageName, callback)
|
||||
}
|
||||
|
||||
fun onCheckAllAppsClicked() {
|
||||
val apps = selectedApps.value?.apps ?: return
|
||||
val allSelected = apps.all { it.selected }
|
||||
if (allSelected) {
|
||||
// unselect all
|
||||
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 {
|
||||
// select all
|
||||
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
|
||||
}
|
||||
}
|
||||
mSelectedApps.value = SelectedAppsState(apps, allSelected)
|
||||
mSelectedApps.value = SelectedAppsState(apps, allSelected, iconsLoaded = true)
|
||||
}
|
||||
|
||||
internal fun onNextClickedAfterSelectingApps() {
|
||||
|
|
|
@ -69,7 +69,7 @@ internal class SettingsViewModel(
|
|||
app: Application,
|
||||
settingsManager: SettingsManager,
|
||||
keyManager: KeyManager,
|
||||
private val pluginManager: StoragePluginManager,
|
||||
pluginManager: StoragePluginManager,
|
||||
private val metadataManager: MetadataManager,
|
||||
private val appListRetriever: AppListRetriever,
|
||||
private val storageBackup: StorageBackup,
|
||||
|
|
|
@ -15,7 +15,7 @@ abstract class RequireProvisioningViewModel(
|
|||
protected val app: Application,
|
||||
protected val settingsManager: SettingsManager,
|
||||
protected val keyManager: KeyManager,
|
||||
private val pluginManager: StoragePluginManager,
|
||||
protected val pluginManager: StoragePluginManager,
|
||||
) : AndroidViewModel(app) {
|
||||
|
||||
abstract val isRestoreOperation: Boolean
|
||||
|
|
|
@ -29,6 +29,7 @@ internal class ApkBackupManager(
|
|||
private val settingsManager: SettingsManager,
|
||||
private val metadataManager: MetadataManager,
|
||||
private val packageService: PackageService,
|
||||
private val iconManager: IconManager,
|
||||
private val apkBackup: ApkBackup,
|
||||
private val pluginManager: StoragePluginManager,
|
||||
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
|
||||
// packages that don't get backed up.
|
||||
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.
|
||||
if (settingsManager.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].
|
||||
*
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -16,6 +16,12 @@ val workerModule = module {
|
|||
packageService = get(),
|
||||
)
|
||||
}
|
||||
factory {
|
||||
IconManager(
|
||||
context = androidContext(),
|
||||
packageService = get(),
|
||||
)
|
||||
}
|
||||
single {
|
||||
ApkBackup(
|
||||
pm = androidContext().packageManager,
|
||||
|
@ -31,6 +37,7 @@ val workerModule = module {
|
|||
metadataManager = get(),
|
||||
packageService = get(),
|
||||
apkBackup = get(),
|
||||
iconManager = get(),
|
||||
pluginManager = get(),
|
||||
nm = get()
|
||||
)
|
||||
|
|
|
@ -31,6 +31,7 @@ import io.mockk.verify
|
|||
import io.mockk.verifyAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
||||
|
@ -38,6 +39,7 @@ internal class ApkBackupManagerTest : TransportTest() {
|
|||
|
||||
private val packageService: PackageService = mockk()
|
||||
private val apkBackup: ApkBackup = mockk()
|
||||
private val iconManager: IconManager = mockk()
|
||||
private val storagePluginManager: StoragePluginManager = mockk()
|
||||
private val plugin: StoragePlugin<*> = mockk()
|
||||
private val nm: BackupNotificationManager = mockk()
|
||||
|
@ -48,6 +50,7 @@ internal class ApkBackupManagerTest : TransportTest() {
|
|||
metadataManager = metadataManager,
|
||||
packageService = packageService,
|
||||
apkBackup = apkBackup,
|
||||
iconManager = iconManager,
|
||||
pluginManager = storagePluginManager,
|
||||
nm = nm,
|
||||
)
|
||||
|
@ -64,6 +67,8 @@ internal class ApkBackupManagerTest : TransportTest() {
|
|||
every { nm.onAppsNotBackedUp() } just Runs
|
||||
every { packageService.notBackedUpPackages } returns listOf(packageInfo)
|
||||
|
||||
expectUploadIcons()
|
||||
|
||||
every {
|
||||
metadataManager.getPackageMetadata(packageInfo.packageName)
|
||||
} returns packageMetadata
|
||||
|
@ -87,6 +92,8 @@ internal class ApkBackupManagerTest : TransportTest() {
|
|||
every { nm.onAppsNotBackedUp() } just Runs
|
||||
every { packageService.notBackedUpPackages } returns listOf(packageInfo)
|
||||
|
||||
expectUploadIcons()
|
||||
|
||||
every {
|
||||
metadataManager.getPackageMetadata(packageInfo.packageName)
|
||||
} returns null
|
||||
|
@ -116,6 +123,8 @@ internal class ApkBackupManagerTest : TransportTest() {
|
|||
every { nm.onAppsNotBackedUp() } just Runs
|
||||
every { packageService.notBackedUpPackages } returns listOf(packageInfo)
|
||||
|
||||
expectUploadIcons()
|
||||
|
||||
every {
|
||||
metadataManager.getPackageMetadata(packageInfo.packageName)
|
||||
} returns packageMetadata
|
||||
|
@ -139,6 +148,8 @@ internal class ApkBackupManagerTest : TransportTest() {
|
|||
every { nm.onAppsNotBackedUp() } just Runs
|
||||
every { packageService.notBackedUpPackages } returns listOf(packageInfo)
|
||||
|
||||
expectUploadIcons()
|
||||
|
||||
every {
|
||||
metadataManager.getPackageMetadata(packageInfo.packageName)
|
||||
} returns packageMetadata
|
||||
|
@ -167,7 +178,7 @@ internal class ApkBackupManagerTest : TransportTest() {
|
|||
}
|
||||
}
|
||||
)
|
||||
|
||||
expectUploadIcons()
|
||||
expectAllAppsWillGetBackedUp()
|
||||
every { settingsManager.backupApks() } returns true
|
||||
|
||||
|
@ -207,6 +218,8 @@ internal class ApkBackupManagerTest : TransportTest() {
|
|||
every { nm.onAppsNotBackedUp() } just Runs
|
||||
every { packageService.notBackedUpPackages } returns listOf(packageInfo)
|
||||
|
||||
expectUploadIcons()
|
||||
|
||||
every {
|
||||
metadataManager.getPackageMetadata(packageInfo.packageName)
|
||||
} 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() {
|
||||
every { nm.onAppsNotBackedUp() } just Runs
|
||||
every { packageService.notBackedUpPackages } returns emptyList()
|
||||
|
|
Loading…
Reference in a new issue