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:
commit
bebb9005da
7 changed files with 285 additions and 16 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue