From 772cdb170fc2822c3b4cece992b0eb94bc423478 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 6 Apr 2022 14:19:07 -0300 Subject: [PATCH] [db] Calculate app compatibility in DB and remove feature version as it app can't even use that. Compatibility checking has been added to the DB layer as a post-processing step only because the UI wants to query for that on the app level (which would need all APKs). --- .../org/fdroid/database/IndexV1InsertTest.kt | 10 ++-- .../org/fdroid/database/IndexV2InsertTest.kt | 4 +- .../org/fdroid/database/RepositoryTest.kt | 3 +- .../org/fdroid/database/UpdateCheckerTest.kt | 2 +- .../java/org/fdroid/database/VersionTest.kt | 39 +++++++++----- .../fdroid/database/test/TestVersionUtils.kt | 4 +- .../src/main/java/org/fdroid/database/App.kt | 19 +++++-- .../main/java/org/fdroid/database/AppDao.kt | 24 +++++++-- .../org/fdroid/database/DbStreamReceiver.kt | 10 +++- .../org/fdroid/database/DbV1StreamReceiver.kt | 8 ++- .../java/org/fdroid/database/Repository.kt | 2 +- .../java/org/fdroid/database/UpdateChecker.kt | 39 +++++++++++--- .../main/java/org/fdroid/database/Version.kt | 52 ++++++++----------- .../java/org/fdroid/database/VersionDao.kt | 30 +++++++---- .../org/fdroid/index/v1/IndexV1Updater.kt | 6 ++- 15 files changed, 171 insertions(+), 81 deletions(-) diff --git a/database/src/androidTest/java/org/fdroid/database/IndexV1InsertTest.kt b/database/src/androidTest/java/org/fdroid/database/IndexV1InsertTest.kt index 38835e78d..4398febce 100644 --- a/database/src/androidTest/java/org/fdroid/database/IndexV1InsertTest.kt +++ b/database/src/androidTest/java/org/fdroid/database/IndexV1InsertTest.kt @@ -25,7 +25,7 @@ class IndexV1InsertTest : DbTest() { val fileSize = c.resources.assets.openFd("index-v1.json").use { it.length } val inputStream = CountingInputStream(c.resources.assets.open("index-v1.json")) var currentByteCount: Long = 0 - val indexProcessor = IndexV1StreamProcessor(DbV1StreamReceiver(db), null) { + val indexProcessor = IndexV1StreamProcessor(DbV1StreamReceiver(db) { true }, null) { val bytesRead = inputStream.byteCount val bytesSinceLastCall = bytesRead - currentByteCount if (bytesSinceLastCall > 0) { @@ -84,10 +84,10 @@ class IndexV1InsertTest : DbTest() { val localizedFileLists2 = localizedFileLists.count { it.repoId == 2L } assertEquals(localizedFileLists1, localizedFileLists2) - appMetadata.filter { it.repoId ==2L }.forEach { m -> + appMetadata.filter { it.repoId == 2L }.forEach { m -> val metadata1 = appDao.getAppMetadata(1, m.packageId) val metadata2 = appDao.getAppMetadata(2, m.packageId) - assertEquals(metadata1, metadata2.copy(repoId = 1)) + assertEquals(metadata1, metadata2.copy(repoId = 1, isCompatible = true)) val lFiles1 = appDao.getLocalizedFiles(1, m.packageId).toSet() val lFiles2 = appDao.getLocalizedFiles(2, m.packageId) @@ -111,7 +111,7 @@ class IndexV1InsertTest : DbTest() { private fun insertV2ForComparison() { val c = getApplicationContext() val inputStream = CountingInputStream(c.resources.assets.open("index-v2.json")) - val indexProcessor = IndexStreamProcessor(DbStreamReceiver(db), null) + val indexProcessor = IndexStreamProcessor(DbStreamReceiver(db) { true }, null) db.runInTransaction { val repoId = db.getRepositoryDao().insertEmptyRepo("https://f-droid.org/repo") inputStream.use { indexStream -> @@ -124,7 +124,7 @@ class IndexV1InsertTest : DbTest() { fun testExceptionWhileStreamingDoesNotSaveIntoDb() { val c = getApplicationContext() val cIn = CountingInputStream(c.resources.assets.open("index-v1.json")) - val indexProcessor = IndexStreamProcessor(DbStreamReceiver(db), null) { + val indexProcessor = IndexStreamProcessor(DbStreamReceiver(db) { true }, null) { if (cIn.byteCount > 824096) throw SerializationException() cIn.byteCount } diff --git a/database/src/androidTest/java/org/fdroid/database/IndexV2InsertTest.kt b/database/src/androidTest/java/org/fdroid/database/IndexV2InsertTest.kt index e6240c057..a058fc2bb 100644 --- a/database/src/androidTest/java/org/fdroid/database/IndexV2InsertTest.kt +++ b/database/src/androidTest/java/org/fdroid/database/IndexV2InsertTest.kt @@ -22,7 +22,7 @@ class IndexV2InsertTest : DbTest() { val fileSize = c.resources.assets.openFd("index-v2.json").use { it.length } val inputStream = CountingInputStream(c.resources.assets.open("index-v2.json")) var currentByteCount: Long = 0 - val indexProcessor = IndexStreamProcessor(DbStreamReceiver(db), null) { + val indexProcessor = IndexStreamProcessor(DbStreamReceiver(db) { true }, null) { val bytesRead = inputStream.byteCount val bytesSinceLastCall = bytesRead - currentByteCount if (bytesSinceLastCall > 0) { @@ -60,7 +60,7 @@ class IndexV2InsertTest : DbTest() { fun testExceptionWhileStreamingDoesNotSaveIntoDb() { val c = getApplicationContext() val cIn = CountingInputStream(c.resources.assets.open("index-v2.json")) - val indexProcessor = IndexStreamProcessor(DbStreamReceiver(db), null) { + val indexProcessor = IndexStreamProcessor(DbStreamReceiver(db) { true }, null) { if (cIn.byteCount > 824096) throw SerializationException() cIn.byteCount } diff --git a/database/src/androidTest/java/org/fdroid/database/RepositoryTest.kt b/database/src/androidTest/java/org/fdroid/database/RepositoryTest.kt index 86c3a38b2..070656fe8 100644 --- a/database/src/androidTest/java/org/fdroid/database/RepositoryTest.kt +++ b/database/src/androidTest/java/org/fdroid/database/RepositoryTest.kt @@ -8,6 +8,7 @@ import org.fdroid.database.test.TestUtils.getRandomString import org.fdroid.database.test.TestVersionUtils.getRandomPackageVersionV2 import org.junit.Test import org.junit.runner.RunWith +import kotlin.random.Random import kotlin.test.assertEquals import kotlin.test.assertNull import kotlin.test.assertTrue @@ -64,7 +65,7 @@ class RepositoryTest : DbTest() { val versionId = getRandomString() appDao.insert(repoId, packageId, getRandomMetadataV2()) val packageVersion = getRandomPackageVersionV2() - versionDao.insert(repoId, packageId, versionId, packageVersion) + versionDao.insert(repoId, packageId, versionId, packageVersion, Random.nextBoolean()) assertEquals(1, repoDao.getRepositories().size) assertEquals(1, appDao.getAppMetadata().size) diff --git a/database/src/androidTest/java/org/fdroid/database/UpdateCheckerTest.kt b/database/src/androidTest/java/org/fdroid/database/UpdateCheckerTest.kt index 47a23fa6d..d33b0edb7 100644 --- a/database/src/androidTest/java/org/fdroid/database/UpdateCheckerTest.kt +++ b/database/src/androidTest/java/org/fdroid/database/UpdateCheckerTest.kt @@ -29,7 +29,7 @@ class UpdateCheckerTest : DbTest() { @Test fun testGetUpdates() { val inputStream = CountingInputStream(context.resources.assets.open("index-v1.json")) - val indexProcessor = IndexV1StreamProcessor(DbV1StreamReceiver(db), null) + val indexProcessor = IndexV1StreamProcessor(DbV1StreamReceiver(db) { true }, null) db.runInTransaction { val repoId = db.getRepositoryDao().insertEmptyRepo("https://f-droid.org/repo") diff --git a/database/src/androidTest/java/org/fdroid/database/VersionTest.kt b/database/src/androidTest/java/org/fdroid/database/VersionTest.kt index 51416fb29..9560f9292 100644 --- a/database/src/androidTest/java/org/fdroid/database/VersionTest.kt +++ b/database/src/androidTest/java/org/fdroid/database/VersionTest.kt @@ -10,6 +10,7 @@ import org.fdroid.database.test.TestVersionUtils.getRandomPackageVersionV2 import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import kotlin.random.Random import kotlin.test.assertEquals import kotlin.test.fail @@ -27,21 +28,25 @@ class VersionTest : DbTest() { val repoId = repoDao.insert(getRandomRepo()) appDao.insert(repoId, packageId, getRandomMetadataV2()) val packageVersion = getRandomPackageVersionV2() - versionDao.insert(repoId, packageId, versionId, packageVersion) + val isCompatible = Random.nextBoolean() + versionDao.insert(repoId, packageId, versionId, packageVersion, isCompatible) val appVersions = versionDao.getAppVersions(repoId, packageId) assertEquals(1, appVersions.size) val appVersion = appVersions[0] assertEquals(versionId, appVersion.version.versionId) - assertEquals(packageVersion.toVersion(repoId, packageId, versionId), appVersion.version) + val version = packageVersion.toVersion(repoId, packageId, versionId, isCompatible) + assertEquals(version, appVersion.version) val manifest = packageVersion.manifest assertEquals(manifest.usesPermission.toSet(), appVersion.usesPermission?.toSet()) assertEquals(manifest.usesPermissionSdk23.toSet(), appVersion.usesPermissionSdk23?.toSet()) - assertEquals(manifest.features.toSet(), appVersion.features?.toSet()) + assertEquals( + manifest.features.map { it.name }.toSet(), + appVersion.version.manifest.features?.toSet() + ) val versionedStrings = versionDao.getVersionedStrings(repoId, packageId) - val expectedSize = - manifest.usesPermission.size + manifest.usesPermissionSdk23.size + manifest.features.size + val expectedSize = manifest.usesPermission.size + manifest.usesPermissionSdk23.size assertEquals(expectedSize, versionedStrings.size) versionDao.deleteAppVersion(repoId, packageId, versionId) @@ -55,11 +60,13 @@ class VersionTest : DbTest() { val repoId = repoDao.insert(getRandomRepo()) appDao.insert(repoId, packageId, getRandomMetadataV2()) val packageVersion1 = getRandomPackageVersionV2() - val version1 = getRandomString() - versionDao.insert(repoId, packageId, version1, packageVersion1) val packageVersion2 = getRandomPackageVersionV2() + val version1 = getRandomString() val version2 = getRandomString() - versionDao.insert(repoId, packageId, version2, packageVersion2) + val isCompatible1 = Random.nextBoolean() + val isCompatible2 = Random.nextBoolean() + versionDao.insert(repoId, packageId, version1, packageVersion1, isCompatible1) + versionDao.insert(repoId, packageId, version2, packageVersion2, isCompatible2) // get app versions from DB and assign them correctly val appVersions = versionDao.getAppVersions(packageId).getOrAwaitValue() ?: fail() @@ -72,19 +79,27 @@ class VersionTest : DbTest() { } else appVersions[1] // check first version matches - assertEquals(packageVersion1.toVersion(repoId, packageId, version1), appVersion.version) + val exVersion1 = packageVersion1.toVersion(repoId, packageId, version1, isCompatible1) + assertEquals(exVersion1, appVersion.version) val manifest = packageVersion1.manifest assertEquals(manifest.usesPermission.toSet(), appVersion.usesPermission?.toSet()) assertEquals(manifest.usesPermissionSdk23.toSet(), appVersion.usesPermissionSdk23?.toSet()) - assertEquals(manifest.features.toSet(), appVersion.features?.toSet()) + assertEquals( + manifest.features.map { it.name }.toSet(), + appVersion.version.manifest.features?.toSet() + ) // check second version matches - assertEquals(packageVersion2.toVersion(repoId, packageId, version2), appVersion2.version) + val exVersion2 = packageVersion2.toVersion(repoId, packageId, version2, isCompatible2) + assertEquals(exVersion2, appVersion2.version) val manifest2 = packageVersion2.manifest assertEquals(manifest2.usesPermission.toSet(), appVersion2.usesPermission?.toSet()) assertEquals(manifest2.usesPermissionSdk23.toSet(), appVersion2.usesPermissionSdk23?.toSet()) - assertEquals(manifest2.features.toSet(), appVersion2.features?.toSet()) + assertEquals( + manifest.features.map { it.name }.toSet(), + appVersion.version.manifest.features?.toSet() + ) // delete app and check that all associated data also gets deleted appDao.deleteAppMetadata(repoId, packageId) diff --git a/database/src/androidTest/java/org/fdroid/database/test/TestVersionUtils.kt b/database/src/androidTest/java/org/fdroid/database/test/TestVersionUtils.kt index 584226852..caa128b4a 100644 --- a/database/src/androidTest/java/org/fdroid/database/test/TestVersionUtils.kt +++ b/database/src/androidTest/java/org/fdroid/database/test/TestVersionUtils.kt @@ -47,9 +47,7 @@ internal object TestVersionUtils { PermissionV2(getRandomString(), Random.nextInt().orNull()) }, nativeCode = getRandomList(Random.nextInt(0, 4)) { getRandomString() }, - features = getRandomList { - FeatureV2(getRandomString(), Random.nextInt().orNull()) - }, + features = getRandomList { FeatureV2(getRandomString()) }, ) } diff --git a/database/src/main/java/org/fdroid/database/App.kt b/database/src/main/java/org/fdroid/database/App.kt index 983b76c7a..4fd4e1867 100644 --- a/database/src/main/java/org/fdroid/database/App.kt +++ b/database/src/main/java/org/fdroid/database/App.kt @@ -35,6 +35,8 @@ data class AppMetadata( val name: LocalizedTextV2? = null, val summary: LocalizedTextV2? = null, val description: LocalizedTextV2? = null, + val localizedName: String? = null, + val localizedSummary: String? = null, val webSite: String? = null, val changelog: String? = null, val license: String? = null, @@ -46,9 +48,20 @@ data class AppMetadata( @Embedded(prefix = "author_") val author: Author? = Author(), @Embedded(prefix = "donation_") val donation: Donation? = Donation(), val categories: List? = null, + /** + * Whether the app is compatible with the current device. + * This value will be computed and is always false until that happened. + * So to always get correct data, this MUST happen within the same transaction + * that adds the [AppMetadata]. + */ + val isCompatible: Boolean, ) -fun MetadataV2.toAppMetadata(repoId: Long, packageId: String) = AppMetadata( +fun MetadataV2.toAppMetadata( + repoId: Long, + packageId: String, + isCompatible: Boolean = false, +) = AppMetadata( repoId = repoId, packageId = packageId, added = added, @@ -67,6 +80,7 @@ fun MetadataV2.toAppMetadata(repoId: Long, packageId: String) = AppMetadata( author = if (author?.isNull == true) null else author, donation = if (donation?.isNull == true) null else donation, categories = categories, + isCompatible = isCompatible, ) data class App( @@ -143,8 +157,7 @@ public data class AppListItem @JvmOverloads constructor( /** * If true, this this app has at least one version that is compatible with this device. */ - @Ignore // TODO actually get this from the DB (probably needs post-processing). - public val isCompatible: Boolean = true, + public val isCompatible: Boolean, /** * The name of the installed version, null if this app is not installed. */ diff --git a/database/src/main/java/org/fdroid/database/AppDao.kt b/database/src/main/java/org/fdroid/database/AppDao.kt index 2337f3dbc..115c75bca 100644 --- a/database/src/main/java/org/fdroid/database/AppDao.kt +++ b/database/src/main/java/org/fdroid/database/AppDao.kt @@ -48,7 +48,7 @@ internal interface AppDaoInt : AppDao { @Transaction override fun insert(repoId: Long, packageId: String, app: MetadataV2) { - insert(app.toAppMetadata(repoId, packageId)) + insert(app.toAppMetadata(repoId, packageId, false)) app.icon.insert(repoId, packageId, "icon") app.featureGraphic.insert(repoId, packageId, "featureGraphic") app.promoGraphic.insert(repoId, packageId, "promoGraphic") @@ -90,6 +90,20 @@ internal interface AppDaoInt : AppDao { @Query("UPDATE AppMetadata SET preferredSigner = :preferredSigner WHERE repoId = :repoId AND packageId = :packageId") fun updatePreferredSigner(repoId: Long, packageId: String, preferredSigner: String?) + /** + * Updates the [AppMetadata.isCompatible] flag + * based on whether at least one [AppVersion] is compatible. + * This needs to run within the transaction that adds [AppMetadata] to the DB. + * Otherwise the compatibility is wrong. + */ + @Query("""UPDATE AppMetadata + SET isCompatible = ( + SELECT TOTAL(isCompatible) > 0 FROM Version + WHERE repoId = :repoId AND AppMetadata.packageId = Version.packageId + ) + WHERE repoId = :repoId""") + fun updateCompatibility(repoId: Long) + override fun getApp(packageId: String): LiveData { return getRepoIdForPackage(packageId).distinctUntilChanged().switchMap { repoId -> if (repoId == null) MutableLiveData(null) @@ -227,7 +241,8 @@ internal interface AppDaoInt : AppDao { } @Transaction - @Query("""SELECT repoId, packageId, app.name, summary, version.antiFeatures + @Query(""" + SELECT repoId, packageId, app.name, summary, version.antiFeatures, app.isCompatible FROM AppMetadata AS app JOIN Version AS version USING (repoId, packageId) JOIN RepositoryPreferences AS pref USING (repoId) @@ -245,7 +260,8 @@ internal interface AppDaoInt : AppDao { // TODO maybe it makes sense to split categories into their own table for this? @Transaction - @Query("""SELECT repoId, packageId, app.name, summary, version.antiFeatures + @Query(""" + SELECT repoId, packageId, app.name, summary, version.antiFeatures, app.isCompatible FROM AppMetadata AS app JOIN Version AS version USING (repoId, packageId) JOIN RepositoryPreferences AS pref USING (repoId) @@ -256,7 +272,7 @@ internal interface AppDaoInt : AppDao { @Transaction @SuppressWarnings(CURSOR_MISMATCH) // no anti-features needed here - @Query("""SELECT repoId, packageId, app.name, summary + @Query("""SELECT repoId, packageId, app.name, summary, app.isCompatible FROM AppMetadata AS app JOIN RepositoryPreferences AS pref USING (repoId) WHERE pref.enabled = 1 AND packageId IN (:packageNames) diff --git a/database/src/main/java/org/fdroid/database/DbStreamReceiver.kt b/database/src/main/java/org/fdroid/database/DbStreamReceiver.kt index 807f0cd00..27c1cf979 100644 --- a/database/src/main/java/org/fdroid/database/DbStreamReceiver.kt +++ b/database/src/main/java/org/fdroid/database/DbStreamReceiver.kt @@ -1,11 +1,13 @@ package org.fdroid.database +import org.fdroid.CompatibilityChecker import org.fdroid.index.v2.IndexStreamReceiver import org.fdroid.index.v2.PackageV2 import org.fdroid.index.v2.RepoV2 internal class DbStreamReceiver( private val db: FDroidDatabaseInt, + private val compatibilityChecker: CompatibilityChecker, ) : IndexStreamReceiver { override fun receive(repoId: Long, repo: RepoV2, certificate: String?) { @@ -14,7 +16,13 @@ internal class DbStreamReceiver( override fun receive(repoId: Long, packageId: String, p: PackageV2) { db.getAppDao().insert(repoId, packageId, p.metadata) - db.getVersionDao().insert(repoId, packageId, p.versions) + db.getVersionDao().insert(repoId, packageId, p.versions) { + compatibilityChecker.isCompatible(it.manifest) + } + } + + override fun onStreamEnded(repoId: Long) { + db.getAppDao().updateCompatibility(repoId) } } diff --git a/database/src/main/java/org/fdroid/database/DbV1StreamReceiver.kt b/database/src/main/java/org/fdroid/database/DbV1StreamReceiver.kt index 68ea29112..672833d1f 100644 --- a/database/src/main/java/org/fdroid/database/DbV1StreamReceiver.kt +++ b/database/src/main/java/org/fdroid/database/DbV1StreamReceiver.kt @@ -1,5 +1,6 @@ package org.fdroid.database +import org.fdroid.CompatibilityChecker import org.fdroid.index.v1.IndexV1StreamReceiver import org.fdroid.index.v2.AntiFeatureV2 import org.fdroid.index.v2.CategoryV2 @@ -10,6 +11,7 @@ import org.fdroid.index.v2.RepoV2 internal class DbV1StreamReceiver( private val db: FDroidDatabaseInt, + private val compatibilityChecker: CompatibilityChecker, ) : IndexV1StreamReceiver { override fun receive(repoId: Long, repo: RepoV2, certificate: String?) { @@ -21,7 +23,9 @@ internal class DbV1StreamReceiver( } override fun receive(repoId: Long, packageId: String, v: Map) { - db.getVersionDao().insert(repoId, packageId, v) + db.getVersionDao().insert(repoId, packageId, v) { + compatibilityChecker.isCompatible(it.manifest) + } } override fun updateRepo( @@ -34,6 +38,8 @@ internal class DbV1StreamReceiver( repoDao.insertAntiFeatures(antiFeatures.toRepoAntiFeatures(repoId)) repoDao.insertCategories(categories.toRepoCategories(repoId)) repoDao.insertReleaseChannels(releaseChannels.toRepoReleaseChannel(repoId)) + + db.getAppDao().updateCompatibility(repoId) } override fun updateAppMetadata(repoId: Long, packageId: String, preferredSigner: String?) { diff --git a/database/src/main/java/org/fdroid/database/Repository.kt b/database/src/main/java/org/fdroid/database/Repository.kt index 85a0c37dc..dbe10067b 100644 --- a/database/src/main/java/org/fdroid/database/Repository.kt +++ b/database/src/main/java/org/fdroid/database/Repository.kt @@ -224,7 +224,7 @@ data class RepositoryPreferences( @PrimaryKey internal val repoId: Long, val weight: Int, val enabled: Boolean = true, - val lastUpdated: Long? = System.currentTimeMillis(), // TODO set this after repo updates + val lastUpdated: Long? = System.currentTimeMillis(), val lastETag: String? = null, val userMirrors: List? = null, val disabledMirrors: List? = null, diff --git a/database/src/main/java/org/fdroid/database/UpdateChecker.kt b/database/src/main/java/org/fdroid/database/UpdateChecker.kt index e826787ef..1d1bcc332 100644 --- a/database/src/main/java/org/fdroid/database/UpdateChecker.kt +++ b/database/src/main/java/org/fdroid/database/UpdateChecker.kt @@ -5,6 +5,7 @@ import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.pm.PackageManager.GET_SIGNATURES import android.os.Build +import org.fdroid.CompatibilityCheckerImpl import org.fdroid.index.IndexUtils public class UpdateChecker( @@ -14,9 +15,16 @@ public class UpdateChecker( private val appDao = db.getAppDao() as AppDaoInt private val versionDao = db.getVersionDao() as VersionDaoInt + private val compatibilityChecker = CompatibilityCheckerImpl(packageManager) - fun getUpdatableApps(): List { + /** + * Returns a list of apps that can be updated. + * @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. + */ + fun getUpdatableApps(releaseChannels: List? = null): List { val updatableApps = ArrayList() + @Suppress("DEPRECATION") // we'll use this as long as it works, new one was broken val installedPackages = packageManager.getInstalledPackages(GET_SIGNATURES) val packageNames = installedPackages.map { it.packageName } @@ -27,7 +35,7 @@ public class UpdateChecker( } installedPackages.iterator().forEach { packageInfo -> val versions = versionsByPackage[packageInfo.packageName] ?: return@forEach // continue - val version = getVersion(versions, packageInfo) + val version = getVersion(versions, packageInfo, releaseChannels) if (version != null) { val versionCode = packageInfo.getVersionCode() val app = getUpdatableApp(version, versionCode) @@ -37,8 +45,14 @@ public class UpdateChecker( return updatableApps } + /** + * Returns an [AppVersion] for the given [packageName] that is an update + * or null if there is none. + * @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. + */ @SuppressLint("PackageManagerGetSignatures") - fun getUpdate(packageName: String): AppVersion? { + fun getUpdate(packageName: String, releaseChannels: List? = null): AppVersion? { val versions = versionDao.getVersions(listOf(packageName)) if (versions.isEmpty()) return null val packageInfo = try { @@ -47,7 +61,7 @@ public class UpdateChecker( } catch (e: PackageManager.NameNotFoundException) { null } - val version = getVersion(versions, packageInfo) ?: return null + val version = getVersion(versions, packageInfo, releaseChannels) ?: return null val versionedStrings = versionDao.getVersionedStrings( repoId = version.repoId, packageId = version.packageId, @@ -56,7 +70,11 @@ public class UpdateChecker( return version.toAppVersion(versionedStrings) } - private fun getVersion(versions: List, packageInfo: PackageInfo?): Version? { + private fun getVersion( + versions: List, + packageInfo: PackageInfo?, + releaseChannels: List?, + ): Version? { val versionCode = packageInfo?.getVersionCode() ?: 0 // the below is rather expensive, so we only do that when there's update candidates // TODO handle signingInfo.signingCertificateHistory as well @@ -69,8 +87,15 @@ public class UpdateChecker( versions.iterator().forEach versions@{ version -> // if version code is not higher than installed skip package as list is sorted if (version.manifest.versionCode <= versionCode) return null - // not considering beta versions for now - if (!version.releaseChannels.isNullOrEmpty()) return@versions + // check release channels if they are not empty + if (!version.releaseChannels.isNullOrEmpty()) { + // if release channels are not empty (stable) don't consider this version + if (releaseChannels == null) return@versions + // don't consider version with non-matching release channel + if (releaseChannels.intersect(version.releaseChannels).isEmpty()) return@versions + } + // skip incompatible versions + if (!compatibilityChecker.isCompatible(version.manifest)) return@versions val canInstall = if (packageInfo == null) { true // take first one with highest version code and repo weight } else { diff --git a/database/src/main/java/org/fdroid/database/Version.kt b/database/src/main/java/org/fdroid/database/Version.kt index 4ad335bb6..6b53232cb 100644 --- a/database/src/main/java/org/fdroid/database/Version.kt +++ b/database/src/main/java/org/fdroid/database/Version.kt @@ -4,7 +4,6 @@ import androidx.core.os.LocaleListCompat import androidx.room.Embedded import androidx.room.Entity import androidx.room.ForeignKey -import org.fdroid.database.VersionedStringType.FEATURE import org.fdroid.database.VersionedStringType.PERMISSION import org.fdroid.database.VersionedStringType.PERMISSION_SDK_23 import org.fdroid.index.v2.FeatureV2 @@ -37,16 +36,21 @@ data class Version( val releaseChannels: List? = emptyList(), val antiFeatures: Map? = null, val whatsNew: LocalizedTextV2? = null, + val isCompatible: Boolean, ) { fun toAppVersion(versionedStrings: List) = AppVersion( version = this, usesPermission = versionedStrings.getPermissions(this), usesPermissionSdk23 = versionedStrings.getPermissionsSdk23(this), - features = versionedStrings.getFeatures(this), ) } -fun PackageVersionV2.toVersion(repoId: Long, packageId: String, versionId: String) = Version( +fun PackageVersionV2.toVersion( + repoId: Long, + packageId: String, + versionId: String, + isCompatible: Boolean, +) = Version( repoId = repoId, packageId = packageId, versionId = versionId, @@ -57,16 +61,16 @@ fun PackageVersionV2.toVersion(repoId: Long, packageId: String, versionId: Strin releaseChannels = releaseChannels, antiFeatures = antiFeatures, whatsNew = whatsNew, + isCompatible = isCompatible, ) data class AppVersion( val version: Version, val usesPermission: List? = null, val usesPermissionSdk23: List? = null, - val features: List? = null, ) { val packageId get() = version.packageId - val featureNames get() = features?.map { it.name }?.toTypedArray() ?: emptyArray() + val featureNames get() = version.manifest.features?.toTypedArray() ?: emptyArray() val nativeCode get() = version.manifest.nativecode?.toTypedArray() ?: emptyArray() val antiFeatureNames: Array get() { @@ -83,7 +87,18 @@ data class AppManifest( val maxSdkVersion: Int? = null, @Embedded(prefix = "signer_") val signer: SignatureV2? = null, val nativecode: List? = emptyList(), -) + val features: List? = emptyList(), +) { + internal fun toManifestV2(): ManifestV2 = ManifestV2( + versionName = versionName, + versionCode = versionCode, + usesSdk = usesSdk, + maxSdkVersion = maxSdkVersion, + signer = signer, + nativeCode = nativecode ?: emptyList(), + features = features?.map { FeatureV2(it) } ?: emptyList(), + ) +} fun ManifestV2.toManifest() = AppManifest( versionName = versionName, @@ -92,12 +107,12 @@ fun ManifestV2.toManifest() = AppManifest( maxSdkVersion = maxSdkVersion, signer = signer, nativecode = nativeCode, + features = features.map { it.name }, ) enum class VersionedStringType { PERMISSION, PERMISSION_SDK_23, - FEATURE, } @Entity( @@ -132,21 +147,9 @@ fun List.toVersionedString( ) } -fun List.toVersionedString(version: Version) = map { feature -> - VersionedString( - repoId = version.repoId, - packageId = version.packageId, - versionId = version.versionId, - type = FEATURE, - name = feature.name, - version = feature.version, - ) -} - fun ManifestV2.getVersionedStrings(version: Version): List { return usesPermission.toVersionedString(version, PERMISSION) + - usesPermissionSdk23.toVersionedString(version, PERMISSION_SDK_23) + - features.toVersionedString(version) + usesPermissionSdk23.toVersionedString(version, PERMISSION_SDK_23) } fun List.getPermissions(version: Version) = mapNotNull { v -> @@ -167,15 +170,6 @@ fun List.getPermissionsSdk23(version: Version) = mapNotNull { v } } -fun List.getFeatures(version: Version) = mapNotNull { v -> - v.map(version, FEATURE) { - FeatureV2( - name = v.name, - version = v.version, - ) - } -} - private fun VersionedString.map( v: Version, wantedType: VersionedStringType, diff --git a/database/src/main/java/org/fdroid/database/VersionDao.kt b/database/src/main/java/org/fdroid/database/VersionDao.kt index 203968a91..4709b7c10 100644 --- a/database/src/main/java/org/fdroid/database/VersionDao.kt +++ b/database/src/main/java/org/fdroid/database/VersionDao.kt @@ -15,8 +15,13 @@ import org.fdroid.database.FDroidDatabaseHolder.dispatcher import org.fdroid.index.v2.PackageVersionV2 public interface VersionDao { - fun insert(repoId: Long, packageId: String, packageVersions: Map) - fun insert(repoId: Long, packageId: String, versionId: String, packageVersion: PackageVersionV2) + fun insert( + repoId: Long, + packageId: String, + packageVersions: Map, + checkIfCompatible: (PackageVersionV2) -> Boolean, + ) + fun getAppVersions(packageId: String): LiveData> fun getAppVersions(repoId: Long, packageId: String): List } @@ -29,21 +34,24 @@ internal interface VersionDaoInt : VersionDao { repoId: Long, packageId: String, packageVersions: Map, + checkIfCompatible: (PackageVersionV2) -> Boolean, ) { // TODO maybe the number of queries here can be reduced - packageVersions.entries.forEach { (versionId, packageVersion) -> - insert(repoId, packageId, versionId, packageVersion) + packageVersions.entries.iterator().forEach { (versionId, packageVersion) -> + val isCompatible = checkIfCompatible(packageVersion) + insert(repoId, packageId, versionId, packageVersion, isCompatible) } } @Transaction - override fun insert( + fun insert( repoId: Long, packageId: String, versionId: String, packageVersion: PackageVersionV2, + isCompatible: Boolean, ) { - val version = packageVersion.toVersion(repoId, packageId, versionId) + val version = packageVersion.toVersion(repoId, packageId, versionId, isCompatible) insert(version) insert(packageVersion.manifest.getVersionedStrings(version)) } @@ -68,8 +76,8 @@ internal interface VersionDaoInt : VersionDao { @Transaction override fun getAppVersions(repoId: Long, packageId: String): List { val versionedStrings = getVersionedStrings(repoId, packageId) - return getVersions(repoId, packageId).map { - version -> version.toAppVersion(versionedStrings) + return getVersions(repoId, packageId).map { version -> + version.toAppVersion(versionedStrings) } } @@ -100,7 +108,11 @@ internal interface VersionDaoInt : VersionDao { fun getVersionedStrings(repoId: Long, packageId: String): List @Query("SELECT * FROM VersionedString WHERE repoId = :repoId AND packageId = :packageId AND versionId = :versionId") - fun getVersionedStrings(repoId: Long, packageId: String, versionId: String): List + fun getVersionedStrings( + repoId: Long, + packageId: String, + versionId: String, + ): List @VisibleForTesting @Query("DELETE FROM Version WHERE repoId = :repoId AND packageId = :packageId AND versionId = :versionId") diff --git a/database/src/main/java/org/fdroid/index/v1/IndexV1Updater.kt b/database/src/main/java/org/fdroid/index/v1/IndexV1Updater.kt index 89c333ef9..7369d0fc7 100644 --- a/database/src/main/java/org/fdroid/index/v1/IndexV1Updater.kt +++ b/database/src/main/java/org/fdroid/index/v1/IndexV1Updater.kt @@ -1,6 +1,7 @@ package org.fdroid.index.v1 import android.content.Context +import org.fdroid.CompatibilityChecker import org.fdroid.database.DbV1StreamReceiver import org.fdroid.database.FDroidDatabase import org.fdroid.database.FDroidDatabaseInt @@ -15,6 +16,7 @@ public class IndexV1Updater( private val context: Context, database: FDroidDatabase, private val downloaderFactory: DownloaderFactory, + private val compatibilityChecker: CompatibilityChecker, ) { private val db: FDroidDatabaseInt = database as FDroidDatabaseInt @@ -63,8 +65,8 @@ public class IndexV1Updater( db.runInTransaction { val cert = verifier.getStreamAndVerify { inputStream -> updateListener?.onStartProcessing() // TODO maybe do more fine-grained reporting - val streamProcessor = - IndexV1StreamProcessor(DbV1StreamReceiver(db), certificate) + val streamReceiver = DbV1StreamReceiver(db, compatibilityChecker) + val streamProcessor = IndexV1StreamProcessor(streamReceiver, certificate) streamProcessor.process(repoId, inputStream) } // update certificate, if we didn't have any before