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:
Chirayu Desai 2023-01-13 01:29:07 +05:30
parent bb562a4cb2
commit 422e3f547d
5 changed files with 69 additions and 4 deletions

View file

@ -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
)
}
emit(installResult)
if (isAllowedToInstallApks) {
emit(installResult)
} else {
installResult.isFinished = true
emit(installResult)
return@flow
}
// re-install individual packages and emit updates
for ((packageName, metadata) in packages) {

View file

@ -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()
}
}
}

View file

@ -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)
}

View file

@ -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

View file

@ -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)