mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-04-23 16:29:28 -04:00
[index] Add UpdateChecker with tests
Also introduce interfaces for important classes, so they can be implemented by stuff like database classes as well. This makes UpdateChecker and CompatibilityChecker more generic. Also add tests for CompatibilityChecker.
This commit is contained in:
@@ -3774,6 +3774,11 @@
|
||||
<sha256 value="dd39be18d38ba8c157d752f8e26aeb05ef80354f73f05faaac9f98c2f3c35496" origin="Generated by Gradle because a key couldn't be downloaded"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="io.mockk" name="mockk" version="1.12.4">
|
||||
<artifact name="mockk-1.12.4.jar">
|
||||
<sha256 value="2c34a3690b958a3cf38b82d0f4910dc9992fb078dce6f56d71498293557bf805" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="io.mockk" name="mockk-agent-android" version="1.12.3">
|
||||
<artifact name="mockk-agent-android-1.12.3.aar">
|
||||
<sha256 value="3a14b27b3888370a8e6e8af62979efdd87f31451c798a41eca741f68ce7772ab" origin="Generated by Gradle because a key couldn't be downloaded"/>
|
||||
@@ -3784,31 +3789,66 @@
|
||||
<sha256 value="c9de731e90bbd7a27ed13e5370e7a10ab44b4fe1e85137bc15568494213aade3" origin="Generated by Gradle because a key couldn't be downloaded"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="io.mockk" name="mockk-agent-api" version="1.12.4">
|
||||
<artifact name="mockk-agent-api-1.12.4.jar">
|
||||
<sha256 value="6eb3407b1f88c0c0ced9636f82874100786b74b3b06c4354c4d85229779fdec8" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="io.mockk" name="mockk-agent-common" version="1.12.3">
|
||||
<artifact name="mockk-agent-common-1.12.3.jar">
|
||||
<sha256 value="201f62541a73bed02ec346e413e7f52f70d300cf8c3ead3d37fca8dfc42fc97c" origin="Generated by Gradle because a key couldn't be downloaded"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="io.mockk" name="mockk-agent-common" version="1.12.4">
|
||||
<artifact name="mockk-agent-common-1.12.4.jar">
|
||||
<sha256 value="427d071ec7a85f105c152a51a89738d8ee52954130e5c09500837dfbe3549329" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="io.mockk" name="mockk-agent-jvm" version="1.12.4">
|
||||
<artifact name="mockk-agent-jvm-1.12.4.jar">
|
||||
<sha256 value="840c11f2e0a14d35e229c2b6018273f4623c7f619ebf9701164bb9c2db99c098" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="io.mockk" name="mockk-android" version="1.12.3">
|
||||
<artifact name="mockk-android-1.12.3.aar">
|
||||
<sha256 value="3ad29a64b6bf8a1ce150f6cda36fdd56dec273a2fbcb169e84ccda6126aa3497" origin="Generated by Gradle because a key couldn't be downloaded"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="io.mockk" name="mockk-android" version="1.12.4">
|
||||
<artifact name="mockk-android-1.12.4.aar">
|
||||
<sha256 value="3ad29a64b6bf8a1ce150f6cda36fdd56dec273a2fbcb169e84ccda6126aa3497" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="io.mockk" name="mockk-common" version="1.12.3">
|
||||
<artifact name="mockk-common-1.12.3.jar">
|
||||
<sha256 value="2fb0d511ab5ec1fa31981d1dba94a9f8da603787a81e59d36395c695b661a2ab" origin="Generated by Gradle because a key couldn't be downloaded"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="io.mockk" name="mockk-common" version="1.12.4">
|
||||
<artifact name="mockk-common-1.12.4.jar">
|
||||
<sha256 value="16f1ba4738535458cb91fa1a759794f6618a6f9f1ae1d149e79b48cc06ea5e7b" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="io.mockk" name="mockk-dsl" version="1.12.3">
|
||||
<artifact name="mockk-dsl-1.12.3.jar">
|
||||
<sha256 value="05ceb1530ec4e6f9c7a1fe9c5b194a97e48023a95aaa4b91c96364f70c020f93" origin="Generated by Gradle because a key couldn't be downloaded"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="io.mockk" name="mockk-dsl" version="1.12.4">
|
||||
<artifact name="mockk-dsl-1.12.4.jar">
|
||||
<sha256 value="7fc96f9ed5118c915a3890ba2e4090c9b283ae7bdc37ab83885415bdf77650e4" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="io.mockk" name="mockk-dsl-jvm" version="1.12.3">
|
||||
<artifact name="mockk-dsl-jvm-1.12.3.jar">
|
||||
<sha256 value="038d1c794cb26e3f56d217fa31ccf9b9f08bdec18c50b078705ed8f2fe1006b9" origin="Generated by Gradle because a key couldn't be downloaded"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="io.mockk" name="mockk-dsl-jvm" version="1.12.4">
|
||||
<artifact name="mockk-dsl-jvm-1.12.4.jar">
|
||||
<sha256 value="faee4b52def68fa182f89d23c2a45f2246ef88b6b1ba98346aa85f57e5ed630f" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="io.netty" name="netty-buffer" version="4.1.34.Final">
|
||||
<artifact name="netty-buffer-4.1.34.Final.jar">
|
||||
<sha256 value="39dfe88df8505fd01fbf9c1dbb6b6fa9b0297e453c3dc4ce039ea578aea2eaa3" origin="Generated by Gradle"/>
|
||||
@@ -4075,6 +4115,16 @@
|
||||
<sha256 value="4b41509cc861e7d9c0bc6d7fd19fd885bf1a43f0357808f8073e5b2029d09d82" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="net.bytebuddy" name="byte-buddy" version="1.12.6">
|
||||
<artifact name="byte-buddy-1.12.6.jar">
|
||||
<sha256 value="211918dc24f0fdef4335ce8af40ef5616e15e818b962a21146397c7701eb75a7" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="net.bytebuddy" name="byte-buddy-agent" version="1.10.20">
|
||||
<artifact name="byte-buddy-agent-1.10.20.jar">
|
||||
<sha256 value="b592a6c43e752bf41659717956c57fbb790394d2ee5f8941876659f9c5c0e7e8" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="net.bytebuddy" name="byte-buddy-agent" version="1.10.5">
|
||||
<artifact name="byte-buddy-agent-1.10.5.jar">
|
||||
<sha256 value="290c9930965ef5810ddb15baf3b3647ce952f40fa2f0af82d5f669e04ba87e5b" origin="Generated by Gradle"/>
|
||||
@@ -4083,6 +4133,11 @@
|
||||
<sha256 value="1e132221d225384596ef5e3fa4808763410981a0ef036f0c25f99c0a73667017" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="net.bytebuddy" name="byte-buddy-agent" version="1.12.6">
|
||||
<artifact name="byte-buddy-agent-1.12.6.jar">
|
||||
<sha256 value="9b29421fe4650b75fc3ed53590f914c54f932e334b3506cc00296dff73024183" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="net.bytebuddy" name="byte-buddy-parent" version="1.10.5">
|
||||
<artifact name="byte-buddy-parent-1.10.5.pom">
|
||||
<sha256 value="d09a551832612058930d5f5a0473c4628be5af8a4961d12b7d5dd6b30160c61a" origin="Generated by Gradle"/>
|
||||
@@ -5367,6 +5422,11 @@
|
||||
<sha256 value="6e0f5490e6b9649ddd2670534e4d3a03bd283c3358b8eef5d1304fd5f8a5a4fb" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-reflect" version="1.6.0">
|
||||
<artifact name="kotlin-reflect-1.6.0.jar">
|
||||
<sha256 value="c6161884209221db7f5ddb031bb480a3c46bb90d5b65d7cc0167b149aaa9c494" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-reflect" version="1.6.10">
|
||||
<artifact name="kotlin-reflect-1.6.10.jar">
|
||||
<sha256 value="3277ac102ae17aad10a55abec75ff5696c8d109790396434b496e75087854203" origin="Generated by Gradle"/>
|
||||
|
||||
@@ -70,6 +70,7 @@ kotlin {
|
||||
androidTest {
|
||||
dependencies {
|
||||
implementation 'junit:junit:4.13.2'
|
||||
implementation 'io.mockk:mockk:1.12.4'
|
||||
}
|
||||
}
|
||||
androidAndroidTest {
|
||||
@@ -105,6 +106,9 @@ android {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
testOptions {
|
||||
unitTests.returnDefaultValues = true
|
||||
}
|
||||
}
|
||||
|
||||
import org.jetbrains.dokka.gradle.DokkaTask
|
||||
|
||||
@@ -3,18 +3,20 @@ package org.fdroid
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build.SUPPORTED_ABIS
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import org.fdroid.index.v2.ManifestV2
|
||||
import org.fdroid.index.v2.PackageManifest
|
||||
|
||||
public fun interface CompatibilityChecker {
|
||||
public fun isCompatible(manifest: ManifestV2): Boolean
|
||||
public fun isCompatible(manifest: PackageManifest): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* This class checks if an APK is compatible with the user's device.
|
||||
*/
|
||||
public class CompatibilityCheckerImpl(
|
||||
public class CompatibilityCheckerImpl @JvmOverloads constructor(
|
||||
packageManager: PackageManager,
|
||||
private val forceTouchApps: Boolean = false,
|
||||
private val sdkInt: Int = SDK_INT,
|
||||
private val supportedAbis: Array<String> = SUPPORTED_ABIS,
|
||||
) : CompatibilityChecker {
|
||||
|
||||
private val features = HashMap<String, Int>().apply {
|
||||
@@ -25,21 +27,22 @@ public class CompatibilityCheckerImpl(
|
||||
}
|
||||
}
|
||||
|
||||
public override fun isCompatible(manifest: ManifestV2): Boolean {
|
||||
if (SDK_INT < manifest.usesSdk?.minSdkVersion ?: 0) return false
|
||||
if (SDK_INT > manifest.maxSdkVersion ?: Int.MAX_VALUE) return false
|
||||
public override fun isCompatible(manifest: PackageManifest): Boolean {
|
||||
if (sdkInt < manifest.minSdkVersion ?: 0) return false
|
||||
if (sdkInt > manifest.maxSdkVersion ?: Int.MAX_VALUE) return false
|
||||
if (!isNativeCodeCompatible(manifest)) return false
|
||||
manifest.features.iterator().forEach { feature ->
|
||||
if (forceTouchApps && feature.name == "android.hardware.touchscreen") return@forEach
|
||||
if (!features.containsKey(feature.name)) return false
|
||||
manifest.featureNames?.iterator()?.forEach { feature ->
|
||||
if (forceTouchApps && feature == "android.hardware.touchscreen") return@forEach
|
||||
if (!features.containsKey(feature)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun isNativeCodeCompatible(manifest: ManifestV2): Boolean {
|
||||
if (manifest.nativecode.isNullOrEmpty()) return true
|
||||
SUPPORTED_ABIS.forEach { supportedAbi ->
|
||||
if (manifest.nativecode.contains(supportedAbi)) return true
|
||||
private fun isNativeCodeCompatible(manifest: PackageManifest): Boolean {
|
||||
val nativeCode = manifest.nativecode
|
||||
if (nativeCode.isNullOrEmpty()) return true
|
||||
supportedAbis.forEach { supportedAbi ->
|
||||
if (nativeCode.contains(supportedAbi)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
152
index/src/androidMain/kotlin/org/fdroid/UpdateChecker.kt
Normal file
152
index/src/androidMain/kotlin/org/fdroid/UpdateChecker.kt
Normal file
@@ -0,0 +1,152 @@
|
||||
package org.fdroid
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
||||
import org.fdroid.index.IndexUtils.getPackageSignature
|
||||
import org.fdroid.index.IndexUtils.getVersionCode
|
||||
import org.fdroid.index.v2.PackageVersion
|
||||
|
||||
public interface PackagePreference {
|
||||
public val ignoreVersionCodeUpdate: Long
|
||||
public val releaseChannels: List<String>
|
||||
}
|
||||
|
||||
public class UpdateChecker(
|
||||
private val compatibilityChecker: CompatibilityChecker,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Returns a [PackageVersion] for the given [packageInfo] that is the suggested update
|
||||
* or null if there is no suitable update in [versions].
|
||||
*
|
||||
* Special case: A version with the [PackageInfo.getLongVersionCode] will be returned
|
||||
* if [PackageVersion.hasKnownVulnerability] is true, even if there is no update.
|
||||
*
|
||||
* @param versions a **sorted** list of [PackageVersion] with highest version code first.
|
||||
* @param packageInfo needs to be retrieved with [GET_SIGNING_CERTIFICATES]
|
||||
* @param releaseChannels optional list of release channels to consider on top of stable.
|
||||
* If this is null or empty, only versions without channel (stable) will be considered.
|
||||
* @param preferencesGetter an optional way to consider additional per app preferences
|
||||
*/
|
||||
public fun <T : PackageVersion> getUpdate(
|
||||
versions: List<T>,
|
||||
packageInfo: PackageInfo,
|
||||
releaseChannels: List<String>? = null,
|
||||
preferencesGetter: (() -> PackagePreference?)? = null,
|
||||
): T? = getUpdate(
|
||||
versions = versions,
|
||||
allowedSignersGetter = {
|
||||
// always gives us the oldest signer, even if they rotated certs by now
|
||||
@Suppress("DEPRECATION")
|
||||
packageInfo.signatures.map { getPackageSignature(it.toByteArray()) }.toSet()
|
||||
},
|
||||
installedVersionCode = packageInfo.getVersionCode(),
|
||||
allowedReleaseChannels = releaseChannels,
|
||||
preferencesGetter = preferencesGetter,
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns the [PackageVersion] that is suggested for a new installation
|
||||
* or null if there is no suitable candidate in [versions].
|
||||
*
|
||||
* @param versions a **sorted** list of [PackageVersion] with highest version code first.
|
||||
* @param preferredSigner The SHA-256 hash of the signing certificate in lower-case hex.
|
||||
* Only versions from this signer will be considered for installation.
|
||||
* @param releaseChannels optional list of release channels to consider on top of stable.
|
||||
* If this is null or empty, only versions without channel (stable) will be considered.
|
||||
* @param preferencesGetter an optional way to consider additional per app preferences
|
||||
*/
|
||||
public fun <T : PackageVersion> getSuggestedVersion(
|
||||
versions: List<T>,
|
||||
preferredSigner: String?,
|
||||
releaseChannels: List<String>? = null,
|
||||
preferencesGetter: (() -> PackagePreference?)? = null,
|
||||
): T? = getUpdate(
|
||||
versions = versions,
|
||||
allowedSignersGetter = preferredSigner?.let { { setOf(it) } },
|
||||
allowedReleaseChannels = releaseChannels,
|
||||
preferencesGetter = preferencesGetter,
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns the [PackageVersion] that is the suggested update
|
||||
* for the given [installedVersionCode] or suggested for new installed if the given code is 0,
|
||||
* or null if there is no suitable candidate in [versions].
|
||||
*
|
||||
* Special case: A version with the [installedVersionCode] will be returned
|
||||
* if [PackageVersion.hasKnownVulnerability] is true, even if there is no update.
|
||||
*
|
||||
* @param versions a **sorted** list of [PackageVersion] with highest version code first.
|
||||
* @param allowedSignersGetter should return set of SHA-256 hashes of the signing certificates
|
||||
* in lower-case hex. Only versions from these signers will be considered for installation.
|
||||
* This is is null or returns null, all signers will be allowed.
|
||||
* If the set of signers is empty, no signers will be allowed, i.e. only apps without signer.
|
||||
* @param allowedReleaseChannels optional list of release channels to consider on top of stable.
|
||||
* If this is null or empty, only versions without channel (stable) will be considered.
|
||||
* @param preferencesGetter an optional way to consider additional per app preferences
|
||||
*/
|
||||
public fun <T : PackageVersion> getUpdate(
|
||||
versions: List<T>,
|
||||
allowedSignersGetter: (() -> Set<String>?)? = null,
|
||||
installedVersionCode: Long = 0,
|
||||
allowedReleaseChannels: List<String>? = null,
|
||||
preferencesGetter: (() -> PackagePreference?)? = null,
|
||||
): T? {
|
||||
// getting signatures is rather expensive, so we only do that when there's update candidates
|
||||
val allowedSigners by lazy { allowedSignersGetter?.let { it() } }
|
||||
versions.iterator().forEach versions@{ version ->
|
||||
// if the installed version has a known vulnerability, we return it as well
|
||||
if (version.versionCode == installedVersionCode && version.hasKnownVulnerability) {
|
||||
return version
|
||||
}
|
||||
// if version code is not higher than installed skip package as list is sorted
|
||||
if (version.versionCode <= installedVersionCode) return null
|
||||
// we don't support versions that have multiple signers
|
||||
if (version.signer?.hasMultipleSigners == true) return@versions
|
||||
// skip incompatible versions
|
||||
if (!compatibilityChecker.isCompatible(version.packageManifest)) return@versions
|
||||
// check if we should ignore this version code
|
||||
val packagePreference = preferencesGetter?.let { it() }
|
||||
val ignoreVersionCode = packagePreference?.ignoreVersionCodeUpdate ?: 0
|
||||
if (ignoreVersionCode >= version.versionCode) return@versions
|
||||
// check if release channel of version is allowed
|
||||
val hasAllowedReleaseChannel = hasAllowedReleaseChannel(
|
||||
allowedReleaseChannels = allowedReleaseChannels?.toMutableSet() ?: LinkedHashSet(),
|
||||
versionReleaseChannels = version.releaseChannels,
|
||||
packagePreference = packagePreference,
|
||||
)
|
||||
if (!hasAllowedReleaseChannel) return@versions
|
||||
// check if this version's signer is allowed
|
||||
val versionSigners = version.signer?.sha256?.toSet()
|
||||
// F-Droid allows versions without signature 🤦, allow those and if no allowed signers
|
||||
if (versionSigners != null && allowedSigners != null) {
|
||||
if (versionSigners.intersect(allowedSigners!!).isEmpty()) return@versions
|
||||
}
|
||||
// no need to see other versions, we got the highest version code per sorting
|
||||
return version
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun hasAllowedReleaseChannel(
|
||||
allowedReleaseChannels: MutableSet<String>,
|
||||
versionReleaseChannels: List<String>?,
|
||||
packagePreference: PackagePreference?,
|
||||
): Boolean {
|
||||
// no channels (aka stable version) is always allowed
|
||||
if (versionReleaseChannels.isNullOrEmpty()) return true
|
||||
|
||||
// add release channels from package preferences into the ones we allow
|
||||
val extraChannels = packagePreference?.releaseChannels
|
||||
if (!extraChannels.isNullOrEmpty()) {
|
||||
allowedReleaseChannels.addAll(extraChannels)
|
||||
}
|
||||
// if allowed releases channels are empty (only stable) don't consider this version
|
||||
if (allowedReleaseChannels.isEmpty()) return false
|
||||
// don't consider version with non-matching release channel
|
||||
if (allowedReleaseChannels.intersect(versionReleaseChannels).isEmpty()) return false
|
||||
// one of the allowed channels is present in this version
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -36,7 +36,7 @@ public object IndexUtils {
|
||||
return messageDigest.digest()
|
||||
}
|
||||
|
||||
internal fun PackageInfo.getVersionCode(): Long {
|
||||
public fun PackageInfo.getVersionCode(): Long {
|
||||
return if (Build.VERSION.SDK_INT >= 28) {
|
||||
longVersionCode
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package org.fdroid
|
||||
|
||||
import android.content.pm.FeatureInfo
|
||||
import android.content.pm.PackageManager
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.fdroid.index.v2.PackageManifest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
internal class CompatibilityCheckerTest {
|
||||
|
||||
private val sdkInt: Int = 30
|
||||
private val supportedAbis = arrayOf("x86")
|
||||
private val packageManager: PackageManager = mockk()
|
||||
|
||||
init {
|
||||
every { packageManager.systemAvailableFeatures } returns arrayOf(
|
||||
FeatureInfo().apply { name = "foo bar" },
|
||||
FeatureInfo().apply { name = "1337" },
|
||||
)
|
||||
}
|
||||
|
||||
private val checker = CompatibilityCheckerImpl(
|
||||
packageManager = packageManager,
|
||||
forceTouchApps = false,
|
||||
sdkInt = sdkInt,
|
||||
supportedAbis = supportedAbis,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun emptyManifestIsCompatible() {
|
||||
val manifest = Manifest()
|
||||
assertTrue(checker.isCompatible(manifest))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun minSdkIsRespected() {
|
||||
// smaller or equal minSdks are compatible
|
||||
val manifest1 = Manifest(minSdkVersion = 1)
|
||||
assertTrue(checker.isCompatible(manifest1))
|
||||
val manifest2 = Manifest(minSdkVersion = sdkInt)
|
||||
assertTrue(checker.isCompatible(manifest2))
|
||||
// a minSdk higher than the system is not compatible
|
||||
val manifest3 = Manifest(minSdkVersion = sdkInt + 1)
|
||||
assertFalse(checker.isCompatible(manifest3))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun maxSdkIsRespected() {
|
||||
// smaller maxSdks are not compatible
|
||||
val manifest1 = Manifest(maxSdkVersion = sdkInt - 1)
|
||||
assertFalse(checker.isCompatible(manifest1))
|
||||
// higher or equal are compatible
|
||||
val manifest2 = Manifest(maxSdkVersion = sdkInt)
|
||||
assertTrue(checker.isCompatible(manifest2))
|
||||
val manifest3 = Manifest(maxSdkVersion = sdkInt + 1)
|
||||
assertTrue(checker.isCompatible(manifest3))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun emptyNativeCodeIsCompatible() {
|
||||
val manifest = Manifest(nativecode = emptyList())
|
||||
assertTrue(checker.isCompatible(manifest))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nativeCodeMustBeAvailable() {
|
||||
val manifest1 = Manifest(nativecode = listOf("x86"))
|
||||
assertTrue(checker.isCompatible(manifest1))
|
||||
val manifest2 = Manifest(nativecode = listOf("x86", "armeabi-v7a"))
|
||||
assertTrue(checker.isCompatible(manifest2))
|
||||
val manifest3 = Manifest(nativecode = listOf("arm64-v8a", "armeabi-v7a"))
|
||||
assertFalse(checker.isCompatible(manifest3))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun featuresMustBeAvailable() {
|
||||
val manifest1 = Manifest(featureNames = listOf("foo bar"))
|
||||
assertTrue(checker.isCompatible(manifest1))
|
||||
val manifest2 = Manifest(featureNames = listOf("1337", "foo bar"))
|
||||
assertTrue(checker.isCompatible(manifest2))
|
||||
val manifest3 = Manifest(featureNames = listOf("1337", "foo bar", "42"))
|
||||
assertFalse(checker.isCompatible(manifest3))
|
||||
val manifest4 = Manifest(featureNames = listOf("foo", "bar"))
|
||||
assertFalse(checker.isCompatible(manifest4))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun forceTouchScreenIsRespected() {
|
||||
val checkerForce = CompatibilityCheckerImpl(packageManager, true, sdkInt, supportedAbis)
|
||||
|
||||
// when forced, apps that need touchscreen on non-touchscreen device are compatible
|
||||
val manifest1 = Manifest(featureNames = listOf("android.hardware.touchscreen"))
|
||||
assertTrue(checkerForce.isCompatible(manifest1))
|
||||
val manifest2 = Manifest(featureNames = listOf("android.hardware.touchscreen"))
|
||||
assertTrue(checkerForce.isCompatible(manifest2))
|
||||
// when not forced, apps that need touchscreen on non-touchscreen device are not compatible
|
||||
val manifest3 = Manifest(featureNames = listOf("android.hardware.touchscreen"))
|
||||
assertFalse(checker.isCompatible(manifest3))
|
||||
val manifest4 = Manifest(featureNames = listOf("android.hardware.touchscreen"))
|
||||
assertFalse(checker.isCompatible(manifest4))
|
||||
}
|
||||
|
||||
private data class Manifest(
|
||||
override val minSdkVersion: Int? = null,
|
||||
override val maxSdkVersion: Int? = null,
|
||||
override val featureNames: List<String>? = null,
|
||||
override val nativecode: List<String>? = null,
|
||||
) : PackageManifest
|
||||
|
||||
}
|
||||
179
index/src/androidTest/kotlin/org/fdroid/UpdateCheckerTest.kt
Normal file
179
index/src/androidTest/kotlin/org/fdroid/UpdateCheckerTest.kt
Normal file
@@ -0,0 +1,179 @@
|
||||
package org.fdroid
|
||||
|
||||
import org.fdroid.index.RELEASE_CHANNEL_BETA
|
||||
import org.fdroid.index.v2.PackageManifest
|
||||
import org.fdroid.index.v2.PackageVersion
|
||||
import org.fdroid.index.v2.SignerV2
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
internal class UpdateCheckerTest {
|
||||
|
||||
private val updateChecker = UpdateChecker { true }
|
||||
private val signer = "9f9261f0b911c60f8db722f5d430a9e9d557a3f8078ce43e1c07522ef41efedb"
|
||||
private val signatureV2 = SignerV2(listOf(signer))
|
||||
private val betaChannels = listOf(RELEASE_CHANNEL_BETA)
|
||||
private val version1 = Version(1)
|
||||
private val version2 = Version(2)
|
||||
private val version3 = Version(3)
|
||||
private val versions = listOf(version3, version2, version1)
|
||||
|
||||
@Test
|
||||
fun highestVersionCode() {
|
||||
assertEquals(version3, updateChecker.getUpdate(versions))
|
||||
assertEquals(version3, updateChecker.getUpdate(versions, installedVersionCode = 2))
|
||||
assertEquals(version3, updateChecker.getUpdate(versions, installedVersionCode = 1))
|
||||
assertEquals(version3, updateChecker.getUpdate(versions, installedVersionCode = 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noUpdateIfSameOrHigherVersionInstalled() {
|
||||
assertNull(updateChecker.getUpdate(versions, installedVersionCode = 3))
|
||||
assertNull(updateChecker.getUpdate(versions, installedVersionCode = 4))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIncompatibleVersionNotConsidered() {
|
||||
var versionNum = 0
|
||||
val updateChecker = UpdateChecker {
|
||||
versionNum++
|
||||
versionNum != 1 // highest version is incompatible
|
||||
}
|
||||
val v = updateChecker.getUpdate(versions)
|
||||
assertEquals(version2, v)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ignoredVersionNotConsidered() {
|
||||
val not4 = { AppPreferences(ignoreVersionCodeUpdate = 4) }
|
||||
val not3 = { AppPreferences(ignoreVersionCodeUpdate = 3) }
|
||||
val not2 = { AppPreferences(ignoreVersionCodeUpdate = 2) }
|
||||
assertNull(updateChecker.getUpdate(versions, preferencesGetter = not4))
|
||||
assertNull(updateChecker.getUpdate(versions, preferencesGetter = not3))
|
||||
assertEquals(version3, updateChecker.getUpdate(versions, preferencesGetter = not2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun betaVersionOnlyReturnedWhenAllowed() {
|
||||
val version3 = version3.copy(releaseChannels = betaChannels)
|
||||
val versions = listOf(version3, version2, version1)
|
||||
// beta not allowed, so 2 returned
|
||||
assertEquals(version2, updateChecker.getUpdate(versions))
|
||||
// now beta is allowed, so 3 returned
|
||||
assertEquals(version3, getWithAllowReleaseChannels(versions, betaChannels))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun emptyReleaseChannelsAlwaysIncluded() {
|
||||
val version3 = version3.copy(releaseChannels = emptyList())
|
||||
val versions = listOf(version3, version2, version1)
|
||||
// version with empty release channels gets returned
|
||||
assertEquals(version3, updateChecker.getUpdate(versions))
|
||||
// version with empty release channels gets returned when allowing also beta
|
||||
assertEquals(version3,
|
||||
updateChecker.getUpdate(versions, allowedReleaseChannels = betaChannels)
|
||||
)
|
||||
// version with empty release channels gets returned when allow list is empty
|
||||
assertEquals(version3, getWithAllowReleaseChannels(versions, emptyList()))
|
||||
assertEquals(this.version3, getWithAllowReleaseChannels(this.versions, emptyList()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyAllowedReleaseChannelsGetIncluded() {
|
||||
val version3 = version3.copy(releaseChannels = listOf("a"))
|
||||
val version2 = version2.copy(releaseChannels = listOf("a", "b", "c"))
|
||||
val versions = listOf(version3, version2, version1)
|
||||
// only stable version gets returned
|
||||
assertEquals(version1, getWithAllowReleaseChannels(versions, null))
|
||||
// as long as "a" is included, 3 gets returned
|
||||
assertEquals(version3, getWithAllowReleaseChannels(versions, listOf("a")))
|
||||
assertEquals(version3, getWithAllowReleaseChannels(versions, listOf("a", "b")))
|
||||
assertEquals(version3, getWithAllowReleaseChannels(versions, listOf("a", "b", "z")))
|
||||
// as long as "b" or "c" is included, 2 gets returned
|
||||
assertEquals(version2, getWithAllowReleaseChannels(versions, listOf("b")))
|
||||
assertEquals(version2, getWithAllowReleaseChannels(versions, listOf("b", "z")))
|
||||
assertEquals(version2, getWithAllowReleaseChannels(versions, listOf("c")))
|
||||
assertEquals(version2, getWithAllowReleaseChannels(versions, listOf("c", "z")))
|
||||
// if neither "a", "b" or "c" is included, 1 gets returned
|
||||
assertEquals(version1, getWithAllowReleaseChannels(versions, listOf("x", "y", "z")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multipleSignersNotSupported() {
|
||||
val version = version3.copy(signer = signatureV2.copy(hasMultipleSigners = true))
|
||||
val versions = listOf(version)
|
||||
assertNull(updateChecker.getUpdate(versions))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyAllowedSignersGetIncluded() {
|
||||
val version3 = version3.copy(signer = SignerV2(listOf("foo", "bar")))
|
||||
val version2 = version2.copy(signer = signatureV2)
|
||||
val versions = listOf(version3, version2, version1)
|
||||
val v2Set = signatureV2.sha256.toMutableSet()
|
||||
// 3 gets returned if at least one of its signers are allowed, or all are allowed
|
||||
assertEquals(version3, updateChecker.getUpdate(versions, { setOf("foo") }))
|
||||
assertEquals(version3, updateChecker.getUpdate(versions, { setOf("bar") }))
|
||||
assertEquals(version3, updateChecker.getUpdate(versions, { v2Set + "bar" }))
|
||||
assertEquals(version3, updateChecker.getUpdate(versions, { setOf("foo", "bar") }))
|
||||
assertEquals(version3, updateChecker.getUpdate(versions, { setOf("foo", "bar", "42") }))
|
||||
assertEquals(version3, updateChecker.getUpdate(versions, { null }))
|
||||
// 2 gets returned if at least one of its signers are allowed
|
||||
assertEquals(version2, updateChecker.getUpdate(versions, { v2Set }))
|
||||
assertEquals(version2, updateChecker.getUpdate(versions, { v2Set + "foo bar" }))
|
||||
// empty set means no signatures are allowed, only works for apps without signature
|
||||
assertEquals(version1, updateChecker.getUpdate(versions, { emptySet() }))
|
||||
// apps without signature get through everything
|
||||
assertEquals(version1, updateChecker.getUpdate(versions, { setOf("no version") }))
|
||||
// if no matching sig can be found, no version gets returned
|
||||
assertNull(updateChecker.getUpdate(listOf(version3, version2), { setOf("no version") }))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun installedVulnerableVersionAlwaysReturned() {
|
||||
val version3 = version3.copy(hasKnownVulnerability = true)
|
||||
val versions = listOf(version3, version2, version1)
|
||||
assertEquals(version3, updateChecker.getUpdate(versions))
|
||||
assertEquals(version3, updateChecker.getUpdate(versions, installedVersionCode = 3))
|
||||
assertEquals(version3, updateChecker.getUpdate(versions, installedVersionCode = 2))
|
||||
}
|
||||
|
||||
private fun getWithAllowReleaseChannels(
|
||||
versions: List<Version>,
|
||||
releaseChannels: List<String>?,
|
||||
): Version? {
|
||||
val v1 = updateChecker.getUpdate(versions, allowedReleaseChannels = releaseChannels)
|
||||
assertEquals(v1,
|
||||
updateChecker.getUpdate(versions) {
|
||||
AppPreferences(releaseChannels = releaseChannels ?: emptyList())
|
||||
}
|
||||
)
|
||||
assertEquals(v1,
|
||||
updateChecker.getUpdate(versions, allowedReleaseChannels = releaseChannels) {
|
||||
AppPreferences(releaseChannels = releaseChannels ?: emptyList())
|
||||
}
|
||||
)
|
||||
return v1
|
||||
}
|
||||
|
||||
private data class Version(
|
||||
override val versionCode: Long,
|
||||
override val signer: SignerV2? = null,
|
||||
override val releaseChannels: List<String>? = null,
|
||||
// the manifest is only needed for compatibility checking which we can test differently
|
||||
override val packageManifest: PackageManifest = object : PackageManifest {
|
||||
override val minSdkVersion: Int? = null
|
||||
override val maxSdkVersion: Int? = null
|
||||
override val featureNames: List<String>? = null
|
||||
override val nativecode: List<String>? = null
|
||||
},
|
||||
override val hasKnownVulnerability: Boolean = false,
|
||||
) : PackageVersion
|
||||
|
||||
private data class AppPreferences(
|
||||
override val ignoreVersionCodeUpdate: Long = 0,
|
||||
override val releaseChannels: List<String> = emptyList(),
|
||||
) : PackagePreference
|
||||
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import org.fdroid.index.v2.LocalizedTextV2
|
||||
import org.fdroid.index.v2.ManifestV2
|
||||
import org.fdroid.index.v2.PackageVersionV2
|
||||
import org.fdroid.index.v2.PermissionV2
|
||||
import org.fdroid.index.v2.SignatureV2
|
||||
import org.fdroid.index.v2.SignerV2
|
||||
import org.fdroid.index.v2.UsesSdkV2
|
||||
|
||||
@Serializable
|
||||
@@ -71,7 +71,7 @@ public data class PackageV1(
|
||||
targetSdkVersion = targetSdkVersion ?: minSdkVersion ?: 1,
|
||||
),
|
||||
maxSdkVersion = maxSdkVersion,
|
||||
signer = signer?.let { SignatureV2(listOf(it)) },
|
||||
signer = signer?.let { SignerV2(listOf(it)) },
|
||||
usesPermission = usesPermission.map { PermissionV2(it.name, it.maxSdk) },
|
||||
usesPermissionSdk23 = usesPermission23.map { PermissionV2(it.name, it.maxSdk) },
|
||||
nativecode = nativeCode ?: emptyList(),
|
||||
|
||||
@@ -71,16 +71,32 @@ public data class Screenshots(
|
||||
get() = phone == null && sevenInch == null && tenInch == null && wear == null && tv == null
|
||||
}
|
||||
|
||||
public interface PackageVersion {
|
||||
public val versionCode: Long
|
||||
public val signer: SignerV2?
|
||||
public val releaseChannels: List<String>?
|
||||
public val packageManifest: PackageManifest
|
||||
public val hasKnownVulnerability: Boolean
|
||||
}
|
||||
|
||||
public const val ANTI_FEATURE_KNOWN_VULNERABILITY: String = "KnownVuln"
|
||||
|
||||
@Serializable
|
||||
public data class PackageVersionV2(
|
||||
val added: Long,
|
||||
val file: FileV1,
|
||||
val src: FileV2? = null,
|
||||
val manifest: ManifestV2,
|
||||
val releaseChannels: List<String> = emptyList(),
|
||||
override val releaseChannels: List<String> = emptyList(),
|
||||
val antiFeatures: Map<String, LocalizedTextV2> = emptyMap(),
|
||||
val whatsNew: LocalizedTextV2 = emptyMap(),
|
||||
) {
|
||||
) : PackageVersion {
|
||||
override val versionCode: Long = manifest.versionCode
|
||||
override val signer: SignerV2? = manifest.signer
|
||||
override val packageManifest: PackageManifest = manifest
|
||||
override val hasKnownVulnerability: Boolean
|
||||
get() = antiFeatures.contains(ANTI_FEATURE_KNOWN_VULNERABILITY)
|
||||
|
||||
public fun walkFiles(fileConsumer: (FileV2?) -> Unit) {
|
||||
fileConsumer(src)
|
||||
}
|
||||
@@ -97,18 +113,28 @@ public data class FileV1(
|
||||
val size: Long? = null,
|
||||
)
|
||||
|
||||
public interface PackageManifest {
|
||||
public val minSdkVersion: Int?
|
||||
public val maxSdkVersion: Int?
|
||||
public val featureNames: List<String>?
|
||||
public val nativecode: List<String>?
|
||||
}
|
||||
|
||||
@Serializable
|
||||
public data class ManifestV2(
|
||||
val versionName: String,
|
||||
val versionCode: Long,
|
||||
val usesSdk: UsesSdkV2? = null,
|
||||
val maxSdkVersion: Int? = null,
|
||||
val signer: SignatureV2? = null, // TODO really null?
|
||||
override val maxSdkVersion: Int? = null,
|
||||
val signer: SignerV2? = null, // yes this can be null for stuff like non-apps
|
||||
val usesPermission: List<PermissionV2> = emptyList(),
|
||||
val usesPermissionSdk23: List<PermissionV2> = emptyList(),
|
||||
val nativecode: List<String> = emptyList(),
|
||||
override val nativecode: List<String> = emptyList(),
|
||||
val features: List<FeatureV2> = emptyList(),
|
||||
)
|
||||
) : PackageManifest {
|
||||
override val minSdkVersion: Int? = usesSdk?.minSdkVersion
|
||||
override val featureNames: List<String> = features.map { it.name }
|
||||
}
|
||||
|
||||
@Serializable
|
||||
public data class UsesSdkV2(
|
||||
@@ -117,7 +143,7 @@ public data class UsesSdkV2(
|
||||
)
|
||||
|
||||
@Serializable
|
||||
public data class SignatureV2(
|
||||
public data class SignerV2(
|
||||
val sha256: List<String>,
|
||||
val hasMultipleSigners: Boolean = false,
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.fdroid.index.v2.PermissionV2
|
||||
import org.fdroid.index.v2.ReleaseChannelV2
|
||||
import org.fdroid.index.v2.RepoV2
|
||||
import org.fdroid.index.v2.Screenshots
|
||||
import org.fdroid.index.v2.SignatureV2
|
||||
import org.fdroid.index.v2.SignerV2
|
||||
import org.fdroid.index.v2.UsesSdkV2
|
||||
|
||||
internal const val LOCALE = "en-US"
|
||||
@@ -72,7 +72,7 @@ internal fun PackageVersionV2.v1compat() = copy(
|
||||
src = src?.v1compat(),
|
||||
manifest = manifest.copy(
|
||||
signer = if (manifest.signer?.sha256?.size ?: 0 <= 1) manifest.signer else {
|
||||
SignatureV2(manifest.signer?.sha256?.subList(0, 1) ?: error(""))
|
||||
SignerV2(manifest.signer?.sha256?.subList(0, 1) ?: error(""))
|
||||
}
|
||||
),
|
||||
releaseChannels = releaseChannels.filter { it == RELEASE_CHANNEL_BETA },
|
||||
@@ -245,7 +245,7 @@ internal object TestDataMidV2 {
|
||||
targetSdkVersion = 32,
|
||||
),
|
||||
maxSdkVersion = 4568,
|
||||
signer = SignatureV2(
|
||||
signer = SignerV2(
|
||||
sha256 = listOf("824a109b2352138c3699760e1683385d0ed50ce526fc7982f8d65757743374bf"),
|
||||
),
|
||||
),
|
||||
@@ -315,7 +315,7 @@ internal object TestDataMidV2 {
|
||||
minSdkVersion = 22,
|
||||
targetSdkVersion = 25,
|
||||
),
|
||||
signer = SignatureV2(
|
||||
signer = SignerV2(
|
||||
sha256 = listOf("43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab"),
|
||||
),
|
||||
usesPermission = listOf(
|
||||
@@ -366,7 +366,7 @@ internal object TestDataMidV2 {
|
||||
minSdkVersion = 22,
|
||||
targetSdkVersion = 25,
|
||||
),
|
||||
signer = SignatureV2(
|
||||
signer = SignerV2(
|
||||
sha256 = listOf("43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab"),
|
||||
),
|
||||
usesPermission = listOf(
|
||||
@@ -416,7 +416,7 @@ internal object TestDataMidV2 {
|
||||
minSdkVersion = 22,
|
||||
targetSdkVersion = 25,
|
||||
),
|
||||
signer = SignatureV2(
|
||||
signer = SignerV2(
|
||||
sha256 = listOf("43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab"),
|
||||
),
|
||||
usesPermission = listOf(
|
||||
@@ -463,7 +463,7 @@ internal object TestDataMidV2 {
|
||||
minSdkVersion = 22,
|
||||
targetSdkVersion = 25,
|
||||
),
|
||||
signer = SignatureV2(
|
||||
signer = SignerV2(
|
||||
sha256 = listOf("43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab"),
|
||||
),
|
||||
usesPermission = listOf(
|
||||
@@ -510,7 +510,7 @@ internal object TestDataMidV2 {
|
||||
minSdkVersion = 22,
|
||||
targetSdkVersion = 25,
|
||||
),
|
||||
signer = SignatureV2(
|
||||
signer = SignerV2(
|
||||
sha256 = listOf("43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab"),
|
||||
),
|
||||
usesPermission = listOf(
|
||||
@@ -839,7 +839,7 @@ internal object TestDataMaxV2 {
|
||||
minSdkVersion = 22,
|
||||
targetSdkVersion = 25,
|
||||
),
|
||||
signer = SignatureV2(
|
||||
signer = SignerV2(
|
||||
sha256 = listOf("43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab"),
|
||||
),
|
||||
usesPermission = listOf(
|
||||
@@ -999,7 +999,7 @@ internal object TestDataMaxV2 {
|
||||
targetSdkVersion = 25,
|
||||
),
|
||||
maxSdkVersion = Int.MAX_VALUE,
|
||||
signer = SignatureV2(
|
||||
signer = SignerV2(
|
||||
sha256 = listOf(
|
||||
"43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab",
|
||||
"33238d512c1e3eb2d6569f4a3bfbf5523418b22e0a3ed1552770abb9a9c9ccvb",
|
||||
|
||||
@@ -5,7 +5,7 @@ import org.fdroid.index.v2.FileV1
|
||||
import org.fdroid.index.v2.ManifestV2
|
||||
import org.fdroid.index.v2.PackageVersionV2
|
||||
import org.fdroid.index.v2.PermissionV2
|
||||
import org.fdroid.index.v2.SignatureV2
|
||||
import org.fdroid.index.v2.SignerV2
|
||||
import org.fdroid.index.v2.UsesSdkV2
|
||||
import org.fdroid.test.TestRepoUtils.getRandomFileV2
|
||||
import org.fdroid.test.TestRepoUtils.getRandomLocalizedTextV2
|
||||
@@ -37,7 +37,7 @@ internal object TestVersionUtils {
|
||||
targetSdkVersion = Random.nextInt(),
|
||||
),
|
||||
maxSdkVersion = Random.nextInt().orNull(),
|
||||
signer = SignatureV2(getRandomList(Random.nextInt(1, 3)) {
|
||||
signer = SignerV2(getRandomList(Random.nextInt(1, 3)) {
|
||||
getRandomString(64)
|
||||
}).orNull(),
|
||||
usesPermission = getRandomList {
|
||||
|
||||
Reference in New Issue
Block a user