Disable auto-restore during install, if it was on

This commit is contained in:
Torsten Grote 2024-08-16 10:47:46 -03:00
parent 09074c5dff
commit 570850aa51
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
4 changed files with 88 additions and 3 deletions

View file

@ -5,11 +5,13 @@
package com.stevesoltys.seedvault.restore.install package com.stevesoltys.seedvault.restore.install
import android.app.backup.IBackupManager
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.PackageManager.GET_SIGNATURES import android.content.pm.PackageManager.GET_SIGNATURES
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
import android.util.Log import android.util.Log
import com.stevesoltys.seedvault.BackupStateManager
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.metadata.ApkSplit import com.stevesoltys.seedvault.metadata.ApkSplit
@ -38,6 +40,8 @@ private val TAG = ApkRestore::class.java.simpleName
internal class ApkRestore( internal class ApkRestore(
private val context: Context, private val context: Context,
private val backupManager: IBackupManager,
private val backupStateManager: BackupStateManager,
private val pluginManager: StoragePluginManager, private val pluginManager: StoragePluginManager,
@Suppress("Deprecation") @Suppress("Deprecation")
private val legacyStoragePlugin: LegacyStoragePlugin, private val legacyStoragePlugin: LegacyStoragePlugin,
@ -81,9 +85,24 @@ internal class ApkRestore(
return return
} }
mInstallResult.value = InstallResult(packages) mInstallResult.value = InstallResult(packages)
val autoRestore = backupStateManager.isAutoRestoreEnabled
try {
// disable auto-restore before installing apps, if it was enabled before
if (autoRestore) backupManager.setAutoRestore(false)
reInstallApps(backup, packages.asIterable().reversed())
} finally {
// re-enable auto-restore, if it was enabled before
if (autoRestore) backupManager.setAutoRestore(true)
}
mInstallResult.update { it.copy(isFinished = true) }
}
private suspend fun reInstallApps(
backup: RestorableBackup,
packages: List<Map.Entry<String, ApkInstallResult>>,
) {
// re-install individual packages and emit updates (start from last and work your way up) // re-install individual packages and emit updates (start from last and work your way up)
for ((packageName, apkInstallResult) in packages.asIterable().reversed()) { for ((packageName, apkInstallResult) in packages) {
try { try {
if (isInstalled(packageName, apkInstallResult.metadata)) { if (isInstalled(packageName, apkInstallResult.metadata)) {
mInstallResult.update { result -> mInstallResult.update { result ->
@ -108,7 +127,6 @@ internal class ApkRestore(
mInstallResult.update { it.fail(packageName) } mInstallResult.update { it.fail(packageName) }
} }
} }
mInstallResult.update { it.copy(isFinished = true) }
} }
@Throws(SecurityException::class) @Throws(SecurityException::class)

View file

@ -14,7 +14,7 @@ val installModule = module {
factory { DeviceInfo(androidContext()) } factory { DeviceInfo(androidContext()) }
factory { ApkSplitCompatibilityChecker(get()) } factory { ApkSplitCompatibilityChecker(get()) }
factory { factory {
ApkRestore(androidContext(), get(), get(), get(), get(), get()) { ApkRestore(androidContext(), get(), get(), get(), get(), get(), get(), get()) {
androidContext().getSystemService(UserManager::class.java)!!.isAllowedToInstallApks() androidContext().getSystemService(UserManager::class.java)!!.isAllowedToInstallApks()
} }
} }

View file

@ -5,6 +5,7 @@
package com.stevesoltys.seedvault.restore.install package com.stevesoltys.seedvault.restore.install
import android.app.backup.IBackupManager
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.PackageManager.NameNotFoundException import android.content.pm.PackageManager.NameNotFoundException
@ -12,6 +13,7 @@ import android.content.pm.Signature
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.PackageUtils import android.util.PackageUtils
import app.cash.turbine.test import app.cash.turbine.test
import com.stevesoltys.seedvault.BackupStateManager
import com.stevesoltys.seedvault.assertReadEquals import com.stevesoltys.seedvault.assertReadEquals
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.ApkSplit import com.stevesoltys.seedvault.metadata.ApkSplit
@ -57,6 +59,8 @@ internal class ApkBackupRestoreTest : TransportTest() {
} }
private val storagePluginManager: StoragePluginManager = mockk() private val storagePluginManager: StoragePluginManager = mockk()
private val backupManager: IBackupManager = mockk()
private val backupStateManager: BackupStateManager = mockk()
@Suppress("Deprecation") @Suppress("Deprecation")
private val legacyStoragePlugin: LegacyStoragePlugin = mockk() private val legacyStoragePlugin: LegacyStoragePlugin = mockk()
@ -68,6 +72,8 @@ internal class ApkBackupRestoreTest : TransportTest() {
private val apkBackup = ApkBackup(pm, crypto, settingsManager, metadataManager) private val apkBackup = ApkBackup(pm, crypto, settingsManager, metadataManager)
private val apkRestore: ApkRestore = ApkRestore( private val apkRestore: ApkRestore = ApkRestore(
context = strictContext, context = strictContext,
backupManager = backupManager,
backupStateManager = backupStateManager,
pluginManager = storagePluginManager, pluginManager = storagePluginManager,
legacyStoragePlugin = legacyStoragePlugin, legacyStoragePlugin = legacyStoragePlugin,
crypto = crypto, crypto = crypto,
@ -146,6 +152,7 @@ internal class ApkBackupRestoreTest : TransportTest() {
val cacheFiles = slot<List<File>>() val cacheFiles = slot<List<File>>()
every { installRestriction.isAllowedToInstallApks() } returns true every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException() every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException()
every { strictContext.cacheDir } returns tmpFile every { strictContext.cacheDir } returns tmpFile
every { crypto.getNameForApk(salt, packageName, "") } returns name every { crypto.getNameForApk(salt, packageName, "") } returns name

View file

@ -5,6 +5,7 @@
package com.stevesoltys.seedvault.restore.install package com.stevesoltys.seedvault.restore.install
import android.app.backup.IBackupManager
import android.content.Context import android.content.Context
import android.content.pm.ApplicationInfo.FLAG_INSTALLED import android.content.pm.ApplicationInfo.FLAG_INSTALLED
import android.content.pm.ApplicationInfo.FLAG_SYSTEM import android.content.pm.ApplicationInfo.FLAG_SYSTEM
@ -15,6 +16,7 @@ import android.content.pm.PackageManager.NameNotFoundException
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import app.cash.turbine.TurbineTestContext import app.cash.turbine.TurbineTestContext
import app.cash.turbine.test import app.cash.turbine.test
import com.stevesoltys.seedvault.BackupStateManager
import com.stevesoltys.seedvault.getRandomBase64 import com.stevesoltys.seedvault.getRandomBase64
import com.stevesoltys.seedvault.getRandomByteArray import com.stevesoltys.seedvault.getRandomByteArray
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString
@ -32,10 +34,13 @@ import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED
import com.stevesoltys.seedvault.transport.TransportTest import com.stevesoltys.seedvault.transport.TransportTest
import com.stevesoltys.seedvault.worker.getSignatures import com.stevesoltys.seedvault.worker.getSignatures
import io.mockk.Runs
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.every import io.mockk.every
import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.verifyOrder
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
@ -57,6 +62,8 @@ internal class ApkRestoreTest : TransportTest() {
private val strictContext: Context = mockk<Context>().apply { private val strictContext: Context = mockk<Context>().apply {
every { packageManager } returns pm every { packageManager } returns pm
} }
private val backupManager: IBackupManager = mockk()
private val backupStateManager: BackupStateManager = mockk()
private val storagePluginManager: StoragePluginManager = mockk() private val storagePluginManager: StoragePluginManager = mockk()
private val storagePlugin: StoragePlugin<*> = mockk() private val storagePlugin: StoragePlugin<*> = mockk()
private val legacyStoragePlugin: LegacyStoragePlugin = mockk() private val legacyStoragePlugin: LegacyStoragePlugin = mockk()
@ -66,6 +73,8 @@ internal class ApkRestoreTest : TransportTest() {
private val apkRestore: ApkRestore = ApkRestore( private val apkRestore: ApkRestore = ApkRestore(
context = strictContext, context = strictContext,
backupManager = backupManager,
backupStateManager = backupStateManager,
pluginManager = storagePluginManager, pluginManager = storagePluginManager,
legacyStoragePlugin = legacyStoragePlugin, legacyStoragePlugin = legacyStoragePlugin,
crypto = crypto, crypto = crypto,
@ -107,6 +116,7 @@ internal class ApkRestoreTest : TransportTest() {
val backup = swapPackages(hashMapOf(packageName to packageMetadata)) val backup = swapPackages(hashMapOf(packageName to packageMetadata))
every { installRestriction.isAllowedToInstallApks() } returns true every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { strictContext.cacheDir } returns File(tmpDir.toString()) every { strictContext.cacheDir } returns File(tmpDir.toString())
every { crypto.getNameForApk(salt, packageName, "") } returns name every { crypto.getNameForApk(salt, packageName, "") } returns name
coEvery { storagePlugin.getInputStream(token, name) } returns apkInputStream coEvery { storagePlugin.getInputStream(token, name) } returns apkInputStream
@ -131,6 +141,7 @@ internal class ApkRestoreTest : TransportTest() {
val backup = swapPackages(hashMapOf(packageName to packageMetadata)) val backup = swapPackages(hashMapOf(packageName to packageMetadata))
every { installRestriction.isAllowedToInstallApks() } returns true every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { storagePlugin.providerPackageName } returns storageProviderPackageName every { storagePlugin.providerPackageName } returns storageProviderPackageName
every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException() every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException()
@ -156,6 +167,7 @@ internal class ApkRestoreTest : TransportTest() {
val backup = swapPackages(hashMapOf(packageName to packageMetadata)) val backup = swapPackages(hashMapOf(packageName to packageMetadata))
every { installRestriction.isAllowedToInstallApks() } returns true every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { storagePlugin.providerPackageName } returns storageProviderPackageName every { storagePlugin.providerPackageName } returns storageProviderPackageName
val packageInfo: PackageInfo = mockk() val packageInfo: PackageInfo = mockk()
@ -178,6 +190,7 @@ internal class ApkRestoreTest : TransportTest() {
packageInfo.packageName = getRandomString() packageInfo.packageName = getRandomString()
every { installRestriction.isAllowedToInstallApks() } returns true every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { strictContext.cacheDir } returns File(tmpDir.toString()) every { strictContext.cacheDir } returns File(tmpDir.toString())
every { crypto.getNameForApk(salt, packageName, "") } returns name every { crypto.getNameForApk(salt, packageName, "") } returns name
coEvery { storagePlugin.getInputStream(token, name) } returns apkInputStream coEvery { storagePlugin.getInputStream(token, name) } returns apkInputStream
@ -194,6 +207,7 @@ internal class ApkRestoreTest : TransportTest() {
@Test @Test
fun `test apkInstaller throws exceptions`(@TempDir tmpDir: Path) = runBlocking { fun `test apkInstaller throws exceptions`(@TempDir tmpDir: Path) = runBlocking {
every { installRestriction.isAllowedToInstallApks() } returns true every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException() every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException()
cacheBaseApkAndGetInfo(tmpDir) cacheBaseApkAndGetInfo(tmpDir)
coEvery { coEvery {
@ -220,6 +234,7 @@ internal class ApkRestoreTest : TransportTest() {
val installResult = InstallResult(packagesMap) val installResult = InstallResult(packagesMap)
every { installRestriction.isAllowedToInstallApks() } returns true every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException() every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException()
cacheBaseApkAndGetInfo(tmpDir) cacheBaseApkAndGetInfo(tmpDir)
coEvery { coEvery {
@ -249,6 +264,7 @@ internal class ApkRestoreTest : TransportTest() {
val installResult = InstallResult(packagesMap) val installResult = InstallResult(packagesMap)
every { installRestriction.isAllowedToInstallApks() } returns true every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException() every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException()
every { strictContext.cacheDir } returns File(tmpDir.toString()) every { strictContext.cacheDir } returns File(tmpDir.toString())
coEvery { coEvery {
@ -274,6 +290,7 @@ internal class ApkRestoreTest : TransportTest() {
val packageInfo: PackageInfo = mockk() val packageInfo: PackageInfo = mockk()
mockkStatic("com.stevesoltys.seedvault.worker.ApkBackupKt") mockkStatic("com.stevesoltys.seedvault.worker.ApkBackupKt")
every { installRestriction.isAllowedToInstallApks() } returns true every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { storagePlugin.providerPackageName } returns storageProviderPackageName every { storagePlugin.providerPackageName } returns storageProviderPackageName
every { pm.getPackageInfo(packageName, any<Int>()) } returns packageInfo every { pm.getPackageInfo(packageName, any<Int>()) } returns packageInfo
every { packageInfo.signingInfo.getSignatures() } returns packageMetadata.signatures!! every { packageInfo.signingInfo.getSignatures() } returns packageMetadata.signatures!!
@ -302,6 +319,7 @@ internal class ApkRestoreTest : TransportTest() {
val packageInfo: PackageInfo = mockk() val packageInfo: PackageInfo = mockk()
mockkStatic("com.stevesoltys.seedvault.worker.ApkBackupKt") mockkStatic("com.stevesoltys.seedvault.worker.ApkBackupKt")
every { installRestriction.isAllowedToInstallApks() } returns true every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { storagePlugin.providerPackageName } returns storageProviderPackageName every { storagePlugin.providerPackageName } returns storageProviderPackageName
every { pm.getPackageInfo(packageName, any<Int>()) } returns packageInfo every { pm.getPackageInfo(packageName, any<Int>()) } returns packageInfo
every { packageInfo.signingInfo.getSignatures() } returns packageMetadata.signatures!! every { packageInfo.signingInfo.getSignatures() } returns packageMetadata.signatures!!
@ -341,6 +359,7 @@ internal class ApkRestoreTest : TransportTest() {
val packageInfo: PackageInfo = mockk() val packageInfo: PackageInfo = mockk()
mockkStatic("com.stevesoltys.seedvault.worker.ApkBackupKt") mockkStatic("com.stevesoltys.seedvault.worker.ApkBackupKt")
every { installRestriction.isAllowedToInstallApks() } returns true every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { storagePlugin.providerPackageName } returns storageProviderPackageName every { storagePlugin.providerPackageName } returns storageProviderPackageName
every { pm.getPackageInfo(packageName, any<Int>()) } returns packageInfo every { pm.getPackageInfo(packageName, any<Int>()) } returns packageInfo
every { packageInfo.signingInfo.getSignatures() } returns listOf("foobar") every { packageInfo.signingInfo.getSignatures() } returns listOf("foobar")
@ -370,6 +389,7 @@ internal class ApkRestoreTest : TransportTest() {
val isSystemApp = Random.nextBoolean() val isSystemApp = Random.nextBoolean()
every { installRestriction.isAllowedToInstallApks() } returns true every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException() every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException()
cacheBaseApkAndGetInfo(tmpDir) cacheBaseApkAndGetInfo(tmpDir)
every { storagePlugin.providerPackageName } returns storageProviderPackageName every { storagePlugin.providerPackageName } returns storageProviderPackageName
@ -438,6 +458,7 @@ internal class ApkRestoreTest : TransportTest() {
) )
every { installRestriction.isAllowedToInstallApks() } returns true every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException() every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException()
// cache APK and get icon as well as app name // cache APK and get icon as well as app name
cacheBaseApkAndGetInfo(tmpDir) cacheBaseApkAndGetInfo(tmpDir)
@ -464,6 +485,7 @@ internal class ApkRestoreTest : TransportTest() {
) )
every { installRestriction.isAllowedToInstallApks() } returns true every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException() every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException()
// cache APK and get icon as well as app name // cache APK and get icon as well as app name
cacheBaseApkAndGetInfo(tmpDir) cacheBaseApkAndGetInfo(tmpDir)
@ -493,6 +515,7 @@ internal class ApkRestoreTest : TransportTest() {
) )
every { installRestriction.isAllowedToInstallApks() } returns true every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException() every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException()
// cache APK and get icon as well as app name // cache APK and get icon as well as app name
cacheBaseApkAndGetInfo(tmpDir) cacheBaseApkAndGetInfo(tmpDir)
@ -524,6 +547,7 @@ internal class ApkRestoreTest : TransportTest() {
) )
every { installRestriction.isAllowedToInstallApks() } returns true every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException() every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException()
// cache APK and get icon as well as app name // cache APK and get icon as well as app name
cacheBaseApkAndGetInfo(tmpDir) cacheBaseApkAndGetInfo(tmpDir)
@ -566,6 +590,7 @@ internal class ApkRestoreTest : TransportTest() {
@Test @Test
fun `storage provider app does not get reinstalled`() = runBlocking { fun `storage provider app does not get reinstalled`() = runBlocking {
every { installRestriction.isAllowedToInstallApks() } returns true every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
// set the storage provider package name to match our current package name, // set the storage provider package name to match our current package name,
// and ensure that the current package is therefore skipped. // and ensure that the current package is therefore skipped.
every { storagePlugin.providerPackageName } returns packageName every { storagePlugin.providerPackageName } returns packageName
@ -592,6 +617,7 @@ internal class ApkRestoreTest : TransportTest() {
).also { assertFalse(it.hasApk()) } ).also { assertFalse(it.hasApk()) }
every { installRestriction.isAllowedToInstallApks() } returns true every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { storagePlugin.providerPackageName } returns storageProviderPackageName every { storagePlugin.providerPackageName } returns storageProviderPackageName
apkRestore.installResult.test { apkRestore.installResult.test {
@ -608,6 +634,40 @@ internal class ApkRestoreTest : TransportTest() {
} }
} }
@Test
fun `auto restore gets turned off, if it was on`(@TempDir tmpDir: Path) = runBlocking {
val packagesMap = mapOf(
packageName to ApkInstallResult(
packageName,
state = SUCCEEDED,
metadata = PackageMetadata(),
)
)
val installResult = InstallResult(packagesMap)
every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns true
every { storagePlugin.providerPackageName } returns storageProviderPackageName
every { backupManager.setAutoRestore(false) } just Runs
every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException()
// cache APK and get icon as well as app name
cacheBaseApkAndGetInfo(tmpDir)
coEvery {
apkInstaller.install(match { it.size == 1 }, packageName, installerName, any())
} returns installResult
every { backupManager.setAutoRestore(true) } just Runs
apkRestore.installResult.test {
awaitItem() // initial empty state
apkRestore.restore(backup)
assertQueuedProgressSuccessFinished()
}
verifyOrder {
backupManager.setAutoRestore(false)
backupManager.setAutoRestore(true)
}
}
@Test @Test
fun `no apks get installed when blocked by policy`() = runBlocking { fun `no apks get installed when blocked by policy`() = runBlocking {
every { installRestriction.isAllowedToInstallApks() } returns false every { installRestriction.isAllowedToInstallApks() } returns false