Add config option to re-install apps with feature modules only on same device
There is a possibility that incompatible APK splits make a an app crash when starting after re-installing it. With that config option each OEM can decide with they want to take this risk or not.
This commit is contained in:
parent
e6723093c9
commit
2cde417c8c
10 changed files with 325 additions and 132 deletions
|
@ -18,6 +18,9 @@ data class RestorableBackup(
|
|||
val time: Long
|
||||
get() = backupMetadata.time
|
||||
|
||||
val deviceName: String
|
||||
get() = backupMetadata.deviceName
|
||||
|
||||
val packageMetadataMap: PackageMetadataMap
|
||||
get() = backupMetadata.packageMetadataMap
|
||||
|
||||
|
|
|
@ -151,9 +151,9 @@ internal class RestoreViewModel(
|
|||
closeSession()
|
||||
}
|
||||
|
||||
private fun getInstallResult(restorableBackup: RestorableBackup): LiveData<InstallResult> {
|
||||
private fun getInstallResult(backup: RestorableBackup): LiveData<InstallResult> {
|
||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||
return apkRestore.restore(restorableBackup.token, restorableBackup.packageMetadataMap)
|
||||
return apkRestore.restore(backup.token, backup.deviceName, backup.packageMetadataMap)
|
||||
.onStart {
|
||||
Log.d(TAG, "Start InstallResult Flow")
|
||||
}.catch { e ->
|
||||
|
|
|
@ -33,7 +33,7 @@ internal class ApkRestore(
|
|||
|
||||
private val pm = context.packageManager
|
||||
|
||||
fun restore(token: Long, packageMetadataMap: PackageMetadataMap) = flow {
|
||||
fun restore(token: Long, deviceName: String, packageMetadataMap: PackageMetadataMap) = flow {
|
||||
// filter out packages without APK and get total
|
||||
val packages = packageMetadataMap.filter { it.value.hasApk() }
|
||||
val total = packages.size
|
||||
|
@ -55,7 +55,7 @@ internal class ApkRestore(
|
|||
// re-install individual packages and emit updates
|
||||
for ((packageName, metadata) in packages) {
|
||||
try {
|
||||
restore(this, token, packageName, metadata, installResult)
|
||||
restore(this, token, deviceName, packageName, metadata, installResult)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error re-installing APK for $packageName.", e)
|
||||
emit(installResult.fail(packageName))
|
||||
|
@ -76,6 +76,7 @@ internal class ApkRestore(
|
|||
private suspend fun restore(
|
||||
collector: FlowCollector<InstallResult>,
|
||||
token: Long,
|
||||
deviceName: String,
|
||||
packageName: String,
|
||||
metadata: PackageMetadata,
|
||||
installResult: MutableInstallResult
|
||||
|
@ -137,7 +138,8 @@ internal class ApkRestore(
|
|||
}
|
||||
|
||||
// process further APK splits, if available
|
||||
val cachedApks = cacheSplitsIfNeeded(token, packageName, cachedApk, metadata.splits)
|
||||
val cachedApks =
|
||||
cacheSplitsIfNeeded(token, deviceName, packageName, cachedApk, metadata.splits)
|
||||
if (cachedApks == null) {
|
||||
Log.w(TAG, "Not installing $packageName because of incompatible splits.")
|
||||
collector.emit(installResult.fail(packageName))
|
||||
|
@ -160,6 +162,7 @@ internal class ApkRestore(
|
|||
@Throws(IOException::class, SecurityException::class)
|
||||
private suspend fun cacheSplitsIfNeeded(
|
||||
token: Long,
|
||||
deviceName: String,
|
||||
packageName: String,
|
||||
cachedApk: File,
|
||||
splits: List<ApkSplit>?
|
||||
|
@ -168,7 +171,7 @@ internal class ApkRestore(
|
|||
val splitNames = splits?.map { it.name } ?: return listOf(cachedApk)
|
||||
|
||||
// return null when splits are incompatible
|
||||
if (!splitCompatChecker.isCompatible(splitNames)) return null
|
||||
if (!splitCompatChecker.isCompatible(deviceName, splitNames)) return null
|
||||
|
||||
// store coming splits in a list
|
||||
val cachedApks = ArrayList<File>(splits.size + 1).apply {
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package com.stevesoltys.seedvault.restore.install
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
|
||||
private const val TAG = "SplitCompatChecker"
|
||||
|
@ -9,6 +7,7 @@ private const val TAG = "SplitCompatChecker"
|
|||
private const val CONFIG_PREFIX = "config."
|
||||
private const val CONFIG_LENGTH = CONFIG_PREFIX.length
|
||||
|
||||
// see https://developer.android.com/training/multiscreen/screendensities#TaskProvideAltBmp
|
||||
private const val LDPI_VALUE = 120
|
||||
private const val MDPI_VALUE = 160
|
||||
private const val TVDPI_VALUE = 213
|
||||
|
@ -17,14 +16,9 @@ private const val XHDPI_VALUE = 320
|
|||
private const val XXHDPI_VALUE = 480
|
||||
private const val XXXHDPI_VALUE = 640
|
||||
|
||||
class DeviceInfo(context: Context) {
|
||||
val densityDpi: Int = context.resources.displayMetrics.densityDpi
|
||||
val supportedABIs: List<String> = Build.SUPPORTED_ABIS.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to determine APK split compatibility with a device by examining the list of split names.
|
||||
* This only looks on the supported ABIs and the screen density.
|
||||
* This only looks on the supported ABIs, the screen density and supported languages.
|
||||
* Other config splits e.g. based on OpenGL or Vulkan version are also possible,
|
||||
* but don't seem to be widely used, so we don't consider those for now.
|
||||
*/
|
||||
|
@ -53,18 +47,29 @@ class ApkSplitCompatibilityChecker(private val deviceInfo: DeviceInfo) {
|
|||
* Returns true if the list of splits can be considered compatible with the current device,
|
||||
* and false otherwise.
|
||||
*/
|
||||
fun isCompatible(splitNames: Collection<String>): Boolean = splitNames.all { splitName ->
|
||||
// all individual splits need to be compatible (which can be hard to judge by name only)
|
||||
isCompatible(splitName)
|
||||
fun isCompatible(deviceName: String, splitNames: Collection<String>): Boolean {
|
||||
val unknownAllowed = deviceInfo.areUnknownSplitsAllowed(deviceName)
|
||||
return splitNames.all { splitName ->
|
||||
// all individual splits need to be compatible (which can be hard to judge by name only)
|
||||
isCompatible(deviceName, unknownAllowed, splitName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCompatible(splitName: String): Boolean {
|
||||
private fun isCompatible(
|
||||
deviceName: String,
|
||||
unknownAllowed: Boolean,
|
||||
splitName: String
|
||||
): Boolean {
|
||||
val index = splitName.indexOf(CONFIG_PREFIX)
|
||||
// If this is not a standardized config split, we just assume that it will work,
|
||||
// as it is most likely a dynamic feature module.
|
||||
if (index == -1) {
|
||||
// If this is not a standardized config split
|
||||
if (index == -1 && unknownAllowed) {
|
||||
// we assume that it will work, as it is most likely a dynamic feature module
|
||||
Log.v(TAG, "Not a config split '$splitName'. Assuming it is ok.")
|
||||
return true
|
||||
} else if (index != 0 && !unknownAllowed) { // not a normal config split at all
|
||||
// we refuse it, since unknown splits are not allowed
|
||||
Log.v(TAG, "Not a config split '$splitName'. Not allowed.")
|
||||
return false
|
||||
}
|
||||
|
||||
val name = splitName.substring(index + CONFIG_LENGTH)
|
||||
|
@ -77,14 +82,25 @@ class ApkSplitCompatibilityChecker(private val deviceInfo: DeviceInfo) {
|
|||
|
||||
// Check if this is a known screen density config
|
||||
densityMap[name]?.let { splitDensity ->
|
||||
// the split's density must not be much lower than the device's.
|
||||
// The split's density must not be much lower than the device's.
|
||||
return isDensityCompatible(splitDensity)
|
||||
}
|
||||
|
||||
// At this point we don't know what to make of that split,
|
||||
// so let's just hope that it will work. It might just be a language.
|
||||
Log.v(TAG, "Unhandled config split '$splitName'. Assuming it is ok.")
|
||||
return true
|
||||
// Check if this is a language split
|
||||
if (deviceInfo.isSupportedLanguage(name)) {
|
||||
// accept all language splits as an extra language should not break things
|
||||
return true
|
||||
}
|
||||
|
||||
// At this point we don't know what to make of that split
|
||||
return if (deviceInfo.isSameDevice(deviceName)) {
|
||||
// so we only allow it if it came from the same device
|
||||
Log.v(TAG, "Unhandled config split '$splitName'. Came from same device.")
|
||||
true
|
||||
} else {
|
||||
Log.w(TAG, "Unhandled config split '$splitName'. Not allowed.")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAbiCompatible(name: String): Boolean {
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package com.stevesoltys.seedvault.restore.install
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.android.internal.app.LocalePicker
|
||||
import com.stevesoltys.seedvault.R
|
||||
|
||||
class DeviceInfo(context: Context) {
|
||||
val densityDpi: Int = context.resources.displayMetrics.densityDpi
|
||||
val supportedABIs: List<String> = Build.SUPPORTED_ABIS.toList()
|
||||
private val deviceName: String = "${Build.MANUFACTURER} ${Build.MODEL}"
|
||||
private val languages = LocalePicker.getSupportedLocales(context)
|
||||
.map { it.substringBefore('-') }
|
||||
.toSet()
|
||||
private val unknownSplitsOnlySameDevice =
|
||||
context.resources.getBoolean(R.bool.re_install_unknown_splits_only_on_same_device)
|
||||
|
||||
fun areUnknownSplitsAllowed(deviceName: String): Boolean {
|
||||
return !unknownSplitsOnlySameDevice || this.deviceName == deviceName
|
||||
}
|
||||
|
||||
fun isSameDevice(deviceName: String): Boolean {
|
||||
return this.deviceName == deviceName
|
||||
}
|
||||
|
||||
fun isSupportedLanguage(name: String): Boolean = languages.contains(name)
|
||||
}
|
|
@ -5,6 +5,7 @@ import org.koin.dsl.module
|
|||
|
||||
val installModule = module {
|
||||
factory { ApkInstaller(androidContext()) }
|
||||
factory { ApkSplitCompatibilityChecker(DeviceInfo(androidContext())) }
|
||||
factory { DeviceInfo(androidContext()) }
|
||||
factory { ApkSplitCompatibilityChecker(get()) }
|
||||
factory { ApkRestore(androidContext(), get(), get(), get()) }
|
||||
}
|
||||
|
|
|
@ -10,10 +10,26 @@
|
|||
-->
|
||||
<bool name="show_restore_in_settings">false</bool>
|
||||
|
||||
<!-- Add only storage that is also available when restoring from backup (e.g. initial device setup) -->
|
||||
<!--
|
||||
Add only storage here that is also available
|
||||
when restoring from backup (e.g. initial device setup)
|
||||
-->
|
||||
<string-array name="storage_authority_whitelist" tools:ignore="InconsistentArrays">
|
||||
<item>com.android.externalstorage.documents</item>
|
||||
<item>org.nextcloud.documents</item>
|
||||
</string-array>
|
||||
|
||||
<!--
|
||||
Android App Bundles split up the app into several APKs.
|
||||
We always back up all the available split APKs
|
||||
and do a compatibility check when re-installing them.
|
||||
If a backed up split is not compatible, the re-install will fail
|
||||
and the user will be given the opportunity to install the app manually before data restore.
|
||||
Unknown splits are treated as compatible as we haven't yet seen a case
|
||||
where this would cause a problem such as an app crashing when starting it after re-install.
|
||||
However, if you prefer to be on the safe side, you can set this to true,
|
||||
to only install unknown splits if they come from the same device.
|
||||
-->
|
||||
<bool name="re_install_unknown_splits_only_on_same_device">false</bool>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -57,6 +57,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
|
||||
private val icon: Drawable = mockk()
|
||||
|
||||
private val deviceName = getRandomString()
|
||||
private val packageName = packageInfo.packageName
|
||||
private val packageMetadata = PackageMetadata(
|
||||
time = Random.nextLong(),
|
||||
|
@ -85,7 +86,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
every { strictContext.cacheDir } returns File(tmpDir.toString())
|
||||
coEvery { restorePlugin.getApkInputStream(token, packageName, "") } returns apkInputStream
|
||||
|
||||
apkRestore.restore(token, packageMetadataMap).collectIndexed { i, value ->
|
||||
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
|
||||
assertQueuedFailFinished(i, value)
|
||||
}
|
||||
}
|
||||
|
@ -99,7 +100,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
coEvery { restorePlugin.getApkInputStream(token, packageName, "") } returns apkInputStream
|
||||
every { pm.getPackageArchiveInfo(any(), any()) } returns packageInfo
|
||||
|
||||
apkRestore.restore(token, packageMetadataMap).collectIndexed { i, value ->
|
||||
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
|
||||
assertQueuedFailFinished(i, value)
|
||||
}
|
||||
}
|
||||
|
@ -111,7 +112,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
apkInstaller.install(match { it.size == 1 }, packageName, installerName, any())
|
||||
} throws SecurityException()
|
||||
|
||||
apkRestore.restore(token, packageMetadataMap).collectIndexed { i, value ->
|
||||
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
|
||||
assertQueuedProgressFailFinished(i, value)
|
||||
}
|
||||
}
|
||||
|
@ -133,7 +134,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
apkInstaller.install(match { it.size == 1 }, packageName, installerName, any())
|
||||
} returns installResult
|
||||
|
||||
apkRestore.restore(token, packageMetadataMap).collectIndexed { i, value ->
|
||||
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
|
||||
assertQueuedProgressSuccessFinished(i, value)
|
||||
}
|
||||
}
|
||||
|
@ -180,7 +181,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
}
|
||||
}
|
||||
|
||||
apkRestore.restore(token, packageMetadataMap).collectIndexed { i, value ->
|
||||
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
|
||||
when (i) {
|
||||
0 -> {
|
||||
val result = value[packageName]
|
||||
|
@ -226,9 +227,11 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
cacheBaseApkAndGetInfo(tmpDir)
|
||||
|
||||
// splits are NOT compatible
|
||||
every { splitCompatChecker.isCompatible(listOf(split1Name, split2Name)) } returns false
|
||||
every {
|
||||
splitCompatChecker.isCompatible(deviceName, listOf(split1Name, split2Name))
|
||||
} returns false
|
||||
|
||||
apkRestore.restore(token, packageMetadataMap).collectIndexed { i, value ->
|
||||
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
|
||||
assertQueuedProgressFailFinished(i, value)
|
||||
}
|
||||
}
|
||||
|
@ -245,12 +248,12 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
// cache APK and get icon as well as app name
|
||||
cacheBaseApkAndGetInfo(tmpDir)
|
||||
|
||||
every { splitCompatChecker.isCompatible(listOf(splitName)) } returns true
|
||||
every { splitCompatChecker.isCompatible(deviceName, listOf(splitName)) } returns true
|
||||
coEvery {
|
||||
restorePlugin.getApkInputStream(token, packageName, "_$sha256")
|
||||
} returns ByteArrayInputStream(getRandomByteArray())
|
||||
|
||||
apkRestore.restore(token, packageMetadataMap).collectIndexed { i, value ->
|
||||
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
|
||||
assertQueuedProgressFailFinished(i, value)
|
||||
}
|
||||
}
|
||||
|
@ -268,12 +271,12 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
// cache APK and get icon as well as app name
|
||||
cacheBaseApkAndGetInfo(tmpDir)
|
||||
|
||||
every { splitCompatChecker.isCompatible(listOf(splitName)) } returns true
|
||||
every { splitCompatChecker.isCompatible(deviceName, listOf(splitName)) } returns true
|
||||
coEvery {
|
||||
restorePlugin.getApkInputStream(token, packageName, "_$sha256")
|
||||
} throws IOException()
|
||||
|
||||
apkRestore.restore(token, packageMetadataMap).collectIndexed { i, value ->
|
||||
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
|
||||
assertQueuedProgressFailFinished(i, value)
|
||||
}
|
||||
}
|
||||
|
@ -295,7 +298,9 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
// cache APK and get icon as well as app name
|
||||
cacheBaseApkAndGetInfo(tmpDir)
|
||||
|
||||
every { splitCompatChecker.isCompatible(listOf(split1Name, split2Name)) } returns true
|
||||
every {
|
||||
splitCompatChecker.isCompatible(deviceName, listOf(split1Name, split2Name))
|
||||
} returns true
|
||||
|
||||
// define bytes of splits and return them as stream (matches above hashes)
|
||||
val split1Bytes = byteArrayOf(0x01, 0x02, 0x03)
|
||||
|
@ -321,7 +326,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
)
|
||||
}
|
||||
|
||||
apkRestore.restore(token, packageMetadataMap).collectIndexed { i, value ->
|
||||
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
|
||||
assertQueuedProgressSuccessFinished(i, value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,144 +4,175 @@ import com.stevesoltys.seedvault.getRandomString
|
|||
import com.stevesoltys.seedvault.transport.TransportTest
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.random.Random
|
||||
|
||||
class ApkSplitCompatibilityCheckerTest : TransportTest() {
|
||||
|
||||
private val deviceInfo: DeviceInfo = mockk()
|
||||
private val deviceName = getRandomString()
|
||||
|
||||
private val checker = ApkSplitCompatibilityChecker(deviceInfo)
|
||||
|
||||
@Test
|
||||
fun `non-config splits always get accepted`() {
|
||||
assertTrue(
|
||||
checker.isCompatible(
|
||||
listOf(
|
||||
getRandomString(),
|
||||
getRandomString(),
|
||||
getRandomString(),
|
||||
getRandomString(),
|
||||
getRandomString(),
|
||||
getRandomString()
|
||||
)
|
||||
)
|
||||
fun `non-config splits always get accepted except when unknowns are not allowed`() {
|
||||
val splits = listOf(
|
||||
getRandomString(),
|
||||
getRandomString(),
|
||||
getRandomString(),
|
||||
getRandomString(),
|
||||
getRandomString(),
|
||||
getRandomString()
|
||||
)
|
||||
every { deviceInfo.areUnknownSplitsAllowed(deviceName) } returns true andThen false
|
||||
assertTrue(checker.isCompatible(deviceName, splits))
|
||||
assertFalse(checker.isCompatible(deviceName, splits))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `non-config splits mixed with unknown config splits always get accepted`() {
|
||||
assertTrue(
|
||||
checker.isCompatible(
|
||||
listOf(
|
||||
"config.de",
|
||||
"config.en",
|
||||
"config.gu",
|
||||
getRandomString(),
|
||||
getRandomString(),
|
||||
getRandomString()
|
||||
)
|
||||
)
|
||||
fun `non-config splits mixed with language config splits get accepted iff allowed`() {
|
||||
val splits = listOf(
|
||||
"config.de",
|
||||
"config.en",
|
||||
"config.gu",
|
||||
getRandomString(),
|
||||
getRandomString(),
|
||||
getRandomString()
|
||||
)
|
||||
every { deviceInfo.areUnknownSplitsAllowed(deviceName) } returns true andThen false
|
||||
every { deviceInfo.isSupportedLanguage("de") } returns true
|
||||
every { deviceInfo.isSupportedLanguage("en") } returns true
|
||||
every { deviceInfo.isSupportedLanguage("gu") } returns true
|
||||
assertTrue(checker.isCompatible(deviceName, splits))
|
||||
assertFalse(checker.isCompatible(deviceName, splits))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unknown config splits get rejected if from different device`() {
|
||||
val unknownName = getRandomString()
|
||||
val splits = listOf("config.$unknownName")
|
||||
every { deviceInfo.isSupportedLanguage(unknownName) } returns false
|
||||
|
||||
// reject if on different device
|
||||
every { deviceInfo.areUnknownSplitsAllowed(deviceName) } returns Random.nextBoolean()
|
||||
every { deviceInfo.isSameDevice(deviceName) } returns false
|
||||
assertFalse(checker.isCompatible(deviceName, splits))
|
||||
|
||||
// accept if same device
|
||||
every { deviceInfo.areUnknownSplitsAllowed(deviceName) } returns Random.nextBoolean()
|
||||
every { deviceInfo.isSameDevice(deviceName) } returns true
|
||||
assertTrue(checker.isCompatible(deviceName, splits))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all supported ABIs get accepted, non-supported rejected`() {
|
||||
val unknownAllowed = Random.nextBoolean()
|
||||
every { deviceInfo.areUnknownSplitsAllowed(deviceName) } returns unknownAllowed
|
||||
every { deviceInfo.supportedABIs } returns listOf("arm64-v8a", "armeabi-v7a", "armeabi")
|
||||
|
||||
assertTrue(checker.isCompatible(listOf("config.arm64_v8a")))
|
||||
assertTrue(checker.isCompatible(listOf("${getRandomString()}.config.arm64_v8a")))
|
||||
assertTrue(checker.isCompatible(listOf("config.armeabi_v7a")))
|
||||
assertTrue(checker.isCompatible(listOf("${getRandomString()}.config.armeabi_v7a")))
|
||||
assertTrue(checker.isCompatible(listOf("config.armeabi")))
|
||||
assertTrue(checker.isCompatible(listOf("${getRandomString()}.config.armeabi")))
|
||||
assertTrue(checker.isCompatible(deviceName, listOf("config.arm64_v8a")))
|
||||
assertEquals(
|
||||
unknownAllowed,
|
||||
checker.isCompatible(deviceName, listOf("${getRandomString()}.config.arm64_v8a"))
|
||||
)
|
||||
assertTrue(checker.isCompatible(deviceName, listOf("config.armeabi_v7a")))
|
||||
assertEquals(
|
||||
unknownAllowed,
|
||||
checker.isCompatible(deviceName, listOf("${getRandomString()}.config.armeabi_v7a"))
|
||||
)
|
||||
assertTrue(checker.isCompatible(deviceName, listOf("config.armeabi")))
|
||||
assertEquals(
|
||||
unknownAllowed,
|
||||
checker.isCompatible(deviceName, listOf("${getRandomString()}.config.armeabi"))
|
||||
)
|
||||
|
||||
assertFalse(checker.isCompatible(listOf("config.x86")))
|
||||
assertFalse(checker.isCompatible(listOf("config.x86_64")))
|
||||
assertFalse(checker.isCompatible(listOf("config.mips")))
|
||||
assertFalse(checker.isCompatible(listOf("config.mips64")))
|
||||
assertFalse(checker.isCompatible(deviceName, listOf("config.x86")))
|
||||
assertFalse(checker.isCompatible(deviceName, listOf("config.x86_64")))
|
||||
assertFalse(checker.isCompatible(deviceName, listOf("config.mips")))
|
||||
assertFalse(checker.isCompatible(deviceName, listOf("config.mips64")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `armeabi rejects arm64_v8a and armeabi-v7a`() {
|
||||
every { deviceInfo.areUnknownSplitsAllowed(deviceName) } returns true
|
||||
every { deviceInfo.supportedABIs } returns listOf("armeabi")
|
||||
|
||||
assertTrue(checker.isCompatible(listOf("config.armeabi")))
|
||||
assertTrue(checker.isCompatible(listOf("${getRandomString()}.config.armeabi")))
|
||||
assertTrue(checker.isCompatible(deviceName, listOf("config.armeabi")))
|
||||
assertTrue(checker.isCompatible(deviceName, listOf("${getRandomString()}.config.armeabi")))
|
||||
|
||||
assertFalse(checker.isCompatible(listOf("config.arm64_v8a")))
|
||||
assertFalse(checker.isCompatible(listOf("${getRandomString()}.config.arm64_v8a")))
|
||||
assertFalse(checker.isCompatible(listOf("config.armeabi_v7a")))
|
||||
assertFalse(checker.isCompatible(listOf("${getRandomString()}.config.armeabi_v7a")))
|
||||
assertFalse(checker.isCompatible(deviceName, listOf("config.arm64_v8a")))
|
||||
assertFalse(
|
||||
checker.isCompatible(deviceName, listOf("${getRandomString()}.config.arm64_v8a"))
|
||||
)
|
||||
assertFalse(checker.isCompatible(deviceName, listOf("config.armeabi_v7a")))
|
||||
assertFalse(
|
||||
checker.isCompatible(deviceName, listOf("${getRandomString()}.config.armeabi_v7a"))
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `screen density is accepted when not too low`() {
|
||||
every { deviceInfo.areUnknownSplitsAllowed(deviceName) } returns Random.nextBoolean()
|
||||
every { deviceInfo.densityDpi } returns 440 // xxhdpi - Pixel 4
|
||||
|
||||
assertTrue(
|
||||
checker.isCompatible(
|
||||
listOf(
|
||||
"config.de",
|
||||
"config.xxxhdpi", // higher density is accepted
|
||||
getRandomString()
|
||||
)
|
||||
)
|
||||
)
|
||||
assertTrue(
|
||||
checker.isCompatible(
|
||||
listOf(
|
||||
"config.de",
|
||||
"config.xxhdpi", // same density is accepted
|
||||
getRandomString()
|
||||
)
|
||||
)
|
||||
)
|
||||
assertTrue(
|
||||
checker.isCompatible(
|
||||
listOf(
|
||||
"config.de",
|
||||
"config.xhdpi", // one lower density is accepted
|
||||
getRandomString()
|
||||
)
|
||||
)
|
||||
)
|
||||
assertFalse(
|
||||
checker.isCompatible(
|
||||
listOf(
|
||||
"config.de",
|
||||
"config.hdpi", // two lower density is not accepted
|
||||
getRandomString()
|
||||
)
|
||||
)
|
||||
)
|
||||
// higher density is accepted
|
||||
assertTrue(checker.isCompatible(deviceName, listOf("config.xxxhdpi")))
|
||||
// same density is accepted
|
||||
assertTrue(checker.isCompatible(deviceName, listOf("config.xxhdpi")))
|
||||
// one lower density is accepted
|
||||
assertTrue(checker.isCompatible(deviceName, listOf("config.xhdpi")))
|
||||
// too low density is not accepted
|
||||
assertFalse(checker.isCompatible(deviceName, listOf("config.hdpi")))
|
||||
// even lower densities are also not accepted
|
||||
assertFalse(checker.isCompatible(listOf("config.tvdpi")))
|
||||
assertFalse(checker.isCompatible(listOf("config.mdpi")))
|
||||
assertFalse(checker.isCompatible(listOf("config.ldpi")))
|
||||
assertFalse(checker.isCompatible(deviceName, listOf("config.tvdpi")))
|
||||
assertFalse(checker.isCompatible(deviceName, listOf("config.mdpi")))
|
||||
assertFalse(checker.isCompatible(deviceName, listOf("config.ldpi")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `screen density accepts all higher densities`() {
|
||||
every { deviceInfo.areUnknownSplitsAllowed(deviceName) } returns Random.nextBoolean()
|
||||
every { deviceInfo.densityDpi } returns 120
|
||||
|
||||
assertTrue(checker.isCompatible(listOf("config.xxxhdpi")))
|
||||
assertTrue(checker.isCompatible(listOf("config.xxhdpi")))
|
||||
assertTrue(checker.isCompatible(listOf("config.xhdpi")))
|
||||
assertTrue(checker.isCompatible(listOf("config.hdpi")))
|
||||
assertTrue(checker.isCompatible(listOf("config.tvdpi")))
|
||||
assertTrue(checker.isCompatible(listOf("config.mdpi")))
|
||||
assertTrue(checker.isCompatible(listOf("config.ldpi")))
|
||||
assertTrue(checker.isCompatible(deviceName, listOf("config.xxxhdpi")))
|
||||
assertTrue(checker.isCompatible(deviceName, listOf("config.xxhdpi")))
|
||||
assertTrue(checker.isCompatible(deviceName, listOf("config.xhdpi")))
|
||||
assertTrue(checker.isCompatible(deviceName, listOf("config.hdpi")))
|
||||
assertTrue(checker.isCompatible(deviceName, listOf("config.tvdpi")))
|
||||
assertTrue(checker.isCompatible(deviceName, listOf("config.mdpi")))
|
||||
assertTrue(checker.isCompatible(deviceName, listOf("config.ldpi")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `config splits in feature modules are considered unknown splits`() {
|
||||
every { deviceInfo.areUnknownSplitsAllowed(deviceName) } returns false
|
||||
|
||||
assertFalse(
|
||||
checker.isCompatible(
|
||||
deviceName,
|
||||
listOf(
|
||||
"${getRandomString()}.config.xhdpi",
|
||||
"${getRandomString()}.config.arm64_v8a"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test mix of unknown and all known config splits`() {
|
||||
val unknownAllowed = Random.nextBoolean()
|
||||
every { deviceInfo.areUnknownSplitsAllowed(deviceName) } returns unknownAllowed
|
||||
every { deviceInfo.supportedABIs } returns listOf("armeabi-v7a", "armeabi")
|
||||
every { deviceInfo.densityDpi } returns 240
|
||||
every { deviceInfo.isSupportedLanguage("de") } returns true
|
||||
|
||||
assertTrue(
|
||||
assertEquals(
|
||||
unknownAllowed,
|
||||
checker.isCompatible(
|
||||
deviceName,
|
||||
listOf(
|
||||
"config.de",
|
||||
"config.xhdpi",
|
||||
|
@ -153,6 +184,7 @@ class ApkSplitCompatibilityCheckerTest : TransportTest() {
|
|||
// same as above, but feature split with unsupported ABI config gets rejected
|
||||
assertFalse(
|
||||
checker.isCompatible(
|
||||
deviceName,
|
||||
listOf(
|
||||
"config.de",
|
||||
"config.xhdpi",
|
||||
|
@ -163,10 +195,14 @@ class ApkSplitCompatibilityCheckerTest : TransportTest() {
|
|||
)
|
||||
)
|
||||
|
||||
assertTrue(checker.isCompatible(listOf("config.xhdpi", "config.armeabi")))
|
||||
assertTrue(checker.isCompatible(listOf("config.hdpi", "config.armeabi_v7a")))
|
||||
assertFalse(checker.isCompatible(listOf("foo.config.ldpi", "config.armeabi_v7a")))
|
||||
assertFalse(checker.isCompatible(listOf("foo.config.xxxhdpi", "bar.config.arm64_v8a")))
|
||||
assertTrue(checker.isCompatible(deviceName, listOf("config.xhdpi", "config.armeabi")))
|
||||
assertTrue(checker.isCompatible(deviceName, listOf("config.hdpi", "config.armeabi_v7a")))
|
||||
assertFalse(
|
||||
checker.isCompatible(deviceName, listOf("foo.config.ldpi", "config.armeabi_v7a"))
|
||||
)
|
||||
assertFalse(
|
||||
checker.isCompatible(deviceName, listOf("foo.config.xxxhdpi", "bar.config.arm64_v8a"))
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
package com.stevesoltys.seedvault.restore.install
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.util.DisplayMetrics
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.getRandomString
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.koin.core.context.stopKoin
|
||||
import org.robolectric.annotation.Config
|
||||
import kotlin.random.Random
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@Config(sdk = [29]) // robolectric does not support 30, yet
|
||||
internal class DeviceInfoTest {
|
||||
|
||||
@After
|
||||
fun afterEachTest() {
|
||||
stopKoin()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test with mocked context`() {
|
||||
val context: Context = mockk()
|
||||
val resources: Resources = mockk()
|
||||
val onlyOnSameDevice = Random.nextBoolean()
|
||||
|
||||
every { context.resources } returns resources
|
||||
every { resources.displayMetrics } returns DisplayMetrics().apply {
|
||||
this.densityDpi = 1337
|
||||
}
|
||||
every { resources.getStringArray(any()) } returns arrayOf("foo-123", "bar-rev")
|
||||
every {
|
||||
resources.getBoolean(R.bool.re_install_unknown_splits_only_on_same_device)
|
||||
} returns onlyOnSameDevice
|
||||
|
||||
val deviceInfo = DeviceInfo(context)
|
||||
|
||||
// the ABI comes from robolectric
|
||||
assertEquals(listOf("armeabi-v7a"), deviceInfo.supportedABIs)
|
||||
|
||||
// check that density is returned as expected
|
||||
assertEquals(1337, deviceInfo.densityDpi)
|
||||
|
||||
// test languages results are as expected
|
||||
assertTrue(deviceInfo.isSupportedLanguage("foo"))
|
||||
assertTrue(deviceInfo.isSupportedLanguage("bar"))
|
||||
assertFalse(deviceInfo.isSupportedLanguage("en"))
|
||||
assertFalse(deviceInfo.isSupportedLanguage("de"))
|
||||
|
||||
// test areUnknownSplitsAllowed
|
||||
val deviceName = "unknown robolectric"
|
||||
if (onlyOnSameDevice) {
|
||||
assertTrue(deviceInfo.areUnknownSplitsAllowed(deviceName))
|
||||
assertFalse(deviceInfo.areUnknownSplitsAllowed("foo bar"))
|
||||
} else {
|
||||
assertTrue(deviceInfo.areUnknownSplitsAllowed(deviceName))
|
||||
assertTrue(deviceInfo.areUnknownSplitsAllowed("foo bar"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test supported languages`() {
|
||||
val deviceInfo = DeviceInfo(ApplicationProvider.getApplicationContext())
|
||||
|
||||
assertTrue(deviceInfo.isSupportedLanguage("en"))
|
||||
assertTrue(deviceInfo.isSupportedLanguage("de"))
|
||||
assertTrue(deviceInfo.isSupportedLanguage("gu"))
|
||||
assertTrue(deviceInfo.isSupportedLanguage("pt"))
|
||||
|
||||
assertFalse(deviceInfo.isSupportedLanguage("foo"))
|
||||
assertFalse(deviceInfo.isSupportedLanguage("bar"))
|
||||
assertFalse(deviceInfo.isSupportedLanguage(getRandomString()))
|
||||
assertFalse(deviceInfo.isSupportedLanguage(getRandomString()))
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue