[db] Implement interfaces from index library

so we can use UpdateChecker and CompatibilityChecker with our DB classes
This commit is contained in:
Torsten Grote
2022-06-02 13:58:39 -03:00
committed by Michael Pöhn
parent 55d78fab01
commit 890dc45718
6 changed files with 59 additions and 78 deletions

View File

@@ -10,15 +10,15 @@ import kotlin.time.ExperimentalTime
import kotlin.time.measureTime
@RunWith(AndroidJUnit4::class)
internal class UpdateCheckerTest : DbTest() {
internal class DbUpdateCheckerTest : DbTest() {
private lateinit var updateChecker: UpdateChecker
private lateinit var updateChecker: DbUpdateChecker
@Before
override fun createDb() {
super.createDb()
// TODO mock packageManager and maybe move to unit tests
updateChecker = UpdateChecker(db, context.packageManager)
updateChecker = DbUpdateChecker(db, context.packageManager)
}
@Test

View File

@@ -464,7 +464,7 @@ internal interface AppDaoInt : AppDao {
override fun getNumberOfAppsInCategory(category: String): Int
/**
* Used by [UpdateChecker] to get specific apps with available updates.
* Used by [DbUpdateChecker] to get specific apps with available updates.
*/
@Transaction
@SuppressWarnings(CURSOR_MISMATCH) // no anti-features needed here

View File

@@ -2,18 +2,19 @@ package org.fdroid.database
import androidx.room.Entity
import androidx.room.PrimaryKey
import org.fdroid.PackagePreference
@Entity
public data class AppPrefs(
@PrimaryKey
val packageId: String,
val ignoreVersionCodeUpdate: Long = 0,
override val ignoreVersionCodeUpdate: Long = 0,
// This is named like this, because it hit a Room bug when joining with Version table
// which had exactly the same field.
internal val appPrefReleaseChannels: List<String>? = null,
) {
) : PackagePreference {
public val ignoreAllUpdates: Boolean get() = ignoreVersionCodeUpdate == Long.MAX_VALUE
public val releaseChannels: List<String> get() = appPrefReleaseChannels ?: emptyList()
public override val releaseChannels: List<String> get() = appPrefReleaseChannels ?: emptyList()
public fun shouldIgnoreUpdate(versionCode: Long): Boolean =
ignoreVersionCodeUpdate >= versionCode

View File

@@ -6,9 +6,10 @@ 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
import org.fdroid.PackagePreference
import org.fdroid.UpdateChecker
public class UpdateChecker(
public class DbUpdateChecker(
db: FDroidDatabase,
private val packageManager: PackageManager,
) {
@@ -17,6 +18,7 @@ public class UpdateChecker(
private val versionDao = db.getVersionDao() as VersionDaoInt
private val appPrefsDao = db.getAppPrefsDao() as AppPrefsDaoInt
private val compatibilityChecker = CompatibilityCheckerImpl(packageManager)
private val updateChecker = UpdateChecker(compatibilityChecker)
/**
* Returns a list of apps that can be updated.
@@ -37,7 +39,7 @@ public class UpdateChecker(
installedPackages.iterator().forEach { packageInfo ->
val packageName = packageInfo.packageName
val versions = versionsByPackage[packageName] ?: return@forEach // continue
val version = getVersion(versions, packageName, packageInfo, releaseChannels)
val version = getVersion(versions, packageName, packageInfo, null, releaseChannels)
if (version != null) {
val versionCode = packageInfo.getVersionCode()
val app = getUpdatableApp(version, versionCode)
@@ -48,13 +50,17 @@ public class UpdateChecker(
}
/**
* Returns an [AppVersion] for the given [packageName] that is an update
* Returns an [AppVersion] for the given [packageName] that is an update or new install
* 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")
public fun getUpdate(packageName: String, releaseChannels: List<String>? = null): AppVersion? {
public fun getSuggestedVersion(
packageName: String,
preferredSigner: String? = null,
releaseChannels: List<String>? = null,
): AppVersion? {
val versions = versionDao.getVersions(listOf(packageName))
if (versions.isEmpty()) return null
val packageInfo = try {
@@ -63,7 +69,8 @@ public class UpdateChecker(
} catch (e: PackageManager.NameNotFoundException) {
null
}
val version = getVersion(versions, packageName, packageInfo, releaseChannels) ?: return null
val version = getVersion(versions, packageName, packageInfo, preferredSigner,
releaseChannels) ?: return null
val versionedStrings = versionDao.getVersionedStrings(
repoId = version.repoId,
packageId = version.packageId,
@@ -76,50 +83,27 @@ public class UpdateChecker(
versions: List<Version>,
packageName: String,
packageInfo: PackageInfo?,
preferredSigner: String?,
releaseChannels: List<String>?,
): 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
@Suppress("DEPRECATION")
val signatures by lazy {
packageInfo?.signatures?.map {
IndexUtils.getPackageSignature(it.toByteArray())
}?.toSet()
val preferencesGetter: (() -> PackagePreference?) = {
appPrefsDao.getAppPrefsOrNull(packageName)
}
val appPrefs by lazy { appPrefsDao.getAppPrefsOrNull(packageName) }
versions.iterator().forEach versions@{ version ->
// if the installed version has a known vulnerability, we return it as well
if (version.manifest.versionCode == versionCode && version.hasKnownVulnerability) {
return version
}
// if version code is not higher than installed skip package as list is sorted
if (version.manifest.versionCode <= versionCode) return null
// skip incompatible versions
if (!compatibilityChecker.isCompatible(version.manifest.toManifestV2())) return@versions
// only check release channels if they are not empty
if (!version.releaseChannels.isNullOrEmpty()) {
// add release channels from AppPrefs into the ones we allow
val channels = releaseChannels?.toMutableSet() ?: LinkedHashSet()
if (!appPrefs?.releaseChannels.isNullOrEmpty()) {
channels.addAll(appPrefs!!.releaseChannels)
}
// if allowed releases channels are empty (only stable) don't consider this version
if (channels.isEmpty()) return@versions
// don't consider version with non-matching release channel
if (channels.intersect(version.releaseChannels).isEmpty()) return@versions
}
val canInstall = if (packageInfo == null) {
true // take first one with highest version code and repo weight
} else {
// TODO also support AppPrefs with ignoring updates
val versionSignatures = version.manifest.signer?.sha256?.toSet()
signatures == versionSignatures
}
// no need to see other versions, we got the highest version code per sorting
if (canInstall) return version
return if (packageInfo == null) {
updateChecker.getSuggestedVersion(
versions = versions,
preferredSigner = preferredSigner,
releaseChannels = releaseChannels,
preferencesGetter = preferencesGetter,
)
} else {
updateChecker.getUpdate(
versions = versions,
packageInfo = packageInfo,
releaseChannels = releaseChannels,
preferencesGetter = preferencesGetter,
)
}
return null
}
private fun getUpdatableApp(version: Version, installedVersionCode: Long): UpdatableApp? {

View File

@@ -6,18 +6,18 @@ import androidx.room.Entity
import androidx.room.ForeignKey
import org.fdroid.database.VersionedStringType.PERMISSION
import org.fdroid.database.VersionedStringType.PERMISSION_SDK_23
import org.fdroid.index.v2.FeatureV2
import org.fdroid.index.v2.ANTI_FEATURE_KNOWN_VULNERABILITY
import org.fdroid.index.v2.FileV1
import org.fdroid.index.v2.FileV2
import org.fdroid.index.v2.LocalizedTextV2
import org.fdroid.index.v2.ManifestV2
import org.fdroid.index.v2.PackageManifest
import org.fdroid.index.v2.PackageVersion
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
private const val ANTI_FEATURE_KNOWN_VULNERABILITY = "KnownVuln"
@Entity(
primaryKeys = ["repoId", "packageId", "versionId"],
foreignKeys = [ForeignKey(
@@ -35,12 +35,15 @@ public data class Version(
@Embedded(prefix = "file_") val file: FileV1,
@Embedded(prefix = "src_") val src: FileV2? = null,
@Embedded(prefix = "manifest_") val manifest: AppManifest,
val releaseChannels: List<String>? = emptyList(),
override val releaseChannels: List<String>? = emptyList(),
val antiFeatures: Map<String, LocalizedTextV2>? = null,
val whatsNew: LocalizedTextV2? = null,
val isCompatible: Boolean,
) {
val hasKnownVulnerability: Boolean
) : PackageVersion {
override val versionCode: Long get() = manifest.versionCode
override val signer: SignerV2? get() = manifest.signer
override val packageManifest: PackageManifest get() = manifest
override val hasKnownVulnerability: Boolean
get() = antiFeatures?.contains(ANTI_FEATURE_KNOWN_VULNERABILITY) == true
internal fun toAppVersion(versionedStrings: List<VersionedString>): AppVersion = AppVersion(
@@ -97,20 +100,13 @@ public data class AppManifest(
val versionName: String,
val versionCode: Long,
@Embedded(prefix = "usesSdk_") val usesSdk: UsesSdkV2? = null,
val maxSdkVersion: Int? = null,
@Embedded(prefix = "signer_") val signer: SignatureV2? = null,
val nativecode: List<String>? = emptyList(),
override val maxSdkVersion: Int? = null,
@Embedded(prefix = "signer_") val signer: SignerV2? = null,
override val nativecode: List<String>? = emptyList(),
val features: List<String>? = 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(),
)
) : PackageManifest {
override val minSdkVersion: Int? get() = usesSdk?.minSdkVersion
override val featureNames: List<String>? get() = features
}
internal fun ManifestV2.toManifest() = AppManifest(

View File

@@ -19,7 +19,7 @@ import org.fdroid.database.FDroidDatabaseHolder.dispatcher
import org.fdroid.database.VersionedStringType.PERMISSION
import org.fdroid.database.VersionedStringType.PERMISSION_SDK_23
import org.fdroid.index.IndexParser.json
import org.fdroid.index.v2.ManifestV2
import org.fdroid.index.v2.PackageManifest
import org.fdroid.index.v2.PackageVersionV2
import org.fdroid.index.v2.PermissionV2
import org.fdroid.index.v2.ReflectionDiffer
@@ -88,7 +88,7 @@ internal interface VersionDaoInt : VersionDao {
repoId: Long,
packageId: String,
versionsDiffMap: Map<String, JsonObject?>?,
checkIfCompatible: (ManifestV2) -> Boolean,
checkIfCompatible: (PackageManifest) -> Boolean,
) {
if (versionsDiffMap == null) { // no more versions, delete all
deleteAppVersion(repoId, packageId)
@@ -100,7 +100,7 @@ internal interface VersionDaoInt : VersionDao {
if (version == null) { // new version, parse normally
val packageVersionV2: PackageVersionV2 =
json.decodeFromJsonElement(jsonObject)
val isCompatible = checkIfCompatible(packageVersionV2.manifest)
val isCompatible = checkIfCompatible(packageVersionV2.packageManifest)
insert(repoId, packageId, versionId, packageVersionV2, isCompatible)
} else { // diff against existing version
diffVersion(version, jsonObject, checkIfCompatible)
@@ -112,7 +112,7 @@ internal interface VersionDaoInt : VersionDao {
private fun diffVersion(
version: Version,
jsonObject: JsonObject,
checkIfCompatible: (ManifestV2) -> Boolean,
checkIfCompatible: (PackageManifest) -> Boolean,
) {
// ensure that diff does not include internal keys
DENY_LIST.forEach { forbiddenKey ->
@@ -123,7 +123,7 @@ internal interface VersionDaoInt : VersionDao {
}
// diff version
val diffedVersion = ReflectionDiffer.applyDiff(version, jsonObject)
val isCompatible = checkIfCompatible(diffedVersion.manifest.toManifestV2())
val isCompatible = checkIfCompatible(diffedVersion.packageManifest)
update(diffedVersion.copy(isCompatible = isCompatible))
// diff versioned strings
val manifest = jsonObject["manifest"]