diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesAdapter.java index da37dceed..44e188278 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesAdapter.java @@ -101,7 +101,7 @@ public class UpdatesAdapter extends RecyclerView.Adapter releaseChannels = Preferences.get().getBackendReleaseChannels(); if (disposable != null) disposable.dispose(); - disposable = Utils.runOffUiThread(() -> updateChecker.getUpdatableApps(releaseChannels), + disposable = Utils.runOffUiThread(() -> updateChecker.getUpdatableApps(releaseChannels, true), this::onCanUpdateLoadFinished); } diff --git a/libs/database/src/main/java/org/fdroid/database/DbUpdateChecker.kt b/libs/database/src/main/java/org/fdroid/database/DbUpdateChecker.kt index 76e0febea..470979a78 100644 --- a/libs/database/src/main/java/org/fdroid/database/DbUpdateChecker.kt +++ b/libs/database/src/main/java/org/fdroid/database/DbUpdateChecker.kt @@ -26,7 +26,11 @@ public class DbUpdateChecker @JvmOverloads constructor( * @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. */ - public fun getUpdatableApps(releaseChannels: List? = null): List { + @JvmOverloads + public fun getUpdatableApps( + releaseChannels: List? = null, + includeKnownVulnerabilities: Boolean = false, + ): List { val updatableApps = ArrayList() @Suppress("DEPRECATION") // we'll use this as long as it works, new one was broken @@ -40,7 +44,14 @@ public class DbUpdateChecker @JvmOverloads constructor( installedPackages.iterator().forEach { packageInfo -> val packageName = packageInfo.packageName val versions = versionsByPackage[packageName] ?: return@forEach // continue - val version = getVersion(versions, packageName, packageInfo, null, releaseChannels) + val version = getVersion( + versions = versions, + packageName = packageName, + packageInfo = packageInfo, + preferredSigner = null, + releaseChannels = releaseChannels, + includeKnownVulnerabilities = includeKnownVulnerabilities, + ) if (version != null) { val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo) val app = getUpdatableApp(version, versionCode) @@ -70,8 +81,13 @@ public class DbUpdateChecker @JvmOverloads constructor( } catch (e: PackageManager.NameNotFoundException) { null } - val version = getVersion(versions, packageName, packageInfo, preferredSigner, - releaseChannels) ?: return null + val version = getVersion( + versions = versions, + packageName = packageName, + packageInfo = packageInfo, + preferredSigner = preferredSigner, + releaseChannels = releaseChannels, + ) ?: return null val versionedStrings = versionDao.getVersionedStrings( repoId = version.repoId, packageName = version.packageName, @@ -86,6 +102,7 @@ public class DbUpdateChecker @JvmOverloads constructor( packageInfo: PackageInfo?, preferredSigner: String?, releaseChannels: List?, + includeKnownVulnerabilities: Boolean = false, ): Version? { val preferencesGetter: (() -> PackagePreference?) = { appPrefsDao.getAppPrefsOrNull(packageName) @@ -102,6 +119,7 @@ public class DbUpdateChecker @JvmOverloads constructor( versions = versions, packageInfo = packageInfo, releaseChannels = releaseChannels, + includeKnownVulnerabilities = includeKnownVulnerabilities, preferencesGetter = preferencesGetter, ) } diff --git a/libs/index/src/androidMain/kotlin/org/fdroid/UpdateChecker.kt b/libs/index/src/androidMain/kotlin/org/fdroid/UpdateChecker.kt index 5f1a91bfb..895abf235 100644 --- a/libs/index/src/androidMain/kotlin/org/fdroid/UpdateChecker.kt +++ b/libs/index/src/androidMain/kotlin/org/fdroid/UpdateChecker.kt @@ -19,19 +19,20 @@ public class UpdateChecker( * 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 + * @param includeKnownVulnerabilities if true, + * versions with the [PackageInfo.getLongVersionCode] will be returned + * if [PackageVersion.hasKnownVulnerability] is true, even without real update. */ public fun getUpdate( versions: List, packageInfo: PackageInfo, releaseChannels: List? = null, + includeKnownVulnerabilities: Boolean = false, preferencesGetter: (() -> PackagePreference?)? = null, ): T? = getUpdate( versions = versions, @@ -43,6 +44,7 @@ public class UpdateChecker( installedVersionCode = PackageInfoCompat.getLongVersionCode(packageInfo), allowedReleaseChannels = releaseChannels, preferencesGetter = preferencesGetter, + includeKnownVulnerabilities = includeKnownVulnerabilities, ) /** @@ -73,9 +75,6 @@ public class UpdateChecker( * 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. @@ -83,22 +82,25 @@ public class UpdateChecker( * 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 + * @param preferencesGetter an optional way to consider additional per app preferences. + * @param includeKnownVulnerabilities if true, versions with the [installedVersionCode] + * will be returned if [PackageVersion.hasKnownVulnerability] is true, even without real update. */ public fun getUpdate( versions: List, allowedSignersGetter: (() -> Set?)? = null, installedVersionCode: Long = 0, allowedReleaseChannels: List? = null, + includeKnownVulnerabilities: Boolean = false, 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 (includeKnownVulnerabilities && + 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 diff --git a/libs/index/src/androidTest/kotlin/org/fdroid/UpdateCheckerTest.kt b/libs/index/src/androidTest/kotlin/org/fdroid/UpdateCheckerTest.kt index 55a38a1b1..5d6cd5c0f 100644 --- a/libs/index/src/androidTest/kotlin/org/fdroid/UpdateCheckerTest.kt +++ b/libs/index/src/androidTest/kotlin/org/fdroid/UpdateCheckerTest.kt @@ -71,7 +71,8 @@ internal class UpdateCheckerTest { // 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, + assertEquals( + version3, updateChecker.getUpdate(versions, allowedReleaseChannels = betaChannels) ) // version with empty release channels gets returned when allow list is empty @@ -134,9 +135,30 @@ internal class UpdateCheckerTest { 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)) + assertEquals( + version3, + updateChecker.getUpdate(versions, includeKnownVulnerabilities = true) + ) + assertEquals( + version3, + updateChecker.getUpdate( + versions, + installedVersionCode = 3, + includeKnownVulnerabilities = true, + ) + ) + assertEquals( + version3, + updateChecker.getUpdate( + versions, + installedVersionCode = 2, + includeKnownVulnerabilities = true, + ) + ) + // when not asking for known vulnerabilities, version3 isn't returned (no update here) + assertNull( + updateChecker.getUpdate(versions, installedVersionCode = 3) + ) } private fun getWithAllowReleaseChannels(