restore: Skip installing APKs if not allowed by policy
* We should not bypass the OS-wide APK install restriction. * Simply treat that as just not having the APK in the first place, since we do support that as an option. * This still lets users install apps via the store it was downloaded from, if said store is installed and allowed to install apps. * Introduce InstallRestriction to make testing easier. Co-Authored-By: Torsten Grote <t@grobox.de> Change-Id: Ic0a56961c9078d4dd542db5d9fc75034abb27bea
This commit is contained in:
parent
bb562a4cb2
commit
422e3f547d
5 changed files with 69 additions and 4 deletions
|
@ -12,6 +12,7 @@ import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
|||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.restore.RestorableBackup
|
||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
|
||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
|
||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
|
||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
|
||||
|
@ -35,6 +36,7 @@ internal class ApkRestore(
|
|||
private val crypto: Crypto,
|
||||
private val splitCompatChecker: ApkSplitCompatibilityChecker,
|
||||
private val apkInstaller: ApkInstaller,
|
||||
private val installRestriction: InstallRestriction,
|
||||
) {
|
||||
|
||||
private val pm = context.packageManager
|
||||
|
@ -47,6 +49,7 @@ internal class ApkRestore(
|
|||
// Otherwise, it gets killed when we install it, terminating our restoration.
|
||||
it.key != storagePlugin.providerPackageName
|
||||
}
|
||||
val isAllowedToInstallApks = installRestriction.isAllowedToInstallApks()
|
||||
val total = packages.size
|
||||
var progress = 0
|
||||
|
||||
|
@ -57,11 +60,17 @@ internal class ApkRestore(
|
|||
installResult[packageName] = ApkInstallResult(
|
||||
packageName = packageName,
|
||||
progress = progress,
|
||||
state = QUEUED,
|
||||
state = if (isAllowedToInstallApks) QUEUED else FAILED,
|
||||
installerPackageName = metadata.installer
|
||||
)
|
||||
}
|
||||
if (isAllowedToInstallApks) {
|
||||
emit(installResult)
|
||||
} else {
|
||||
installResult.isFinished = true
|
||||
emit(installResult)
|
||||
return@flow
|
||||
}
|
||||
|
||||
// re-install individual packages and emit updates
|
||||
for ((packageName, metadata) in packages) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.stevesoltys.seedvault.restore.install
|
||||
|
||||
import android.os.UserManager
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
@ -7,5 +8,9 @@ val installModule = module {
|
|||
factory { ApkInstaller(androidContext()) }
|
||||
factory { DeviceInfo(androidContext()) }
|
||||
factory { ApkSplitCompatibilityChecker(get()) }
|
||||
factory { ApkRestore(androidContext(), get(), get(), get(), get(), get()) }
|
||||
factory {
|
||||
ApkRestore(androidContext(), get(), get(), get(), get(), get()) {
|
||||
androidContext().getSystemService(UserManager::class.java).isAllowedToInstallApks()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package com.stevesoltys.seedvault.restore.install
|
||||
|
||||
import android.os.UserManager
|
||||
|
||||
internal fun interface InstallRestriction {
|
||||
fun isAllowedToInstallApks(): Boolean
|
||||
}
|
||||
|
||||
private fun UserManager.isRestricted(restriction: String): Boolean {
|
||||
return userRestrictions.getBoolean(restriction, false)
|
||||
}
|
||||
|
||||
internal fun UserManager.isAllowedToInstallApks(): Boolean {
|
||||
return isRestricted(UserManager.DISALLOW_INSTALL_APPS) ||
|
||||
isRestricted(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES) ||
|
||||
isRestricted(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY)
|
||||
}
|
|
@ -52,6 +52,7 @@ internal class ApkBackupRestoreTest : TransportTest() {
|
|||
private val storagePlugin: StoragePlugin<*> = mockk()
|
||||
private val splitCompatChecker: ApkSplitCompatibilityChecker = mockk()
|
||||
private val apkInstaller: ApkInstaller = mockk()
|
||||
private val installRestriction: InstallRestriction = mockk()
|
||||
|
||||
private val apkBackup = ApkBackup(pm, crypto, settingsManager, metadataManager)
|
||||
private val apkRestore: ApkRestore = ApkRestore(
|
||||
|
@ -60,7 +61,8 @@ internal class ApkBackupRestoreTest : TransportTest() {
|
|||
legacyStoragePlugin = legacyStoragePlugin,
|
||||
crypto = crypto,
|
||||
splitCompatChecker = splitCompatChecker,
|
||||
apkInstaller = apkInstaller
|
||||
apkInstaller = apkInstaller,
|
||||
installRestriction = installRestriction,
|
||||
)
|
||||
|
||||
private val signatureBytes = byteArrayOf(0x01, 0x02, 0x03)
|
||||
|
@ -132,6 +134,7 @@ internal class ApkBackupRestoreTest : TransportTest() {
|
|||
val apkPath = slot<String>()
|
||||
val cacheFiles = slot<List<File>>()
|
||||
|
||||
every { installRestriction.isAllowedToInstallApks() } returns true
|
||||
every { strictContext.cacheDir } returns tmpFile
|
||||
every { crypto.getNameForApk(salt, packageName, "") } returns name
|
||||
coEvery { storagePlugin.getInputStream(token, name) } returns inputStream
|
||||
|
|
|
@ -54,6 +54,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
private val legacyStoragePlugin: LegacyStoragePlugin = mockk()
|
||||
private val splitCompatChecker: ApkSplitCompatibilityChecker = mockk()
|
||||
private val apkInstaller: ApkInstaller = mockk()
|
||||
private val installRestriction: InstallRestriction = mockk()
|
||||
|
||||
private val apkRestore: ApkRestore = ApkRestore(
|
||||
context = strictContext,
|
||||
|
@ -62,6 +63,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
crypto = crypto,
|
||||
splitCompatChecker = splitCompatChecker,
|
||||
apkInstaller = apkInstaller,
|
||||
installRestriction = installRestriction,
|
||||
)
|
||||
|
||||
private val icon: Drawable = mockk()
|
||||
|
@ -96,6 +98,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
val packageMetadata = packageMetadata.copy(sha256 = getRandomString())
|
||||
val backup = swapPackages(hashMapOf(packageName to packageMetadata))
|
||||
|
||||
every { installRestriction.isAllowedToInstallApks() } returns true
|
||||
every { strictContext.cacheDir } returns File(tmpDir.toString())
|
||||
every { crypto.getNameForApk(salt, packageName, "") } returns name
|
||||
coEvery { storagePlugin.getInputStream(token, name) } returns apkInputStream
|
||||
|
@ -111,6 +114,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
// change package name to random string
|
||||
packageInfo.packageName = getRandomString()
|
||||
|
||||
every { installRestriction.isAllowedToInstallApks() } returns true
|
||||
every { strictContext.cacheDir } returns File(tmpDir.toString())
|
||||
every { crypto.getNameForApk(salt, packageName, "") } returns name
|
||||
coEvery { storagePlugin.getInputStream(token, name) } returns apkInputStream
|
||||
|
@ -124,6 +128,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
|
||||
@Test
|
||||
fun `test apkInstaller throws exceptions`(@TempDir tmpDir: Path) = runBlocking {
|
||||
every { installRestriction.isAllowedToInstallApks() } returns true
|
||||
cacheBaseApkAndGetInfo(tmpDir)
|
||||
coEvery {
|
||||
apkInstaller.install(match { it.size == 1 }, packageName, installerName, any())
|
||||
|
@ -147,6 +152,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
)
|
||||
}
|
||||
|
||||
every { installRestriction.isAllowedToInstallApks() } returns true
|
||||
cacheBaseApkAndGetInfo(tmpDir)
|
||||
coEvery {
|
||||
apkInstaller.install(match { it.size == 1 }, packageName, installerName, any())
|
||||
|
@ -173,6 +179,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
)
|
||||
}
|
||||
|
||||
every { installRestriction.isAllowedToInstallApks() } returns true
|
||||
every { strictContext.cacheDir } returns File(tmpDir.toString())
|
||||
@Suppress("Deprecation")
|
||||
coEvery {
|
||||
|
@ -200,6 +207,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
val willFail = Random.nextBoolean()
|
||||
val isSystemApp = Random.nextBoolean()
|
||||
|
||||
every { installRestriction.isAllowedToInstallApks() } returns true
|
||||
cacheBaseApkAndGetInfo(tmpDir)
|
||||
every { storagePlugin.providerPackageName } returns storageProviderPackageName
|
||||
|
||||
|
@ -274,6 +282,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
)
|
||||
)
|
||||
|
||||
every { installRestriction.isAllowedToInstallApks() } returns true
|
||||
// cache APK and get icon as well as app name
|
||||
cacheBaseApkAndGetInfo(tmpDir)
|
||||
|
||||
|
@ -296,6 +305,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
splits = listOf(ApkSplit(splitName, Random.nextLong(), getRandomBase64(23)))
|
||||
)
|
||||
|
||||
every { installRestriction.isAllowedToInstallApks() } returns true
|
||||
// cache APK and get icon as well as app name
|
||||
cacheBaseApkAndGetInfo(tmpDir)
|
||||
|
||||
|
@ -321,6 +331,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
splits = listOf(ApkSplit(splitName, Random.nextLong(), sha256))
|
||||
)
|
||||
|
||||
every { installRestriction.isAllowedToInstallApks() } returns true
|
||||
// cache APK and get icon as well as app name
|
||||
cacheBaseApkAndGetInfo(tmpDir)
|
||||
|
||||
|
@ -348,6 +359,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
)
|
||||
)
|
||||
|
||||
every { installRestriction.isAllowedToInstallApks() } returns true
|
||||
// cache APK and get icon as well as app name
|
||||
cacheBaseApkAndGetInfo(tmpDir)
|
||||
|
||||
|
@ -387,6 +399,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
|
||||
@Test
|
||||
fun `storage provider app does not get reinstalled`(@TempDir tmpDir: Path) = runBlocking {
|
||||
every { installRestriction.isAllowedToInstallApks() } returns true
|
||||
// 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
|
||||
|
@ -406,6 +419,24 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no apks get installed when blocked by policy`(@TempDir tmpDir: Path) = runBlocking {
|
||||
every { installRestriction.isAllowedToInstallApks() } returns false
|
||||
every { storagePlugin.providerPackageName } returns storageProviderPackageName
|
||||
|
||||
apkRestore.restore(backup).collectIndexed { i, value ->
|
||||
when (i) {
|
||||
0 -> {
|
||||
// single package fails without attempting to install it
|
||||
assertEquals(1, value.total)
|
||||
assertEquals(FAILED, value[packageName].state)
|
||||
assertTrue(value.isFinished)
|
||||
}
|
||||
else -> fail("more values emitted")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun swapPackages(packageMetadataMap: PackageMetadataMap): RestorableBackup {
|
||||
val metadata = metadata.copy(packageMetadataMap = packageMetadataMap)
|
||||
return backup.copy(backupMetadata = metadata)
|
||||
|
|
Loading…
Reference in a new issue