Skip to content

Commit

Permalink
Disable auto-restore during install, if it was on
Browse files Browse the repository at this point in the history
  • Loading branch information
grote committed Aug 16, 2024
1 parent 09074c5 commit 570850a
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<Map.Entry<String, ApkInstallResult>>,
) {
// 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 ->
Expand All @@ -108,7 +127,6 @@ internal class ApkRestore(
mInstallResult.update { it.fail(packageName) }
}
}
mInstallResult.update { it.copy(isFinished = true) }
}

@Throws(SecurityException::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@

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
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
Expand Down Expand Up @@ -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()
Expand All @@ -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,
Expand Down Expand Up @@ -146,6 +152,7 @@ internal class ApkBackupRestoreTest : TransportTest() {
val cacheFiles = slot<List<File>>()

every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException()
every { strictContext.cacheDir } returns tmpFile
every { crypto.getNameForApk(salt, packageName, "") } returns name
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.app.backup.IBackupManager
import android.content.Context
import android.content.pm.ApplicationInfo.FLAG_INSTALLED
import android.content.pm.ApplicationInfo.FLAG_SYSTEM
Expand All @@ -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
Expand All @@ -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
Expand All @@ -57,6 +62,8 @@ internal class ApkRestoreTest : TransportTest() {
private val strictContext: Context = mockk<Context>().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()
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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<Int>()) } throws NameNotFoundException()

Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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<Int>()) } throws NameNotFoundException()
cacheBaseApkAndGetInfo(tmpDir)
coEvery {
Expand All @@ -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<Int>()) } throws NameNotFoundException()
cacheBaseApkAndGetInfo(tmpDir)
coEvery {
Expand Down Expand Up @@ -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<Int>()) } throws NameNotFoundException()
every { strictContext.cacheDir } returns File(tmpDir.toString())
coEvery {
Expand All @@ -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<Int>()) } returns packageInfo
every { packageInfo.signingInfo.getSignatures() } returns packageMetadata.signatures!!
Expand Down Expand Up @@ -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<Int>()) } returns packageInfo
every { packageInfo.signingInfo.getSignatures() } returns packageMetadata.signatures!!
Expand Down Expand Up @@ -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<Int>()) } returns packageInfo
every { packageInfo.signingInfo.getSignatures() } returns listOf("foobar")
Expand Down Expand Up @@ -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<Int>()) } throws NameNotFoundException()
cacheBaseApkAndGetInfo(tmpDir)
every { storagePlugin.providerPackageName } returns storageProviderPackageName
Expand Down Expand Up @@ -438,6 +458,7 @@ internal class ApkRestoreTest : TransportTest() {
)

every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException()
// cache APK and get icon as well as app name
cacheBaseApkAndGetInfo(tmpDir)
Expand All @@ -464,6 +485,7 @@ internal class ApkRestoreTest : TransportTest() {
)

every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException()
// cache APK and get icon as well as app name
cacheBaseApkAndGetInfo(tmpDir)
Expand Down Expand Up @@ -493,6 +515,7 @@ internal class ApkRestoreTest : TransportTest() {
)

every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException()
// cache APK and get icon as well as app name
cacheBaseApkAndGetInfo(tmpDir)
Expand Down Expand Up @@ -524,6 +547,7 @@ internal class ApkRestoreTest : TransportTest() {
)

every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException()
// cache APK and get icon as well as app name
cacheBaseApkAndGetInfo(tmpDir)
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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
fun `no apks get installed when blocked by policy`() = runBlocking {
every { installRestriction.isAllowedToInstallApks() } returns false
Expand Down

0 comments on commit 570850a

Please sign in to comment.