[db] Add plumping for fetching and adding a new repo

This commit is contained in:
Torsten Grote
2023-06-23 18:14:30 -03:00
parent 937fe99062
commit f3e8a0a45b
18 changed files with 1640 additions and 1 deletions

View File

@@ -8,6 +8,7 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import org.fdroid.LocaleChooser.getBestLocale
import java.util.Locale
import java.util.concurrent.Callable
@Database(
// When bumping this version, please make sure to add one (or more) migration(s) below!
@@ -101,6 +102,11 @@ public interface FDroidDatabase {
*/
public fun runInTransaction(body: Runnable)
/**
* Like [runInTransaction], but can return something.
*/
public fun <V> runInTransaction(body: Callable<V>): V
/**
* Removes all apps and associated data (such as versions) from all repositories.
* The repository data and app preferences are kept as-is.

View File

@@ -93,6 +93,7 @@ public data class Repository internal constructor(
/**
* Used to create a minimal version of a [Repository].
*/
@JvmOverloads
public constructor(
repoId: Long,
address: String,
@@ -102,6 +103,8 @@ public data class Repository internal constructor(
version: Long,
weight: Int,
lastUpdated: Long,
username: String? = null,
password: String? = null,
) : this(
repository = CoreRepository(
repoId = repoId,
@@ -121,6 +124,8 @@ public data class Repository internal constructor(
repoId = repoId,
weight = weight,
lastUpdated = lastUpdated,
username = username,
password = password,
)
)
@@ -401,3 +406,16 @@ private fun validateCertificate(certificate: String?) {
certificate.chunked(2).find { it.toIntOrNull(16) == null } == null
) { "Invalid certificate: $certificate" }
}
/**
* A reduced version of [Repository] used to add new repositories.
*/
public data class NewRepository(
val name: LocalizedTextV2,
val icon: LocalizedFileV2,
val address: String,
val formatVersion: IndexFormatVersion?,
val certificate: String,
val username: String? = null,
val password: String? = null,
)

View File

@@ -28,11 +28,17 @@ public interface RepositoryDao {
*/
public fun insert(initialRepo: InitialRepository): Long
/**
* Inserts a new repository into the database.
*/
public fun insert(newRepository: NewRepository): Long
/**
* Inserts an empty [Repository] for an initial update.
*
* @return the [Repository.repoId] of the inserted repo.
*/
@Deprecated("Use insert instead")
public fun insertEmptyRepo(
address: String,
username: String? = null,
@@ -145,6 +151,32 @@ internal interface RepositoryDaoInt : RepositoryDao {
}
@Transaction
override fun insert(newRepository: NewRepository): Long {
val repo = CoreRepository(
name = newRepository.name,
icon = newRepository.icon,
address = newRepository.address,
timestamp = -1,
version = null,
formatVersion = newRepository.formatVersion,
maxAge = null,
certificate = newRepository.certificate,
)
val repoId = insertOrReplace(repo)
val currentMaxWeight = getMaxRepositoryWeight()
val repositoryPreferences = RepositoryPreferences(
repoId = repoId,
weight = currentMaxWeight + 1,
lastUpdated = null,
username = newRepository.username,
password = newRepository.password,
)
insert(repositoryPreferences)
return repoId
}
@Transaction
@Deprecated("Use insert instead")
override fun insertEmptyRepo(
address: String,
username: String?,
@@ -191,6 +223,10 @@ internal interface RepositoryDaoInt : RepositoryDao {
@Query("SELECT * FROM ${CoreRepository.TABLE} WHERE repoId = :repoId")
override fun getRepository(repoId: Long): Repository?
@Transaction
@Query("SELECT * FROM ${CoreRepository.TABLE} WHERE certificate = :certificate COLLATE NOCASE")
fun getRepository(certificate: String): Repository?
@Transaction
@Query("SELECT * FROM ${CoreRepository.TABLE}")
override fun getRepositories(): List<Repository>

View File

@@ -1,5 +1,8 @@
package org.fdroid.index
import android.content.Context
import androidx.annotation.AnyThread
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
@@ -13,23 +16,45 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.fdroid.database.FDroidDatabase
import org.fdroid.database.Repository
import org.fdroid.download.DownloaderFactory
import org.fdroid.download.HttpManager
import org.fdroid.repo.AddRepoState
import org.fdroid.repo.RepoAdder
import java.io.File
import java.net.Proxy
import java.util.concurrent.CountDownLatch
import kotlin.coroutines.CoroutineContext
@OptIn(DelicateCoroutinesApi::class)
public class RepoManager @JvmOverloads constructor(
context: Context,
db: FDroidDatabase,
downloaderFactory: DownloaderFactory,
httpManager: HttpManager,
private val coroutineContext: CoroutineContext = Dispatchers.IO,
) {
private val repositoryDao = db.getRepositoryDao()
private val tempFileProvider = TempFileProvider {
File.createTempFile("dl-", "", context.cacheDir)
}
private val repoAdder = RepoAdder(
context = context,
db = db,
tempFileProvider = tempFileProvider,
downloaderFactory = downloaderFactory,
httpManager = httpManager,
coroutineContext = coroutineContext,
)
private val _repositoriesState: MutableStateFlow<List<Repository>> =
MutableStateFlow(emptyList())
public val repositoriesState: StateFlow<List<Repository>> = _repositoriesState.asStateFlow()
public val liveRepositories: LiveData<List<Repository>> = _repositoriesState.asLiveData()
public val addRepoState: StateFlow<AddRepoState> = repoAdder.addRepoState.asStateFlow()
public val liveAddRepoState: LiveData<AddRepoState> = repoAdder.addRepoState.asLiveData()
/**
* Used internally as a mechanism to wait until repositories are loaded from the DB.
* This happens quite fast and the load is triggered at construction time.
@@ -92,4 +117,42 @@ public class RepoManager @JvmOverloads constructor(
}
}
/**
* Fetches a preview of the repository at the given [url]
* with the intention of possibly adding it to the database.
* Progress can be observed via [addRepoState] or [liveAddRepoState].
*/
@AnyThread
@JvmOverloads
public fun fetchRepositoryPreview(
url: String,
username: String? = null,
password: String? = null,
proxy: Proxy? = null,
) {
repoAdder.fetchRepository(url, username, password, proxy)
}
/**
* When [addRepoState] is in [org.fdroid.repo.Fetched],
* you can call this to actually add the repo to the DB.
* @throws IllegalStateException if [addRepoState] is currently in any other state.
*/
@AnyThread
public fun addFetchedRepository() {
GlobalScope.launch(coroutineContext) {
repoAdder.addFetchedRepository()
}
}
/**
* Aborts the process of fetching a [Repository] preview,
* e.g. when the user leaves the UI flow or wants to cancel the preview process.
* Note that this won't work after [addFetchedRepository] has already been called.
*/
@UiThread
public fun abortAddingRepository() {
repoAdder.abortAddingRepo()
}
}

View File

@@ -0,0 +1,281 @@
package org.fdroid.repo
import android.content.Context
import android.net.Uri
import android.os.Build.VERSION.SDK_INT
import android.os.UserManager
import android.os.UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES
import android.os.UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import androidx.core.content.ContextCompat.getSystemService
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import mu.KotlinLogging
import org.fdroid.database.AppOverviewItem
import org.fdroid.database.FDroidDatabase
import org.fdroid.database.MinimalApp
import org.fdroid.database.NewRepository
import org.fdroid.database.Repository
import org.fdroid.database.RepositoryDaoInt
import org.fdroid.download.DownloaderFactory
import org.fdroid.download.HttpManager
import org.fdroid.download.NotFoundException
import org.fdroid.index.IndexFormatVersion
import org.fdroid.index.SigningException
import org.fdroid.index.TempFileProvider
import org.fdroid.repo.AddRepoError.ErrorType.INVALID_FINGERPRINT
import org.fdroid.repo.AddRepoError.ErrorType.INVALID_INDEX
import org.fdroid.repo.AddRepoError.ErrorType.IO_ERROR
import org.fdroid.repo.AddRepoError.ErrorType.UNKNOWN_SOURCES_DISALLOWED
import java.io.IOException
import java.net.Proxy
import kotlin.coroutines.CoroutineContext
internal const val REPO_ID = 0L
public sealed class AddRepoState
public object None : AddRepoState()
public class Fetching(
public val repo: Repository?,
public val apps: List<MinimalApp>,
public val fetchResult: FetchResult?,
/**
* true if fetching is complete.
*/
public val done: Boolean = false,
) : AddRepoState() {
/**
* true if the repository can be added (be it as new [Repository] or new mirror).
*/
public val canAdd: Boolean = repo != null &&
(fetchResult != null && fetchResult !is FetchResult.IsExistingRepository)
}
public object Adding : AddRepoState()
public class Added(
public val repo: Repository,
) : AddRepoState()
public data class AddRepoError(
public val errorType: ErrorType,
public val exception: Exception? = null,
) : AddRepoState() {
public enum class ErrorType {
UNKNOWN_SOURCES_DISALLOWED,
INVALID_FINGERPRINT,
INVALID_INDEX,
IO_ERROR,
}
}
public sealed class FetchResult {
public object IsNewRepository : FetchResult()
public data class IsNewMirror(
internal val existingRepoId: Long,
internal val newMirrorUrl: String,
) : FetchResult()
public object IsExistingRepository : FetchResult()
}
@OptIn(DelicateCoroutinesApi::class)
internal class RepoAdder(
private val context: Context,
private val db: FDroidDatabase,
private val tempFileProvider: TempFileProvider,
private val downloaderFactory: DownloaderFactory,
private val httpManager: HttpManager,
private val repoUriGetter: RepoUriGetter = RepoUriGetter,
private val coroutineContext: CoroutineContext = Dispatchers.IO,
) {
private val log = KotlinLogging.logger {}
private val repositoryDao = db.getRepositoryDao() as RepositoryDaoInt
internal val addRepoState: MutableStateFlow<AddRepoState> = MutableStateFlow(None)
private var fetchJob: Job? = null
internal fun fetchRepository(url: String, username: String?, password: String?, proxy: Proxy?) {
fetchJob = GlobalScope.launch(coroutineContext) {
fetchRepositoryInt(url, username, password, proxy)
}
}
@WorkerThread
@VisibleForTesting
internal suspend fun fetchRepositoryInt(
url: String,
username: String? = null,
password: String? = null,
proxy: Proxy? = null,
) {
if (hasDisallowInstallUnknownSources(context)) {
addRepoState.value = AddRepoError(UNKNOWN_SOURCES_DISALLOWED)
return
}
// get repo url and fingerprint
val nUri = repoUriGetter.getUri(url)
log.info("Parsed URI: $nUri")
// some plumping to receive the repo preview
var receivedRepo: Repository? = null
val apps = ArrayList<AppOverviewItem>()
var fetchResult: FetchResult? = null
val receiver = object : RepoPreviewReceiver {
override fun onRepoReceived(repo: Repository) {
receivedRepo = repo
fetchResult = getFetchResult(nUri.uri.toString(), repo)
addRepoState.value = Fetching(receivedRepo, apps.toList(), fetchResult)
}
override fun onAppReceived(app: AppOverviewItem) {
apps.add(app)
addRepoState.value = Fetching(receivedRepo, apps.toList(), fetchResult)
}
}
// set a state early, so the ui can show progress animation
addRepoState.value = Fetching(receivedRepo, apps, fetchResult)
// try fetching repo with v2 format first and fallback to v1
try {
try {
val repo = getTempRepo(nUri.uri, IndexFormatVersion.TWO, username, password)
val repoFetcher =
RepoV2Fetcher(tempFileProvider, downloaderFactory, httpManager, proxy)
repoFetcher.fetchRepo(nUri.uri, repo, receiver, nUri.fingerprint)
} catch (e: NotFoundException) {
log.warn(e) { "Did not find v2 repo, trying v1 now." }
// try to fetch v1 repo
val repo = getTempRepo(nUri.uri, IndexFormatVersion.ONE, username, password)
val repoFetcher = RepoV1Fetcher(tempFileProvider, downloaderFactory)
repoFetcher.fetchRepo(nUri.uri, repo, receiver, nUri.fingerprint)
}
} catch (e: SigningException) {
log.error(e) { "Error verifying repo with given fingerprint." }
addRepoState.value = AddRepoError(INVALID_FINGERPRINT, e)
return
} catch (e: IOException) {
log.error(e) { "Error fetching repo." }
addRepoState.value = AddRepoError(IO_ERROR, e)
return
}
// set final result
val finalRepo = receivedRepo
if (finalRepo == null) {
addRepoState.value = AddRepoError(INVALID_INDEX)
} else {
addRepoState.value = Fetching(finalRepo, apps, fetchResult, done = true)
}
}
private fun getFetchResult(url: String, repo: Repository): FetchResult {
val cert = repo.certificate ?: error("Certificate was null")
val existingRepo = repositoryDao.getRepository(cert)
return if (existingRepo == null) {
FetchResult.IsNewRepository
} else {
val existingMirror = if (existingRepo.address.trimEnd('/') == url) {
url
} else {
existingRepo.mirrors.find { it.url.trimEnd('/') == url }
?: existingRepo.userMirrors.find { it.trimEnd('/') == url }
}
if (existingMirror == null) {
FetchResult.IsNewMirror(existingRepo.repoId, url)
} else {
FetchResult.IsExistingRepository
}
}
}
@WorkerThread
internal fun addFetchedRepository() {
// prevent double calls (e.g. caused by double tapping a UI button)
if (addRepoState.compareAndSet(Adding, Adding)) return
// cancel fetch preview job, so it stops emitting new states
fetchJob?.cancel()
// get current state before changing it
val state = (addRepoState.value as? Fetching)
?: throw IllegalStateException("Unexpected state: ${addRepoState.value}")
addRepoState.value = Adding
val repo = state.repo ?: throw IllegalStateException("No repo: ${addRepoState.value}")
val fetchResult = state.fetchResult
?: throw IllegalStateException("No fetchResult: ${addRepoState.value}")
val modifiedRepo: Repository = when (fetchResult) {
is FetchResult.IsExistingRepository -> error("Unexpected result: $fetchResult")
is FetchResult.IsNewRepository -> {
// reset the timestamp of the actual repo,
// so a following repo update will pick this up
val newRepo = NewRepository(
name = repo.repository.name,
icon = repo.repository.icon ?: emptyMap(),
address = repo.address,
formatVersion = repo.formatVersion,
certificate = repo.certificate ?: error("Repo had no certificate"),
username = repo.username,
password = repo.password,
)
val repoId = repositoryDao.insert(newRepo)
repositoryDao.getRepository(repoId) ?: error("New repository not found in DB")
}
is FetchResult.IsNewMirror -> {
val repoId = fetchResult.existingRepoId
db.runInTransaction<Repository> {
val existingRepo = repositoryDao.getRepository(repoId)
?: error("No repo with $repoId")
val userMirrors = existingRepo.userMirrors.toMutableList().apply {
add(fetchResult.newMirrorUrl)
}
repositoryDao.updateUserMirrors(repoId, userMirrors)
existingRepo
}
}
}
addRepoState.value = Added(modifiedRepo)
}
internal fun abortAddingRepo() {
addRepoState.value = None
fetchJob?.cancel()
}
private fun hasDisallowInstallUnknownSources(context: Context): Boolean {
val userManager = getSystemService(context, UserManager::class.java)
?: error("No UserManager available.")
return if (SDK_INT < 29) userManager.hasUserRestriction(DISALLOW_INSTALL_UNKNOWN_SOURCES)
else userManager.hasUserRestriction(DISALLOW_INSTALL_UNKNOWN_SOURCES) ||
userManager.hasUserRestriction(DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY)
}
private fun getTempRepo(
uri: Uri,
indexFormatVersion: IndexFormatVersion,
username: String?,
password: String?,
) = Repository(
repoId = REPO_ID,
address = uri.toString(),
timestamp = -1L,
formatVersion = indexFormatVersion,
certificate = null,
version = 0L,
weight = 0,
lastUpdated = -1L,
username = username,
password = password,
)
}

View File

@@ -0,0 +1,23 @@
package org.fdroid.repo
import android.net.Uri
import org.fdroid.database.AppOverviewItem
import org.fdroid.database.Repository
import org.fdroid.download.NotFoundException
import org.fdroid.index.SigningException
import java.io.IOException
internal fun interface RepoFetcher {
@Throws(IOException::class, SigningException::class, NotFoundException::class)
suspend fun fetchRepo(
uri: Uri,
repo: Repository,
receiver: RepoPreviewReceiver,
fingerprint: String?,
)
}
internal interface RepoPreviewReceiver {
fun onRepoReceived(repo: Repository)
fun onAppReceived(app: AppOverviewItem)
}

View File

@@ -0,0 +1,48 @@
package org.fdroid.repo
import android.net.Uri
import org.fdroid.database.Repository
internal object RepoUriGetter {
fun getUri(url: String): NormalizedUri {
val uri = Uri.parse(url)
val fingerprint = uri.getQueryParameter("fingerprint")?.lowercase()
val pathSegments = uri.pathSegments
val normalizedUri = uri.buildUpon().apply {
clearQuery() // removes fingerprint and other query params
fragment("") // remove # hash fragment
if (pathSegments.size >= 2 &&
pathSegments[pathSegments.lastIndex - 1] == "fdroid" &&
pathSegments.last() == "repo"
) {
// path already is /fdroid/repo, use as is
} else if (pathSegments.lastOrNull() == "repo") {
// path already ends in /repo, use as is
} else if (pathSegments.size >= 1 && pathSegments.last() == "fdroid") {
// path is /fdroid with missing /repo, so add that
appendPath("repo")
} else {
// path is missing /fdroid/repo, so add it
appendPath("fdroid")
appendPath("repo")
}
}.build().let { newUri ->
// hacky way to remove trailing slash
val path = newUri.path
if (path != null && path.endsWith('/')) {
newUri.buildUpon().path(path.trimEnd('/')).build()
} else {
newUri
}
}
return NormalizedUri(normalizedUri, fingerprint)
}
/**
* A class for normalizing the [Repository] URI and holding an optional fingerprint.
*/
data class NormalizedUri(val uri: Uri, val fingerprint: String?)
}

View File

@@ -0,0 +1,66 @@
package org.fdroid.repo
import android.content.res.Resources
import android.net.Uri
import androidx.core.os.ConfigurationCompat.getLocales
import androidx.core.os.LocaleListCompat
import org.fdroid.database.Repository
import org.fdroid.download.DownloaderFactory
import org.fdroid.index.IndexConverter
import org.fdroid.index.IndexFormatVersion
import org.fdroid.index.IndexParser
import org.fdroid.index.SigningException
import org.fdroid.index.TempFileProvider
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
internal class RepoV1Fetcher(
private val tempFileProvider: TempFileProvider,
private val downloaderFactory: DownloaderFactory,
) : RepoFetcher {
private val locales: LocaleListCompat = getLocales(Resources.getSystem().configuration)
@Throws(SigningException::class)
override suspend fun fetchRepo(
uri: Uri,
repo: Repository,
receiver: RepoPreviewReceiver,
fingerprint: String?,
) {
// download and verify index-v1.jar
val indexFile = tempFileProvider.createTempFile()
val entryDownloader = downloaderFactory.create(
repo = repo,
uri = uri.buildUpon().appendPath(SIGNED_FILE_NAME).build(),
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()
}
val version = indexV1.repo.version
val indexV2 = IndexConverter().toIndexV2(indexV1)
val receivedRepo = RepoV2StreamReceiver.getRepository(
repo = indexV2.repo,
version = version.toLong(),
formatVersion = IndexFormatVersion.ONE,
certificate = cert,
username = repo.username,
password = repo.password,
)
receiver.onRepoReceived(receivedRepo)
indexV2.packages.forEach { (packageName, packageV2) ->
val app = RepoV2StreamReceiver.getAppOverViewItem(packageName, packageV2, locales)
receiver.onAppReceived(app)
}
}
}

View File

@@ -0,0 +1,74 @@
package org.fdroid.repo
import android.net.Uri
import mu.KotlinLogging
import org.fdroid.database.Repository
import org.fdroid.download.DownloadRequest
import org.fdroid.download.DownloaderFactory
import org.fdroid.download.HttpManager
import org.fdroid.download.getDigestInputStream
import org.fdroid.index.IndexParser
import org.fdroid.index.SigningException
import org.fdroid.index.TempFileProvider
import org.fdroid.index.parseEntry
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.net.Proxy
internal class RepoV2Fetcher(
private val tempFileProvider: TempFileProvider,
private val downloaderFactory: DownloaderFactory,
private val httpManager: HttpManager,
private val proxy: Proxy? = null,
) : RepoFetcher {
private val log = KotlinLogging.logger {}
@Throws(SigningException::class)
override suspend fun fetchRepo(
uri: Uri,
repo: Repository,
receiver: RepoPreviewReceiver,
fingerprint: String?,
) {
// download and verify entry
val entryFile = tempFileProvider.createTempFile()
val entryDownloader = downloaderFactory.create(
repo = repo,
uri = uri.buildUpon().appendPath(SIGNED_FILE_NAME).build(),
indexFile = FileV2.fromPath("/$SIGNED_FILE_NAME"),
destFile = entryFile,
)
val (cert, entry) = try {
entryDownloader.download()
val verifier = EntryVerifier(entryFile, null, fingerprint)
verifier.getStreamAndVerify { inputStream ->
IndexParser.parseEntry(inputStream)
}
} finally {
entryFile.delete()
}
log.info { "Downloaded entry, now streaming index..." }
// stream index
val indexRequest = DownloadRequest(
indexFile = FileV2.fromPath(entry.index.name.trimStart('/')),
mirrors = repo.getMirrors(),
proxy = proxy,
username = repo.username,
password = repo.password,
)
val streamReceiver = RepoV2StreamReceiver(receiver, repo.username, repo.password)
val streamProcessor = IndexV2FullStreamProcessor(streamReceiver, cert)
val digestInputStream = httpManager.getDigestInputStream(indexRequest)
digestInputStream.use { inputStream ->
streamProcessor.process(entry.version, inputStream) { }
}
val hexDigest = digestInputStream.getDigestHex()
if (!hexDigest.equals(entry.index.sha256, ignoreCase = true)) {
throw SigningException("Invalid ${entry.index.name} hash: $hexDigest")
}
}
}

View File

@@ -0,0 +1,102 @@
package org.fdroid.repo
import android.content.res.Resources
import androidx.core.os.ConfigurationCompat.getLocales
import androidx.core.os.LocaleListCompat
import org.fdroid.LocaleChooser.getBestLocale
import org.fdroid.database.AppOverviewItem
import org.fdroid.database.LocalizedIcon
import org.fdroid.database.Repository
import org.fdroid.database.RepositoryPreferences
import org.fdroid.database.toCoreRepository
import org.fdroid.database.toRepoAntiFeatures
import org.fdroid.database.toRepoCategories
import org.fdroid.database.toRepoReleaseChannel
import org.fdroid.index.IndexFormatVersion
import org.fdroid.index.v2.IndexV2StreamReceiver
import org.fdroid.index.v2.PackageV2
import org.fdroid.index.v2.RepoV2
internal open class RepoV2StreamReceiver(
private val receiver: RepoPreviewReceiver,
private val username: String?,
private val password: String?,
) : IndexV2StreamReceiver {
companion object {
fun getRepository(
repo: RepoV2,
version: Long,
formatVersion: IndexFormatVersion,
certificate: String?,
username: String?,
password: String?,
) = Repository(
repository = repo.toCoreRepository(
version = version,
formatVersion = formatVersion,
certificate = certificate
),
mirrors = emptyList(),
antiFeatures = repo.antiFeatures.toRepoAntiFeatures(REPO_ID),
categories = repo.categories.toRepoCategories(REPO_ID),
releaseChannels = repo.releaseChannels.toRepoReleaseChannel(REPO_ID),
preferences = RepositoryPreferences(
repoId = REPO_ID,
weight = 0,
enabled = true,
username = username,
password = password,
),
)
fun getAppOverViewItem(
packageName: String,
p: PackageV2,
locales: LocaleListCompat,
) = AppOverviewItem(
repoId = REPO_ID,
packageName = packageName,
added = p.metadata.added,
lastUpdated = p.metadata.lastUpdated,
name = p.metadata.name.getBestLocale(locales),
summary = p.metadata.summary.getBestLocale(locales),
antiFeatures = p.versions.values.lastOrNull()?.antiFeatures,
localizedIcon = p.metadata.icon?.map { (locale, file) ->
LocalizedIcon(
repoId = 0L,
packageName = packageName,
type = "icon",
locale = locale,
name = file.name,
sha256 = file.sha256,
size = file.size,
ipfsCidV1 = file.ipfsCidV1,
)
},
)
}
private val locales: LocaleListCompat = getLocales(Resources.getSystem().configuration)
override fun receive(repo: RepoV2, version: Long, certificate: String) {
receiver.onRepoReceived(
getRepository(
repo = repo,
version = version,
formatVersion = IndexFormatVersion.TWO,
certificate = certificate,
username = username,
password = password,
)
)
}
override fun receive(packageName: String, p: PackageV2) {
receiver.onAppReceived(getAppOverViewItem(packageName, p, locales))
}
override fun onStreamEnded() {
}
}