From 4579b82a4c543f74f89462b2fd2b2aebe4761dbd Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 14 Oct 2025 15:09:29 -0300 Subject: [PATCH 01/27] [download] verify already downloaded files --- .../org/fdroid/download/HttpDownloaderV2.kt | 35 ++++++++++++- .../org/fdroid/download/HttpDownloaderTest.kt | 49 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) 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/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() From c5cdcdfd3f06e4a301a7db6903daf20780cfa825 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 1 Oct 2025 08:47:02 -0300 Subject: [PATCH 02/27] [download] limit download progress reporting to once per second More often isn't needed and runs into issues with UI refresh rates and worse notification rate-limiting which causes important notifications to get missed --- .../src/androidMain/kotlin/org/fdroid/download/Downloader.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From e99840458e85440d9510e92b166d5a81c3ac17f1 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 2 Jul 2025 11:44:01 -0300 Subject: [PATCH 03/27] [download] add coil support --- gradle/verification-metadata.xml | 10 ++++ libs/download/build.gradle | 4 ++ .../download/coil/DownloadRequestFetcher.kt | 53 +++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 libs/download/src/androidMain/kotlin/org/fdroid/download/coil/DownloadRequestFetcher.kt diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 255cf9d46..2905d69db 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -10627,6 +10627,16 @@ + + + + + + + + + + diff --git a/libs/download/build.gradle b/libs/download/build.gradle index 6de2c6176..347d70296 100644 --- a/libs/download/build.gradle +++ b/libs/download/build.gradle @@ -59,6 +59,10 @@ kotlin { transitive = 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") { + transitive = false // we don't need all that it pulls in, just the basics + } + implementation "javax.inject:javax.inject:1" } } androidUnitTest { 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..7b863412f --- /dev/null +++ b/libs/download/src/androidMain/kotlin/org/fdroid/download/coil/DownloadRequestFetcher.kt @@ -0,0 +1,53 @@ +package org.fdroid.download.coil + +import coil3.ImageLoader +import coil3.decode.DataSource +import coil3.decode.ImageSource +import coil3.fetch.FetchResult +import coil3.fetch.Fetcher +import coil3.fetch.SourceFetchResult +import io.ktor.utils.io.jvm.javaio.toInputStream +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 javax.inject.Inject + +public class DownloadRequestFetcher( + private val httpManager: HttpManager, + private val downloadRequest: DownloadRequest, +) : Fetcher { + + override suspend fun fetch(): FetchResult? { + // 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 resultStream = if (sha256 == null) { + inputStream + } else { + AutoVerifyingInputStream(inputStream, sha256) + } + return SourceFetchResult( + source = ImageSource( + source = resultStream.source().buffer(), + fileSystem = FileSystem.SYSTEM, + metadata = null, + ), + mimeType = null, + dataSource = DataSource.NETWORK, + ) + } + + public class Factory @Inject constructor( + private val httpManager: HttpManager, + ) : Fetcher.Factory { + override fun create( + data: DownloadRequest, + options: coil3.request.Options, + imageLoader: ImageLoader + ): Fetcher? = DownloadRequestFetcher(httpManager, data) + } +} From a47423ef01bd1df337e8f42dfc8ae115f81c9e33 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 22 Oct 2025 10:13:09 -0300 Subject: [PATCH 04/27] [download] implement coil disk caching unfortunately, in coil disk caching is not generic, but deeply integrated in their NetworkFetcher, so if you need a custom Fetcher then you need to implement disk caching yourself. Maybe in the future, we can hook into their NetworkFetcher and try to use our mirror logic around it. This or they make disk caching work for all fetchers. --- .../download/coil/DownloadRequestFetcher.kt | 135 +++++++++++++++--- .../download/glide/DownloadRequestLoader.kt | 10 +- 2 files changed, 123 insertions(+), 22 deletions(-) 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 index 7b863412f..dbf7c1e24 100644 --- a/libs/download/src/androidMain/kotlin/org/fdroid/download/coil/DownloadRequestFetcher.kt +++ b/libs/download/src/androidMain/kotlin/org/fdroid/download/coil/DownloadRequestFetcher.kt @@ -1,43 +1,135 @@ +/** + * 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? { - // 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 resultStream = if (sha256 == null) { - inputStream - } else { - AutoVerifyingInputStream(inputStream, sha256) + 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() } - return SourceFetchResult( - source = ImageSource( - source = resultStream.source().buffer(), - fileSystem = FileSystem.SYSTEM, - metadata = null, - ), - mimeType = null, - dataSource = DataSource.NETWORK, + } + + 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, ) } @@ -46,8 +138,13 @@ public class DownloadRequestFetcher( ) : Fetcher.Factory { override fun create( data: DownloadRequest, - options: coil3.request.Options, - imageLoader: ImageLoader - ): Fetcher? = DownloadRequestFetcher(httpManager, data) + 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) } From 0fac4d96d00e12a177fc3f96da997f495a8d1340 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 17 Oct 2025 10:33:11 -0300 Subject: [PATCH 05/27] [download] mirror share uri: fingerprint is non-null now --- .../src/commonMain/kotlin/org/fdroid/download/Mirror.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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() From 982b300e6729966aef8b8babb5091103aa009353 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 14 Jul 2025 17:22:22 -0300 Subject: [PATCH 06/27] [index] add versionName to PackageVersion --- .../kotlin/org/fdroid/UpdateCheckerTest.kt | 9 ++++++--- .../commonMain/kotlin/org/fdroid/index/v2/PackageV2.kt | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) 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 From 31d5832d73bc54c5ef6d1cbad1569cb7ed022a47 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 30 Jun 2025 15:22:53 -0300 Subject: [PATCH 07/27] [db] improve RepoAdder --- .../main/java/org/fdroid/repo/RepoAdder.kt | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) 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..06d044d34 100644 --- a/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt +++ b/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt @@ -10,12 +10,15 @@ 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.flow.MutableStateFlow +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.SerializationException @@ -174,30 +177,36 @@ internal class RepoAdder( 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 if (finalRepo == null) { - addRepoState.value = AddRepoError(INVALID_INDEX) + onError(AddRepoError(INVALID_INDEX)) } else { addRepoState.value = Fetching(fetchUrl, finalRepo, apps, fetchResult, done = true) } } + private suspend fun onError(state: AddRepoError) { + if (currentCoroutineContext().isActive) { + addRepoState.value = state + } + } + private suspend fun fetchRepo( uri: Uri, fingerprint: String?, @@ -226,7 +235,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('/') @@ -261,7 +270,7 @@ internal class RepoAdder( } @WorkerThread - internal fun addFetchedRepository(): Repository? { + internal suspend fun addFetchedRepository(): Repository? { // prevent double calls (e.g. caused by double tapping a UI button) if (addRepoState.compareAndSet(Adding, Adding)) return null @@ -289,7 +298,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, ) @@ -348,7 +357,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, ) @@ -364,7 +373,7 @@ internal class RepoAdder( // 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 +407,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() } From 0f3f2d6f80e534ca074ed30b83a1a7a401ec9341 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 31 Jul 2025 12:04:42 -0300 Subject: [PATCH 08/27] [db] improve search This now also searches in descriptions, author and package names. The search also considers all languages now and is insensitive to diacritics in most languages. The AppMetadataFts needed to be re-created for this to work. A migration and a test for this have been added. --- .../9.json | 1033 +++++++++++++++++ .../database/CountryCodeMigrationTest.kt | 2 +- .../database/FtsAddColumnsMigrationTest.kt | 240 ++++ .../FtsCaseInsensitiveMigrationTest.kt | 23 +- .../fdroid/database/MultiRepoMigrationTest.kt | 6 +- .../database/PreferredRepoMigrationTest.kt | 2 +- .../database/RepoCertNonNullMigrationTest.kt | 2 +- .../src/main/java/org/fdroid/database/App.kt | 18 +- .../org/fdroid/database/FDroidDatabase.kt | 3 +- .../fdroid/database/FDroidDatabaseHolder.kt | 2 +- .../java/org/fdroid/database/Migrations.kt | 63 + 11 files changed, 1363 insertions(+), 31 deletions(-) create mode 100644 libs/database/schemas/org.fdroid.database.FDroidDatabaseInt/9.json create mode 100644 libs/database/src/dbTest/java/org/fdroid/database/FtsAddColumnsMigrationTest.kt 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/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/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/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/main/java/org/fdroid/database/App.kt b/libs/database/src/main/java/org/fdroid/database/App.kt index 35d06f33e..7c4fbcfd2 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 @@ -127,16 +128,23 @@ internal fun MetadataV2.toAppMetadata( @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" 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..b3c4160cc 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! ], ) 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')") + } +} From 23dde0bc9bd3e99c69d5dce4f09bba1687744106 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 31 Jul 2025 15:36:31 -0300 Subject: [PATCH 09/27] [db] Better support search for CJK languages by inserting zero whitespace between their characters to help the existing sqlite FTS tokenizers to split them up. We have considered splitting them up only at word boundaries, but after consulting native speakers decided to do splitting by chars instead. Doing this is a hack, but due to the limitations of tokenizers currently available with sqlite, we saw no better solution. While the ICU tokenizer is available as well, it doesn't handle diacritics in other languages. The zero whitespace is added to zh, ja and ko locales when saving their text to the database. It happens for app names, summaries and descriptions either when loading a full index or when applying diffs. Tests have been added for both cases. --- .../dbTest/java/org/fdroid/database/DbTest.kt | 7 +++- .../org/fdroid/database/IndexV2DiffTest.kt | 31 +++++++++++++++++ .../src/main/java/org/fdroid/database/App.kt | 33 +++++++++++++++++-- .../main/java/org/fdroid/database/AppDao.kt | 20 +++++++---- .../src/main/assets/diff-empty-max/1337.json | 9 +++-- .../src/main/assets/diff-empty-max/23.json | 9 +++-- .../src/main/assets/diff-empty-max/42.json | 9 +++-- .../src/main/assets/index-max-v2.json | 9 +++-- .../main/kotlin/org/fdroid/test/TestDataV2.kt | 3 ++ 9 files changed, 107 insertions(+), 23 deletions(-) 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/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/main/java/org/fdroid/database/App.kt b/libs/database/src/main/java/org/fdroid/database/App.kt index 7c4fbcfd2..e5dfbf6cc 100644 --- a/libs/database/src/main/java/org/fdroid/database/App.kt +++ b/libs/database/src/main/java/org/fdroid/database/App.kt @@ -97,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, @@ -125,6 +125,33 @@ 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, 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..2cc79e034 100644 --- a/libs/database/src/main/java/org/fdroid/database/AppDao.kt +++ b/libs/database/src/main/java/org/fdroid/database/AppDao.kt @@ -237,13 +237,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) 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, From 7c490a2903cb1104f8b236bc2a892f1af27e5113 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 17 Jul 2025 17:43:20 -0300 Subject: [PATCH 10/27] [db] expose installedVersionName for UpdatableApp so that we can show to what version the update version would get applied to --- libs/database/src/main/java/org/fdroid/database/App.kt | 1 + .../src/main/java/org/fdroid/database/DbUpdateChecker.kt | 3 +++ 2 files changed, 4 insertions(+) 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 e5dfbf6cc..54a40d004 100644 --- a/libs/database/src/main/java/org/fdroid/database/App.kt +++ b/libs/database/src/main/java/org/fdroid/database/App.kt @@ -379,6 +379,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/DbUpdateChecker.kt b/libs/database/src/main/java/org/fdroid/database/DbUpdateChecker.kt index 887a9226b..100878b09 100644 --- a/libs/database/src/main/java/org/fdroid/database/DbUpdateChecker.kt +++ b/libs/database/src/main/java/org/fdroid/database/DbUpdateChecker.kt @@ -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, From b33800bf5cc8c6f15ce6c9815a4b023a9f60da60 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Sat, 12 Jul 2025 10:21:52 -0300 Subject: [PATCH 11/27] [db] Add more information to versions such as versionName and added date. Also allow passing in UpdateChecker for DbUpdateChecker --- .../fdroid/database/DbUpdateCheckerTest.kt | 2 +- .../org/fdroid/database/DbUpdateChecker.kt | 2 +- .../main/java/org/fdroid/database/Version.kt | 27 ++++++++++++++++--- 3 files changed, 25 insertions(+), 6 deletions(-) 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/main/java/org/fdroid/database/DbUpdateChecker.kt b/libs/database/src/main/java/org/fdroid/database/DbUpdateChecker.kt index 100878b09..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. 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() From dff226a935fbf970a290cbe070245ba90e60fa42 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 25 Jul 2025 17:24:26 -0300 Subject: [PATCH 12/27] [db] Add different search API for more flexible search result sorting and faster search --- .../main/java/org/fdroid/database/AppDao.kt | 16 +++ .../java/org/fdroid/database/AppSearchItem.kt | 98 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 libs/database/src/main/java/org/fdroid/database/AppSearchItem.kt 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 2cc79e034..1ac9eb469 100644 --- a/libs/database/src/main/java/org/fdroid/database/AppDao.kt +++ b/libs/database/src/main/java/org/fdroid/database/AppDao.kt @@ -141,6 +141,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 @@ -706,6 +708,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" +) From a45c9763e5b223546fcbfddb1c21eb93dccb388c Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 8 Aug 2025 16:03:12 -0300 Subject: [PATCH 13/27] [db] retrieve categories to enable post-query filtering --- .../src/main/java/org/fdroid/database/App.kt | 2 ++ .../main/java/org/fdroid/database/AppDao.kt | 26 +++++++++---------- 2 files changed, 15 insertions(+), 13 deletions(-) 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 54a40d004..c2ec54ced 100644 --- a/libs/database/src/main/java/org/fdroid/database/App.kt +++ b/libs/database/src/main/java/org/fdroid/database/App.kt @@ -288,6 +288,7 @@ public data class AppOverviewItem internal constructor( public override val name: String? = null, @ColumnInfo(name = "localizedSummary") public override val summary: String? = null, + public val categories: List? = null, internal val antiFeatures: Map? = null, @Relation( parentColumn = "packageName", @@ -326,6 +327,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", 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 1ac9eb469..1ec1bb399 100644 --- a/libs/database/src/main/java/org/fdroid/database/AppDao.kt +++ b/libs/database/src/main/java/org/fdroid/database/AppDao.kt @@ -374,7 +374,7 @@ internal interface AppDaoInt : AppDao { @Transaction @Query("""SELECT repoId, packageName, app.added, app.lastUpdated, localizedName, - localizedSummary, version.antiFeatures, app.isCompatible + localizedSummary, categories, version.antiFeatures, app.isCompatible FROM ${AppMetadata.TABLE} AS app JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId) JOIN PreferredRepo USING (packageName) @@ -389,7 +389,7 @@ internal interface AppDaoInt : AppDao { @Transaction @Query("""SELECT repoId, packageName, app.added, app.lastUpdated, localizedName, - localizedSummary, version.antiFeatures, app.isCompatible + localizedSummary, categories, version.antiFeatures, app.isCompatible FROM ${AppMetadata.TABLE} AS app JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId) JOIN PreferredRepo USING (packageName) @@ -409,7 +409,7 @@ 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, categories, app.isCompatible FROM ${AppMetadata.TABLE} AS app WHERE repoId = :repoId AND packageName = :packageName""") fun getAppOverviewItem(repoId: Long, packageName: String): AppOverviewItem? @@ -438,7 +438,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) @@ -469,7 +469,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""") @@ -494,7 +494,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) @@ -545,7 +545,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) @@ -562,7 +562,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) @@ -582,7 +582,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 ( @@ -594,7 +594,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) @@ -607,7 +607,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) @@ -626,7 +626,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) @@ -638,7 +638,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 ( From 290cfcee3433195a3dc6071ab66090bdfa00cf26 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 29 Aug 2025 17:07:34 -0300 Subject: [PATCH 14/27] [db] New AppOverviewItems AppDao methods --- .../fdroid/database/AppOverviewItemsTest.kt | 134 +++++++++++---- .../main/java/org/fdroid/database/AppDao.kt | 154 ++++++++++++++++++ 2 files changed, 256 insertions(+), 32 deletions(-) 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..f50a003a2 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,6 +416,65 @@ 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) 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 1ec1bb399..91dffbd96 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. @@ -413,6 +462,111 @@ internal interface AppDaoInt : AppDao { 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, 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, 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 // From 3284745c8193ce249fee5aa477f95d15ed258d69 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 1 Sep 2025 15:50:43 -0300 Subject: [PATCH 15/27] [db] deprecate caching of localized app name and summary This led to many hard to debug issues in the past. It is easier to always fetch fresh data and not cache it. Previously, we needed the cache as a search index. Now, search uses all localizations, so the cache isn't needed anymore. --- .../org/fdroid/database/AppOverviewItemsTest.kt | 3 ++- .../src/main/java/org/fdroid/database/App.kt | 14 ++++++++++++++ .../src/main/java/org/fdroid/database/AppDao.kt | 11 ++++++----- .../java/org/fdroid/database/FDroidDatabase.kt | 3 +++ .../java/org/fdroid/repo/RepoV2StreamReceiver.kt | 2 ++ 5 files changed, 27 insertions(+), 6 deletions(-) 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 f50a003a2..c029272e5 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/AppOverviewItemsTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/AppOverviewItemsTest.kt @@ -481,7 +481,8 @@ internal class AppOverviewItemsTest : AppTest() { 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/main/java/org/fdroid/database/App.kt b/libs/database/src/main/java/org/fdroid/database/App.kt index c2ec54ced..fed489aa6 100644 --- a/libs/database/src/main/java/org/fdroid/database/App.kt +++ b/libs/database/src/main/java/org/fdroid/database/App.kt @@ -285,9 +285,15 @@ 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( @@ -300,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 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 91dffbd96..9541bdfae 100644 --- a/libs/database/src/main/java/org/fdroid/database/AppDao.kt +++ b/libs/database/src/main/java/org/fdroid/database/AppDao.kt @@ -376,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?) @@ -423,7 +424,7 @@ internal interface AppDaoInt : AppDao { @Transaction @Query("""SELECT repoId, packageName, app.added, app.lastUpdated, localizedName, - localizedSummary, categories, 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) @@ -438,7 +439,7 @@ internal interface AppDaoInt : AppDao { @Transaction @Query("""SELECT repoId, packageName, app.added, app.lastUpdated, localizedName, - localizedSummary, categories, 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) @@ -458,7 +459,7 @@ internal interface AppDaoInt : AppDao { @Transaction @SuppressWarnings(QUERY_MISMATCH) // no anti-features needed here @Query("""SELECT repoId, packageName, added, app.lastUpdated, localizedName, - localizedSummary, categories, 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? @@ -508,7 +509,7 @@ internal interface AppDaoInt : AppDao { @Transaction @Query("""SELECT repoId, packageName, app.added, app.lastUpdated, localizedName, - localizedSummary, categories, version.antiFeatures, app.isCompatible + 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""") @@ -543,7 +544,7 @@ internal interface AppDaoInt : AppDao { val queryBuilder = StringBuilder( """ SELECT repoId, packageName, app.added, app.lastUpdated, localizedName, - localizedSummary, categories, version.antiFeatures, app.isCompatible + 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) 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 b3c4160cc..04c98325e 100644 --- a/libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt +++ b/libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt @@ -60,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 { @@ -119,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/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( From 5ab649f14d859a833cd696df0e1822857487f2ae Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 14 Oct 2025 16:10:44 -0300 Subject: [PATCH 16/27] [db] optimize adding repos We now hang on to the index file while streaming it for repo preview purposes. This avoids having to re-download that file and we can properly add the repo right away. This then allows us to bring the user to the list of apps in that repository without it being initially empty. --- .../org/fdroid/index/v1/IndexV1UpdaterTest.kt | 2 +- .../org/fdroid/index/v2/IndexV2UpdaterTest.kt | 6 +- .../java/org/fdroid/index/IndexUpdater.kt | 4 +- .../main/java/org/fdroid/index/RepoManager.kt | 15 ++++- .../main/java/org/fdroid/index/RepoUpdater.kt | 10 +++- .../org/fdroid/index/v1/IndexV1Updater.kt | 2 +- .../org/fdroid/index/v2/IndexV2Updater.kt | 4 +- .../main/java/org/fdroid/repo/RepoAdder.kt | 49 +++++++++++---- .../main/java/org/fdroid/repo/RepoFetcher.kt | 8 ++- .../java/org/fdroid/repo/RepoV1Fetcher.kt | 18 +++--- .../java/org/fdroid/repo/RepoV2Fetcher.kt | 22 ++++--- .../java/org/fdroid/repo/SavingInputStream.kt | 45 ++++++++++++++ .../fdroid/repo/RepoAdderIntegrationTest.kt | 34 +++++++++-- .../java/org/fdroid/repo/RepoAdderTest.kt | 59 +++++++++++++++---- 14 files changed, 219 insertions(+), 59 deletions(-) create mode 100644 libs/database/src/main/java/org/fdroid/repo/SavingInputStream.kt 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/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..8a61fcfdc 100644 --- a/libs/database/src/main/java/org/fdroid/index/RepoManager.kt +++ b/libs/database/src/main/java/org/fdroid/index/RepoManager.kt @@ -15,6 +15,8 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow 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 @@ -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, ) 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 06d044d34..682f6164a 100644 --- a/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt +++ b/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt @@ -23,6 +23,7 @@ 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 @@ -34,6 +35,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 @@ -42,6 +45,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 @@ -57,11 +61,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)" @@ -72,6 +77,7 @@ public object Adding : AddRepoState() public class Added( public val repo: Repository, + public val updateResult: IndexUpdateResult?, ) : AddRepoState() public data class AddRepoError( @@ -103,6 +109,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, @@ -173,7 +180,7 @@ internal class RepoAdder( 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." } @@ -197,7 +204,7 @@ internal class RepoAdder( if (finalRepo == null) { onError(AddRepoError(INVALID_INDEX)) } else { - addRepoState.value = Fetching(fetchUrl, finalRepo, apps, fetchResult, done = true) + addRepoState.value = Fetching(fetchUrl, finalRepo, apps, fetchResult, indexFile) } } @@ -207,6 +214,10 @@ internal class RepoAdder( } } + /** + * 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?, @@ -214,10 +225,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 ) @@ -225,8 +235,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) } @@ -270,7 +279,7 @@ internal class RepoAdder( } @WorkerThread - internal suspend fun addFetchedRepository(): Repository? { + internal fun addFetchedRepository(): Repository? { // prevent double calls (e.g. caused by double tapping a UI button) if (addRepoState.compareAndSet(Adding, Adding)) return null @@ -280,6 +289,7 @@ internal class RepoAdder( // get current state before changing it val state = (addRepoState.value as? Fetching) ?: throw IllegalStateException("Unexpected state: ${addRepoState.value}") + log.info { "Moved to state \'Adding\'..." } addRepoState.value = Adding val repo = state.receivedRepo @@ -287,6 +297,7 @@ internal class RepoAdder( 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") @@ -314,6 +325,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" } } } @@ -330,11 +352,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() } 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/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..415fa0259 100644 --- a/libs/database/src/test/java/org/fdroid/repo/RepoAdderTest.kt +++ b/libs/database/src/test/java/org/fdroid/repo/RepoAdderTest.kt @@ -9,6 +9,7 @@ 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.Runs import io.mockk.coEvery import io.mockk.every @@ -21,8 +22,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 +39,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 +80,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 +100,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 +188,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 +197,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 +223,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 +232,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 +439,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 +475,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 +526,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 +593,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 +653,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 +716,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 +773,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 +850,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 +858,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 +902,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 { From 3d5acc401b433effc28dcc1c8ef0381b405311fb Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 17 Oct 2025 10:31:59 -0300 Subject: [PATCH 17/27] [db] Update repo shareUri to use fdroid.link --- .../src/main/java/org/fdroid/database/Repository.kt | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) 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", From 4ad356249a2916b8379d084375329fe4173502e7 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 17 Oct 2025 10:29:42 -0300 Subject: [PATCH 18/27] [db] Fix UI update when deleting repo --- .../src/main/java/org/fdroid/index/RepoManager.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) 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 8a61fcfdc..2f18bd24c 100644 --- a/libs/database/src/main/java/org/fdroid/index/RepoManager.kt +++ b/libs/database/src/main/java/org/fdroid/index/RepoManager.kt @@ -13,6 +13,7 @@ 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 @@ -145,6 +146,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 @@ -152,11 +161,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 - } } } From 77d812701596a7ef63fbada0633f621e91545791 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 17 Oct 2025 10:31:08 -0300 Subject: [PATCH 19/27] [db] Don't cancel index fetching after enabling archive This can cause other exceptions (e.g. in json parsing) which prevent repo from getting added properly. Also, when updating the archive for the first time, we can now re-use the downloaded index. --- .../database/src/main/java/org/fdroid/index/RepoManager.kt | 7 +------ libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt | 1 - 2 files changed, 1 insertion(+), 7 deletions(-) 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 2f18bd24c..1c47a150a 100644 --- a/libs/database/src/main/java/org/fdroid/index/RepoManager.kt +++ b/libs/database/src/main/java/org/fdroid/index/RepoManager.kt @@ -31,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 @@ -255,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/repo/RepoAdder.kt b/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt index 682f6164a..a26697da5 100644 --- a/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt +++ b/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt @@ -391,7 +391,6 @@ 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) { From 800d5bfe85e277ac138cbf3410ff3b43b4e619e4 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 20 Oct 2025 14:14:10 -0300 Subject: [PATCH 20/27] [db] Fix bug where repos/mirrors could be added twice This could happen when tapping a button quickly two times in the UI. While we had provisions to ensure proper states, these didn't work, because a parallel fetching job would mess with our state allowing the adding code to run more than once. --- .../main/java/org/fdroid/repo/RepoAdder.kt | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) 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 a26697da5..3f9c601c6 100644 --- a/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt +++ b/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt @@ -15,9 +15,10 @@ 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 @@ -168,15 +169,18 @@ 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 @@ -201,6 +205,7 @@ internal class RepoAdder( } // set final result val finalRepo = receivedRepo + coroutineContext.ensureActive() // ensure active before updating state if (finalRepo == null) { onError(AddRepoError(INVALID_INDEX)) } else { @@ -280,17 +285,17 @@ 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}") - log.info { "Moved to state \'Adding\'..." } - 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}") From cc2010ff63b74dfa300bf106657ed2556df2c517 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 29 Oct 2025 15:33:37 -0300 Subject: [PATCH 21/27] [db] Increase concurrency delay for RepoAdderTest still a hack, but synchronizing the coroutines doesn't seem trivial --- .../src/test/java/org/fdroid/repo/RepoAdderTest.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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 415fa0259..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,6 +8,7 @@ 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 @@ -970,7 +971,7 @@ internal class RepoAdderTest { url, expectedFetchResult, TestDataMinV2.repo, - ) { awaitItem -> + ) { val state = awaitItem() assertIs(state) assertEquals(TestDataMinV2.packages.size, state.apps.size) @@ -989,7 +990,7 @@ internal class RepoAdderTest { url, expectedFetchResult, TestDataMidV2.repo, - ) { awaitItem -> + ) { val state = awaitItem() assertIs(state) assertEquals(1, state.apps.size) @@ -1011,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()) @@ -1019,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) } @@ -1043,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() } } } From ba7731af6c2e1df0db9826cd912ae077d107997f Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 30 Oct 2025 17:27:38 -0300 Subject: [PATCH 22/27] Migrate build scripts from Groovy to Kotlin --- build.gradle | 1 + gradle/libs.versions.toml | 1 + gradle/verification-metadata.xml | 10 +++ libs/database/build.gradle | 136 ----------------------------- libs/database/build.gradle.kts | 138 ++++++++++++++++++++++++++++++ libs/download/build.gradle | 142 ------------------------------- libs/download/build.gradle.kts | 123 ++++++++++++++++++++++++++ libs/index/build.gradle | 124 --------------------------- libs/index/build.gradle.kts | 104 ++++++++++++++++++++++ libs/sharedTest/build.gradle | 29 ------- libs/sharedTest/build.gradle.kts | 32 +++++++ 11 files changed, 409 insertions(+), 431 deletions(-) delete mode 100644 libs/database/build.gradle create mode 100644 libs/database/build.gradle.kts delete mode 100644 libs/download/build.gradle create mode 100644 libs/download/build.gradle.kts delete mode 100644 libs/index/build.gradle create mode 100644 libs/index/build.gradle.kts delete mode 100644 libs/sharedTest/build.gradle create mode 100644 libs/sharedTest/build.gradle.kts diff --git a/build.gradle b/build.gradle index 3b2e04502..f2e9904ad 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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6625ab512..ea11f2cad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -167,6 +167,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 2905d69db..3d9eae7bf 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -11007,6 +11007,11 @@ + + + + + @@ -12284,6 +12289,11 @@ + + + + + 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..1e17ff6b9 --- /dev/null +++ b/libs/database/build.gradle.kts @@ -0,0 +1,138 @@ +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() + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + optIn.add("kotlin.RequiresOptIn") + } +} + +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(RoomSchemaArgProvider(File(projectDir, "schemas"))) +} + +signing { + useGpgCmd() +} + +tasks.withType().configureEach { + pluginsMapConfiguration.set( + mapOf( + "org.jetbrains.dokka.base.DokkaBase" to """{ + "customAssets": ["${file("${rootProject.rootDir}/logo-icon.svg")}"], + "footerMessage": "© 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/download/build.gradle b/libs/download/build.gradle deleted file mode 100644 index 347d70296..000000000 --- a/libs/download/build.gradle +++ /dev/null @@ -1,142 +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 - implementation("io.coil-kt.coil3:coil-core:3.3.0") { - transitive = 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 - } - } - 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..5b060b484 --- /dev/null +++ b/libs/download/build.gradle.kts @@ -0,0 +1,123 @@ +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() + sourceSets { + 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") { + 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() +} + +tasks.withType().configureEach { + pluginsMapConfiguration.set( + mapOf( + "org.jetbrains.dokka.base.DokkaBase" to """{ + "customAssets": ["${file("${rootProject.rootDir}/logo-icon.svg")}"], + "footerMessage": "© 2010-2025 F-Droid Limited and Contributors", + }""", + ), + ) +} 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..d07aea402 --- /dev/null +++ b/libs/index/build.gradle.kts @@ -0,0 +1,104 @@ +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() + sourceSets { + 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) + } + } + } +} + +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() +} + +tasks.withType().configureEach { + pluginsMapConfiguration.set( + mapOf( + "org.jetbrains.dokka.base.DokkaBase" to """{ + "customAssets": ["${file("${rootProject.rootDir}/logo-icon.svg")}"], + "footerMessage": "© 2010-2025 F-Droid Limited and Contributors", + }""", + ), + ) +} 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) +} From 911b337db73197425879a4899ba81291f5ce153b Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 31 Oct 2025 15:16:52 -0300 Subject: [PATCH 23/27] Untangle libraries slightly by creating a common core library --- gradle/libs.versions.toml | 2 - gradle/verification-metadata.xml | 5 ++ libs/core/.gitignore | 1 + libs/core/build.gradle.kts | 63 +++++++++++++++++++ .../commonMain/kotlin/org/fdroid/IndexFile.kt | 0 libs/database/build.gradle.kts | 3 +- libs/download/build.gradle.kts | 1 + libs/index/build.gradle.kts | 4 +- settings.gradle | 1 + 9 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 libs/core/.gitignore create mode 100644 libs/core/build.gradle.kts rename libs/{download => core}/src/commonMain/kotlin/org/fdroid/IndexFile.kt (100%) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ea11f2cad..f3075365b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 3d9eae7bf..bf3d468fb 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -19916,6 +19916,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/build.gradle.kts b/libs/core/build.gradle.kts new file mode 100644 index 000000000..1192836ac --- /dev/null +++ b/libs/core/build.gradle.kts @@ -0,0 +1,63 @@ +plugins { + alias(libs.plugins.jetbrains.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.jetbrains.dokka) + alias(libs.plugins.vanniktech.maven.publish) +} +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} +kotlin { + androidTarget { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } + publishLibraryVariants("release") + } + explicitApi() + 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" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +signing { + useGpgCmd() +} + +tasks.withType().configureEach { + pluginsMapConfiguration.set( + mapOf( + "org.jetbrains.dokka.base.DokkaBase" to """{ + "customAssets": ["${file("${rootProject.rootDir}/logo-icon.svg")}"], + "footerMessage": "© 2010-2025 F-Droid Limited and Contributors", + }""", + ), + ) +} 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/build.gradle.kts b/libs/database/build.gradle.kts index 1e17ff6b9..ba46e71a0 100644 --- a/libs/database/build.gradle.kts +++ b/libs/database/build.gradle.kts @@ -72,8 +72,9 @@ kotlin { } dependencies { - implementation(project(":libs:download")) + 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) diff --git a/libs/download/build.gradle.kts b/libs/download/build.gradle.kts index 5b060b484..8a13e9d1e 100644 --- a/libs/download/build.gradle.kts +++ b/libs/download/build.gradle.kts @@ -19,6 +19,7 @@ kotlin { sourceSets { commonMain { dependencies { + api(project(":libs:core")) api(libs.ktor.client.core) implementation(libs.microutils.kotlin.logging) } diff --git a/libs/index/build.gradle.kts b/libs/index/build.gradle.kts index d07aea402..fb4a5e399 100644 --- a/libs/index/build.gradle.kts +++ b/libs/index/build.gradle.kts @@ -20,11 +20,9 @@ kotlin { sourceSets { commonMain { dependencies { + implementation(project(":libs:core")) implementation(libs.kotlinx.serialization.json) implementation(libs.microutils.kotlin.logging) - - implementation(project(":libs:download")) - implementation(libs.ktor.io) } } commonTest { 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' From 1e4c9f39ed108b58ff718e4ebe23e3e7c31f4e78 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 31 Oct 2025 16:42:50 -0300 Subject: [PATCH 24/27] Enable Binary compatibility validation which is still experimental, but allows us to sport API breakage in CI. It still doesn't produce anything for the database lib. --- .gitlab-ci.yml | 2 +- libs/core/api/android/core.api | 8 + libs/core/build.gradle.kts | 4 + libs/database/api/database.api | 0 libs/database/build.gradle.kts | 4 + libs/download/api/android/download.api | 210 ++++ libs/download/build.gradle.kts | 4 + libs/index/api/android/index.api | 1252 ++++++++++++++++++++++++ libs/index/build.gradle.kts | 4 + 9 files changed, 1487 insertions(+), 1 deletion(-) create mode 100644 libs/core/api/android/core.api create mode 100644 libs/database/api/database.api create mode 100644 libs/download/api/android/download.api create mode 100644 libs/index/api/android/index.api diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8ac39438e..b8d84a631 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: 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 index 1192836ac..92f1918c9 100644 --- a/libs/core/build.gradle.kts +++ b/libs/core/build.gradle.kts @@ -16,6 +16,10 @@ kotlin { publishLibraryVariants("release") } explicitApi() + @OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class) + abiValidation { + enabled = true + } compilerOptions { optIn.add("kotlin.RequiresOptIn") } 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.kts b/libs/database/build.gradle.kts index ba46e71a0..3d94c7e56 100644 --- a/libs/database/build.gradle.kts +++ b/libs/database/build.gradle.kts @@ -65,6 +65,10 @@ android { 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") 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.kts b/libs/download/build.gradle.kts index 8a13e9d1e..199983420 100644 --- a/libs/download/build.gradle.kts +++ b/libs/download/build.gradle.kts @@ -16,6 +16,10 @@ kotlin { optIn.add("kotlin.RequiresOptIn") } explicitApi() + @OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class) + abiValidation { + enabled = true + } sourceSets { commonMain { dependencies { 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.kts b/libs/index/build.gradle.kts index fb4a5e399..e7917a4f0 100644 --- a/libs/index/build.gradle.kts +++ b/libs/index/build.gradle.kts @@ -17,6 +17,10 @@ kotlin { optIn.add("kotlin.RequiresOptIn") } explicitApi() + @OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class) + abiValidation { + enabled = true + } sourceSets { commonMain { dependencies { From 5a04b315d5717efd64c4d3dfb332ac002f9e8321 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 31 Oct 2025 17:19:19 -0300 Subject: [PATCH 25/27] Upgrade maven publishing plugin --- build.gradle | 6 --- gradle.properties | 5 ++- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 70 ++++++++++++++++++++++++++++++++ libs/core/build.gradle.kts | 26 +++++++++--- libs/core/gradle.properties | 7 ++++ libs/database/build.gradle.kts | 2 +- libs/database/gradle.properties | 2 +- libs/download/build.gradle.kts | 2 +- libs/download/gradle.properties | 2 +- libs/index/build.gradle.kts | 2 +- libs/index/gradle.properties | 2 +- 12 files changed, 108 insertions(+), 20 deletions(-) create mode 100644 libs/core/gradle.properties diff --git a/build.gradle b/build.gradle index f2e9904ad..8a9f49847 100644 --- a/build.gradle +++ b/build.gradle @@ -21,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 f3075365b..fe1495c65 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ kotlin = "2.2.0" 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" +mavenPublish = "0.34.0" jlleitschuhKtlint = "13.1.0" kotlinxSerializationJson = "1.9.0" # 1.4.1 because https://github.com/Kotlin/kotlinx.serialization/issues/2231 diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index bf3d468fb..ab9b0450d 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -10167,6 +10167,11 @@ + + + + + @@ -10308,6 +10313,11 @@ + + + + + @@ -10353,6 +10363,11 @@ + + + + + @@ -10378,11 +10393,21 @@ + + + + + + + + + + @@ -10460,12 +10485,22 @@ + + + + + + + + + + @@ -15128,6 +15163,11 @@ + + + + + @@ -15148,6 +15188,11 @@ + + + + + @@ -15294,6 +15339,11 @@ + + + + + @@ -15364,6 +15414,11 @@ + + + + + @@ -15384,6 +15439,11 @@ + + + + + @@ -15404,6 +15464,11 @@ + + + + + @@ -18121,6 +18186,11 @@ + + + + + diff --git a/libs/core/build.gradle.kts b/libs/core/build.gradle.kts index 92f1918c9..055616067 100644 --- a/libs/core/build.gradle.kts +++ b/libs/core/build.gradle.kts @@ -4,10 +4,6 @@ plugins { alias(libs.plugins.jetbrains.dokka) alias(libs.plugins.vanniktech.maven.publish) } -java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 -} kotlin { androidTarget { compilerOptions { @@ -45,6 +41,15 @@ android { 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 @@ -55,12 +60,23 @@ 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"), + ), + ) +} + tasks.withType().configureEach { pluginsMapConfiguration.set( mapOf( "org.jetbrains.dokka.base.DokkaBase" to """{ "customAssets": ["${file("${rootProject.rootDir}/logo-icon.svg")}"], - "footerMessage": "© 2010-2025 F-Droid Limited and Contributors", + "footerMessage": "© 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/database/build.gradle.kts b/libs/database/build.gradle.kts index 3d94c7e56..a1b122d4b 100644 --- a/libs/database/build.gradle.kts +++ b/libs/database/build.gradle.kts @@ -128,7 +128,7 @@ tasks.withType().configureEach { mapOf( "org.jetbrains.dokka.base.DokkaBase" to """{ "customAssets": ["${file("${rootProject.rootDir}/logo-icon.svg")}"], - "footerMessage": "© 2010-2025 F-Droid Limited and Contributors", + "footerMessage": "© 2010-2025 F-Droid Limited and Contributors" }""", ), ) 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/download/build.gradle.kts b/libs/download/build.gradle.kts index 199983420..8d1e0c0b2 100644 --- a/libs/download/build.gradle.kts +++ b/libs/download/build.gradle.kts @@ -121,7 +121,7 @@ tasks.withType().configureEach { mapOf( "org.jetbrains.dokka.base.DokkaBase" to """{ "customAssets": ["${file("${rootProject.rootDir}/logo-icon.svg")}"], - "footerMessage": "© 2010-2025 F-Droid Limited and Contributors", + "footerMessage": "© 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/index/build.gradle.kts b/libs/index/build.gradle.kts index e7917a4f0..875ebefc7 100644 --- a/libs/index/build.gradle.kts +++ b/libs/index/build.gradle.kts @@ -99,7 +99,7 @@ tasks.withType().configureEach { mapOf( "org.jetbrains.dokka.base.DokkaBase" to """{ "customAssets": ["${file("${rootProject.rootDir}/logo-icon.svg")}"], - "footerMessage": "© 2010-2025 F-Droid Limited and Contributors", + "footerMessage": "© 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. From b62233e8d2b80c7fe6d33801de8a26c60f58bea3 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 3 Nov 2025 10:07:35 -0300 Subject: [PATCH 26/27] Upgrade dependencies prior to library publishing --- gradle/libs.versions.toml | 12 +- gradle/verification-metadata.xml | 529 +++++++++++++++++++++++++++++++ 2 files changed, 535 insertions(+), 6 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fe1495c65..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" +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" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index ab9b0450d..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 @@ + + + + + @@ -11032,6 +11157,11 @@ + + + + + @@ -11130,6 +11260,11 @@ + + + + + @@ -11196,6 +11331,11 @@ + + + + + @@ -11356,6 +11496,11 @@ + + + + + @@ -11412,6 +11557,11 @@ + + + + + @@ -11494,6 +11644,11 @@ + + + + + @@ -11551,6 +11706,11 @@ + + + + + @@ -11618,6 +11778,11 @@ + + + + + @@ -11674,6 +11839,11 @@ + + + + + @@ -11762,6 +11932,11 @@ + + + + + @@ -11834,6 +12009,11 @@ + + + + + @@ -11864,6 +12044,11 @@ + + + + + @@ -11951,6 +12136,11 @@ + + + + + @@ -12054,6 +12244,11 @@ + + + + + @@ -12125,6 +12320,11 @@ + + + + + @@ -12218,6 +12418,11 @@ + + + + + @@ -12248,6 +12453,11 @@ + + + + + @@ -12314,6 +12524,11 @@ + + + + + @@ -12396,6 +12611,11 @@ + + + + + @@ -12452,6 +12672,11 @@ + + + + + @@ -12472,6 +12697,11 @@ + + + + + @@ -12487,6 +12717,11 @@ + + + + + @@ -12570,6 +12805,11 @@ + + + + + @@ -12636,6 +12876,11 @@ + + + + + @@ -12724,6 +12969,11 @@ + + + + + @@ -12780,6 +13030,11 @@ + + + + + @@ -12852,6 +13107,11 @@ + + + + + @@ -12908,6 +13168,11 @@ + + + + + @@ -13568,6 +13833,16 @@ + + + + + + + + + + @@ -14433,6 +14708,11 @@ + + + + + @@ -14484,6 +14764,11 @@ + + + + + @@ -14585,6 +14870,11 @@ + + + + + @@ -15148,6 +15438,11 @@ + + + + + @@ -15163,6 +15458,11 @@ + + + + + @@ -15228,6 +15528,11 @@ + + + + + @@ -15269,6 +15574,11 @@ + + + + + @@ -15304,6 +15614,11 @@ + + + + + @@ -15504,6 +15819,11 @@ + + + + + @@ -15520,11 +15840,21 @@ + + + + + + + + + + @@ -15540,6 +15870,11 @@ + + + + + @@ -15550,6 +15885,11 @@ + + + + + @@ -15690,6 +16030,11 @@ + + + + + @@ -15710,6 +16055,11 @@ + + + + + @@ -15730,6 +16080,11 @@ + + + + + @@ -15795,6 +16150,11 @@ + + + + + @@ -15850,6 +16210,11 @@ + + + + + @@ -15865,6 +16230,11 @@ + + + + + @@ -15920,6 +16290,11 @@ + + + + + @@ -15985,6 +16360,11 @@ + + + + + @@ -16052,6 +16432,11 @@ + + + + + @@ -16077,6 +16462,11 @@ + + + + + @@ -16182,6 +16572,14 @@ + + + + + + + + @@ -16222,6 +16620,11 @@ + + + + + @@ -16257,6 +16660,11 @@ + + + + + @@ -16312,6 +16720,11 @@ + + + + + @@ -16367,6 +16780,11 @@ + + + + + @@ -16422,6 +16840,11 @@ + + + + + @@ -16432,6 +16855,11 @@ + + + + + @@ -16497,6 +16925,11 @@ + + + + + @@ -16662,6 +17095,11 @@ + + + + + @@ -16682,6 +17120,11 @@ + + + + + @@ -16747,6 +17190,11 @@ + + + + + @@ -16802,6 +17250,11 @@ + + + + + @@ -16857,6 +17310,11 @@ + + + + + @@ -16912,6 +17370,11 @@ + + + + + @@ -16967,6 +17430,11 @@ + + + + + @@ -17028,6 +17496,11 @@ + + + + + @@ -17048,6 +17521,11 @@ + + + + + @@ -17242,6 +17720,19 @@ + + + + + + + + + + + + + @@ -17641,6 +18132,14 @@ + + + + + + + + @@ -17776,6 +18275,11 @@ + + + + + @@ -17821,6 +18325,11 @@ + + + + + @@ -17901,6 +18410,11 @@ + + + + + @@ -17956,6 +18470,11 @@ + + + + + @@ -17966,6 +18485,16 @@ + + + + + + + + + + From 07b56f0516c5198f7a6dd5f64972da3ee78ed09e Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 3 Nov 2025 10:29:35 -0300 Subject: [PATCH 27/27] Upgrade Dokka syntax to v2 --- .gitlab-ci.yml | 3 ++- libs/core/build.gradle.kts | 14 +++++--------- libs/database/build.gradle.kts | 14 +++++--------- libs/download/build.gradle.kts | 14 +++++--------- libs/index/build.gradle.kts | 14 +++++--------- 5 files changed, 22 insertions(+), 37 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b8d84a631..9702bd75f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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/libs/core/build.gradle.kts b/libs/core/build.gradle.kts index 055616067..05b9624ef 100644 --- a/libs/core/build.gradle.kts +++ b/libs/core/build.gradle.kts @@ -71,13 +71,9 @@ mavenPublishing { ) } -tasks.withType().configureEach { - pluginsMapConfiguration.set( - mapOf( - "org.jetbrains.dokka.base.DokkaBase" to """{ - "customAssets": ["${file("${rootProject.rootDir}/logo-icon.svg")}"], - "footerMessage": "© 2010-2025 F-Droid Limited and Contributors" - }""", - ), - ) +dokka { + pluginsConfiguration.html { + customAssets.from("${file("${rootProject.rootDir}/logo-icon.svg")}") + footerMessage.set("© 2010-2025 F-Droid Limited and Contributors") + } } diff --git a/libs/database/build.gradle.kts b/libs/database/build.gradle.kts index a1b122d4b..db3f0e6e8 100644 --- a/libs/database/build.gradle.kts +++ b/libs/database/build.gradle.kts @@ -123,15 +123,11 @@ signing { useGpgCmd() } -tasks.withType().configureEach { - pluginsMapConfiguration.set( - mapOf( - "org.jetbrains.dokka.base.DokkaBase" to """{ - "customAssets": ["${file("${rootProject.rootDir}/logo-icon.svg")}"], - "footerMessage": "© 2010-2025 F-Droid Limited and Contributors" - }""", - ), - ) +dokka { + pluginsConfiguration.html { + customAssets.from("${file("${rootProject.rootDir}/logo-icon.svg")}") + footerMessage.set("© 2010-2025 F-Droid Limited and Contributors") + } } class RoomSchemaArgProvider( diff --git a/libs/download/build.gradle.kts b/libs/download/build.gradle.kts index 8d1e0c0b2..706e6d1a9 100644 --- a/libs/download/build.gradle.kts +++ b/libs/download/build.gradle.kts @@ -116,13 +116,9 @@ signing { useGpgCmd() } -tasks.withType().configureEach { - pluginsMapConfiguration.set( - mapOf( - "org.jetbrains.dokka.base.DokkaBase" to """{ - "customAssets": ["${file("${rootProject.rootDir}/logo-icon.svg")}"], - "footerMessage": "© 2010-2025 F-Droid Limited and Contributors" - }""", - ), - ) +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/build.gradle.kts b/libs/index/build.gradle.kts index 875ebefc7..d172b8909 100644 --- a/libs/index/build.gradle.kts +++ b/libs/index/build.gradle.kts @@ -94,13 +94,9 @@ signing { useGpgCmd() } -tasks.withType().configureEach { - pluginsMapConfiguration.set( - mapOf( - "org.jetbrains.dokka.base.DokkaBase" to """{ - "customAssets": ["${file("${rootProject.rootDir}/logo-icon.svg")}"], - "footerMessage": "© 2010-2025 F-Droid Limited and Contributors" - }""", - ), - ) +dokka { + pluginsConfiguration.html { + customAssets.from("${file("${rootProject.rootDir}/logo-icon.svg")}") + footerMessage.set("© 2010-2025 F-Droid Limited and Contributors") + } }