[db] First prototype

This commit is contained in:
Torsten Grote 2022-02-23 15:38:00 -03:00 committed by Michael Pöhn
parent ade37a4d9c
commit ca6da651ec
19 changed files with 2200 additions and 6 deletions

View File

@ -173,8 +173,8 @@ deploy_nightly:
- echo "<item>${CI_PROJECT_PATH}-nightly</item>" >> app/src/main/res/values/default_repos.xml
- echo "<item>${CI_PROJECT_URL}-nightly/raw/master/fdroid/repo</item>" >> app/src/main/res/values/default_repos.xml
- cat config/nightly-repo/repo.xml >> app/src/main/res/values/default_repos.xml
- export DB=`sed -n 's,.*DB_VERSION *= *\([0-9][0-9]*\).*,\1,p' app/src/main/java/org/fdroid/fdroid/data/DBHelper.java`
- export versionCode=`printf '%d%05d' $DB $(date '+%s'| cut -b4-8)`
- export DB=`sed -n 's,.*version *= *\([0-9][0-9]*\).*,\1,p' database/src/main/java/org/fdroid/database/FDroidDatabase.kt`
- export versionCode=`printf '%d%05d' $DB $(date '+%s'| cut -b1-8)`
- sed -i "s,^\(\s*versionCode\) *[0-9].*,\1 $versionCode," app/build.gradle
# build the APKs!
- ./gradlew assembleDebug

1
database/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

57
database/build.gradle Normal file
View File

@ -0,0 +1,57 @@
plugins {
id 'kotlin-android'
id 'com.android.library'
id 'kotlin-kapt'
// id "org.jlleitschuh.gradle.ktlint" version "10.2.1"
}
android {
compileSdkVersion 30
defaultConfig {
minSdkVersion 22
consumerProguardFiles "consumer-rules.pro"
javaCompileOptions {
annotationProcessorOptions {
arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments disableAnalytics: 'true'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation project(":index")
def room_version = "2.4.2"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation 'io.github.microutils:kotlin-logging:2.1.21'
implementation "org.slf4j:slf4j-android:1.7.36"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2"
testImplementation 'junit:junit:4.13.1'
testImplementation 'org.jetbrains.kotlin:kotlin-test'
androidTestImplementation 'org.jetbrains.kotlin:kotlin-test'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

View File

21
database/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,52 @@
package org.fdroid.database
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.fdroid.index.v2.RepoV2
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.runner.RunWith
import java.io.IOException
@RunWith(AndroidJUnit4::class)
abstract class DbTest {
internal lateinit var repoDao: RepositoryDaoInt
private lateinit var db: FDroidDatabase
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(context, FDroidDatabase::class.java).build()
repoDao = db.getRepositoryDaoInt()
}
@After
@Throws(IOException::class)
fun closeDb() {
db.close()
}
protected fun assertRepoEquals(repoV2: RepoV2, repo: Repository) {
val repoId = repo.repository.repoId
// mirrors
val expectedMirrors = repoV2.mirrors.map { it.toMirror(repoId) }.toSet()
Assert.assertEquals(expectedMirrors, repo.mirrors.toSet())
// anti-features
val expectedAntiFeatures = repoV2.antiFeatures.toRepoAntiFeatures(repoId).toSet()
Assert.assertEquals(expectedAntiFeatures, repo.antiFeatures.toSet())
// categories
val expectedCategories = repoV2.categories.toRepoCategories(repoId).toSet()
Assert.assertEquals(expectedCategories, repo.categories.toSet())
// release channels
val expectedReleaseChannels = repoV2.releaseChannels.toRepoReleaseChannel(repoId).toSet()
Assert.assertEquals(expectedReleaseChannels, repo.releaseChannels.toSet())
// core repo
val coreRepo = repoV2.toCoreRepository().copy(repoId = repoId)
Assert.assertEquals(coreRepo, repo.repository)
}
}

View File

@ -0,0 +1,263 @@
package org.fdroid.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import org.fdroid.database.TestUtils.applyDiff
import org.fdroid.database.TestUtils.getRandomFileV2
import org.fdroid.database.TestUtils.getRandomLocalizedTextV2
import org.fdroid.database.TestUtils.getRandomMap
import org.fdroid.database.TestUtils.getRandomMirror
import org.fdroid.database.TestUtils.getRandomRepo
import org.fdroid.database.TestUtils.getRandomString
import org.fdroid.database.TestUtils.randomDiff
import org.fdroid.index.v2.AntiFeatureV2
import org.fdroid.index.v2.CategoryV2
import org.fdroid.index.v2.ReleaseChannelV2
import org.fdroid.index.v2.RepoV2
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.random.Random
import kotlin.test.assertEquals
/**
* Tests that repository diffs get applied to the database correctly.
*/
@RunWith(AndroidJUnit4::class)
class RepositoryDiffTest : DbTest() {
private val j = Json
@Test
fun timestampDiff() {
val repo = getRandomRepo()
val updateTimestamp = repo.timestamp + 1
val json = """
{
"timestamp": $updateTimestamp
}
""".trimIndent()
testDiff(repo, json) { repos ->
assertEquals(updateTimestamp, repos[0].repository.timestamp)
assertRepoEquals(repo.copy(timestamp = updateTimestamp), repos[0])
}
}
@Test
fun timestampDiffTwoReposInDb() {
// insert repo
val repo = getRandomRepo()
repoDao.insert(repo)
// insert another repo before updating
repoDao.insert(getRandomRepo())
// check that the repo got added and retrieved as expected
var repos = repoDao.getRepositories().sortedBy { it.repository.repoId }
assertEquals(2, repos.size)
val repoId = repos[0].repository.repoId
val updateTimestamp = Random.nextLong()
val json = """
{
"timestamp": $updateTimestamp
}
""".trimIndent()
// decode diff from JSON and update DB with it
val diff = j.parseToJsonElement(json).jsonObject // Json.decodeFromString<RepoDiffV2>(json)
repoDao.updateRepository(repoId, diff)
// fetch repos again and check that the result is as expected
repos = repoDao.getRepositories().sortedBy { it.repository.repoId }
assertEquals(2, repos.size)
assertEquals(repoId, repos[0].repository.repoId)
assertEquals(updateTimestamp, repos[0].repository.timestamp)
assertRepoEquals(repo.copy(timestamp = updateTimestamp), repos[0])
}
@Test
fun iconDiff() {
val repo = getRandomRepo()
val updateIcon = getRandomFileV2()
val json = """
{
"icon": ${Json.encodeToString(updateIcon)}
}
""".trimIndent()
testDiff(repo, json) { repos ->
assertEquals(updateIcon, repos[0].repository.icon)
assertRepoEquals(repo.copy(icon = updateIcon), repos[0])
}
}
@Test
fun iconPartialDiff() {
val repo = getRandomRepo()
val updateIcon = repo.icon!!.copy(name = getRandomString())
val json = """
{
"icon": { "name": "${updateIcon.name}" }
}
""".trimIndent()
testDiff(repo, json) { repos ->
assertEquals(updateIcon, repos[0].repository.icon)
assertRepoEquals(repo.copy(icon = updateIcon), repos[0])
}
}
@Test
fun iconRemoval() {
val repo = getRandomRepo()
val json = """
{
"icon": null
}
""".trimIndent()
testDiff(repo, json) { repos ->
assertEquals(null, repos[0].repository.icon)
assertRepoEquals(repo.copy(icon = null), repos[0])
}
}
@Test
fun mirrorDiff() {
val repo = getRandomRepo()
val updateMirrors = repo.mirrors.toMutableList().apply {
removeLastOrNull()
add(getRandomMirror())
add(getRandomMirror())
}
val json = """
{
"mirrors": ${Json.encodeToString(updateMirrors)}
}
""".trimIndent()
testDiff(repo, json) { repos ->
val expectedMirrors = updateMirrors.map { mirror ->
mirror.toMirror(repos[0].repository.repoId)
}.toSet()
assertEquals(expectedMirrors, repos[0].mirrors.toSet())
assertRepoEquals(repo.copy(mirrors = updateMirrors), repos[0])
}
}
@Test
fun descriptionDiff() {
val repo = getRandomRepo().copy(description = mapOf("de" to "foo", "en" to "bar"))
val updateText = if (Random.nextBoolean()) mapOf("de" to null, "en" to "foo") else null
val json = """
{
"description": ${Json.encodeToString(updateText)}
}
""".trimIndent()
val expectedText = if (updateText == null) emptyMap() else mapOf("en" to "foo")
testDiff(repo, json) { repos ->
assertEquals(expectedText, repos[0].repository.description)
assertRepoEquals(repo.copy(description = expectedText), repos[0])
}
}
@Test
fun antiFeaturesDiff() {
val repo = getRandomRepo().copy(antiFeatures = getRandomMap {
getRandomString() to AntiFeatureV2(getRandomFileV2(), getRandomLocalizedTextV2())
})
val antiFeatures = repo.antiFeatures.randomDiff {
AntiFeatureV2(getRandomFileV2(), getRandomLocalizedTextV2())
}
val json = """
{
"antiFeatures": ${Json.encodeToString(antiFeatures)}
}
""".trimIndent()
testDiff(repo, json) { repos ->
val expectedFeatures = repo.antiFeatures.applyDiff(antiFeatures)
val expectedRepoAntiFeatures =
expectedFeatures.toRepoAntiFeatures(repos[0].repository.repoId)
assertEquals(expectedRepoAntiFeatures.toSet(), repos[0].antiFeatures.toSet())
assertRepoEquals(repo.copy(antiFeatures = expectedFeatures), repos[0])
}
}
@Test
fun antiFeatureKeyChangeDiff() {
// TODO test with changing keys
}
@Test
fun categoriesDiff() {
val repo = getRandomRepo().copy(categories = getRandomMap {
getRandomString() to CategoryV2(getRandomFileV2(), getRandomLocalizedTextV2())
})
val categories = repo.categories.randomDiff {
CategoryV2(getRandomFileV2(), getRandomLocalizedTextV2())
}
val json = """
{
"categories": ${Json.encodeToString(categories)}
}
""".trimIndent()
testDiff(repo, json) { repos ->
val expectedFeatures = repo.categories.applyDiff(categories)
val expectedRepoCategories =
expectedFeatures.toRepoCategories(repos[0].repository.repoId)
assertEquals(expectedRepoCategories.toSet(), repos[0].categories.toSet())
assertRepoEquals(repo.copy(categories = expectedFeatures), repos[0])
}
}
@Test
fun categoriesKeyChangeDiff() {
// TODO test with changing keys
}
@Test
fun releaseChannelsDiff() {
val repo = getRandomRepo().copy(releaseChannels = getRandomMap {
getRandomString() to ReleaseChannelV2(getRandomLocalizedTextV2())
})
val releaseChannels = repo.releaseChannels.randomDiff {
ReleaseChannelV2(getRandomLocalizedTextV2())
}
val json = """
{
"releaseChannels": ${Json.encodeToString(releaseChannels)}
}
""".trimIndent()
testDiff(repo, json) { repos ->
val expectedFeatures = repo.releaseChannels.applyDiff(releaseChannels)
val expectedRepoReleaseChannels =
expectedFeatures.toRepoReleaseChannel(repos[0].repository.repoId)
assertEquals(expectedRepoReleaseChannels.toSet(), repos[0].releaseChannels.toSet())
assertRepoEquals(repo.copy(releaseChannels = expectedFeatures), repos[0])
}
}
@Test
fun releaseChannelKeyChangeDiff() {
// TODO test with changing keys
}
private fun testDiff(repo: RepoV2, json: String, repoChecker: (List<Repository>) -> Unit) {
// insert repo
repoDao.insert(repo)
// check that the repo got added and retrieved as expected
var repos = repoDao.getRepositories()
assertEquals(1, repos.size)
val repoId = repos[0].repository.repoId
// decode diff from JSON and update DB with it
val diff = j.parseToJsonElement(json).jsonObject // Json.decodeFromString<RepoDiffV2>(json)
repoDao.updateRepository(repoId, diff)
// fetch repos again and check that the result is as expected
repos = repoDao.getRepositories().sortedBy { it.repository.repoId }
assertEquals(1, repos.size)
assertEquals(repoId, repos[0].repository.repoId)
repoChecker(repos)
}
}

View File

@ -0,0 +1,46 @@
package org.fdroid.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.fdroid.database.TestUtils.getRandomRepo
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class RepositoryTest : DbTest() {
@Test
fun insertAndDeleteTwoRepos() {
// insert first repo
val repo1 = getRandomRepo()
repoDao.insert(repo1)
// check that first repo got added and retrieved as expected
var repos = repoDao.getRepositories()
assertEquals(1, repos.size)
assertRepoEquals(repo1, repos[0])
// insert second repo
val repo2 = getRandomRepo()
repoDao.insert(repo2)
// check that both repos got added and retrieved as expected
repos = repoDao.getRepositories().sortedBy { it.repository.repoId }
assertEquals(2, repos.size)
assertRepoEquals(repo1, repos[0])
assertRepoEquals(repo2, repos[1])
// remove first repo and check that the database only returns one
repoDao.removeRepository(repos[0].repository)
assertEquals(1, repoDao.getRepositories().size)
// remove second repo as well and check that all associated data got removed as well
repoDao.removeRepository(repos[1].repository)
assertEquals(0, repoDao.getRepositories().size)
assertEquals(0, repoDao.getMirrors().size)
assertEquals(0, repoDao.getAntiFeatures().size)
assertEquals(0, repoDao.getCategories().size)
assertEquals(0, repoDao.getReleaseChannels().size)
}
}

View File

@ -0,0 +1,99 @@
package org.fdroid.database
import org.fdroid.index.v2.AntiFeatureV2
import org.fdroid.index.v2.CategoryV2
import org.fdroid.index.v2.FileV2
import org.fdroid.index.v2.LocalizedTextV2
import org.fdroid.index.v2.MirrorV2
import org.fdroid.index.v2.ReleaseChannelV2
import org.fdroid.index.v2.RepoV2
import kotlin.random.Random
object TestUtils {
private val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
fun getRandomString(length: Int = Random.nextInt(1, 128)) = (1..length)
.map { Random.nextInt(0, charPool.size) }
.map(charPool::get)
.joinToString("")
fun <T> getRandomList(
size: Int = Random.nextInt(0, 23),
factory: () -> T,
): List<T> = if (size == 0) emptyList() else buildList {
repeat(Random.nextInt(0, size)) {
add(factory())
}
}
fun <A, B> getRandomMap(
size: Int = Random.nextInt(0, 23),
factory: () -> Pair<A, B>,
): Map<A, B> = if (size == 0) emptyMap() else buildMap {
repeat(size) {
val pair = factory()
put(pair.first, pair.second)
}
}
private fun <T> T.orNull(): T? {
return if (Random.nextBoolean()) null else this
}
fun getRandomMirror() = MirrorV2(
url = getRandomString(),
location = getRandomString().orNull()
)
fun getRandomLocalizedTextV2(size: Int = Random.nextInt(0, 23)): LocalizedTextV2 = buildMap {
repeat(size) {
put(getRandomString(4), getRandomString())
}
}
fun getRandomFileV2() = FileV2(
name = getRandomString(),
sha256 = getRandomString(64),
size = Random.nextLong(-1, Long.MAX_VALUE)
)
fun getRandomRepo() = RepoV2(
name = getRandomString(),
icon = getRandomFileV2(),
address = getRandomString(),
description = getRandomLocalizedTextV2(),
mirrors = getRandomList { getRandomMirror() },
timestamp = System.currentTimeMillis(),
antiFeatures = getRandomMap {
getRandomString() to AntiFeatureV2(getRandomFileV2(), getRandomLocalizedTextV2())
},
categories = getRandomMap {
getRandomString() to CategoryV2(getRandomFileV2(), getRandomLocalizedTextV2())
},
releaseChannels = getRandomMap {
getRandomString() to ReleaseChannelV2(getRandomLocalizedTextV2())
},
)
/**
* Create a map diff by adding or removing keys. Note that this does not change keys.
*/
fun <T> Map<String, T?>.randomDiff(factory: () -> T): Map<String, T?> = buildMap {
if (this@randomDiff.isNotEmpty()) {
// remove random keys
while (Random.nextBoolean()) put(this@randomDiff.keys.random(), null)
// Note: we don't replace random keys, because we can't easily diff inside T
}
// add random keys
while (Random.nextBoolean()) put(getRandomString(), factory())
}
fun <T> Map<String, T>.applyDiff(diff: Map<String, T?>): Map<String, T> = toMutableMap().apply {
diff.entries.forEach { (key, value) ->
if (value == null) remove(key)
else set(key, value)
}
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.fdroid.database">
</manifest>

View File

@ -0,0 +1,22 @@
package org.fdroid.database
import androidx.room.TypeConverter
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import org.fdroid.index.IndexParser.json
import org.fdroid.index.v2.LocalizedTextV2
internal class Converters {
private val localizedTextV2Serializer = MapSerializer(String.serializer(), String.serializer())
@TypeConverter
fun fromStringToLocalizedTextV2(value: String?): LocalizedTextV2? {
return value?.let { json.decodeFromString(localizedTextV2Serializer, it) }
}
@TypeConverter
fun localizedTextV2toString(text: LocalizedTextV2?): String? {
return text?.let { json.encodeToString(localizedTextV2Serializer, it) }
}
}

View File

@ -0,0 +1,41 @@
package org.fdroid.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
@Database(entities = [
CoreRepository::class,
Mirror::class,
AntiFeature::class,
Category::class,
ReleaseChannel::class,
], version = 1)
@TypeConverters(Converters::class)
internal abstract class FDroidDatabase internal constructor() : RoomDatabase() {
abstract fun getRepositoryDaoInt(): RepositoryDaoInt
companion object {
// Singleton prevents multiple instances of database opening at the same time.
@Volatile
private var INSTANCE: FDroidDatabase? = null
fun getDb(context: Context, name: String = "fdroid_db"): FDroidDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
FDroidDatabase::class.java,
name,
).build()
INSTANCE = instance
// return instance
instance
}
}
}
}

View File

@ -0,0 +1,151 @@
package org.fdroid.database
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import androidx.room.Relation
import org.fdroid.index.v2.AntiFeatureV2
import org.fdroid.index.v2.CategoryV2
import org.fdroid.index.v2.FileV2
import org.fdroid.index.v2.LocalizedTextV2
import org.fdroid.index.v2.MirrorV2
import org.fdroid.index.v2.ReleaseChannelV2
import org.fdroid.index.v2.RepoV2
@Entity
data class CoreRepository(
@PrimaryKey(autoGenerate = true) val repoId: Long = 0,
val name: String,
@Embedded(prefix = "icon") val icon: FileV2?,
val address: String,
val timestamp: Long,
val description: LocalizedTextV2 = emptyMap(),
)
fun RepoV2.toCoreRepository() = CoreRepository(
name = name,
icon = icon,
address = address,
timestamp = timestamp,
description = description,
)
data class Repository(
@Embedded val repository: CoreRepository,
@Relation(
parentColumn = "repoId",
entityColumn = "repoId",
)
val mirrors: List<Mirror>,
@Relation(
parentColumn = "repoId",
entityColumn = "repoId",
)
val antiFeatures: List<AntiFeature>,
@Relation(
parentColumn = "repoId",
entityColumn = "repoId",
)
val categories: List<Category>,
@Relation(
parentColumn = "repoId",
entityColumn = "repoId",
)
val releaseChannels: List<ReleaseChannel>,
)
@Entity(
primaryKeys = ["repoId", "url"],
foreignKeys = [ForeignKey(
entity = CoreRepository::class,
parentColumns = ["repoId"],
childColumns = ["repoId"],
onDelete = ForeignKey.CASCADE,
)],
)
data class Mirror(
val repoId: Long,
val url: String,
val location: String? = null,
)
fun MirrorV2.toMirror(repoId: Long) = Mirror(
repoId = repoId,
url = url,
location = location,
)
@Entity(
primaryKeys = ["repoId", "name"],
foreignKeys = [ForeignKey(
entity = CoreRepository::class,
parentColumns = ["repoId"],
childColumns = ["repoId"],
onDelete = ForeignKey.CASCADE,
)],
)
data class AntiFeature(
val repoId: Long,
val name: String,
@Embedded(prefix = "icon") val icon: FileV2? = null,
val description: LocalizedTextV2,
)
fun Map<String, AntiFeatureV2>.toRepoAntiFeatures(repoId: Long) = map {
AntiFeature(
repoId = repoId,
name = it.key,
icon = it.value.icon,
description = it.value.description,
)
}
@Entity(
primaryKeys = ["repoId", "name"],
foreignKeys = [ForeignKey(
entity = CoreRepository::class,
parentColumns = ["repoId"],
childColumns = ["repoId"],
onDelete = ForeignKey.CASCADE,
)],
)
data class Category(
val repoId: Long,
val name: String,
@Embedded(prefix = "icon") val icon: FileV2? = null,
val description: LocalizedTextV2,
)
fun Map<String, CategoryV2>.toRepoCategories(repoId: Long) = map {
Category(
repoId = repoId,
name = it.key,
icon = it.value.icon,
description = it.value.description,
)
}
@Entity(
primaryKeys = ["repoId", "name"],
foreignKeys = [ForeignKey(
entity = CoreRepository::class,
parentColumns = ["repoId"],
childColumns = ["repoId"],
onDelete = ForeignKey.CASCADE,
)],
)
data class ReleaseChannel(
val repoId: Long,
val name: String,
@Embedded(prefix = "icon") val icon: FileV2? = null,
val description: LocalizedTextV2,
)
fun Map<String, ReleaseChannelV2>.toRepoReleaseChannel(repoId: Long) = map {
ReleaseChannel(
repoId = repoId,
name = it.key,
description = it.value.description,
)
}

View File

@ -0,0 +1,202 @@
package org.fdroid.database
import androidx.annotation.VisibleForTesting
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy.ABORT
import androidx.room.OnConflictStrategy.REPLACE
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.jsonObject
import org.fdroid.index.ReflectionDiffer.applyDiff
import org.fdroid.index.IndexParser.json
import org.fdroid.index.v2.MirrorV2
import org.fdroid.index.v2.RepoV2
public interface RepositoryDao {
fun insert(repository: RepoV2)
}
@Dao
internal interface RepositoryDaoInt : RepositoryDao {
@Insert(onConflict = ABORT)
fun insert(repository: CoreRepository): Long
@Insert(onConflict = REPLACE)
fun insertMirrors(mirrors: List<Mirror>)
@Insert(onConflict = REPLACE)
fun insertAntiFeatures(repoFeature: List<AntiFeature>)
@Insert(onConflict = REPLACE)
fun insertCategories(repoFeature: List<Category>)
@Insert(onConflict = REPLACE)
fun insertReleaseChannels(repoFeature: List<ReleaseChannel>)
@Transaction
override fun insert(repository: RepoV2) {
val repoId = insert(repository.toCoreRepository())
insertMirrors(repository.mirrors.map { it.toMirror(repoId) })
insertAntiFeatures(repository.antiFeatures.toRepoAntiFeatures(repoId))
insertCategories(repository.categories.toRepoCategories(repoId))
insertReleaseChannels(repository.releaseChannels.toRepoReleaseChannel(repoId))
}
@Transaction
@Query("SELECT * FROM CoreRepository WHERE repoId = :repoId")
fun getRepository(repoId: Long): Repository
@Transaction
fun updateRepository(repoId: Long, jsonObject: JsonObject) {
// get existing repo
val repo = getRepository(repoId)
// update repo with JSON diff
updateRepository(applyDiff(repo.repository, jsonObject))
// replace mirror list, if it is in the diff
if (jsonObject.containsKey("mirrors")) {
val mirrorArray = jsonObject["mirrors"] as JsonArray
val mirrors = json.decodeFromJsonElement<List<MirrorV2>>(mirrorArray).map {
it.toMirror(repoId)
}
// delete and re-insert mirrors, because it is easier than diffing
deleteMirrors(repoId)
insertMirrors(mirrors)
}
// diff and update the antiFeatures
diffAndUpdateTable(
jsonObject,
"antiFeatures",
repo.antiFeatures,
{ name -> AntiFeature(repoId, name, null, emptyMap()) },
{ item -> item.name },
{ deleteAntiFeatures(repoId) },
{ name -> deleteAntiFeature(repoId, name) },
{ list -> insertAntiFeatures(list) },
)
// diff and update the categories
diffAndUpdateTable(
jsonObject,
"categories",
repo.categories,
{ name -> Category(repoId, name, null, emptyMap()) },
{ item -> item.name },
{ deleteCategories(repoId) },
{ name -> deleteCategory(repoId, name) },
{ list -> insertCategories(list) },
)
// diff and update the releaseChannels
diffAndUpdateTable(
jsonObject,
"releaseChannels",
repo.releaseChannels,
{ name -> ReleaseChannel(repoId, name, null, emptyMap()) },
{ item -> item.name },
{ deleteReleaseChannels(repoId) },
{ name -> deleteReleaseChannel(repoId, name) },
{ list -> insertReleaseChannels(list) },
)
}
/**
* Applies the diff from [JsonObject] identified by the given [key] of the given [jsonObject]
* to the given [itemList] and updates the DB as needed.
*
* @param newItem A function to produce a new [T] which typically contains the primary key(s).
*/
private fun <T : Any> diffAndUpdateTable(
jsonObject: JsonObject,
key: String,
itemList: List<T>,
newItem: (String) -> T,
keyGetter: (T) -> String,
deleteAll: () -> Unit,
deleteOne: (String) -> Unit,
insertReplace: (List<T>) -> Unit,
) {
if (!jsonObject.containsKey(key)) return
if (jsonObject[key] == JsonNull) {
deleteAll()
} else {
val features = jsonObject[key]?.jsonObject ?: error("no $key object")
val list = itemList.toMutableList()
features.entries.forEach { (key, value) ->
if (value is JsonNull) {
list.removeAll { keyGetter(it) == key }
deleteOne(key)
} else {
val index = list.indexOfFirst { keyGetter(it) == key }
val item = if (index == -1) null else list[index]
if (item == null) {
list.add(applyDiff(newItem(key), value.jsonObject))
} else {
list[index] = applyDiff(item, value.jsonObject)
}
}
}
insertReplace(list)
}
}
@Update
fun updateRepository(repo: CoreRepository): Int
@Transaction
@Query("SELECT * FROM CoreRepository")
fun getRepositories(): List<Repository>
@VisibleForTesting
@Query("SELECT * FROM Mirror")
fun getMirrors(): List<Mirror>
@VisibleForTesting
@Query("DELETE FROM Mirror WHERE repoId = :repoId")
fun deleteMirrors(repoId: Long)
@VisibleForTesting
@Query("SELECT * FROM AntiFeature")
fun getAntiFeatures(): List<AntiFeature>
@VisibleForTesting
@Query("DELETE FROM AntiFeature WHERE repoId = :repoId")
fun deleteAntiFeatures(repoId: Long)
@VisibleForTesting
@Query("DELETE FROM AntiFeature WHERE repoId = :repoId AND name = :name")
fun deleteAntiFeature(repoId: Long, name: String)
@VisibleForTesting
@Query("SELECT * FROM Category")
fun getCategories(): List<Category>
@VisibleForTesting
@Query("DELETE FROM Category WHERE repoId = :repoId")
fun deleteCategories(repoId: Long)
@VisibleForTesting
@Query("DELETE FROM Category WHERE repoId = :repoId AND name = :name")
fun deleteCategory(repoId: Long, name: String)
@VisibleForTesting
@Query("SELECT * FROM ReleaseChannel")
fun getReleaseChannels(): List<ReleaseChannel>
@VisibleForTesting
@Query("DELETE FROM ReleaseChannel WHERE repoId = :repoId")
fun deleteReleaseChannels(repoId: Long)
@VisibleForTesting
@Query("DELETE FROM ReleaseChannel WHERE repoId = :repoId AND name = :name")
fun deleteReleaseChannel(repoId: Long, name: String)
@Delete
fun removeRepository(repository: CoreRepository)
}

View File

@ -0,0 +1,32 @@
package org.fdroid.database
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import org.fdroid.index.ReflectionDiffer.applyDiff
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertEquals
class ReflectionTest {
@Test
fun testRepository() {
val repo = TestUtils2.getRandomRepo().toCoreRepository()
val icon = TestUtils2.getRandomFileV2()
val description = if (Random.nextBoolean()) mapOf("de" to null, "en" to "foo") else null
val json = """
{
"name": "test",
"timestamp": ${Long.MAX_VALUE},
"icon": ${Json.encodeToString(icon)},
"description": ${Json.encodeToString(description)}
}
""".trimIndent()
val diff = Json.parseToJsonElement(json).jsonObject
val diffed = applyDiff(repo, diff)
println(diffed)
assertEquals(Long.MAX_VALUE, diffed.timestamp)
}
}

View File

@ -0,0 +1,79 @@
package org.fdroid.database
import org.fdroid.index.v2.AntiFeatureV2
import org.fdroid.index.v2.CategoryV2
import org.fdroid.index.v2.FileV2
import org.fdroid.index.v2.LocalizedTextV2
import org.fdroid.index.v2.MirrorV2
import org.fdroid.index.v2.ReleaseChannelV2
import org.fdroid.index.v2.RepoV2
import kotlin.random.Random
object TestUtils2 {
private val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
fun getRandomString(length: Int = Random.nextInt(1, 128)) = (1..length)
.map { Random.nextInt(0, charPool.size) }
.map(charPool::get)
.joinToString("")
fun <T> getRandomList(
size: Int = Random.nextInt(0, 23),
factory: () -> T,
): List<T> = if (size == 0) emptyList() else buildList {
repeat(Random.nextInt(0, size)) {
add(factory())
}
}
fun <A, B> getRandomMap(
size: Int = Random.nextInt(0, 23),
factory: () -> Pair<A, B>,
): Map<A, B> = if (size == 0) emptyMap() else buildMap {
repeat(Random.nextInt(0, size)) {
val pair = factory()
put(pair.first, pair.second)
}
}
private fun <T> T.orNull(): T? {
return if (Random.nextBoolean()) null else this
}
fun getRandomMirror() = MirrorV2(
url = getRandomString(),
location = getRandomString().orNull()
)
fun getRandomLocalizedTextV2(size: Int = Random.nextInt(0, 23)): LocalizedTextV2 = buildMap {
repeat(size) {
put(getRandomString(4), getRandomString())
}
}
fun getRandomFileV2() = FileV2(
name = getRandomString(),
sha256 = getRandomString(64),
size = Random.nextLong(-1, Long.MAX_VALUE)
)
fun getRandomRepo() = RepoV2(
name = getRandomString(),
icon = getRandomFileV2(),
address = getRandomString(),
description = getRandomLocalizedTextV2(),
mirrors = getRandomList { getRandomMirror() },
timestamp = System.currentTimeMillis(),
antiFeatures = getRandomMap {
getRandomString() to AntiFeatureV2(getRandomFileV2(), getRandomLocalizedTextV2())
},
categories = getRandomMap {
getRandomString() to CategoryV2(getRandomFileV2(), getRandomLocalizedTextV2())
},
releaseChannels = getRandomMap {
getRandomString() to ReleaseChannelV2(getRandomLocalizedTextV2())
},
)
}

View File

@ -10,6 +10,9 @@
<ignored-keys>
<ignored-key id="3967d4eda591b991" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="02216ed811210daa" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="5f7786df73e61f56" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="bf984b4145ea13f7" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="eb380dc13c39f675" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="4dbf5995d492505d" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="280d66a55f5316c5" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="d9c565aa72ba2fdd" reason="Key couldn't be downloaded from any key server"/>
@ -51,7 +54,10 @@
</trusted-key>
<trusted-key id="2e3a1affe42b5f53af19f780bcf4173966770193" group="org.jetbrains" name="annotations" version="13.0"/>
<trusted-key id="31bae2e51d95e0f8ad9b7bcc40a3c4432bd7308c" group="com.googlecode.juniversalchardet" name="juniversalchardet" version="1.0.3"/>
<trusted-key id="3288b8be8512d6c0ca185268c51e6cbc7ff46f0b" group="com.google.auto.service" name="auto-service" version="1.0-rc4"/>
<trusted-key id="3288b8be8512d6c0ca185268c51e6cbc7ff46f0b">
<trusting group="com.google.auto.service" name="auto-service" version="1.0-rc4"/>
<trusting group="^com[.]google[.]auto($|([.].*))" regex="true"/>
</trusted-key>
<trusted-key id="3872ed7d5904493d23d78fa2c4c8cb73b1435348" group="com.android.tools.build" name="transform-api" version="2.0.0-deprecated-use-gradle-api"/>
<trusted-key id="394cb436c56916fc01eea4a77c30f7b1329dba87" group="io.ktor"/>
<trusted-key id="3d11126ea77e4e07fbabb38614a84c976d265b25" group="com.google.protobuf"/>
@ -243,7 +249,7 @@
</component>
<component group="androidx.annotation" name="annotation-experimental" version="1.1.0">
<artifact name="annotation-experimental-1.1.0.aar">
<sha256 value="0157de61a2064047896a058080f3fd67ba57ad9a94857b3f7a363660243e3f90" origin="Generated by Gradle"/>
<sha256 value="0157de61a2064047896a058080f3fd67ba57ad9a94857b3f7a363660243e3f90" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.appcompat" name="appcompat" version="1.1.0">
@ -287,6 +293,11 @@
<sha256 value="4b6f1d459ddd146b4e85ed6d46e86eb8c2639c5de47904e6db4d698721334220" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.arch.core" name="core-common" version="2.0.1">
<artifact name="core-common-2.0.1.jar">
<sha256 value="e7316a84b899eb2afb1551784e9807fb64bdfcc105636fe0551cd036801f97c8" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.arch.core" name="core-common" version="2.1.0">
<artifact name="core-common-2.1.0.jar">
<sha256 value="fe1237bf029d063e7f29fe39aeaf73ef74c8b0a3658486fc29d3c54326653889" origin="Generated by Gradle because artifact wasn't signed"/>
@ -295,6 +306,11 @@
<sha256 value="83bbb3960eaabc600ac366c94cb59414e441532a1d6aa9388b0b8bfface5cf01" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.arch.core" name="core-runtime" version="2.0.1">
<artifact name="core-runtime-2.0.1.aar">
<sha256 value="0527703682f06f3afa8303ca7bfc5804e3d0e5432df425ac62d08c4e93cc05d3" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.arch.core" name="core-runtime" version="2.1.0">
<artifact name="core-runtime-2.1.0.aar">
<sha256 value="dd77615bd3dd275afb11b62df25bae46b10b4a117cd37943af45bdcbf8755852" origin="Generated by Gradle because artifact wasn't signed"/>
@ -705,11 +721,46 @@
<sha256 value="2b130dd4a1d3d91b6701ed33096d389f01c4fc1197a7acd6b91724ddc5acfc06" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.room" name="room-common" version="2.4.2">
<artifact name="room-common-2.4.2.jar">
<sha256 value="6505f987e696f54475cd82c922e4f4df8c6cd5282e2601bf118e1de7320c36cf" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.room" name="room-compiler" version="2.4.2">
<artifact name="room-compiler-2.4.2.jar">
<sha256 value="0e6930971a8b15f503e308da2c2f75587540cf5f014b664a555ac299197e4fca" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.room" name="room-compiler-processing" version="2.4.2">
<artifact name="room-compiler-processing-2.4.2.jar">
<sha256 value="e2d8462db15394945f5fe0be69792e5c25399a54d2fe17c6a954845e70f06377" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.room" name="room-ktx" version="2.4.2">
<artifact name="room-ktx-2.4.2.aar">
<sha256 value="23aac021051bce72413e037be3dc636380693a07f7dad914c1dafff54899293a" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.room" name="room-migration" version="2.4.2">
<artifact name="room-migration-2.4.2.jar">
<sha256 value="e0efe1ed8557f82628bfcb0b2058a5125472dcf31ef9af85c646d7eaaf900d20" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.room" name="room-runtime" version="2.2.5">
<artifact name="room-runtime-2.2.5.aar">
<sha256 value="24a5549b796e43e337513d2908adac67f45350d9a90bca7e2e6120692140bb14" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.room" name="room-runtime" version="2.4.1">
<artifact name="room-runtime-2.4.1.aar">
<sha256 value="6696d47c0573b67e015f99de467d2be83fd2051c49388e25a95e854417592045" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.room" name="room-runtime" version="2.4.2">
<artifact name="room-runtime-2.4.2.aar">
<sha256 value="b49477511a14b0d3f713d8b90ffce686ac161314111a5897a13aa82d4c892217" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.savedstate" name="savedstate" version="1.0.0">
<artifact name="savedstate-1.0.0.aar">
<sha256 value="2510a5619c37579c9ce1a04574faaf323cd0ffe2fc4e20fa8f8f01e5bb402e83" origin="Generated by Gradle because artifact wasn't signed"/>
@ -736,11 +787,21 @@
<sha256 value="8341ff092d6060d62a07227f29237155fff36fb16f96c95fbd9a884e375db912" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.sqlite" name="sqlite" version="2.2.0">
<artifact name="sqlite-2.2.0.aar">
<sha256 value="6156d5d2c17bd8c5460f199142e4283053b1da750994f6b396c62c50fcc7270c" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.sqlite" name="sqlite-framework" version="2.1.0">
<artifact name="sqlite-framework-2.1.0.aar">
<sha256 value="8673737fdb2efbad91aeaeed1927ebb29212d36a867d93b9639c8069019f8a1e" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.sqlite" name="sqlite-framework" version="2.2.0">
<artifact name="sqlite-framework-2.2.0.aar">
<sha256 value="e5f5fbe7c209e21cde21d1d781481c9b0245839bc03bdd89fa4a798945bdb6a5" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.swiperefreshlayout" name="swiperefreshlayout" version="1.0.0">
<artifact name="swiperefreshlayout-1.0.0.aar">
<sha256 value="9761b3a809c9b093fd06a3c4bbc645756dec0e95b5c9da419bc9f2a3f3026e8d" origin="Generated by Gradle because artifact wasn't signed"/>
@ -785,6 +846,11 @@
<sha256 value="46a912a1e175f27a97521af3f50e5af87c22c49275dd2c57c043740012806325" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.test" name="monitor" version="1.4.0">
<artifact name="monitor-1.4.0.aar">
<sha256 value="46a912a1e175f27a97521af3f50e5af87c22c49275dd2c57c043740012806325" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.test" name="rules" version="1.2.0">
<artifact name="rules-1.2.0.aar">
<sha256 value="24bd7111e0db91b4a5f6d5c3e3e89698580dc90d29273d04a775bb7fe7c2a761" origin="Generated by Gradle because artifact wasn't signed"/>
@ -880,6 +946,21 @@
<sha256 value="a97209d75a9a85815fa8934f5a4a320de1163ffe94e2f0b328c0c98a59660690" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.test.ext" name="junit" version="1.1.3">
<artifact name="junit-1.1.3.aar">
<sha256 value="a97209d75a9a85815fa8934f5a4a320de1163ffe94e2f0b328c0c98a59660690" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.test.ext" name="junit" version="1.1.3">
<artifact name="junit-1.1.3.aar">
<sha256 value="a97209d75a9a85815fa8934f5a4a320de1163ffe94e2f0b328c0c98a59660690" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.test.services" name="storage" version="1.4.0">
<artifact name="storage-1.4.0.aar">
<sha256 value="35cfbf442abb83e5876cd5deb9de02ae047459f18f831097c5caa76d626bc38a" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="androidx.test.uiautomator" name="uiautomator" version="2.2.0">
<artifact name="uiautomator-2.2.0.aar">
<sha256 value="2838e9d961dbffefbbd229a2bd4f6f82ac4fb2462975862a9e75e9ed325a3197" origin="Generated by Gradle because artifact wasn't signed"/>
@ -2199,6 +2280,11 @@
<sha256 value="c6221763bd79c4f1c3dc7f750b5f29a0bb38b367b81314c4f71896e340c40825" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.code.gson" name="gson" version="2.8.0">
<artifact name="gson-2.8.0.jar">
<pgp value="9e84765a7aa3e3d3d5598a408e3f0de7ae354651"/>
</artifact>
</component>
<component group="com.google.code.gson" name="gson" version="2.8.5">
<artifact name="gson-2.8.5.jar">
<pgp value="afcc4c7594d09e2182c60e0f7a01b0f236e5430f"/>
@ -2234,6 +2320,12 @@
<artifact name="dagger-2.28.3.jar">
<pgp value="4f8fec6785f611d9a712ea2734918b7d3969d2f5"/>
<sha256 value="f1dd23f8ae34a8e91366723991ead0d6499d1a3e9163ce550c200b02d76a872b" origin="Generated by Gradle"/>
<sha256 value="f1dd23f8ae34a8e91366723991ead0d6499d1a3e9163ce550c200b02d76a872b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.devtools.ksp" name="symbol-processing-api" version="1.6.10-1.0.2">
<artifact name="symbol-processing-api-1.6.10-1.0.2.jar">
<sha256 value="caa18d15fc54b6da32746a79fe74f6c267ae24364c426f3fc61f209fdb87cb50" origin="Generated by Gradle because a key couldn't be downloaded"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_annotation" version="2.2.0">
@ -2604,6 +2696,11 @@
<sha256 value="f8ab13b14be080fe2f617f90e55599760e4a1b4deeea5c595df63d0d6375ed6d" origin="Generated by Gradle because a key couldn't be downloaded"/>
</artifact>
</component>
<component group="com.intellij" name="annotations" version="12.0">
<artifact name="annotations-12.0.jar">
<sha256 value="f8ab13b14be080fe2f617f90e55599760e4a1b4deeea5c595df63d0d6375ed6d" origin="Generated by Gradle because a key couldn't be downloaded"/>
</artifact>
</component>
<component group="com.jakewharton.android.repackaged" name="dalvik-dx" version="9.0.0_r3">
<artifact name="dalvik-dx-9.0.0_r3.jar">
<pgp value="47bf592261cd1a8a69b703b4e0cb7823cfd00fbf"/>
@ -2764,6 +2861,11 @@
<sha256 value="1690340a222279f2cbadf373e88826fa20f7f3cc3ec0252f36818fed32701ab1" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.squareup" name="javapoet" version="1.13.0">
<artifact name="javapoet-1.13.0.jar">
<sha256 value="4c7517e848a71b36d069d12bb3bf46a70fd4cda3105d822b0ed2e19c00b69291" origin="Generated by Gradle because a key couldn't be downloaded"/>
</artifact>
</component>
<component group="com.squareup" name="javawriter" version="2.1.1">
<artifact name="javawriter-2.1.1.jar">
<pgp value="90ee19787a7bcf6fd37a1e9180c08b1c29100955"/>
@ -2797,6 +2899,11 @@
<sha256 value="2570fab55515cbf881d7a4ceef49fc515490bc027057e666776a2832465aeca0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.squareup" name="kotlinpoet" version="1.8.0">
<artifact name="kotlinpoet-1.8.0.jar">
<pgp value="afa2b1823fc021bfd08c211fd5f4c07a434ab3da"/>
</artifact>
</component>
<component group="com.squareup" name="kotlinpoet" version="1.8.0">
<artifact name="kotlinpoet-1.8.0.jar">
<pgp value="afa2b1823fc021bfd08c211fd5f4c07a434ab3da"/>
@ -5989,6 +6096,11 @@
<sha256 value="f8c8b7485d4a575e38e5e94945539d1d4eccd3228a199e1a9aa094e8c26174ee" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core" version="1.5.2">
<artifact name="kotlinx-coroutines-core-metadata-1.5.2-all.jar">
<sha256 value="4d19a1c1c82bd973d034644f4ffa3d5355cb61bd34575aff86cc609e0e41d6e1" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core" version="1.5.2-native-mt">
<artifact name="kotlinx-coroutines-core-1.5.2-native-mt.jar">
<sha256 value="78492527a0d09e0c53c81aacc2e073a83ee0fc3105e701496819ec67c98df16f" origin="Generated by Gradle"/>
@ -6695,7 +6807,12 @@
<component group="org.tensorflow" name="tensorflow-lite-metadata" version="0.1.0-rc2">
<artifact name="tensorflow-lite-metadata-0.1.0-rc2.jar">
<pgp value="db0597e3144342256bc81e3ec727d053c4481cf5"/>
<sha256 value="2c2a264f842498c36d34d2a7b91342490d9a962862c85baac1acd54ec2fca6d9" origin="Generated by Gradle"/>
<sha256 value="2c2a264f842498c36d34d2a7b91342490d9a962862c85baac1acd54ec2fca6d9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.xerial" name="sqlite-jdbc" version="3.36.0">
<artifact name="sqlite-jdbc-3.36.0.jar">
<pgp value="56b505dc8a29c69138a430b9429c8816dea04cdb"/>
</artifact>
</component>
<component group="org.testng" name="testng" version="7.4.0">

View File

@ -1,3 +1,4 @@
include ':app'
include ':download'
include ':index'
include ':index'
include ':database'