diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt index ae16652f..dc3af875 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt @@ -5,11 +5,13 @@ package com.stevesoltys.seedvault.restore.install +import android.app.backup.IBackupManager import android.content.Context import android.content.pm.PackageManager import android.content.pm.PackageManager.GET_SIGNATURES import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES import android.util.Log +import com.stevesoltys.seedvault.BackupStateManager import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.metadata.ApkSplit @@ -38,6 +40,8 @@ private val TAG = ApkRestore::class.java.simpleName internal class ApkRestore( private val context: Context, + private val backupManager: IBackupManager, + private val backupStateManager: BackupStateManager, private val pluginManager: StoragePluginManager, @Suppress("Deprecation") private val legacyStoragePlugin: LegacyStoragePlugin, @@ -81,9 +85,24 @@ internal class ApkRestore( return } 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>, + ) { // 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 { if (isInstalled(packageName, apkInstallResult.metadata)) { mInstallResult.update { result -> @@ -108,7 +127,6 @@ internal class ApkRestore( mInstallResult.update { it.fail(packageName) } } } - mInstallResult.update { it.copy(isFinished = true) } } @Throws(SecurityException::class) diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallModule.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallModule.kt index 812e6928..8fbeaaae 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallModule.kt @@ -14,7 +14,7 @@ val installModule = module { factory { DeviceInfo(androidContext()) } factory { ApkSplitCompatibilityChecker(get()) } factory { - ApkRestore(androidContext(), get(), get(), get(), get(), get()) { + ApkRestore(androidContext(), get(), get(), get(), get(), get(), get(), get()) { androidContext().getSystemService(UserManager::class.java)!!.isAllowedToInstallApks() } } diff --git a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt index 0d28f165..562766b7 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt @@ -5,6 +5,7 @@ package com.stevesoltys.seedvault.restore.install +import android.app.backup.IBackupManager import android.content.Context import android.content.pm.PackageManager import android.content.pm.PackageManager.NameNotFoundException @@ -12,6 +13,7 @@ import android.content.pm.Signature import android.graphics.drawable.Drawable import android.util.PackageUtils import app.cash.turbine.test +import com.stevesoltys.seedvault.BackupStateManager import com.stevesoltys.seedvault.assertReadEquals import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.metadata.ApkSplit @@ -57,6 +59,8 @@ internal class ApkBackupRestoreTest : TransportTest() { } private val storagePluginManager: StoragePluginManager = mockk() + private val backupManager: IBackupManager = mockk() + private val backupStateManager: BackupStateManager = mockk() @Suppress("Deprecation") private val legacyStoragePlugin: LegacyStoragePlugin = mockk() @@ -68,6 +72,8 @@ internal class ApkBackupRestoreTest : TransportTest() { private val apkBackup = ApkBackup(pm, crypto, settingsManager, metadataManager) private val apkRestore: ApkRestore = ApkRestore( context = strictContext, + backupManager = backupManager, + backupStateManager = backupStateManager, pluginManager = storagePluginManager, legacyStoragePlugin = legacyStoragePlugin, crypto = crypto, @@ -146,6 +152,7 @@ internal class ApkBackupRestoreTest : TransportTest() { val cacheFiles = slot>() every { installRestriction.isAllowedToInstallApks() } returns true + every { backupStateManager.isAutoRestoreEnabled } returns false every { pm.getPackageInfo(packageName, any()) } throws NameNotFoundException() every { strictContext.cacheDir } returns tmpFile every { crypto.getNameForApk(salt, packageName, "") } returns name diff --git a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt index ccbdab15..8d2548c4 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt @@ -5,6 +5,7 @@ package com.stevesoltys.seedvault.restore.install +import android.app.backup.IBackupManager import android.content.Context import android.content.pm.ApplicationInfo.FLAG_INSTALLED import android.content.pm.ApplicationInfo.FLAG_SYSTEM @@ -15,6 +16,7 @@ import android.content.pm.PackageManager.NameNotFoundException import android.graphics.drawable.Drawable import app.cash.turbine.TurbineTestContext import app.cash.turbine.test +import com.stevesoltys.seedvault.BackupStateManager import com.stevesoltys.seedvault.getRandomBase64 import com.stevesoltys.seedvault.getRandomByteArray 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.transport.TransportTest import com.stevesoltys.seedvault.worker.getSignatures +import io.mockk.Runs import io.mockk.coEvery import io.mockk.every +import io.mockk.just import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.verifyOrder import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Assertions @@ -57,6 +62,8 @@ internal class ApkRestoreTest : TransportTest() { private val strictContext: Context = mockk().apply { every { packageManager } returns pm } + private val backupManager: IBackupManager = mockk() + private val backupStateManager: BackupStateManager = mockk() private val storagePluginManager: StoragePluginManager = mockk() private val storagePlugin: StoragePlugin<*> = mockk() private val legacyStoragePlugin: LegacyStoragePlugin = mockk() @@ -66,6 +73,8 @@ internal class ApkRestoreTest : TransportTest() { private val apkRestore: ApkRestore = ApkRestore( context = strictContext, + backupManager = backupManager, + backupStateManager = backupStateManager, pluginManager = storagePluginManager, legacyStoragePlugin = legacyStoragePlugin, crypto = crypto, @@ -107,6 +116,7 @@ internal class ApkRestoreTest : TransportTest() { val backup = swapPackages(hashMapOf(packageName to packageMetadata)) every { installRestriction.isAllowedToInstallApks() } returns true + every { backupStateManager.isAutoRestoreEnabled } returns false every { strictContext.cacheDir } returns File(tmpDir.toString()) every { crypto.getNameForApk(salt, packageName, "") } returns name coEvery { storagePlugin.getInputStream(token, name) } returns apkInputStream @@ -131,6 +141,7 @@ internal class ApkRestoreTest : TransportTest() { val backup = swapPackages(hashMapOf(packageName to packageMetadata)) every { installRestriction.isAllowedToInstallApks() } returns true + every { backupStateManager.isAutoRestoreEnabled } returns false every { storagePlugin.providerPackageName } returns storageProviderPackageName every { pm.getPackageInfo(packageName, any()) } throws NameNotFoundException() @@ -156,6 +167,7 @@ internal class ApkRestoreTest : TransportTest() { val backup = swapPackages(hashMapOf(packageName to packageMetadata)) every { installRestriction.isAllowedToInstallApks() } returns true + every { backupStateManager.isAutoRestoreEnabled } returns false every { storagePlugin.providerPackageName } returns storageProviderPackageName val packageInfo: PackageInfo = mockk() @@ -178,6 +190,7 @@ internal class ApkRestoreTest : TransportTest() { packageInfo.packageName = getRandomString() every { installRestriction.isAllowedToInstallApks() } returns true + every { backupStateManager.isAutoRestoreEnabled } returns false every { strictContext.cacheDir } returns File(tmpDir.toString()) every { crypto.getNameForApk(salt, packageName, "") } returns name coEvery { storagePlugin.getInputStream(token, name) } returns apkInputStream @@ -194,6 +207,7 @@ internal class ApkRestoreTest : TransportTest() { @Test fun `test apkInstaller throws exceptions`(@TempDir tmpDir: Path) = runBlocking { every { installRestriction.isAllowedToInstallApks() } returns true + every { backupStateManager.isAutoRestoreEnabled } returns false every { pm.getPackageInfo(packageName, any()) } throws NameNotFoundException() cacheBaseApkAndGetInfo(tmpDir) coEvery { @@ -220,6 +234,7 @@ internal class ApkRestoreTest : TransportTest() { val installResult = InstallResult(packagesMap) every { installRestriction.isAllowedToInstallApks() } returns true + every { backupStateManager.isAutoRestoreEnabled } returns false every { pm.getPackageInfo(packageName, any()) } throws NameNotFoundException() cacheBaseApkAndGetInfo(tmpDir) coEvery { @@ -249,6 +264,7 @@ internal class ApkRestoreTest : TransportTest() { val installResult = InstallResult(packagesMap) every { installRestriction.isAllowedToInstallApks() } returns true + every { backupStateManager.isAutoRestoreEnabled } returns false every { pm.getPackageInfo(packageName, any()) } throws NameNotFoundException() every { strictContext.cacheDir } returns File(tmpDir.toString()) coEvery { @@ -274,6 +290,7 @@ internal class ApkRestoreTest : TransportTest() { val packageInfo: PackageInfo = mockk() mockkStatic("com.stevesoltys.seedvault.worker.ApkBackupKt") every { installRestriction.isAllowedToInstallApks() } returns true + every { backupStateManager.isAutoRestoreEnabled } returns false every { storagePlugin.providerPackageName } returns storageProviderPackageName every { pm.getPackageInfo(packageName, any()) } returns packageInfo every { packageInfo.signingInfo.getSignatures() } returns packageMetadata.signatures!! @@ -302,6 +319,7 @@ internal class ApkRestoreTest : TransportTest() { val packageInfo: PackageInfo = mockk() mockkStatic("com.stevesoltys.seedvault.worker.ApkBackupKt") every { installRestriction.isAllowedToInstallApks() } returns true + every { backupStateManager.isAutoRestoreEnabled } returns false every { storagePlugin.providerPackageName } returns storageProviderPackageName every { pm.getPackageInfo(packageName, any()) } returns packageInfo every { packageInfo.signingInfo.getSignatures() } returns packageMetadata.signatures!! @@ -341,6 +359,7 @@ internal class ApkRestoreTest : TransportTest() { val packageInfo: PackageInfo = mockk() mockkStatic("com.stevesoltys.seedvault.worker.ApkBackupKt") every { installRestriction.isAllowedToInstallApks() } returns true + every { backupStateManager.isAutoRestoreEnabled } returns false every { storagePlugin.providerPackageName } returns storageProviderPackageName every { pm.getPackageInfo(packageName, any()) } returns packageInfo every { packageInfo.signingInfo.getSignatures() } returns listOf("foobar") @@ -370,6 +389,7 @@ internal class ApkRestoreTest : TransportTest() { val isSystemApp = Random.nextBoolean() every { installRestriction.isAllowedToInstallApks() } returns true + every { backupStateManager.isAutoRestoreEnabled } returns false every { pm.getPackageInfo(packageName, any()) } throws NameNotFoundException() cacheBaseApkAndGetInfo(tmpDir) every { storagePlugin.providerPackageName } returns storageProviderPackageName @@ -438,6 +458,7 @@ internal class ApkRestoreTest : TransportTest() { ) every { installRestriction.isAllowedToInstallApks() } returns true + every { backupStateManager.isAutoRestoreEnabled } returns false every { pm.getPackageInfo(packageName, any()) } throws NameNotFoundException() // cache APK and get icon as well as app name cacheBaseApkAndGetInfo(tmpDir) @@ -464,6 +485,7 @@ internal class ApkRestoreTest : TransportTest() { ) every { installRestriction.isAllowedToInstallApks() } returns true + every { backupStateManager.isAutoRestoreEnabled } returns false every { pm.getPackageInfo(packageName, any()) } throws NameNotFoundException() // cache APK and get icon as well as app name cacheBaseApkAndGetInfo(tmpDir) @@ -493,6 +515,7 @@ internal class ApkRestoreTest : TransportTest() { ) every { installRestriction.isAllowedToInstallApks() } returns true + every { backupStateManager.isAutoRestoreEnabled } returns false every { pm.getPackageInfo(packageName, any()) } throws NameNotFoundException() // cache APK and get icon as well as app name cacheBaseApkAndGetInfo(tmpDir) @@ -524,6 +547,7 @@ internal class ApkRestoreTest : TransportTest() { ) every { installRestriction.isAllowedToInstallApks() } returns true + every { backupStateManager.isAutoRestoreEnabled } returns false every { pm.getPackageInfo(packageName, any()) } throws NameNotFoundException() // cache APK and get icon as well as app name cacheBaseApkAndGetInfo(tmpDir) @@ -566,6 +590,7 @@ internal class ApkRestoreTest : TransportTest() { @Test fun `storage provider app does not get reinstalled`() = runBlocking { every { installRestriction.isAllowedToInstallApks() } returns true + every { backupStateManager.isAutoRestoreEnabled } returns false // set the storage provider package name to match our current package name, // and ensure that the current package is therefore skipped. every { storagePlugin.providerPackageName } returns packageName @@ -592,6 +617,7 @@ internal class ApkRestoreTest : TransportTest() { ).also { assertFalse(it.hasApk()) } every { installRestriction.isAllowedToInstallApks() } returns true + every { backupStateManager.isAutoRestoreEnabled } returns false every { storagePlugin.providerPackageName } returns storageProviderPackageName 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()) } 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 fun `no apks get installed when blocked by policy`() = runBlocking { every { installRestriction.isAllowedToInstallApks() } returns false