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:
Torsten Grote 2020-10-15 16:12:51 -03:00
parent e6723093c9
commit 2cde417c8c
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
10 changed files with 325 additions and 132 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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