[db] First prototype
This commit is contained in:
parent
ade37a4d9c
commit
ca6da651ec
|
@ -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
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -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'
|
||||
}
|
|
@ -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
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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())
|
||||
},
|
||||
)
|
||||
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
include ':app'
|
||||
include ':download'
|
||||
include ':index'
|
||||
include ':index'
|
||||
include ':database'
|
||||
|
|
Loading…
Reference in New Issue