commit
249df47bef
@ -7,8 +7,8 @@ trim_trailing_whitespace = true
|
|||||||
|
|
||||||
[*.{kt,kts}]
|
[*.{kt,kts}]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
ij_kotlin_allow_trailing_comma = true
|
ij_kotlin_allow_trailing_comma=true
|
||||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
ij_kotlin_allow_trailing_comma_on_call_site=true
|
||||||
ij_kotlin_name_count_to_use_star_import = 999
|
ij_kotlin_name_count_to_use_star_import = 999
|
||||||
ij_kotlin_name_count_to_use_star_import_for_members = 999
|
ij_kotlin_name_count_to_use_star_import_for_members = 999
|
||||||
|
|
||||||
|
@ -20,8 +20,8 @@ android {
|
|||||||
applicationId = "com.looker.droidify"
|
applicationId = "com.looker.droidify"
|
||||||
versionCode = 650
|
versionCode = 650
|
||||||
versionName = latestVersionName
|
versionName = latestVersionName
|
||||||
vectorDrawables.useSupportLibrary = false
|
vectorDrawables.useSupportLibrary = true
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "com.looker.droidify.TestRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@ -40,15 +40,13 @@ android {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
androidResources {
|
ksp {
|
||||||
generateLocaleConfig = true
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
arg("room.generateKotlin", "true")
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets.forEach { source ->
|
androidResources {
|
||||||
val javaDir = source.java.srcDirs.find { it.name == "java" }
|
generateLocaleConfig = true
|
||||||
source.java {
|
|
||||||
srcDir(File(javaDir?.parentFile, "kotlin"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@ -130,19 +128,17 @@ dependencies {
|
|||||||
implementation(libs.kotlin.stdlib)
|
implementation(libs.kotlin.stdlib)
|
||||||
implementation(libs.datetime)
|
implementation(libs.datetime)
|
||||||
|
|
||||||
implementation(libs.coroutines.core)
|
implementation(libs.bundles.coroutines)
|
||||||
implementation(libs.coroutines.android)
|
|
||||||
implementation(libs.coroutines.guava)
|
|
||||||
|
|
||||||
implementation(libs.libsu.core)
|
implementation(libs.libsu.core)
|
||||||
implementation(libs.shizuku.api)
|
implementation(libs.bundles.shizuku)
|
||||||
api(libs.shizuku.provider)
|
|
||||||
|
|
||||||
implementation(libs.jackson.core)
|
implementation(libs.jackson.core)
|
||||||
implementation(libs.serialization)
|
implementation(libs.serialization)
|
||||||
|
|
||||||
implementation(libs.ktor.core)
|
implementation(libs.bundles.ktor)
|
||||||
implementation(libs.ktor.okhttp)
|
implementation(libs.bundles.room)
|
||||||
|
ksp(libs.room.compiler)
|
||||||
|
|
||||||
implementation(libs.work.ktx)
|
implementation(libs.work.ktx)
|
||||||
|
|
||||||
@ -155,8 +151,10 @@ dependencies {
|
|||||||
testImplementation(platform(libs.junit.bom))
|
testImplementation(platform(libs.junit.bom))
|
||||||
testImplementation(libs.bundles.test.unit)
|
testImplementation(libs.bundles.test.unit)
|
||||||
testRuntimeOnly(libs.junit.platform)
|
testRuntimeOnly(libs.junit.platform)
|
||||||
androidTestImplementation(platform(libs.junit.bom))
|
androidTestImplementation(libs.hilt.test)
|
||||||
|
androidTestImplementation(libs.room.test)
|
||||||
androidTestImplementation(libs.bundles.test.android)
|
androidTestImplementation(libs.bundles.test.android)
|
||||||
|
kspAndroidTest(libs.hilt.compiler)
|
||||||
|
|
||||||
// debugImplementation(libs.leakcanary)
|
// debugImplementation(libs.leakcanary)
|
||||||
}
|
}
|
||||||
|
1084
app/schemas/com.looker.droidify.data.local.DroidifyDatabase/1.json
Normal file
1084
app/schemas/com.looker.droidify.data.local.DroidifyDatabase/1.json
Normal file
File diff suppressed because it is too large
Load Diff
116
app/src/androidTest/kotlin/com/looker/droidify/RoomTesting.kt
Normal file
116
app/src/androidTest/kotlin/com/looker/droidify/RoomTesting.kt
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package com.looker.droidify
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.looker.droidify.data.local.dao.AppDao
|
||||||
|
import com.looker.droidify.data.local.dao.IndexDao
|
||||||
|
import com.looker.droidify.datastore.model.SortOrder
|
||||||
|
import com.looker.droidify.domain.model.Fingerprint
|
||||||
|
import com.looker.droidify.domain.model.Repo
|
||||||
|
import com.looker.droidify.domain.model.VersionInfo
|
||||||
|
import com.looker.droidify.model.Repository
|
||||||
|
import com.looker.droidify.sync.FakeDownloader
|
||||||
|
import com.looker.droidify.sync.common.JsonParser
|
||||||
|
import com.looker.droidify.sync.common.downloadIndex
|
||||||
|
import com.looker.droidify.sync.v2.model.IndexV2
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
@HiltAndroidTest
|
||||||
|
class RoomTesting {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val hiltRule = HiltAndroidRule(this)
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var indexDao: IndexDao
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var appDao: AppDao
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ApplicationContext
|
||||||
|
lateinit var context: Context
|
||||||
|
|
||||||
|
private val defaults = Repository.defaultRepositories
|
||||||
|
private val izzyLegacy = defaults[4]
|
||||||
|
private val fdroidLegacy = defaults[0]
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun before() = runTest {
|
||||||
|
hiltRule.inject()
|
||||||
|
launch {
|
||||||
|
val izzy = izzyLegacy.toRepo(1)
|
||||||
|
val izzyFile = FakeDownloader.downloadIndex(context, izzy, "i2", "index-v2.json")
|
||||||
|
val izzyIndex =
|
||||||
|
JsonParser.decodeFromString<IndexV2>(izzyFile.readBytes().decodeToString())
|
||||||
|
indexDao.insertIndex(
|
||||||
|
fingerprint = izzy.fingerprint!!,
|
||||||
|
index = izzyIndex,
|
||||||
|
expectedRepoId = izzy.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// launch {
|
||||||
|
// val fdroid = fdroidLegacy.toRepo(2)
|
||||||
|
// val fdroidFile =
|
||||||
|
// FakeDownloader.downloadIndex(context, fdroid, "f2", "fdroid-index-v2.json")
|
||||||
|
// val fdroidIndex =
|
||||||
|
// JsonParser.decodeFromString<IndexV2>(fdroidFile.readBytes().decodeToString())
|
||||||
|
// indexDao.insertIndex(
|
||||||
|
// fingerprint = fdroid.fingerprint!!,
|
||||||
|
// index = fdroidIndex,
|
||||||
|
// expectedRepoId = fdroid.id,
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sortOrderTest() = runTest {
|
||||||
|
val lastUpdatedQuery = appDao.query(sortOrder = SortOrder.UPDATED)
|
||||||
|
var previousUpdated = Long.MAX_VALUE
|
||||||
|
lastUpdatedQuery.forEach {
|
||||||
|
println("Previous: $previousUpdated, Current: ${it.lastUpdated}")
|
||||||
|
assertTrue(it.lastUpdated <= previousUpdated)
|
||||||
|
previousUpdated = it.lastUpdated
|
||||||
|
}
|
||||||
|
|
||||||
|
val addedQuery = appDao.query(sortOrder = SortOrder.ADDED)
|
||||||
|
var previousAdded = Long.MAX_VALUE
|
||||||
|
addedQuery.forEach {
|
||||||
|
println("Previous: $previousAdded, Current: ${it.added}")
|
||||||
|
assertTrue(it.added <= previousAdded)
|
||||||
|
previousAdded = it.added
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun categoryTest() = runTest {
|
||||||
|
val categoryQuery = appDao.query(
|
||||||
|
sortOrder = SortOrder.UPDATED,
|
||||||
|
categoriesToInclude = listOf("Games", "Food"),
|
||||||
|
)
|
||||||
|
val nonCategoryQuery = appDao.query(
|
||||||
|
sortOrder = SortOrder.UPDATED,
|
||||||
|
categoriesToExclude = listOf("Games", "Food"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Repository.toRepo(id: Int) = Repo(
|
||||||
|
id = id,
|
||||||
|
enabled = enabled,
|
||||||
|
address = address,
|
||||||
|
name = name,
|
||||||
|
description = description,
|
||||||
|
fingerprint = Fingerprint(fingerprint),
|
||||||
|
authentication = null,
|
||||||
|
versionInfo = VersionInfo(timestamp, entityTag),
|
||||||
|
mirrors = emptyList(),
|
||||||
|
)
|
16
app/src/androidTest/kotlin/com/looker/droidify/TestRunner.kt
Normal file
16
app/src/androidTest/kotlin/com/looker/droidify/TestRunner.kt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package com.looker.droidify
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.test.runner.AndroidJUnitRunner
|
||||||
|
import dagger.hilt.android.testing.HiltTestApplication
|
||||||
|
|
||||||
|
class TestRunner : AndroidJUnitRunner() {
|
||||||
|
override fun newApplication(
|
||||||
|
cl: ClassLoader,
|
||||||
|
appName: String,
|
||||||
|
context: Context,
|
||||||
|
): Application {
|
||||||
|
return super.newApplication(cl, HiltTestApplication::class.java.getName(), context)
|
||||||
|
}
|
||||||
|
}
|
@ -4,12 +4,18 @@ import android.content.Context
|
|||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.SmallTest
|
import androidx.test.filters.SmallTest
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import com.looker.droidify.database.Database
|
||||||
|
import com.looker.droidify.index.RepositoryUpdater.IndexType
|
||||||
import com.looker.droidify.model.Repository
|
import com.looker.droidify.model.Repository
|
||||||
|
import com.looker.droidify.sync.FakeDownloader
|
||||||
|
import com.looker.droidify.sync.common.assets
|
||||||
|
import com.looker.droidify.sync.common.benchmark
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.math.sqrt
|
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@ -21,7 +27,9 @@ class RepositoryUpdaterTest {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
context = InstrumentationRegistry.getInstrumentation().context
|
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
Database.init(context)
|
||||||
|
RepositoryUpdater.init(CoroutineScope(Dispatchers.Default), FakeDownloader)
|
||||||
repository = Repository(
|
repository = Repository(
|
||||||
id = 15,
|
id = 15,
|
||||||
address = "https://apt.izzysoft.de/fdroid/repo",
|
address = "https://apt.izzysoft.de/fdroid/repo",
|
||||||
@ -41,13 +49,14 @@ class RepositoryUpdaterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun processFile() {
|
fun processFile() {
|
||||||
testRepetition(1) {
|
val output = benchmark(1) {
|
||||||
val createFile = File.createTempFile("index", "entry")
|
val createFile = File.createTempFile("index", "entry")
|
||||||
val mergerFile = File.createTempFile("index", "merger")
|
val mergerFile = File.createTempFile("index", "merger")
|
||||||
val jarStream = context.resources.assets.open("index-v1.jar")
|
val jarStream = context.resources.assets.open("index-v1.jar")
|
||||||
jarStream.copyTo(createFile.outputStream())
|
jarStream.copyTo(createFile.outputStream())
|
||||||
process(createFile, mergerFile)
|
process(createFile, mergerFile)
|
||||||
}
|
}
|
||||||
|
println(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun process(file: File, merger: File) = measureTimeMillis {
|
private fun process(file: File, merger: File) = measureTimeMillis {
|
||||||
@ -65,28 +74,4 @@ class RepositoryUpdaterTest {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun testRepetition(repetition: Int, block: () -> Long) {
|
|
||||||
val times = (1..repetition).map {
|
|
||||||
System.gc()
|
|
||||||
System.runFinalization()
|
|
||||||
block().toDouble()
|
|
||||||
}
|
|
||||||
val meanAndDeviation = times.culledMeanAndDeviation()
|
|
||||||
println(times)
|
|
||||||
println("${meanAndDeviation.first} ± ${meanAndDeviation.second}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<Double>.culledMeanAndDeviation(): Pair<Double, Double> = when {
|
|
||||||
isEmpty() -> Double.NaN to Double.NaN
|
|
||||||
size == 1 || size == 2 -> this.meanAndDeviation()
|
|
||||||
else -> sorted().subList(1, size - 1).meanAndDeviation()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun List<Double>.meanAndDeviation(): Pair<Double, Double> {
|
|
||||||
val mean = average()
|
|
||||||
return mean to sqrt(fold(0.0) { acc, value -> acc + (value - mean).squared() } / size)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Double.squared() = this * this
|
|
||||||
|
@ -44,7 +44,7 @@ class EntrySyncableTest {
|
|||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
@Before
|
@Before
|
||||||
fun before() {
|
fun before() {
|
||||||
context = InstrumentationRegistry.getInstrumentation().context
|
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
dispatcher = StandardTestDispatcher()
|
dispatcher = StandardTestDispatcher()
|
||||||
validator = IndexJarValidator(dispatcher)
|
validator = IndexJarValidator(dispatcher)
|
||||||
parser = EntryParser(dispatcher, JsonParser, validator)
|
parser = EntryParser(dispatcher, JsonParser, validator)
|
||||||
|
@ -7,8 +7,8 @@ import com.looker.droidify.domain.model.Repo
|
|||||||
import com.looker.droidify.sync.common.IndexJarValidator
|
import com.looker.droidify.sync.common.IndexJarValidator
|
||||||
import com.looker.droidify.sync.common.Izzy
|
import com.looker.droidify.sync.common.Izzy
|
||||||
import com.looker.droidify.sync.common.JsonParser
|
import com.looker.droidify.sync.common.JsonParser
|
||||||
import com.looker.droidify.sync.common.downloadIndex
|
|
||||||
import com.looker.droidify.sync.common.benchmark
|
import com.looker.droidify.sync.common.benchmark
|
||||||
|
import com.looker.droidify.sync.common.downloadIndex
|
||||||
import com.looker.droidify.sync.common.toV2
|
import com.looker.droidify.sync.common.toV2
|
||||||
import com.looker.droidify.sync.v1.V1Parser
|
import com.looker.droidify.sync.v1.V1Parser
|
||||||
import com.looker.droidify.sync.v1.V1Syncable
|
import com.looker.droidify.sync.v1.V1Syncable
|
||||||
@ -17,6 +17,7 @@ import com.looker.droidify.sync.v2.V2Parser
|
|||||||
import com.looker.droidify.sync.v2.model.FileV2
|
import com.looker.droidify.sync.v2.model.FileV2
|
||||||
import com.looker.droidify.sync.v2.model.IndexV2
|
import com.looker.droidify.sync.v2.model.IndexV2
|
||||||
import com.looker.droidify.sync.v2.model.MetadataV2
|
import com.looker.droidify.sync.v2.model.MetadataV2
|
||||||
|
import com.looker.droidify.sync.v2.model.PackageV2
|
||||||
import com.looker.droidify.sync.v2.model.VersionV2
|
import com.looker.droidify.sync.v2.model.VersionV2
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
@ -28,6 +29,8 @@ import kotlin.test.Test
|
|||||||
import kotlin.test.assertContentEquals
|
import kotlin.test.assertContentEquals
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertNotNull
|
import kotlin.test.assertNotNull
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class V1SyncableTest {
|
class V1SyncableTest {
|
||||||
@ -42,7 +45,7 @@ class V1SyncableTest {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun before() {
|
fun before() {
|
||||||
context = InstrumentationRegistry.getInstrumentation().context
|
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
dispatcher = StandardTestDispatcher()
|
dispatcher = StandardTestDispatcher()
|
||||||
validator = IndexJarValidator(dispatcher)
|
validator = IndexJarValidator(dispatcher)
|
||||||
parser = V1Parser(dispatcher, JsonParser, validator)
|
parser = V1Parser(dispatcher, JsonParser, validator)
|
||||||
@ -102,9 +105,38 @@ class V1SyncableTest {
|
|||||||
testIndexConversion("index-v1.jar", "index-v2-updated.json")
|
testIndexConversion("index-v1.jar", "index-v2-updated.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Test
|
@Test
|
||||||
fun v1tov2FDroidRepo() = runTest(dispatcher) {
|
fun targetPropertyTest() = runTest(dispatcher) {
|
||||||
testIndexConversion("fdroid-index-v1.jar", "fdroid-index-v2.json")
|
val v2IzzyFile =
|
||||||
|
FakeDownloader.downloadIndex(context, repo, "izzy-v2", "index-v2-updated.json")
|
||||||
|
val v2FdroidFile =
|
||||||
|
FakeDownloader.downloadIndex(context, repo, "fdroid-v2", "fdroid-index-v2.json")
|
||||||
|
val (_, v2Izzy) = v2Parser.parse(v2IzzyFile, repo)
|
||||||
|
val (_, v2Fdroid) = v2Parser.parse(v2FdroidFile, repo)
|
||||||
|
|
||||||
|
val performTest: (PackageV2) -> Unit = { data ->
|
||||||
|
print("lib: ")
|
||||||
|
println(data.metadata.liberapay)
|
||||||
|
print("donate: ")
|
||||||
|
println(data.metadata.donate)
|
||||||
|
print("bit: ")
|
||||||
|
println(data.metadata.bitcoin)
|
||||||
|
print("flattr: ")
|
||||||
|
println(data.metadata.flattrID)
|
||||||
|
print("Open: ")
|
||||||
|
println(data.metadata.openCollective)
|
||||||
|
print("LiteCoin: ")
|
||||||
|
println(data.metadata.litecoin)
|
||||||
|
}
|
||||||
|
|
||||||
|
v2Izzy.packages.forEach { (packageName, data) ->
|
||||||
|
println("Testing on Izzy $packageName")
|
||||||
|
performTest(data)
|
||||||
|
}
|
||||||
|
v2Fdroid.packages.forEach { (packageName, data) ->
|
||||||
|
println("Testing on FDroid $packageName")
|
||||||
|
performTest(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun testIndexConversion(
|
private suspend fun testIndexConversion(
|
||||||
@ -252,6 +284,8 @@ private fun assertVersion(
|
|||||||
assertNotNull(foundVersion)
|
assertNotNull(foundVersion)
|
||||||
|
|
||||||
assertEquals(expectedVersion.added, foundVersion.added)
|
assertEquals(expectedVersion.added, foundVersion.added)
|
||||||
|
assertEquals(expectedVersion.file.sha256, foundVersion.file.sha256)
|
||||||
|
assertEquals(expectedVersion.file.size, foundVersion.file.size)
|
||||||
assertEquals(expectedVersion.file.name, foundVersion.file.name)
|
assertEquals(expectedVersion.file.name, foundVersion.file.name)
|
||||||
assertEquals(expectedVersion.src?.name, foundVersion.src?.name)
|
assertEquals(expectedVersion.src?.name, foundVersion.src?.name)
|
||||||
|
|
||||||
@ -261,7 +295,13 @@ private fun assertVersion(
|
|||||||
assertEquals(expectedMan.versionCode, foundMan.versionCode)
|
assertEquals(expectedMan.versionCode, foundMan.versionCode)
|
||||||
assertEquals(expectedMan.versionName, foundMan.versionName)
|
assertEquals(expectedMan.versionName, foundMan.versionName)
|
||||||
assertEquals(expectedMan.maxSdkVersion, foundMan.maxSdkVersion)
|
assertEquals(expectedMan.maxSdkVersion, foundMan.maxSdkVersion)
|
||||||
|
assertNotNull(expectedMan.usesSdk)
|
||||||
|
assertNotNull(foundMan.usesSdk)
|
||||||
assertEquals(expectedMan.usesSdk, foundMan.usesSdk)
|
assertEquals(expectedMan.usesSdk, foundMan.usesSdk)
|
||||||
|
assertTrue(expectedMan.usesSdk.minSdkVersion >= 1)
|
||||||
|
assertTrue(expectedMan.usesSdk.targetSdkVersion >= 1)
|
||||||
|
assertTrue(foundMan.usesSdk.minSdkVersion >= 1)
|
||||||
|
assertTrue(foundMan.usesSdk.targetSdkVersion >= 1)
|
||||||
|
|
||||||
assertContentEquals(
|
assertContentEquals(
|
||||||
expectedMan.features.sortedBy { it.name },
|
expectedMan.features.sortedBy { it.name },
|
||||||
|
@ -8,11 +8,6 @@ internal inline fun benchmark(
|
|||||||
extraMessage: String? = null,
|
extraMessage: String? = null,
|
||||||
block: () -> Long,
|
block: () -> Long,
|
||||||
): String {
|
): String {
|
||||||
if (extraMessage != null) {
|
|
||||||
println("=".repeat(50))
|
|
||||||
println(extraMessage)
|
|
||||||
println("=".repeat(50))
|
|
||||||
}
|
|
||||||
val times = DoubleArray(repetition)
|
val times = DoubleArray(repetition)
|
||||||
repeat(repetition) { iteration ->
|
repeat(repetition) { iteration ->
|
||||||
System.gc()
|
System.gc()
|
||||||
@ -20,11 +15,19 @@ internal inline fun benchmark(
|
|||||||
times[iteration] = block().toDouble()
|
times[iteration] = block().toDouble()
|
||||||
}
|
}
|
||||||
val meanAndDeviation = times.culledMeanAndDeviation()
|
val meanAndDeviation = times.culledMeanAndDeviation()
|
||||||
return buildString {
|
return buildString(200) {
|
||||||
append("=".repeat(50))
|
append("=".repeat(50))
|
||||||
append("\n")
|
append("\n")
|
||||||
append(times.joinToString(" | "))
|
if (extraMessage != null) {
|
||||||
append("\n")
|
append(extraMessage)
|
||||||
|
append("\n")
|
||||||
|
append("=".repeat(50))
|
||||||
|
append("\n")
|
||||||
|
}
|
||||||
|
if (times.size > 1) {
|
||||||
|
append(times.joinToString(" | "))
|
||||||
|
append("\n")
|
||||||
|
}
|
||||||
append("${meanAndDeviation.first} ms ± ${meanAndDeviation.second.toFloat()} ms")
|
append("${meanAndDeviation.first} ms ± ${meanAndDeviation.second.toFloat()} ms")
|
||||||
append("\n")
|
append("\n")
|
||||||
append("=".repeat(50))
|
append("=".repeat(50))
|
||||||
|
@ -6,7 +6,7 @@ import com.looker.droidify.domain.model.Repo
|
|||||||
import com.looker.droidify.domain.model.VersionInfo
|
import com.looker.droidify.domain.model.VersionInfo
|
||||||
|
|
||||||
val Izzy = Repo(
|
val Izzy = Repo(
|
||||||
id = 1L,
|
id = 1,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
address = "https://apt.izzysoft.de/fdroid/repo",
|
address = "https://apt.izzysoft.de/fdroid/repo",
|
||||||
name = "IzzyOnDroid F-Droid Repo",
|
name = "IzzyOnDroid F-Droid Repo",
|
||||||
@ -15,6 +15,4 @@ val Izzy = Repo(
|
|||||||
authentication = Authentication("", ""),
|
authentication = Authentication("", ""),
|
||||||
versionInfo = VersionInfo(0L, null),
|
versionInfo = VersionInfo(0L, null),
|
||||||
mirrors = emptyList(),
|
mirrors = emptyList(),
|
||||||
antiFeatures = emptyList(),
|
|
||||||
categories = emptyList(),
|
|
||||||
)
|
)
|
||||||
|
1
app/src/debug/assets/izzy_index_v2.json
Normal file
1
app/src/debug/assets/izzy_index_v2.json
Normal file
File diff suppressed because one or more lines are too long
@ -80,17 +80,17 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
if (BuildConfig.DEBUG && SdkCheck.isOreo) strictThreadPolicy()
|
// if (BuildConfig.DEBUG && SdkCheck.isOreo) strictThreadPolicy()
|
||||||
|
|
||||||
val databaseUpdated = Database.init(this)
|
val databaseUpdated = Database.init(this)
|
||||||
ProductPreferences.init(this, appScope)
|
ProductPreferences.init(this, appScope)
|
||||||
RepositoryUpdater.init(appScope, downloader)
|
// RepositoryUpdater.init(appScope, downloader)
|
||||||
listenApplications()
|
listenApplications()
|
||||||
checkLanguage()
|
checkLanguage()
|
||||||
updatePreference()
|
updatePreference()
|
||||||
appScope.launch { installer() }
|
appScope.launch { installer() }
|
||||||
|
|
||||||
if (databaseUpdated) forceSyncAll()
|
// if (databaseUpdated) forceSyncAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTerminate() {
|
override fun onTerminate() {
|
||||||
@ -107,7 +107,7 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
|||||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||||
addDataScheme("package")
|
addDataScheme("package")
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
val installedItems =
|
val installedItems =
|
||||||
packageManager.getInstalledPackagesCompat()
|
packageManager.getInstalledPackagesCompat()
|
||||||
@ -200,7 +200,7 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
|||||||
periodMillis = period,
|
periodMillis = period,
|
||||||
networkType = syncConditions.toJobNetworkType(),
|
networkType = syncConditions.toJobNetworkType(),
|
||||||
isCharging = syncConditions.pluggedIn,
|
isCharging = syncConditions.pluggedIn,
|
||||||
isBatteryLow = syncConditions.batteryNotLow
|
isBatteryLow = syncConditions.batteryNotLow,
|
||||||
)
|
)
|
||||||
jobScheduler?.schedule(job)
|
jobScheduler?.schedule(job)
|
||||||
}
|
}
|
||||||
@ -212,10 +212,13 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
|||||||
Database.RepositoryAdapter.put(it.copy(lastModified = "", entityTag = ""))
|
Database.RepositoryAdapter.put(it.copy(lastModified = "", entityTag = ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Connection(SyncService::class.java, onBind = { connection, binder ->
|
Connection(
|
||||||
binder.sync(SyncService.SyncRequest.FORCE)
|
SyncService::class.java,
|
||||||
connection.unbind(this)
|
onBind = { connection, binder ->
|
||||||
}).bind(this)
|
binder.sync(SyncService.SyncRequest.FORCE)
|
||||||
|
connection.unbind(this)
|
||||||
|
},
|
||||||
|
).bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
@ -256,12 +259,12 @@ fun strictThreadPolicy() {
|
|||||||
.detectNetwork()
|
.detectNetwork()
|
||||||
.detectUnbufferedIo()
|
.detectUnbufferedIo()
|
||||||
.penaltyLog()
|
.penaltyLog()
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
StrictMode.setVmPolicy(
|
StrictMode.setVmPolicy(
|
||||||
StrictMode.VmPolicy.Builder()
|
StrictMode.VmPolicy.Builder()
|
||||||
.detectAll()
|
.detectAll()
|
||||||
.penaltyLog()
|
.penaltyLog()
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
|
package com.looker.droidify.data.encryption
|
||||||
|
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.KeyGenerator
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
import kotlin.io.encoding.Base64
|
||||||
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
|
private const val KEY_SIZE = 256
|
||||||
|
private const val IV_SIZE = 16
|
||||||
|
private const val ALGORITHM = "AES"
|
||||||
|
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
value class Key(val secretKey: ByteArray) {
|
||||||
|
|
||||||
|
val spec: SecretKeySpec
|
||||||
|
get() = SecretKeySpec(secretKey, ALGORITHM)
|
||||||
|
|
||||||
|
fun encrypt(input: String): Pair<Encrypted, ByteArray> {
|
||||||
|
val iv = generateIV()
|
||||||
|
val ivSpec = IvParameterSpec(iv)
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, spec, ivSpec)
|
||||||
|
val encrypted = cipher.doFinal(input.toByteArray())
|
||||||
|
return Encrypted(Base64.encode(encrypted)) to iv
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Before encrypting we convert it to a base64 string
|
||||||
|
* */
|
||||||
|
@JvmInline
|
||||||
|
value class Encrypted(val value: String) {
|
||||||
|
fun decrypt(key: Key, iv: ByteArray): String {
|
||||||
|
val iv = IvParameterSpec(iv)
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, key.spec, iv)
|
||||||
|
val decrypted = cipher.doFinal(Base64.decode(value))
|
||||||
|
return String(decrypted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateSecretKey(): ByteArray {
|
||||||
|
return with(KeyGenerator.getInstance(ALGORITHM)) {
|
||||||
|
init(KEY_SIZE)
|
||||||
|
generateKey().encoded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateIV(): ByteArray {
|
||||||
|
val iv = ByteArray(IV_SIZE)
|
||||||
|
val secureRandom = SecureRandom()
|
||||||
|
secureRandom.nextBytes(iv)
|
||||||
|
return iv
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
package com.looker.droidify.data.local
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.BuiltInTypeConverters
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
import com.looker.droidify.data.local.converters.Converters
|
||||||
|
import com.looker.droidify.data.local.converters.PermissionConverter
|
||||||
|
import com.looker.droidify.data.local.dao.AppDao
|
||||||
|
import com.looker.droidify.data.local.dao.AuthDao
|
||||||
|
import com.looker.droidify.data.local.dao.IndexDao
|
||||||
|
import com.looker.droidify.data.local.dao.RepoDao
|
||||||
|
import com.looker.droidify.data.local.model.AntiFeatureAppRelation
|
||||||
|
import com.looker.droidify.data.local.model.AntiFeatureEntity
|
||||||
|
import com.looker.droidify.data.local.model.AntiFeatureRepoRelation
|
||||||
|
import com.looker.droidify.data.local.model.AppEntity
|
||||||
|
import com.looker.droidify.data.local.model.AuthenticationEntity
|
||||||
|
import com.looker.droidify.data.local.model.AuthorEntity
|
||||||
|
import com.looker.droidify.data.local.model.CategoryAppRelation
|
||||||
|
import com.looker.droidify.data.local.model.CategoryEntity
|
||||||
|
import com.looker.droidify.data.local.model.CategoryRepoRelation
|
||||||
|
import com.looker.droidify.data.local.model.DonateEntity
|
||||||
|
import com.looker.droidify.data.local.model.GraphicEntity
|
||||||
|
import com.looker.droidify.data.local.model.InstalledEntity
|
||||||
|
import com.looker.droidify.data.local.model.LinksEntity
|
||||||
|
import com.looker.droidify.data.local.model.MirrorEntity
|
||||||
|
import com.looker.droidify.data.local.model.RepoEntity
|
||||||
|
import com.looker.droidify.data.local.model.ScreenshotEntity
|
||||||
|
import com.looker.droidify.data.local.model.VersionEntity
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
entities = [
|
||||||
|
AntiFeatureEntity::class,
|
||||||
|
AntiFeatureAppRelation::class,
|
||||||
|
AntiFeatureRepoRelation::class,
|
||||||
|
AuthenticationEntity::class,
|
||||||
|
AuthorEntity::class,
|
||||||
|
AppEntity::class,
|
||||||
|
CategoryEntity::class,
|
||||||
|
CategoryAppRelation::class,
|
||||||
|
CategoryRepoRelation::class,
|
||||||
|
DonateEntity::class,
|
||||||
|
GraphicEntity::class,
|
||||||
|
InstalledEntity::class,
|
||||||
|
LinksEntity::class,
|
||||||
|
MirrorEntity::class,
|
||||||
|
RepoEntity::class,
|
||||||
|
ScreenshotEntity::class,
|
||||||
|
VersionEntity::class,
|
||||||
|
],
|
||||||
|
version = 1,
|
||||||
|
)
|
||||||
|
@TypeConverters(
|
||||||
|
PermissionConverter::class,
|
||||||
|
Converters::class,
|
||||||
|
builtInTypeConverters = BuiltInTypeConverters(),
|
||||||
|
)
|
||||||
|
abstract class DroidifyDatabase : RoomDatabase() {
|
||||||
|
abstract fun appDao(): AppDao
|
||||||
|
abstract fun repoDao(): RepoDao
|
||||||
|
abstract fun authDao(): AuthDao
|
||||||
|
abstract fun indexDao(): IndexDao
|
||||||
|
}
|
||||||
|
|
||||||
|
fun droidifyDatabase(context: Context): DroidifyDatabase = Room
|
||||||
|
.databaseBuilder(
|
||||||
|
context = context,
|
||||||
|
klass = DroidifyDatabase::class.java,
|
||||||
|
name = "droidify_room",
|
||||||
|
)
|
||||||
|
.addCallback(
|
||||||
|
object : RoomDatabase.Callback() {
|
||||||
|
override fun onOpen(db: SupportSQLiteDatabase) {
|
||||||
|
super.onOpen(db)
|
||||||
|
db.query("PRAGMA synchronous = OFF")
|
||||||
|
db.query("PRAGMA journal_mode = WAL")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.build()
|
@ -0,0 +1,61 @@
|
|||||||
|
package com.looker.droidify.data.local.converters
|
||||||
|
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import com.looker.droidify.sync.common.JsonParser
|
||||||
|
import com.looker.droidify.sync.v2.model.FileV2
|
||||||
|
import com.looker.droidify.sync.v2.model.LocalizedIcon
|
||||||
|
import com.looker.droidify.sync.v2.model.LocalizedString
|
||||||
|
import kotlinx.serialization.builtins.ListSerializer
|
||||||
|
import kotlinx.serialization.builtins.MapSerializer
|
||||||
|
import kotlinx.serialization.builtins.serializer
|
||||||
|
|
||||||
|
private val localizedStringSerializer =
|
||||||
|
MapSerializer(String.serializer(), String.serializer())
|
||||||
|
private val stringListSerializer = ListSerializer(String.serializer())
|
||||||
|
private val localizedIconSerializer =
|
||||||
|
MapSerializer(String.serializer(), FileV2.serializer())
|
||||||
|
private val mapOfLocalizedStringsSerializer =
|
||||||
|
MapSerializer(String.serializer(), localizedStringSerializer)
|
||||||
|
|
||||||
|
object Converters {
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromLocalizedString(value: LocalizedString): String {
|
||||||
|
return JsonParser.encodeToString(localizedStringSerializer, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toLocalizedString(value: String): LocalizedString {
|
||||||
|
return JsonParser.decodeFromString(localizedStringSerializer, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromLocalizedIcon(value: LocalizedIcon?): String? {
|
||||||
|
return value?.let { JsonParser.encodeToString(localizedIconSerializer, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toLocalizedIcon(value: String?): LocalizedIcon? {
|
||||||
|
return value?.let { JsonParser.decodeFromString(localizedIconSerializer, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromLocalizedList(value: Map<String, LocalizedString>): String {
|
||||||
|
return JsonParser.encodeToString(mapOfLocalizedStringsSerializer, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toLocalizedList(value: String): Map<String, LocalizedString> {
|
||||||
|
return JsonParser.decodeFromString(mapOfLocalizedStringsSerializer, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromStringList(value: List<String>): String {
|
||||||
|
return JsonParser.encodeToString(stringListSerializer, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toStringList(value: String): List<String> {
|
||||||
|
return JsonParser.decodeFromString(stringListSerializer, value)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package com.looker.droidify.data.local.converters
|
||||||
|
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import com.looker.droidify.sync.common.JsonParser
|
||||||
|
import com.looker.droidify.sync.v2.model.PermissionV2
|
||||||
|
import kotlinx.serialization.builtins.ListSerializer
|
||||||
|
|
||||||
|
private val permissionListSerializer = ListSerializer(PermissionV2.serializer())
|
||||||
|
|
||||||
|
object PermissionConverter {
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromPermissionV2List(value: List<PermissionV2>): String {
|
||||||
|
return JsonParser.encodeToString(permissionListSerializer, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toPermissionV2List(value: String): List<PermissionV2> {
|
||||||
|
return JsonParser.decodeFromString(permissionListSerializer, value)
|
||||||
|
}
|
||||||
|
}
|
189
app/src/main/kotlin/com/looker/droidify/data/local/dao/AppDao.kt
Normal file
189
app/src/main/kotlin/com/looker/droidify/data/local/dao/AppDao.kt
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
package com.looker.droidify.data.local.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.RawQuery
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||||
|
import com.looker.droidify.data.local.model.AntiFeatureAppRelation
|
||||||
|
import com.looker.droidify.data.local.model.AppEntity
|
||||||
|
import com.looker.droidify.data.local.model.AppEntityRelations
|
||||||
|
import com.looker.droidify.data.local.model.CategoryAppRelation
|
||||||
|
import com.looker.droidify.data.local.model.VersionEntity
|
||||||
|
import com.looker.droidify.datastore.model.SortOrder
|
||||||
|
import com.looker.droidify.sync.v2.model.DefaultName
|
||||||
|
import com.looker.droidify.sync.v2.model.Tag
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface AppDao {
|
||||||
|
|
||||||
|
@RawQuery(
|
||||||
|
observedEntities = [
|
||||||
|
AppEntity::class,
|
||||||
|
VersionEntity::class,
|
||||||
|
CategoryAppRelation::class,
|
||||||
|
AntiFeatureAppRelation::class,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
fun _rawStreamAppEntities(query: SimpleSQLiteQuery): Flow<List<AppEntity>>
|
||||||
|
|
||||||
|
@RawQuery
|
||||||
|
suspend fun _rawQueryAppEntities(query: SimpleSQLiteQuery): List<AppEntity>
|
||||||
|
|
||||||
|
fun stream(
|
||||||
|
sortOrder: SortOrder,
|
||||||
|
searchQuery: String? = null,
|
||||||
|
repoId: Int? = null,
|
||||||
|
categoriesToInclude: List<DefaultName>? = null,
|
||||||
|
categoriesToExclude: List<DefaultName>? = null,
|
||||||
|
antiFeaturesToInclude: List<Tag>? = null,
|
||||||
|
antiFeaturesToExclude: List<Tag>? = null,
|
||||||
|
): Flow<List<AppEntity>> = _rawStreamAppEntities(
|
||||||
|
searchQuery(
|
||||||
|
sortOrder = sortOrder,
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
repoId = repoId,
|
||||||
|
categoriesToInclude = categoriesToInclude,
|
||||||
|
categoriesToExclude = categoriesToExclude,
|
||||||
|
antiFeaturesToInclude = antiFeaturesToInclude,
|
||||||
|
antiFeaturesToExclude = antiFeaturesToExclude,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun query(
|
||||||
|
sortOrder: SortOrder,
|
||||||
|
searchQuery: String? = null,
|
||||||
|
repoId: Int? = null,
|
||||||
|
categoriesToInclude: List<DefaultName>? = null,
|
||||||
|
categoriesToExclude: List<DefaultName>? = null,
|
||||||
|
antiFeaturesToInclude: List<Tag>? = null,
|
||||||
|
antiFeaturesToExclude: List<Tag>? = null,
|
||||||
|
): List<AppEntity> = _rawQueryAppEntities(
|
||||||
|
searchQuery(
|
||||||
|
sortOrder = sortOrder,
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
repoId = repoId,
|
||||||
|
categoriesToInclude = categoriesToInclude,
|
||||||
|
categoriesToExclude = categoriesToExclude,
|
||||||
|
antiFeaturesToInclude = antiFeaturesToInclude,
|
||||||
|
antiFeaturesToExclude = antiFeaturesToExclude,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun searchQuery(
|
||||||
|
sortOrder: SortOrder,
|
||||||
|
searchQuery: String?,
|
||||||
|
repoId: Int?,
|
||||||
|
categoriesToInclude: List<DefaultName>?,
|
||||||
|
categoriesToExclude: List<DefaultName>?,
|
||||||
|
antiFeaturesToInclude: List<Tag>?,
|
||||||
|
antiFeaturesToExclude: List<Tag>?,
|
||||||
|
): SimpleSQLiteQuery {
|
||||||
|
val args = arrayListOf<Any?>()
|
||||||
|
|
||||||
|
val query = buildString(1024) {
|
||||||
|
append("SELECT DISTINCT app.* FROM app")
|
||||||
|
append(" LEFT JOIN version ON app.id = version.appId")
|
||||||
|
append(" LEFT JOIN category_app_relation ON app.id = category_app_relation.id")
|
||||||
|
append(" LEFT JOIN anti_features_app_relation ON app.id = anti_features_app_relation.appId")
|
||||||
|
append(" WHERE 1")
|
||||||
|
|
||||||
|
if (repoId != null) {
|
||||||
|
append(" AND app.repoId = ?")
|
||||||
|
args.add(repoId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoriesToInclude != null) {
|
||||||
|
append(" AND category_app_relation.defaultName IN (")
|
||||||
|
append(categoriesToInclude.joinToString(", ") { "?" })
|
||||||
|
append(")")
|
||||||
|
args.addAll(categoriesToInclude)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoriesToExclude != null) {
|
||||||
|
append(" AND category_app_relation.defaultName NOT IN (")
|
||||||
|
append(categoriesToExclude.joinToString(", ") { "?" })
|
||||||
|
append(")")
|
||||||
|
args.addAll(categoriesToExclude)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (antiFeaturesToInclude != null) {
|
||||||
|
append(" AND anti_features_app_relation.tag IN (")
|
||||||
|
append(antiFeaturesToInclude.joinToString(", ") { "?" })
|
||||||
|
append(")")
|
||||||
|
args.addAll(antiFeaturesToInclude)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (antiFeaturesToExclude != null) {
|
||||||
|
append(" AND anti_features_app_relation.tag NOT IN (")
|
||||||
|
append(antiFeaturesToExclude.joinToString(", ") { "?" })
|
||||||
|
append(")")
|
||||||
|
args.addAll(antiFeaturesToExclude)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchQuery != null) {
|
||||||
|
val searchPattern = "%${searchQuery}%"
|
||||||
|
append(
|
||||||
|
"""
|
||||||
|
AND (
|
||||||
|
app.name LIKE ?
|
||||||
|
OR app.summary LIKE ?
|
||||||
|
OR app.packageName LIKE ?
|
||||||
|
OR app.description LIKE ?
|
||||||
|
)""",
|
||||||
|
)
|
||||||
|
args.addAll(listOf(searchPattern, searchPattern, searchPattern, searchPattern))
|
||||||
|
}
|
||||||
|
|
||||||
|
append(" ORDER BY ")
|
||||||
|
|
||||||
|
// Weighting: name > summary > packageName > description
|
||||||
|
if (searchQuery != null) {
|
||||||
|
val searchPattern = "%${searchQuery}%"
|
||||||
|
append("(CASE WHEN app.name LIKE ? THEN 4 ELSE 0 END) + ")
|
||||||
|
append("(CASE WHEN app.summary LIKE ? THEN 3 ELSE 0 END) + ")
|
||||||
|
append("(CASE WHEN app.packageName LIKE ? THEN 2 ELSE 0 END) + ")
|
||||||
|
append("(CASE WHEN app.description LIKE ? THEN 1 ELSE 0 END) DESC, ")
|
||||||
|
args.addAll(listOf(searchPattern, searchPattern, searchPattern, searchPattern))
|
||||||
|
}
|
||||||
|
|
||||||
|
when (sortOrder) {
|
||||||
|
SortOrder.UPDATED -> append("app.lastUpdated DESC, ")
|
||||||
|
SortOrder.ADDED -> append("app.added DESC, ")
|
||||||
|
SortOrder.SIZE -> append("version.apk_size DESC, ")
|
||||||
|
SortOrder.NAME -> Unit
|
||||||
|
}
|
||||||
|
append("app.name COLLATE LOCALIZED ASC")
|
||||||
|
}
|
||||||
|
|
||||||
|
return SimpleSQLiteQuery(query, args.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT app.*
|
||||||
|
FROM app
|
||||||
|
LEFT JOIN installed
|
||||||
|
ON app.packageName = installed.packageName
|
||||||
|
LEFT JOIN version
|
||||||
|
ON version.appId = app.id
|
||||||
|
WHERE installed.packageName IS NOT NULL
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN version.versionCode > installed.versionCode THEN 1 ELSE 2 END,
|
||||||
|
app.lastUpdated DESC,
|
||||||
|
app.name COLLATE LOCALIZED ASC
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
fun installedStream(): Flow<List<AppEntity>>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM app WHERE packageName = :packageName")
|
||||||
|
fun queryAppEntity(packageName: String): Flow<List<AppEntityRelations>>
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM app")
|
||||||
|
suspend fun count(): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM app WHERE id = :id")
|
||||||
|
suspend fun delete(id: Int)
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package com.looker.droidify.data.local.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.looker.droidify.data.local.model.AuthenticationEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface AuthDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insert(authentication: AuthenticationEntity)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM authentication WHERE repoId = :repoId")
|
||||||
|
suspend fun getAuthentication(repoId: Int): AuthenticationEntity?
|
||||||
|
}
|
@ -0,0 +1,181 @@
|
|||||||
|
package com.looker.droidify.data.local.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Update
|
||||||
|
import com.looker.droidify.data.local.model.AntiFeatureAppRelation
|
||||||
|
import com.looker.droidify.data.local.model.AntiFeatureEntity
|
||||||
|
import com.looker.droidify.data.local.model.AntiFeatureRepoRelation
|
||||||
|
import com.looker.droidify.data.local.model.AppEntity
|
||||||
|
import com.looker.droidify.data.local.model.AuthorEntity
|
||||||
|
import com.looker.droidify.data.local.model.CategoryAppRelation
|
||||||
|
import com.looker.droidify.data.local.model.CategoryEntity
|
||||||
|
import com.looker.droidify.data.local.model.CategoryRepoRelation
|
||||||
|
import com.looker.droidify.data.local.model.DonateEntity
|
||||||
|
import com.looker.droidify.data.local.model.GraphicEntity
|
||||||
|
import com.looker.droidify.data.local.model.LinksEntity
|
||||||
|
import com.looker.droidify.data.local.model.MirrorEntity
|
||||||
|
import com.looker.droidify.data.local.model.RepoEntity
|
||||||
|
import com.looker.droidify.data.local.model.ScreenshotEntity
|
||||||
|
import com.looker.droidify.data.local.model.VersionEntity
|
||||||
|
import com.looker.droidify.data.local.model.antiFeatureEntity
|
||||||
|
import com.looker.droidify.data.local.model.appEntity
|
||||||
|
import com.looker.droidify.data.local.model.authorEntity
|
||||||
|
import com.looker.droidify.data.local.model.categoryEntity
|
||||||
|
import com.looker.droidify.data.local.model.donateEntity
|
||||||
|
import com.looker.droidify.data.local.model.linkEntity
|
||||||
|
import com.looker.droidify.data.local.model.localizedGraphics
|
||||||
|
import com.looker.droidify.data.local.model.localizedScreenshots
|
||||||
|
import com.looker.droidify.data.local.model.mirrorEntity
|
||||||
|
import com.looker.droidify.data.local.model.repoEntity
|
||||||
|
import com.looker.droidify.data.local.model.versionEntities
|
||||||
|
import com.looker.droidify.domain.model.Fingerprint
|
||||||
|
import com.looker.droidify.sync.v2.model.IndexV2
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface IndexDao {
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun insertIndex(
|
||||||
|
fingerprint: Fingerprint,
|
||||||
|
index: IndexV2,
|
||||||
|
expectedRepoId: Int = 0,
|
||||||
|
) {
|
||||||
|
val repoId = upsertRepo(
|
||||||
|
index.repo.repoEntity(
|
||||||
|
id = expectedRepoId,
|
||||||
|
fingerprint = fingerprint,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val antiFeatures = index.repo.antiFeatures.flatMap { (tag, feature) ->
|
||||||
|
feature.antiFeatureEntity(tag)
|
||||||
|
}
|
||||||
|
val categories = index.repo.categories.flatMap { (defaultName, category) ->
|
||||||
|
category.categoryEntity(defaultName)
|
||||||
|
}
|
||||||
|
val antiFeatureRepoRelations = antiFeatures.map { AntiFeatureRepoRelation(repoId, it.tag) }
|
||||||
|
val categoryRepoRelations = categories.map { CategoryRepoRelation(repoId, it.defaultName) }
|
||||||
|
val mirrors = index.repo.mirrors.map { it.mirrorEntity(repoId) }
|
||||||
|
insertAntiFeatures(antiFeatures)
|
||||||
|
insertAntiFeatureRepoRelation(antiFeatureRepoRelations)
|
||||||
|
insertCategories(categories)
|
||||||
|
insertCategoryRepoRelation(categoryRepoRelations)
|
||||||
|
insertMirror(mirrors)
|
||||||
|
index.packages.forEach { (packageName, packages) ->
|
||||||
|
val metadata = packages.metadata
|
||||||
|
val author = metadata.authorEntity()
|
||||||
|
val authorId = upsertAuthor(author)
|
||||||
|
val appId = appIdByPackageName(repoId, packageName) ?: insertApp(
|
||||||
|
appEntity = metadata.appEntity(
|
||||||
|
packageName = packageName,
|
||||||
|
repoId = repoId,
|
||||||
|
authorId = authorId,
|
||||||
|
),
|
||||||
|
).toInt()
|
||||||
|
val versions = packages.versionEntities(appId)
|
||||||
|
insertVersions(versions.keys.toList())
|
||||||
|
insertAntiFeatureAppRelation(versions.values.flatten())
|
||||||
|
val appCategories = packages.metadata.categories.map { CategoryAppRelation(appId, it) }
|
||||||
|
insertCategoryAppRelation(appCategories)
|
||||||
|
metadata.linkEntity(appId)?.let { insertLink(it) }
|
||||||
|
metadata.screenshots?.localizedScreenshots(appId)?.let { insertScreenshots(it) }
|
||||||
|
metadata.localizedGraphics(appId)?.let { insertGraphics(it) }
|
||||||
|
metadata.donateEntity(appId)?.let { insertDonate(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
suspend fun insertRepo(repoEntity: RepoEntity): Long
|
||||||
|
|
||||||
|
@Update(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun updateRepo(repoEntity: RepoEntity): Int
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun upsertRepo(repoEntity: RepoEntity): Int {
|
||||||
|
val id = insertRepo(repoEntity)
|
||||||
|
return if (id == -1L) {
|
||||||
|
updateRepo(repoEntity)
|
||||||
|
} else {
|
||||||
|
id.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertMirror(mirrors: List<MirrorEntity>)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertAntiFeatures(antiFeatures: List<AntiFeatureEntity>)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertCategories(categories: List<CategoryEntity>)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
suspend fun insertAntiFeatureRepoRelation(crossRef: List<AntiFeatureRepoRelation>)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
suspend fun insertCategoryRepoRelation(crossRef: List<CategoryRepoRelation>)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
suspend fun insertApp(appEntity: AppEntity): Long
|
||||||
|
|
||||||
|
@Query("SELECT id FROM app WHERE packageName = :packageName AND repoId = :repoId LIMIT 1")
|
||||||
|
suspend fun appIdByPackageName(repoId: Int, packageName: String): Int?
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
suspend fun insertAuthor(authorEntity: AuthorEntity): Long
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT id FROM author
|
||||||
|
WHERE
|
||||||
|
(:email IS NULL AND email IS NULL OR email = :email) AND
|
||||||
|
(:name IS NULL AND name IS NULL OR name = :name COLLATE NOCASE) AND
|
||||||
|
(:website IS NULL AND website IS NULL OR website = :website COLLATE NOCASE)
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
suspend fun authorId(
|
||||||
|
email: String?,
|
||||||
|
name: String?,
|
||||||
|
website: String?,
|
||||||
|
): Int?
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun upsertAuthor(authorEntity: AuthorEntity): Int {
|
||||||
|
val id = insertAuthor(authorEntity)
|
||||||
|
return if (id == -1L) {
|
||||||
|
authorId(
|
||||||
|
email = authorEntity.email,
|
||||||
|
name = authorEntity.name,
|
||||||
|
website = authorEntity.website,
|
||||||
|
)!!.toInt()
|
||||||
|
} else {
|
||||||
|
id.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
suspend fun insertScreenshots(screenshotEntity: List<ScreenshotEntity>)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertLink(linksEntity: LinksEntity)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertGraphics(graphicEntity: List<GraphicEntity>)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
suspend fun insertDonate(donateEntity: List<DonateEntity>)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertVersions(versions: List<VersionEntity>)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
suspend fun insertCategoryAppRelation(crossRef: List<CategoryAppRelation>)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
suspend fun insertAntiFeatureAppRelation(crossRef: List<AntiFeatureAppRelation>)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package com.looker.droidify.data.local.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.looker.droidify.data.local.model.RepoEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface RepoDao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM repository")
|
||||||
|
fun stream(): Flow<List<RepoEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM repository WHERE id = :repoId")
|
||||||
|
fun repo(repoId: Int): Flow<RepoEntity>
|
||||||
|
|
||||||
|
@Query("DELETE FROM repository WHERE id = :id")
|
||||||
|
suspend fun delete(id: Int)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
package com.looker.droidify.data.local.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import com.looker.droidify.sync.v2.model.AntiFeatureReason
|
||||||
|
import com.looker.droidify.sync.v2.model.AntiFeatureV2
|
||||||
|
import com.looker.droidify.sync.v2.model.Tag
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "anti_feature",
|
||||||
|
primaryKeys = ["tag", "locale"],
|
||||||
|
)
|
||||||
|
data class AntiFeatureEntity(
|
||||||
|
val icon: String?,
|
||||||
|
val name: String,
|
||||||
|
val description: String?,
|
||||||
|
val locale: String,
|
||||||
|
val tag: Tag,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "anti_feature_repo_relation",
|
||||||
|
primaryKeys = ["id", "tag"],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = RepoEntity::class,
|
||||||
|
childColumns = ["id"],
|
||||||
|
parentColumns = ["id"],
|
||||||
|
onDelete = ForeignKey.CASCADE,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
data class AntiFeatureRepoRelation(
|
||||||
|
@ColumnInfo("id")
|
||||||
|
val repoId: Int,
|
||||||
|
val tag: Tag,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "anti_features_app_relation",
|
||||||
|
primaryKeys = ["tag", "appId", "versionCode"],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = AppEntity::class,
|
||||||
|
childColumns = ["appId"],
|
||||||
|
parentColumns = ["id"],
|
||||||
|
onDelete = ForeignKey.CASCADE,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
data class AntiFeatureAppRelation(
|
||||||
|
val tag: Tag,
|
||||||
|
val reason: AntiFeatureReason,
|
||||||
|
val appId: Int,
|
||||||
|
val versionCode: Long,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun AntiFeatureV2.antiFeatureEntity(
|
||||||
|
tag: Tag,
|
||||||
|
): List<AntiFeatureEntity> {
|
||||||
|
return name.map { (locale, localizedName) ->
|
||||||
|
AntiFeatureEntity(
|
||||||
|
icon = icon[locale]?.name,
|
||||||
|
name = localizedName,
|
||||||
|
description = description[locale],
|
||||||
|
tag = tag,
|
||||||
|
locale = locale,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
package com.looker.droidify.data.local.model
|
||||||
|
|
||||||
|
import androidx.room.Embedded
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.ForeignKey.Companion.CASCADE
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.Junction
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import androidx.room.Relation
|
||||||
|
import com.looker.droidify.sync.v2.model.LocalizedIcon
|
||||||
|
import com.looker.droidify.sync.v2.model.LocalizedString
|
||||||
|
import com.looker.droidify.sync.v2.model.MetadataV2
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "app",
|
||||||
|
indices = [
|
||||||
|
Index("authorId"),
|
||||||
|
Index("repoId"),
|
||||||
|
Index("packageName"),
|
||||||
|
Index("packageName", "repoId", unique = true),
|
||||||
|
],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = RepoEntity::class,
|
||||||
|
childColumns = ["repoId"],
|
||||||
|
parentColumns = ["id"],
|
||||||
|
onDelete = CASCADE,
|
||||||
|
),
|
||||||
|
ForeignKey(
|
||||||
|
entity = AuthorEntity::class,
|
||||||
|
childColumns = ["authorId"],
|
||||||
|
parentColumns = ["id"],
|
||||||
|
onDelete = CASCADE,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
data class AppEntity(
|
||||||
|
val added: Long,
|
||||||
|
val lastUpdated: Long,
|
||||||
|
val license: String?,
|
||||||
|
val name: LocalizedString,
|
||||||
|
val icon: LocalizedIcon?,
|
||||||
|
val preferredSigner: String?,
|
||||||
|
val summary: LocalizedString?,
|
||||||
|
val description: LocalizedString?,
|
||||||
|
val packageName: String,
|
||||||
|
val authorId: Int,
|
||||||
|
val repoId: Int,
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
val id: Int = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AppEntityRelations(
|
||||||
|
@Embedded val app: AppEntity,
|
||||||
|
@Relation(
|
||||||
|
parentColumn = "authorId",
|
||||||
|
entityColumn = "id",
|
||||||
|
)
|
||||||
|
val author: AuthorEntity,
|
||||||
|
@Relation(
|
||||||
|
parentColumn = "id",
|
||||||
|
entityColumn = "appId",
|
||||||
|
)
|
||||||
|
val links: LinksEntity?,
|
||||||
|
@Relation(
|
||||||
|
parentColumn = "id",
|
||||||
|
entityColumn = "defaultName",
|
||||||
|
associateBy = Junction(CategoryAppRelation::class),
|
||||||
|
)
|
||||||
|
val categories: List<CategoryEntity>,
|
||||||
|
@Relation(
|
||||||
|
parentColumn = "id",
|
||||||
|
entityColumn = "appId",
|
||||||
|
)
|
||||||
|
val graphics: List<GraphicEntity>?,
|
||||||
|
@Relation(
|
||||||
|
parentColumn = "id",
|
||||||
|
entityColumn = "appId",
|
||||||
|
)
|
||||||
|
val screenshots: List<ScreenshotEntity>?,
|
||||||
|
@Relation(
|
||||||
|
parentColumn = "id",
|
||||||
|
entityColumn = "appId",
|
||||||
|
)
|
||||||
|
val versions: List<VersionEntity>?,
|
||||||
|
@Relation(
|
||||||
|
parentColumn = "packageName",
|
||||||
|
entityColumn = "packageName",
|
||||||
|
)
|
||||||
|
val installed: InstalledEntity?,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun MetadataV2.appEntity(
|
||||||
|
packageName: String,
|
||||||
|
repoId: Int,
|
||||||
|
authorId: Int,
|
||||||
|
) = AppEntity(
|
||||||
|
added = added,
|
||||||
|
lastUpdated = lastUpdated,
|
||||||
|
license = license,
|
||||||
|
name = name,
|
||||||
|
icon = icon,
|
||||||
|
preferredSigner = preferredSigner,
|
||||||
|
summary = summary,
|
||||||
|
description = description,
|
||||||
|
packageName = packageName,
|
||||||
|
authorId = authorId,
|
||||||
|
repoId = repoId,
|
||||||
|
)
|
@ -0,0 +1,26 @@
|
|||||||
|
package com.looker.droidify.data.local.model
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.ForeignKey.Companion.CASCADE
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import com.looker.droidify.data.encryption.Encrypted
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "authentication",
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = RepoEntity::class,
|
||||||
|
childColumns = ["repoId"],
|
||||||
|
parentColumns = ["id"],
|
||||||
|
onDelete = CASCADE,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
data class AuthenticationEntity(
|
||||||
|
val password: Encrypted,
|
||||||
|
val username: String,
|
||||||
|
val initializationVector: String,
|
||||||
|
@PrimaryKey
|
||||||
|
val repoId: Int,
|
||||||
|
)
|
@ -0,0 +1,33 @@
|
|||||||
|
package com.looker.droidify.data.local.model
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import com.looker.droidify.domain.model.Author
|
||||||
|
import com.looker.droidify.sync.v2.model.MetadataV2
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "author",
|
||||||
|
indices = [Index("email", "name", "website", unique = true)],
|
||||||
|
)
|
||||||
|
data class AuthorEntity(
|
||||||
|
val email: String?,
|
||||||
|
val name: String?,
|
||||||
|
val website: String?,
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
val id: Int = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun MetadataV2.authorEntity() = AuthorEntity(
|
||||||
|
email = authorEmail,
|
||||||
|
name = authorName,
|
||||||
|
website = authorWebSite,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun AuthorEntity.toAuthor() = Author(
|
||||||
|
email = email,
|
||||||
|
name = name,
|
||||||
|
phone = null,
|
||||||
|
web = website,
|
||||||
|
id = id,
|
||||||
|
)
|
@ -0,0 +1,71 @@
|
|||||||
|
package com.looker.droidify.data.local.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.Index
|
||||||
|
import com.looker.droidify.sync.v2.model.CategoryV2
|
||||||
|
import com.looker.droidify.sync.v2.model.DefaultName
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "category",
|
||||||
|
primaryKeys = ["defaultName", "locale"],
|
||||||
|
indices = [Index("defaultName")]
|
||||||
|
)
|
||||||
|
data class CategoryEntity(
|
||||||
|
val icon: String?,
|
||||||
|
val name: String,
|
||||||
|
val description: String?,
|
||||||
|
val locale: String,
|
||||||
|
val defaultName: DefaultName,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "category_repo_relation",
|
||||||
|
primaryKeys = ["id", "defaultName"],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = RepoEntity::class,
|
||||||
|
childColumns = ["id"],
|
||||||
|
parentColumns = ["id"],
|
||||||
|
onDelete = ForeignKey.CASCADE,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
data class CategoryRepoRelation(
|
||||||
|
@ColumnInfo("id")
|
||||||
|
val repoId: Int,
|
||||||
|
val defaultName: DefaultName,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "category_app_relation",
|
||||||
|
primaryKeys = ["id", "defaultName"],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = AppEntity::class,
|
||||||
|
childColumns = ["id"],
|
||||||
|
parentColumns = ["id"],
|
||||||
|
onDelete = ForeignKey.CASCADE,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
data class CategoryAppRelation(
|
||||||
|
@ColumnInfo("id")
|
||||||
|
val appId: Int,
|
||||||
|
val defaultName: DefaultName,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun CategoryV2.categoryEntity(
|
||||||
|
defaultName: DefaultName,
|
||||||
|
): List<CategoryEntity> {
|
||||||
|
return name.map { (locale, localizedName) ->
|
||||||
|
CategoryEntity(
|
||||||
|
icon = icon[locale]?.name,
|
||||||
|
name = localizedName,
|
||||||
|
description = description[locale],
|
||||||
|
defaultName = defaultName,
|
||||||
|
locale = locale,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
package com.looker.droidify.data.local.model
|
||||||
|
|
||||||
|
import androidx.annotation.IntDef
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.ForeignKey.Companion.CASCADE
|
||||||
|
import androidx.room.Index
|
||||||
|
import com.looker.droidify.domain.model.Donation
|
||||||
|
import com.looker.droidify.sync.v2.model.MetadataV2
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "donate",
|
||||||
|
primaryKeys = ["type", "appId"],
|
||||||
|
indices = [Index("appId")],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = AppEntity::class,
|
||||||
|
childColumns = ["appId"],
|
||||||
|
parentColumns = ["id"],
|
||||||
|
onDelete = CASCADE,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
data class DonateEntity(
|
||||||
|
@DonationType
|
||||||
|
val type: Int,
|
||||||
|
val value: String,
|
||||||
|
val appId: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun MetadataV2.donateEntity(appId: Int): List<DonateEntity>? {
|
||||||
|
return buildList {
|
||||||
|
if (bitcoin != null) {
|
||||||
|
add(DonateEntity(BITCOIN_ADD, bitcoin, appId))
|
||||||
|
}
|
||||||
|
if (litecoin != null) {
|
||||||
|
add(DonateEntity(LITECOIN_ADD, litecoin, appId))
|
||||||
|
}
|
||||||
|
if (liberapay != null) {
|
||||||
|
add(DonateEntity(LIBERAPAY_ID, liberapay, appId))
|
||||||
|
}
|
||||||
|
if (openCollective != null) {
|
||||||
|
add(DonateEntity(OPEN_COLLECTIVE_ID, openCollective, appId))
|
||||||
|
}
|
||||||
|
if (flattrID != null) {
|
||||||
|
add(DonateEntity(FLATTR_ID, flattrID, appId))
|
||||||
|
}
|
||||||
|
if (!donate.isNullOrEmpty()) {
|
||||||
|
add(DonateEntity(REGULAR, donate.joinToString(STRING_LIST_SEPARATOR), appId))
|
||||||
|
}
|
||||||
|
}.ifEmpty { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<DonateEntity>.toDonation(): Donation {
|
||||||
|
var bitcoinAddress: String? = null
|
||||||
|
var litecoinAddress: String? = null
|
||||||
|
var liberapayId: String? = null
|
||||||
|
var openCollectiveId: String? = null
|
||||||
|
var flattrId: String? = null
|
||||||
|
var regular: List<String>? = null
|
||||||
|
for (entity in this) {
|
||||||
|
when (entity.type) {
|
||||||
|
BITCOIN_ADD -> bitcoinAddress = entity.value
|
||||||
|
FLATTR_ID -> flattrId = entity.value
|
||||||
|
LIBERAPAY_ID -> liberapayId = entity.value
|
||||||
|
LITECOIN_ADD -> litecoinAddress = entity.value
|
||||||
|
OPEN_COLLECTIVE_ID -> openCollectiveId = entity.value
|
||||||
|
REGULAR -> regular = entity.value.split(STRING_LIST_SEPARATOR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Donation(
|
||||||
|
bitcoinAddress = bitcoinAddress,
|
||||||
|
litecoinAddress = litecoinAddress,
|
||||||
|
liberapayId = liberapayId,
|
||||||
|
openCollectiveId = openCollectiveId,
|
||||||
|
flattrId = flattrId,
|
||||||
|
regularUrl = regular,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val STRING_LIST_SEPARATOR = "&^%#@!"
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
@IntDef(
|
||||||
|
BITCOIN_ADD,
|
||||||
|
LITECOIN_ADD,
|
||||||
|
LIBERAPAY_ID,
|
||||||
|
OPEN_COLLECTIVE_ID,
|
||||||
|
FLATTR_ID,
|
||||||
|
REGULAR,
|
||||||
|
)
|
||||||
|
private annotation class DonationType
|
||||||
|
|
||||||
|
private const val BITCOIN_ADD = 0
|
||||||
|
private const val LITECOIN_ADD = 1
|
||||||
|
private const val LIBERAPAY_ID = 2
|
||||||
|
private const val OPEN_COLLECTIVE_ID = 3
|
||||||
|
private const val FLATTR_ID = 4
|
||||||
|
private const val REGULAR = 5
|
@ -0,0 +1,83 @@
|
|||||||
|
package com.looker.droidify.data.local.model
|
||||||
|
|
||||||
|
import androidx.annotation.IntDef
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.ForeignKey.Companion.CASCADE
|
||||||
|
import androidx.room.Index
|
||||||
|
import com.looker.droidify.domain.model.Graphics
|
||||||
|
import com.looker.droidify.sync.v2.model.MetadataV2
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "graphic",
|
||||||
|
primaryKeys = ["type", "locale", "appId"],
|
||||||
|
indices = [Index("appId", "locale")],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = AppEntity::class,
|
||||||
|
childColumns = ["appId"],
|
||||||
|
parentColumns = ["id"],
|
||||||
|
onDelete = CASCADE,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
data class GraphicEntity(
|
||||||
|
@GraphicType
|
||||||
|
val type: Int,
|
||||||
|
val url: String,
|
||||||
|
val locale: String,
|
||||||
|
val appId: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun MetadataV2.localizedGraphics(appId: Int): List<GraphicEntity>? {
|
||||||
|
return buildList {
|
||||||
|
promoGraphic?.forEach { (locale, value) ->
|
||||||
|
add(GraphicEntity(PROMO_GRAPHIC, value.name, locale, appId))
|
||||||
|
}
|
||||||
|
featureGraphic?.forEach { (locale, value) ->
|
||||||
|
add(GraphicEntity(FEATURE_GRAPHIC, value.name, locale, appId))
|
||||||
|
}
|
||||||
|
tvBanner?.forEach { (locale, value) ->
|
||||||
|
add(GraphicEntity(TV_BANNER, value.name, locale, appId))
|
||||||
|
}
|
||||||
|
video?.forEach { (locale, value) ->
|
||||||
|
add(GraphicEntity(VIDEO, value, locale, appId))
|
||||||
|
}
|
||||||
|
}.ifEmpty { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<GraphicEntity>.toGraphics(): Graphics {
|
||||||
|
var featureGraphic: String? = null
|
||||||
|
var promoGraphic: String? = null
|
||||||
|
var tvBanner: String? = null
|
||||||
|
var video: String? = null
|
||||||
|
|
||||||
|
for (entity in this) {
|
||||||
|
when (entity.type) {
|
||||||
|
FEATURE_GRAPHIC -> featureGraphic = entity.url
|
||||||
|
PROMO_GRAPHIC -> promoGraphic = entity.url
|
||||||
|
TV_BANNER -> tvBanner = entity.url
|
||||||
|
VIDEO -> video = entity.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Graphics(
|
||||||
|
featureGraphic = featureGraphic,
|
||||||
|
promoGraphic = promoGraphic,
|
||||||
|
tvBanner = tvBanner,
|
||||||
|
video = video,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
@IntDef(
|
||||||
|
VIDEO,
|
||||||
|
TV_BANNER,
|
||||||
|
PROMO_GRAPHIC,
|
||||||
|
FEATURE_GRAPHIC,
|
||||||
|
)
|
||||||
|
annotation class GraphicType
|
||||||
|
|
||||||
|
private const val VIDEO = 0
|
||||||
|
private const val TV_BANNER = 1
|
||||||
|
private const val PROMO_GRAPHIC = 2
|
||||||
|
private const val FEATURE_GRAPHIC = 3
|
@ -0,0 +1,13 @@
|
|||||||
|
package com.looker.droidify.data.local.model
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity("installed")
|
||||||
|
data class InstalledEntity(
|
||||||
|
val versionCode: String,
|
||||||
|
val versionName: String,
|
||||||
|
val signature: String,
|
||||||
|
@PrimaryKey
|
||||||
|
val packageName: String,
|
||||||
|
)
|
@ -0,0 +1,56 @@
|
|||||||
|
package com.looker.droidify.data.local.model
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.ForeignKey.Companion.CASCADE
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import com.looker.droidify.domain.model.Links
|
||||||
|
import com.looker.droidify.sync.v2.model.MetadataV2
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "link",
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = AppEntity::class,
|
||||||
|
childColumns = ["appId"],
|
||||||
|
parentColumns = ["id"],
|
||||||
|
onDelete = CASCADE,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
data class LinksEntity(
|
||||||
|
val changelog: String?,
|
||||||
|
val issueTracker: String?,
|
||||||
|
val translation: String?,
|
||||||
|
val sourceCode: String?,
|
||||||
|
val webSite: String?,
|
||||||
|
@PrimaryKey
|
||||||
|
val appId: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun MetadataV2.isLinkNull(): Boolean {
|
||||||
|
return changelog == null &&
|
||||||
|
issueTracker == null &&
|
||||||
|
translation == null &&
|
||||||
|
sourceCode == null &&
|
||||||
|
webSite == null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MetadataV2.linkEntity(appId: Int) = if (!isLinkNull()) {
|
||||||
|
LinksEntity(
|
||||||
|
appId = appId,
|
||||||
|
changelog = changelog,
|
||||||
|
issueTracker = issueTracker,
|
||||||
|
translation = translation,
|
||||||
|
sourceCode = sourceCode,
|
||||||
|
webSite = webSite,
|
||||||
|
)
|
||||||
|
} else null
|
||||||
|
|
||||||
|
fun LinksEntity.toLinks() = Links(
|
||||||
|
changelog = changelog,
|
||||||
|
issueTracker = issueTracker,
|
||||||
|
translation = translation,
|
||||||
|
sourceCode = sourceCode,
|
||||||
|
webSite = webSite,
|
||||||
|
)
|
@ -0,0 +1,36 @@
|
|||||||
|
package com.looker.droidify.data.local.model
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.ForeignKey.Companion.CASCADE
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import com.looker.droidify.sync.v2.model.MirrorV2
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "mirror",
|
||||||
|
indices = [Index("repoId")],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = RepoEntity::class,
|
||||||
|
childColumns = ["repoId"],
|
||||||
|
parentColumns = ["id"],
|
||||||
|
onDelete = CASCADE,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class MirrorEntity(
|
||||||
|
val url: String,
|
||||||
|
val countryCode: String?,
|
||||||
|
val isPrimary: Boolean,
|
||||||
|
val repoId: Int,
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
val id: Int = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun MirrorV2.mirrorEntity(repoId: Int) = MirrorEntity(
|
||||||
|
url = url,
|
||||||
|
countryCode = countryCode,
|
||||||
|
isPrimary = isPrimary == true,
|
||||||
|
repoId = repoId,
|
||||||
|
)
|
@ -0,0 +1,54 @@
|
|||||||
|
package com.looker.droidify.data.local.model
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import com.looker.droidify.domain.model.Authentication
|
||||||
|
import com.looker.droidify.domain.model.Fingerprint
|
||||||
|
import com.looker.droidify.domain.model.Repo
|
||||||
|
import com.looker.droidify.domain.model.VersionInfo
|
||||||
|
import com.looker.droidify.sync.v2.model.LocalizedIcon
|
||||||
|
import com.looker.droidify.sync.v2.model.LocalizedString
|
||||||
|
import com.looker.droidify.sync.v2.model.RepoV2
|
||||||
|
import com.looker.droidify.sync.v2.model.localizedValue
|
||||||
|
|
||||||
|
@Entity(tableName = "repository")
|
||||||
|
data class RepoEntity(
|
||||||
|
val icon: LocalizedIcon?,
|
||||||
|
val address: String,
|
||||||
|
val name: LocalizedString,
|
||||||
|
val description: LocalizedString,
|
||||||
|
val fingerprint: Fingerprint,
|
||||||
|
val timestamp: Long,
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
val id: Int = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun RepoV2.repoEntity(
|
||||||
|
id: Int,
|
||||||
|
fingerprint: Fingerprint,
|
||||||
|
) = RepoEntity(
|
||||||
|
id = id,
|
||||||
|
icon = icon,
|
||||||
|
address = address,
|
||||||
|
name = name,
|
||||||
|
description = description,
|
||||||
|
timestamp = timestamp,
|
||||||
|
fingerprint = fingerprint,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun RepoEntity.toRepo(
|
||||||
|
locale: String,
|
||||||
|
mirrors: List<String>,
|
||||||
|
enabled: Boolean,
|
||||||
|
authentication: Authentication? = null,
|
||||||
|
) = Repo(
|
||||||
|
name = name.localizedValue(locale) ?: "Unknown",
|
||||||
|
description = description.localizedValue(locale) ?: "Unknown",
|
||||||
|
fingerprint = fingerprint,
|
||||||
|
authentication = authentication,
|
||||||
|
enabled = enabled,
|
||||||
|
address = address,
|
||||||
|
versionInfo = VersionInfo(timestamp = timestamp, etag = null),
|
||||||
|
mirrors = mirrors,
|
||||||
|
id = id,
|
||||||
|
)
|
@ -0,0 +1,99 @@
|
|||||||
|
package com.looker.droidify.data.local.model
|
||||||
|
|
||||||
|
import androidx.annotation.IntDef
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.ForeignKey.Companion.CASCADE
|
||||||
|
import androidx.room.Index
|
||||||
|
import com.looker.droidify.domain.model.Screenshots
|
||||||
|
import com.looker.droidify.sync.v2.model.LocalizedFiles
|
||||||
|
import com.looker.droidify.sync.v2.model.ScreenshotsV2
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "screenshot",
|
||||||
|
primaryKeys = ["path", "type", "locale", "appId"],
|
||||||
|
indices = [Index("appId", "locale")],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = AppEntity::class,
|
||||||
|
childColumns = ["appId"],
|
||||||
|
parentColumns = ["id"],
|
||||||
|
onDelete = CASCADE,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
data class ScreenshotEntity(
|
||||||
|
val path: String,
|
||||||
|
@ScreenshotType
|
||||||
|
val type: Int,
|
||||||
|
val locale: String,
|
||||||
|
val appId: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ScreenshotsV2.localizedScreenshots(appId: Int): List<ScreenshotEntity> {
|
||||||
|
if (isNull) return emptyList()
|
||||||
|
val screenshots = mutableListOf<ScreenshotEntity>()
|
||||||
|
|
||||||
|
val screenshotIterator: (Int, LocalizedFiles?) -> Unit = { type, localizedFiles ->
|
||||||
|
localizedFiles?.forEach { (locale, files) ->
|
||||||
|
for ((path, _, _) in files) {
|
||||||
|
screenshots.add(
|
||||||
|
ScreenshotEntity(
|
||||||
|
locale = locale,
|
||||||
|
appId = appId,
|
||||||
|
type = type,
|
||||||
|
path = path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
screenshotIterator(PHONE, phone)
|
||||||
|
screenshotIterator(SEVEN_INCH, sevenInch)
|
||||||
|
screenshotIterator(TEN_INCH, tenInch)
|
||||||
|
screenshotIterator(WEAR, wear)
|
||||||
|
screenshotIterator(TV, tv)
|
||||||
|
return screenshots
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<ScreenshotEntity>.toScreenshots(): Screenshots {
|
||||||
|
val phone = mutableListOf<String>()
|
||||||
|
val sevenInch = mutableListOf<String>()
|
||||||
|
val tenInch = mutableListOf<String>()
|
||||||
|
val wear = mutableListOf<String>()
|
||||||
|
val tv = mutableListOf<String>()
|
||||||
|
for (index in this.indices) {
|
||||||
|
val entity = get(index)
|
||||||
|
when (entity.type) {
|
||||||
|
PHONE -> phone.add(entity.path)
|
||||||
|
SEVEN_INCH -> sevenInch.add(entity.path)
|
||||||
|
TEN_INCH -> tenInch.add(entity.path)
|
||||||
|
TV -> tv.add(entity.path)
|
||||||
|
WEAR -> wear.add(entity.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Screenshots(
|
||||||
|
phone = phone,
|
||||||
|
sevenInch = sevenInch,
|
||||||
|
tenInch = tenInch,
|
||||||
|
wear = wear,
|
||||||
|
tv = tv,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
@IntDef(
|
||||||
|
PHONE,
|
||||||
|
SEVEN_INCH,
|
||||||
|
TEN_INCH,
|
||||||
|
WEAR,
|
||||||
|
TV,
|
||||||
|
)
|
||||||
|
private annotation class ScreenshotType
|
||||||
|
|
||||||
|
private const val PHONE = 0
|
||||||
|
private const val SEVEN_INCH = 1
|
||||||
|
private const val TEN_INCH = 2
|
||||||
|
private const val WEAR = 3
|
||||||
|
private const val TV = 4
|
@ -0,0 +1,74 @@
|
|||||||
|
package com.looker.droidify.data.local.model
|
||||||
|
|
||||||
|
import androidx.room.Embedded
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.ForeignKey.Companion.CASCADE
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import com.looker.droidify.sync.v2.model.ApkFileV2
|
||||||
|
import com.looker.droidify.sync.v2.model.FileV2
|
||||||
|
import com.looker.droidify.sync.v2.model.LocalizedString
|
||||||
|
import com.looker.droidify.sync.v2.model.PackageV2
|
||||||
|
import com.looker.droidify.sync.v2.model.PermissionV2
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "version",
|
||||||
|
indices = [Index("appId")],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = AppEntity::class,
|
||||||
|
childColumns = ["appId"],
|
||||||
|
parentColumns = ["id"],
|
||||||
|
onDelete = CASCADE,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
data class VersionEntity(
|
||||||
|
val added: Long,
|
||||||
|
val whatsNew: LocalizedString,
|
||||||
|
val versionName: String,
|
||||||
|
val versionCode: Long,
|
||||||
|
val maxSdkVersion: Int?,
|
||||||
|
val minSdkVersion: Int,
|
||||||
|
val targetSdkVersion: Int,
|
||||||
|
@Embedded("apk_")
|
||||||
|
val apk: ApkFileV2,
|
||||||
|
@Embedded("src_")
|
||||||
|
val src: FileV2?,
|
||||||
|
val features: List<String>,
|
||||||
|
val nativeCode: List<String>,
|
||||||
|
val permissions: List<PermissionV2>,
|
||||||
|
val permissionsSdk23: List<PermissionV2>,
|
||||||
|
val appId: Int,
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
val id: Int = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun PackageV2.versionEntities(appId: Int): Map<VersionEntity, List<AntiFeatureAppRelation>> {
|
||||||
|
return versions.map { (_, version) ->
|
||||||
|
VersionEntity(
|
||||||
|
added = version.added,
|
||||||
|
whatsNew = version.whatsNew,
|
||||||
|
versionName = version.manifest.versionName,
|
||||||
|
versionCode = version.manifest.versionCode,
|
||||||
|
maxSdkVersion = version.manifest.maxSdkVersion,
|
||||||
|
minSdkVersion = version.manifest.usesSdk?.minSdkVersion ?: -1,
|
||||||
|
targetSdkVersion = version.manifest.usesSdk?.targetSdkVersion ?: -1,
|
||||||
|
apk = version.file,
|
||||||
|
src = version.src,
|
||||||
|
features = version.manifest.features.map { it.name },
|
||||||
|
nativeCode = version.manifest.nativecode,
|
||||||
|
permissions = version.manifest.usesPermission,
|
||||||
|
permissionsSdk23 = version.manifest.usesPermissionSdk23,
|
||||||
|
appId = appId,
|
||||||
|
) to version.antiFeatures.map { (tag, reason) ->
|
||||||
|
AntiFeatureAppRelation(
|
||||||
|
tag = tag,
|
||||||
|
reason = reason,
|
||||||
|
appId = appId,
|
||||||
|
versionCode = version.manifest.versionCode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.toMap()
|
||||||
|
}
|
@ -607,6 +607,19 @@ object Database {
|
|||||||
.map { getUpdates(skipSignatureCheck) }
|
.map { getUpdates(skipSignatureCheck) }
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
|
fun getAll(): List<Product> {
|
||||||
|
return db.query(
|
||||||
|
Schema.Product.name,
|
||||||
|
columns = arrayOf(
|
||||||
|
Schema.Product.ROW_REPOSITORY_ID,
|
||||||
|
Schema.Product.ROW_DESCRIPTION,
|
||||||
|
Schema.Product.ROW_DATA,
|
||||||
|
),
|
||||||
|
selection = null,
|
||||||
|
signal = null,
|
||||||
|
).use { it.asSequence().map(::transform).toList() }
|
||||||
|
}
|
||||||
|
|
||||||
fun get(packageName: String, signal: CancellationSignal?): List<Product> {
|
fun get(packageName: String, signal: CancellationSignal?): List<Product> {
|
||||||
return db.query(
|
return db.query(
|
||||||
Schema.Product.name,
|
Schema.Product.name,
|
||||||
@ -719,7 +732,7 @@ object Database {
|
|||||||
when (order) {
|
when (order) {
|
||||||
SortOrder.UPDATED -> builder += "product.${Schema.Product.ROW_UPDATED} DESC,"
|
SortOrder.UPDATED -> builder += "product.${Schema.Product.ROW_UPDATED} DESC,"
|
||||||
SortOrder.ADDED -> builder += "product.${Schema.Product.ROW_ADDED} DESC,"
|
SortOrder.ADDED -> builder += "product.${Schema.Product.ROW_ADDED} DESC,"
|
||||||
SortOrder.NAME -> Unit
|
else -> Unit
|
||||||
}::class
|
}::class
|
||||||
builder += "product.${Schema.Product.ROW_NAME} COLLATE LOCALIZED ASC"
|
builder += "product.${Schema.Product.ROW_NAME} COLLATE LOCALIZED ASC"
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ fun Context?.sortOrderName(sortOrder: SortOrder) = this?.let {
|
|||||||
SortOrder.UPDATED -> getString(stringRes.recently_updated)
|
SortOrder.UPDATED -> getString(stringRes.recently_updated)
|
||||||
SortOrder.ADDED -> getString(stringRes.whats_new)
|
SortOrder.ADDED -> getString(stringRes.whats_new)
|
||||||
SortOrder.NAME -> getString(stringRes.name)
|
SortOrder.NAME -> getString(stringRes.name)
|
||||||
// SortOrder.SIZE -> getString(stringRes.size)
|
SortOrder.SIZE -> getString(stringRes.size)
|
||||||
}
|
}
|
||||||
} ?: ""
|
} ?: ""
|
||||||
|
|
||||||
|
@ -4,5 +4,8 @@ package com.looker.droidify.datastore.model
|
|||||||
enum class SortOrder {
|
enum class SortOrder {
|
||||||
UPDATED,
|
UPDATED,
|
||||||
ADDED,
|
ADDED,
|
||||||
NAME
|
NAME,
|
||||||
|
SIZE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun supportedSortOrders(): List<SortOrder> = listOf(SortOrder.UPDATED, SortOrder.ADDED, SortOrder.NAME)
|
||||||
|
51
app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt
Normal file
51
app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package com.looker.droidify.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.looker.droidify.data.local.DroidifyDatabase
|
||||||
|
import com.looker.droidify.data.local.dao.AppDao
|
||||||
|
import com.looker.droidify.data.local.dao.AuthDao
|
||||||
|
import com.looker.droidify.data.local.dao.IndexDao
|
||||||
|
import com.looker.droidify.data.local.dao.RepoDao
|
||||||
|
import com.looker.droidify.data.local.droidifyDatabase
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object DatabaseModule {
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideDatabase(
|
||||||
|
@ApplicationContext
|
||||||
|
context: Context,
|
||||||
|
): DroidifyDatabase = droidifyDatabase(context)
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideAppDao(
|
||||||
|
db: DroidifyDatabase,
|
||||||
|
): AppDao = db.appDao()
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideRepoDao(
|
||||||
|
db: DroidifyDatabase,
|
||||||
|
): RepoDao = db.repoDao()
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideAuthDao(
|
||||||
|
db: DroidifyDatabase,
|
||||||
|
): AuthDao = db.authDao()
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideIndexDao(
|
||||||
|
db: DroidifyDatabase,
|
||||||
|
): IndexDao = db.indexDao()
|
||||||
|
}
|
23
app/src/main/kotlin/com/looker/droidify/di/SyncableModule.kt
Normal file
23
app/src/main/kotlin/com/looker/droidify/di/SyncableModule.kt
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package com.looker.droidify.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.looker.droidify.sync.LocalSyncable
|
||||||
|
import com.looker.droidify.sync.Syncable
|
||||||
|
import com.looker.droidify.sync.v2.model.IndexV2
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object SyncableModule {
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideSyncable(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
): Syncable<IndexV2> = LocalSyncable(context)
|
||||||
|
}
|
@ -6,7 +6,7 @@ data class App(
|
|||||||
val categories: List<String>,
|
val categories: List<String>,
|
||||||
val links: Links,
|
val links: Links,
|
||||||
val metadata: Metadata,
|
val metadata: Metadata,
|
||||||
val author: Author,
|
val author: Author?,
|
||||||
val screenshots: Screenshots,
|
val screenshots: Screenshots,
|
||||||
val graphics: Graphics,
|
val graphics: Graphics,
|
||||||
val donation: Donation,
|
val donation: Donation,
|
||||||
@ -15,34 +15,35 @@ data class App(
|
|||||||
)
|
)
|
||||||
|
|
||||||
data class Author(
|
data class Author(
|
||||||
val id: Long,
|
val id: Int,
|
||||||
val name: String,
|
val name: String?,
|
||||||
val email: String,
|
val email: String?,
|
||||||
val web: String
|
val phone: String?,
|
||||||
|
val web: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Donation(
|
data class Donation(
|
||||||
val regularUrl: String? = null,
|
val regularUrl: List<String>? = null,
|
||||||
val bitcoinAddress: String? = null,
|
val bitcoinAddress: String? = null,
|
||||||
val flattrId: String? = null,
|
val flattrId: String? = null,
|
||||||
val liteCoinAddress: String? = null,
|
val litecoinAddress: String? = null,
|
||||||
val openCollectiveId: String? = null,
|
val openCollectiveId: String? = null,
|
||||||
val librePayId: String? = null,
|
val liberapayId: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Graphics(
|
data class Graphics(
|
||||||
val featureGraphic: String = "",
|
val featureGraphic: String? = null,
|
||||||
val promoGraphic: String = "",
|
val promoGraphic: String? = null,
|
||||||
val tvBanner: String = "",
|
val tvBanner: String? = null,
|
||||||
val video: String = ""
|
val video: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Links(
|
data class Links(
|
||||||
val changelog: String = "",
|
val changelog: String? = null,
|
||||||
val issueTracker: String = "",
|
val issueTracker: String? = null,
|
||||||
val sourceCode: String = "",
|
val sourceCode: String? = null,
|
||||||
val translation: String = "",
|
val translation: String? = null,
|
||||||
val webSite: String = ""
|
val webSite: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Metadata(
|
data class Metadata(
|
||||||
|
@ -1,25 +1,23 @@
|
|||||||
package com.looker.droidify.domain.model
|
package com.looker.droidify.domain.model
|
||||||
|
|
||||||
data class Repo(
|
data class Repo(
|
||||||
val id: Long,
|
val id: Int,
|
||||||
val enabled: Boolean,
|
val enabled: Boolean,
|
||||||
val address: String,
|
val address: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
val fingerprint: Fingerprint?,
|
val fingerprint: Fingerprint?,
|
||||||
val authentication: Authentication,
|
val authentication: Authentication?,
|
||||||
val versionInfo: VersionInfo,
|
val versionInfo: VersionInfo,
|
||||||
val mirrors: List<String>,
|
val mirrors: List<String>,
|
||||||
val antiFeatures: List<AntiFeature>,
|
|
||||||
val categories: List<Category>
|
|
||||||
) {
|
) {
|
||||||
val shouldAuthenticate =
|
val shouldAuthenticate = authentication != null
|
||||||
authentication.username.isNotEmpty() && authentication.password.isNotEmpty()
|
|
||||||
|
|
||||||
fun update(fingerprint: Fingerprint, timestamp: Long? = null, etag: String? = null): Repo {
|
fun update(fingerprint: Fingerprint, timestamp: Long? = null, etag: String? = null): Repo {
|
||||||
return copy(
|
return copy(
|
||||||
fingerprint = fingerprint,
|
fingerprint = fingerprint,
|
||||||
versionInfo = timestamp?.let { VersionInfo(timestamp = it, etag = etag) } ?: versionInfo
|
versionInfo = timestamp?.let { VersionInfo(timestamp = it, etag = etag) }
|
||||||
|
?: versionInfo,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -28,22 +26,22 @@ data class AntiFeature(
|
|||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
val icon: String = "",
|
val icon: String = "",
|
||||||
val description: String = ""
|
val description: String = "",
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Category(
|
data class Category(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
val icon: String = "",
|
val icon: String = "",
|
||||||
val description: String = ""
|
val description: String = "",
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Authentication(
|
data class Authentication(
|
||||||
val username: String,
|
val username: String,
|
||||||
val password: String
|
val password: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class VersionInfo(
|
data class VersionInfo(
|
||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
val etag: String?
|
val etag: String?,
|
||||||
)
|
)
|
||||||
|
@ -82,7 +82,9 @@ internal class KtorDownloader(
|
|||||||
if (networkResponse !is NetworkResponse.Success) {
|
if (networkResponse !is NetworkResponse.Success) {
|
||||||
return@execute networkResponse
|
return@execute networkResponse
|
||||||
}
|
}
|
||||||
response.bodyAsChannel().copyTo(target.outputStream())
|
target.outputStream().use { output ->
|
||||||
|
response.bodyAsChannel().copyTo(output)
|
||||||
|
}
|
||||||
validator?.validate(target)
|
validator?.validate(target)
|
||||||
networkResponse
|
networkResponse
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
package com.looker.droidify.sync
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.looker.droidify.domain.model.Fingerprint
|
||||||
|
import com.looker.droidify.domain.model.Repo
|
||||||
|
import com.looker.droidify.sync.common.JsonParser
|
||||||
|
import com.looker.droidify.sync.v2.V2Parser
|
||||||
|
import com.looker.droidify.sync.v2.model.IndexV2
|
||||||
|
import com.looker.droidify.utility.common.cache.Cache
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
|
class LocalSyncable(
|
||||||
|
private val context: Context,
|
||||||
|
) : Syncable<IndexV2> {
|
||||||
|
override val parser: Parser<IndexV2>
|
||||||
|
get() = V2Parser(Dispatchers.IO, JsonParser)
|
||||||
|
|
||||||
|
override suspend fun sync(repo: Repo): Pair<Fingerprint, IndexV2?> {
|
||||||
|
val file = Cache.getTemporaryFile(context).apply {
|
||||||
|
outputStream().use {
|
||||||
|
it.write(context.assets.open("izzy_index_v2.json").readBytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parser.parse(file, repo)
|
||||||
|
}
|
||||||
|
}
|
@ -2,19 +2,14 @@ package com.looker.droidify.sync
|
|||||||
|
|
||||||
import com.looker.droidify.domain.model.Fingerprint
|
import com.looker.droidify.domain.model.Fingerprint
|
||||||
import com.looker.droidify.domain.model.Repo
|
import com.looker.droidify.domain.model.Repo
|
||||||
|
import com.looker.droidify.sync.v2.model.IndexV2
|
||||||
|
|
||||||
/**
|
|
||||||
* Expected Architecture: [https://excalidraw.com/#json=JqpGunWTJONjq-ecDNiPg,j9t0X4coeNvIG7B33GTq6A]
|
|
||||||
*
|
|
||||||
* Current Issue: When downloading entry.jar we need to re-call the synchronizer,
|
|
||||||
* which this arch doesn't allow.
|
|
||||||
*/
|
|
||||||
interface Syncable<T> {
|
interface Syncable<T> {
|
||||||
|
|
||||||
val parser: Parser<T>
|
val parser: Parser<T>
|
||||||
|
|
||||||
suspend fun sync(
|
suspend fun sync(
|
||||||
repo: Repo,
|
repo: Repo,
|
||||||
): Pair<Fingerprint, com.looker.droidify.sync.v2.model.IndexV2?>
|
): Pair<Fingerprint, IndexV2?>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import com.looker.droidify.sync.v1.model.RepoV1
|
|||||||
import com.looker.droidify.sync.v1.model.maxSdk
|
import com.looker.droidify.sync.v1.model.maxSdk
|
||||||
import com.looker.droidify.sync.v1.model.name
|
import com.looker.droidify.sync.v1.model.name
|
||||||
import com.looker.droidify.sync.v2.model.AntiFeatureV2
|
import com.looker.droidify.sync.v2.model.AntiFeatureV2
|
||||||
|
import com.looker.droidify.sync.v2.model.ApkFileV2
|
||||||
import com.looker.droidify.sync.v2.model.CategoryV2
|
import com.looker.droidify.sync.v2.model.CategoryV2
|
||||||
import com.looker.droidify.sync.v2.model.FeatureV2
|
import com.looker.droidify.sync.v2.model.FeatureV2
|
||||||
import com.looker.droidify.sync.v2.model.FileV2
|
import com.looker.droidify.sync.v2.model.FileV2
|
||||||
@ -94,7 +95,7 @@ private fun AppV1.toV2(preferredSigner: String?): MetadataV2 = MetadataV2(
|
|||||||
added = added ?: 0L,
|
added = added ?: 0L,
|
||||||
lastUpdated = lastUpdated ?: 0L,
|
lastUpdated = lastUpdated ?: 0L,
|
||||||
icon = localized?.localizedIcon(packageName, icon) { it.icon },
|
icon = localized?.localizedIcon(packageName, icon) { it.icon },
|
||||||
name = localized?.localizedString(name) { it.name },
|
name = localized?.localizedString(name) { it.name } ?: emptyMap(),
|
||||||
description = localized?.localizedString(description) { it.description },
|
description = localized?.localizedString(description) { it.description },
|
||||||
summary = localized?.localizedString(summary) { it.summary },
|
summary = localized?.localizedString(summary) { it.summary },
|
||||||
authorEmail = authorEmail,
|
authorEmail = authorEmail,
|
||||||
@ -157,7 +158,7 @@ private fun PackageV1.toVersionV2(
|
|||||||
packageAntiFeatures: List<String>,
|
packageAntiFeatures: List<String>,
|
||||||
): VersionV2 = VersionV2(
|
): VersionV2 = VersionV2(
|
||||||
added = added ?: 0L,
|
added = added ?: 0L,
|
||||||
file = FileV2(
|
file = ApkFileV2(
|
||||||
name = "/$apkName",
|
name = "/$apkName",
|
||||||
sha256 = hash,
|
sha256 = hash,
|
||||||
size = size,
|
size = size,
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package com.looker.droidify.sync.common
|
package com.looker.droidify.sync.common
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.looker.droidify.utility.common.cache.Cache
|
|
||||||
import com.looker.droidify.domain.model.Repo
|
import com.looker.droidify.domain.model.Repo
|
||||||
import com.looker.droidify.network.Downloader
|
import com.looker.droidify.network.Downloader
|
||||||
|
import com.looker.droidify.utility.common.cache.Cache
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -16,23 +16,25 @@ suspend fun Downloader.downloadIndex(
|
|||||||
url: String,
|
url: String,
|
||||||
diff: Boolean = false,
|
diff: Boolean = false,
|
||||||
): File = withContext(Dispatchers.IO) {
|
): File = withContext(Dispatchers.IO) {
|
||||||
val tempFile = Cache.getIndexFile(context, "repo_${repo.id}_$fileName")
|
val indexFile = Cache.getIndexFile(context, "repo_${repo.id}_$fileName")
|
||||||
downloadToFile(
|
downloadToFile(
|
||||||
url = url,
|
url = url,
|
||||||
target = tempFile,
|
target = indexFile,
|
||||||
headers = {
|
headers = {
|
||||||
if (repo.shouldAuthenticate) {
|
if (repo.shouldAuthenticate) {
|
||||||
authentication(
|
with(requireNotNull(repo.authentication)) {
|
||||||
repo.authentication.username,
|
authentication(
|
||||||
repo.authentication.password
|
username = username,
|
||||||
)
|
password = password,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (repo.versionInfo.timestamp > 0L && !diff) {
|
if (repo.versionInfo.timestamp > 0L && !diff) {
|
||||||
ifModifiedSince(Date(repo.versionInfo.timestamp))
|
ifModifiedSince(Date(repo.versionInfo.timestamp))
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
tempFile
|
indexFile
|
||||||
}
|
}
|
||||||
|
|
||||||
const val INDEX_V1_NAME = "index-v1.jar"
|
const val INDEX_V1_NAME = "index-v1.jar"
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
package com.looker.droidify.sync.v2
|
package com.looker.droidify.sync.v2
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.looker.droidify.utility.common.cache.Cache
|
|
||||||
import com.looker.droidify.domain.model.Fingerprint
|
import com.looker.droidify.domain.model.Fingerprint
|
||||||
import com.looker.droidify.domain.model.Repo
|
import com.looker.droidify.domain.model.Repo
|
||||||
|
import com.looker.droidify.network.Downloader
|
||||||
import com.looker.droidify.sync.Parser
|
import com.looker.droidify.sync.Parser
|
||||||
import com.looker.droidify.sync.Syncable
|
import com.looker.droidify.sync.Syncable
|
||||||
import com.looker.droidify.network.Downloader
|
|
||||||
import com.looker.droidify.sync.common.ENTRY_V2_NAME
|
import com.looker.droidify.sync.common.ENTRY_V2_NAME
|
||||||
import com.looker.droidify.sync.common.INDEX_V2_NAME
|
import com.looker.droidify.sync.common.INDEX_V2_NAME
|
||||||
import com.looker.droidify.sync.common.IndexJarValidator
|
import com.looker.droidify.sync.common.IndexJarValidator
|
||||||
@ -15,7 +14,9 @@ import com.looker.droidify.sync.common.downloadIndex
|
|||||||
import com.looker.droidify.sync.v2.model.Entry
|
import com.looker.droidify.sync.v2.model.Entry
|
||||||
import com.looker.droidify.sync.v2.model.IndexV2
|
import com.looker.droidify.sync.v2.model.IndexV2
|
||||||
import com.looker.droidify.sync.v2.model.IndexV2Diff
|
import com.looker.droidify.sync.v2.model.IndexV2Diff
|
||||||
|
import com.looker.droidify.utility.common.cache.Cache
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@ -51,7 +52,7 @@ class EntrySyncable(
|
|||||||
context = context,
|
context = context,
|
||||||
repo = repo,
|
repo = repo,
|
||||||
url = repo.address.removeSuffix("/") + "/$ENTRY_V2_NAME",
|
url = repo.address.removeSuffix("/") + "/$ENTRY_V2_NAME",
|
||||||
fileName = ENTRY_V2_NAME
|
fileName = ENTRY_V2_NAME,
|
||||||
)
|
)
|
||||||
val (fingerprint, entry) = parser.parse(jar, repo)
|
val (fingerprint, entry) = parser.parse(jar, repo)
|
||||||
jar.delete()
|
jar.delete()
|
||||||
@ -61,7 +62,6 @@ class EntrySyncable(
|
|||||||
val indexPath = repo.address.removeSuffix("/") + index.name
|
val indexPath = repo.address.removeSuffix("/") + index.name
|
||||||
val indexFile = Cache.getIndexFile(context, "repo_${repo.id}_$INDEX_V2_NAME")
|
val indexFile = Cache.getIndexFile(context, "repo_${repo.id}_$INDEX_V2_NAME")
|
||||||
val indexV2 = if (index != entry.index && indexFile.exists()) {
|
val indexV2 = if (index != entry.index && indexFile.exists()) {
|
||||||
// example https://apt.izzysoft.de/fdroid/repo/diff/1725372028000.json
|
|
||||||
val diffFile = downloader.downloadIndex(
|
val diffFile = downloader.downloadIndex(
|
||||||
context = context,
|
context = context,
|
||||||
repo = repo,
|
repo = repo,
|
||||||
@ -69,16 +69,11 @@ class EntrySyncable(
|
|||||||
fileName = "diff_${repo.versionInfo.timestamp}.json",
|
fileName = "diff_${repo.versionInfo.timestamp}.json",
|
||||||
diff = true,
|
diff = true,
|
||||||
)
|
)
|
||||||
// TODO: Maybe parse in parallel
|
val diff = async { diffParser.parse(diffFile, repo).second }
|
||||||
diffParser.parse(diffFile, repo).second.let {
|
val oldIndex = async { indexParser.parse(indexFile, repo).second }
|
||||||
|
diff.await().patchInto(oldIndex.await()) { index ->
|
||||||
diffFile.delete()
|
diffFile.delete()
|
||||||
it.patchInto(
|
Json.encodeToStream(index, indexFile.outputStream())
|
||||||
indexParser.parse(
|
|
||||||
indexFile,
|
|
||||||
repo
|
|
||||||
).second) { index ->
|
|
||||||
Json.encodeToStream(index, indexFile.outputStream())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// example https://apt.izzysoft.de/fdroid/repo/index-v2.json
|
// example https://apt.izzysoft.de/fdroid/repo/index-v2.json
|
||||||
|
@ -12,3 +12,10 @@ data class FileV2(
|
|||||||
val sha256: String? = null,
|
val sha256: String? = null,
|
||||||
val size: Long? = null,
|
val size: Long? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ApkFileV2(
|
||||||
|
val name: String,
|
||||||
|
val sha256: String,
|
||||||
|
val size: Long,
|
||||||
|
)
|
||||||
|
@ -1,7 +1,44 @@
|
|||||||
package com.looker.droidify.sync.v2.model
|
package com.looker.droidify.sync.v2.model
|
||||||
|
|
||||||
|
import androidx.core.os.LocaleListCompat
|
||||||
|
|
||||||
typealias LocalizedString = Map<String, String>
|
typealias LocalizedString = Map<String, String>
|
||||||
typealias NullableLocalizedString = Map<String, String?>
|
typealias NullableLocalizedString = Map<String, String?>
|
||||||
typealias LocalizedIcon = Map<String, FileV2>
|
typealias LocalizedIcon = Map<String, FileV2>
|
||||||
typealias LocalizedList = Map<String, List<String>>
|
typealias LocalizedList = Map<String, List<String>>
|
||||||
typealias LocalizedFiles = Map<String, List<FileV2>>
|
typealias LocalizedFiles = Map<String, List<FileV2>>
|
||||||
|
|
||||||
|
typealias DefaultName = String
|
||||||
|
typealias Tag = String
|
||||||
|
|
||||||
|
typealias AntiFeatureReason = LocalizedString
|
||||||
|
|
||||||
|
fun Map<String, Any>?.localesSize(): Int? = this?.keys?.size
|
||||||
|
|
||||||
|
fun Map<String, Any>?.locales(): List<String> = buildList {
|
||||||
|
if (!isNullOrEmpty()) {
|
||||||
|
for (locale in this@locales!!.keys) {
|
||||||
|
add(locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Map<String, T>?.localizedValue(locale: String): T? {
|
||||||
|
if (isNullOrEmpty()) return null
|
||||||
|
val localeList = LocaleListCompat.forLanguageTags(locale)
|
||||||
|
val match = localeList.getFirstMatch(keys.toTypedArray()) ?: return null
|
||||||
|
return get(match.toLanguageTag()) ?: run {
|
||||||
|
val langCountryTag = "${match.language}-${match.country}"
|
||||||
|
getOrStartsWith(langCountryTag) ?: run {
|
||||||
|
val langTag = match.language
|
||||||
|
getOrStartsWith(langTag) ?: get("en-US") ?: get("en") ?: values.first()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> Map<String, T>.getOrStartsWith(s: String): T? = get(s) ?: run {
|
||||||
|
entries.forEach { (key, value) ->
|
||||||
|
if (key.startsWith(s)) return value
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
@ -23,7 +23,7 @@ data class PackageV2Diff(
|
|||||||
added = metadata?.added ?: 0L,
|
added = metadata?.added ?: 0L,
|
||||||
lastUpdated = metadata?.lastUpdated ?: 0L,
|
lastUpdated = metadata?.lastUpdated ?: 0L,
|
||||||
name = metadata?.name
|
name = metadata?.name
|
||||||
?.mapNotNull { (key, value) -> value?.let { key to value } }?.toMap(),
|
?.mapNotNull { (key, value) -> value?.let { key to value } }?.toMap() ?: emptyMap(),
|
||||||
summary = metadata?.summary
|
summary = metadata?.summary
|
||||||
?.mapNotNull { (key, value) -> value?.let { key to value } }?.toMap(),
|
?.mapNotNull { (key, value) -> value?.let { key to value } }?.toMap(),
|
||||||
description = metadata?.description
|
description = metadata?.description
|
||||||
@ -116,7 +116,7 @@ data class PackageV2Diff(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MetadataV2(
|
data class MetadataV2(
|
||||||
val name: LocalizedString? = null,
|
val name: LocalizedString,
|
||||||
val summary: LocalizedString? = null,
|
val summary: LocalizedString? = null,
|
||||||
val description: LocalizedString? = null,
|
val description: LocalizedString? = null,
|
||||||
val icon: LocalizedIcon? = null,
|
val icon: LocalizedIcon? = null,
|
||||||
@ -129,7 +129,7 @@ data class MetadataV2(
|
|||||||
val bitcoin: String? = null,
|
val bitcoin: String? = null,
|
||||||
val categories: List<String> = emptyList(),
|
val categories: List<String> = emptyList(),
|
||||||
val changelog: String? = null,
|
val changelog: String? = null,
|
||||||
val donate: List<String> = emptyList(),
|
val donate: List<String>? = null,
|
||||||
val featureGraphic: LocalizedIcon? = null,
|
val featureGraphic: LocalizedIcon? = null,
|
||||||
val flattrID: String? = null,
|
val flattrID: String? = null,
|
||||||
val issueTracker: String? = null,
|
val issueTracker: String? = null,
|
||||||
@ -183,25 +183,25 @@ data class MetadataV2Diff(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class VersionV2(
|
data class VersionV2(
|
||||||
val added: Long,
|
val added: Long,
|
||||||
val file: FileV2,
|
val file: ApkFileV2,
|
||||||
val src: FileV2? = null,
|
val src: FileV2? = null,
|
||||||
val whatsNew: LocalizedString = emptyMap(),
|
val whatsNew: LocalizedString = emptyMap(),
|
||||||
val manifest: ManifestV2,
|
val manifest: ManifestV2,
|
||||||
val antiFeatures: Map<String, LocalizedString> = emptyMap(),
|
val antiFeatures: Map<Tag, AntiFeatureReason> = emptyMap(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class VersionV2Diff(
|
data class VersionV2Diff(
|
||||||
val added: Long? = null,
|
val added: Long? = null,
|
||||||
val file: FileV2? = null,
|
val file: ApkFileV2? = null,
|
||||||
val src: FileV2? = null,
|
val src: FileV2? = null,
|
||||||
val whatsNew: LocalizedString? = null,
|
val whatsNew: LocalizedString? = null,
|
||||||
val manifest: ManifestV2? = null,
|
val manifest: ManifestV2? = null,
|
||||||
val antiFeatures: Map<String, LocalizedString>? = null,
|
val antiFeatures: Map<Tag, AntiFeatureReason>? = null,
|
||||||
) {
|
) {
|
||||||
fun toVersion() = VersionV2(
|
fun toVersion() = VersionV2(
|
||||||
added = added ?: 0,
|
added = added ?: 0,
|
||||||
file = file ?: FileV2(""),
|
file = file ?: ApkFileV2("", "", -1L),
|
||||||
src = src ?: FileV2(""),
|
src = src ?: FileV2(""),
|
||||||
whatsNew = whatsNew ?: emptyMap(),
|
whatsNew = whatsNew ?: emptyMap(),
|
||||||
manifest = manifest ?: ManifestV2(
|
manifest = manifest ?: ManifestV2(
|
||||||
|
@ -10,8 +10,8 @@ data class RepoV2(
|
|||||||
val icon: LocalizedIcon? = null,
|
val icon: LocalizedIcon? = null,
|
||||||
val name: LocalizedString = emptyMap(),
|
val name: LocalizedString = emptyMap(),
|
||||||
val description: LocalizedString = emptyMap(),
|
val description: LocalizedString = emptyMap(),
|
||||||
val antiFeatures: Map<String, AntiFeatureV2> = emptyMap(),
|
val antiFeatures: Map<Tag, AntiFeatureV2> = emptyMap(),
|
||||||
val categories: Map<String, CategoryV2> = emptyMap(),
|
val categories: Map<DefaultName, CategoryV2> = emptyMap(),
|
||||||
val mirrors: List<MirrorV2> = emptyList(),
|
val mirrors: List<MirrorV2> = emptyList(),
|
||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
)
|
)
|
||||||
@ -22,8 +22,8 @@ data class RepoV2Diff(
|
|||||||
val icon: LocalizedIcon? = null,
|
val icon: LocalizedIcon? = null,
|
||||||
val name: LocalizedString? = null,
|
val name: LocalizedString? = null,
|
||||||
val description: LocalizedString? = null,
|
val description: LocalizedString? = null,
|
||||||
val antiFeatures: Map<String, AntiFeatureV2?>? = null,
|
val antiFeatures: Map<Tag, AntiFeatureV2?>? = null,
|
||||||
val categories: Map<String, CategoryV2?>? = null,
|
val categories: Map<DefaultName, CategoryV2?>? = null,
|
||||||
val mirrors: List<MirrorV2>? = null,
|
val mirrors: List<MirrorV2>? = null,
|
||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
) {
|
) {
|
||||||
@ -69,7 +69,7 @@ data class RepoV2Diff(
|
|||||||
data class MirrorV2(
|
data class MirrorV2(
|
||||||
val url: String,
|
val url: String,
|
||||||
val isPrimary: Boolean? = null,
|
val isPrimary: Boolean? = null,
|
||||||
val location: String? = null
|
val countryCode: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -31,6 +31,7 @@ import com.google.android.material.shape.ShapeAppearanceModel
|
|||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import com.looker.droidify.R
|
import com.looker.droidify.R
|
||||||
import com.looker.droidify.databinding.TabsToolbarBinding
|
import com.looker.droidify.databinding.TabsToolbarBinding
|
||||||
|
import com.looker.droidify.datastore.model.supportedSortOrders
|
||||||
import com.looker.droidify.datastore.extension.sortOrderName
|
import com.looker.droidify.datastore.extension.sortOrderName
|
||||||
import com.looker.droidify.datastore.model.SortOrder
|
import com.looker.droidify.datastore.model.SortOrder
|
||||||
import com.looker.droidify.model.ProductItem
|
import com.looker.droidify.model.ProductItem
|
||||||
@ -212,7 +213,7 @@ class TabsFragment : ScreenFragment() {
|
|||||||
.setIcon(toolbar.context.getMutatedIcon(R.drawable.ic_sort))
|
.setIcon(toolbar.context.getMutatedIcon(R.drawable.ic_sort))
|
||||||
.let { menu ->
|
.let { menu ->
|
||||||
menu.item.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
menu.item.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
||||||
val menuItems = SortOrder.entries.map { sortOrder ->
|
val menuItems = supportedSortOrders().map { sortOrder ->
|
||||||
menu.add(context.sortOrderName(sortOrder))
|
menu.add(context.sortOrderName(sortOrder))
|
||||||
.setOnMenuItemClickListener {
|
.setOnMenuItemClickListener {
|
||||||
viewModel.setSortOrder(sortOrder)
|
viewModel.setSortOrder(sortOrder)
|
||||||
@ -224,9 +225,7 @@ class TabsFragment : ScreenFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
favouritesItem = add(1, 0, 0, stringRes.favourites)
|
favouritesItem = add(1, 0, 0, stringRes.favourites)
|
||||||
.setIcon(
|
.setIcon(toolbar.context.getMutatedIcon(R.drawable.ic_favourite_checked))
|
||||||
toolbar.context.getMutatedIcon(R.drawable.ic_favourite_checked)
|
|
||||||
)
|
|
||||||
.setOnMenuItemClickListener {
|
.setOnMenuItemClickListener {
|
||||||
view.post { mainActivity.navigateFavourites() }
|
view.post { mainActivity.navigateFavourites() }
|
||||||
true
|
true
|
||||||
|
@ -7,6 +7,7 @@ import com.looker.droidify.database.Database
|
|||||||
import com.looker.droidify.datastore.SettingsRepository
|
import com.looker.droidify.datastore.SettingsRepository
|
||||||
import com.looker.droidify.datastore.get
|
import com.looker.droidify.datastore.get
|
||||||
import com.looker.droidify.datastore.model.SortOrder
|
import com.looker.droidify.datastore.model.SortOrder
|
||||||
|
import com.looker.droidify.domain.model.Fingerprint
|
||||||
import com.looker.droidify.model.ProductItem
|
import com.looker.droidify.model.ProductItem
|
||||||
import com.looker.droidify.ui.tabsFragment.TabsFragment.BackAction
|
import com.looker.droidify.ui.tabsFragment.TabsFragment.BackAction
|
||||||
import com.looker.droidify.utility.common.extension.asStateFlow
|
import com.looker.droidify.utility.common.extension.asStateFlow
|
||||||
@ -20,7 +21,9 @@ import javax.inject.Inject
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class TabsViewModel @Inject constructor(
|
class TabsViewModel @Inject constructor(
|
||||||
private val settingsRepository: SettingsRepository,
|
private val settingsRepository: SettingsRepository,
|
||||||
private val savedStateHandle: SavedStateHandle
|
private val indexDao: IndexDao,
|
||||||
|
private val syncable: Syncable<IndexV2>,
|
||||||
|
private val savedStateHandle: SavedStateHandle,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val currentSection =
|
val currentSection =
|
||||||
@ -37,7 +40,7 @@ class TabsViewModel @Inject constructor(
|
|||||||
val sections =
|
val sections =
|
||||||
combine(
|
combine(
|
||||||
Database.CategoryAdapter.getAllStream(),
|
Database.CategoryAdapter.getAllStream(),
|
||||||
Database.RepositoryAdapter.getEnabledStream()
|
Database.RepositoryAdapter.getEnabledStream(),
|
||||||
) { categories, repos ->
|
) { categories, repos ->
|
||||||
val productCategories = categories
|
val productCategories = categories
|
||||||
.asSequence()
|
.asSequence()
|
||||||
@ -80,6 +83,30 @@ class TabsViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun calcBackAction(
|
||||||
|
currentSection: ProductItem.Section,
|
||||||
|
isSearchActionItemExpanded: Boolean,
|
||||||
|
showSections: Boolean,
|
||||||
|
): BackAction {
|
||||||
|
return when {
|
||||||
|
currentSection != ProductItem.Section.All -> {
|
||||||
|
BackAction.ProductAll
|
||||||
|
}
|
||||||
|
|
||||||
|
isSearchActionItemExpanded -> {
|
||||||
|
BackAction.CollapseSearchView
|
||||||
|
}
|
||||||
|
|
||||||
|
showSections -> {
|
||||||
|
BackAction.HideSections
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
BackAction.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val STATE_SECTION = "section"
|
private const val STATE_SECTION = "section"
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,18 @@
|
|||||||
package com.looker.droidify.utility.common.extension
|
package com.looker.droidify.utility.common.extension
|
||||||
|
|
||||||
|
inline fun <K, E> Map<K, E>.windowed(windowSize: Int, block: (Map<K, E>) -> Unit) {
|
||||||
|
var index = 0
|
||||||
|
val windowedPackages: HashMap<K, E> = HashMap(windowSize)
|
||||||
|
forEach {
|
||||||
|
index++
|
||||||
|
windowedPackages.put(it.key, it.value)
|
||||||
|
if (windowedPackages.size == windowSize || index == size) {
|
||||||
|
block(windowedPackages)
|
||||||
|
windowedPackages.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inline fun <K, E> Map<K, E>.updateAsMutable(block: MutableMap<K, E>.() -> Unit): Map<K, E> {
|
inline fun <K, E> Map<K, E>.updateAsMutable(block: MutableMap<K, E>.() -> Unit): Map<K, E> {
|
||||||
return toMutableMap().apply(block)
|
return toMutableMap().apply(block)
|
||||||
}
|
}
|
||||||
|
9
app/src/test/kotlin/com/looker/droidify/Resource.kt
Normal file
9
app/src/test/kotlin/com/looker/droidify/Resource.kt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package com.looker.droidify
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
fun assets(name: String): File? {
|
||||||
|
val url = Thread.currentThread().contextClassLoader?.getResource(name) ?: return null
|
||||||
|
return File(url.file)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
|||||||
|
package com.looker.droidify.encryption
|
||||||
|
|
||||||
|
import com.looker.droidify.data.encryption.Key
|
||||||
|
import com.looker.droidify.data.encryption.generateSecretKey
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFails
|
||||||
|
import kotlin.test.assertNotEquals
|
||||||
|
|
||||||
|
class EncryptionTest {
|
||||||
|
|
||||||
|
private val secretKey = Key(generateSecretKey())
|
||||||
|
private val fakeKey = Key(generateSecretKey())
|
||||||
|
|
||||||
|
private val testString = "This is a test string"
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `encrypt and decrypt`() {
|
||||||
|
val (encrypted, iv) = secretKey.encrypt(testString)
|
||||||
|
assertNotEquals(testString, encrypted.value, "Encrypted and original string are the same")
|
||||||
|
val decrypted = encrypted.decrypt(secretKey, iv)
|
||||||
|
assertEquals(testString, decrypted, "Decrypted string does not match original")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `encrypt and decrypt with fake key`() {
|
||||||
|
val (encrypted, iv) = secretKey.encrypt(testString)
|
||||||
|
assertNotEquals(testString, encrypted.value, "Encrypted and original string are the same")
|
||||||
|
assertFails { encrypted.decrypt(fakeKey, iv) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `encrypt and decrypt with wrong iv`() {
|
||||||
|
val (encrypted, iv) = secretKey.encrypt(testString)
|
||||||
|
assertNotEquals(testString, encrypted.value, "Encrypted and original string are the same")
|
||||||
|
|
||||||
|
val fakeIv = iv.clone().apply { this[lastIndex] = "1".toByte() }
|
||||||
|
val output = encrypted.decrypt(secretKey, fakeIv)
|
||||||
|
assertNotEquals(testString, output, "Encrypted and original string are the same")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
package com.looker.droidify.index
|
||||||
|
|
||||||
|
import com.looker.droidify.assets
|
||||||
|
import com.looker.droidify.sync.common.JsonParser
|
||||||
|
import com.looker.droidify.sync.v2.model.IndexV2
|
||||||
|
import com.looker.droidify.sync.v2.model.PackageV2
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlin.collections.component1
|
||||||
|
import kotlin.collections.component2
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
class IndexValuesTest {
|
||||||
|
|
||||||
|
private val dispatcher = StandardTestDispatcher()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test values in index v2`() = runTest(dispatcher) {
|
||||||
|
val izzy = assets("izzy_index_v2_updated.json")!!
|
||||||
|
val fdroid = assets("fdroid_index_v2.json")!!
|
||||||
|
val izzyIndex: IndexV2 = JsonParser.decodeFromString(izzy.readBytes().decodeToString())
|
||||||
|
val fdroidIndex: IndexV2 = JsonParser.decodeFromString(fdroid.readBytes().decodeToString())
|
||||||
|
|
||||||
|
var hits = 0
|
||||||
|
var total = 0
|
||||||
|
|
||||||
|
val nativeCode = mutableSetOf<String>()
|
||||||
|
val performTest: (PackageV2) -> Unit = { data ->
|
||||||
|
data.versions.forEach { t, u ->
|
||||||
|
hits++
|
||||||
|
nativeCode.addAll(u.manifest.nativecode)
|
||||||
|
}
|
||||||
|
total++
|
||||||
|
}
|
||||||
|
|
||||||
|
izzyIndex.packages.forEach { (packageName, data) ->
|
||||||
|
// println("Testing on Izzy $packageName")
|
||||||
|
performTest(data)
|
||||||
|
}
|
||||||
|
fdroidIndex.packages.forEach { (packageName, data) ->
|
||||||
|
// println("Testing on FDroid $packageName")
|
||||||
|
performTest(data)
|
||||||
|
}
|
||||||
|
println(nativeCode)
|
||||||
|
println("Hits: %d, %.2f%%".format(hits, hits * 100 / total.toFloat()))
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ import io.ktor.client.plugins.SocketTimeoutException
|
|||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertIs
|
import kotlin.test.assertIs
|
||||||
@ -32,19 +33,19 @@ class KtorDownloaderTest {
|
|||||||
|
|
||||||
private val downloader = KtorDownloader(engine, dispatcher)
|
private val downloader = KtorDownloader(engine, dispatcher)
|
||||||
|
|
||||||
@org.junit.jupiter.api.Test
|
@Test
|
||||||
fun `head call success`() = runTest(dispatcher) {
|
fun `head call success`() = runTest(dispatcher) {
|
||||||
val response = downloader.headCall("https://success.com")
|
val response = downloader.headCall("https://success.com")
|
||||||
assertIs<NetworkResponse.Success>(response)
|
assertIs<NetworkResponse.Success>(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@org.junit.jupiter.api.Test
|
@Test
|
||||||
fun `head call if path not found`() = runTest(dispatcher) {
|
fun `head call if path not found`() = runTest(dispatcher) {
|
||||||
val response = downloader.headCall("https://notfound.com")
|
val response = downloader.headCall("https://notfound.com")
|
||||||
assertIs<NetworkResponse.Error.Http>(response)
|
assertIs<NetworkResponse.Error.Http>(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@org.junit.jupiter.api.Test
|
@Test
|
||||||
fun `save text to file success`() = runTest(dispatcher) {
|
fun `save text to file success`() = runTest(dispatcher) {
|
||||||
val file = File.createTempFile("test", "success")
|
val file = File.createTempFile("test", "success")
|
||||||
val response = downloader.downloadToFile(
|
val response = downloader.downloadToFile(
|
||||||
@ -55,7 +56,7 @@ class KtorDownloaderTest {
|
|||||||
assertEquals("success", file.readText())
|
assertEquals("success", file.readText())
|
||||||
}
|
}
|
||||||
|
|
||||||
@org.junit.jupiter.api.Test
|
@Test
|
||||||
fun `save text to read-only file`() = runTest(dispatcher) {
|
fun `save text to read-only file`() = runTest(dispatcher) {
|
||||||
val file = File.createTempFile("test", "success")
|
val file = File.createTempFile("test", "success")
|
||||||
file.setReadOnly()
|
file.setReadOnly()
|
||||||
@ -66,7 +67,7 @@ class KtorDownloaderTest {
|
|||||||
assertIs<NetworkResponse.Error.IO>(response)
|
assertIs<NetworkResponse.Error.IO>(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@org.junit.jupiter.api.Test
|
@Test
|
||||||
fun `save text to file with slow connection`() = runTest(dispatcher) {
|
fun `save text to file with slow connection`() = runTest(dispatcher) {
|
||||||
val file = File.createTempFile("test", "success")
|
val file = File.createTempFile("test", "success")
|
||||||
val response = downloader.downloadToFile(
|
val response = downloader.downloadToFile(
|
||||||
@ -76,7 +77,7 @@ class KtorDownloaderTest {
|
|||||||
assertIs<NetworkResponse.Error.ConnectionTimeout>(response)
|
assertIs<NetworkResponse.Error.ConnectionTimeout>(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@org.junit.jupiter.api.Test
|
@Test
|
||||||
fun `save text to file with socket error`() = runTest(dispatcher) {
|
fun `save text to file with socket error`() = runTest(dispatcher) {
|
||||||
val file = File.createTempFile("test", "success")
|
val file = File.createTempFile("test", "success")
|
||||||
val response = downloader.downloadToFile(
|
val response = downloader.downloadToFile(
|
||||||
@ -86,7 +87,7 @@ class KtorDownloaderTest {
|
|||||||
assertIs<NetworkResponse.Error.SocketTimeout>(response)
|
assertIs<NetworkResponse.Error.SocketTimeout>(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@org.junit.jupiter.api.Test
|
@Test
|
||||||
fun `save text to file if not modifier`() = runTest(dispatcher) {
|
fun `save text to file if not modifier`() = runTest(dispatcher) {
|
||||||
val file = File.createTempFile("test", "success")
|
val file = File.createTempFile("test", "success")
|
||||||
val response = downloader.downloadToFile(
|
val response = downloader.downloadToFile(
|
||||||
@ -100,7 +101,7 @@ class KtorDownloaderTest {
|
|||||||
assertEquals("", file.readText())
|
assertEquals("", file.readText())
|
||||||
}
|
}
|
||||||
|
|
||||||
@org.junit.jupiter.api.Test
|
@Test
|
||||||
fun `save text to file with wrong authentication`() =
|
fun `save text to file with wrong authentication`() =
|
||||||
runTest(dispatcher) {
|
runTest(dispatcher) {
|
||||||
val file = File.createTempFile("test", "success")
|
val file = File.createTempFile("test", "success")
|
||||||
|
1
app/src/test/resources/fdroid_index_v2.json
Normal file
1
app/src/test/resources/fdroid_index_v2.json
Normal file
File diff suppressed because one or more lines are too long
1
app/src/test/resources/izzy_index_v2.json
Normal file
1
app/src/test/resources/izzy_index_v2.json
Normal file
File diff suppressed because one or more lines are too long
1
app/src/test/resources/izzy_index_v2_updated.json
Normal file
1
app/src/test/resources/izzy_index_v2_updated.json
Normal file
File diff suppressed because one or more lines are too long
@ -5,4 +5,6 @@ plugins {
|
|||||||
alias(libs.plugins.hilt) apply false
|
alias(libs.plugins.hilt) apply false
|
||||||
alias(libs.plugins.kotlin.serialization) apply false
|
alias(libs.plugins.kotlin.serialization) apply false
|
||||||
alias(libs.plugins.kotlin.parcelize) apply false
|
alias(libs.plugins.kotlin.parcelize) apply false
|
||||||
|
alias(libs.plugins.hilt) apply false
|
||||||
|
alias(libs.plugins.ksp) apply false
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,6 @@
|
|||||||
org.gradle.daemon=true
|
org.gradle.daemon=true
|
||||||
org.gradle.jvmargs=-Xmx6g -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g
|
org.gradle.jvmargs=-Xmx6g -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
org.gradle.parallel=true
|
|
||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
org.gradle.configuration-cache=true
|
org.gradle.configuration-cache=true
|
||||||
android.defaults.buildfeatures.resvalues=false
|
|
||||||
android.defaults.buildfeatures.shaders=false
|
android.defaults.buildfeatures.shaders=false
|
||||||
|
@ -46,17 +46,16 @@ lifecycle-runtime= { group = "androidx.lifecycle", name = "lifecycle-runtime", v
|
|||||||
lifecycle-viewModel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "lifecycle" }
|
lifecycle-viewModel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "lifecycle" }
|
||||||
recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recycler-view" }
|
recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recycler-view" }
|
||||||
sqlite-ktx = { group = "androidx.sqlite", name = "sqlite-ktx", version.ref = "sqlite" }
|
sqlite-ktx = { group = "androidx.sqlite", name = "sqlite-ktx", version.ref = "sqlite" }
|
||||||
test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" }
|
|
||||||
test-ext = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "test-ext" }
|
test-ext = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "test-ext" }
|
||||||
test-rules = { group = "androidx.test", name = "rules", version.ref = "test-rules" }
|
test-rules = { group = "androidx.test", name = "rules", version.ref = "test-rules" }
|
||||||
test-runner = { group = "androidx.test", name = "runner", version.ref = "test-runner" }
|
test-runner = { group = "androidx.test", name = "runner", version.ref = "test-runner" }
|
||||||
test-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "ui-automator" }
|
|
||||||
work-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" }
|
work-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" }
|
||||||
work-testing = { group = "androidx.work", name = "work-testing", version.ref = "work" }
|
work-testing = { group = "androidx.work", name = "work-testing", version.ref = "work" }
|
||||||
coil-core = { group = "io.coil-kt.coil3", name = "coil", version.ref = "coil" }
|
coil-core = { group = "io.coil-kt.coil3", name = "coil", version.ref = "coil" }
|
||||||
coil-network = { group = "io.coil-kt.coil3", name = "coil-network-ktor3", version.ref = "coil" }
|
coil-network = { group = "io.coil-kt.coil3", name = "coil-network-ktor3", version.ref = "coil" }
|
||||||
hilt-core = { group = "com.google.dagger", name = "hilt-core", version.ref = "hilt" }
|
hilt-core = { group = "com.google.dagger", name = "hilt-core", version.ref = "hilt" }
|
||||||
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
|
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
|
||||||
|
hilt-test = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" }
|
||||||
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
|
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
|
||||||
hilt-ext-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hiltExt" }
|
hilt-ext-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hiltExt" }
|
||||||
hilt-ext-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltExt" }
|
hilt-ext-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltExt" }
|
||||||
@ -77,6 +76,7 @@ libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref
|
|||||||
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
||||||
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
||||||
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
||||||
|
room-test = { group = "androidx.room", name = "room-testing", version.ref = "room" }
|
||||||
shizuku-api = { group = "dev.rikka.shizuku", name = "api", version.ref = "shizuku" }
|
shizuku-api = { group = "dev.rikka.shizuku", name = "api", version.ref = "shizuku" }
|
||||||
shizuku-provider = { group = "dev.rikka.shizuku", name = "provider", version.ref = "shizuku" }
|
shizuku-provider = { group = "dev.rikka.shizuku", name = "provider", version.ref = "shizuku" }
|
||||||
image-viewer = { module = "com.github.stfalcon-studio:StfalconImageViewer", version.ref = "image-viewer" }
|
image-viewer = { module = "com.github.stfalcon-studio:StfalconImageViewer", version.ref = "image-viewer" }
|
||||||
@ -93,6 +93,10 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi
|
|||||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
|
room = ["room-runtime", "room-ktx"]
|
||||||
|
shizuku = ["shizuku-provider", "shizuku-api"]
|
||||||
|
ktor = ["ktor-core", "ktor-okhttp"]
|
||||||
|
coroutines = ["coroutines-core", "coroutines-android", "coroutines-guava"]
|
||||||
coil = ["coil-core", "coil-network"]
|
coil = ["coil-core", "coil-network"]
|
||||||
test-unit = ["junit-jupiter", "ktor-mock", "coroutines-test", "kotlin-test"]
|
test-unit = ["junit-jupiter", "ktor-mock", "coroutines-test", "kotlin-test"]
|
||||||
test-android = ["test-runner", "test-rules", "test-ext", "test-espresso-core", "coroutines-test", "kotlin-test"]
|
test-android = ["test-runner", "test-rules", "test-ext", "coroutines-test", "kotlin-test"]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user