Skip to content

Commit

Permalink
WIP improve ApkRestore
Browse files Browse the repository at this point in the history
  • Loading branch information
grote committed May 28, 2024
1 parent 1d6c473 commit 6a4cb3f
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 141 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,7 @@ internal interface LargeRestoreTestBase : LargeTestBase {

withContext(Dispatchers.Main) {
withTimeout(RESTORE_TIMEOUT) {
while (spyRestoreViewModel.installResult.value == null ||
spyRestoreViewModel.nextButtonEnabled.value == false
) {
while (spyRestoreViewModel.installResult.value?.isFinished != true) {
delay(100)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import androidx.appcompat.content.res.AppCompatResources.getDrawable
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope
import com.stevesoltys.seedvault.BackupMonitor
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
Expand Down Expand Up @@ -117,16 +116,9 @@ internal class RestoreViewModel(
internal val selectedApps: LiveData<SelectedAppsState> =
appSelectionManager.selectedAppsLiveData

internal val installResult: LiveData<InstallResult> =
mChosenRestorableBackup.switchMap { backup ->
// TODO does this stay stable when re-observing this LiveData?
// TODO pass in app selection done by user
getInstallResult(backup)
}
internal val installIntentCreator by lazy { InstallIntentCreator(app.packageManager) }
internal val installResult: LiveData<InstallResult> = apkRestore.installResult.asLiveData()

private val mNextButtonEnabled = MutableLiveData<Boolean>().apply { value = false }
internal val nextButtonEnabled: LiveData<Boolean> = mNextButtonEnabled
internal val installIntentCreator by lazy { InstallIntentCreator(app.packageManager) }

private val mRestoreProgress = MutableLiveData<LinkedList<AppRestoreResult>>().apply {
value = LinkedList<AppRestoreResult>().apply {
Expand Down Expand Up @@ -193,34 +185,25 @@ internal class RestoreViewModel(
}
}

suspend fun loadIcon(packageName: String, callback: (Drawable) -> Unit) {
iconManager.loadIcon(packageName, callback)
}

fun onCheckAllAppsClicked() = appSelectionManager.onCheckAllAppsClicked()
fun onAppSelected(item: SelectableAppItem) = appSelectionManager.onAppSelected(item)

internal fun onNextClickedAfterSelectingApps() {
val backup = chosenRestorableBackup.value ?: error("No chosen backup")
// replace original chosen backup with unselected packages removed
mChosenRestorableBackup.value = appSelectionManager.onAppSelectionFinished(backup)
val filteredBackup = appSelectionManager.onAppSelectionFinished(backup)
mChosenRestorableBackup.value = filteredBackup
viewModelScope.launch(ioDispatcher) {
apkRestore.restore(filteredBackup)
}
// tell UI to move to InstallFragment
mDisplayFragment.setEvent(RESTORE_APPS)
}

private fun getInstallResult(backup: RestorableBackup): LiveData<InstallResult> {
@Suppress("EXPERIMENTAL_API_USAGE")
return apkRestore.restore(backup)
.onStart {
Log.d(TAG, "Start InstallResult Flow")
}.catch { e ->
Log.d(TAG, "Exception in InstallResult Flow", e)
}.onCompletion { e ->
Log.d(TAG, "Completed InstallResult Flow", e)
mNextButtonEnabled.postValue(true)
}
.flowOn(ioDispatcher)
// collect on the same thread, so concurrency issues don't mess up live data updates
// e.g. InstallResult#isFinished isn't reported too early.
.asLiveData(ioDispatcher)
}

internal fun onNextClickedAfterInstallingApps() {
mDisplayFragment.postEvent(RESTORE_BACKUP)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ internal class ApkInstaller(private val context: Context) {
cachedApks: List<File>,
packageName: String,
installerPackageName: String?,
installResult: MutableInstallResult,
) = suspendCancellableCoroutine<InstallResult> { cont ->
installResult: InstallResult,
) = suspendCancellableCoroutine { cont ->
val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, i: Intent) {
if (i.action != BROADCAST_ACTION) return
Expand Down Expand Up @@ -110,7 +110,7 @@ internal class ApkInstaller(private val context: Context) {
i: Intent,
expectedPackageName: String,
cachedApks: List<File>,
installResult: MutableInstallResult,
installResult: InstallResult,
): InstallResult {
val packageName = i.getStringExtra(EXTRA_PACKAGE_NAME)!!
val success = i.getIntExtra(EXTRA_STATUS, -1) == STATUS_SUCCESS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import com.stevesoltys.seedvault.transport.backup.isSystemApp
import com.stevesoltys.seedvault.worker.copyStreamsAndGetHash
import com.stevesoltys.seedvault.worker.getSignatures
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.io.File
import java.io.IOException

Expand All @@ -47,71 +47,72 @@ internal class ApkRestore(
private val pm = context.packageManager
private val storagePlugin get() = pluginManager.appPlugin

fun restore(backup: RestorableBackup) = flow {
private val mInstallResult = MutableStateFlow(InstallResult(0))
val installResult = mInstallResult.asStateFlow()

suspend fun restore(backup: RestorableBackup) {
// we don't filter out apps without APK, so the user can manually install them
val packages = backup.packageMetadataMap.filter {
// We need to exclude the DocumentsProvider used to retrieve backup data.
// Otherwise, it gets killed when we install it, terminating our restoration.
it.key != storagePlugin.providerPackageName
}
val isAllowedToInstallApks = installRestriction.isAllowedToInstallApks()
val total = packages.size
var progress = 0

// queue all packages and emit LiveData
val installResult = MutableInstallResult(total)
packages.forEach { (packageName, metadata) ->
// we don't filter out apps without APK, so the user can manually install them
val results = packages.mapValues { (packageName, metadata) ->
progress++
installResult[packageName] = ApkInstallResult(
ApkInstallResult(
packageName = packageName,
progress = progress,
state = if (isAllowedToInstallApks) QUEUED else FAILED,
name = metadata.name?.toString(),
installerPackageName = metadata.installer
installerPackageName = metadata.installer,
)
} as MutableMap
if (!isAllowedToInstallApks) {
mInstallResult.value = InstallResult(
total = results.size,
isFinished = true,
installResults = results,
)
return
}
if (isAllowedToInstallApks) {
emit(installResult)
} else {
installResult.isFinished = true
emit(installResult)
return@flow
}
mInstallResult.value = InstallResult(results.size, installResults = results)

// re-install individual packages and emit updates
for ((packageName, metadata) in packages) {
Log.e("TEST", "LOOP ${installResult.value[packageName]?.progress} $packageName")
try {
if (metadata.hasApk()) {
restore(this, backup, packageName, metadata, installResult)
restore(backup, packageName, metadata)
} else {
emit(installResult.fail(packageName))
mInstallResult.value = installResult.value.fail(packageName)
}
} catch (e: IOException) {
Log.e(TAG, "Error re-installing APK for $packageName.", e)
emit(installResult.fail(packageName))
mInstallResult.value = installResult.value.fail(packageName)
} catch (e: SecurityException) {
Log.e(TAG, "Security error re-installing APK for $packageName.", e)
emit(installResult.fail(packageName))
mInstallResult.value = installResult.value.fail(packageName)
} catch (e: TimeoutCancellationException) {
Log.e(TAG, "Timeout while re-installing APK for $packageName.", e)
emit(installResult.fail(packageName))
mInstallResult.value = installResult.value.fail(packageName)
} catch (e: Exception) {
Log.e(TAG, "Unexpected exception while re-installing APK for $packageName.", e)
emit(installResult.fail(packageName))
mInstallResult.value = installResult.value.fail(packageName)
}
}
installResult.isFinished = true
emit(installResult)
mInstallResult.value = installResult.value.copy(isFinished = true)
}

@Suppress("ThrowsCount")
@Throws(IOException::class, SecurityException::class)
private suspend fun restore(
collector: FlowCollector<InstallResult>,
backup: RestorableBackup,
packageName: String,
metadata: PackageMetadata,
installResult: MutableInstallResult,
) {
// cache the APK and get its hash
val (cachedApk, sha256) = cacheApk(backup.version, backup.token, backup.salt, packageName)
Expand Down Expand Up @@ -156,32 +157,28 @@ internal class ApkRestore(
val icon = appInfo?.loadIcon(pm)
val name = appInfo?.let { pm.getApplicationLabel(it).toString() }

installResult.update(packageName) { result ->
mInstallResult.value = installResult.value.update(packageName) { result ->
result.copy(state = IN_PROGRESS, name = name, icon = icon)
}
collector.emit(installResult)

// ensure system apps are actually already installed and newer system apps as well
if (metadata.system) {
shouldInstallSystemApp(packageName, metadata, installResult)?.let {
collector.emit(it)
return
}
if (metadata.system) shouldInstallSystemApp(packageName, metadata)?.let {
mInstallResult.value = it
return
}

// process further APK splits, if available
val cachedApks =
cacheSplitsIfNeeded(backup, packageName, cachedApk, metadata.splits)
val cachedApks = cacheSplitsIfNeeded(backup, packageName, cachedApk, metadata.splits)
if (cachedApks == null) {
Log.w(TAG, "Not installing $packageName because of incompatible splits.")
collector.emit(installResult.fail(packageName))
mInstallResult.value = installResult.value.fail(packageName)
return
}

// install APK and emit updates from it
val result =
apkInstaller.install(cachedApks, packageName, metadata.installer, installResult)
collector.emit(result)
apkInstaller.install(cachedApks, packageName, metadata.installer, installResult.value)
mInstallResult.value = result
}

/**
Expand Down Expand Up @@ -240,7 +237,6 @@ internal class ApkRestore(
val cachedApk = File.createTempFile(packageName + suffix, ".apk", context.cacheDir)
// copy APK to cache file and calculate SHA-256 hash while we are at it
val inputStream = if (version == 0.toByte()) {
@Suppress("Deprecation")
legacyStoragePlugin.getApkInputStream(token, packageName, suffix)
} else {
val name = crypto.getNameForApk(salt, packageName, suffix)
Expand All @@ -257,23 +253,22 @@ internal class ApkRestore(
private fun shouldInstallSystemApp(
packageName: String,
metadata: PackageMetadata,
installResult: MutableInstallResult,
): InstallResult? {
val installedPackageInfo = try {
pm.getPackageInfo(packageName, 0)
} catch (e: PackageManager.NameNotFoundException) {
Log.w(TAG, "Not installing system app $packageName because not installed here.")
// we report a different FAILED status here to prevent manual installs
return installResult.fail(packageName, FAILED_SYSTEM_APP)
return installResult.value.fail(packageName, FAILED_SYSTEM_APP)
}
// metadata.version is not null, because here hasApk() must be true
val isOlder = metadata.version!! <= installedPackageInfo.longVersionCode
return if (isOlder) {
Log.w(TAG, "Not installing $packageName because ours is older.")
installResult.update(packageName) { it.copy(state = SUCCEEDED) }
installResult.value.update(packageName) { it.copy(state = SUCCEEDED) }
} else if (!installedPackageInfo.isSystemApp()) {
Log.w(TAG, "Not installing $packageName because not a system app here.")
installResult.update(packageName) { it.copy(state = SUCCEEDED) }
installResult.value.update(packageName) { it.copy(state = SUCCEEDED) }
} else {
null // everything is good, we can re-install this
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package com.stevesoltys.seedvault.restore.install

import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
Expand All @@ -22,12 +23,17 @@ import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED
import com.stevesoltys.seedvault.ui.AppViewHolder
import com.stevesoltys.seedvault.ui.notification.getAppName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

internal interface InstallItemListener {
fun onFailedItemClicked(item: ApkInstallResult)
}

internal class InstallProgressAdapter(
private val scope: CoroutineScope,
private val iconLoader: suspend (ApkInstallResult, (Drawable) -> Unit) -> Unit,
private val listener: InstallItemListener,
) : Adapter<InstallProgressAdapter.AppInstallViewHolder>() {

Expand Down Expand Up @@ -72,13 +78,19 @@ internal class InstallProgressAdapter(
finished = true
}

internal inner class AppInstallViewHolder(v: View) : AppViewHolder(v) {
override fun onViewRecycled(holder: AppInstallViewHolder) {
holder.iconJob?.cancel()
}

internal inner class AppInstallViewHolder(v: View) : AppViewHolder(v) {
var iconJob: Job? = null
fun bind(item: ApkInstallResult) {
v.setOnClickListener(null)
v.background = null

appIcon.setImageDrawable(item.icon)
if (item.icon == null) iconJob = scope.launch {
iconLoader(item, appIcon::setImageDrawable)
} else appIcon.setImageDrawable(item.icon)
appName.text = item.name ?: getAppName(v.context, item.packageName.toString())
appInfo.visibility = GONE
when (item.state) {
Expand Down
Loading

0 comments on commit 6a4cb3f

Please sign in to comment.