diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 67c21b387..387a79787 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -3774,6 +3774,11 @@
+
+
+
+
+
@@ -3784,31 +3789,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -4075,6 +4115,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -4083,6 +4133,11 @@
+
+
+
+
+
@@ -5367,6 +5422,11 @@
+
+
+
+
+
diff --git a/index/build.gradle b/index/build.gradle
index ab946f853..1afc4b850 100644
--- a/index/build.gradle
+++ b/index/build.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
diff --git a/index/src/androidMain/kotlin/org/fdroid/CompatibilityChecker.kt b/index/src/androidMain/kotlin/org/fdroid/CompatibilityChecker.kt
index 09c708cbe..5d4ee694c 100644
--- a/index/src/androidMain/kotlin/org/fdroid/CompatibilityChecker.kt
+++ b/index/src/androidMain/kotlin/org/fdroid/CompatibilityChecker.kt
@@ -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 = SUPPORTED_ABIS,
) : CompatibilityChecker {
private val features = HashMap().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
}
diff --git a/index/src/androidMain/kotlin/org/fdroid/UpdateChecker.kt b/index/src/androidMain/kotlin/org/fdroid/UpdateChecker.kt
new file mode 100644
index 000000000..85799b9c9
--- /dev/null
+++ b/index/src/androidMain/kotlin/org/fdroid/UpdateChecker.kt
@@ -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
+}
+
+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 getUpdate(
+ versions: List,
+ packageInfo: PackageInfo,
+ releaseChannels: List? = 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 getSuggestedVersion(
+ versions: List,
+ preferredSigner: String?,
+ releaseChannels: List? = 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 getUpdate(
+ versions: List,
+ allowedSignersGetter: (() -> Set?)? = null,
+ installedVersionCode: Long = 0,
+ allowedReleaseChannels: List? = 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,
+ versionReleaseChannels: List?,
+ 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
+ }
+
+}
diff --git a/index/src/androidMain/kotlin/org/fdroid/index/IndexUtils.kt b/index/src/androidMain/kotlin/org/fdroid/index/IndexUtils.kt
index 68df42d82..c2a670c70 100644
--- a/index/src/androidMain/kotlin/org/fdroid/index/IndexUtils.kt
+++ b/index/src/androidMain/kotlin/org/fdroid/index/IndexUtils.kt
@@ -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 {
diff --git a/index/src/androidTest/kotlin/org/fdroid/CompatibilityCheckerTest.kt b/index/src/androidTest/kotlin/org/fdroid/CompatibilityCheckerTest.kt
new file mode 100644
index 000000000..9174f4c97
--- /dev/null
+++ b/index/src/androidTest/kotlin/org/fdroid/CompatibilityCheckerTest.kt
@@ -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? = null,
+ override val nativecode: List? = null,
+ ) : PackageManifest
+
+}
diff --git a/index/src/androidTest/kotlin/org/fdroid/UpdateCheckerTest.kt b/index/src/androidTest/kotlin/org/fdroid/UpdateCheckerTest.kt
new file mode 100644
index 000000000..55a38a1b1
--- /dev/null
+++ b/index/src/androidTest/kotlin/org/fdroid/UpdateCheckerTest.kt
@@ -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,
+ releaseChannels: List?,
+ ): 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? = 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? = null
+ override val nativecode: List? = null
+ },
+ override val hasKnownVulnerability: Boolean = false,
+ ) : PackageVersion
+
+ private data class AppPreferences(
+ override val ignoreVersionCodeUpdate: Long = 0,
+ override val releaseChannels: List = emptyList(),
+ ) : PackagePreference
+
+}
diff --git a/index/src/commonMain/kotlin/org/fdroid/index/v1/PackageV1.kt b/index/src/commonMain/kotlin/org/fdroid/index/v1/PackageV1.kt
index 1677b6625..573701409 100644
--- a/index/src/commonMain/kotlin/org/fdroid/index/v1/PackageV1.kt
+++ b/index/src/commonMain/kotlin/org/fdroid/index/v1/PackageV1.kt
@@ -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(),
diff --git a/index/src/commonMain/kotlin/org/fdroid/index/v2/PackageV2.kt b/index/src/commonMain/kotlin/org/fdroid/index/v2/PackageV2.kt
index 5dacdc4a2..5b196f10d 100644
--- a/index/src/commonMain/kotlin/org/fdroid/index/v2/PackageV2.kt
+++ b/index/src/commonMain/kotlin/org/fdroid/index/v2/PackageV2.kt
@@ -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?
+ 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 = emptyList(),
+ override val releaseChannels: List = emptyList(),
val antiFeatures: Map = 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?
+ public val nativecode: List?
+}
+
@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 = emptyList(),
val usesPermissionSdk23: List = emptyList(),
- val nativecode: List = emptyList(),
+ override val nativecode: List = emptyList(),
val features: List = emptyList(),
-)
+) : PackageManifest {
+ override val minSdkVersion: Int? = usesSdk?.minSdkVersion
+ override val featureNames: List = 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,
val hasMultipleSigners: Boolean = false,
)
diff --git a/index/src/sharedTest/kotlin/org/fdroid/test/TestDataV2.kt b/index/src/sharedTest/kotlin/org/fdroid/test/TestDataV2.kt
index 6e9587486..a7d6f3495 100644
--- a/index/src/sharedTest/kotlin/org/fdroid/test/TestDataV2.kt
+++ b/index/src/sharedTest/kotlin/org/fdroid/test/TestDataV2.kt
@@ -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",
diff --git a/index/src/sharedTest/kotlin/org/fdroid/test/TestVersionUtils.kt b/index/src/sharedTest/kotlin/org/fdroid/test/TestVersionUtils.kt
index 7227567ea..a83a3ba5d 100644
--- a/index/src/sharedTest/kotlin/org/fdroid/test/TestVersionUtils.kt
+++ b/index/src/sharedTest/kotlin/org/fdroid/test/TestVersionUtils.kt
@@ -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 {