Add a compatibility checker for APK splits
that tries to figure out compatibility only based on the name of the split. This is not an exact science and there might be errors, but we hope to correctly identify most cases that matter in practice.
This commit is contained in:
parent
6e4c117fca
commit
68a6403c4b
2 changed files with 290 additions and 0 deletions
|
@ -0,0 +1,118 @@
|
|||
package com.stevesoltys.seedvault.restore.install
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
|
||||
private const val TAG = "SplitCompatChecker"
|
||||
|
||||
private const val CONFIG_PREFIX = "config."
|
||||
private const val CONFIG_LENGTH = CONFIG_PREFIX.length
|
||||
|
||||
private const val LDPI_VALUE = 120
|
||||
private const val MDPI_VALUE = 160
|
||||
private const val TVDPI_VALUE = 213
|
||||
private const val HDPI_VALUE = 240
|
||||
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.
|
||||
* 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.
|
||||
*/
|
||||
class ApkSplitCompatibilityChecker(private val deviceInfo: DeviceInfo) {
|
||||
|
||||
private val abiMap = mapOf(
|
||||
"armeabi" to "armeabi",
|
||||
"armeabi_v7a" to "armeabi-v7a",
|
||||
"arm64_v8a" to "arm64-v8a",
|
||||
"x86" to "x86",
|
||||
"x86_64" to "x86_64",
|
||||
"mips" to "mips",
|
||||
"mips64" to "mips64"
|
||||
)
|
||||
private val densityMap = mapOf(
|
||||
"ldpi" to LDPI_VALUE,
|
||||
"mdpi" to MDPI_VALUE,
|
||||
"tvdpi" to TVDPI_VALUE,
|
||||
"hdpi" to HDPI_VALUE,
|
||||
"xhdpi" to XHDPI_VALUE,
|
||||
"xxhdpi" to XXHDPI_VALUE,
|
||||
"xxxhdpi" to XXXHDPI_VALUE
|
||||
)
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
private fun isCompatible(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) {
|
||||
Log.v(TAG, "Not a config split '$splitName'. Assuming it is ok.")
|
||||
return true
|
||||
}
|
||||
|
||||
val name = splitName.substring(index + CONFIG_LENGTH)
|
||||
|
||||
// Check if this is a known ABI config
|
||||
if (abiMap.containsKey(name)) {
|
||||
// The ABI split must be supported by the current device
|
||||
return isAbiCompatible(name)
|
||||
}
|
||||
|
||||
// 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.
|
||||
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
|
||||
}
|
||||
|
||||
private fun isAbiCompatible(name: String): Boolean {
|
||||
return if (deviceInfo.supportedABIs.contains(abiMap[name])) {
|
||||
Log.v(TAG, "Config split '$name' is supported ABI (${deviceInfo.supportedABIs})")
|
||||
true
|
||||
} else {
|
||||
Log.w(TAG, "Config split '$name' is not supported ABI (${deviceInfo.supportedABIs})")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isDensityCompatible(splitDensity: Int): Boolean {
|
||||
@Suppress("MagicNumber")
|
||||
val acceptableDiff = deviceInfo.densityDpi / 3
|
||||
return if (deviceInfo.densityDpi - splitDensity > acceptableDiff) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Config split density $splitDensity not compatible with ${deviceInfo.densityDpi}"
|
||||
)
|
||||
false
|
||||
} else {
|
||||
Log.v(
|
||||
TAG,
|
||||
"Config split density $splitDensity compatible with ${deviceInfo.densityDpi}"
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
package com.stevesoltys.seedvault.restore.install
|
||||
|
||||
import com.stevesoltys.seedvault.getRandomString
|
||||
import com.stevesoltys.seedvault.transport.TransportTest
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ApkSplitCompatibilityCheckerTest : TransportTest() {
|
||||
|
||||
private val deviceInfo: DeviceInfo = mockk()
|
||||
|
||||
private val checker = ApkSplitCompatibilityChecker(deviceInfo)
|
||||
|
||||
@Test
|
||||
fun `non-config splits always get accepted`() {
|
||||
assertTrue(
|
||||
checker.isCompatible(
|
||||
listOf(
|
||||
getRandomString(),
|
||||
getRandomString(),
|
||||
getRandomString(),
|
||||
getRandomString(),
|
||||
getRandomString(),
|
||||
getRandomString()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@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()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all supported ABIs get accepted, non-supported rejected`() {
|
||||
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")))
|
||||
|
||||
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")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `armeabi rejects arm64_v8a and armeabi-v7a`() {
|
||||
every { deviceInfo.supportedABIs } returns listOf("armeabi")
|
||||
|
||||
assertTrue(checker.isCompatible(listOf("config.armeabi")))
|
||||
assertTrue(checker.isCompatible(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")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `screen density is accepted when not too low`() {
|
||||
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()
|
||||
)
|
||||
)
|
||||
)
|
||||
// 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")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `screen density accepts all higher densities`() {
|
||||
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")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test mix of unknown and all known config splits`() {
|
||||
every { deviceInfo.supportedABIs } returns listOf("armeabi-v7a", "armeabi")
|
||||
every { deviceInfo.densityDpi } returns 240
|
||||
|
||||
assertTrue(
|
||||
checker.isCompatible(
|
||||
listOf(
|
||||
"config.de",
|
||||
"config.xhdpi",
|
||||
"config.armeabi",
|
||||
getRandomString()
|
||||
)
|
||||
)
|
||||
)
|
||||
// same as above, but feature split with unsupported ABI config gets rejected
|
||||
assertFalse(
|
||||
checker.isCompatible(
|
||||
listOf(
|
||||
"config.de",
|
||||
"config.xhdpi",
|
||||
"config.armeabi",
|
||||
"${getRandomString()}.config.arm64_v8a",
|
||||
getRandomString()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
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")))
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue