Merge pull request #719 from grote/only-install-if-not-installed

Don't re-install apps that are already installed and disable auto-restore
This commit is contained in:
Torsten Grote 2024-08-21 17:22:41 -03:00 committed by GitHub
commit bebb9005da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 285 additions and 16 deletions

View file

@ -17,7 +17,6 @@ import android.os.ServiceManager.getService
import android.os.StrictMode import android.os.StrictMode
import android.os.UserHandle import android.os.UserHandle
import android.os.UserManager import android.os.UserManager
import android.provider.Settings
import androidx.work.ExistingPeriodicWorkPolicy.UPDATE import androidx.work.ExistingPeriodicWorkPolicy.UPDATE
import androidx.work.WorkManager import androidx.work.WorkManager
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
@ -148,6 +147,7 @@ open class App : Application() {
private val metadataManager: MetadataManager by inject() private val metadataManager: MetadataManager by inject()
private val backupManager: IBackupManager by inject() private val backupManager: IBackupManager by inject()
private val pluginManager: StoragePluginManager by inject() private val pluginManager: StoragePluginManager by inject()
private val backupStateManager: BackupStateManager by inject()
/** /**
* The responsibility for the current token was moved to the [SettingsManager] * The responsibility for the current token was moved to the [SettingsManager]
@ -168,7 +168,7 @@ open class App : Application() {
* Introduced in the first half of 2024 and can be removed after a suitable migration period. * Introduced in the first half of 2024 and can be removed after a suitable migration period.
*/ */
protected open fun migrateToOwnScheduling() { protected open fun migrateToOwnScheduling() {
if (!isFrameworkSchedulingEnabled()) { // already on own scheduling if (!backupStateManager.isFrameworkSchedulingEnabled) { // already on own scheduling
// fix things for removable drive users who had a job scheduled here before // fix things for removable drive users who had a job scheduled here before
if (pluginManager.isOnRemovableDrive) AppBackupWorker.unschedule(applicationContext) if (pluginManager.isOnRemovableDrive) AppBackupWorker.unschedule(applicationContext)
return return
@ -184,10 +184,6 @@ open class App : Application() {
} }
} }
private fun isFrameworkSchedulingEnabled(): Boolean = Settings.Secure.getInt(
contentResolver, Settings.Secure.BACKUP_SCHEDULING_ENABLED, 1
) == 1 // 1 means enabled which is the default
} }
const val MAGIC_PACKAGE_MANAGER: String = PACKAGE_MANAGER_SENTINEL const val MAGIC_PACKAGE_MANAGER: String = PACKAGE_MANAGER_SENTINEL

View file

@ -6,6 +6,9 @@
package com.stevesoltys.seedvault package com.stevesoltys.seedvault
import android.content.Context import android.content.Context
import android.provider.Settings
import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE
import android.provider.Settings.Secure.BACKUP_SCHEDULING_ENABLED
import android.util.Log import android.util.Log
import androidx.work.WorkInfo.State.RUNNING import androidx.work.WorkInfo.State.RUNNING
import androidx.work.WorkManager import androidx.work.WorkManager
@ -22,6 +25,7 @@ class BackupStateManager(
) { ) {
private val workManager = WorkManager.getInstance(context) private val workManager = WorkManager.getInstance(context)
private val contentResolver = context.contentResolver
val isBackupRunning: Flow<Boolean> = combine( val isBackupRunning: Flow<Boolean> = combine(
flow = ConfigurableBackupTransportService.isRunning, flow = ConfigurableBackupTransportService.isRunning,
@ -37,4 +41,10 @@ class BackupStateManager(
appBackupRunning || filesBackupRunning || workInfoState == RUNNING appBackupRunning || filesBackupRunning || workInfoState == RUNNING
} }
val isAutoRestoreEnabled: Boolean
get() = Settings.Secure.getInt(contentResolver, BACKUP_AUTO_RESTORE, 1) == 1
val isFrameworkSchedulingEnabled: Boolean
get() = Settings.Secure.getInt(contentResolver, BACKUP_SCHEDULING_ENABLED, 1) == 1
} }

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,14 +85,33 @@ 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 (apkInstallResult.metadata.hasApk()) { if (isInstalled(packageName, apkInstallResult.metadata)) {
restore(backup, packageName, apkInstallResult.metadata) mInstallResult.update { result ->
} else { result.update(packageName) { it.copy(state = SUCCEEDED) }
}
} else if (!apkInstallResult.metadata.hasApk()) { // no APK available for install
mInstallResult.update { it.fail(packageName) } mInstallResult.update { it.fail(packageName) }
} else {
restore(backup, packageName, apkInstallResult.metadata)
} }
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Error re-installing APK for $packageName.", e) Log.e(TAG, "Error re-installing APK for $packageName.", e)
@ -104,7 +127,23 @@ internal class ApkRestore(
mInstallResult.update { it.fail(packageName) } mInstallResult.update { it.fail(packageName) }
} }
} }
mInstallResult.update { it.copy(isFinished = true) } }
@Throws(SecurityException::class)
private fun isInstalled(packageName: String, metadata: PackageMetadata): Boolean {
@Suppress("DEPRECATION") // GET_SIGNATURES is needed even though deprecated
val flags = GET_SIGNING_CERTIFICATES or GET_SIGNATURES
val packageInfo = try {
pm.getPackageInfo(packageName, flags)
} catch (e: PackageManager.NameNotFoundException) {
null
} ?: return false
val signatures = metadata.signatures
if (signatures != null && signatures != packageInfo.signingInfo.getSignatures()) {
// this will get caught and flag app as failed, could receive dedicated handling later
throw SecurityException("Signature mismatch for $packageName")
}
return packageInfo.longVersionCode >= (metadata.version ?: 0)
} }
@Suppress("ThrowsCount") @Suppress("ThrowsCount")

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

@ -10,8 +10,6 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.PowerManager import android.os.PowerManager
import android.os.RemoteException import android.os.RemoteException
import android.provider.Settings
import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE
import android.util.Log import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
@ -25,6 +23,7 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.TwoStatePreference import androidx.preference.TwoStatePreference
import androidx.work.WorkInfo import androidx.work.WorkInfo
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.stevesoltys.seedvault.BackupStateManager
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.permitDiskReads import com.stevesoltys.seedvault.permitDiskReads
import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.plugins.StoragePluginManager
@ -42,6 +41,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
private val viewModel: SettingsViewModel by sharedViewModel() private val viewModel: SettingsViewModel by sharedViewModel()
private val storagePluginManager: StoragePluginManager by inject() private val storagePluginManager: StoragePluginManager by inject()
private val backupStateManager: BackupStateManager by inject()
private val backupManager: IBackupManager by inject() private val backupManager: IBackupManager by inject()
private val notificationManager: BackupNotificationManager by inject() private val notificationManager: BackupNotificationManager by inject()
@ -271,7 +271,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
private fun setAutoRestoreState() { private fun setAutoRestoreState() {
activity?.contentResolver?.let { activity?.contentResolver?.let {
autoRestore.isChecked = Settings.Secure.getInt(it, BACKUP_AUTO_RESTORE, 1) == 1 autoRestore.isChecked = backupStateManager.isAutoRestoreEnabled
} }
val storage = this.storageProperties val storage = this.storageProperties
if (storage?.isUsb == true) { if (storage?.isUsb == true) {

View file

@ -5,12 +5,15 @@
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.Signature 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
@ -56,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()
@ -67,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,
@ -145,6 +152,8 @@ 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 { strictContext.cacheDir } returns tmpFile every { strictContext.cacheDir } returns tmpFile
every { crypto.getNameForApk(salt, packageName, "") } returns name every { crypto.getNameForApk(salt, packageName, "") } returns name
coEvery { storagePlugin.getInputStream(token, name) } returns inputStream coEvery { storagePlugin.getInputStream(token, name) } returns inputStream

View file

@ -5,15 +5,18 @@
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
import android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP import android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
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
@ -30,9 +33,14 @@ import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED 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 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.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
@ -54,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()
@ -63,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,
@ -104,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
@ -116,12 +129,68 @@ internal class ApkRestoreTest : TransportTest() {
} }
} }
@Test
fun `test app without APK does not attempt install`(@TempDir tmpDir: Path) = runBlocking {
// remove all APK info
val packageMetadata = packageMetadata.copy(
version = null,
installer = null,
sha256 = null,
signatures = null,
)
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()
apkRestore.installResult.test {
awaitItem() // initial empty state
apkRestore.restore(backup)
assertEquals(QUEUED, awaitItem()[packageName].state)
assertEquals(FAILED, awaitItem()[packageName].state)
assertTrue(awaitItem().isFinished)
ensureAllEventsConsumed()
}
}
@Test
fun `test app without APK succeeds if installed`(@TempDir tmpDir: Path) = runBlocking {
// remove all APK info
val packageMetadata = packageMetadata.copy(
version = null,
installer = null,
sha256 = null,
signatures = null,
)
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()
every { pm.getPackageInfo(packageName, any<Int>()) } returns packageInfo
every { packageInfo.longVersionCode } returns 42
apkRestore.installResult.test {
awaitItem() // initial empty state
apkRestore.restore(backup)
assertEquals(QUEUED, awaitItem()[packageName].state)
assertEquals(SUCCEEDED, awaitItem()[packageName].state)
assertTrue(awaitItem().isFinished)
ensureAllEventsConsumed()
}
}
@Test @Test
fun `package name mismatch causes FAILED status`(@TempDir tmpDir: Path) = runBlocking { fun `package name mismatch causes FAILED status`(@TempDir tmpDir: Path) = runBlocking {
// change package name to random string // change package name to random string
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
@ -138,6 +207,8 @@ 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()
cacheBaseApkAndGetInfo(tmpDir) cacheBaseApkAndGetInfo(tmpDir)
coEvery { coEvery {
apkInstaller.install(match { it.size == 1 }, packageName, installerName, any()) apkInstaller.install(match { it.size == 1 }, packageName, installerName, any())
@ -163,6 +234,8 @@ 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()
cacheBaseApkAndGetInfo(tmpDir) cacheBaseApkAndGetInfo(tmpDir)
coEvery { coEvery {
apkInstaller.install(match { it.size == 1 }, packageName, installerName, any()) apkInstaller.install(match { it.size == 1 }, packageName, installerName, any())
@ -191,6 +264,8 @@ 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 { strictContext.cacheDir } returns File(tmpDir.toString()) every { strictContext.cacheDir } returns File(tmpDir.toString())
coEvery { coEvery {
legacyStoragePlugin.getApkInputStream(token, packageName, "") legacyStoragePlugin.getApkInputStream(token, packageName, "")
@ -210,6 +285,100 @@ internal class ApkRestoreTest : TransportTest() {
} }
} }
@Test
fun `test app only installed not already installed`(@TempDir tmpDir: Path) = runBlocking {
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!!
every {
packageInfo.longVersionCode
} returns packageMetadata.version!! + Random.nextLong(0, 2) // can be newer
apkRestore.installResult.test {
awaitItem() // initial empty state
apkRestore.restore(backup)
awaitQueuedItem()
awaitItem().also { systemItem ->
val result = systemItem[packageName]
assertEquals(SUCCEEDED, result.state)
}
awaitItem().also { finishedItem ->
assertTrue(finishedItem.isFinished)
}
ensureAllEventsConsumed()
}
}
@Test
fun `test app still installed if older version is installed`(@TempDir tmpDir: Path) =
runBlocking {
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!!
every { packageInfo.longVersionCode } returns packageMetadata.version!! - 1
cacheBaseApkAndGetInfo(tmpDir)
val packagesMap = mapOf(
packageName to ApkInstallResult(
packageName,
state = SUCCEEDED,
metadata = PackageMetadata(),
)
)
val installResult = InstallResult(packagesMap)
coEvery {
apkInstaller.install(match { it.size == 1 }, packageName, installerName, any())
} returns installResult
apkRestore.installResult.test {
awaitItem() // initial empty state
apkRestore.restore(backup)
awaitQueuedItem()
awaitInProgressItem()
awaitItem().also { systemItem ->
val result = systemItem[packageName]
assertEquals(SUCCEEDED, result.state)
}
awaitItem().also { finishedItem ->
assertTrue(finishedItem.isFinished)
}
ensureAllEventsConsumed()
}
}
@Test
fun `test app fails if installed with different signer`(@TempDir tmpDir: Path) = runBlocking {
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")
apkRestore.installResult.test {
awaitItem() // initial empty state
apkRestore.restore(backup)
awaitQueuedItem()
awaitItem().also { systemItem ->
val result = systemItem[packageName]
assertEquals(FAILED, result.state)
}
awaitItem().also { finishedItem ->
assertTrue(finishedItem.isFinished)
}
ensureAllEventsConsumed()
}
}
@Test @Test
fun `test system apps only reinstalled when older system apps exist`(@TempDir tmpDir: Path) = fun `test system apps only reinstalled when older system apps exist`(@TempDir tmpDir: Path) =
runBlocking { runBlocking {
@ -220,13 +389,15 @@ 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()
cacheBaseApkAndGetInfo(tmpDir) cacheBaseApkAndGetInfo(tmpDir)
every { storagePlugin.providerPackageName } returns storageProviderPackageName every { storagePlugin.providerPackageName } returns storageProviderPackageName
if (willFail) { if (willFail) {
every { every {
pm.getPackageInfo(packageName, 0) pm.getPackageInfo(packageName, 0)
} throws PackageManager.NameNotFoundException() } throws NameNotFoundException()
} else { } else {
installedPackageInfo.applicationInfo = mockk { installedPackageInfo.applicationInfo = mockk {
flags = flags =
@ -287,6 +458,8 @@ 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()
// cache APK and get icon as well as app name // cache APK and get icon as well as app name
cacheBaseApkAndGetInfo(tmpDir) cacheBaseApkAndGetInfo(tmpDir)
@ -312,6 +485,8 @@ 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()
// cache APK and get icon as well as app name // cache APK and get icon as well as app name
cacheBaseApkAndGetInfo(tmpDir) cacheBaseApkAndGetInfo(tmpDir)
@ -340,6 +515,8 @@ 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()
// cache APK and get icon as well as app name // cache APK and get icon as well as app name
cacheBaseApkAndGetInfo(tmpDir) cacheBaseApkAndGetInfo(tmpDir)
@ -370,6 +547,8 @@ 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()
// cache APK and get icon as well as app name // cache APK and get icon as well as app name
cacheBaseApkAndGetInfo(tmpDir) cacheBaseApkAndGetInfo(tmpDir)
@ -411,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
@ -437,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 {
@ -453,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