diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8ac39438e..9702bd75f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -152,7 +152,7 @@ libs lint ktlintCheck:
script:
# always report on lint errors to the build log
- sed -i -e 's,textReport .*,textReport true,' app/build.gradle
- - ./gradlew :libs:database:lint :libs:download:lint :libs:index:lint ktlintCheck
+ - ./gradlew :libs:database:lint :libs:download:lint :libs:index:lint ktlintCheck checkLegacyAbi
# Reference: https://gitlab.com/components/code-quality-oss/codequality-os-scanners-integration/-/blob/4121970daed111dda84cab4547e1f2951684653c/templates/pmd.yml#L52-92
app lint pmd:
@@ -347,9 +347,10 @@ pages:
only:
- master
script:
- - ./gradlew :libs:download:dokkaHtml :libs:index:dokkaHtml :libs:database:dokkaHtml
+ - ./gradlew :libs:core:dokkaGenerateHtml :libs:download:dokkaGenerateHtml :libs:index:dokkaGenerateHtml :libs:database:dokkaGenerateHtml
- mkdir -p public/libs
- touch public/index.html public/libs/index.html
+ - cp -r libs/core/build/dokka/html public/libs/core
- cp -r libs/download/build/dokka/html public/libs/download
- cp -r libs/index/build/dokka/html public/libs/index
- cp -r libs/database/build/dokka/html public/libs/database
diff --git a/build.gradle b/build.gradle
index 3b2e04502..8a9f49847 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,6 +6,7 @@ buildscript {
}
plugins {
alias libs.plugins.android.application apply false
+ alias libs.plugins.android.library apply false
alias libs.plugins.android.ksp apply false
alias libs.plugins.jetbrains.kotlin.android apply false
alias libs.plugins.jetbrains.kotlin.multiplatform apply false
@@ -20,12 +21,6 @@ allprojects {
mavenCentral()
maven { url 'https://maven.google.com/' }
}
- // use new Sonatype server for mavenCentral publishing
- plugins.withId("com.vanniktech.maven.publish") {
- mavenPublish {
- sonatypeHost = "S01"
- }
- }
}
subprojects {
apply plugin: "org.jlleitschuh.gradle.ktlint"
diff --git a/gradle.properties b/gradle.properties
index ce02a7cda..e72a4767e 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -8,6 +8,9 @@ kotlin.mpp.androidSourceSetLayoutVersion=2
# Gradle Maven Publish Info below (https://github.com/vanniktech/gradle-maven-publish-plugin)
# These are common for all libraries in gradle submodules.
# Specifics for each library are defined in the submodule's gradle.properties file.
+mavenCentralPublishing=true
+mavenCentralAutomaticPublishing=true
+signAllPublications=true
GROUP=org.fdroid
@@ -22,5 +25,3 @@ POM_SCM_DEV_CONNECTION=scm:git:ssh://git@gitlab.com:fdroid/fdroidclient.git
POM_DEVELOPER_ID=grote
POM_DEVELOPER_NAME=Torsten Grote
POM_DEVELOPER_URL=https://github.com/grote/
-
-SONATYPE_AUTOMATIC_RELEASE=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6625ab512..5d2926e2f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,18 +1,18 @@
[versions]
compileSdk = "36"
-kotlin = "2.2.0"
+kotlin = "2.2.21"
androidGradlePlugin = "8.11.1" # 8.12.0 pulls in aapt2 which has issue on buildserver
-androidKspPlugin = "2.2.0-2.0.2" # first version needs to match kotlin version
-dokka = "2.0.0"
-mavenPublish = "0.18.0"
+androidKspPlugin = "2.2.21-2.0.4" # first version needs to match kotlin version
+dokka = "2.1.0"
+mavenPublish = "0.34.0"
jlleitschuhKtlint = "13.1.0"
-kotlinxSerializationJson = "1.9.0" # 1.4.1 because https://github.com/Kotlin/kotlinx.serialization/issues/2231
+kotlinxSerializationJson = "1.9.0"
kotlinxCoroutinesTest = "1.10.2"
-ktor = "3.3.0"
+ktor = "3.3.1"
okhttp = "4.12.0"
-room = "2.8.1"
+room = "2.8.3"
glide = "5.0.5"
glideCompose = "1.0.0-beta08"
@@ -130,11 +130,9 @@ microutils-kotlin-logging = { module = "io.github.microutils:kotlin-logging", ve
commons-io = { module = "commons-io:commons-io", version.ref = "commonsIo" }
commons-net = { module = "commons-net:commons-net", version.ref = "commonsNet" }
-ktor-io = { module = "io.ktor:ktor-io", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" }
-ktor-client-curl = { module = "io.ktor:ktor-client-curl", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
goncalossilva-resources = { module = "com.goncalossilva:resources", version.ref = "goncalossilvaResources" }
@@ -167,6 +165,7 @@ json = { module = "org.json:json", version.ref = "json" }
[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
+android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
android-ksp = { id = "com.google.devtools.ksp", version.ref = "androidKspPlugin" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
jetbrains-kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 255cf9d46..018a8eff0 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -3956,6 +3956,11 @@
+
+
+
+
+
@@ -3971,6 +3976,11 @@
+
+
+
+
+
@@ -4011,6 +4021,11 @@
+
+
+
+
+
@@ -4051,6 +4066,11 @@
+
+
+
+
+
@@ -4066,6 +4086,11 @@
+
+
+
+
+
@@ -4106,6 +4131,11 @@
+
+
+
+
+
@@ -4148,6 +4178,11 @@
+
+
+
+
+
@@ -4163,6 +4198,11 @@
+
+
+
+
+
@@ -4220,6 +4260,11 @@
+
+
+
+
+
@@ -4235,6 +4280,11 @@
+
+
+
+
+
@@ -4260,6 +4310,11 @@
+
+
+
+
+
@@ -4275,6 +4330,11 @@
+
+
+
+
+
@@ -8372,6 +8432,11 @@
+
+
+
+
+
@@ -8395,6 +8460,11 @@
+
+
+
+
+
@@ -8423,6 +8493,11 @@
+
+
+
+
+
@@ -8438,6 +8513,11 @@
+
+
+
+
+
@@ -8468,6 +8548,11 @@
+
+
+
+
+
@@ -8478,6 +8563,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -9105,6 +9200,11 @@
+
+
+
+
+
@@ -9115,6 +9215,11 @@
+
+
+
+
+
@@ -9157,6 +9262,11 @@
+
+
+
+
+
@@ -9172,6 +9282,11 @@
+
+
+
+
+
@@ -9187,6 +9302,11 @@
+
+
+
+
+
@@ -9202,6 +9322,11 @@
+
+
+
+
+
@@ -10167,6 +10292,11 @@
+
+
+
+
+
@@ -10308,6 +10438,11 @@
+
+
+
+
+
@@ -10353,6 +10488,11 @@
+
+
+
+
+
@@ -10378,11 +10518,21 @@
+
+
+
+
+
+
+
+
+
+
@@ -10460,12 +10610,22 @@
+
+
+
+
+
+
+
+
+
+
@@ -10627,6 +10787,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -10987,6 +11157,11 @@
+
+
+
+
+
@@ -10997,6 +11172,11 @@
+
+
+
+
+
@@ -11080,6 +11260,11 @@
+
+
+
+
+
@@ -11146,6 +11331,11 @@
+
+
+
+
+
@@ -11306,6 +11496,11 @@
+
+
+
+
+
@@ -11362,6 +11557,11 @@
+
+
+
+
+
@@ -11444,6 +11644,11 @@
+
+
+
+
+
@@ -11501,6 +11706,11 @@
+
+
+
+
+
@@ -11568,6 +11778,11 @@
+
+
+
+
+
@@ -11624,6 +11839,11 @@
+
+
+
+
+
@@ -11712,6 +11932,11 @@
+
+
+
+
+
@@ -11784,6 +12009,11 @@
+
+
+
+
+
@@ -11814,6 +12044,11 @@
+
+
+
+
+
@@ -11901,6 +12136,11 @@
+
+
+
+
+
@@ -12004,6 +12244,11 @@
+
+
+
+
+
@@ -12075,6 +12320,11 @@
+
+
+
+
+
@@ -12168,6 +12418,11 @@
+
+
+
+
+
@@ -12198,6 +12453,11 @@
+
+
+
+
+
@@ -12264,6 +12524,11 @@
+
+
+
+
+
@@ -12274,6 +12539,11 @@
+
+
+
+
+
@@ -12341,6 +12611,11 @@
+
+
+
+
+
@@ -12397,6 +12672,11 @@
+
+
+
+
+
@@ -12417,6 +12697,11 @@
+
+
+
+
+
@@ -12432,6 +12717,11 @@
+
+
+
+
+
@@ -12515,6 +12805,11 @@
+
+
+
+
+
@@ -12581,6 +12876,11 @@
+
+
+
+
+
@@ -12669,6 +12969,11 @@
+
+
+
+
+
@@ -12725,6 +13030,11 @@
+
+
+
+
+
@@ -12797,6 +13107,11 @@
+
+
+
+
+
@@ -12853,6 +13168,11 @@
+
+
+
+
+
@@ -13513,6 +13833,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -14378,6 +14708,11 @@
+
+
+
+
+
@@ -14429,6 +14764,11 @@
+
+
+
+
+
@@ -14530,6 +14870,11 @@
+
+
+
+
+
@@ -15093,6 +15438,11 @@
+
+
+
+
+
@@ -15108,6 +15458,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -15128,6 +15488,11 @@
+
+
+
+
+
@@ -15163,6 +15528,11 @@
+
+
+
+
+
@@ -15204,6 +15574,11 @@
+
+
+
+
+
@@ -15239,6 +15614,11 @@
+
+
+
+
+
@@ -15274,6 +15654,11 @@
+
+
+
+
+
@@ -15344,6 +15729,11 @@
+
+
+
+
+
@@ -15364,6 +15754,11 @@
+
+
+
+
+
@@ -15384,6 +15779,11 @@
+
+
+
+
+
@@ -15419,6 +15819,11 @@
+
+
+
+
+
@@ -15435,11 +15840,21 @@
+
+
+
+
+
+
+
+
+
+
@@ -15455,6 +15870,11 @@
+
+
+
+
+
@@ -15465,6 +15885,11 @@
+
+
+
+
+
@@ -15605,6 +16030,11 @@
+
+
+
+
+
@@ -15625,6 +16055,11 @@
+
+
+
+
+
@@ -15645,6 +16080,11 @@
+
+
+
+
+
@@ -15710,6 +16150,11 @@
+
+
+
+
+
@@ -15765,6 +16210,11 @@
+
+
+
+
+
@@ -15780,6 +16230,11 @@
+
+
+
+
+
@@ -15835,6 +16290,11 @@
+
+
+
+
+
@@ -15900,6 +16360,11 @@
+
+
+
+
+
@@ -15967,6 +16432,11 @@
+
+
+
+
+
@@ -15992,6 +16462,11 @@
+
+
+
+
+
@@ -16097,6 +16572,14 @@
+
+
+
+
+
+
+
+
@@ -16137,6 +16620,11 @@
+
+
+
+
+
@@ -16172,6 +16660,11 @@
+
+
+
+
+
@@ -16227,6 +16720,11 @@
+
+
+
+
+
@@ -16282,6 +16780,11 @@
+
+
+
+
+
@@ -16337,6 +16840,11 @@
+
+
+
+
+
@@ -16347,6 +16855,11 @@
+
+
+
+
+
@@ -16412,6 +16925,11 @@
+
+
+
+
+
@@ -16577,6 +17095,11 @@
+
+
+
+
+
@@ -16597,6 +17120,11 @@
+
+
+
+
+
@@ -16662,6 +17190,11 @@
+
+
+
+
+
@@ -16717,6 +17250,11 @@
+
+
+
+
+
@@ -16772,6 +17310,11 @@
+
+
+
+
+
@@ -16827,6 +17370,11 @@
+
+
+
+
+
@@ -16882,6 +17430,11 @@
+
+
+
+
+
@@ -16943,6 +17496,11 @@
+
+
+
+
+
@@ -16963,6 +17521,11 @@
+
+
+
+
+
@@ -17157,6 +17720,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -17556,6 +18132,14 @@
+
+
+
+
+
+
+
+
@@ -17691,6 +18275,11 @@
+
+
+
+
+
@@ -17736,6 +18325,11 @@
+
+
+
+
+
@@ -17816,6 +18410,11 @@
+
+
+
+
+
@@ -17871,6 +18470,11 @@
+
+
+
+
+
@@ -17881,6 +18485,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -18101,6 +18715,11 @@
+
+
+
+
+
@@ -19896,6 +20515,11 @@
+
+
+
+
+
diff --git a/libs/core/.gitignore b/libs/core/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/libs/core/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/libs/core/api/android/core.api b/libs/core/api/android/core.api
new file mode 100644
index 000000000..4af8b61f5
--- /dev/null
+++ b/libs/core/api/android/core.api
@@ -0,0 +1,8 @@
+public abstract interface class org/fdroid/IndexFile {
+ public abstract fun getIpfsCidV1 ()Ljava/lang/String;
+ public abstract fun getName ()Ljava/lang/String;
+ public abstract fun getSha256 ()Ljava/lang/String;
+ public abstract fun getSize ()Ljava/lang/Long;
+ public abstract fun serialize ()Ljava/lang/String;
+}
+
diff --git a/libs/core/build.gradle.kts b/libs/core/build.gradle.kts
new file mode 100644
index 000000000..05b9624ef
--- /dev/null
+++ b/libs/core/build.gradle.kts
@@ -0,0 +1,79 @@
+plugins {
+ alias(libs.plugins.jetbrains.kotlin.multiplatform)
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.jetbrains.dokka)
+ alias(libs.plugins.vanniktech.maven.publish)
+}
+kotlin {
+ androidTarget {
+ compilerOptions {
+ jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
+ }
+ publishLibraryVariants("release")
+ }
+ explicitApi()
+ @OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class)
+ abiValidation {
+ enabled = true
+ }
+ compilerOptions {
+ optIn.add("kotlin.RequiresOptIn")
+ }
+ sourceSets {
+ commonMain {
+ dependencies {
+ }
+ }
+ commonTest {
+ dependencies {
+ implementation(kotlin("test"))
+ }
+ }
+ }
+}
+
+android {
+ namespace = "org.fdroid.core"
+ @Suppress("ktlint:standard:chain-method-continuation")
+ compileSdk = libs.versions.compileSdk.get().toInt()
+ defaultConfig {
+ minSdk = 21
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunnerArguments["disableAnalytics"] = "true"
+ }
+ buildTypes {
+ getByName("release") {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro",
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+}
+
+signing {
+ useGpgCmd()
+}
+
+mavenPublishing {
+ @Suppress("ktlint:standard:chain-method-continuation")
+ configure(
+ com.vanniktech.maven.publish.KotlinMultiplatform(
+ javadocJar = com.vanniktech.maven.publish.JavadocJar.Dokka("dokkaHtml"),
+ sourcesJar = true,
+ androidVariantsToPublish = listOf("release"),
+ ),
+ )
+}
+
+dokka {
+ pluginsConfiguration.html {
+ customAssets.from("${file("${rootProject.rootDir}/logo-icon.svg")}")
+ footerMessage.set("© 2010-2025 F-Droid Limited and Contributors")
+ }
+}
diff --git a/libs/core/gradle.properties b/libs/core/gradle.properties
new file mode 100644
index 000000000..5f2ad1d2e
--- /dev/null
+++ b/libs/core/gradle.properties
@@ -0,0 +1,7 @@
+POM_ARTIFACT_ID=core
+VERSION_NAME=0.0.1
+
+POM_NAME=F-Droid core library
+POM_DESCRIPTION=A Kotlin multi-platform library to provide core classes.
+POM_INCEPTION_YEAR=2025
+POM_URL=https://gitlab.com/fdroid/fdroidclient/-/tree/master/libs/core
diff --git a/libs/download/src/commonMain/kotlin/org/fdroid/IndexFile.kt b/libs/core/src/commonMain/kotlin/org/fdroid/IndexFile.kt
similarity index 100%
rename from libs/download/src/commonMain/kotlin/org/fdroid/IndexFile.kt
rename to libs/core/src/commonMain/kotlin/org/fdroid/IndexFile.kt
diff --git a/libs/database/api/database.api b/libs/database/api/database.api
new file mode 100644
index 000000000..e69de29bb
diff --git a/libs/database/build.gradle b/libs/database/build.gradle
deleted file mode 100644
index d5ea66b7a..000000000
--- a/libs/database/build.gradle
+++ /dev/null
@@ -1,136 +0,0 @@
-plugins {
- id 'kotlin-android'
- id 'com.android.library'
- id 'com.google.devtools.ksp'
- id 'org.jetbrains.dokka'
- id 'com.vanniktech.maven.publish'
-}
-
-android {
- namespace "org.fdroid.database"
- compileSdk libs.versions.compileSdk.get().toInteger()
-
- defaultConfig {
- minSdkVersion 23
- targetSdk 34 // relevant for instrumentation tests (targetSdk 21 fails on Android 14)
-
- consumerProguardFiles "consumer-rules.pro"
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- testInstrumentationRunnerArguments disableAnalytics: 'true'
- }
-
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- }
- }
- sourceSets {
- androidTest {
- java.srcDirs += "src/dbTest/java"
- // Adds exported schema location as test app assets.
- assets.srcDirs += files("$projectDir/schemas".toString())
- }
- test {
- java.srcDirs += "src/dbTest/java"
- // Adds exported schema location as test app assets.
- assets.srcDirs += files("$projectDir/schemas".toString())
- }
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_17
- targetCompatibility JavaVersion.VERSION_17
- }
- testOptions {
- unitTests {
- includeAndroidResources = true
- }
- }
- kotlinOptions {
- jvmTarget = '17'
- freeCompilerArgs += "-Xexplicit-api=strict"
- freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
- }
- aaptOptions {
- // needed only for instrumentation tests: assets.openFd()
- noCompress "json"
- }
- packagingOptions {
- exclude 'META-INF/AL2.0'
- exclude 'META-INF/LGPL2.1'
- exclude 'META-INF/LICENSE.md'
- exclude 'META-INF/LICENSE-notice.md'
- }
-}
-
-dependencies {
- implementation project(":libs:download")
- implementation project(":libs:index")
-
- implementation libs.androidx.core.ktx
- implementation libs.androidx.lifecycle.livedata.ktx
-
- implementation libs.androidx.room.runtime
- implementation libs.androidx.room.ktx
- ksp libs.androidx.room.compiler
-
- implementation libs.microutils.kotlin.logging
- implementation libs.kotlinx.serialization.json
-
- testImplementation project(":libs:sharedTest")
- testImplementation libs.junit
- testImplementation libs.mockk
- testImplementation libs.kotlin.test
- testImplementation libs.androidx.test.core.ktx
- testImplementation libs.androidx.test.ext.junit
- testImplementation libs.androidx.core.testing
- testImplementation libs.androidx.room.testing
- testImplementation libs.robolectric
- testImplementation libs.commons.io
- testImplementation libs.logback.classic
- testImplementation libs.kotlinx.coroutines.test
- testImplementation libs.turbine
- testImplementation libs.okhttp
-
- androidTestImplementation project(":libs:sharedTest")
- androidTestImplementation libs.mockk.android
- androidTestImplementation libs.kotlin.test
- androidTestImplementation libs.androidx.test.ext.junit
- androidTestImplementation libs.androidx.core.testing
- androidTestImplementation libs.androidx.espresso.core
- androidTestImplementation libs.androidx.room.testing
- androidTestImplementation libs.commons.io
-}
-
-ksp {
- arg(new RoomSchemaArgProvider(new File(projectDir, "schemas")))
-}
-
-signing {
- useGpgCmd()
-}
-
-import org.jetbrains.dokka.gradle.DokkaTask
-tasks.withType(DokkaTask).configureEach {
- pluginsMapConfiguration.set(
- ["org.jetbrains.dokka.base.DokkaBase": """{
- "customAssets": ["${file("${rootProject.rootDir}/logo-icon.svg")}"],
- "footerMessage": "© 2010-2022 F-Droid Limited and Contributors"
- }"""]
- )
-}
-
-class RoomSchemaArgProvider implements CommandLineArgumentProvider {
- @InputDirectory
- @PathSensitive(PathSensitivity.RELATIVE)
- File schemaDir
-
- RoomSchemaArgProvider(File schemaDir) {
- this.schemaDir = schemaDir
- }
-
- @Override
- Iterable asArguments() {
- return ["room.schemaLocation=${schemaDir.path}".toString()]
- }
-}
diff --git a/libs/database/build.gradle.kts b/libs/database/build.gradle.kts
new file mode 100644
index 000000000..db3f0e6e8
--- /dev/null
+++ b/libs/database/build.gradle.kts
@@ -0,0 +1,139 @@
+plugins {
+ alias(libs.plugins.jetbrains.kotlin.android)
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.android.ksp)
+ alias(libs.plugins.jetbrains.dokka)
+ alias(libs.plugins.vanniktech.maven.publish)
+}
+
+android {
+ namespace = "org.fdroid.database"
+ @Suppress("ktlint:standard:chain-method-continuation")
+ compileSdk = libs.versions.compileSdk.get().toInt()
+
+ defaultConfig {
+ minSdk = 23
+ consumerProguardFiles("consumer-rules.pro")
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunnerArguments["disableAnalytics"] = "true"
+ }
+
+ buildTypes {
+ getByName("release") {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro",
+ )
+ }
+ }
+ sourceSets {
+ getByName("androidTest") {
+ java.srcDirs("src/dbTest/java")
+ // Adds exported schema location as test app assets.
+ assets.srcDirs(files("$projectDir/schemas"))
+ }
+ getByName("test") {
+ java.srcDirs("src/dbTest/java")
+ // Adds exported schema location as test app assets.
+ assets.srcDirs(files("$projectDir/schemas"))
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ testOptions {
+ targetSdk = 34 // relevant for instrumentation tests (targetSdk 21 fails on Android 14)
+ unitTests {
+ isIncludeAndroidResources = true
+ }
+ }
+ androidResources {
+ // needed only for instrumentation tests: assets.openFd()
+ noCompress += "json"
+ }
+ packaging {
+ resources {
+ excludes.add("META-INF/AL2.0")
+ excludes.add("META-INF/LGPL2.1")
+ excludes.add("META-INF/LICENSE.md")
+ excludes.add("META-INF/LICENSE-notice.md")
+ }
+ }
+}
+
+kotlin {
+ explicitApi()
+ @OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class)
+ abiValidation {
+ enabled = true
+ }
+ compilerOptions {
+ jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
+ optIn.add("kotlin.RequiresOptIn")
+ }
+}
+
+dependencies {
+ implementation(project(":libs:core"))
+ implementation(project(":libs:index"))
+ implementation(project(":libs:download")) // needed for updater code
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.livedata.ktx)
+
+ implementation(libs.androidx.room.runtime)
+ implementation(libs.androidx.room.ktx)
+ ksp(libs.androidx.room.compiler)
+
+ implementation(libs.microutils.kotlin.logging)
+ implementation(libs.kotlinx.serialization.json)
+
+ testImplementation(project(":libs:sharedTest"))
+ testImplementation(libs.junit)
+ testImplementation(libs.mockk)
+ testImplementation(libs.kotlin.test)
+ testImplementation(libs.androidx.test.core.ktx)
+ testImplementation(libs.androidx.test.ext.junit)
+ testImplementation(libs.androidx.core.testing)
+ testImplementation(libs.androidx.room.testing)
+ testImplementation(libs.robolectric)
+ testImplementation(libs.commons.io)
+ testImplementation(libs.logback.classic)
+ testImplementation(libs.kotlinx.coroutines.test)
+ testImplementation(libs.turbine)
+ testImplementation(libs.okhttp)
+
+ androidTestImplementation(project(":libs:sharedTest"))
+ androidTestImplementation(libs.mockk.android)
+ androidTestImplementation(libs.kotlin.test)
+ androidTestImplementation(libs.androidx.test.ext.junit)
+ androidTestImplementation(libs.androidx.core.testing)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(libs.androidx.room.testing)
+ androidTestImplementation(libs.commons.io)
+}
+
+ksp {
+ arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
+}
+
+signing {
+ useGpgCmd()
+}
+
+dokka {
+ pluginsConfiguration.html {
+ customAssets.from("${file("${rootProject.rootDir}/logo-icon.svg")}")
+ footerMessage.set("© 2010-2025 F-Droid Limited and Contributors")
+ }
+}
+
+class RoomSchemaArgProvider(
+ @get:InputDirectory
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ val schemaDir: File,
+) : CommandLineArgumentProvider {
+ override fun asArguments(): Iterable = listOf("room.schemaLocation=${schemaDir.path}")
+}
diff --git a/libs/database/gradle.properties b/libs/database/gradle.properties
index 1863bb57e..bec8bc1a4 100644
--- a/libs/database/gradle.properties
+++ b/libs/database/gradle.properties
@@ -1,5 +1,5 @@
POM_ARTIFACT_ID=database
-VERSION_NAME=0.1.0
+VERSION_NAME=0.2.0
POM_NAME=F-Droid database library
POM_DESCRIPTION=An Android library to store F-Droid related info in Room such as repositories, apps and their versions.
diff --git a/libs/database/schemas/org.fdroid.database.FDroidDatabaseInt/9.json b/libs/database/schemas/org.fdroid.database.FDroidDatabaseInt/9.json
new file mode 100644
index 000000000..77de33c3a
--- /dev/null
+++ b/libs/database/schemas/org.fdroid.database.FDroidDatabaseInt/9.json
@@ -0,0 +1,1033 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 9,
+ "identityHash": "33bf34813c9be51d73151e7f48001ae5",
+ "entities": [
+ {
+ "tableName": "CoreRepository",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon` TEXT, `address` TEXT NOT NULL, `webBaseUrl` TEXT, `timestamp` INTEGER NOT NULL, `version` INTEGER, `formatVersion` TEXT, `maxAge` INTEGER, `description` TEXT NOT NULL, `certificate` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "icon",
+ "columnName": "icon",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "address",
+ "columnName": "address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "webBaseUrl",
+ "columnName": "webBaseUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "formatVersion",
+ "columnName": "formatVersion",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "maxAge",
+ "columnName": "maxAge",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "certificate",
+ "columnName": "certificate",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "repoId"
+ ]
+ }
+ },
+ {
+ "tableName": "Mirror",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `url` TEXT NOT NULL, `countryCode` TEXT, PRIMARY KEY(`repoId`, `url`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "countryCode",
+ "columnName": "countryCode",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId",
+ "url"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "CoreRepository",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "repoId"
+ ],
+ "referencedColumns": [
+ "repoId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "AntiFeature",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `id` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, PRIMARY KEY(`repoId`, `id`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "icon",
+ "columnName": "icon",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId",
+ "id"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "CoreRepository",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "repoId"
+ ],
+ "referencedColumns": [
+ "repoId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "Category",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `id` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, PRIMARY KEY(`repoId`, `id`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "icon",
+ "columnName": "icon",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId",
+ "id"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "CoreRepository",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "repoId"
+ ],
+ "referencedColumns": [
+ "repoId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "ReleaseChannel",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `id` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, PRIMARY KEY(`repoId`, `id`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "icon",
+ "columnName": "icon",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId",
+ "id"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "CoreRepository",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "repoId"
+ ],
+ "referencedColumns": [
+ "repoId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "RepositoryPreferences",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `lastUpdated` INTEGER, `lastETag` TEXT, `userMirrors` TEXT, `disabledMirrors` TEXT, `username` TEXT, `password` TEXT, PRIMARY KEY(`repoId`))",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "weight",
+ "columnName": "weight",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enabled",
+ "columnName": "enabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastUpdated",
+ "columnName": "lastUpdated",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "lastETag",
+ "columnName": "lastETag",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "userMirrors",
+ "columnName": "userMirrors",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "disabledMirrors",
+ "columnName": "disabledMirrors",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "username",
+ "columnName": "username",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "password",
+ "columnName": "password",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId"
+ ]
+ }
+ },
+ {
+ "tableName": "AppMetadata",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `added` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, `name` TEXT, `summary` TEXT, `description` TEXT, `localizedName` TEXT, `localizedSummary` TEXT, `webSite` TEXT, `changelog` TEXT, `license` TEXT, `sourceCode` TEXT, `issueTracker` TEXT, `translation` TEXT, `preferredSigner` TEXT, `video` TEXT, `authorName` TEXT, `authorEmail` TEXT, `authorWebSite` TEXT, `authorPhone` TEXT, `donate` TEXT, `liberapayID` TEXT, `liberapay` TEXT, `openCollective` TEXT, `bitcoin` TEXT, `litecoin` TEXT, `flattrID` TEXT, `categories` TEXT, `isCompatible` INTEGER NOT NULL, PRIMARY KEY(`repoId`, `packageName`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packageName",
+ "columnName": "packageName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "added",
+ "columnName": "added",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastUpdated",
+ "columnName": "lastUpdated",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "summary",
+ "columnName": "summary",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "localizedName",
+ "columnName": "localizedName",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "localizedSummary",
+ "columnName": "localizedSummary",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "webSite",
+ "columnName": "webSite",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "changelog",
+ "columnName": "changelog",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "license",
+ "columnName": "license",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "sourceCode",
+ "columnName": "sourceCode",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "issueTracker",
+ "columnName": "issueTracker",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "translation",
+ "columnName": "translation",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "preferredSigner",
+ "columnName": "preferredSigner",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "video",
+ "columnName": "video",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "authorName",
+ "columnName": "authorName",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "authorEmail",
+ "columnName": "authorEmail",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "authorWebSite",
+ "columnName": "authorWebSite",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "authorPhone",
+ "columnName": "authorPhone",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "donate",
+ "columnName": "donate",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "liberapayID",
+ "columnName": "liberapayID",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "liberapay",
+ "columnName": "liberapay",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "openCollective",
+ "columnName": "openCollective",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "bitcoin",
+ "columnName": "bitcoin",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "litecoin",
+ "columnName": "litecoin",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "flattrID",
+ "columnName": "flattrID",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "categories",
+ "columnName": "categories",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "isCompatible",
+ "columnName": "isCompatible",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId",
+ "packageName"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "CoreRepository",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "repoId"
+ ],
+ "referencedColumns": [
+ "repoId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "AppMetadataFts",
+ "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`repoId` INTEGER NOT NULL, `name` TEXT, `summary` TEXT, `description` TEXT, `authorName` TEXT, `packageName` TEXT NOT NULL, tokenize=unicode61 `remove_diacritics=1` `separators=.` `tokenchars=-`, content=`AppMetadata`, notindexed=`repoId`)",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "summary",
+ "columnName": "summary",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "authorName",
+ "columnName": "authorName",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "packageName",
+ "columnName": "packageName",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": []
+ },
+ "ftsVersion": "FTS4",
+ "ftsOptions": {
+ "tokenizer": "unicode61",
+ "tokenizerArgs": [
+ "remove_diacritics=1",
+ "separators=.",
+ "tokenchars=-"
+ ],
+ "contentTable": "AppMetadata",
+ "languageIdColumnName": "",
+ "matchInfo": "FTS4",
+ "notIndexedColumns": [
+ "repoId"
+ ],
+ "prefixSizes": [],
+ "preferredOrder": "ASC"
+ },
+ "contentSyncTriggers": [
+ "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_BEFORE_UPDATE BEFORE UPDATE ON `AppMetadata` BEGIN DELETE FROM `AppMetadataFts` WHERE `docid`=OLD.`rowid`; END",
+ "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_BEFORE_DELETE BEFORE DELETE ON `AppMetadata` BEGIN DELETE FROM `AppMetadataFts` WHERE `docid`=OLD.`rowid`; END",
+ "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_AFTER_UPDATE AFTER UPDATE ON `AppMetadata` BEGIN INSERT INTO `AppMetadataFts`(`docid`, `repoId`, `name`, `summary`, `description`, `authorName`, `packageName`) VALUES (NEW.`rowid`, NEW.`repoId`, NEW.`name`, NEW.`summary`, NEW.`description`, NEW.`authorName`, NEW.`packageName`); END",
+ "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_AFTER_INSERT AFTER INSERT ON `AppMetadata` BEGIN INSERT INTO `AppMetadataFts`(`docid`, `repoId`, `name`, `summary`, `description`, `authorName`, `packageName`) VALUES (NEW.`rowid`, NEW.`repoId`, NEW.`name`, NEW.`summary`, NEW.`description`, NEW.`authorName`, NEW.`packageName`); END"
+ ]
+ },
+ {
+ "tableName": "LocalizedFile",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `type` TEXT NOT NULL, `locale` TEXT NOT NULL, `name` TEXT NOT NULL, `sha256` TEXT, `size` INTEGER, `ipfsCidV1` TEXT, PRIMARY KEY(`repoId`, `packageName`, `type`, `locale`), FOREIGN KEY(`repoId`, `packageName`) REFERENCES `AppMetadata`(`repoId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packageName",
+ "columnName": "packageName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "locale",
+ "columnName": "locale",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sha256",
+ "columnName": "sha256",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "ipfsCidV1",
+ "columnName": "ipfsCidV1",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId",
+ "packageName",
+ "type",
+ "locale"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "AppMetadata",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "repoId",
+ "packageName"
+ ],
+ "referencedColumns": [
+ "repoId",
+ "packageName"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LocalizedFileList",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `type` TEXT NOT NULL, `locale` TEXT NOT NULL, `name` TEXT NOT NULL, `sha256` TEXT, `size` INTEGER, `ipfsCidV1` TEXT, PRIMARY KEY(`repoId`, `packageName`, `type`, `locale`, `name`), FOREIGN KEY(`repoId`, `packageName`) REFERENCES `AppMetadata`(`repoId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packageName",
+ "columnName": "packageName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "locale",
+ "columnName": "locale",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sha256",
+ "columnName": "sha256",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "ipfsCidV1",
+ "columnName": "ipfsCidV1",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId",
+ "packageName",
+ "type",
+ "locale",
+ "name"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "AppMetadata",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "repoId",
+ "packageName"
+ ],
+ "referencedColumns": [
+ "repoId",
+ "packageName"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "Version",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `versionId` TEXT NOT NULL, `added` INTEGER NOT NULL, `releaseChannels` TEXT, `antiFeatures` TEXT, `whatsNew` TEXT, `isCompatible` INTEGER NOT NULL, `file_name` TEXT NOT NULL, `file_sha256` TEXT NOT NULL, `file_size` INTEGER, `file_ipfsCidV1` TEXT, `src_name` TEXT, `src_sha256` TEXT, `src_size` INTEGER, `src_ipfsCidV1` TEXT, `manifest_versionName` TEXT NOT NULL, `manifest_versionCode` INTEGER NOT NULL, `manifest_maxSdkVersion` INTEGER, `manifest_nativecode` TEXT, `manifest_features` TEXT, `manifest_usesSdk_minSdkVersion` INTEGER, `manifest_usesSdk_targetSdkVersion` INTEGER, `manifest_signer_sha256` TEXT, `manifest_signer_hasMultipleSigners` INTEGER, PRIMARY KEY(`repoId`, `packageName`, `versionId`), FOREIGN KEY(`repoId`, `packageName`) REFERENCES `AppMetadata`(`repoId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packageName",
+ "columnName": "packageName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "versionId",
+ "columnName": "versionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "added",
+ "columnName": "added",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "releaseChannels",
+ "columnName": "releaseChannels",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "antiFeatures",
+ "columnName": "antiFeatures",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "whatsNew",
+ "columnName": "whatsNew",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "isCompatible",
+ "columnName": "isCompatible",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "file.name",
+ "columnName": "file_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "file.sha256",
+ "columnName": "file_sha256",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "file.size",
+ "columnName": "file_size",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "file.ipfsCidV1",
+ "columnName": "file_ipfsCidV1",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "src.name",
+ "columnName": "src_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "src.sha256",
+ "columnName": "src_sha256",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "src.size",
+ "columnName": "src_size",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "src.ipfsCidV1",
+ "columnName": "src_ipfsCidV1",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "manifest.versionName",
+ "columnName": "manifest_versionName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "manifest.versionCode",
+ "columnName": "manifest_versionCode",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "manifest.maxSdkVersion",
+ "columnName": "manifest_maxSdkVersion",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "manifest.nativecode",
+ "columnName": "manifest_nativecode",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "manifest.features",
+ "columnName": "manifest_features",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "manifest.usesSdk.minSdkVersion",
+ "columnName": "manifest_usesSdk_minSdkVersion",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "manifest.usesSdk.targetSdkVersion",
+ "columnName": "manifest_usesSdk_targetSdkVersion",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "manifest.signer.sha256",
+ "columnName": "manifest_signer_sha256",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "manifest.signer.hasMultipleSigners",
+ "columnName": "manifest_signer_hasMultipleSigners",
+ "affinity": "INTEGER"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId",
+ "packageName",
+ "versionId"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "AppMetadata",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "repoId",
+ "packageName"
+ ],
+ "referencedColumns": [
+ "repoId",
+ "packageName"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "VersionedString",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `versionId` TEXT NOT NULL, `type` TEXT NOT NULL, `name` TEXT NOT NULL, `version` INTEGER, PRIMARY KEY(`repoId`, `packageName`, `versionId`, `type`, `name`), FOREIGN KEY(`repoId`, `packageName`, `versionId`) REFERENCES `Version`(`repoId`, `packageName`, `versionId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packageName",
+ "columnName": "packageName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "versionId",
+ "columnName": "versionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId",
+ "packageName",
+ "versionId",
+ "type",
+ "name"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "Version",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "repoId",
+ "packageName",
+ "versionId"
+ ],
+ "referencedColumns": [
+ "repoId",
+ "packageName",
+ "versionId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "AppPrefs",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `ignoreVersionCodeUpdate` INTEGER NOT NULL, `preferredRepoId` INTEGER, `appPrefReleaseChannels` TEXT, PRIMARY KEY(`packageName`))",
+ "fields": [
+ {
+ "fieldPath": "packageName",
+ "columnName": "packageName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ignoreVersionCodeUpdate",
+ "columnName": "ignoreVersionCodeUpdate",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "preferredRepoId",
+ "columnName": "preferredRepoId",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "appPrefReleaseChannels",
+ "columnName": "appPrefReleaseChannels",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "packageName"
+ ]
+ }
+ }
+ ],
+ "views": [
+ {
+ "viewName": "LocalizedIcon",
+ "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM LocalizedFile WHERE type='icon'"
+ },
+ {
+ "viewName": "HighestVersion",
+ "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT repoId, packageName, antiFeatures FROM Version\n GROUP BY repoId, packageName HAVING MAX(manifest_versionCode)"
+ },
+ {
+ "viewName": "PreferredRepo",
+ "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT packageName, repoId AS preferredRepoId FROM AppMetadata\n JOIN RepositoryPreferences AS pref USING (repoId)\n LEFT JOIN AppPrefs USING (packageName)\n WHERE pref.enabled = 1 AND (repoId = COALESCE(preferredRepoId, repoId) OR\n NOT EXISTS (SELECT 1 FROM AppMetadata WHERE repoId=AppPrefs.preferredRepoId AND packageName=AppPrefs.packageName)\n )\n GROUP BY packageName HAVING MAX(pref.weight)"
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '33bf34813c9be51d73151e7f48001ae5')"
+ ]
+ }
+}
diff --git a/libs/database/src/dbTest/java/org/fdroid/database/AppOverviewItemsTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/AppOverviewItemsTest.kt
index b99234465..c029272e5 100644
--- a/libs/database/src/dbTest/java/org/fdroid/database/AppOverviewItemsTest.kt
+++ b/libs/database/src/dbTest/java/org/fdroid/database/AppOverviewItemsTest.kt
@@ -1,16 +1,20 @@
package org.fdroid.database
import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.runBlocking
import org.fdroid.LocaleChooser.getBestLocale
import org.fdroid.database.TestUtils.getOrAwaitValue
import org.fdroid.database.TestUtils.getOrFail
import org.fdroid.index.v2.MetadataV2
+import org.fdroid.test.TestAppUtils.getRandomMetadataV2
import org.fdroid.test.TestRepoUtils.getRandomRepo
+import org.fdroid.test.TestUtils.getRandomString
import org.fdroid.test.TestVersionUtils.getRandomPackageVersionV2
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.test.assertEquals
import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
@@ -19,13 +23,13 @@ import kotlin.test.assertTrue
internal class AppOverviewItemsTest : AppTest() {
@Test
- fun testAntiFeatures() {
- // insert one apps with without version
+ fun testAntiFeatures() = runBlocking {
+ // insert one app with without version
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, app1, locales)
// without version, anti-features are empty
- appDao.getAppOverviewItems().getOrFail().let { apps ->
+ getItems().forEach { apps ->
assertEquals(1, apps.size)
assertNull(apps[0].antiFeatures)
}
@@ -33,7 +37,7 @@ internal class AppOverviewItemsTest : AppTest() {
// with one version, the app has those anti-features
val version = getRandomPackageVersionV2(versionCode = 42)
versionDao.insert(repoId, packageName, "1", version, true)
- appDao.getAppOverviewItems().getOrFail().let { apps ->
+ getItems().forEach { apps ->
assertEquals(1, apps.size)
assertEquals(version.antiFeatures, apps[0].antiFeatures)
}
@@ -41,7 +45,7 @@ internal class AppOverviewItemsTest : AppTest() {
// with two versions, the app has the anti-features of the highest version
val version2 = getRandomPackageVersionV2(versionCode = 23)
versionDao.insert(repoId, packageName, "2", version2, true)
- appDao.getAppOverviewItems().getOrFail().let { apps ->
+ getItems().forEach { apps ->
assertEquals(1, apps.size)
assertEquals(version.antiFeatures, apps[0].antiFeatures)
}
@@ -49,20 +53,20 @@ internal class AppOverviewItemsTest : AppTest() {
// with three versions, the app has the anti-features of the highest version
val version3 = getRandomPackageVersionV2(versionCode = 1337)
versionDao.insert(repoId, packageName, "3", version3, true)
- appDao.getAppOverviewItems().getOrFail().let { apps ->
+ getItems().forEach { apps ->
assertEquals(1, apps.size)
assertEquals(version3.antiFeatures, apps[0].antiFeatures)
}
}
@Test
- fun testIcons() {
+ fun testIcons() = runBlocking {
// insert one app
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, app1, locales)
// icon is returned correctly
- appDao.getAppOverviewItems().getOrFail().let { apps ->
+ getItems().forEach { apps ->
assertEquals(1, apps.size)
assertEquals(app1.icon.getBestLocale(locales), apps[0].getIcon(locales))
}
@@ -72,14 +76,14 @@ internal class AppOverviewItemsTest : AppTest() {
appDao.insert(repoId2, packageName, app2, locales)
// app is still returned as before
- appDao.getAppOverviewItems().getOrFail().let { apps ->
+ getItems().forEach { apps ->
assertEquals(1, apps.size)
assertEquals(app1.icon.getBestLocale(locales), apps[0].getIcon(locales))
}
// after preferring second repo, icon is returned from app in second repo
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2))
- appDao.getAppOverviewItems().getOrFail().let { apps ->
+ getItems().forEach { apps ->
assertEquals(1, apps.size)
assertEquals(app2.icon.getBestLocale(locales), apps[0].getIcon(locales))
}
@@ -99,17 +103,18 @@ internal class AppOverviewItemsTest : AppTest() {
}
@Test
- fun testIncompatibleFlag() {
+ fun testIncompatibleFlag() = runBlocking {
// insert two apps
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales)
appDao.insert(repoId, packageName2, app2, locales)
// both apps are not compatible
- appDao.getAppOverviewItems().getOrFail().also {
- assertEquals(2, it.size)
- }.forEach {
- assertFalse(it.isCompatible)
+ getItems().forEach { apps ->
+ assertEquals(2, apps.size)
+ apps.forEach {
+ assertFalse(it.isCompatible)
+ }
}
// both apps, in the same category, are not compatible
appDao.getAppOverviewItems("A").getOrFail().also {
@@ -128,10 +133,10 @@ internal class AppOverviewItemsTest : AppTest() {
appDao.updateCompatibility(repoId)
// now only one is not compatible
- appDao.getAppOverviewItems().getOrFail().also {
- assertEquals(2, it.size)
- assertFalse(it[0].isCompatible)
- assertTrue(it[1].isCompatible)
+ getItems().forEach { apps ->
+ assertEquals(2, apps.size)
+ assertFalse(apps[0].isCompatible)
+ assertTrue(apps[1].isCompatible)
}
appDao.getAppOverviewItems("A").getOrFail().also {
assertEquals(2, it.size)
@@ -143,14 +148,14 @@ internal class AppOverviewItemsTest : AppTest() {
}
@Test
- fun testGetByRepoWeight() {
+ fun testGetByRepoWeight() = runBlocking {
// insert one app with one version
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, app1, locales)
versionDao.insert(repoId, packageName, "1", getRandomPackageVersionV2(2), true)
// app is returned correctly
- appDao.getAppOverviewItems().getOrFail().let { apps ->
+ getItems().forEach { apps ->
assertEquals(1, apps.size)
assertEquals(app1, apps[0])
}
@@ -160,21 +165,21 @@ internal class AppOverviewItemsTest : AppTest() {
appDao.insert(repoId2, packageName, app2, locales)
// app is still returned as before, new repo doesn't override old one
- appDao.getAppOverviewItems().getOrFail().let { apps ->
+ getItems().forEach { apps ->
assertEquals(1, apps.size)
assertEquals(app1, apps[0])
}
// now second app from second repo is returned after preferring it explicitly
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2))
- appDao.getAppOverviewItems().getOrFail().let { apps ->
+ getItems().forEach { apps ->
assertEquals(1, apps.size)
assertEquals(app2, apps[0])
}
}
@Test
- fun testGetByRepoPref() {
+ fun testGetByRepoPref() = runBlocking {
// insert same app into three repos (repoId1 has highest weight)
val repoId1 = repoDao.insertOrReplace(getRandomRepo())
val repoId3 = repoDao.insertOrReplace(getRandomRepo())
@@ -184,7 +189,7 @@ internal class AppOverviewItemsTest : AppTest() {
appDao.insert(repoId3, packageName, app3, locales)
// app is returned correctly from repo1
- appDao.getAppOverviewItems().getOrFail().let { apps ->
+ getItems().forEach { apps ->
assertEquals(1, apps.size)
assertEquals(app1, apps[0])
}
@@ -195,7 +200,7 @@ internal class AppOverviewItemsTest : AppTest() {
// prefer repo3 for this app
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId3))
- appDao.getAppOverviewItems().getOrFail().let { apps ->
+ getItems().forEach { apps ->
assertEquals(1, apps.size)
assertEquals(app3, apps[0])
}
@@ -206,7 +211,7 @@ internal class AppOverviewItemsTest : AppTest() {
// prefer repo2 for this app
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2))
- appDao.getAppOverviewItems().getOrFail().let { apps ->
+ getItems().forEach { apps ->
assertEquals(1, apps.size)
assertEquals(app2, apps[0])
}
@@ -220,7 +225,7 @@ internal class AppOverviewItemsTest : AppTest() {
// prefer repo1 for this app
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1))
- appDao.getAppOverviewItems().getOrFail().let { apps ->
+ getItems().forEach { apps ->
assertEquals(1, apps.size)
assertEquals(app1, apps[0])
}
@@ -341,7 +346,7 @@ internal class AppOverviewItemsTest : AppTest() {
}
@Test
- fun testOnlyFromEnabledRepos() {
+ fun testOnlyFromEnabledRepos() = runBlocking {
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales)
appDao.insert(repoId, packageName2, app2, locales)
@@ -349,18 +354,24 @@ internal class AppOverviewItemsTest : AppTest() {
appDao.insert(repoId2, packageName3, app3, locales)
// 3 apps from 2 repos
- assertEquals(3, appDao.getAppOverviewItems().getOrAwaitValue()?.size)
+ getItems().forEach { apps ->
+ assertEquals(3, apps.size)
+ }
assertEquals(3, appDao.getAppOverviewItems("A").getOrAwaitValue()?.size)
// only 1 app after disabling first repo
repoDao.setRepositoryEnabled(repoId, false)
- assertEquals(1, appDao.getAppOverviewItems().getOrAwaitValue()?.size)
+ getItems().forEach { apps ->
+ assertEquals(1, apps.size)
+ }
assertEquals(1, appDao.getAppOverviewItems("A").getOrAwaitValue()?.size)
assertEquals(1, appDao.getAppOverviewItems("B").getOrAwaitValue()?.size)
// no more apps after disabling all repos
repoDao.setRepositoryEnabled(repoId2, false)
- assertEquals(0, appDao.getAppOverviewItems().getOrAwaitValue()?.size)
+ getItems().forEach { apps ->
+ assertEquals(0, apps.size)
+ }
assertEquals(0, appDao.getAppOverviewItems("A").getOrAwaitValue()?.size)
assertEquals(0, appDao.getAppOverviewItems("B").getOrAwaitValue()?.size)
}
@@ -405,13 +416,73 @@ internal class AppOverviewItemsTest : AppTest() {
assertEquals(app2, appDao.getAppOverviewItem(repoId2, packageName))
}
+ @Test
+ fun testByAuthor() = runBlocking {
+ val author = getRandomString()
+ val packageName = getRandomString()
+ val repoId = repoDao.insertOrReplace(getRandomRepo())
+ appDao.insert(repoId, packageName, getRandomMetadataV2(author), locales)
+
+ // author has only one app
+ assertFalse(appDao.hasAuthorMoreThanOneApp(author).getOrFail())
+ assertEquals(0, appDao.getAppsByAuthor("foo bar").size)
+ appDao.getAppsByAuthor(author).let { apps ->
+ assertEquals(1, apps.size)
+ assertEquals(packageName, apps[0].packageName)
+ }
+
+ // now add 49 more apps
+ (1 until 50).forEach { _ ->
+ appDao.insert(repoId, getRandomString(), getRandomMetadataV2(author), locales)
+ }
+ assertTrue(appDao.hasAuthorMoreThanOneApp(author).getOrFail())
+ assertEquals(50, appDao.getAppsByAuthor(author).size)
+ }
+
+ @Test
+ fun testByCategory() = runBlocking {
+ // insert three apps
+ val repoId = repoDao.insertOrReplace(getRandomRepo())
+ appDao.insert(repoId, packageName1, app1, locales)
+ appDao.insert(repoId, packageName2, app2, locales)
+ appDao.insert(repoId, packageName3, app3, locales)
+
+ // only two apps are in category B
+ appDao.getAppsByCategory("B").let { apps ->
+ assertEquals(2, apps.size)
+ assertNotEquals(packageName2, apps[0].packageName)
+ assertNotEquals(packageName2, apps[1].packageName)
+ }
+
+ // no app is in category C
+ assertEquals(0, appDao.getAppsByCategory("C").size)
+
+ // we'll add app1 as a variant of app2, but its repo has lower weight, so no effect
+ val repoId2 = repoDao.insertOrReplace(getRandomRepo())
+ appDao.insert(repoId2, packageName2, app1, locales)
+ appDao.getAppsByCategory("B").let { apps ->
+ assertEquals(2, apps.size)
+ assertNotEquals(packageName2, apps[0].packageName)
+ assertNotEquals(packageName2, apps[1].packageName)
+ }
+ }
+
+ private suspend fun getItems(): List> {
+ return listOf(
+ appDao.getAppOverviewItems().getOrFail(),
+ // manually sort the second list, so both results are comparable
+ appDao.getAllApps().sortedByDescending { it.lastUpdated },
+ )
+ }
+
private fun assertEquals(expected: MetadataV2, actual: AppOverviewItem?) {
assertNotNull(actual)
assertEquals(expected.added, actual.added)
assertEquals(expected.lastUpdated, actual.lastUpdated)
assertEquals(expected.name.getBestLocale(locales), actual.name)
assertEquals(expected.summary.getBestLocale(locales), actual.summary)
- assertEquals(expected.summary.getBestLocale(locales), actual.summary)
+ assertEquals(expected.name.getBestLocale(locales), actual.getName(locales))
+ assertEquals(expected.summary.getBestLocale(locales), actual.getSummary(locales))
assertEquals(expected.icon.getBestLocale(locales), actual.getIcon(locales))
}
diff --git a/libs/database/src/dbTest/java/org/fdroid/database/CountryCodeMigrationTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/CountryCodeMigrationTest.kt
index 0a20fbc2d..011e7da5d 100644
--- a/libs/database/src/dbTest/java/org/fdroid/database/CountryCodeMigrationTest.kt
+++ b/libs/database/src/dbTest/java/org/fdroid/database/CountryCodeMigrationTest.kt
@@ -80,7 +80,7 @@ internal class CountryCodeMigrationTest {
FDroidDatabaseInt::class.java,
TEST_DB
)
- .addMigrations(MIGRATION_2_3, MIGRATION_5_6)
+ .addMigrations(MIGRATION_2_3, MIGRATION_5_6, MIGRATION_8_9)
.allowMainThreadQueries()
.build().use { db ->
// check repo got timestamp and etag reset
diff --git a/libs/database/src/dbTest/java/org/fdroid/database/DbTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/DbTest.kt
index f4208e62a..e4b31214c 100644
--- a/libs/database/src/dbTest/java/org/fdroid/database/DbTest.kt
+++ b/libs/database/src/dbTest/java/org/fdroid/database/DbTest.kt
@@ -114,7 +114,12 @@ internal abstract class DbTest {
assertEquals(sortedIndex.packages.size, appDao.countApps(), "number of packages")
sortedIndex.packages.forEach { (packageName, packageV2) ->
assertEquals(
- packageV2.metadata,
+ // zero-whitespace hack needs to get applied to expected data
+ packageV2.metadata.copy(
+ name = packageV2.metadata.name.zero(),
+ summary = packageV2.metadata.summary.zero(),
+ description = packageV2.metadata.description.zero(),
+ ),
appDao.getApp(repoId, packageName)?.toMetadataV2()?.sort()
)
val versions = versionDao.getAppVersions(repoId, packageName).getOrFail().map {
diff --git a/libs/database/src/dbTest/java/org/fdroid/database/DbUpdateCheckerTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/DbUpdateCheckerTest.kt
index 5b1edf429..57327014c 100644
--- a/libs/database/src/dbTest/java/org/fdroid/database/DbUpdateCheckerTest.kt
+++ b/libs/database/src/dbTest/java/org/fdroid/database/DbUpdateCheckerTest.kt
@@ -40,7 +40,7 @@ internal class DbUpdateCheckerTest : AppTest() {
override fun createDb() {
super.createDb()
every { packageManager.systemAvailableFeatures } returns emptyArray()
- updateChecker = DbUpdateChecker(db, packageManager) { true }
+ updateChecker = DbUpdateChecker(db, packageManager, { true })
}
@Test
diff --git a/libs/database/src/dbTest/java/org/fdroid/database/FtsAddColumnsMigrationTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/FtsAddColumnsMigrationTest.kt
new file mode 100644
index 000000000..3e006705f
--- /dev/null
+++ b/libs/database/src/dbTest/java/org/fdroid/database/FtsAddColumnsMigrationTest.kt
@@ -0,0 +1,240 @@
+package org.fdroid.database
+
+import android.content.ContentValues
+import android.content.Context
+import android.database.sqlite.SQLiteDatabase.CONFLICT_FAIL
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.room.testing.MigrationTestHelper
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlinx.coroutines.runBlocking
+import org.fdroid.database.TestUtils.getOrFail
+import org.fdroid.test.TestUtils.getRandomString
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.random.Random
+import kotlin.test.assertEquals
+
+private const val TEST_DB = "fts-test"
+
+@RunWith(AndroidJUnit4::class)
+internal class FtsAddColumnsMigrationTest {
+
+ @get:Rule
+ val helper: MigrationTestHelper = MigrationTestHelper(
+ instrumentation = InstrumentationRegistry.getInstrumentation(),
+ databaseClass = FDroidDatabaseInt::class.java,
+ specs = emptyList(),
+ openFactory = FrameworkSQLiteOpenHelperFactory(),
+ )
+
+ @get:Rule
+ val instantExec = InstantTaskExecutorRule()
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ private val repo = ContentValues().apply {
+ put("repoId", 1)
+ put("name", Converters.localizedTextV2toString(mapOf("de" to "a", "en-US" to "b")))
+ put("address", getRandomString())
+ put("certificate", "abcdef")
+ put("description", Converters.localizedTextV2toString(mapOf("de" to "aa", "en-US" to "bb")))
+ put("version", Random.nextLong())
+ put("timestamp", Random.nextLong())
+ }
+
+ private val repoPrefs = ContentValues().apply {
+ put("repoId", 1)
+ put("enabled", true)
+ put("weight", 1)
+ }
+
+ private val oeffiMetadata = ContentValues().apply {
+ put("packageName", "de.schildbach.oeffi")
+ put("repoId", 1)
+ put(
+ "name", Converters.localizedTextV2toString(
+ mapOf(
+ "de" to "Öffi", "en-US" to "Offi"
+ )
+ )
+ )
+ put(
+ "description", Converters.localizedTextV2toString(
+ mapOf(
+ "de" to "Öffentlicher Nahverkehr", "en-US" to "Public Transport"
+ )
+ )
+ )
+ put("license", "GPL-3.0")
+ put(
+ "summary", Converters.localizedTextV2toString(
+ mapOf(
+ "de" to "Der König des Fahrplandschungels!",
+ "en-US" to " King of public transit planning!"
+ )
+ )
+ )
+ put("localizedName", "Öffi")
+ put("localizedSummary", "Der König des Fahrplandschungels!")
+ put("added", Random.nextLong())
+ put("lastUpdated", Random.nextLong())
+ put("isCompatible", true)
+ }
+
+ private val oeffiVersion = ContentValues().apply {
+ put("repoId", 1)
+ put("packageName", "de.schildbach.oeffi")
+ put("versionId", 1)
+ put("added", 1000)
+ put("isCompatible", true)
+ put("file_name", "transportr.apk")
+ put("file_sha256", "73475CB40A568E8DA8A045CED110137E159F890AC4DA883B6B17DC651B3A8049")
+ put("manifest_versionName", "1.0.0")
+ put("manifest_versionCode", 1)
+ }
+
+ private val transportrMetadata = ContentValues().apply {
+ put("packageName", "de.grobox.liberario")
+ put("repoId", 1)
+ put(
+ "name", Converters.localizedTextV2toString(
+ mapOf(
+ "de" to "Transportr", "en-US" to "Transportr"
+ )
+ )
+ )
+ put(
+ "description", Converters.localizedTextV2toString(
+ mapOf(
+ "de" to "Öffentlicher Nahverkehr", "en-US" to "Public Transport"
+ )
+ )
+ )
+ put("license", "GPL-3.0")
+ put(
+ "summary", Converters.localizedTextV2toString(
+ mapOf(
+ "de" to "Freier Assistent für den öffentlichen Nahverkehr ohne Werbung",
+ "en-US" to "Free Public Transport Assistant without Ads or Tracking"
+ )
+ )
+ )
+ put("localizedName", "Transportr")
+ put(
+ "localizedSummary",
+ "Freier Assistent für den öffentlichen Nahverkehr ohne Werbung und Tracking"
+ )
+ put("added", Random.nextLong())
+ put("lastUpdated", Random.nextLong())
+ put("isCompatible", true)
+ }
+
+ private val transportrVersion = ContentValues().apply {
+ put("repoId", 1)
+ put("packageName", "de.grobox.liberario")
+ put("versionId", 1)
+ put("added", 1000)
+ put("isCompatible", true)
+ put("file_name", "transportr.apk")
+ put("file_sha256", "73475CB40A568E8DA8A045CED110137E159F890AC4DA883B6B17DC651B3A8049")
+ put("manifest_versionName", "1.0.0")
+ put("manifest_versionCode", 1)
+ }
+
+ @Test
+ fun testMigration() = runBlocking {
+ helper.createDatabase(TEST_DB, 8).use { db ->
+ // Database has schema version 8. Insert some data using SQL queries.
+ // We can't use DAO classes because they expect the latest schema.
+ db.insert(CoreRepository.TABLE, CONFLICT_FAIL, repo)
+ db.insert(RepositoryPreferences.TABLE, CONFLICT_FAIL, repoPrefs)
+ db.insert(AppMetadata.TABLE, CONFLICT_FAIL, oeffiMetadata)
+ db.insert(AppMetadata.TABLE, CONFLICT_FAIL, transportrMetadata)
+ db.insert(Version.TABLE, CONFLICT_FAIL, oeffiVersion)
+ db.insert(Version.TABLE, CONFLICT_FAIL, transportrVersion)
+
+ // default search with no diacritics
+ assertSearch(db, "*Transport*", 1)
+ assertSearch(db, "Transportr", 1)
+ assertSearch(db, "*f*", 2)
+ // no or wrong diacritics
+ assertSearch(db, "Offi", 0)
+ assertSearch(db, "offi", 0)
+ assertSearch(db, "õffi", 0)
+ assertSearch(db, "*Offi*", 0)
+ assertSearch(db, "*offi*", 0)
+ assertSearch(db, "Konig", 0)
+ // correct diacritics
+ assertSearch(db, "*Öffi*", 1)
+ assertSearch(db, "*öffi*", 1)
+ assertSearch(db, "Öffi", 1)
+ assertSearch(db, "öffi", 1)
+ // both apps have "öff" in their name or summary
+ assertSearch(db, "*öff*", 2)
+ assertSearch(db, "König", 1)
+ }
+
+ helper.runMigrationsAndValidate(TEST_DB, 9, true, MIGRATION_8_9).close()
+
+ // now get the Room DB, so we can use our DAOs for verifying the migration
+ Room.databaseBuilder(context, FDroidDatabaseInt::class.java, TEST_DB)
+ .allowMainThreadQueries()
+ .build().use { db ->
+ // assert that apps are still there
+ val metadata = db.getAppDao().getAppMetadata()
+ assertEquals(2, metadata.size)
+ // default search with no diacritics
+ assertGetAppListItems(db, "*Transport*", 2)
+ assertGetAppListItems(db, "Transportr", 1)
+ assertGetAppListItems(db, "*f*", 2)
+ // no or wrong diacritics also produces results now
+ assertGetAppListItems(db, "Offi", 1)
+ assertGetAppListItems(db, "offi", 1)
+ assertGetAppListItems(db, "õffi", 1)
+ assertGetAppListItems(db, "*Offi*", 1)
+ assertGetAppListItems(db, "*offi*", 1)
+ assertGetAppListItems(db, "Konig", 1)
+ // correct diacritics still produces results
+ assertGetAppListItems(db, "*Öffi*", 1)
+ assertGetAppListItems(db, "*öffi*", 1)
+ assertGetAppListItems(db, "Öffi", 1)
+ assertGetAppListItems(db, "öffi", 1)
+ // both apps have "öff" in their name or summary
+ assertGetAppListItems(db, "*öff*", 2)
+ assertGetAppListItems(db, "König", 1)
+
+ // a new app also gets updated in the index
+ assertGetAppListItems(db, "Rosa", 0)
+ assertGetAppListItems(db, "Elefant", 0)
+ val newApp = AppMetadata(
+ repoId = 1,
+ packageName = "org.example",
+ added = 23,
+ lastUpdated = 42,
+ name = mapOf("de_DE" to "Rosa Elefant"),
+ isCompatible = true
+ )
+ db.getAppDao().insert(newApp)
+ assertGetAppListItems(db, "Rosa", 1)
+ assertGetAppListItems(db, "Elefant", 1)
+ }
+ }
+
+ private fun assertSearch(db: SupportSQLiteDatabase, query: String, expected: Int) {
+ db.query("SELECT * FROM AppMetadataFts WHERE AppMetadataFts MATCH '$query'")
+ .use { cursor -> assertEquals(expected, cursor.count) }
+ }
+
+ private fun assertGetAppListItems(db: FDroidDatabaseInt, query: String, expected: Int) {
+ db.getAppDao().getAppListItems(query).getOrFail().let { result ->
+ assertEquals(expected, result.size, "${result.map { it.name }}")
+ }
+ }
+
+}
diff --git a/libs/database/src/dbTest/java/org/fdroid/database/FtsCaseInsensitiveMigrationTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/FtsCaseInsensitiveMigrationTest.kt
index 33b5d02c9..815db1845 100644
--- a/libs/database/src/dbTest/java/org/fdroid/database/FtsCaseInsensitiveMigrationTest.kt
+++ b/libs/database/src/dbTest/java/org/fdroid/database/FtsCaseInsensitiveMigrationTest.kt
@@ -176,29 +176,16 @@ internal class FtsCaseInsensitiveMigrationTest {
// now get the Room DB, so we can use our DAOs for verifying the migration
Room.databaseBuilder(context, FDroidDatabaseInt::class.java, TEST_DB)
.allowMainThreadQueries()
+ .addMigrations(MIGRATION_8_9) // was added later
.build().use { db ->
// assert that apps are still there
val metadata = db.getAppDao().getAppMetadata()
assertEquals(2, metadata.size)
// default search with no diacritics
- assertGetAppListItems(db, "*Transport*", 1)
- assertGetAppListItems(db, "Transportr", 1)
- assertGetAppListItems(db, "*f*", 2)
- // no or wrong diacritics
- assertGetAppListItems(db, "Offi", 0)
- assertGetAppListItems(db, "offi", 0)
- assertGetAppListItems(db, "õffi", 0)
- assertGetAppListItems(db, "*Offi*", 0)
- assertGetAppListItems(db, "*offi*", 0)
- assertGetAppListItems(db, "Konig", 0)
- // correct diacritics
- assertGetAppListItems(db, "*Öffi*", 1)
- assertGetAppListItems(db, "*öffi*", 1)
- assertGetAppListItems(db, "Öffi", 1)
- assertGetAppListItems(db, "öffi", 1)
- // both apps have "öff" in their name or summary
- assertGetAppListItems(db, "*öff*", 2)
- assertGetAppListItems(db, "König", 1)
+ assertGetAppListItems(db, "*Transport*", 2)
+
+ // other tests here were removed, because MIGRATION_8_9 changed this once again
+ // and has its own test
}
}
diff --git a/libs/database/src/dbTest/java/org/fdroid/database/IndexV2DiffTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/IndexV2DiffTest.kt
index d3d37c70a..f0e4db3c7 100644
--- a/libs/database/src/dbTest/java/org/fdroid/database/IndexV2DiffTest.kt
+++ b/libs/database/src/dbTest/java/org/fdroid/database/IndexV2DiffTest.kt
@@ -390,6 +390,37 @@ internal class IndexV2DiffTest : DbTest() {
)
}
+ @Test
+ fun testMinAddChinese() {
+ val diffJson = """{
+ "packages": {
+ "org.fdroid.min1": {
+ "metadata": {
+ "name": { "zh-CN": "自由软件仓库" },
+ "summary": { "ja": "这个仓库中的" },
+ "description": { "ko-KR": "切始终是从" }
+ }
+ }
+ }
+ }""".trimIndent()
+ testJsonDiff(
+ startPath = "index-min-v2.json",
+ diff = diffJson,
+ endIndex = TestDataMinV2.index.copy(
+ packages = TestDataMinV2.index.packages.mapValues {
+ it.value.copy(
+ metadata = it.value.metadata.copy(
+ // zero whitespaces (to separate tokens) will be added in testJsonDiff()
+ name = mapOf("zh-CN" to "自由软件仓库"),
+ summary = mapOf("ja" to "这个仓库中的"),
+ description = mapOf("ko-KR" to "切始终是从"),
+ )
+ )
+ }
+ ),
+ )
+ }
+
private fun testJsonDiff(startPath: String, diff: String, endIndex: IndexV2) {
testDiff(startPath, ByteArrayInputStream(diff.toByteArray()), endIndex)
}
diff --git a/libs/database/src/dbTest/java/org/fdroid/database/MultiRepoMigrationTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/MultiRepoMigrationTest.kt
index 696bb05a5..0a4f171ff 100644
--- a/libs/database/src/dbTest/java/org/fdroid/database/MultiRepoMigrationTest.kt
+++ b/libs/database/src/dbTest/java/org/fdroid/database/MultiRepoMigrationTest.kt
@@ -232,7 +232,7 @@ internal class MultiRepoMigrationTest {
// now get the Room DB, so we can use our DAOs for verifying the migration
databaseBuilder(getApplicationContext(), FDroidDatabaseInt::class.java, TEST_DB)
- .addMigrations(MIGRATION_2_3, MIGRATION_5_6)
+ .addMigrations(MIGRATION_2_3, MIGRATION_5_6, MIGRATION_8_9)
.allowMainThreadQueries()
.build()
.use { db ->
@@ -275,7 +275,7 @@ internal class MultiRepoMigrationTest {
// now get the Room DB, so we can use our DAOs for verifying the migration
databaseBuilder(getApplicationContext(), FDroidDatabaseInt::class.java, TEST_DB)
- .addMigrations(MIGRATION_2_3, MIGRATION_5_6)
+ .addMigrations(MIGRATION_2_3, MIGRATION_5_6, MIGRATION_8_9)
.allowMainThreadQueries()
.build().use { db ->
// repo without cert did not get migrated, because we auto-migrate to latest version
@@ -311,7 +311,7 @@ internal class MultiRepoMigrationTest {
// now get the Room DB, so we can use our DAOs for verifying the migration
databaseBuilder(getApplicationContext(), FDroidDatabaseInt::class.java, TEST_DB)
- .addMigrations(MIGRATION_2_3, MIGRATION_5_6)
+ .addMigrations(MIGRATION_2_3, MIGRATION_5_6, MIGRATION_8_9)
.allowMainThreadQueries()
.build().use { db ->
check(db)
diff --git a/libs/database/src/dbTest/java/org/fdroid/database/PreferredRepoMigrationTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/PreferredRepoMigrationTest.kt
index 4d258564a..8a30dbd57 100644
--- a/libs/database/src/dbTest/java/org/fdroid/database/PreferredRepoMigrationTest.kt
+++ b/libs/database/src/dbTest/java/org/fdroid/database/PreferredRepoMigrationTest.kt
@@ -80,7 +80,7 @@ internal class PreferredRepoMigrationTest {
FDroidDatabaseInt::class.java,
TEST_DB
)
- .addMigrations(MIGRATION_2_3, MIGRATION_5_6)
+ .addMigrations(MIGRATION_2_3, MIGRATION_5_6, MIGRATION_8_9)
.allowMainThreadQueries()
.build().use { db ->
// repo without cert did not get migrated, the other one did
diff --git a/libs/database/src/dbTest/java/org/fdroid/database/RepoCertNonNullMigrationTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/RepoCertNonNullMigrationTest.kt
index f3411efae..167210a88 100644
--- a/libs/database/src/dbTest/java/org/fdroid/database/RepoCertNonNullMigrationTest.kt
+++ b/libs/database/src/dbTest/java/org/fdroid/database/RepoCertNonNullMigrationTest.kt
@@ -78,7 +78,7 @@ internal class RepoCertNonNullMigrationTest {
// now get the Room DB, so we can use our DAOs for verifying the migration
Room.databaseBuilder(getApplicationContext(), FDroidDatabaseInt::class.java, TEST_DB)
- .addMigrations(MIGRATION_2_3, MIGRATION_5_6)
+ .addMigrations(MIGRATION_2_3, MIGRATION_5_6, MIGRATION_8_9)
.allowMainThreadQueries()
.build().use { db ->
// repo without cert did not get migrated, the other one did
diff --git a/libs/database/src/dbTest/java/org/fdroid/index/v1/IndexV1UpdaterTest.kt b/libs/database/src/dbTest/java/org/fdroid/index/v1/IndexV1UpdaterTest.kt
index 89ed3d0fb..fc2d1b021 100644
--- a/libs/database/src/dbTest/java/org/fdroid/index/v1/IndexV1UpdaterTest.kt
+++ b/libs/database/src/dbTest/java/org/fdroid/index/v1/IndexV1UpdaterTest.kt
@@ -194,7 +194,7 @@ internal class IndexV1UpdaterTest : DbTest() {
assets.open(jar).use { inputStream ->
jarFile.outputStream().use { inputStream.copyTo(it) }
}
- every { tempFileProvider.createTempFile() } returns jarFile
+ every { tempFileProvider.createTempFile(null) } returns jarFile
every {
downloaderFactory.createWithTryFirstMirror(repo, uri, indexFile, jarFile)
} returns downloader
diff --git a/libs/database/src/dbTest/java/org/fdroid/index/v2/IndexV2UpdaterTest.kt b/libs/database/src/dbTest/java/org/fdroid/index/v2/IndexV2UpdaterTest.kt
index 38db9bd6b..ed8bcfea2 100644
--- a/libs/database/src/dbTest/java/org/fdroid/index/v2/IndexV2UpdaterTest.kt
+++ b/libs/database/src/dbTest/java/org/fdroid/index/v2/IndexV2UpdaterTest.kt
@@ -283,7 +283,9 @@ internal class IndexV2UpdaterTest : DbTest() {
assets.open("diff-empty-min/23.json").use { inputStream ->
indexFile.outputStream().use { inputStream.copyTo(it) }
}
- every { tempFileProvider.createTempFile() } returnsMany listOf(entryFile, indexFile)
+ every {
+ tempFileProvider.createTempFile(any())
+ } returnsMany listOf(entryFile, indexFile)
val result2 = indexUpdater.update(repo2)
assertIs(result2)
@@ -312,7 +314,7 @@ internal class IndexV2UpdaterTest : DbTest() {
indexFile.outputStream().use { inputStream.copyTo(it) }
}
- every { tempFileProvider.createTempFile() } returnsMany listOf(entryFile, indexFile)
+ every { tempFileProvider.createTempFile(any()) } returnsMany listOf(entryFile, indexFile)
every {
downloaderFactory.createWithTryFirstMirror(repo, entryUri, entryFileV2, any())
} returns downloader
diff --git a/libs/database/src/main/java/org/fdroid/database/App.kt b/libs/database/src/main/java/org/fdroid/database/App.kt
index 35d06f33e..fed489aa6 100644
--- a/libs/database/src/main/java/org/fdroid/database/App.kt
+++ b/libs/database/src/main/java/org/fdroid/database/App.kt
@@ -9,6 +9,7 @@ import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Fts4
+import androidx.room.FtsOptions
import androidx.room.Ignore
import androidx.room.Relation
import org.fdroid.LocaleChooser.getBestLocale
@@ -96,9 +97,9 @@ internal fun MetadataV2.toAppMetadata(
packageName = packageName,
added = added,
lastUpdated = lastUpdated,
- name = name,
- summary = summary,
- description = description,
+ name = name.zero(),
+ summary = summary.zero(),
+ description = description.zero(),
localizedName = name.getBestLocale(locales),
localizedSummary = summary.getBestLocale(locales),
webSite = webSite,
@@ -124,19 +125,53 @@ internal fun MetadataV2.toAppMetadata(
isCompatible = isCompatible,
)
+/**
+ * Introduce zero whitespace for CJK (Chinese, Japanese, Korean) languages.
+ * This is needed, because the sqlite tokenizers available to us either handle those languages
+ * or do diacritics removals.
+ * Since we can't remove diacritics here ourselves,
+ * we help the tokenizer for CJK languages instead.
+ */
+internal fun LocalizedTextV2?.zero(): LocalizedTextV2? {
+ if (this == null) return null
+ return toMutableMap().mapValues { (locale, text) ->
+ if (locale.startsWith("zh") || locale.startsWith("ja") || locale.startsWith("ko")) {
+ StringBuilder().apply {
+ text.forEachIndexed { i, char ->
+ if (Character.isIdeographic(char.code) && i + 1 < text.length) {
+ append(char)
+ append("\u200B")
+ } else {
+ append(char)
+ }
+ }
+ }.toString()
+ } else {
+ text
+ }
+ }
+}
+
@Entity(tableName = AppMetadataFts.TABLE)
@Fts4(
contentEntity = AppMetadata::class,
- // make FTS for non-ASCII characters case insensitive, but do not remove diacritics
- tokenizer = "unicode61 \"remove_diacritics=0\""
+ // make FTS for non-ASCII characters case insensitive, CJK languages are handled separately,
+ // because there's no tokenizer available that handles everything
+ tokenizer = FtsOptions.TOKENIZER_UNICODE61,
+ // can't use remove_diacritics=2 because it is SDK_INT >=30
+ // see: https://www.twisterrob.net/blog/2023/10/sqlite-unicode61-remove-diacritics-2.html
+ // separators=. is mainly for package name search
+ // tokenchars=- is so that searching for F-Droid works as expected
+ tokenizerArgs = ["remove_diacritics=1", "separators=.", "tokenchars=-"],
+ notIndexed = ["repoId"],
)
internal data class AppMetadataFts(
val repoId: Long,
- val packageName: String,
- @ColumnInfo(name = "localizedName")
val name: String? = null,
- @ColumnInfo(name = "localizedSummary")
val summary: String? = null,
+ val description: String? = null,
+ val authorName: String? = null,
+ val packageName: String,
) {
internal companion object {
const val TABLE = "AppMetadataFts"
@@ -250,9 +285,16 @@ public data class AppOverviewItem internal constructor(
public val added: Long,
public val lastUpdated: Long,
@ColumnInfo(name = "localizedName")
+ @Deprecated("Use getName() method instead.")
public override val name: String? = null,
@ColumnInfo(name = "localizedSummary")
+ @Deprecated("Use getSummary() method instead.")
public override val summary: String? = null,
+ @ColumnInfo(name = "name")
+ internal val internalName: LocalizedTextV2? = null,
+ @ColumnInfo(name = "summary")
+ internal val internalSummary: LocalizedTextV2? = null,
+ public val categories: List? = null,
internal val antiFeatures: Map? = null,
@Relation(
parentColumn = "packageName",
@@ -264,6 +306,14 @@ public data class AppOverviewItem internal constructor(
*/
public val isCompatible: Boolean,
) : MinimalApp {
+ public fun getName(localeList: LocaleListCompat): String? {
+ return internalName.getBestLocale(localeList)
+ }
+
+ public fun getSummary(localeList: LocaleListCompat): String? {
+ return internalSummary.getBestLocale(localeList)
+ }
+
public override fun getIcon(localeList: LocaleListCompat): FileV2? {
return localizedIcon?.filter { icon ->
icon.repoId == repoId
@@ -291,6 +341,7 @@ public data class AppListItem internal constructor(
@ColumnInfo(name = "localizedSummary")
public override val summary: String? = null,
public val lastUpdated: Long,
+ public val categories: List? = null,
internal val antiFeatures: String?,
@Relation(
parentColumn = "packageName",
@@ -344,6 +395,7 @@ public data class UpdatableApp internal constructor(
public override val repoId: Long,
public override val packageName: String,
public val installedVersionCode: Long,
+ public val installedVersionName: String,
public val update: AppVersion,
public val isFromPreferredRepo: Boolean,
/**
diff --git a/libs/database/src/main/java/org/fdroid/database/AppDao.kt b/libs/database/src/main/java/org/fdroid/database/AppDao.kt
index 2e62839e7..9541bdfae 100644
--- a/libs/database/src/main/java/org/fdroid/database/AppDao.kt
+++ b/libs/database/src/main/java/org/fdroid/database/AppDao.kt
@@ -19,6 +19,8 @@ import androidx.room.RoomRawQuery
import androidx.room.RoomWarnings.Companion.QUERY_MISMATCH
import androidx.room.Transaction
import androidx.room.Update
+import androidx.sqlite.SQLiteStatement
+import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
@@ -34,6 +36,7 @@ import org.fdroid.index.v2.LocalizedFileListV2
import org.fdroid.index.v2.LocalizedFileV2
import org.fdroid.index.v2.MetadataV2
import org.fdroid.index.v2.ReflectionDiffer.applyDiff
+import java.util.concurrent.TimeUnit
public interface AppDao {
/**
@@ -83,16 +86,62 @@ public interface AppDao {
* Apps without name, icon or summary are at the end (or excluded if limit is too small).
* Includes anti-features from the version with the highest version code.
*/
+ @Deprecated("Use getNewAppsFlow and getRecentlyUpdatedAppsFlow instead")
public fun getAppOverviewItems(limit: Int = 200): LiveData>
/**
* Returns a limited number of apps with limited data within the given [category].
*/
+ @Deprecated("Use getAppsByCategory instead")
public fun getAppOverviewItems(
category: String,
limit: Int = 50,
): LiveData>
+ /**
+ * Returns all apps from the database.
+ */
+ public suspend fun getAllApps(): List
+
+ /**
+ * Returns all apps whose author is set exactly to [authorName].
+ */
+ public suspend fun getAppsByAuthor(authorName: String): List
+
+ /**
+ * Returns all apps that are in the category with [categoryId].
+ */
+ public suspend fun getAppsByCategory(categoryId: String): List
+
+ /**
+ * Returns apps that are new. This means that they were added and last updated at the same time.
+ * @param maxAgeInDays the number of days that is still considered "new".
+ * Apps older than this won't be returned.
+ */
+ public suspend fun getNewApps(maxAgeInDays: Long = 14): List
+
+ /**
+ * Get apps that were recently updated.
+ * This excludes apps returned by [getNewApps].
+ * @param limit only return that many apps and not more.
+ */
+ public suspend fun getRecentlyUpdatedApps(limit: Int = 200): List
+
+ /**
+ * Get all apps from the repository identified by [repoId].
+ */
+ public suspend fun getAppsByRepository(repoId: Long): List
+
+ /**
+ * Same as [getNewApps], but returns an observable [Flow].
+ */
+ public fun getNewAppsFlow(maxAgeInDays: Long = 14): Flow>
+
+ /**
+ * Same as [getRecentlyUpdatedApps], but returns an observable [Flow].
+ */
+ public fun getRecentlyUpdatedAppsFlow(limit: Int = 200): Flow>
+
/**
* Returns a list of all [AppListItem] sorted by the given [sortOrder],
* or a subset of [AppListItem]s filtered by the given [searchQuery] if it is non-null.
@@ -141,6 +190,8 @@ public interface AppDao {
public fun getInstalledAppListItems(packageManager: PackageManager): LiveData>
+ public suspend fun getAppSearchItems(searchQuery: String): List
+
public fun getNumberOfAppsInCategory(category: String): Int
public fun getNumberOfAppsInRepository(repoId: Long): Int
@@ -237,13 +288,19 @@ internal interface AppDaoInt : AppDao {
}
// diff metadata
val diffedApp = applyDiff(metadata, jsonObject)
- val updatedApp =
- if (jsonObject.containsKey("name") || jsonObject.containsKey("summary")) {
- diffedApp.copy(
- localizedName = diffedApp.name.getBestLocale(locales),
- localizedSummary = diffedApp.summary.getBestLocale(locales),
- )
- } else diffedApp
+ val containsName = jsonObject.containsKey("name")
+ val containsSummary = jsonObject.containsKey("summary")
+ val containsDescription = jsonObject.containsKey("description")
+ val updatedApp = if (containsName || containsSummary || containsDescription) {
+ diffedApp.copy(
+ name = if (containsName) diffedApp.name.zero() else diffedApp.name,
+ summary = if (containsSummary) diffedApp.summary.zero() else diffedApp.summary,
+ description = if (containsDescription) diffedApp.description.zero()
+ else diffedApp.description,
+ localizedName = diffedApp.name.getBestLocale(locales),
+ localizedSummary = diffedApp.summary.getBestLocale(locales),
+ )
+ } else diffedApp
updateAppMetadata(updatedApp)
// diff localizedFiles
val localizedFiles = getLocalizedFiles(repoId, packageName)
@@ -319,6 +376,7 @@ internal interface AppDaoInt : AppDao {
WHERE repoId = :repoId""")
override fun updateCompatibility(repoId: Long)
+ @Deprecated("Will be removed in future version")
@Query("""UPDATE ${AppMetadata.TABLE} SET localizedName = :name, localizedSummary = :summary
WHERE repoId = :repoId AND packageName = :packageName""")
fun updateAppMetadata(repoId: Long, packageName: String, name: String?, summary: String?)
@@ -366,7 +424,7 @@ internal interface AppDaoInt : AppDao {
@Transaction
@Query("""SELECT repoId, packageName, app.added, app.lastUpdated, localizedName,
- localizedSummary, version.antiFeatures, app.isCompatible
+ localizedSummary, app.name, summary, categories, version.antiFeatures, app.isCompatible
FROM ${AppMetadata.TABLE} AS app
JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId)
JOIN PreferredRepo USING (packageName)
@@ -381,7 +439,7 @@ internal interface AppDaoInt : AppDao {
@Transaction
@Query("""SELECT repoId, packageName, app.added, app.lastUpdated, localizedName,
- localizedSummary, version.antiFeatures, app.isCompatible
+ localizedSummary, app.name, summary, categories, version.antiFeatures, app.isCompatible
FROM ${AppMetadata.TABLE} AS app
JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId)
JOIN PreferredRepo USING (packageName)
@@ -401,10 +459,115 @@ internal interface AppDaoInt : AppDao {
@Transaction
@SuppressWarnings(QUERY_MISMATCH) // no anti-features needed here
@Query("""SELECT repoId, packageName, added, app.lastUpdated, localizedName,
- localizedSummary, app.isCompatible
+ localizedSummary, name, summary, categories, app.isCompatible
FROM ${AppMetadata.TABLE} AS app WHERE repoId = :repoId AND packageName = :packageName""")
fun getAppOverviewItem(repoId: Long, packageName: String): AppOverviewItem?
+ @Transaction
+ override suspend fun getAllApps(): List {
+ val query = getAppsQuery("") {}
+ return getApps(query)
+ }
+
+ @Transaction
+ override suspend fun getAppsByAuthor(authorName: String): List {
+ val query = getAppsQuery("authorName = ?") { statement ->
+ statement.bindText(1, authorName)
+ }
+ return getApps(query)
+ }
+
+ @Transaction
+ override suspend fun getAppsByCategory(categoryId: String): List {
+ val query = getAppsQuery("categories LIKE '%,' || ? || ',%'") { statement ->
+ statement.bindText(1, categoryId)
+ }
+ return getApps(query)
+ }
+
+ @Transaction
+ override suspend fun getNewApps(maxAgeInDays: Long): List {
+ val query =
+ getAppsQuery("app.added = app.lastUpdated AND app.lastUpdated > ?") { statement ->
+ statement.bindLong(
+ 1,
+ System.currentTimeMillis() - TimeUnit.DAYS.toMillis(maxAgeInDays)
+ )
+ }
+ return getApps(query)
+ }
+
+ @Transaction
+ override suspend fun getRecentlyUpdatedApps(limit: Int): List {
+ val query = getAppsQuery(
+ "app.added != app.lastUpdated ORDER BY app.lastUpdated DESC LIMIT ?"
+ ) { statement ->
+ statement.bindInt(1, limit)
+ }
+ return getApps(query)
+ }
+
+ @Transaction
+ @Query("""SELECT repoId, packageName, app.added, app.lastUpdated, localizedName,
+ localizedSummary, name, summary, categories, version.antiFeatures, app.isCompatible
+ FROM ${AppMetadata.TABLE} AS app
+ LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName)
+ WHERE repoId = :repoId""")
+ override suspend fun getAppsByRepository(repoId: Long): List
+
+ override fun getNewAppsFlow(maxAgeInDays: Long): Flow> {
+ val query =
+ getAppsQuery(
+ "app.added = app.lastUpdated AND app.lastUpdated > ? ORDER BY app.added DESC"
+ ) { statement ->
+ statement.bindLong(
+ 1,
+ System.currentTimeMillis() - TimeUnit.DAYS.toMillis(maxAgeInDays)
+ )
+ }
+ return getAppsFlow(query)
+ }
+
+ override fun getRecentlyUpdatedAppsFlow(limit: Int): Flow> {
+ val query = getAppsQuery(
+ "app.added != app.lastUpdated ORDER BY app.lastUpdated DESC LIMIT ?"
+ ) { statement ->
+ statement.bindInt(1, limit)
+ }
+ return getAppsFlow(query)
+ }
+
+ private fun getAppsQuery(
+ whereQuery: String,
+ onBindStatement: (SQLiteStatement) -> Unit,
+ ): RoomRawQuery {
+ val queryBuilder = StringBuilder(
+ """
+ SELECT repoId, packageName, app.added, app.lastUpdated, localizedName,
+ localizedSummary, name, summary, categories, version.antiFeatures, app.isCompatible
+ FROM ${AppMetadata.TABLE} AS app
+ JOIN PreferredRepo USING (packageName)
+ LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName)
+ WHERE repoId = preferredRepoId"""
+ )
+ if (whereQuery.isNotEmpty()) queryBuilder.append(" AND ").append(whereQuery)
+ return RoomRawQuery(
+ sql = queryBuilder.toString().trimIndent(),
+ onBindStatement = onBindStatement,
+ )
+ }
+
+ @RawQuery
+ suspend fun getApps(query: RoomRawQuery): List
+
+ @Transaction
+ @RawQuery(
+ observedEntities = [
+ AppMetadata::class, Version::class, Repository::class, RepositoryPreferences::class,
+ ]
+ )
+ fun getAppsFlow(query: RoomRawQuery): Flow>
+
//
// AppListItems
//
@@ -430,7 +593,7 @@ internal interface AppDaoInt : AppDao {
val queryBuilder =
StringBuilder("""
SELECT repoId, packageName, localizedName, localizedSummary, app.lastUpdated,
- version.antiFeatures, app.isCompatible, app.preferredSigner
+ categories, version.antiFeatures, app.isCompatible, app.preferredSigner
FROM ${AppMetadata.TABLE} AS app
JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId)
JOIN PreferredRepo USING (packageName)
@@ -461,7 +624,7 @@ internal interface AppDaoInt : AppDao {
val queryBuilder =
StringBuilder("""
SELECT repoId, packageName, localizedName, localizedSummary, app.lastUpdated,
- version.antiFeatures, app.isCompatible, app.preferredSigner
+ categories, version.antiFeatures, app.isCompatible, app.preferredSigner
FROM ${AppMetadata.TABLE} AS app
LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName)
WHERE repoId = :repoId""")
@@ -486,7 +649,7 @@ internal interface AppDaoInt : AppDao {
val queryBuilder =
StringBuilder("""
SELECT repoId, packageName, localizedName, localizedSummary, app.lastUpdated,
- version.antiFeatures, app.isCompatible, app.preferredSigner
+ categories, version.antiFeatures, app.isCompatible, app.preferredSigner
FROM ${AppMetadata.TABLE} AS app
JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId)
JOIN PreferredRepo USING (packageName)
@@ -537,7 +700,7 @@ internal interface AppDaoInt : AppDao {
@Transaction
@Query("""
SELECT repoId, packageName, app.localizedName, app.localizedSummary, app.lastUpdated,
- version.antiFeatures, app.isCompatible, app.preferredSigner
+ categories, version.antiFeatures, app.isCompatible, app.preferredSigner
FROM ${AppMetadata.TABLE} AS app
JOIN PreferredRepo USING (packageName)
JOIN ${AppMetadataFts.TABLE} USING (repoId, packageName)
@@ -554,7 +717,7 @@ internal interface AppDaoInt : AppDao {
@Transaction
@Query("""
SELECT repoId, packageName, app.localizedName, app.localizedSummary, app.lastUpdated,
- version.antiFeatures, app.isCompatible, app.preferredSigner
+ categories, version.antiFeatures, app.isCompatible, app.preferredSigner
FROM ${AppMetadata.TABLE} AS app
JOIN ${AppMetadataFts.TABLE} USING (repoId, packageName)
JOIN PreferredRepo USING (packageName)
@@ -574,7 +737,7 @@ internal interface AppDaoInt : AppDao {
@Transaction
@Query("""
SELECT repoId, packageName, app.localizedName, app.localizedSummary, app.lastUpdated,
- version.antiFeatures, app.isCompatible, app.preferredSigner
+ categories, version.antiFeatures, app.isCompatible, app.preferredSigner
FROM ${AppMetadata.TABLE} AS app
LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName)
WHERE repoId = :repoId AND app.rowid IN (
@@ -586,7 +749,7 @@ internal interface AppDaoInt : AppDao {
@Transaction
@Query("""
SELECT repoId, packageName, localizedName, localizedSummary, app.lastUpdated,
- version.antiFeatures, app.isCompatible, app.preferredSigner
+ categories, version.antiFeatures, app.isCompatible, app.preferredSigner
FROM ${AppMetadata.TABLE} AS app
LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName)
JOIN PreferredRepo USING (packageName)
@@ -599,7 +762,7 @@ internal interface AppDaoInt : AppDao {
@Transaction
@Query("""
SELECT repoId, packageName, localizedName, localizedSummary, app.lastUpdated,
- version.antiFeatures, app.isCompatible, app.preferredSigner
+ categories, version.antiFeatures, app.isCompatible, app.preferredSigner
FROM ${AppMetadata.TABLE} AS app
JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId)
JOIN PreferredRepo USING (packageName)
@@ -618,7 +781,7 @@ internal interface AppDaoInt : AppDao {
@Transaction
@SuppressWarnings(QUERY_MISMATCH) // no anti-features needed here
@Query("""SELECT repoId, packageName, localizedName, localizedSummary, app.lastUpdated,
- app.isCompatible, app.preferredSigner
+ categories, app.isCompatible, app.preferredSigner
FROM ${AppMetadata.TABLE} AS app
JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId)
JOIN PreferredRepo USING (packageName)
@@ -630,7 +793,7 @@ internal interface AppDaoInt : AppDao {
@Transaction
@Query(
"""SELECT repoId, packageName, app.localizedName, app.localizedSummary, app.lastUpdated,
- version.antiFeatures, app.isCompatible, app.preferredSigner
+ categories, version.antiFeatures, app.isCompatible, app.preferredSigner
FROM ${AppMetadata.TABLE} AS app
LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName)
WHERE authorName = :authorName AND app.rowid IN (
@@ -700,6 +863,20 @@ internal interface AppDaoInt : AppDao {
}
}
+ @Transaction
+ @Query(
+ """
+ SELECT repoId, packageName, app.lastUpdated, app.name, app.summary,
+ app.description, app.authorName, app.categories,
+ matchinfo(${AppMetadataFts.TABLE}, 'pcx')
+ FROM ${AppMetadata.TABLE} AS app
+ JOIN PreferredRepo USING (packageName)
+ JOIN ${AppMetadataFts.TABLE} USING (repoId, packageName)
+ WHERE ${AppMetadataFts.TABLE} MATCH :searchQuery AND
+ repoId = preferredRepoId"""
+ )
+ override suspend fun getAppSearchItems(searchQuery: String): List
+
//
// Misc Queries
//
diff --git a/libs/database/src/main/java/org/fdroid/database/AppSearchItem.kt b/libs/database/src/main/java/org/fdroid/database/AppSearchItem.kt
new file mode 100644
index 000000000..1c3c81a46
--- /dev/null
+++ b/libs/database/src/main/java/org/fdroid/database/AppSearchItem.kt
@@ -0,0 +1,98 @@
+package org.fdroid.database
+
+import androidx.core.os.LocaleListCompat
+import androidx.room.ColumnInfo
+import androidx.room.Ignore
+import androidx.room.Relation
+import org.fdroid.LocaleChooser.getBestLocale
+import org.fdroid.index.v2.FileV2
+import org.fdroid.index.v2.LocalizedTextV2
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import kotlin.math.min
+
+@OptIn(ExperimentalUnsignedTypes::class)
+@ConsistentCopyVisibility
+public data class AppSearchItem internal constructor(
+ public val repoId: Long,
+ public val packageName: String,
+ public val lastUpdated: Long,
+ public val name: LocalizedTextV2? = null,
+ public val summary: LocalizedTextV2? = null,
+ public val description: LocalizedTextV2? = null,
+ public val authorName: String? = null,
+ public val categories: List? = null,
+ @Relation(
+ parentColumn = "packageName",
+ entityColumn = "packageName",
+ )
+ internal val localizedIcon: List? = null,
+ @Suppress("ArrayInDataClass")
+ @ColumnInfo("matchinfo(${AppMetadataFts.TABLE}, 'pcx')")
+ internal val matchInfo: ByteArray,
+) : Comparable {
+ public fun getIcon(localeList: LocaleListCompat): FileV2? {
+ return localizedIcon?.filter { icon ->
+ icon.repoId == repoId
+ }?.toLocalizedFileV2().getBestLocale(localeList)
+ }
+
+ @Ignore
+ public val score: Double
+
+ init {
+ val info = matchInfo.toIntArray()
+ val numPhrases = info[0]
+ val numColumns = info[1]
+ val scoreMap = mutableMapOf()
+ for (phrase in 0 until numPhrases) {
+ val offset = 2 + phrase * numColumns * 3
+ // start with 1 below, because we don't care about repoId column
+ for (column in 1 until numColumns) {
+ val numHitsInRow = info[offset + 3 * column]
+ // increase score if this column had a hit
+ if (numHitsInRow > 0) {
+ // each hit in a column only contributes to the score once
+ scoreMap.getOrPut(column) {
+ weights[column] ?: error("No weight for column $column")
+ }
+ }
+ }
+ }
+ val weeksOld = (System.currentTimeMillis() - lastUpdated) / (1000 * 60 * 60 * 24 * 7)
+ val punishment = min(100, weeksOld / 3)
+ score = scoreMap.values.sum().toDouble() - punishment
+ }
+
+ private fun ByteArray.toIntArray(skipSize: Int = 4): IntArray {
+ val intArray = IntArray(size / skipSize)
+ // go through each 4 bytes to turn them into integers
+ (indices step skipSize).forEachIndexed { intIndex, byteIndex ->
+ // we are cutting the first two bytes off, because we don't want to deal with UInt
+ // and expected integers are small enough
+ intArray[intIndex] = ByteBuffer.wrap(this, byteIndex, 4)
+ .order(ByteOrder.LITTLE_ENDIAN)
+ .int
+ }
+ return intArray
+ }
+
+ override fun compareTo(other: AppSearchItem): Int {
+ val scoreComp = score.compareTo(other.score)
+ return if (scoreComp == 0) {
+ lastUpdated.compareTo(other.lastUpdated)
+ } else {
+ scoreComp
+ }
+ }
+}
+
+@Suppress("ktlint:standard:no-multi-spaces")
+private val weights = mapOf(
+ // 0 is repoId which we ignore
+ 1 to 100, // "name"
+ 2 to 50, // "summary"
+ 3 to 25, // "description"
+ 4 to 10, // "authorName"
+ 5 to 5, // "packageName"
+)
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 887a9226b..9a04ce1cf 100644
--- a/libs/database/src/main/java/org/fdroid/database/DbUpdateChecker.kt
+++ b/libs/database/src/main/java/org/fdroid/database/DbUpdateChecker.kt
@@ -14,12 +14,12 @@ public class DbUpdateChecker @JvmOverloads constructor(
db: FDroidDatabase,
private val packageManager: PackageManager,
compatibilityChecker: CompatibilityChecker = CompatibilityCheckerImpl(packageManager),
+ private val updateChecker: UpdateChecker = UpdateChecker(compatibilityChecker),
) {
private val appDao = db.getAppDao() as AppDaoInt
private val versionDao = db.getVersionDao() as VersionDaoInt
private val appPrefsDao = db.getAppPrefsDao() as AppPrefsDaoInt
- private val updateChecker = UpdateChecker(compatibilityChecker)
/**
* Returns a list of apps that can be updated.
@@ -69,6 +69,7 @@ public class DbUpdateChecker @JvmOverloads constructor(
val app = getUpdatableApp(
version = version,
installedVersionCode = getLongVersionCode(packageInfo),
+ installedVersionName = packageInfo.versionName ?: "???", // should never be null
isFromPreferredRepo = preferredRepoId == version.repoId,
)
if (app != null) updatableApps.add(app)
@@ -158,6 +159,7 @@ public class DbUpdateChecker @JvmOverloads constructor(
private fun getUpdatableApp(
version: Version,
installedVersionCode: Long,
+ installedVersionName: String,
isFromPreferredRepo: Boolean,
): UpdatableApp? {
val versionedStrings = versionDao.getVersionedStrings(
@@ -171,6 +173,7 @@ public class DbUpdateChecker @JvmOverloads constructor(
repoId = version.repoId,
packageName = version.packageName,
installedVersionCode = installedVersionCode,
+ installedVersionName = installedVersionName,
update = version.toAppVersion(versionedStrings),
isFromPreferredRepo = isFromPreferredRepo,
hasKnownVulnerability = version.hasKnownVulnerability,
diff --git a/libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt b/libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt
index fc997fbf2..04c98325e 100644
--- a/libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt
+++ b/libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt
@@ -16,7 +16,7 @@ import java.util.concurrent.Callable
// When bumping this version, please make sure to add one (or more) migration(s) below!
// Consider also providing tests for that migration.
// Don't forget to commit the new schema to the git repo as well.
- version = 8,
+ version = 9,
entities = [
// repo
CoreRepository::class,
@@ -50,6 +50,7 @@ import java.util.concurrent.Callable
// 5 to 6 is a manual migration
AutoMigration(6, 7),
AutoMigration(7, 8, CountryCodeMigration::class),
+ // 8 to 9 is a manual migration
// add future migrations above!
],
)
@@ -59,6 +60,8 @@ internal abstract class FDroidDatabaseInt : RoomDatabase(), FDroidDatabase, Clos
abstract override fun getAppDao(): AppDaoInt
abstract override fun getVersionDao(): VersionDaoInt
abstract override fun getAppPrefsDao(): AppPrefsDaoInt
+
+ @Deprecated("Will be removed in future version")
override fun afterLocalesChanged(locales: LocaleListCompat) {
val appDao = getAppDao()
runInTransaction {
@@ -118,6 +121,7 @@ public interface FDroidDatabase {
* Call this after the system [Locale]s have changed.
* If this isn't called, the cached localized app metadata (e.g. name, summary) will be wrong.
*/
+ @Deprecated("Will be removed in future version")
public fun afterLocalesChanged(
locales: LocaleListCompat = getLocales(Resources.getSystem().configuration),
)
diff --git a/libs/database/src/main/java/org/fdroid/database/FDroidDatabaseHolder.kt b/libs/database/src/main/java/org/fdroid/database/FDroidDatabaseHolder.kt
index 373d8990f..87d79b369 100644
--- a/libs/database/src/main/java/org/fdroid/database/FDroidDatabaseHolder.kt
+++ b/libs/database/src/main/java/org/fdroid/database/FDroidDatabaseHolder.kt
@@ -58,7 +58,7 @@ public object FDroidDatabaseHolder {
FDroidDatabaseInt::class.java,
name,
).apply {
- addMigrations(MIGRATION_2_3, MIGRATION_5_6)
+ addMigrations(MIGRATION_2_3, MIGRATION_5_6, MIGRATION_8_9)
// We allow destructive migration (if no real migration was provided),
// so we have the option to nuke the DB in production (if that will ever be needed).
fallbackToDestructiveMigration(false)
diff --git a/libs/database/src/main/java/org/fdroid/database/Migrations.kt b/libs/database/src/main/java/org/fdroid/database/Migrations.kt
index 3afd5fabe..d1481b40e 100644
--- a/libs/database/src/main/java/org/fdroid/database/Migrations.kt
+++ b/libs/database/src/main/java/org/fdroid/database/Migrations.kt
@@ -159,3 +159,66 @@ internal class CountryCodeMigration : AutoMigrationSpec {
}
}
}
+
+/**
+ * The tokenizer of the FTS4 table for the app metadata was modified.
+ * This migration is needed to recreate the FTS table to respect the new tokenizer
+ * and also re-create the triggers to keep the FTS table in sync with the AppMetadata table.
+ */
+internal val MIGRATION_8_9 = object : Migration(8, 9) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL("DROP TABLE `AppMetadataFts`")
+ db.execSQL("DROP TRIGGER IF EXISTS `room_fts_content_sync_AppMetadataFts_BEFORE_UPDATE`")
+ db.execSQL("DROP TRIGGER IF EXISTS `room_fts_content_sync_AppMetadataFts_BEFORE_DELETE`")
+ db.execSQL("DROP TRIGGER IF EXISTS `room_fts_content_sync_AppMetadataFts_AFTER_UPDATE`")
+ db.execSQL("DROP TRIGGER IF EXISTS `room_fts_content_sync_AppMetadataFts_AFTER_INSERT`")
+ // table creation taken from auto-generated code:
+ // build/generated/ksp/debug/kotlin/org/fdroid/database/FDroidDatabaseInt_Impl.kt
+ db.execSQL(
+ """
+ CREATE VIRTUAL TABLE IF NOT EXISTS `AppMetadataFts`
+ USING FTS4(
+ `repoId` INTEGER NOT NULL,
+ `name` TEXT, `summary` TEXT,
+ `description` TEXT,
+ `authorName` TEXT,
+ `packageName` TEXT NOT NULL,
+ tokenize=unicode61 `remove_diacritics=1` `separators=.` `tokenchars=-`,
+ content=`AppMetadata`,
+ notindexed=`repoId`
+ )""".trimIndent()
+ )
+ db.execSQL(
+ """
+ CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_BEFORE_UPDATE
+ BEFORE UPDATE ON `AppMetadata` BEGIN DELETE FROM `AppMetadataFts`
+ WHERE `docid`=OLD.`rowid`; END
+ """.trimIndent()
+ )
+ db.execSQL(
+ """
+ CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_BEFORE_DELETE
+ BEFORE DELETE ON `AppMetadata` BEGIN DELETE FROM `AppMetadataFts`
+ WHERE `docid`=OLD.`rowid`; END
+ """.trimIndent()
+ )
+ db.execSQL(
+ """
+ CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_AFTER_UPDATE
+ AFTER UPDATE ON `AppMetadata` BEGIN INSERT INTO
+ `AppMetadataFts`(`docid`, `repoId`, `name`, `summary`, `description`, `authorName`, `packageName`)
+ VALUES (NEW.`rowid`, NEW.`repoId`, NEW.`name`, NEW.`summary`, NEW.`description`, NEW.`authorName`, NEW.`packageName`)
+ ; END""".trimIndent()
+ )
+ db.execSQL(
+ """
+ CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_AFTER_INSERT
+ AFTER INSERT ON `AppMetadata` BEGIN INSERT INTO
+ `AppMetadataFts`(`docid`, `repoId`, `name`, `summary`, `description`, `authorName`, `packageName`)
+ VALUES (NEW.`rowid`, NEW.`repoId`, NEW.`name`, NEW.`summary`, NEW.`description`, NEW.`authorName`, NEW.`packageName`)
+ ; END""".trimIndent()
+ )
+ // rebuild the FTS table to populate it with the new tokenizer
+ db.execSQL("INSERT INTO AppMetadataFts(AppMetadataFts) VALUES('rebuild')")
+ }
+}
diff --git a/libs/database/src/main/java/org/fdroid/database/Repository.kt b/libs/database/src/main/java/org/fdroid/database/Repository.kt
index 0c903fa92..a13100866 100644
--- a/libs/database/src/main/java/org/fdroid/database/Repository.kt
+++ b/libs/database/src/main/java/org/fdroid/database/Repository.kt
@@ -1,8 +1,6 @@
package org.fdroid.database
-import android.util.Log
import androidx.annotation.WorkerThread
-import androidx.core.net.toUri
import androidx.core.os.LocaleListCompat
import androidx.room.Embedded
import androidx.room.Entity
@@ -229,19 +227,14 @@ public data class Repository internal constructor(
}
val shareUri: String
- @WorkerThread
+ @WorkerThread // because fingerprint creation can take time
get() {
- var uri = address.toUri()
- try {
- uri = uri.buildUpon().appendQueryParameter("fingerprint", fingerprint).build()
- } catch (e: UnsupportedOperationException) {
- Log.e(TAG, "Failed to append fingerprint to URI: $e")
- }
- return uri.toString()
+ return "https://fdroid.link/#$address?fingerprint=$fingerprint"
}
}
// Dummy repo to use in Compose Previews and in tests
+@Deprecated("Will be removed in future version")
public val DUMMY_TEST_REPO: Repository = Repository(
repoId = 1L,
address = "https://example.com/fdroid/repo",
diff --git a/libs/database/src/main/java/org/fdroid/database/Version.kt b/libs/database/src/main/java/org/fdroid/database/Version.kt
index 9cce8ebfe..d8866289b 100644
--- a/libs/database/src/main/java/org/fdroid/database/Version.kt
+++ b/libs/database/src/main/java/org/fdroid/database/Version.kt
@@ -5,6 +5,7 @@ import androidx.room.DatabaseView
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
+import androidx.room.Ignore
import androidx.room.Relation
import org.fdroid.LocaleChooser.getBestLocale
import org.fdroid.database.VersionedStringType.PERMISSION
@@ -40,7 +41,7 @@ internal data class Version(
val repoId: Long,
val packageName: String,
val versionId: String,
- val added: Long,
+ override val added: Long,
@Embedded(prefix = "file_") val file: FileV1,
@Embedded(prefix = "src_") val src: FileV2? = null,
@Embedded(prefix = "manifest_") val manifest: AppManifest,
@@ -54,6 +55,8 @@ internal data class Version(
}
override val versionCode: Long get() = manifest.versionCode
+ override val versionName: String get() = manifest.versionName
+ override val size: Long? get() = file.size
override val signer: SignerV2? get() = manifest.signer
override val packageManifest: PackageManifest get() = manifest
override val hasKnownVulnerability: Boolean
@@ -95,10 +98,11 @@ public data class AppVersion internal constructor(
entityColumn = "versionId",
)
internal val versionedStrings: List?,
-) {
+) : PackageVersion {
public val repoId: Long get() = version.repoId
public val packageName: String get() = version.packageName
- public val added: Long get() = version.added
+ public override val added: Long get() = version.added
+ override val size: Long? get() = version.file.size
public val isCompatible: Boolean get() = version.isCompatible
public val manifest: AppManifest get() = version.manifest
public val file: FileV1 get() = version.file
@@ -109,7 +113,22 @@ public data class AppVersion internal constructor(
get() = versionedStrings?.getPermissionsSdk23(version) ?: emptyList()
public val featureNames: List get() = version.manifest.features ?: emptyList()
public val nativeCode: List get() = version.manifest.nativecode ?: emptyList()
- public val releaseChannels: List get() = version.releaseChannels ?: emptyList()
+ public override val releaseChannels: List get() = version.releaseChannels ?: emptyList()
+
+ @get:Ignore
+ public override val versionCode: Long get() = version.manifest.versionCode
+
+ @get:Ignore
+ override val versionName: String get() = version.manifest.versionName
+
+ @get:Ignore
+ public override val signer: SignerV2? get() = version.manifest.signer
+
+ @Ignore
+ public override val packageManifest: PackageManifest = version.manifest
+
+ @Ignore
+ public override val hasKnownVulnerability: Boolean = version.hasKnownVulnerability
public val antiFeatureKeys: List
get() = version.antiFeatures?.map { it.key } ?: emptyList()
diff --git a/libs/database/src/main/java/org/fdroid/index/IndexUpdater.kt b/libs/database/src/main/java/org/fdroid/index/IndexUpdater.kt
index 1695b550e..506340bb9 100644
--- a/libs/database/src/main/java/org/fdroid/index/IndexUpdater.kt
+++ b/libs/database/src/main/java/org/fdroid/index/IndexUpdater.kt
@@ -16,7 +16,7 @@ public sealed class IndexUpdateResult {
public object Unchanged : IndexUpdateResult()
public object Processed : IndexUpdateResult()
public object NotFound : IndexUpdateResult()
- public class Error(public val e: Exception) : IndexUpdateResult()
+ public data class Error(public val e: Exception) : IndexUpdateResult()
}
public interface IndexUpdateListener {
@@ -41,7 +41,7 @@ internal val defaultRepoUriBuilder = RepoUriBuilder { repo, pathElements ->
public fun interface TempFileProvider {
@Throws(IOException::class)
- public fun createTempFile(): File
+ public fun createTempFile(sha256: String?): File
}
/**
diff --git a/libs/database/src/main/java/org/fdroid/index/RepoManager.kt b/libs/database/src/main/java/org/fdroid/index/RepoManager.kt
index 60537fd48..1c47a150a 100644
--- a/libs/database/src/main/java/org/fdroid/index/RepoManager.kt
+++ b/libs/database/src/main/java/org/fdroid/index/RepoManager.kt
@@ -13,8 +13,11 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import org.fdroid.CompatibilityChecker
+import org.fdroid.CompatibilityCheckerImpl
import org.fdroid.database.AppPrefs
import org.fdroid.database.AppPrefsDaoInt
import org.fdroid.database.FDroidDatabase
@@ -28,7 +31,6 @@ import org.fdroid.repo.RepoAdder
import org.fdroid.repo.RepoUriGetter
import java.io.File
import java.net.Proxy
-import java.util.concurrent.CancellationException
import java.util.concurrent.CountDownLatch
import kotlin.coroutines.CoroutineContext
@@ -39,12 +41,20 @@ public class RepoManager @JvmOverloads constructor(
downloaderFactory: DownloaderFactory,
httpManager: HttpManager,
repoUriBuilder: RepoUriBuilder = defaultRepoUriBuilder,
+ compatibilityChecker: CompatibilityChecker = CompatibilityCheckerImpl(
+ packageManager = context.packageManager,
+ forceTouchApps = false,
+ ),
private val coroutineContext: CoroutineContext = Dispatchers.IO,
) {
private val repositoryDao = db.getRepositoryDao() as RepositoryDaoInt
private val appPrefsDao = db.getAppPrefsDao() as AppPrefsDaoInt
- private val tempFileProvider = TempFileProvider {
- File.createTempFile("dl-", "", context.cacheDir)
+ private val tempFileProvider = TempFileProvider { sha256 ->
+ if (sha256 == null) {
+ File.createTempFile("dl-", "", context.cacheDir)
+ } else {
+ File(context.cacheDir, sha256).apply { createNewFile() }
+ }
}
private val repoAdder = RepoAdder(
context = context,
@@ -52,6 +62,7 @@ public class RepoManager @JvmOverloads constructor(
tempFileProvider = tempFileProvider,
downloaderFactory = downloaderFactory,
httpManager = httpManager,
+ compatibilityChecker = compatibilityChecker,
repoUriBuilder = repoUriBuilder,
coroutineContext = coroutineContext,
)
@@ -134,6 +145,14 @@ public class RepoManager @JvmOverloads constructor(
*/
@WorkerThread
public fun deleteRepository(repoId: Long) {
+ // while this will get updated automatically, getting the update may be slow,
+ // so to speed up the UI, we emit the state change right away (deletion is unlikely to fail)
+ _repositoriesState.update {
+ _repositoriesState.value.filter { repo ->
+ // keep only repos that are not the deleted one
+ repo.repoId != repoId
+ }
+ }
db.runInTransaction {
// find and remove archive repo if existing
val repository = repositoryDao.getRepository(repoId) ?: return@runInTransaction
@@ -141,11 +160,6 @@ public class RepoManager @JvmOverloads constructor(
if (archiveRepoId != null) repositoryDao.deleteRepository(archiveRepoId)
// delete main repo
repositoryDao.deleteRepository(repoId)
- // while this gets updated automatically, getting the update may be slow,
- // so to speed up the UI, we emit the state change right away
- _repositoriesState.value = _repositoriesState.value.filter { repo ->
- repo.repoId == repoId
- }
}
}
@@ -240,11 +254,7 @@ public class RepoManager @JvmOverloads constructor(
var archiveRepoId = repositoryDao.getArchiveRepoId(cert)
if (enabled) {
if (archiveRepoId == null) {
- try {
- archiveRepoId = repoAdder.addArchiveRepo(repository, proxy)
- } catch (e: CancellationException) {
- if (e.message != "expected") throw e
- }
+ archiveRepoId = repoAdder.addArchiveRepo(repository, proxy)
} else {
repositoryDao.setRepositoryEnabled(archiveRepoId, true)
}
diff --git a/libs/database/src/main/java/org/fdroid/index/RepoUpdater.kt b/libs/database/src/main/java/org/fdroid/index/RepoUpdater.kt
index 926824934..858f4493d 100644
--- a/libs/database/src/main/java/org/fdroid/index/RepoUpdater.kt
+++ b/libs/database/src/main/java/org/fdroid/index/RepoUpdater.kt
@@ -20,11 +20,15 @@ public class RepoUpdater(
downloaderFactory: DownloaderFactory,
repoUriBuilder: RepoUriBuilder = defaultRepoUriBuilder,
compatibilityChecker: CompatibilityChecker,
- listener: IndexUpdateListener,
+ listener: IndexUpdateListener? = null,
) {
private val log = KotlinLogging.logger {}
- private val tempFileProvider = TempFileProvider {
- File.createTempFile("dl-", "", tempDir)
+ private val tempFileProvider = TempFileProvider { sha256 ->
+ if (sha256 == null) {
+ File.createTempFile("dl-", "", tempDir)
+ } else {
+ File(tempDir, sha256).apply { createNewFile() }
+ }
}
/**
diff --git a/libs/database/src/main/java/org/fdroid/index/v1/IndexV1Updater.kt b/libs/database/src/main/java/org/fdroid/index/v1/IndexV1Updater.kt
index ed4f822ff..505022730 100644
--- a/libs/database/src/main/java/org/fdroid/index/v1/IndexV1Updater.kt
+++ b/libs/database/src/main/java/org/fdroid/index/v1/IndexV1Updater.kt
@@ -42,7 +42,7 @@ public class IndexV1Updater(
if (repo.formatVersion != null && repo.formatVersion != ONE) {
log.error { "Format downgrade for ${repo.address}" }
}
- val file = tempFileProvider.createTempFile()
+ val file = tempFileProvider.createTempFile(null)
val downloader = downloaderFactory.createWithTryFirstMirror(
repo = repo,
uri = repoUriBuilder.getUri(repo, SIGNED_FILE_NAME),
diff --git a/libs/database/src/main/java/org/fdroid/index/v2/IndexV2Updater.kt b/libs/database/src/main/java/org/fdroid/index/v2/IndexV2Updater.kt
index c17154084..87d10f022 100644
--- a/libs/database/src/main/java/org/fdroid/index/v2/IndexV2Updater.kt
+++ b/libs/database/src/main/java/org/fdroid/index/v2/IndexV2Updater.kt
@@ -54,7 +54,7 @@ public class IndexV2Updater(
}
private fun getCertAndEntry(repo: Repository, certificate: String): Pair {
- val file = tempFileProvider.createTempFile()
+ val file = tempFileProvider.createTempFile(null)
val downloader = downloaderFactory.createWithTryFirstMirror(
repo = repo,
uri = repoUriBuilder.getUri(repo, SIGNED_FILE_NAME),
@@ -80,7 +80,7 @@ public class IndexV2Updater(
repoVersion: Long,
streamProcessor: IndexV2StreamProcessor,
): IndexUpdateResult {
- val file = tempFileProvider.createTempFile()
+ val file = tempFileProvider.createTempFile(entryFile.sha256)
val downloader = downloaderFactory.createWithTryFirstMirror(
repo = repo,
uri = repoUriBuilder.getUri(repo, entryFile.name.trimStart('/')),
diff --git a/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt b/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt
index 6b9cebf1a..3f9c601c6 100644
--- a/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt
+++ b/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt
@@ -10,16 +10,21 @@ import androidx.annotation.AnyThread
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import androidx.core.content.ContextCompat.getSystemService
+import androidx.core.net.toUri
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancel
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.getAndUpdate
+import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.SerializationException
import mu.KotlinLogging
+import org.fdroid.CompatibilityChecker
import org.fdroid.database.AppOverviewItem
import org.fdroid.database.FDroidDatabase
import org.fdroid.database.MinimalApp
@@ -31,6 +36,8 @@ import org.fdroid.download.HttpManager
import org.fdroid.download.HttpManager.Companion.isInvalidHttpUrl
import org.fdroid.download.NotFoundException
import org.fdroid.index.IndexFormatVersion
+import org.fdroid.index.IndexUpdateResult
+import org.fdroid.index.RepoUpdater
import org.fdroid.index.RepoUriBuilder
import org.fdroid.index.SigningException
import org.fdroid.index.TempFileProvider
@@ -39,6 +46,7 @@ import org.fdroid.repo.AddRepoError.ErrorType.INVALID_INDEX
import org.fdroid.repo.AddRepoError.ErrorType.IO_ERROR
import org.fdroid.repo.AddRepoError.ErrorType.IS_ARCHIVE_REPO
import org.fdroid.repo.AddRepoError.ErrorType.UNKNOWN_SOURCES_DISALLOWED
+import java.io.File
import java.io.IOException
import java.net.Proxy
import kotlin.coroutines.CoroutineContext
@@ -54,11 +62,12 @@ public class Fetching(
public val receivedRepo: Repository?,
public val apps: List,
public val fetchResult: FetchResult?,
+ public val indexFile: File? = null,
+) : AddRepoState() {
/**
* true if fetching is complete.
*/
- public val done: Boolean = false,
-) : AddRepoState() {
+ public val done: Boolean = indexFile != null
override fun toString(): String {
return "Fetching(fetchUrl=$fetchUrl, repo=${receivedRepo?.address}, apps=${apps.size}, " +
"fetchResult=$fetchResult, done=$done)"
@@ -69,6 +78,7 @@ public object Adding : AddRepoState()
public class Added(
public val repo: Repository,
+ public val updateResult: IndexUpdateResult?,
) : AddRepoState()
public data class AddRepoError(
@@ -100,6 +110,7 @@ internal class RepoAdder(
private val tempFileProvider: TempFileProvider,
private val downloaderFactory: DownloaderFactory,
private val httpManager: HttpManager,
+ private val compatibilityChecker: CompatibilityChecker,
private val repoUriGetter: RepoUriGetter = RepoUriGetter,
private val repoUriBuilder: RepoUriBuilder = defaultRepoUriBuilder,
private val coroutineContext: CoroutineContext = Dispatchers.IO,
@@ -158,46 +169,60 @@ internal class RepoAdder(
)
}
fetchResult = getFetchResult(fetchUrl, repo)
+ coroutineContext.ensureActive() // ensure active before updating state
addRepoState.value = Fetching(fetchUrl, receivedRepo, apps.toList(), fetchResult)
}
override fun onAppReceived(app: AppOverviewItem) {
apps.add(app)
+ coroutineContext.ensureActive() // ensure active before updating state
addRepoState.value = Fetching(fetchUrl, receivedRepo, apps.toList(), fetchResult)
}
}
// set a state early, so the ui can show progress animation
+ coroutineContext.ensureActive() // ensure active before updating state
addRepoState.value = Fetching(fetchUrl, receivedRepo, apps, fetchResult)
// try fetching repo with v2 format first and fallback to v1
- try {
+ val indexFile = try {
fetchRepo(nUri.uri, nUri.fingerprint, proxy, nUri.username, nUri.password, receiver)
} catch (e: SigningException) {
log.error(e) { "Error verifying repo with given fingerprint." }
- addRepoState.value = AddRepoError(INVALID_FINGERPRINT, e)
+ onError(AddRepoError(INVALID_FINGERPRINT, e))
return
} catch (e: IOException) {
log.error(e) { "Error fetching repo." }
- addRepoState.value = AddRepoError(IO_ERROR, e)
+ onError(AddRepoError(IO_ERROR, e))
return
} catch (e: SerializationException) {
log.error(e) { "Error fetching repo." }
- addRepoState.value = AddRepoError(INVALID_INDEX, e)
+ onError(AddRepoError(INVALID_INDEX, e))
return
} catch (e: NotFoundException) { // v1 repos can also have 404
log.error(e) { "Error fetching repo." }
- addRepoState.value = AddRepoError(INVALID_INDEX, e)
+ onError(AddRepoError(INVALID_INDEX, e))
return
}
// set final result
val finalRepo = receivedRepo
+ coroutineContext.ensureActive() // ensure active before updating state
if (finalRepo == null) {
- addRepoState.value = AddRepoError(INVALID_INDEX)
+ onError(AddRepoError(INVALID_INDEX))
} else {
- addRepoState.value = Fetching(fetchUrl, finalRepo, apps, fetchResult, done = true)
+ addRepoState.value = Fetching(fetchUrl, finalRepo, apps, fetchResult, indexFile)
}
}
+ private suspend fun onError(state: AddRepoError) {
+ if (currentCoroutineContext().isActive) {
+ addRepoState.value = state
+ }
+ }
+
+ /**
+ * Fetches the repo from the given [uri] and posts updates to [receiver].
+ * @return the temporary file the repo was written to.
+ */
private suspend fun fetchRepo(
uri: Uri,
fingerprint: String?,
@@ -205,10 +230,9 @@ internal class RepoAdder(
username: String?,
password: String?,
receiver: RepoPreviewReceiver,
- ) {
- try {
- val repo =
- getTempRepo(uri, IndexFormatVersion.TWO, username, password)
+ ): File {
+ return try {
+ val repo = getTempRepo(uri, IndexFormatVersion.TWO, username, password)
val repoFetcher = RepoV2Fetcher(
tempFileProvider, downloaderFactory, httpManager, repoUriBuilder, proxy
)
@@ -216,8 +240,7 @@ internal class RepoAdder(
} catch (e: NotFoundException) {
log.warn(e) { "Did not find v2 repo, trying v1 now." }
// try to fetch v1 repo
- val repo =
- getTempRepo(uri, IndexFormatVersion.ONE, username, password)
+ val repo = getTempRepo(uri, IndexFormatVersion.ONE, username, password)
val repoFetcher = RepoV1Fetcher(tempFileProvider, downloaderFactory, repoUriBuilder)
repoFetcher.fetchRepo(uri, repo, receiver, fingerprint)
}
@@ -226,7 +249,7 @@ internal class RepoAdder(
private fun getFetchResult(fetchUrlIn: String, fetchedRepo: Repository): FetchResult {
// Note the delicate difference between fetchedRepo (from the network) and
// existingRepo (from the database) in this function!
- val cert = fetchedRepo.certificate ?: error("Certificate was null")
+ val cert = fetchedRepo.certificate
val existingRepo = repositoryDao.getRepository(cert)
val fetchUrl = fetchUrlIn.trimEnd('/')
@@ -262,22 +285,24 @@ internal class RepoAdder(
@WorkerThread
internal fun addFetchedRepository(): Repository? {
- // prevent double calls (e.g. caused by double tapping a UI button)
- if (addRepoState.compareAndSet(Adding, Adding)) return null
-
- // cancel fetch preview job, so it stops emitting new states
+ // first cancel fetch preview job, so it stops emitting new states,
+ // screwing up the atomicity of getAndUpdate() below.
fetchJob?.cancel()
// get current state before changing it
- val state = (addRepoState.value as? Fetching)
- ?: throw IllegalStateException("Unexpected state: ${addRepoState.value}")
- addRepoState.value = Adding
+ // prevent double calls (e.g. caused by double tapping a UI button)
+ val state = addRepoState.getAndUpdate {
+ log.info { "Previous state was $it" }
+ Adding
+ } as? Fetching ?: error("Unexpected previous state")
+ log.info { "Moved to state ${addRepoState.value}, cancelling preview job..." }
val repo = state.receivedRepo
?: throw IllegalStateException("No repo: ${addRepoState.value}")
val fetchResult = state.fetchResult
?: throw IllegalStateException("No fetchResult: ${addRepoState.value}")
+ var indexUpdateResult: IndexUpdateResult? = null
val modifiedRepo: Repository = when (fetchResult) {
is FetchResult.IsExistingRepository -> error("Repo exists: $fetchResult")
is FetchResult.IsExistingMirror -> error("Mirror exists: $fetchResult")
@@ -289,7 +314,7 @@ internal class RepoAdder(
icon = repo.repository.icon ?: emptyMap(),
address = repo.address,
formatVersion = repo.formatVersion,
- certificate = repo.certificate ?: error("Repo had no certificate"),
+ certificate = repo.certificate,
username = repo.username,
password = repo.password,
)
@@ -305,6 +330,17 @@ internal class RepoAdder(
repositoryDao.updateUserMirrors(repoId, userMirrors)
}
repositoryDao.getRepository(repoId) ?: error("New repository not found in DB")
+ }.also { repo ->
+ // Update the repo before returning, so we already have its content.
+ // This should pick up [indexFile] automatically without re-downloading,
+ // because we use the sha256 hash as the file name.
+ indexUpdateResult = RepoUpdater(
+ tempDir = context.cacheDir,
+ db = db,
+ downloaderFactory = downloaderFactory,
+ compatibilityChecker = compatibilityChecker,
+ ).update(repo)
+ log.info { "Updated repo: $indexUpdateResult" }
}
}
@@ -321,11 +357,14 @@ internal class RepoAdder(
}
}
}
- addRepoState.value = Added(modifiedRepo)
+ log.info { "Added repository" }
+ state.indexFile?.delete()
+ addRepoState.value = Added(modifiedRepo, indexUpdateResult)
return modifiedRepo
}
internal fun abortAddingRepo() {
+ (addRepoState.value as? Fetching)?.indexFile?.delete()
addRepoState.value = None
fetchJob?.cancel()
}
@@ -348,7 +387,7 @@ internal class RepoAdder(
icon = archiveRepo.repository.icon ?: emptyMap(),
address = archiveRepo.address,
formatVersion = archiveRepo.formatVersion,
- certificate = archiveRepo.certificate ?: error("Repo had no certificate"),
+ certificate = archiveRepo.certificate,
username = archiveRepo.username,
password = archiveRepo.password,
)
@@ -357,14 +396,13 @@ internal class RepoAdder(
repositoryDao.setWeight(repoId, repo.weight - 1)
archiveRepoId = repoId
}
- cancel("expected") // no need to continue downloading the entire repo
}
override fun onAppReceived(app: AppOverviewItem) {
// no-op
}
}
- val uri = Uri.parse(address)
+ val uri = address.toUri()
fetchRepo(uri, repo.fingerprint, proxy, repo.username, repo.password, receiver)
return@withContext archiveRepoId
}
@@ -398,7 +436,7 @@ internal class RepoAdder(
}
internal val defaultRepoUriBuilder = RepoUriBuilder { repo, pathElements ->
- val builder = Uri.parse(repo.address).buildUpon()
+ val builder = repo.address.toUri().buildUpon()
pathElements.forEach { builder.appendEncodedPath(it) }
builder.build()
}
diff --git a/libs/database/src/main/java/org/fdroid/repo/RepoFetcher.kt b/libs/database/src/main/java/org/fdroid/repo/RepoFetcher.kt
index e8c244c02..eb47ae5e6 100644
--- a/libs/database/src/main/java/org/fdroid/repo/RepoFetcher.kt
+++ b/libs/database/src/main/java/org/fdroid/repo/RepoFetcher.kt
@@ -6,9 +6,15 @@ import org.fdroid.database.AppOverviewItem
import org.fdroid.database.Repository
import org.fdroid.download.NotFoundException
import org.fdroid.index.SigningException
+import java.io.File
import java.io.IOException
internal fun interface RepoFetcher {
+ /**
+ * Fetches the repo from the given [uri] and posts updates to [receiver].
+ * @return the temporary file the repo was written to.
+ * Note that the OS may delete this at any time.
+ */
@Throws(
IOException::class,
SigningException::class,
@@ -20,7 +26,7 @@ internal fun interface RepoFetcher {
repo: Repository,
receiver: RepoPreviewReceiver,
fingerprint: String?,
- )
+ ): File
}
internal interface RepoPreviewReceiver {
diff --git a/libs/database/src/main/java/org/fdroid/repo/RepoV1Fetcher.kt b/libs/database/src/main/java/org/fdroid/repo/RepoV1Fetcher.kt
index ecd7fa8ef..7b5cf50b9 100644
--- a/libs/database/src/main/java/org/fdroid/repo/RepoV1Fetcher.kt
+++ b/libs/database/src/main/java/org/fdroid/repo/RepoV1Fetcher.kt
@@ -15,6 +15,7 @@ import org.fdroid.index.parseV1
import org.fdroid.index.v1.IndexV1Verifier
import org.fdroid.index.v1.SIGNED_FILE_NAME
import org.fdroid.index.v2.FileV2
+import java.io.File
internal class RepoV1Fetcher(
private val tempFileProvider: TempFileProvider,
@@ -30,23 +31,19 @@ internal class RepoV1Fetcher(
repo: Repository,
receiver: RepoPreviewReceiver,
fingerprint: String?,
- ) {
+ ): File {
// download and verify index-v1.jar
- val indexFile = tempFileProvider.createTempFile()
+ val indexFile = tempFileProvider.createTempFile(null)
val entryDownloader = downloaderFactory.create(
repo = repo,
uri = repoUriBuilder.getUri(repo, SIGNED_FILE_NAME),
indexFile = FileV2.fromPath("/$SIGNED_FILE_NAME"),
destFile = indexFile,
)
- val (cert, indexV1) = try {
- entryDownloader.download()
- val verifier = IndexV1Verifier(indexFile, null, fingerprint)
- verifier.getStreamAndVerify { inputStream ->
- IndexParser.parseV1(inputStream)
- }
- } finally {
- indexFile.delete()
+ entryDownloader.download()
+ val verifier = IndexV1Verifier(indexFile, null, fingerprint)
+ val (cert, indexV1) = verifier.getStreamAndVerify { inputStream ->
+ IndexParser.parseV1(inputStream)
}
val version = indexV1.repo.version
val indexV2 = IndexConverter().toIndexV2(indexV1)
@@ -63,5 +60,6 @@ internal class RepoV1Fetcher(
val app = RepoV2StreamReceiver.getAppOverViewItem(packageName, packageV2, locales)
receiver.onAppReceived(app)
}
+ return indexFile
}
}
diff --git a/libs/database/src/main/java/org/fdroid/repo/RepoV2Fetcher.kt b/libs/database/src/main/java/org/fdroid/repo/RepoV2Fetcher.kt
index ed18010e7..eaf129e72 100644
--- a/libs/database/src/main/java/org/fdroid/repo/RepoV2Fetcher.kt
+++ b/libs/database/src/main/java/org/fdroid/repo/RepoV2Fetcher.kt
@@ -17,6 +17,7 @@ import org.fdroid.index.v2.EntryVerifier
import org.fdroid.index.v2.FileV2
import org.fdroid.index.v2.IndexV2FullStreamProcessor
import org.fdroid.index.v2.SIGNED_FILE_NAME
+import java.io.File
import java.net.Proxy
import java.security.DigestInputStream
import java.security.MessageDigest
@@ -36,9 +37,9 @@ internal class RepoV2Fetcher(
repo: Repository,
receiver: RepoPreviewReceiver,
fingerprint: String?,
- ) {
+ ): File {
// download and verify entry
- val entryFile = tempFileProvider.createTempFile()
+ val entryFile = tempFileProvider.createTempFile(null)
val entryDownloader = downloaderFactory.create(
repo = repo,
uri = repoUriBuilder.getUri(repo, SIGNED_FILE_NAME),
@@ -59,7 +60,8 @@ internal class RepoV2Fetcher(
val streamReceiver = RepoV2StreamReceiver(receiver, cert, repo.username, repo.password)
val streamProcessor = IndexV2FullStreamProcessor(streamReceiver)
- val digestInputStream = if (uri.scheme?.startsWith("http") == true) {
+ val indexFile = tempFileProvider.createTempFile(entry.index.sha256)
+ val inputStream = if (uri.scheme?.startsWith("http") == true) {
// stream index for http(s) downloads
val indexRequest = DownloadRequest(
indexFile = entry.index,
@@ -68,10 +70,11 @@ internal class RepoV2Fetcher(
username = repo.username,
password = repo.password,
)
- httpManager.getDigestInputStream(indexRequest)
+ val digestInputStream = httpManager.getDigestInputStream(indexRequest)
+ // wrap stream to exfiltrate index file for later usage
+ SavingInputStream(digestInputStream, indexFile)
} else {
// no streaming supported, download file first
- val indexFile = tempFileProvider.createTempFile()
val indexDownloader = downloaderFactory.create(
repo = repo,
uri = repoUriBuilder.getUri(repo, entry.index.name.trimStart('/')),
@@ -82,12 +85,17 @@ internal class RepoV2Fetcher(
val digest = MessageDigest.getInstance("SHA-256")
DigestInputStream(indexFile.inputStream(), digest)
}
- digestInputStream.use { inputStream ->
+ inputStream.use { inputStream ->
streamProcessor.process(entry.version, inputStream) { }
}
- val hexDigest = digestInputStream.getDigestHex()
+ val hexDigest = when (inputStream) {
+ is DigestInputStream -> inputStream.getDigestHex()
+ is SavingInputStream -> inputStream.inputStream.getDigestHex()
+ else -> error("Unknown InputStream ${inputStream::class.java}")
+ }
if (!hexDigest.equals(entry.index.sha256, ignoreCase = true)) {
throw SigningException("Invalid ${entry.index.name} hash: $hexDigest")
}
+ return indexFile
}
}
diff --git a/libs/database/src/main/java/org/fdroid/repo/RepoV2StreamReceiver.kt b/libs/database/src/main/java/org/fdroid/repo/RepoV2StreamReceiver.kt
index 52c43f107..7003b223d 100644
--- a/libs/database/src/main/java/org/fdroid/repo/RepoV2StreamReceiver.kt
+++ b/libs/database/src/main/java/org/fdroid/repo/RepoV2StreamReceiver.kt
@@ -61,6 +61,8 @@ internal open class RepoV2StreamReceiver(
lastUpdated = p.metadata.lastUpdated,
name = p.metadata.name.getBestLocale(locales),
summary = p.metadata.summary.getBestLocale(locales),
+ internalName = p.metadata.name,
+ internalSummary = p.metadata.summary,
antiFeatures = p.versions.values.lastOrNull()?.antiFeatures,
localizedIcon = p.metadata.icon?.map { (locale, file) ->
LocalizedIcon(
diff --git a/libs/database/src/main/java/org/fdroid/repo/SavingInputStream.kt b/libs/database/src/main/java/org/fdroid/repo/SavingInputStream.kt
new file mode 100644
index 000000000..d3d77038c
--- /dev/null
+++ b/libs/database/src/main/java/org/fdroid/repo/SavingInputStream.kt
@@ -0,0 +1,45 @@
+package org.fdroid.repo
+
+import java.io.File
+import java.io.InputStream
+import java.security.DigestInputStream
+
+internal class SavingInputStream(
+ val inputStream: DigestInputStream,
+ outputFile: File,
+) : InputStream() {
+
+ private val outputStream = outputFile.outputStream()
+
+ override fun read(): Int {
+ val byte = inputStream.read()
+ if (byte != -1) {
+ outputStream.write(byte)
+ }
+ return byte
+ }
+
+ override fun read(b: ByteArray): Int {
+ val bytesRead = inputStream.read(b)
+ if (bytesRead != -1) {
+ outputStream.write(b, 0, bytesRead)
+ }
+ return bytesRead
+ }
+
+ override fun read(b: ByteArray, off: Int, len: Int): Int {
+ val bytesRead = inputStream.read(b, off, len)
+ if (bytesRead != -1) {
+ outputStream.write(b, off, bytesRead)
+ }
+ return bytesRead
+ }
+
+ override fun close() {
+ try {
+ inputStream.close()
+ } finally {
+ outputStream.close()
+ }
+ }
+}
diff --git a/libs/database/src/test/java/org/fdroid/repo/RepoAdderIntegrationTest.kt b/libs/database/src/test/java/org/fdroid/repo/RepoAdderIntegrationTest.kt
index 478470eb8..06bb96b70 100644
--- a/libs/database/src/test/java/org/fdroid/repo/RepoAdderIntegrationTest.kt
+++ b/libs/database/src/test/java/org/fdroid/repo/RepoAdderIntegrationTest.kt
@@ -4,16 +4,21 @@ import androidx.core.os.LocaleListCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import app.cash.turbine.test
+import io.mockk.MockKException
import io.mockk.every
import io.mockk.mockk
+import io.mockk.slot
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
-import org.fdroid.database.FDroidDatabase
+import org.fdroid.CompatibilityChecker
+import org.fdroid.database.FDroidDatabaseInt
import org.fdroid.database.NewRepository
import org.fdroid.database.Repository
import org.fdroid.database.RepositoryDaoInt
import org.fdroid.download.HttpManager
import org.fdroid.download.TestDownloadFactory
+import org.fdroid.index.IndexFormatVersion
+import org.fdroid.index.IndexUpdateResult
import org.fdroid.index.TempFileProvider
import org.fdroid.repo.AddRepoError.ErrorType.INVALID_FINGERPRINT
import org.junit.Assume.assumeTrue
@@ -22,7 +27,9 @@ import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
+import java.util.concurrent.Callable
import kotlin.test.assertEquals
+import kotlin.test.assertIs
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
@@ -34,17 +41,25 @@ internal class RepoAdderIntegrationTest {
var folder: TemporaryFolder = TemporaryFolder()
private val context = InstrumentationRegistry.getInstrumentation().targetContext
- private val db = mockk()
+ private val db = mockk()
private val repoDao = mockk()
private val tempFileProvider = TempFileProvider { folder.newFile() }
private val httpManager = HttpManager("test")
private val downloaderFactory = TestDownloadFactory(httpManager)
+ private val compatibilityChecker = mockk()
private val repoAdder: RepoAdder
init {
every { db.getRepositoryDao() } returns repoDao
- repoAdder = RepoAdder(context, db, tempFileProvider, downloaderFactory, httpManager)
+ repoAdder = RepoAdder(
+ context = context,
+ db = db,
+ tempFileProvider = tempFileProvider,
+ downloaderFactory = downloaderFactory,
+ httpManager = httpManager,
+ compatibilityChecker = compatibilityChecker,
+ )
}
@Before
@@ -80,7 +95,9 @@ internal class RepoAdderIntegrationTest {
assertEquals(1, (awaitItem() as Fetching).apps.size)
assertEquals(2, (awaitItem() as Fetching).apps.size)
assertEquals(3, (awaitItem() as Fetching).apps.size)
- assertTrue(awaitItem() is Fetching)
+ assertEquals(4, (awaitItem() as Fetching).apps.size)
+ assertEquals(5, (awaitItem() as Fetching).apps.size)
+ assertTrue((awaitItem() as Fetching).done)
}
val state = repoAdder.addRepoState.value
@@ -90,7 +107,12 @@ internal class RepoAdderIntegrationTest {
println(" ${app.packageName} ${app.summary}")
}
+ val runSlot = slot>()
+ every { db.runInTransaction(capture(runSlot)) } answers {
+ runSlot.captured.call()
+ }
val newRepo: Repository = mockk()
+ every { newRepo.formatVersion } returns IndexFormatVersion.TWO
every { repoDao.insert(any()) } returns 42L
every { repoDao.getRepository(42L) } returns newRepo
@@ -99,6 +121,10 @@ internal class RepoAdderIntegrationTest {
val addedState = awaitItem()
assertTrue(addedState is Added, addedState.toString())
assertEquals(newRepo, addedState.repo)
+ // we are not mocking all the actual repo adding,
+ // so just assert that this fails due to mocking
+ assertIs(addedState.updateResult)
+ assertIs(addedState.updateResult.e)
}
}
diff --git a/libs/database/src/test/java/org/fdroid/repo/RepoAdderTest.kt b/libs/database/src/test/java/org/fdroid/repo/RepoAdderTest.kt
index ce3422baa..8f547c042 100644
--- a/libs/database/src/test/java/org/fdroid/repo/RepoAdderTest.kt
+++ b/libs/database/src/test/java/org/fdroid/repo/RepoAdderTest.kt
@@ -8,7 +8,9 @@ import android.os.UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES
import androidx.core.os.LocaleListCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
+import app.cash.turbine.TurbineTestContext
import app.cash.turbine.test
+import io.mockk.MockKException
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.every
@@ -21,8 +23,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
+import org.fdroid.CompatibilityChecker
import org.fdroid.LocaleChooser.getBestLocale
-import org.fdroid.database.FDroidDatabase
+import org.fdroid.database.FDroidDatabaseInt
import org.fdroid.database.Mirror
import org.fdroid.database.NewRepository
import org.fdroid.database.Repository
@@ -37,6 +40,7 @@ import org.fdroid.download.NotFoundException
import org.fdroid.download.getDigestInputStream
import org.fdroid.index.IndexFormatVersion
import org.fdroid.index.IndexParser.json
+import org.fdroid.index.IndexUpdateResult
import org.fdroid.index.SigningException
import org.fdroid.index.TempFileProvider
import org.fdroid.index.v2.IndexV2
@@ -77,10 +81,11 @@ internal class RepoAdderTest {
var folder: TemporaryFolder = TemporaryFolder()
private val context = InstrumentationRegistry.getInstrumentation().targetContext
- private val db = mockk()
+ private val db = mockk()
private val repoDao = mockk()
private val tempFileProvider = mockk()
private val httpManager = mockk()
+ private val compatibilityChecker = mockk()
private val downloaderFactory = mockk()
private val downloader = mockk()
private val digest = mockk()
@@ -96,14 +101,28 @@ internal class RepoAdderTest {
mockkStatic("org.fdroid.download.HttpManagerKt")
- repoAdder = RepoAdder(context, db, tempFileProvider, downloaderFactory, httpManager)
+ repoAdder = RepoAdder(
+ context = context,
+ db = db,
+ tempFileProvider = tempFileProvider,
+ downloaderFactory = downloaderFactory,
+ httpManager = httpManager,
+ compatibilityChecker = compatibilityChecker,
+ )
}
@Test
fun testDisallowInstallUnknownSources() = runTest {
val context = mockk()
val userManager = mockk()
- val repoAdder = RepoAdder(context, db, tempFileProvider, downloaderFactory, httpManager)
+ val repoAdder = RepoAdder(
+ context,
+ db,
+ tempFileProvider,
+ downloaderFactory,
+ httpManager,
+ compatibilityChecker,
+ )
every { context.getSystemService(UserManager::class.java) } returns userManager
every {
@@ -170,6 +189,8 @@ internal class RepoAdderTest {
val fetching: Fetching = awaitItem() as Fetching // still Fetching from last call
assertEquals(expectedResult, fetching.fetchResult)
+ every { newRepo.formatVersion } returns IndexFormatVersion.TWO
+
repoAdder.addFetchedRepository()
assertIs(awaitItem()) // now moved to Adding
@@ -177,6 +198,10 @@ internal class RepoAdderTest {
val addedState = awaitItem()
assertIs(addedState)
assertEquals(newRepo, addedState.repo)
+ // we are not mocking all the actual repo adding,
+ // so just assert that this fails due to mocking
+ assertIs(addedState.updateResult)
+ assertIs(addedState.updateResult.e)
}
}
@@ -199,6 +224,8 @@ internal class RepoAdderTest {
val fetching: Fetching = awaitItem() as Fetching // still Fetching from last call
assertIs(fetching.fetchResult)
+ every { newRepo.formatVersion } returns IndexFormatVersion.TWO
+
repoAdder.addFetchedRepository()
assertIs(awaitItem()) // now moved to Adding
@@ -206,6 +233,10 @@ internal class RepoAdderTest {
val addedState = awaitItem()
assertIs(addedState)
assertEquals(newRepo, addedState.repo)
+ // we are not mocking all the actual repo adding,
+ // so just assert that this fails due to mocking
+ assertIs(addedState.updateResult)
+ assertIs(addedState.updateResult.e)
}
verify(exactly = 0) {
@@ -409,7 +440,7 @@ internal class RepoAdderTest {
val url = "https://example.org/repo"
val jarFile = folder.newFile()
- every { tempFileProvider.createTempFile() } returns jarFile
+ every { tempFileProvider.createTempFile(any()) } returns jarFile
every {
downloaderFactory.create(
repo = match { it.address == url && it.formatVersion == IndexFormatVersion.TWO },
@@ -445,7 +476,7 @@ internal class RepoAdderTest {
val index = "{ invalid JSON foo bar,".toByteArray()
val indexStream = DigestInputStream(ByteArrayInputStream(index), digest)
- every { tempFileProvider.createTempFile() } returns jarFile
+ every { tempFileProvider.createTempFile(any()) } returns jarFile
every {
downloaderFactory.create(
repo = match {
@@ -496,7 +527,7 @@ internal class RepoAdderTest {
val url = "https://example.org/repo/"
val jarFile = folder.newFile()
- every { tempFileProvider.createTempFile() } returns jarFile
+ every { tempFileProvider.createTempFile(any()) } returns jarFile
every {
downloaderFactory.create(
repo = match {
@@ -563,7 +594,7 @@ internal class RepoAdderTest {
val index = json.encodeToString(IndexV2.serializer(), indexV2).toByteArray()
val indexStream = DigestInputStream(ByteArrayInputStream(index), digest)
- every { tempFileProvider.createTempFile() } returns jarFile
+ every { tempFileProvider.createTempFile(any()) } returns jarFile
every {
downloaderFactory.create(
repo = match {
@@ -623,7 +654,7 @@ internal class RepoAdderTest {
val index = json.encodeToString(IndexV2.serializer(), indexV2).toByteArray()
val indexStream = DigestInputStream(ByteArrayInputStream(index), digest)
- every { tempFileProvider.createTempFile() } returns jarFile
+ every { tempFileProvider.createTempFile(any()) } returns jarFile
every {
downloaderFactory.create(
repo = match {
@@ -686,7 +717,7 @@ internal class RepoAdderTest {
val urlTrimmed = "http://testy.at.or.at/fdroid/repo"
val jarFile = folder.newFile()
- every { tempFileProvider.createTempFile() } returns jarFile
+ every { tempFileProvider.createTempFile(any()) } returns jarFile
every {
downloaderFactory.create(
@@ -743,7 +774,7 @@ internal class RepoAdderTest {
val url = "https://example.org/repo"
val jarFile = folder.newFile()
- every { tempFileProvider.createTempFile() } returns jarFile
+ every { tempFileProvider.createTempFile(any()) } returns jarFile
every {
downloaderFactory.create(
repo = match { it.address == url && it.formatVersion == IndexFormatVersion.TWO },
@@ -820,6 +851,7 @@ internal class RepoAdderTest {
repoAdder.addRepoState.test {
assertIs(awaitItem()) // still Fetching from last call
+ every { newRepo.formatVersion } returns IndexFormatVersion.TWO
repoAdder.addFetchedRepository()
assertIs(awaitItem()) // now moved to Adding
@@ -827,6 +859,10 @@ internal class RepoAdderTest {
val addedState = awaitItem()
assertIs(addedState)
assertEquals(newRepo, addedState.repo)
+ // we are not mocking all the actual repo adding,
+ // so just assert that this fails due to mocking
+ assertIs(addedState.updateResult)
+ assertIs(addedState.updateResult.e)
}
}
@@ -867,7 +903,7 @@ internal class RepoAdderTest {
val indexInputStream = assets.open(indexFile)
val indexDigestStream = DigestInputStream(indexInputStream, digest)
- every { tempFileProvider.createTempFile() } returns jarFile
+ every { tempFileProvider.createTempFile(any()) } returns jarFile
every {
downloaderFactory.create(
repo = match {
@@ -935,7 +971,7 @@ internal class RepoAdderTest {
url,
expectedFetchResult,
TestDataMinV2.repo,
- ) { awaitItem ->
+ ) {
val state = awaitItem()
assertIs(state)
assertEquals(TestDataMinV2.packages.size, state.apps.size)
@@ -954,7 +990,7 @@ internal class RepoAdderTest {
url,
expectedFetchResult,
TestDataMidV2.repo,
- ) { awaitItem ->
+ ) {
val state = awaitItem()
assertIs(state)
assertEquals(1, state.apps.size)
@@ -976,7 +1012,7 @@ internal class RepoAdderTest {
url: String,
expectedFetchResult: FetchResult,
expectedRepo: RepoV2,
- onAppsReceived: suspend (suspend () -> AddRepoState) -> Unit,
+ onAppsReceived: suspend TurbineTestContext.() -> Unit,
) {
repoAdder.addRepoState.test {
assertIs(awaitItem())
@@ -984,7 +1020,7 @@ internal class RepoAdderTest {
launch(Dispatchers.IO) {
// FIXME executing this block may emit items too fast, so we might miss one
// causing flaky tests. A short delay may fix it, let's see.
- delay(250)
+ delay(750)
repoAdder.fetchRepository(url = url, proxy = null)
}
@@ -1008,12 +1044,14 @@ internal class RepoAdderTest {
assertFalse(state2.done)
// onAppReceived (state3)
- onAppsReceived(::awaitItem)
+ onAppsReceived(this)
// final result
val state4 = awaitItem()
assertIs(state4)
assertTrue(state4.done)
+
+ expectNoEvents()
}
}
}
diff --git a/libs/download/api/android/download.api b/libs/download/api/android/download.api
new file mode 100644
index 000000000..2472e03c5
--- /dev/null
+++ b/libs/download/api/android/download.api
@@ -0,0 +1,210 @@
+public abstract interface class org/fdroid/download/BytesReceiver {
+ public abstract fun receive ([BLjava/lang/Long;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public final class org/fdroid/download/DownloadRequest {
+ public fun (Ljava/lang/String;Ljava/util/List;)V
+ public fun (Ljava/lang/String;Ljava/util/List;Ljava/net/Proxy;)V
+ public fun (Ljava/lang/String;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;)V
+ public fun (Ljava/lang/String;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;Ljava/lang/String;)V
+ public fun (Ljava/lang/String;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;Ljava/lang/String;Lorg/fdroid/download/Mirror;)V
+ public synthetic fun (Ljava/lang/String;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;Ljava/lang/String;Lorg/fdroid/download/Mirror;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun (Lorg/fdroid/IndexFile;Ljava/util/List;)V
+ public fun (Lorg/fdroid/IndexFile;Ljava/util/List;Ljava/net/Proxy;)V
+ public fun (Lorg/fdroid/IndexFile;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;)V
+ public fun (Lorg/fdroid/IndexFile;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;Ljava/lang/String;)V
+ public fun (Lorg/fdroid/IndexFile;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;Ljava/lang/String;Lorg/fdroid/download/Mirror;)V
+ public synthetic fun (Lorg/fdroid/IndexFile;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;Ljava/lang/String;Lorg/fdroid/download/Mirror;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Lorg/fdroid/IndexFile;
+ public final fun component2 ()Ljava/util/List;
+ public final fun component3 ()Ljava/net/Proxy;
+ public final fun component4 ()Ljava/lang/String;
+ public final fun component5 ()Ljava/lang/String;
+ public final fun component6 ()Lorg/fdroid/download/Mirror;
+ public final fun copy (Lorg/fdroid/IndexFile;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;Ljava/lang/String;Lorg/fdroid/download/Mirror;)Lorg/fdroid/download/DownloadRequest;
+ public static synthetic fun copy$default (Lorg/fdroid/download/DownloadRequest;Lorg/fdroid/IndexFile;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;Ljava/lang/String;Lorg/fdroid/download/Mirror;ILjava/lang/Object;)Lorg/fdroid/download/DownloadRequest;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getHasCredentials ()Z
+ public final fun getIndexFile ()Lorg/fdroid/IndexFile;
+ public final fun getMirrors ()Ljava/util/List;
+ public final fun getPassword ()Ljava/lang/String;
+ public final fun getProxy ()Ljava/net/Proxy;
+ public final fun getTryFirstMirror ()Lorg/fdroid/download/Mirror;
+ public final fun getUsername ()Ljava/lang/String;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public abstract class org/fdroid/download/Downloader {
+ public static final field Companion Lorg/fdroid/download/Downloader$Companion;
+ protected final field outputFile Ljava/io/File;
+ public fun (Lorg/fdroid/IndexFile;Ljava/io/File;)V
+ public final fun cancelDownload ()V
+ public abstract fun close ()V
+ public abstract fun download ()V
+ protected final fun downloadFromBytesReceiver (ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ protected final fun downloadFromStream (Z)V
+ protected fun getBytes (ZLorg/fdroid/download/BytesReceiver;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public final fun getCacheTag ()Ljava/lang/String;
+ protected final fun getIndexFile ()Lorg/fdroid/IndexFile;
+ protected abstract fun getInputStream (Z)Ljava/io/InputStream;
+ public abstract fun hasChanged ()Z
+ public final fun setCacheTag (Ljava/lang/String;)V
+ public final fun setListener (Lorg/fdroid/fdroid/ProgressListener;)V
+ protected abstract fun totalDownloadSize ()J
+ public final fun wasCancelled ()Z
+}
+
+public final class org/fdroid/download/Downloader$Companion {
+}
+
+public final class org/fdroid/download/HeadInfo {
+ public fun (ZLjava/lang/String;Ljava/lang/Long;Ljava/lang/String;)V
+ public final fun component1 ()Z
+ public final fun component2 ()Ljava/lang/String;
+ public final fun component3 ()Ljava/lang/Long;
+ public final fun component4 ()Ljava/lang/String;
+ public final fun copy (ZLjava/lang/String;Ljava/lang/Long;Ljava/lang/String;)Lorg/fdroid/download/HeadInfo;
+ public static synthetic fun copy$default (Lorg/fdroid/download/HeadInfo;ZLjava/lang/String;Ljava/lang/Long;Ljava/lang/String;ILjava/lang/Object;)Lorg/fdroid/download/HeadInfo;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getContentLength ()Ljava/lang/Long;
+ public final fun getETag ()Ljava/lang/String;
+ public final fun getETagChanged ()Z
+ public final fun getLastModified ()Ljava/lang/String;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final class org/fdroid/download/HttpDownloader : org/fdroid/download/Downloader {
+ public fun (Lorg/fdroid/download/HttpManager;Lorg/fdroid/download/DownloadRequest;Ljava/io/File;)V
+ public fun close ()V
+ public fun download ()V
+ public fun hasChanged ()Z
+}
+
+public final class org/fdroid/download/HttpDownloaderV2 : org/fdroid/download/Downloader {
+ public fun (Lorg/fdroid/download/HttpManager;Lorg/fdroid/download/DownloadRequest;Ljava/io/File;)V
+ public fun close ()V
+ public fun download ()V
+ public fun hasChanged ()Z
+}
+
+public class org/fdroid/download/HttpManager {
+ public static final field Companion Lorg/fdroid/download/HttpManager$Companion;
+ public fun (Ljava/lang/String;)V
+ public fun (Ljava/lang/String;Ljava/lang/String;)V
+ public fun (Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy;)V
+ public fun (Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy;Lokhttp3/Dns;)V
+ public fun (Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy;Lokhttp3/Dns;Lorg/fdroid/download/MirrorParameterManager;)V
+ public fun (Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy;Lokhttp3/Dns;Lorg/fdroid/download/MirrorParameterManager;Z)V
+ public fun (Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy;Lokhttp3/Dns;Lorg/fdroid/download/MirrorParameterManager;ZLorg/fdroid/download/MirrorChooser;)V
+ public fun (Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy;Lokhttp3/Dns;Lorg/fdroid/download/MirrorParameterManager;ZLorg/fdroid/download/MirrorChooser;Lio/ktor/client/engine/HttpClientEngineFactory;)V
+ public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy;Lokhttp3/Dns;Lorg/fdroid/download/MirrorParameterManager;ZLorg/fdroid/download/MirrorChooser;Lio/ktor/client/engine/HttpClientEngineFactory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun get (Lorg/fdroid/download/DownloadRequest;Ljava/lang/Long;Lorg/fdroid/download/BytesReceiver;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public final fun get (Lorg/fdroid/download/DownloadRequest;Lorg/fdroid/download/BytesReceiver;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public static synthetic fun get$default (Lorg/fdroid/download/HttpManager;Lorg/fdroid/download/DownloadRequest;Ljava/lang/Long;Lorg/fdroid/download/BytesReceiver;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+ public final fun head (Lorg/fdroid/download/DownloadRequest;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public static synthetic fun head$default (Lorg/fdroid/download/HttpManager;Lorg/fdroid/download/DownloadRequest;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+ public final fun post (Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public static synthetic fun post$default (Lorg/fdroid/download/HttpManager;Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+}
+
+public final class org/fdroid/download/HttpManager$Companion {
+ public final fun isInvalidHttpUrl (Ljava/lang/String;)Z
+}
+
+public final class org/fdroid/download/HttpManagerKt {
+ public static final fun getDigestInputStream (Lorg/fdroid/download/HttpManager;Lorg/fdroid/download/DownloadRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public static final fun getInputStream (Lorg/fdroid/download/HttpManager;Lorg/fdroid/download/DownloadRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public final class org/fdroid/download/HttpPoster {
+ public fun (Lorg/fdroid/download/HttpManager;Ljava/lang/String;)V
+ public final fun post (Ljava/lang/String;)V
+}
+
+public final class org/fdroid/download/Mirror {
+ public static final field Companion Lorg/fdroid/download/Mirror$Companion;
+ public fun (Ljava/lang/String;)V
+ public fun (Ljava/lang/String;Ljava/lang/String;)V
+ public fun (Ljava/lang/String;Ljava/lang/String;Z)V
+ public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/lang/String;
+ public final fun component2 ()Ljava/lang/String;
+ public final fun component3 ()Z
+ public final fun copy (Ljava/lang/String;Ljava/lang/String;Z)Lorg/fdroid/download/Mirror;
+ public static synthetic fun copy$default (Lorg/fdroid/download/Mirror;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)Lorg/fdroid/download/Mirror;
+ public fun equals (Ljava/lang/Object;)Z
+ public static final fun fromStrings (Ljava/util/List;)Ljava/util/List;
+ public final fun getBaseUrl ()Ljava/lang/String;
+ public final fun getCountryCode ()Ljava/lang/String;
+ public final fun getFDroidLinkUrl (Ljava/lang/String;)Ljava/lang/String;
+ public final fun getUrl ()Lio/ktor/http/Url;
+ public final fun getUrl (Ljava/lang/String;)Lio/ktor/http/Url;
+ public fun hashCode ()I
+ public final fun isHttp ()Z
+ public final fun isIpfsGateway ()Z
+ public final fun isLocal ()Z
+ public final fun isOnion ()Z
+ public fun toString ()Ljava/lang/String;
+}
+
+public final class org/fdroid/download/Mirror$Companion {
+ public final fun fromStrings (Ljava/util/List;)Ljava/util/List;
+}
+
+public abstract interface class org/fdroid/download/MirrorChooser {
+ public abstract fun mirrorRequest (Lorg/fdroid/download/DownloadRequest;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public abstract fun orderMirrors (Lorg/fdroid/download/DownloadRequest;)Ljava/util/List;
+}
+
+public abstract interface class org/fdroid/download/MirrorParameterManager {
+ public abstract fun getCurrentLocation ()Ljava/lang/String;
+ public abstract fun getMirrorErrorCount (Ljava/lang/String;)I
+ public abstract fun incrementMirrorErrorCount (Ljava/lang/String;)V
+ public abstract fun preferForeignMirrors ()Z
+}
+
+public final class org/fdroid/download/NoResumeException : java/lang/Exception {
+ public fun ()V
+}
+
+public final class org/fdroid/download/NotFoundException : java/lang/Exception {
+ public fun ()V
+ public fun (Ljava/lang/Throwable;)V
+ public synthetic fun (Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+}
+
+public final class org/fdroid/download/coil/DownloadRequestFetcher : coil3/fetch/Fetcher {
+ public fun (Lorg/fdroid/download/HttpManager;Lorg/fdroid/download/DownloadRequest;Lcoil3/request/Options;Lkotlin/Lazy;)V
+ public fun fetch (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public final class org/fdroid/download/coil/DownloadRequestFetcher$Factory : coil3/fetch/Fetcher$Factory {
+ public fun (Lorg/fdroid/download/HttpManager;)V
+ public synthetic fun create (Ljava/lang/Object;Lcoil3/request/Options;Lcoil3/ImageLoader;)Lcoil3/fetch/Fetcher;
+ public fun create (Lorg/fdroid/download/DownloadRequest;Lcoil3/request/Options;Lcoil3/ImageLoader;)Lcoil3/fetch/Fetcher;
+}
+
+public final class org/fdroid/download/glide/DownloadRequestLoader : com/bumptech/glide/load/model/ModelLoader {
+ public fun (Lorg/fdroid/download/HttpManager;)V
+ public synthetic fun buildLoadData (Ljava/lang/Object;IILcom/bumptech/glide/load/Options;)Lcom/bumptech/glide/load/model/ModelLoader$LoadData;
+ public fun buildLoadData (Lorg/fdroid/download/DownloadRequest;IILcom/bumptech/glide/load/Options;)Lcom/bumptech/glide/load/model/ModelLoader$LoadData;
+ public synthetic fun handles (Ljava/lang/Object;)Z
+ public fun handles (Lorg/fdroid/download/DownloadRequest;)Z
+}
+
+public final class org/fdroid/download/glide/DownloadRequestLoader$Factory : com/bumptech/glide/load/model/ModelLoaderFactory {
+ public fun (Lorg/fdroid/download/HttpManager;)V
+ public fun build (Lcom/bumptech/glide/load/model/MultiModelLoaderFactory;)Lcom/bumptech/glide/load/model/ModelLoader;
+ public fun teardown ()V
+}
+
+public final class org/fdroid/fdroid/DigestInputStreamKt {
+ public static final fun getDigestHex (Ljava/security/DigestInputStream;)Ljava/lang/String;
+}
+
+public abstract interface class org/fdroid/fdroid/ProgressListener {
+ public abstract fun onProgress (JJ)V
+}
+
diff --git a/libs/download/build.gradle b/libs/download/build.gradle
deleted file mode 100644
index 6de2c6176..000000000
--- a/libs/download/build.gradle
+++ /dev/null
@@ -1,138 +0,0 @@
-plugins {
- id 'org.jetbrains.kotlin.multiplatform'
- id 'com.android.library'
- id 'org.jetbrains.dokka'
- id 'com.vanniktech.maven.publish'
-}
-
-kotlin {
- androidTarget {
- compilations.configureEach {
- kotlinOptions.jvmTarget = '17'
- }
- publishLibraryVariants("release")
- }
-// def hostOs = System.getProperty("os.name")
-// def isMingwX64 = hostOs.startsWith("Windows")
-// def nativeTarget
-// if (hostOs == "Mac OS X") nativeTarget = macosX64('native')
-// else if (hostOs == "Linux") nativeTarget = linuxX64("native")
-// else if (isMingwX64) nativeTarget = mingwX64("native")
-// else throw new GradleException("Host OS is not supported in Kotlin/Native.")
-
- sourceSets {
- configureEach {
- languageSettings {
- optIn('kotlin.RequiresOptIn')
- explicitApi('strict')
- }
- }
- commonMain {
- dependencies {
- api libs.ktor.client.core
- implementation libs.microutils.kotlin.logging
- }
- }
- commonTest {
- dependencies {
- implementation kotlin('test')
- implementation libs.ktor.client.mock
- implementation libs.mockk
- }
- }
- // JVM is disabled for now, because Android app is including it instead of Android library
- jvmMain {
- dependencies {
- implementation libs.ktor.client.cio
- }
- }
- jvmTest {
- dependencies {
- implementation libs.junit
- }
- }
- androidMain {
- dependencies {
- implementation libs.ktor.client.okhttp
- //noinspection UseTomlInstead
- implementation("com.github.bumptech.glide:glide:4.16.0") {
- transitive = false // we don't need all that it pulls in, just the basics
- }
- implementation libs.glide.annotations
- }
- }
- androidUnitTest {
- dependencies {
- implementation kotlin('test')
- implementation libs.json
- implementation libs.junit
- implementation libs.logback.classic
- }
- }
- androidInstrumentedTest {
- dependsOn(commonTest)
- dependencies {
- implementation project(":libs:sharedTest")
- implementation libs.androidx.test.runner
- implementation libs.androidx.test.ext.junit
- implementation libs.mockk.android
- }
- }
- nativeMain {
- dependencies {
- implementation libs.ktor.client.curl
- }
- }
- nativeTest {
-
- }
- }
-}
-
-android {
- namespace "org.fdroid.download"
- compileSdk libs.versions.compileSdk.get().toInteger()
- sourceSets {
- main.manifest.srcFile('src/androidMain/AndroidManifest.xml')
- getByName("androidTest").java.srcDir(file("src/androidAndroidTest/kotlin"))
- }
- defaultConfig {
- minSdkVersion 21
- targetSdkVersion 34 // needed for instrumentation tests
- testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
- testInstrumentationRunnerArguments disableAnalytics: 'true'
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_17
- targetCompatibility JavaVersion.VERSION_17
- }
- lintOptions {
- checkReleaseBuilds false
- abortOnError true
-
- htmlReport true
- xmlReport false
- textReport true
-
- lintConfig file("lint.xml")
- }
- testOptions {
- packaging {
- resources.excludes.add("META-INF/*")
- }
- }
-}
-
-signing {
- useGpgCmd()
-}
-
-import org.jetbrains.dokka.gradle.DokkaTask
-tasks.withType(DokkaTask).configureEach {
- pluginsMapConfiguration.set(
- ["org.jetbrains.dokka.base.DokkaBase": """{
- "customAssets": ["${file("${rootProject.rootDir}/logo-icon.svg")}"],
- "footerMessage": "© 2010-2022 F-Droid Limited and Contributors"
- }"""]
- )
-}
diff --git a/libs/download/build.gradle.kts b/libs/download/build.gradle.kts
new file mode 100644
index 000000000..706e6d1a9
--- /dev/null
+++ b/libs/download/build.gradle.kts
@@ -0,0 +1,124 @@
+plugins {
+ alias(libs.plugins.jetbrains.kotlin.multiplatform)
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.jetbrains.dokka)
+ alias(libs.plugins.vanniktech.maven.publish)
+}
+
+kotlin {
+ androidTarget {
+ compilerOptions {
+ jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
+ }
+ publishLibraryVariants("release")
+ }
+ compilerOptions {
+ optIn.add("kotlin.RequiresOptIn")
+ }
+ explicitApi()
+ @OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class)
+ abiValidation {
+ enabled = true
+ }
+ sourceSets {
+ commonMain {
+ dependencies {
+ api(project(":libs:core"))
+ api(libs.ktor.client.core)
+ implementation(libs.microutils.kotlin.logging)
+ }
+ }
+ commonTest {
+ dependencies {
+ implementation(kotlin("test"))
+ implementation(libs.ktor.client.mock)
+ implementation(libs.mockk)
+ }
+ }
+ // JVM is disabled for now, because Android app is including it instead of Android library
+ jvmMain {
+ dependencies {
+ implementation(libs.ktor.client.cio)
+ }
+ }
+ jvmTest {
+ dependencies {
+ implementation(libs.junit)
+ }
+ }
+ androidMain {
+ dependencies {
+ implementation(libs.ktor.client.okhttp)
+ //noinspection UseTomlInstead
+ implementation("com.github.bumptech.glide:glide:4.16.0") {
+ isTransitive = false // we don't need all that it pulls in, just the basics
+ }
+ implementation(libs.glide.annotations)
+ implementation("io.coil-kt.coil3:coil-core:3.3.0") {
+ isTransitive = false // we don't need all that it pulls in, just the basics
+ }
+ implementation("javax.inject:javax.inject:1")
+ }
+ }
+ androidUnitTest {
+ dependencies {
+ implementation(kotlin("test"))
+ implementation(libs.json)
+ implementation(libs.junit)
+ implementation(libs.logback.classic)
+ }
+ }
+ val commonTest by getting
+ androidInstrumentedTest {
+ dependsOn(commonTest)
+ dependencies {
+ implementation(project(":libs:sharedTest"))
+ implementation(libs.androidx.test.runner)
+ implementation(libs.androidx.test.ext.junit)
+ implementation(libs.mockk.android)
+ }
+ }
+ }
+}
+
+android {
+ namespace = "org.fdroid.download"
+ @Suppress("ktlint:standard:chain-method-continuation")
+ compileSdk = libs.versions.compileSdk.get().toInt()
+ defaultConfig {
+ minSdk = 21
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunnerArguments["disableAnalytics"] = "true"
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ lint {
+ checkReleaseBuilds = false
+ abortOnError = true
+
+ htmlReport = true
+ xmlReport = false
+ textReport = true
+
+ lintConfig = file("lint.xml")
+ }
+ testOptions {
+ targetSdk = 34 // needed for instrumentation tests
+ packaging {
+ resources.excludes.add("META-INF/*")
+ }
+ }
+}
+
+signing {
+ useGpgCmd()
+}
+
+dokka {
+ pluginsConfiguration.html {
+ customAssets.from("${file("${rootProject.rootDir}/logo-icon.svg")}")
+ footerMessage.set("© 2010-2025 F-Droid Limited and Contributors")
+ }
+}
diff --git a/libs/download/gradle.properties b/libs/download/gradle.properties
index 9784f28a8..5827ff7fd 100644
--- a/libs/download/gradle.properties
+++ b/libs/download/gradle.properties
@@ -1,5 +1,5 @@
POM_ARTIFACT_ID=download
-VERSION_NAME=0.1.1
+VERSION_NAME=0.2.0
POM_NAME=F-Droid download library
POM_DESCRIPTION=A Kotlin multi-platform library to download F-Droid related files via HTTP.
diff --git a/libs/download/src/androidMain/kotlin/org/fdroid/download/Downloader.kt b/libs/download/src/androidMain/kotlin/org/fdroid/download/Downloader.kt
index 2faecec70..9244acad2 100644
--- a/libs/download/src/androidMain/kotlin/org/fdroid/download/Downloader.kt
+++ b/libs/download/src/androidMain/kotlin/org/fdroid/download/Downloader.kt
@@ -180,7 +180,7 @@ public abstract class Downloader(
private fun reportProgress(lastTimeReported: Long, bytesRead: Long, bytesTotal: Long): Long {
val now = System.currentTimeMillis()
- return if (now - lastTimeReported > 100) {
+ return if (now - lastTimeReported > 1000) {
log.debug { "onProgress: $bytesRead/$bytesTotal" }
progressListener?.onProgress(bytesRead, bytesTotal)
now
diff --git a/libs/download/src/androidMain/kotlin/org/fdroid/download/HttpDownloaderV2.kt b/libs/download/src/androidMain/kotlin/org/fdroid/download/HttpDownloaderV2.kt
index 03ff9fb2d..fe25c5085 100644
--- a/libs/download/src/androidMain/kotlin/org/fdroid/download/HttpDownloaderV2.kt
+++ b/libs/download/src/androidMain/kotlin/org/fdroid/download/HttpDownloaderV2.kt
@@ -25,9 +25,12 @@ import io.ktor.client.plugins.ResponseException
import io.ktor.http.HttpStatusCode.Companion.NotFound
import kotlinx.coroutines.runBlocking
import mu.KotlinLogging
+import org.fdroid.fdroid.toHex
import java.io.File
import java.io.IOException
import java.io.InputStream
+import java.security.MessageDigest
+import java.security.NoSuchAlgorithmException
/**
* Download files over HTTP, with support for proxies, `.onion` addresses, HTTP Basic Auth, etc.
@@ -62,10 +65,23 @@ public class HttpDownloaderV2 constructor(
var resumable = false
val fileLength = outputFile.length()
if (fileLength > (request.indexFile.size ?: -1)) {
+ // file was larger than expected, so delete and re-download
if (!outputFile.delete()) log.warn { "Warning: outputFile not deleted" }
} else if (fileLength == request.indexFile.size && outputFile.isFile) {
log.debug { "Already have outputFile, not downloading: ${outputFile.name}" }
- return // already have it!
+ if (request.indexFile.sha256 == null) {
+ // no way to check file, so we trust that what we have is legit (v1 only)
+ return
+ } else {
+ if (hashFile(outputFile) == request.indexFile.sha256) {
+ // hash matched, so we already have the good file, don't download again
+ return
+ } else {
+ log.warn { "Hash mismatch for ${request.indexFile}" }
+ // delete file and continue
+ if (!outputFile.delete()) log.warn { "Warning: outputFile not deleted" }
+ }
+ }
} else if (fileLength > 0) {
resumable = true
}
@@ -90,4 +106,21 @@ public class HttpDownloaderV2 constructor(
override fun close() {
}
+ private fun hashFile(file: File): String {
+ val messageDigest: MessageDigest = try {
+ MessageDigest.getInstance("SHA-256")
+ } catch (e: NoSuchAlgorithmException) {
+ throw AssertionError(e)
+ }
+ file.inputStream().use { inputStream ->
+ val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
+ var bytes = inputStream.read(buffer)
+ while (bytes >= 0) {
+ messageDigest.update(buffer, 0, bytes)
+ bytes = inputStream.read(buffer)
+ }
+ }
+ return messageDigest.digest().toHex()
+ }
+
}
diff --git a/libs/download/src/androidMain/kotlin/org/fdroid/download/coil/DownloadRequestFetcher.kt b/libs/download/src/androidMain/kotlin/org/fdroid/download/coil/DownloadRequestFetcher.kt
new file mode 100644
index 000000000..dbf7c1e24
--- /dev/null
+++ b/libs/download/src/androidMain/kotlin/org/fdroid/download/coil/DownloadRequestFetcher.kt
@@ -0,0 +1,150 @@
+/**
+ * Contains disk cache related code from https://github.com/coil-kt/coil
+ * coil-network-core/src/commonMain/kotlin/coil3/network/NetworkFetcher.kt
+ * under Apache-2.0 license.
+ */
+
+package org.fdroid.download.coil
+
+import coil3.ImageLoader
+import coil3.annotation.InternalCoilApi
+import coil3.decode.DataSource
+import coil3.decode.ImageSource
+import coil3.disk.DiskCache
+import coil3.fetch.FetchResult
+import coil3.fetch.Fetcher
+import coil3.fetch.SourceFetchResult
+import coil3.request.Options
+import coil3.util.MimeTypeMap
+import io.ktor.utils.io.jvm.javaio.toInputStream
+import okio.BufferedSource
+import okio.FileSystem
+import okio.buffer
+import okio.source
+import org.fdroid.download.DownloadRequest
+import org.fdroid.download.HttpManager
+import org.fdroid.download.glide.AutoVerifyingInputStream
+import org.fdroid.download.glide.getKey
+import javax.inject.Inject
+
+public class DownloadRequestFetcher(
+ private val httpManager: HttpManager,
+ private val downloadRequest: DownloadRequest,
+ private val options: Options,
+ private val diskCache: Lazy,
+) : Fetcher {
+
+ private val fileSystem: FileSystem
+ get() = diskCache.value?.fileSystem ?: options.fileSystem
+
+ private val diskCacheKey: String
+ get() = options.diskCacheKey ?: downloadRequest.getKey()
+
+ @OptIn(InternalCoilApi::class)
+ private val mimeType: String?
+ get() = MimeTypeMap.getMimeTypeFromUrl(downloadRequest.indexFile.name)
+
+ override suspend fun fetch(): FetchResult? {
+ var snapshot = readFromDiskCache()
+ try {
+ if (snapshot != null) {
+ // we have the request cached, so return it right away
+ return SourceFetchResult(
+ source = snapshot.toImageSource(),
+ mimeType = mimeType,
+ dataSource = DataSource.DISK,
+ )
+ }
+ // TODO use channel directly and auto-verify hash without InputStream wrapper
+ // may need https://github.com/Kotlin/kotlinx-io/blob/master/integration/kotlinx-io-okio/Module.md
+ val inputStream = httpManager.getChannel(downloadRequest).toInputStream()
+ val sha256 = downloadRequest.indexFile.sha256
+ val bufferedSource = if (sha256 == null) {
+ inputStream
+ } else {
+ AutoVerifyingInputStream(inputStream, sha256)
+ }.source().buffer()
+ snapshot = writeToDiskCache(snapshot, bufferedSource)
+ if (snapshot == null) {
+ // we couldn't write the snapshot, so try returning directly
+ return SourceFetchResult(
+ source = ImageSource(
+ source = bufferedSource,
+ fileSystem = FileSystem.SYSTEM,
+ metadata = null,
+ ),
+ mimeType = mimeType,
+ dataSource = DataSource.NETWORK,
+ )
+ }
+ return SourceFetchResult(
+ source = snapshot.toImageSource(),
+ mimeType = mimeType,
+ dataSource = DataSource.NETWORK,
+ )
+ } finally {
+ snapshot?.close()
+ }
+ }
+
+ private fun readFromDiskCache(): DiskCache.Snapshot? {
+ return if (options.diskCachePolicy.readEnabled) {
+ diskCache.value?.openSnapshot(downloadRequest.getKey())
+ } else {
+ null
+ }
+ }
+
+ private fun writeToDiskCache(
+ snapshot: DiskCache.Snapshot?,
+ bufferedSource: BufferedSource,
+ ): DiskCache.Snapshot? {
+ // Short circuit if we're not allowed to cache this response.
+ if (!options.diskCachePolicy.writeEnabled) return null
+
+ // Open a new editor. Return null if we're unable to write to this entry.
+ val editor = if (snapshot != null) {
+ snapshot.closeAndOpenEditor()
+ } else {
+ diskCache.value?.openEditor(diskCacheKey)
+ } ?: return null
+
+ return try {
+ fileSystem.write(editor.data) {
+ writeAll(bufferedSource)
+ }
+ editor.commitAndOpenSnapshot()
+ } catch (e: Exception) {
+ try {
+ editor.abort()
+ } catch (_: Exception) {
+ // ignore
+ }
+ throw e
+ }
+ }
+
+ private fun DiskCache.Snapshot.toImageSource(): ImageSource {
+ return ImageSource(
+ file = data,
+ fileSystem = fileSystem,
+ diskCacheKey = diskCacheKey,
+ closeable = this,
+ )
+ }
+
+ public class Factory @Inject constructor(
+ private val httpManager: HttpManager,
+ ) : Fetcher.Factory {
+ override fun create(
+ data: DownloadRequest,
+ options: Options,
+ imageLoader: ImageLoader,
+ ): Fetcher? = DownloadRequestFetcher(
+ httpManager = httpManager,
+ downloadRequest = data,
+ options = options,
+ diskCache = lazy { imageLoader.diskCache },
+ )
+ }
+}
diff --git a/libs/download/src/androidMain/kotlin/org/fdroid/download/glide/DownloadRequestLoader.kt b/libs/download/src/androidMain/kotlin/org/fdroid/download/glide/DownloadRequestLoader.kt
index f8c25ade0..47c748526 100644
--- a/libs/download/src/androidMain/kotlin/org/fdroid/download/glide/DownloadRequestLoader.kt
+++ b/libs/download/src/androidMain/kotlin/org/fdroid/download/glide/DownloadRequestLoader.kt
@@ -24,7 +24,7 @@ public class DownloadRequestLoader(
height: Int,
options: Options,
): LoadData {
- return LoadData(downloadRequest.getKey(), HttpFetcher(httpManager, downloadRequest))
+ return LoadData(downloadRequest.getObjectKey(), HttpFetcher(httpManager, downloadRequest))
}
public class Factory(
@@ -41,6 +41,10 @@ public class DownloadRequestLoader(
}
-internal fun DownloadRequest.getKey(): ObjectKey {
- return ObjectKey(indexFile.sha256 ?: (mirrors[0].baseUrl + indexFile.name))
+internal fun DownloadRequest.getObjectKey(): ObjectKey {
+ return ObjectKey(getKey())
+}
+
+internal fun DownloadRequest.getKey(): String {
+ return indexFile.sha256 ?: (mirrors[0].baseUrl + indexFile.name)
}
diff --git a/libs/download/src/androidUnitTest/kotlin/org/fdroid/download/HttpDownloaderTest.kt b/libs/download/src/androidUnitTest/kotlin/org/fdroid/download/HttpDownloaderTest.kt
index 97d4262d3..f920b58ea 100644
--- a/libs/download/src/androidUnitTest/kotlin/org/fdroid/download/HttpDownloaderTest.kt
+++ b/libs/download/src/androidUnitTest/kotlin/org/fdroid/download/HttpDownloaderTest.kt
@@ -25,6 +25,7 @@ import org.fdroid.runSuspend
import org.junit.Assume.assumeTrue
import org.junit.Rule
import org.junit.rules.TemporaryFolder
+import java.io.File
import java.io.IOException
import java.net.BindException
import java.net.ServerSocket
@@ -33,6 +34,7 @@ import kotlin.test.Ignore
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
import kotlin.test.fail
@@ -214,6 +216,53 @@ internal class HttpDownloaderTest {
assertContentEquals(firstBytes + secondBytes, file.readBytes())
}
+ /**
+ * Tests re-using an already downloaded file with hash verification.
+ * This can fail if the hashing doesn't take the already downloaded bytes into account.
+ */
+ @Test
+ fun testCompleteResumeWithHashSuccess() = runSuspend {
+ val sha256 = "efabb260da949061c88173c19f369b4aa0eaa82003c7c2dec08b5dfe75525368"
+ val file = File(folder.newFolder(), sha256).apply { createNewFile() }
+ val bytes = ("These are the first bytes that were already downloaded." +
+ "These are the last bytes that still need to be downloaded.").encodeToByteArray()
+ file.writeBytes(bytes)
+ // specifying the sha256 hash forces its validation
+ val indexFile = getIndexFile("foo/bar", sha256, bytes.size.toLong())
+ val downloadRequest = DownloadRequest(indexFile, mirrors)
+
+ val httpManager = HttpManager(userAgent, null)
+ val httpDownloader = HttpDownloaderV2(httpManager, downloadRequest, file)
+ // this throws if the hash doesn't match while downloading
+ httpDownloader.download()
+
+ assertContentEquals(bytes, file.readBytes())
+ }
+
+ @Test
+ fun testCompleteResumeWithHashFailure() = runSuspend {
+ val sha256 = "efabb260da949061c88173c19f369b4aa0eaa82003c7c2dec08b5dfe75525368"
+ val file = File(folder.newFolder(), sha256).apply { createNewFile() }
+ val bytes = ("These are the first bytes that were already downloaded." +
+ "These are the last bytes that still need to be downloaded.").encodeToByteArray()
+ file.writeBytes(bytes)
+ // specifying the sha256 hash forces its validation
+ val indexFile = getIndexFile("foo/bar", sha256.replaceFirst('e', 'f'), bytes.size.toLong())
+ val downloadRequest = DownloadRequest(indexFile, mirrors)
+ val mockEngine = MockEngine.config {
+ reuseHandlers = false
+ addHandler {
+ respond("", OK)
+ }
+ }
+ val httpManager = HttpManager(userAgent, null, httpClientEngineFactory = mockEngine)
+ val httpDownloader = HttpDownloaderV2(httpManager, downloadRequest, file)
+ val e = assertFailsWith {
+ httpDownloader.download()
+ }
+ assertEquals("Hash not matching", e.message)
+ }
+
@Test
fun testResumeError() = runSuspend {
val file = folder.newFile()
diff --git a/libs/download/src/commonMain/kotlin/org/fdroid/download/Mirror.kt b/libs/download/src/commonMain/kotlin/org/fdroid/download/Mirror.kt
index e886a684c..e9d7858e5 100644
--- a/libs/download/src/commonMain/kotlin/org/fdroid/download/Mirror.kt
+++ b/libs/download/src/commonMain/kotlin/org/fdroid/download/Mirror.kt
@@ -36,9 +36,8 @@ public data class Mirror @JvmOverloads constructor(
return URLBuilder(url).appendPathSegments(path.trimStart('/')).build()
}
- public fun getFDroidLinkUrl(repoFingerprint: String?): String {
- val fpr = repoFingerprint?.let { "?fingerprint=$repoFingerprint" } ?: ""
- return "https://fdroid.link/#$url$fpr"
+ public fun getFDroidLinkUrl(repoFingerprint: String): String {
+ return "https://fdroid.link/#$url?fingerprint=$repoFingerprint"
}
public fun isOnion(): Boolean = url.isOnion()
diff --git a/libs/index/api/android/index.api b/libs/index/api/android/index.api
new file mode 100644
index 000000000..0ac289286
--- /dev/null
+++ b/libs/index/api/android/index.api
@@ -0,0 +1,1252 @@
+public abstract interface class org/fdroid/CompatibilityChecker {
+ public abstract fun isCompatible (Lorg/fdroid/index/v2/PackageManifest;)Z
+}
+
+public final class org/fdroid/CompatibilityCheckerImpl : org/fdroid/CompatibilityChecker {
+ public fun (Landroid/content/pm/PackageManager;)V
+ public fun (Landroid/content/pm/PackageManager;Z)V
+ public fun (Landroid/content/pm/PackageManager;ZI)V
+ public fun (Landroid/content/pm/PackageManager;ZI[Ljava/lang/String;)V
+ public synthetic fun (Landroid/content/pm/PackageManager;ZI[Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun isCompatible (Lorg/fdroid/index/v2/PackageManifest;)Z
+}
+
+public final class org/fdroid/CompatibilityCheckerUtils {
+ public static final field INSTANCE Lorg/fdroid/CompatibilityCheckerUtils;
+ public final fun minInstallableTargetSdk ()I
+ public final fun minInstallableTargetSdk (I)I
+ public static synthetic fun minInstallableTargetSdk$default (Lorg/fdroid/CompatibilityCheckerUtils;IILjava/lang/Object;)I
+}
+
+public final class org/fdroid/LocaleChooser {
+ public static final field INSTANCE Lorg/fdroid/LocaleChooser;
+ public final fun getBestLocale (Ljava/util/Map;Landroidx/core/os/LocaleListCompat;)Ljava/lang/Object;
+}
+
+public abstract interface class org/fdroid/PackagePreference {
+ public abstract fun getIgnoreVersionCodeUpdate ()J
+ public abstract fun getReleaseChannels ()Ljava/util/List;
+}
+
+public final class org/fdroid/UpdateChecker {
+ public fun (Lorg/fdroid/CompatibilityChecker;)V
+ public final fun getSuggestedVersion (Ljava/util/List;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function0;)Lorg/fdroid/index/v2/PackageVersion;
+ public static synthetic fun getSuggestedVersion$default (Lorg/fdroid/UpdateChecker;Ljava/util/List;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lorg/fdroid/index/v2/PackageVersion;
+ public final fun getUpdate (Ljava/util/List;Landroid/content/pm/PackageInfo;Ljava/util/List;ZLkotlin/jvm/functions/Function0;)Lorg/fdroid/index/v2/PackageVersion;
+ public final fun getUpdate (Ljava/util/List;Lkotlin/jvm/functions/Function0;JLjava/util/List;ZLkotlin/jvm/functions/Function0;)Lorg/fdroid/index/v2/PackageVersion;
+ public static synthetic fun getUpdate$default (Lorg/fdroid/UpdateChecker;Ljava/util/List;Landroid/content/pm/PackageInfo;Ljava/util/List;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lorg/fdroid/index/v2/PackageVersion;
+ public static synthetic fun getUpdate$default (Lorg/fdroid/UpdateChecker;Ljava/util/List;Lkotlin/jvm/functions/Function0;JLjava/util/List;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lorg/fdroid/index/v2/PackageVersion;
+}
+
+public final class org/fdroid/index/IndexConverter {
+ public fun ()V
+ public fun (Ljava/lang/String;)V
+ public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun toIndexV2 (Lorg/fdroid/index/v1/IndexV1;)Lorg/fdroid/index/v2/IndexV2;
+}
+
+public final class org/fdroid/index/IndexConverterKt {
+ public static final field RELEASE_CHANNEL_BETA Ljava/lang/String;
+ public static final fun getV1ReleaseChannels ()Ljava/util/Map;
+}
+
+public abstract class org/fdroid/index/IndexCreator {
+ public fun (Landroid/content/pm/PackageManager;Ljava/io/File;Ljava/util/Set;)V
+ protected final fun copyApkToRepo (Landroid/content/pm/PackageInfo;)Ljava/io/File;
+ protected final fun copyIconToRepo (Landroid/content/pm/PackageInfo;)Ljava/lang/String;
+ public abstract fun createRepo ()Ljava/lang/Object;
+ protected final fun getPackageManager ()Landroid/content/pm/PackageManager;
+ protected final fun getPackageNames ()Ljava/util/Set;
+ protected final fun getRepoDir ()Ljava/io/File;
+ protected final fun hashFile (Ljava/io/File;)Ljava/lang/String;
+ protected final fun parseNativeCode (Landroid/content/pm/PackageInfo;)Ljava/util/List;
+ protected final fun prepareIconFolders ()V
+}
+
+public final class org/fdroid/index/IndexParser {
+ public static final field INSTANCE Lorg/fdroid/index/IndexParser;
+ public static final fun getJson ()Lkotlinx/serialization/json/Json;
+ public static final fun parseEntry (Ljava/lang/String;)Lorg/fdroid/index/v2/Entry;
+ public static final fun parseV1 (Ljava/lang/String;)Lorg/fdroid/index/v1/IndexV1;
+ public static final fun parseV2 (Ljava/lang/String;)Lorg/fdroid/index/v2/IndexV2;
+}
+
+public final class org/fdroid/index/IndexParserKt {
+ public static final fun parseEntry (Lorg/fdroid/index/IndexParser;Ljava/io/InputStream;)Lorg/fdroid/index/v2/Entry;
+ public static final fun parseV1 (Lorg/fdroid/index/IndexParser;Ljava/io/InputStream;)Lorg/fdroid/index/v1/IndexV1;
+ public static final fun parseV2 (Lorg/fdroid/index/IndexParser;Ljava/io/InputStream;)Lorg/fdroid/index/v2/IndexV2;
+}
+
+public final class org/fdroid/index/IndexUtils {
+ public static final field INSTANCE Lorg/fdroid/index/IndexUtils;
+ public final fun getFingerprint (Ljava/lang/String;)Ljava/lang/String;
+ public final fun getPackageSigner ([B)Ljava/lang/String;
+ public final fun getsig ([B)Ljava/lang/String;
+}
+
+public abstract class org/fdroid/index/JarIndexVerifier {
+ public fun (Ljava/io/File;Ljava/lang/String;Ljava/lang/String;)V
+ protected abstract fun checkAttributes (Ljava/util/jar/Attributes;)V
+ protected abstract fun getJsonFileName ()Ljava/lang/String;
+ public final fun getStreamAndVerify (Lkotlin/jvm/functions/Function1;)Lkotlin/Pair;
+}
+
+public final class org/fdroid/index/SigningException : java/lang/Exception {
+ public fun (Ljava/lang/String;)V
+ public fun (Ljava/lang/String;Ljava/lang/Throwable;)V
+ public fun (Ljava/lang/Throwable;)V
+}
+
+public final class org/fdroid/index/v1/AppV1 {
+ public static final field Companion Lorg/fdroid/index/v1/AppV1$Companion;
+ public fun (Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/util/List;)V
+ public synthetic fun (Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/util/List;
+ public final fun component10 ()Ljava/lang/String;
+ public final fun component11 ()Ljava/lang/String;
+ public final fun component12 ()Ljava/lang/String;
+ public final fun component13 ()Ljava/lang/String;
+ public final fun component14 ()Ljava/lang/String;
+ public final fun component15 ()Ljava/lang/String;
+ public final fun component16 ()Ljava/lang/String;
+ public final fun component17 ()Ljava/lang/String;
+ public final fun component18 ()Ljava/lang/String;
+ public final fun component19 ()Ljava/lang/String;
+ public final fun component2 ()Ljava/util/List;
+ public final fun component20 ()Ljava/lang/String;
+ public final fun component21 ()Ljava/lang/String;
+ public final fun component22 ()Ljava/lang/String;
+ public final fun component23 ()Ljava/lang/String;
+ public final fun component24 ()Ljava/lang/String;
+ public final fun component25 ()Ljava/lang/String;
+ public final fun component26 ()Ljava/lang/Long;
+ public final fun component27 ()Ljava/lang/String;
+ public final fun component28 ()Ljava/lang/String;
+ public final fun component29 ()Ljava/lang/Long;
+ public final fun component3 ()Ljava/lang/String;
+ public final fun component30 ()Ljava/util/Map;
+ public final fun component31 ()Ljava/util/List;
+ public final fun component4 ()Ljava/lang/String;
+ public final fun component5 ()Ljava/lang/String;
+ public final fun component6 ()Ljava/lang/String;
+ public final fun component7 ()Ljava/lang/String;
+ public final fun component8 ()Ljava/lang/String;
+ public final fun component9 ()Ljava/lang/String;
+ public final fun copy (Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/util/List;)Lorg/fdroid/index/v1/AppV1;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v1/AppV1;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/util/List;ILjava/lang/Object;)Lorg/fdroid/index/v1/AppV1;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getAdded ()Ljava/lang/Long;
+ public final fun getAllowedAPKSigningKeys ()Ljava/util/List;
+ public final fun getAntiFeatures ()Ljava/util/List;
+ public final fun getAuthorEmail ()Ljava/lang/String;
+ public final fun getAuthorName ()Ljava/lang/String;
+ public final fun getAuthorPhone ()Ljava/lang/String;
+ public final fun getAuthorWebSite ()Ljava/lang/String;
+ public final fun getBinaries ()Ljava/lang/String;
+ public final fun getBitcoin ()Ljava/lang/String;
+ public final fun getCategories ()Ljava/util/List;
+ public final fun getChangelog ()Ljava/lang/String;
+ public final fun getDescription ()Ljava/lang/String;
+ public final fun getDonate ()Ljava/lang/String;
+ public final fun getFlattrID ()Ljava/lang/String;
+ public final fun getIcon ()Ljava/lang/String;
+ public final fun getIssueTracker ()Ljava/lang/String;
+ public final fun getLastUpdated ()Ljava/lang/Long;
+ public final fun getLiberapay ()Ljava/lang/String;
+ public final fun getLiberapayID ()Ljava/lang/String;
+ public final fun getLicense ()Ljava/lang/String;
+ public final fun getLitecoin ()Ljava/lang/String;
+ public final fun getLocalized ()Ljava/util/Map;
+ public final fun getName ()Ljava/lang/String;
+ public final fun getOpenCollective ()Ljava/lang/String;
+ public final fun getPackageName ()Ljava/lang/String;
+ public final fun getSourceCode ()Ljava/lang/String;
+ public final fun getSuggestedVersionCode ()Ljava/lang/String;
+ public final fun getSuggestedVersionName ()Ljava/lang/String;
+ public final fun getSummary ()Ljava/lang/String;
+ public final fun getTranslation ()Ljava/lang/String;
+ public final fun getWebSite ()Ljava/lang/String;
+ public fun hashCode ()I
+ public final fun toMetadataV2 (Ljava/lang/String;Ljava/lang/String;)Lorg/fdroid/index/v2/MetadataV2;
+ public static synthetic fun toMetadataV2$default (Lorg/fdroid/index/v1/AppV1;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/fdroid/index/v2/MetadataV2;
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v1/AppV1$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v1/AppV1$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v1/AppV1;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v1/AppV1;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v1/AppV1$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v1/IndexV1 {
+ public static final field Companion Lorg/fdroid/index/v1/IndexV1$Companion;
+ public fun (Lorg/fdroid/index/v1/RepoV1;Lorg/fdroid/index/v1/Requests;Ljava/util/List;Ljava/util/Map;)V
+ public synthetic fun (Lorg/fdroid/index/v1/RepoV1;Lorg/fdroid/index/v1/Requests;Ljava/util/List;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Lorg/fdroid/index/v1/RepoV1;
+ public final fun component2 ()Lorg/fdroid/index/v1/Requests;
+ public final fun component3 ()Ljava/util/List;
+ public final fun component4 ()Ljava/util/Map;
+ public final fun copy (Lorg/fdroid/index/v1/RepoV1;Lorg/fdroid/index/v1/Requests;Ljava/util/List;Ljava/util/Map;)Lorg/fdroid/index/v1/IndexV1;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v1/IndexV1;Lorg/fdroid/index/v1/RepoV1;Lorg/fdroid/index/v1/Requests;Ljava/util/List;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v1/IndexV1;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getApps ()Ljava/util/List;
+ public final fun getPackages ()Ljava/util/Map;
+ public final fun getRepo ()Lorg/fdroid/index/v1/RepoV1;
+ public final fun getRequests ()Lorg/fdroid/index/v1/Requests;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v1/IndexV1$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v1/IndexV1$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v1/IndexV1;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v1/IndexV1;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v1/IndexV1$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v1/IndexV1Creator : org/fdroid/index/IndexCreator {
+ public fun (Landroid/content/pm/PackageManager;Ljava/io/File;Ljava/util/Set;Lorg/fdroid/index/v1/RepoV1;)V
+ public synthetic fun createRepo ()Ljava/lang/Object;
+ public fun createRepo ()Lorg/fdroid/index/v1/IndexV1;
+}
+
+public final class org/fdroid/index/v1/IndexV1StreamProcessor {
+ public fun (Lorg/fdroid/index/v1/IndexV1StreamReceiver;JLjava/lang/String;Lkotlinx/serialization/json/Json;)V
+ public synthetic fun (Lorg/fdroid/index/v1/IndexV1StreamReceiver;JLjava/lang/String;Lkotlinx/serialization/json/Json;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun process (Ljava/io/InputStream;)V
+}
+
+public abstract interface class org/fdroid/index/v1/IndexV1StreamReceiver {
+ public abstract fun receive (Ljava/lang/String;Ljava/util/Map;)V
+ public abstract fun receive (Ljava/lang/String;Lorg/fdroid/index/v2/MetadataV2;)V
+ public abstract fun receive (Lorg/fdroid/index/v2/RepoV2;J)V
+ public abstract fun updateAppMetadata (Ljava/lang/String;Ljava/lang/String;)V
+ public abstract fun updateRepo (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V
+}
+
+public final class org/fdroid/index/v1/IndexV1Verifier : org/fdroid/index/JarIndexVerifier {
+ public fun (Ljava/io/File;Ljava/lang/String;Ljava/lang/String;)V
+}
+
+public final class org/fdroid/index/v1/IndexV1VerifierKt {
+ public static final field DATA_FILE_NAME Ljava/lang/String;
+}
+
+public final class org/fdroid/index/v1/Localized {
+ public static final field Companion Lorg/fdroid/index/v1/Localized$Companion;
+ public fun ()V
+ public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+ public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/lang/String;
+ public final fun component10 ()Ljava/util/List;
+ public final fun component11 ()Ljava/lang/String;
+ public final fun component12 ()Ljava/lang/String;
+ public final fun component13 ()Ljava/lang/String;
+ public final fun component14 ()Ljava/lang/String;
+ public final fun component2 ()Ljava/lang/String;
+ public final fun component3 ()Ljava/lang/String;
+ public final fun component4 ()Ljava/lang/String;
+ public final fun component5 ()Ljava/lang/String;
+ public final fun component6 ()Ljava/util/List;
+ public final fun component7 ()Ljava/util/List;
+ public final fun component8 ()Ljava/util/List;
+ public final fun component9 ()Ljava/util/List;
+ public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lorg/fdroid/index/v1/Localized;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v1/Localized;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/fdroid/index/v1/Localized;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getDescription ()Ljava/lang/String;
+ public final fun getFeatureGraphic ()Ljava/lang/String;
+ public final fun getIcon ()Ljava/lang/String;
+ public final fun getName ()Ljava/lang/String;
+ public final fun getPhoneScreenshots ()Ljava/util/List;
+ public final fun getPromoGraphic ()Ljava/lang/String;
+ public final fun getSevenInchScreenshots ()Ljava/util/List;
+ public final fun getSummary ()Ljava/lang/String;
+ public final fun getTenInchScreenshots ()Ljava/util/List;
+ public final fun getTvBanner ()Ljava/lang/String;
+ public final fun getTvScreenshots ()Ljava/util/List;
+ public final fun getVideo ()Ljava/lang/String;
+ public final fun getWearScreenshots ()Ljava/util/List;
+ public final fun getWhatsNew ()Ljava/lang/String;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v1/Localized$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v1/Localized$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v1/Localized;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v1/Localized;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v1/Localized$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v1/OldIndexException : java/lang/Exception {
+ public fun (ZLjava/lang/String;)V
+ public final fun isSameTimestamp ()Z
+}
+
+public final class org/fdroid/index/v1/PackageV1 {
+ public static final field Companion Lorg/fdroid/index/v1/PackageV1$Companion;
+ public fun (Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/Long;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;)V
+ public synthetic fun (Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/Long;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/lang/Long;
+ public final fun component10 ()Ljava/lang/String;
+ public final fun component11 ()J
+ public final fun component12 ()Ljava/lang/String;
+ public final fun component13 ()Ljava/util/List;
+ public final fun component14 ()Ljava/util/List;
+ public final fun component15 ()Ljava/lang/Long;
+ public final fun component16 ()Ljava/lang/String;
+ public final fun component17 ()Ljava/util/List;
+ public final fun component18 ()Ljava/util/List;
+ public final fun component19 ()Ljava/util/List;
+ public final fun component2 ()Ljava/lang/String;
+ public final fun component3 ()Ljava/lang/String;
+ public final fun component4 ()Ljava/lang/String;
+ public final fun component5 ()Ljava/lang/Integer;
+ public final fun component6 ()Ljava/lang/Integer;
+ public final fun component7 ()Ljava/lang/Integer;
+ public final fun component8 ()Ljava/lang/String;
+ public final fun component9 ()Ljava/lang/String;
+ public final fun copy (Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/Long;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;)Lorg/fdroid/index/v1/PackageV1;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v1/PackageV1;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/Long;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;ILjava/lang/Object;)Lorg/fdroid/index/v1/PackageV1;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getAdded ()Ljava/lang/Long;
+ public final fun getAntiFeatures ()Ljava/util/List;
+ public final fun getApkName ()Ljava/lang/String;
+ public final fun getFeatures ()Ljava/util/List;
+ public final fun getHash ()Ljava/lang/String;
+ public final fun getHashType ()Ljava/lang/String;
+ public final fun getMaxSdkVersion ()Ljava/lang/Integer;
+ public final fun getMinSdkVersion ()Ljava/lang/Integer;
+ public final fun getNativeCode ()Ljava/util/List;
+ public final fun getPackageName ()Ljava/lang/String;
+ public final fun getSig ()Ljava/lang/String;
+ public final fun getSigner ()Ljava/lang/String;
+ public final fun getSize ()J
+ public final fun getSrcName ()Ljava/lang/String;
+ public final fun getTargetSdkVersion ()Ljava/lang/Integer;
+ public final fun getUsesPermission ()Ljava/util/List;
+ public final fun getUsesPermission23 ()Ljava/util/List;
+ public final fun getVersionCode ()Ljava/lang/Long;
+ public final fun getVersionName ()Ljava/lang/String;
+ public fun hashCode ()I
+ public final fun toPackageVersionV2 (Ljava/util/List;Ljava/util/Map;Ljava/util/Map;)Lorg/fdroid/index/v2/PackageVersionV2;
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v1/PackageV1$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v1/PackageV1$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v1/PackageV1;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v1/PackageV1;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v1/PackageV1$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v1/PermissionV1 {
+ public static final field Companion Lorg/fdroid/index/v1/PermissionV1$Companion;
+ public fun (Ljava/lang/String;Ljava/lang/Integer;)V
+ public synthetic fun (Ljava/lang/String;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/lang/String;
+ public final fun component2 ()Ljava/lang/Integer;
+ public final fun copy (Ljava/lang/String;Ljava/lang/Integer;)Lorg/fdroid/index/v1/PermissionV1;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v1/PermissionV1;Ljava/lang/String;Ljava/lang/Integer;ILjava/lang/Object;)Lorg/fdroid/index/v1/PermissionV1;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getMaxSdk ()Ljava/lang/Integer;
+ public final fun getName ()Ljava/lang/String;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final class org/fdroid/index/v1/PermissionV1$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v1/RepoV1 {
+ public static final field Companion Lorg/fdroid/index/v1/RepoV1$Companion;
+ public fun (JILjava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V
+ public synthetic fun (JILjava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()J
+ public final fun component2 ()I
+ public final fun component3 ()Ljava/lang/Integer;
+ public final fun component4 ()Ljava/lang/String;
+ public final fun component5 ()Ljava/lang/String;
+ public final fun component6 ()Ljava/lang/String;
+ public final fun component7 ()Ljava/lang/String;
+ public final fun component8 ()Ljava/util/List;
+ public final fun copy (JILjava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)Lorg/fdroid/index/v1/RepoV1;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v1/RepoV1;JILjava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lorg/fdroid/index/v1/RepoV1;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getAddress ()Ljava/lang/String;
+ public final fun getDescription ()Ljava/lang/String;
+ public final fun getIcon ()Ljava/lang/String;
+ public final fun getMaxAge ()Ljava/lang/Integer;
+ public final fun getMirrors ()Ljava/util/List;
+ public final fun getName ()Ljava/lang/String;
+ public final fun getTimestamp ()J
+ public final fun getVersion ()I
+ public fun hashCode ()I
+ public final fun toRepoV2 (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Lorg/fdroid/index/v2/RepoV2;
+ public static synthetic fun toRepoV2$default (Lorg/fdroid/index/v1/RepoV1;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/RepoV2;
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v1/RepoV1$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v1/RepoV1$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v1/RepoV1;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v1/RepoV1;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v1/RepoV1$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v1/Requests {
+ public static final field Companion Lorg/fdroid/index/v1/Requests$Companion;
+ public fun (Ljava/util/List;Ljava/util/List;)V
+ public final fun component1 ()Ljava/util/List;
+ public final fun component2 ()Ljava/util/List;
+ public final fun copy (Ljava/util/List;Ljava/util/List;)Lorg/fdroid/index/v1/Requests;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v1/Requests;Ljava/util/List;Ljava/util/List;ILjava/lang/Object;)Lorg/fdroid/index/v1/Requests;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getInstall ()Ljava/util/List;
+ public final fun getUninstall ()Ljava/util/List;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v1/Requests$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v1/Requests$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v1/Requests;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v1/Requests;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v1/Requests$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/AntiFeatureV2 {
+ public static final field Companion Lorg/fdroid/index/v2/AntiFeatureV2$Companion;
+ public fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V
+ public synthetic fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/util/Map;
+ public final fun component2 ()Ljava/util/Map;
+ public final fun component3 ()Ljava/util/Map;
+ public final fun copy (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Lorg/fdroid/index/v2/AntiFeatureV2;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v2/AntiFeatureV2;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/AntiFeatureV2;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getDescription ()Ljava/util/Map;
+ public final fun getIcon ()Ljava/util/Map;
+ public final fun getName ()Ljava/util/Map;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v2/AntiFeatureV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/AntiFeatureV2$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/AntiFeatureV2;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/AntiFeatureV2;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/AntiFeatureV2$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/CategoryV2 {
+ public static final field Companion Lorg/fdroid/index/v2/CategoryV2$Companion;
+ public fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V
+ public synthetic fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/util/Map;
+ public final fun component2 ()Ljava/util/Map;
+ public final fun component3 ()Ljava/util/Map;
+ public final fun copy (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Lorg/fdroid/index/v2/CategoryV2;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v2/CategoryV2;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/CategoryV2;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getDescription ()Ljava/util/Map;
+ public final fun getIcon ()Ljava/util/Map;
+ public final fun getName ()Ljava/util/Map;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v2/CategoryV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/CategoryV2$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/CategoryV2;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/CategoryV2;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/CategoryV2$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/Entry {
+ public static final field Companion Lorg/fdroid/index/v2/Entry$Companion;
+ public fun (JJLjava/lang/Integer;Lorg/fdroid/index/v2/EntryFileV2;Ljava/util/Map;)V
+ public synthetic fun (JJLjava/lang/Integer;Lorg/fdroid/index/v2/EntryFileV2;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()J
+ public final fun component2 ()J
+ public final fun component3 ()Ljava/lang/Integer;
+ public final fun component4 ()Lorg/fdroid/index/v2/EntryFileV2;
+ public final fun component5 ()Ljava/util/Map;
+ public final fun copy (JJLjava/lang/Integer;Lorg/fdroid/index/v2/EntryFileV2;Ljava/util/Map;)Lorg/fdroid/index/v2/Entry;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v2/Entry;JJLjava/lang/Integer;Lorg/fdroid/index/v2/EntryFileV2;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/Entry;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getDiff (J)Lorg/fdroid/index/v2/EntryFileV2;
+ public final fun getDiffs ()Ljava/util/Map;
+ public final fun getIndex ()Lorg/fdroid/index/v2/EntryFileV2;
+ public final fun getMaxAge ()Ljava/lang/Integer;
+ public final fun getTimestamp ()J
+ public final fun getVersion ()J
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v2/Entry$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/Entry$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/Entry;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/Entry;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/Entry$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/EntryFileV2 : org/fdroid/IndexFile {
+ public static final field Companion Lorg/fdroid/index/v2/EntryFileV2$Companion;
+ public fun (Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;I)V
+ public synthetic fun (Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;IILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/lang/String;
+ public final fun component2 ()Ljava/lang/String;
+ public final fun component3 ()J
+ public final fun component4 ()Ljava/lang/String;
+ public final fun component5 ()I
+ public final fun copy (Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;I)Lorg/fdroid/index/v2/EntryFileV2;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v2/EntryFileV2;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;IILjava/lang/Object;)Lorg/fdroid/index/v2/EntryFileV2;
+ public fun equals (Ljava/lang/Object;)Z
+ public fun getIpfsCidV1 ()Ljava/lang/String;
+ public fun getName ()Ljava/lang/String;
+ public final fun getNumPackages ()I
+ public fun getSha256 ()Ljava/lang/String;
+ public fun getSize ()Ljava/lang/Long;
+ public fun hashCode ()I
+ public fun serialize ()Ljava/lang/String;
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v2/EntryFileV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/EntryFileV2$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/EntryFileV2;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/EntryFileV2;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/EntryFileV2$Companion {
+ public final fun deserialize (Ljava/lang/String;)Lorg/fdroid/index/v2/EntryFileV2;
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/EntryVerifier : org/fdroid/index/JarIndexVerifier {
+ public fun (Ljava/io/File;Ljava/lang/String;Ljava/lang/String;)V
+}
+
+public final class org/fdroid/index/v2/EntryVerifierKt {
+ public static final field DATA_FILE_NAME Ljava/lang/String;
+}
+
+public final class org/fdroid/index/v2/FeatureV2 {
+ public static final field Companion Lorg/fdroid/index/v2/FeatureV2$Companion;
+ public fun (Ljava/lang/String;)V
+ public final fun component1 ()Ljava/lang/String;
+ public final fun copy (Ljava/lang/String;)Lorg/fdroid/index/v2/FeatureV2;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v2/FeatureV2;Ljava/lang/String;ILjava/lang/Object;)Lorg/fdroid/index/v2/FeatureV2;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getName ()Ljava/lang/String;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v2/FeatureV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/FeatureV2$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/FeatureV2;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/FeatureV2;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/FeatureV2$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/FileV1 : org/fdroid/IndexFile {
+ public static final field Companion Lorg/fdroid/index/v2/FileV1$Companion;
+ public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;)V
+ public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/lang/String;
+ public final fun component2 ()Ljava/lang/String;
+ public final fun component3 ()Ljava/lang/Long;
+ public final fun component4 ()Ljava/lang/String;
+ public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;)Lorg/fdroid/index/v2/FileV1;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v2/FileV1;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;ILjava/lang/Object;)Lorg/fdroid/index/v2/FileV1;
+ public static final fun deserialize (Ljava/lang/String;)Lorg/fdroid/index/v2/FileV1;
+ public fun equals (Ljava/lang/Object;)Z
+ public fun getIpfsCidV1 ()Ljava/lang/String;
+ public fun getName ()Ljava/lang/String;
+ public fun getSha256 ()Ljava/lang/String;
+ public fun getSize ()Ljava/lang/Long;
+ public fun hashCode ()I
+ public fun serialize ()Ljava/lang/String;
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v2/FileV1$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/FileV1$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/FileV1;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/FileV1;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/FileV1$Companion {
+ public final fun deserialize (Ljava/lang/String;)Lorg/fdroid/index/v2/FileV1;
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/FileV2 : org/fdroid/IndexFile {
+ public static final field Companion Lorg/fdroid/index/v2/FileV2$Companion;
+ public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;)V
+ public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/lang/String;
+ public final fun component2 ()Ljava/lang/String;
+ public final fun component3 ()Ljava/lang/Long;
+ public final fun component4 ()Ljava/lang/String;
+ public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;)Lorg/fdroid/index/v2/FileV2;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v2/FileV2;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;ILjava/lang/Object;)Lorg/fdroid/index/v2/FileV2;
+ public static final fun deserialize (Ljava/lang/String;)Lorg/fdroid/index/v2/FileV2;
+ public fun equals (Ljava/lang/Object;)Z
+ public static final fun fromPath (Ljava/lang/String;)Lorg/fdroid/index/v2/FileV2;
+ public fun getIpfsCidV1 ()Ljava/lang/String;
+ public fun getName ()Ljava/lang/String;
+ public fun getSha256 ()Ljava/lang/String;
+ public fun getSize ()Ljava/lang/Long;
+ public fun hashCode ()I
+ public fun serialize ()Ljava/lang/String;
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v2/FileV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/FileV2$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/FileV2;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/FileV2;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/FileV2$Companion {
+ public final fun deserialize (Ljava/lang/String;)Lorg/fdroid/index/v2/FileV2;
+ public final fun fromPath (Ljava/lang/String;)Lorg/fdroid/index/v2/FileV2;
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/IndexV2 {
+ public static final field Companion Lorg/fdroid/index/v2/IndexV2$Companion;
+ public fun (Lorg/fdroid/index/v2/RepoV2;Ljava/util/Map;)V
+ public synthetic fun (Lorg/fdroid/index/v2/RepoV2;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Lorg/fdroid/index/v2/RepoV2;
+ public final fun component2 ()Ljava/util/Map;
+ public final fun copy (Lorg/fdroid/index/v2/RepoV2;Ljava/util/Map;)Lorg/fdroid/index/v2/IndexV2;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v2/IndexV2;Lorg/fdroid/index/v2/RepoV2;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/IndexV2;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getPackages ()Ljava/util/Map;
+ public final fun getRepo ()Lorg/fdroid/index/v2/RepoV2;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+ public final fun walkFiles (Lkotlin/jvm/functions/Function1;)V
+}
+
+public final synthetic class org/fdroid/index/v2/IndexV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/IndexV2$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/IndexV2;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/IndexV2;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/IndexV2$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/IndexV2DiffStreamProcessor : org/fdroid/index/v2/IndexV2StreamProcessor {
+ public fun (Lorg/fdroid/index/v2/IndexV2DiffStreamReceiver;Lkotlinx/serialization/json/Json;)V
+ public synthetic fun (Lorg/fdroid/index/v2/IndexV2DiffStreamReceiver;Lkotlinx/serialization/json/Json;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun process (JLjava/io/InputStream;Lkotlin/jvm/functions/Function1;)V
+}
+
+public abstract interface class org/fdroid/index/v2/IndexV2DiffStreamReceiver {
+ public abstract fun onStreamEnded ()V
+ public abstract fun receivePackageMetadataDiff (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V
+ public abstract fun receiveRepoDiff (JLkotlinx/serialization/json/JsonObject;)V
+ public abstract fun receiveVersionsDiff (Ljava/lang/String;Ljava/util/Map;)V
+}
+
+public final class org/fdroid/index/v2/IndexV2FullStreamProcessor : org/fdroid/index/v2/IndexV2StreamProcessor {
+ public fun (Lorg/fdroid/index/v2/IndexV2StreamReceiver;Lkotlinx/serialization/json/Json;)V
+ public synthetic fun (Lorg/fdroid/index/v2/IndexV2StreamReceiver;Lkotlinx/serialization/json/Json;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun process (JLjava/io/InputStream;Lkotlin/jvm/functions/Function1;)V
+}
+
+public abstract interface class org/fdroid/index/v2/IndexV2StreamProcessor {
+ public abstract fun process (JLjava/io/InputStream;Lkotlin/jvm/functions/Function1;)V
+}
+
+public abstract interface class org/fdroid/index/v2/IndexV2StreamReceiver {
+ public abstract fun onStreamEnded ()V
+ public abstract fun receive (Ljava/lang/String;Lorg/fdroid/index/v2/PackageV2;)V
+ public abstract fun receive (Lorg/fdroid/index/v2/RepoV2;J)V
+}
+
+public final class org/fdroid/index/v2/ManifestV2 : org/fdroid/index/v2/PackageManifest {
+ public static final field Companion Lorg/fdroid/index/v2/ManifestV2$Companion;
+ public fun (Ljava/lang/String;JLorg/fdroid/index/v2/UsesSdkV2;Ljava/lang/Integer;Lorg/fdroid/index/v2/SignerV2;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;)V
+ public synthetic fun (Ljava/lang/String;JLorg/fdroid/index/v2/UsesSdkV2;Ljava/lang/Integer;Lorg/fdroid/index/v2/SignerV2;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/lang/String;
+ public final fun component2 ()J
+ public final fun component3 ()Lorg/fdroid/index/v2/UsesSdkV2;
+ public final fun component4 ()Ljava/lang/Integer;
+ public final fun component5 ()Lorg/fdroid/index/v2/SignerV2;
+ public final fun component6 ()Ljava/util/List;
+ public final fun component7 ()Ljava/util/List;
+ public final fun component8 ()Ljava/util/List;
+ public final fun component9 ()Ljava/util/List;
+ public final fun copy (Ljava/lang/String;JLorg/fdroid/index/v2/UsesSdkV2;Ljava/lang/Integer;Lorg/fdroid/index/v2/SignerV2;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;)Lorg/fdroid/index/v2/ManifestV2;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v2/ManifestV2;Ljava/lang/String;JLorg/fdroid/index/v2/UsesSdkV2;Ljava/lang/Integer;Lorg/fdroid/index/v2/SignerV2;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;ILjava/lang/Object;)Lorg/fdroid/index/v2/ManifestV2;
+ public fun equals (Ljava/lang/Object;)Z
+ public fun getFeatureNames ()Ljava/util/List;
+ public final fun getFeatures ()Ljava/util/List;
+ public fun getMaxSdkVersion ()Ljava/lang/Integer;
+ public fun getMinSdkVersion ()Ljava/lang/Integer;
+ public fun getNativecode ()Ljava/util/List;
+ public final fun getSigner ()Lorg/fdroid/index/v2/SignerV2;
+ public fun getTargetSdkVersion ()Ljava/lang/Integer;
+ public final fun getUsesPermission ()Ljava/util/List;
+ public final fun getUsesPermissionSdk23 ()Ljava/util/List;
+ public final fun getUsesSdk ()Lorg/fdroid/index/v2/UsesSdkV2;
+ public final fun getVersionCode ()J
+ public final fun getVersionName ()Ljava/lang/String;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v2/ManifestV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/ManifestV2$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/ManifestV2;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/ManifestV2;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/ManifestV2$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/MetadataV2 {
+ public static final field Companion Lorg/fdroid/index/v2/MetadataV2$Companion;
+ public fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;JJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Lorg/fdroid/index/v2/Screenshots;)V
+ public synthetic fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;JJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Lorg/fdroid/index/v2/Screenshots;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/util/Map;
+ public final fun component10 ()Ljava/lang/String;
+ public final fun component11 ()Ljava/lang/String;
+ public final fun component12 ()Ljava/lang/String;
+ public final fun component13 ()Ljava/util/List;
+ public final fun component14 ()Ljava/lang/String;
+ public final fun component15 ()Ljava/lang/String;
+ public final fun component16 ()Ljava/lang/String;
+ public final fun component17 ()Ljava/lang/String;
+ public final fun component18 ()Ljava/util/List;
+ public final fun component19 ()Ljava/lang/String;
+ public final fun component2 ()Ljava/util/Map;
+ public final fun component20 ()Ljava/lang/String;
+ public final fun component21 ()Ljava/lang/String;
+ public final fun component22 ()Ljava/lang/String;
+ public final fun component23 ()Ljava/lang/String;
+ public final fun component24 ()Ljava/lang/String;
+ public final fun component25 ()Ljava/util/Map;
+ public final fun component26 ()Ljava/util/Map;
+ public final fun component27 ()Ljava/util/Map;
+ public final fun component28 ()Ljava/util/Map;
+ public final fun component29 ()Ljava/util/Map;
+ public final fun component3 ()Ljava/util/Map;
+ public final fun component30 ()Lorg/fdroid/index/v2/Screenshots;
+ public final fun component4 ()J
+ public final fun component5 ()J
+ public final fun component6 ()Ljava/lang/String;
+ public final fun component7 ()Ljava/lang/String;
+ public final fun component8 ()Ljava/lang/String;
+ public final fun component9 ()Ljava/lang/String;
+ public final fun copy (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;JJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Lorg/fdroid/index/v2/Screenshots;)Lorg/fdroid/index/v2/MetadataV2;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v2/MetadataV2;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;JJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Lorg/fdroid/index/v2/Screenshots;ILjava/lang/Object;)Lorg/fdroid/index/v2/MetadataV2;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getAdded ()J
+ public final fun getAuthorEmail ()Ljava/lang/String;
+ public final fun getAuthorName ()Ljava/lang/String;
+ public final fun getAuthorPhone ()Ljava/lang/String;
+ public final fun getAuthorWebSite ()Ljava/lang/String;
+ public final fun getBitcoin ()Ljava/lang/String;
+ public final fun getCategories ()Ljava/util/List;
+ public final fun getChangelog ()Ljava/lang/String;
+ public final fun getDescription ()Ljava/util/Map;
+ public final fun getDonate ()Ljava/util/List;
+ public final fun getFeatureGraphic ()Ljava/util/Map;
+ public final fun getFlattrID ()Ljava/lang/String;
+ public final fun getIcon ()Ljava/util/Map;
+ public final fun getIssueTracker ()Ljava/lang/String;
+ public final fun getLastUpdated ()J
+ public final fun getLiberapay ()Ljava/lang/String;
+ public final fun getLiberapayID ()Ljava/lang/String;
+ public final fun getLicense ()Ljava/lang/String;
+ public final fun getLitecoin ()Ljava/lang/String;
+ public final fun getName ()Ljava/util/Map;
+ public final fun getOpenCollective ()Ljava/lang/String;
+ public final fun getPreferredSigner ()Ljava/lang/String;
+ public final fun getPromoGraphic ()Ljava/util/Map;
+ public final fun getScreenshots ()Lorg/fdroid/index/v2/Screenshots;
+ public final fun getSourceCode ()Ljava/lang/String;
+ public final fun getSummary ()Ljava/util/Map;
+ public final fun getTranslation ()Ljava/lang/String;
+ public final fun getTvBanner ()Ljava/util/Map;
+ public final fun getVideo ()Ljava/util/Map;
+ public final fun getWebSite ()Ljava/lang/String;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+ public final fun walkFiles (Lkotlin/jvm/functions/Function1;)V
+}
+
+public final synthetic class org/fdroid/index/v2/MetadataV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/MetadataV2$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/MetadataV2;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/MetadataV2;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/MetadataV2$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/MirrorV2 {
+ public static final field Companion Lorg/fdroid/index/v2/MirrorV2$Companion;
+ public fun (Ljava/lang/String;Ljava/lang/String;)V
+ public synthetic fun (Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/lang/String;
+ public final fun component2 ()Ljava/lang/String;
+ public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lorg/fdroid/index/v2/MirrorV2;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v2/MirrorV2;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/fdroid/index/v2/MirrorV2;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getCountryCode ()Ljava/lang/String;
+ public final fun getUrl ()Ljava/lang/String;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v2/MirrorV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/MirrorV2$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/MirrorV2;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/MirrorV2;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/MirrorV2$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public abstract interface class org/fdroid/index/v2/PackageManifest {
+ public abstract fun getFeatureNames ()Ljava/util/List;
+ public abstract fun getMaxSdkVersion ()Ljava/lang/Integer;
+ public abstract fun getMinSdkVersion ()Ljava/lang/Integer;
+ public abstract fun getNativecode ()Ljava/util/List;
+ public abstract fun getTargetSdkVersion ()Ljava/lang/Integer;
+}
+
+public final class org/fdroid/index/v2/PackageV2 {
+ public static final field Companion Lorg/fdroid/index/v2/PackageV2$Companion;
+ public fun (Lorg/fdroid/index/v2/MetadataV2;Ljava/util/Map;)V
+ public synthetic fun (Lorg/fdroid/index/v2/MetadataV2;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Lorg/fdroid/index/v2/MetadataV2;
+ public final fun component2 ()Ljava/util/Map;
+ public final fun copy (Lorg/fdroid/index/v2/MetadataV2;Ljava/util/Map;)Lorg/fdroid/index/v2/PackageV2;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v2/PackageV2;Lorg/fdroid/index/v2/MetadataV2;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/PackageV2;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getMetadata ()Lorg/fdroid/index/v2/MetadataV2;
+ public final fun getVersions ()Ljava/util/Map;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+ public final fun walkFiles (Lkotlin/jvm/functions/Function1;)V
+}
+
+public final synthetic class org/fdroid/index/v2/PackageV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/PackageV2$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/PackageV2;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/PackageV2;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/PackageV2$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/PackageV2Kt {
+ public static final field ANTI_FEATURE_KNOWN_VULNERABILITY Ljava/lang/String;
+}
+
+public abstract interface class org/fdroid/index/v2/PackageVersion {
+ public abstract fun getAdded ()J
+ public abstract fun getHasKnownVulnerability ()Z
+ public abstract fun getPackageManifest ()Lorg/fdroid/index/v2/PackageManifest;
+ public abstract fun getReleaseChannels ()Ljava/util/List;
+ public abstract fun getSigner ()Lorg/fdroid/index/v2/SignerV2;
+ public abstract fun getSize ()Ljava/lang/Long;
+ public abstract fun getVersionCode ()J
+ public abstract fun getVersionName ()Ljava/lang/String;
+}
+
+public final class org/fdroid/index/v2/PackageVersionV2 : org/fdroid/index/v2/PackageVersion {
+ public static final field Companion Lorg/fdroid/index/v2/PackageVersionV2$Companion;
+ public fun (JLorg/fdroid/index/v2/FileV1;Lorg/fdroid/index/v2/FileV2;Lorg/fdroid/index/v2/ManifestV2;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;)V
+ public synthetic fun (JLorg/fdroid/index/v2/FileV1;Lorg/fdroid/index/v2/FileV2;Lorg/fdroid/index/v2/ManifestV2;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()J
+ public final fun component2 ()Lorg/fdroid/index/v2/FileV1;
+ public final fun component3 ()Lorg/fdroid/index/v2/FileV2;
+ public final fun component4 ()Lorg/fdroid/index/v2/ManifestV2;
+ public final fun component5 ()Ljava/util/List;
+ public final fun component6 ()Ljava/util/Map;
+ public final fun component7 ()Ljava/util/Map;
+ public final fun copy (JLorg/fdroid/index/v2/FileV1;Lorg/fdroid/index/v2/FileV2;Lorg/fdroid/index/v2/ManifestV2;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;)Lorg/fdroid/index/v2/PackageVersionV2;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v2/PackageVersionV2;JLorg/fdroid/index/v2/FileV1;Lorg/fdroid/index/v2/FileV2;Lorg/fdroid/index/v2/ManifestV2;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/PackageVersionV2;
+ public fun equals (Ljava/lang/Object;)Z
+ public fun getAdded ()J
+ public final fun getAntiFeatures ()Ljava/util/Map;
+ public final fun getFile ()Lorg/fdroid/index/v2/FileV1;
+ public fun getHasKnownVulnerability ()Z
+ public final fun getManifest ()Lorg/fdroid/index/v2/ManifestV2;
+ public fun getPackageManifest ()Lorg/fdroid/index/v2/PackageManifest;
+ public fun getReleaseChannels ()Ljava/util/List;
+ public fun getSigner ()Lorg/fdroid/index/v2/SignerV2;
+ public fun getSize ()Ljava/lang/Long;
+ public final fun getSrc ()Lorg/fdroid/index/v2/FileV2;
+ public fun getVersionCode ()J
+ public fun getVersionName ()Ljava/lang/String;
+ public final fun getWhatsNew ()Ljava/util/Map;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+ public final fun walkFiles (Lkotlin/jvm/functions/Function1;)V
+}
+
+public final synthetic class org/fdroid/index/v2/PackageVersionV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/PackageVersionV2$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/PackageVersionV2;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/PackageVersionV2;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/PackageVersionV2$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/PermissionV2 {
+ public static final field Companion Lorg/fdroid/index/v2/PermissionV2$Companion;
+ public fun (Ljava/lang/String;Ljava/lang/Integer;)V
+ public synthetic fun (Ljava/lang/String;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/lang/String;
+ public final fun component2 ()Ljava/lang/Integer;
+ public final fun copy (Ljava/lang/String;Ljava/lang/Integer;)Lorg/fdroid/index/v2/PermissionV2;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v2/PermissionV2;Ljava/lang/String;Ljava/lang/Integer;ILjava/lang/Object;)Lorg/fdroid/index/v2/PermissionV2;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getMaxSdkVersion ()Ljava/lang/Integer;
+ public final fun getName ()Ljava/lang/String;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v2/PermissionV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/PermissionV2$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/PermissionV2;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/PermissionV2;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/PermissionV2$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/ReflectionDiffer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/ReflectionDiffer;
+ public final fun applyDiff (Ljava/lang/Object;Lkotlinx/serialization/json/JsonObject;)Ljava/lang/Object;
+}
+
+public final class org/fdroid/index/v2/ReleaseChannelV2 {
+ public static final field Companion Lorg/fdroid/index/v2/ReleaseChannelV2$Companion;
+ public fun (Ljava/util/Map;Ljava/util/Map;)V
+ public synthetic fun (Ljava/util/Map;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/util/Map;
+ public final fun component2 ()Ljava/util/Map;
+ public final fun copy (Ljava/util/Map;Ljava/util/Map;)Lorg/fdroid/index/v2/ReleaseChannelV2;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v2/ReleaseChannelV2;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/ReleaseChannelV2;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getDescription ()Ljava/util/Map;
+ public final fun getName ()Ljava/util/Map;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v2/ReleaseChannelV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/ReleaseChannelV2$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/ReleaseChannelV2;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/ReleaseChannelV2;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/ReleaseChannelV2$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/RepoV2 {
+ public static final field Companion Lorg/fdroid/index/v2/RepoV2$Companion;
+ public fun (Ljava/util/Map;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/List;JLjava/util/Map;Ljava/util/Map;Ljava/util/Map;)V
+ public synthetic fun (Ljava/util/Map;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/List;JLjava/util/Map;Ljava/util/Map;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/util/Map;
+ public final fun component10 ()Ljava/util/Map;
+ public final fun component2 ()Ljava/util/Map;
+ public final fun component3 ()Ljava/lang/String;
+ public final fun component4 ()Ljava/lang/String;
+ public final fun component5 ()Ljava/util/Map;
+ public final fun component6 ()Ljava/util/List;
+ public final fun component7 ()J
+ public final fun component8 ()Ljava/util/Map;
+ public final fun component9 ()Ljava/util/Map;
+ public final fun copy (Ljava/util/Map;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/List;JLjava/util/Map;Ljava/util/Map;Ljava/util/Map;)Lorg/fdroid/index/v2/RepoV2;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v2/RepoV2;Ljava/util/Map;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/List;JLjava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/RepoV2;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getAddress ()Ljava/lang/String;
+ public final fun getAntiFeatures ()Ljava/util/Map;
+ public final fun getCategories ()Ljava/util/Map;
+ public final fun getDescription ()Ljava/util/Map;
+ public final fun getIcon ()Ljava/util/Map;
+ public final fun getMirrors ()Ljava/util/List;
+ public final fun getName ()Ljava/util/Map;
+ public final fun getReleaseChannels ()Ljava/util/Map;
+ public final fun getTimestamp ()J
+ public final fun getWebBaseUrl ()Ljava/lang/String;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+ public final fun walkFiles (Lkotlin/jvm/functions/Function1;)V
+}
+
+public final synthetic class org/fdroid/index/v2/RepoV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/RepoV2$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/RepoV2;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/RepoV2;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/RepoV2$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/Screenshots {
+ public static final field Companion Lorg/fdroid/index/v2/Screenshots$Companion;
+ public fun ()V
+ public fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V
+ public synthetic fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/util/Map;
+ public final fun component2 ()Ljava/util/Map;
+ public final fun component3 ()Ljava/util/Map;
+ public final fun component4 ()Ljava/util/Map;
+ public final fun component5 ()Ljava/util/Map;
+ public final fun copy (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Lorg/fdroid/index/v2/Screenshots;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v2/Screenshots;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/Screenshots;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getPhone ()Ljava/util/Map;
+ public final fun getSevenInch ()Ljava/util/Map;
+ public final fun getTenInch ()Ljava/util/Map;
+ public final fun getTv ()Ljava/util/Map;
+ public final fun getWear ()Ljava/util/Map;
+ public fun hashCode ()I
+ public final fun isNull ()Z
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v2/Screenshots$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/Screenshots$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/Screenshots;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/Screenshots;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/Screenshots$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/SignerV2 {
+ public static final field Companion Lorg/fdroid/index/v2/SignerV2$Companion;
+ public fun (Ljava/util/List;Z)V
+ public synthetic fun (Ljava/util/List;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/util/List;
+ public final fun component2 ()Z
+ public final fun copy (Ljava/util/List;Z)Lorg/fdroid/index/v2/SignerV2;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v2/SignerV2;Ljava/util/List;ZILjava/lang/Object;)Lorg/fdroid/index/v2/SignerV2;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getHasMultipleSigners ()Z
+ public final fun getSha256 ()Ljava/util/List;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v2/SignerV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/SignerV2$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/SignerV2;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/SignerV2;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/SignerV2$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/UsesSdkV2 {
+ public static final field Companion Lorg/fdroid/index/v2/UsesSdkV2$Companion;
+ public fun (II)V
+ public final fun component1 ()I
+ public final fun component2 ()I
+ public final fun copy (II)Lorg/fdroid/index/v2/UsesSdkV2;
+ public static synthetic fun copy$default (Lorg/fdroid/index/v2/UsesSdkV2;IIILjava/lang/Object;)Lorg/fdroid/index/v2/UsesSdkV2;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getMinSdkVersion ()I
+ public final fun getTargetSdkVersion ()I
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class org/fdroid/index/v2/UsesSdkV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lorg/fdroid/index/v2/UsesSdkV2$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/UsesSdkV2;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/UsesSdkV2;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class org/fdroid/index/v2/UsesSdkV2$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
diff --git a/libs/index/build.gradle b/libs/index/build.gradle
deleted file mode 100644
index 9c19f5487..000000000
--- a/libs/index/build.gradle
+++ /dev/null
@@ -1,124 +0,0 @@
-plugins {
- id 'org.jetbrains.kotlin.multiplatform'
- id 'org.jetbrains.kotlin.plugin.serialization'
- id 'com.android.library'
- id 'org.jetbrains.dokka'
- id 'com.vanniktech.maven.publish'
-}
-
-kotlin {
- androidTarget {
- compilations.configureEach {
- kotlinOptions.jvmTarget = '17'
- }
- publishLibraryVariants("release")
- }
-// def hostOs = System.getProperty("os.name")
-// def isMingwX64 = hostOs.startsWith("Windows")
-// def nativeTarget
-// if (hostOs == "Mac OS X") nativeTarget = macosX64('native')
-// else if (hostOs == "Linux") nativeTarget = linuxX64("native") {
-// binaries { sharedLib { baseName = "fdroid_index" } }
-// }
-// else if (isMingwX64) nativeTarget = mingwX64("native")
-// else throw new GradleException("Host OS is not supported in Kotlin/Native.")
-
- sourceSets {
- configureEach {
- languageSettings {
- optIn('kotlin.RequiresOptIn')
- explicitApi('strict')
- }
- }
- commonMain {
- dependencies {
- implementation libs.kotlinx.serialization.json
- implementation libs.microutils.kotlin.logging
-
- implementation project(":libs:download")
- implementation libs.ktor.io
- }
- }
- commonTest {
- dependencies {
- implementation project(":libs:sharedTest")
- implementation kotlin('test')
- implementation libs.goncalossilva.resources
- }
- }
- // JVM is disabled for now, because Android app is including it instead of Android library
- jvmMain {
- dependencies {
- }
- }
- jvmTest {
- dependencies {
- implementation libs.junit
- }
- }
- androidMain {
- dependencies {
- implementation libs.kotlin.reflect
- implementation libs.androidx.core.ktx
- }
- }
- androidUnitTest {
- dependencies {
- implementation libs.junit
- implementation libs.mockk
- }
- }
- androidInstrumentedTest {
- dependencies {
- implementation project(":libs:sharedTest")
- implementation kotlin('test')
- implementation libs.androidx.test.runner
- implementation libs.androidx.test.ext.junit
- }
- }
- nativeMain {
- dependencies {
- }
- }
- nativeTest {
-
- }
- }
-}
-
-android {
- namespace "org.fdroid.index"
- compileSdk libs.versions.compileSdk.get().toInteger()
- sourceSets {
- main.manifest.srcFile('src/androidMain/AndroidManifest.xml')
- getByName("androidTest").java.srcDir(file("src/androidAndroidTest/kotlin"))
- }
- defaultConfig {
- minSdkVersion 21
- consumerProguardFiles 'consumer-rules.pro'
-
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- testInstrumentationRunnerArguments disableAnalytics: 'true'
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_17
- targetCompatibility JavaVersion.VERSION_17
- }
- testOptions {
- unitTests.returnDefaultValues = true
- }
-}
-
-signing {
- useGpgCmd()
-}
-
-import org.jetbrains.dokka.gradle.DokkaTask
-tasks.withType(DokkaTask).configureEach {
- pluginsMapConfiguration.set(
- ["org.jetbrains.dokka.base.DokkaBase": """{
- "customAssets": ["${file("${rootProject.rootDir}/logo-icon.svg")}"],
- "footerMessage": "© 2010-2022 F-Droid Limited and Contributors"
- }"""]
- )
-}
diff --git a/libs/index/build.gradle.kts b/libs/index/build.gradle.kts
new file mode 100644
index 000000000..d172b8909
--- /dev/null
+++ b/libs/index/build.gradle.kts
@@ -0,0 +1,102 @@
+plugins {
+ alias(libs.plugins.jetbrains.kotlin.multiplatform)
+ alias(libs.plugins.jetbrains.kotlin.plugin.serialization)
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.jetbrains.dokka)
+ alias(libs.plugins.vanniktech.maven.publish)
+}
+
+kotlin {
+ androidTarget {
+ compilerOptions {
+ jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
+ }
+ publishLibraryVariants("release")
+ }
+ compilerOptions {
+ optIn.add("kotlin.RequiresOptIn")
+ }
+ explicitApi()
+ @OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class)
+ abiValidation {
+ enabled = true
+ }
+ sourceSets {
+ commonMain {
+ dependencies {
+ implementation(project(":libs:core"))
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.microutils.kotlin.logging)
+ }
+ }
+ commonTest {
+ dependencies {
+ implementation(project(":libs:sharedTest"))
+ implementation(kotlin("test"))
+ implementation(libs.goncalossilva.resources)
+ }
+ }
+ // JVM is disabled for now, because Android app is including it instead of Android library
+ jvmMain {
+ dependencies {
+ }
+ }
+ jvmTest {
+ dependencies {
+ implementation(libs.junit)
+ }
+ }
+ androidMain {
+ dependencies {
+ implementation(libs.kotlin.reflect)
+ implementation(libs.androidx.core.ktx)
+ }
+ }
+ androidUnitTest {
+ dependencies {
+ implementation(libs.junit)
+ implementation(libs.mockk)
+ }
+ }
+ androidInstrumentedTest {
+ dependencies {
+ implementation(project(":libs:sharedTest"))
+ implementation(kotlin("test"))
+ implementation(libs.androidx.test.runner)
+ implementation(libs.androidx.test.ext.junit)
+ }
+ }
+ }
+}
+
+android {
+ namespace = "org.fdroid.index"
+ @Suppress("ktlint:standard:chain-method-continuation")
+ compileSdk = libs.versions.compileSdk.get().toInt()
+ defaultConfig {
+ minSdk = 21
+ consumerProguardFiles("consumer-rules.pro")
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunnerArguments["disableAnalytics"] = "true"
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ testOptions {
+ unitTests {
+ isIncludeAndroidResources = true
+ }
+ }
+}
+
+signing {
+ useGpgCmd()
+}
+
+dokka {
+ pluginsConfiguration.html {
+ customAssets.from("${file("${rootProject.rootDir}/logo-icon.svg")}")
+ footerMessage.set("© 2010-2025 F-Droid Limited and Contributors")
+ }
+}
diff --git a/libs/index/gradle.properties b/libs/index/gradle.properties
index 704e98b76..96bdb7818 100644
--- a/libs/index/gradle.properties
+++ b/libs/index/gradle.properties
@@ -1,5 +1,5 @@
POM_ARTIFACT_ID=index
-VERSION_NAME=0.1.1
+VERSION_NAME=0.2.0
POM_NAME=F-Droid index library
POM_DESCRIPTION=A Kotlin multi-platform library to parse version 1 and 2 of the F-Droid index format.
diff --git a/libs/index/src/androidUnitTest/kotlin/org/fdroid/UpdateCheckerTest.kt b/libs/index/src/androidUnitTest/kotlin/org/fdroid/UpdateCheckerTest.kt
index 8a6ba5697..29fcaa51f 100644
--- a/libs/index/src/androidUnitTest/kotlin/org/fdroid/UpdateCheckerTest.kt
+++ b/libs/index/src/androidUnitTest/kotlin/org/fdroid/UpdateCheckerTest.kt
@@ -14,9 +14,9 @@ internal class UpdateCheckerTest {
private val signer = "9f9261f0b911c60f8db722f5d430a9e9d557a3f8078ce43e1c07522ef41efedb"
private val signerV2 = 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 version1 = Version(1, "1", 23, 1)
+ private val version2 = Version(2, "2", 23, 2)
+ private val version3 = Version(3, "2", 23, 3)
private val versions = listOf(version3, version2, version1)
@Test
@@ -181,6 +181,9 @@ internal class UpdateCheckerTest {
private data class Version(
override val versionCode: Long,
+ override val versionName: String,
+ override val added: Long,
+ override val size: Long?,
override val signer: SignerV2? = null,
override val releaseChannels: List? = null,
// the manifest is only needed for compatibility checking which we can test differently
diff --git a/libs/index/src/commonMain/kotlin/org/fdroid/index/v2/PackageV2.kt b/libs/index/src/commonMain/kotlin/org/fdroid/index/v2/PackageV2.kt
index aba140323..25fcd2757 100644
--- a/libs/index/src/commonMain/kotlin/org/fdroid/index/v2/PackageV2.kt
+++ b/libs/index/src/commonMain/kotlin/org/fdroid/index/v2/PackageV2.kt
@@ -2,8 +2,6 @@ package org.fdroid.index.v2
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
-import kotlinx.serialization.decodeFromString
-import kotlinx.serialization.encodeToString
import org.fdroid.IndexFile
import org.fdroid.index.IndexParser
@@ -78,6 +76,9 @@ public data class Screenshots(
public interface PackageVersion {
public val versionCode: Long
+ public val versionName: String
+ public val added: Long
+ public val size: Long?
public val signer: SignerV2?
public val releaseChannels: List?
public val packageManifest: PackageManifest
@@ -88,7 +89,7 @@ public const val ANTI_FEATURE_KNOWN_VULNERABILITY: String = "KnownVuln"
@Serializable
public data class PackageVersionV2(
- val added: Long,
+ override val added: Long,
val file: FileV1,
val src: FileV2? = null,
val manifest: ManifestV2,
@@ -97,6 +98,8 @@ public data class PackageVersionV2(
val whatsNew: LocalizedTextV2 = emptyMap(),
) : PackageVersion {
override val versionCode: Long = manifest.versionCode
+ override val versionName: String = manifest.versionName
+ override val size: Long? = file.size
override val signer: SignerV2? = manifest.signer
override val packageManifest: PackageManifest = manifest
override val hasKnownVulnerability: Boolean
diff --git a/libs/sharedTest/build.gradle b/libs/sharedTest/build.gradle
deleted file mode 100644
index f90cf96ac..000000000
--- a/libs/sharedTest/build.gradle
+++ /dev/null
@@ -1,29 +0,0 @@
-plugins {
- id 'kotlin-android'
- id 'com.android.library'
-}
-
-// not really an Android library, but index is not publishing for JVM at the moment
-android {
- namespace 'org.fdroid.test'
- compileSdk libs.versions.compileSdk.get().toInteger()
- defaultConfig {
- minSdkVersion 21
- }
-
- kotlinOptions {
- jvmTarget = '17'
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_17
- targetCompatibility JavaVersion.VERSION_17
- }
-}
-
-dependencies {
- implementation project(":libs:download")
- implementation project(":libs:index")
-
- implementation libs.kotlin.test
- implementation libs.kotlinx.serialization.json
-}
diff --git a/libs/sharedTest/build.gradle.kts b/libs/sharedTest/build.gradle.kts
new file mode 100644
index 000000000..9c93e2fe7
--- /dev/null
+++ b/libs/sharedTest/build.gradle.kts
@@ -0,0 +1,32 @@
+plugins {
+ alias(libs.plugins.jetbrains.kotlin.android)
+ alias(libs.plugins.android.library)
+}
+
+// not really an Android library, but index is not publishing for JVM at the moment
+android {
+ namespace = "org.fdroid.test"
+ @Suppress("ktlint:standard:chain-method-continuation")
+ compileSdk = libs.versions.compileSdk.get().toInt()
+ defaultConfig {
+ minSdk = 21
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+}
+
+kotlin {
+ compilerOptions {
+ jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
+ }
+}
+
+dependencies {
+ implementation(project(":libs:download"))
+ implementation(project(":libs:index"))
+
+ implementation(libs.kotlin.test)
+ implementation(libs.kotlinx.serialization.json)
+}
diff --git a/libs/sharedTest/src/main/assets/diff-empty-max/1337.json b/libs/sharedTest/src/main/assets/diff-empty-max/1337.json
index a07819f0c..9a76377dd 100644
--- a/libs/sharedTest/src/main/assets/diff-empty-max/1337.json
+++ b/libs/sharedTest/src/main/assets/diff-empty-max/1337.json
@@ -320,15 +320,18 @@
"metadata": {
"name": {
"en-US": "App3",
- "en": "en "
+ "en": "en ",
+ "zh-CN": "自由软件仓库"
},
"summary": {
"en-US": "App3 summary",
- "en": "en "
+ "en": "en ",
+ "ja": "这个仓库中的"
},
"description": {
"en-US": "App3 description",
- "en": "en "
+ "en": "en ",
+ "ko-KR": "切始终是从"
},
"added": 1234567890,
"lastUpdated": 9223372036854775807,
diff --git a/libs/sharedTest/src/main/assets/diff-empty-max/23.json b/libs/sharedTest/src/main/assets/diff-empty-max/23.json
index 51e398b82..314a6844b 100644
--- a/libs/sharedTest/src/main/assets/diff-empty-max/23.json
+++ b/libs/sharedTest/src/main/assets/diff-empty-max/23.json
@@ -596,15 +596,18 @@
"metadata": {
"name": {
"en-US": "App3",
- "en": "en "
+ "en": "en ",
+ "zh-CN": "自由软件仓库"
},
"summary": {
"en-US": "App3 summary",
- "en": "en "
+ "en": "en ",
+ "ja": "这个仓库中的"
},
"description": {
"en-US": "App3 description",
- "en": "en "
+ "en": "en ",
+ "ko-KR": "切始终是从"
},
"added": 1234567890,
"lastUpdated": 9223372036854775807,
diff --git a/libs/sharedTest/src/main/assets/diff-empty-max/42.json b/libs/sharedTest/src/main/assets/diff-empty-max/42.json
index aebb63085..ce3679463 100644
--- a/libs/sharedTest/src/main/assets/diff-empty-max/42.json
+++ b/libs/sharedTest/src/main/assets/diff-empty-max/42.json
@@ -593,15 +593,18 @@
"metadata": {
"name": {
"en-US": "App3",
- "en": "en "
+ "en": "en ",
+ "zh-CN": "自由软件仓库"
},
"summary": {
"en-US": "App3 summary",
- "en": "en "
+ "en": "en ",
+ "ja": "这个仓库中的"
},
"description": {
"en-US": "App3 description",
- "en": "en "
+ "en": "en ",
+ "ko-KR": "切始终是从"
},
"added": 1234567890,
"lastUpdated": 9223372036854775807,
diff --git a/libs/sharedTest/src/main/assets/index-max-v2.json b/libs/sharedTest/src/main/assets/index-max-v2.json
index 6be919aac..4da00689b 100644
--- a/libs/sharedTest/src/main/assets/index-max-v2.json
+++ b/libs/sharedTest/src/main/assets/index-max-v2.json
@@ -603,15 +603,18 @@
"metadata": {
"name": {
"en-US": "App3",
- "en": "en "
+ "en": "en ",
+ "zh-CN": "自由软件仓库"
},
"summary": {
"en-US": "App3 summary",
- "en": "en "
+ "en": "en ",
+ "ja": "这个仓库中的"
},
"description": {
"en-US": "App3 description",
- "en": "en "
+ "en": "en ",
+ "ko-KR": "切始终是从"
},
"added": 1234567890,
"lastUpdated": 9223372036854775807,
diff --git a/libs/sharedTest/src/main/kotlin/org/fdroid/test/TestDataV2.kt b/libs/sharedTest/src/main/kotlin/org/fdroid/test/TestDataV2.kt
index 6dcc611b5..de53c1b8a 100644
--- a/libs/sharedTest/src/main/kotlin/org/fdroid/test/TestDataV2.kt
+++ b/libs/sharedTest/src/main/kotlin/org/fdroid/test/TestDataV2.kt
@@ -1055,14 +1055,17 @@ object TestDataMaxV2 {
name = mapOf(
LOCALE to "App3",
"en" to "en ",
+ "zh-CN" to "自由软件仓库",
),
summary = mapOf(
LOCALE to "App3 summary",
"en" to "en ",
+ "ja" to "这个仓库中的",
),
description = mapOf(
LOCALE to "App3 description",
"en" to "en ",
+ "ko-KR" to "切始终是从",
),
added = 1234567890,
lastUpdated = Long.MAX_VALUE,
diff --git a/settings.gradle b/settings.gradle
index 2b7c73528..23b740ff3 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -7,6 +7,7 @@ pluginManagement {
}
include ':app'
+include ':libs:core'
include ':libs:sharedTest'
include ':libs:download'
include ':libs:index'