diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt index aa9e5f304..43491eed5 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt @@ -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) { diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallModule.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallModule.kt index 33e640b3c..7416c1039 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallModule.kt @@ -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() + } + } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallRestriction.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallRestriction.kt new file mode 100644 index 000000000..0f3a01540 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallRestriction.kt @@ -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) +} diff --git a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt index 966e843bc..4a23ae128 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt @@ -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() val cacheFiles = slot>() + every { installRestriction.isAllowedToInstallApks() } returns true every { strictContext.cacheDir } returns tmpFile every { crypto.getNameForApk(salt, packageName, "") } returns name coEvery { storagePlugin.getInputStream(token, name) } returns inputStream diff --git a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt index 034038a8d..1af61a8b3 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt @@ -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)