commit
249df47bef
@ -20,8 +20,8 @@ android {
|
||||
applicationId = "com.looker.droidify"
|
||||
versionCode = 650
|
||||
versionName = latestVersionName
|
||||
vectorDrawables.useSupportLibrary = false
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
testInstrumentationRunner = "com.looker.droidify.TestRunner"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@ -40,15 +40,13 @@ android {
|
||||
)
|
||||
}
|
||||
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
arg("room.generateKotlin", "true")
|
||||
}
|
||||
|
||||
sourceSets.forEach { source ->
|
||||
val javaDir = source.java.srcDirs.find { it.name == "java" }
|
||||
source.java {
|
||||
srcDir(File(javaDir?.parentFile, "kotlin"))
|
||||
}
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@ -130,19 +128,17 @@ dependencies {
|
||||
implementation(libs.kotlin.stdlib)
|
||||
implementation(libs.datetime)
|
||||
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(libs.coroutines.android)
|
||||
implementation(libs.coroutines.guava)
|
||||
implementation(libs.bundles.coroutines)
|
||||
|
||||
implementation(libs.libsu.core)
|
||||
implementation(libs.shizuku.api)
|
||||
api(libs.shizuku.provider)
|
||||
implementation(libs.bundles.shizuku)
|
||||
|
||||
implementation(libs.jackson.core)
|
||||
implementation(libs.serialization)
|
||||
|
||||
implementation(libs.ktor.core)
|
||||
implementation(libs.ktor.okhttp)
|
||||
implementation(libs.bundles.ktor)
|
||||
implementation(libs.bundles.room)
|
||||
ksp(libs.room.compiler)
|
||||
|
||||
implementation(libs.work.ktx)
|
||||
|
||||
@ -155,8 +151,10 @@ dependencies {
|
||||
testImplementation(platform(libs.junit.bom))
|
||||
testImplementation(libs.bundles.test.unit)
|
||||
testRuntimeOnly(libs.junit.platform)
|
||||
androidTestImplementation(platform(libs.junit.bom))
|
||||
androidTestImplementation(libs.hilt.test)
|
||||
androidTestImplementation(libs.room.test)
|
||||
androidTestImplementation(libs.bundles.test.android)
|
||||
kspAndroidTest(libs.hilt.compiler)
|
||||
|
||||
// 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.filters.SmallTest
|
||||
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.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.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.File
|
||||
import kotlin.math.sqrt
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@ -21,7 +27,9 @@ class RepositoryUpdaterTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
context = InstrumentationRegistry.getInstrumentation().context
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
Database.init(context)
|
||||
RepositoryUpdater.init(CoroutineScope(Dispatchers.Default), FakeDownloader)
|
||||
repository = Repository(
|
||||
id = 15,
|
||||
address = "https://apt.izzysoft.de/fdroid/repo",
|
||||
@ -41,13 +49,14 @@ class RepositoryUpdaterTest {
|
||||
|
||||
@Test
|
||||
fun processFile() {
|
||||
testRepetition(1) {
|
||||
val output = benchmark(1) {
|
||||
val createFile = File.createTempFile("index", "entry")
|
||||
val mergerFile = File.createTempFile("index", "merger")
|
||||
val jarStream = context.resources.assets.open("index-v1.jar")
|
||||
jarStream.copyTo(createFile.outputStream())
|
||||
process(createFile, mergerFile)
|
||||
}
|
||||
println(output)
|
||||
}
|
||||
|
||||
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)
|
||||
@Before
|
||||
fun before() {
|
||||
context = InstrumentationRegistry.getInstrumentation().context
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
dispatcher = StandardTestDispatcher()
|
||||
validator = IndexJarValidator(dispatcher)
|
||||
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.Izzy
|
||||
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.downloadIndex
|
||||
import com.looker.droidify.sync.common.toV2
|
||||
import com.looker.droidify.sync.v1.V1Parser
|
||||
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.IndexV2
|
||||
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 kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
@ -28,6 +29,8 @@ import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class V1SyncableTest {
|
||||
@ -42,7 +45,7 @@ class V1SyncableTest {
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
context = InstrumentationRegistry.getInstrumentation().context
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
dispatcher = StandardTestDispatcher()
|
||||
validator = IndexJarValidator(dispatcher)
|
||||
parser = V1Parser(dispatcher, JsonParser, validator)
|
||||
@ -102,9 +105,38 @@ class V1SyncableTest {
|
||||
testIndexConversion("index-v1.jar", "index-v2-updated.json")
|
||||
}
|
||||
|
||||
// @Test
|
||||
fun v1tov2FDroidRepo() = runTest(dispatcher) {
|
||||
testIndexConversion("fdroid-index-v1.jar", "fdroid-index-v2.json")
|
||||
@Test
|
||||
fun targetPropertyTest() = runTest(dispatcher) {
|
||||
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(
|
||||
@ -252,6 +284,8 @@ private fun assertVersion(
|
||||
assertNotNull(foundVersion)
|
||||
|
||||
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.src?.name, foundVersion.src?.name)
|
||||
|
||||
@ -261,7 +295,13 @@ private fun assertVersion(
|
||||
assertEquals(expectedMan.versionCode, foundMan.versionCode)
|
||||
assertEquals(expectedMan.versionName, foundMan.versionName)
|
||||
assertEquals(expectedMan.maxSdkVersion, foundMan.maxSdkVersion)
|
||||
assertNotNull(expectedMan.usesSdk)
|
||||
assertNotNull(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(
|
||||
expectedMan.features.sortedBy { it.name },
|
||||
|
@ -8,11 +8,6 @@ internal inline fun benchmark(
|
||||
extraMessage: String? = null,
|
||||
block: () -> Long,
|
||||
): String {
|
||||
if (extraMessage != null) {
|
||||
println("=".repeat(50))
|
||||
println(extraMessage)
|
||||
println("=".repeat(50))
|
||||
}
|
||||
val times = DoubleArray(repetition)
|
||||
repeat(repetition) { iteration ->
|
||||
System.gc()
|
||||
@ -20,11 +15,19 @@ internal inline fun benchmark(
|
||||
times[iteration] = block().toDouble()
|
||||
}
|
||||
val meanAndDeviation = times.culledMeanAndDeviation()
|
||||
return buildString {
|
||||
return buildString(200) {
|
||||
append("=".repeat(50))
|
||||
append("\n")
|
||||
if (extraMessage != null) {
|
||||
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("\n")
|
||||
append("=".repeat(50))
|
||||
|
@ -6,7 +6,7 @@ import com.looker.droidify.domain.model.Repo
|
||||
import com.looker.droidify.domain.model.VersionInfo
|
||||
|
||||
val Izzy = Repo(
|
||||
id = 1L,
|
||||
id = 1,
|
||||
enabled = true,
|
||||
address = "https://apt.izzysoft.de/fdroid/repo",
|
||||
name = "IzzyOnDroid F-Droid Repo",
|
||||
@ -15,6 +15,4 @@ val Izzy = Repo(
|
||||
authentication = Authentication("", ""),
|
||||
versionInfo = VersionInfo(0L, null),
|
||||
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() {
|
||||
super.onCreate()
|
||||
|
||||
if (BuildConfig.DEBUG && SdkCheck.isOreo) strictThreadPolicy()
|
||||
// if (BuildConfig.DEBUG && SdkCheck.isOreo) strictThreadPolicy()
|
||||
|
||||
val databaseUpdated = Database.init(this)
|
||||
ProductPreferences.init(this, appScope)
|
||||
RepositoryUpdater.init(appScope, downloader)
|
||||
// RepositoryUpdater.init(appScope, downloader)
|
||||
listenApplications()
|
||||
checkLanguage()
|
||||
updatePreference()
|
||||
appScope.launch { installer() }
|
||||
|
||||
if (databaseUpdated) forceSyncAll()
|
||||
// if (databaseUpdated) forceSyncAll()
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
@ -107,7 +107,7 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
addDataScheme("package")
|
||||
}
|
||||
},
|
||||
)
|
||||
val installedItems =
|
||||
packageManager.getInstalledPackagesCompat()
|
||||
@ -200,7 +200,7 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
||||
periodMillis = period,
|
||||
networkType = syncConditions.toJobNetworkType(),
|
||||
isCharging = syncConditions.pluggedIn,
|
||||
isBatteryLow = syncConditions.batteryNotLow
|
||||
isBatteryLow = syncConditions.batteryNotLow,
|
||||
)
|
||||
jobScheduler?.schedule(job)
|
||||
}
|
||||
@ -212,10 +212,13 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
||||
Database.RepositoryAdapter.put(it.copy(lastModified = "", entityTag = ""))
|
||||
}
|
||||
}
|
||||
Connection(SyncService::class.java, onBind = { connection, binder ->
|
||||
Connection(
|
||||
SyncService::class.java,
|
||||
onBind = { connection, binder ->
|
||||
binder.sync(SyncService.SyncRequest.FORCE)
|
||||
connection.unbind(this)
|
||||
}).bind(this)
|
||||
},
|
||||
).bind(this)
|
||||
}
|
||||
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
@ -256,12 +259,12 @@ fun strictThreadPolicy() {
|
||||
.detectNetwork()
|
||||
.detectUnbufferedIo()
|
||||
.penaltyLog()
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
StrictMode.setVmPolicy(
|
||||
StrictMode.VmPolicy.Builder()
|
||||
.detectAll()
|
||||
.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) }
|
||||
.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> {
|
||||
return db.query(
|
||||
Schema.Product.name,
|
||||
@ -719,7 +732,7 @@ object Database {
|
||||
when (order) {
|
||||
SortOrder.UPDATED -> builder += "product.${Schema.Product.ROW_UPDATED} DESC,"
|
||||
SortOrder.ADDED -> builder += "product.${Schema.Product.ROW_ADDED} DESC,"
|
||||
SortOrder.NAME -> Unit
|
||||
else -> Unit
|
||||
}::class
|
||||
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.ADDED -> getString(stringRes.whats_new)
|
||||
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 {
|
||||
UPDATED,
|
||||
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 links: Links,
|
||||
val metadata: Metadata,
|
||||
val author: Author,
|
||||
val author: Author?,
|
||||
val screenshots: Screenshots,
|
||||
val graphics: Graphics,
|
||||
val donation: Donation,
|
||||
@ -15,34 +15,35 @@ data class App(
|
||||
)
|
||||
|
||||
data class Author(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val email: String,
|
||||
val web: String
|
||||
val id: Int,
|
||||
val name: String?,
|
||||
val email: String?,
|
||||
val phone: String?,
|
||||
val web: String?,
|
||||
)
|
||||
|
||||
data class Donation(
|
||||
val regularUrl: String? = null,
|
||||
val regularUrl: List<String>? = null,
|
||||
val bitcoinAddress: String? = null,
|
||||
val flattrId: String? = null,
|
||||
val liteCoinAddress: String? = null,
|
||||
val litecoinAddress: String? = null,
|
||||
val openCollectiveId: String? = null,
|
||||
val librePayId: String? = null,
|
||||
val liberapayId: String? = null,
|
||||
)
|
||||
|
||||
data class Graphics(
|
||||
val featureGraphic: String = "",
|
||||
val promoGraphic: String = "",
|
||||
val tvBanner: String = "",
|
||||
val video: String = ""
|
||||
val featureGraphic: String? = null,
|
||||
val promoGraphic: String? = null,
|
||||
val tvBanner: String? = null,
|
||||
val video: String? = null,
|
||||
)
|
||||
|
||||
data class Links(
|
||||
val changelog: String = "",
|
||||
val issueTracker: String = "",
|
||||
val sourceCode: String = "",
|
||||
val translation: String = "",
|
||||
val webSite: String = ""
|
||||
val changelog: String? = null,
|
||||
val issueTracker: String? = null,
|
||||
val sourceCode: String? = null,
|
||||
val translation: String? = null,
|
||||
val webSite: String? = null,
|
||||
)
|
||||
|
||||
data class Metadata(
|
||||
|
@ -1,25 +1,23 @@
|
||||
package com.looker.droidify.domain.model
|
||||
|
||||
data class Repo(
|
||||
val id: Long,
|
||||
val id: Int,
|
||||
val enabled: Boolean,
|
||||
val address: String,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val fingerprint: Fingerprint?,
|
||||
val authentication: Authentication,
|
||||
val authentication: Authentication?,
|
||||
val versionInfo: VersionInfo,
|
||||
val mirrors: List<String>,
|
||||
val antiFeatures: List<AntiFeature>,
|
||||
val categories: List<Category>
|
||||
) {
|
||||
val shouldAuthenticate =
|
||||
authentication.username.isNotEmpty() && authentication.password.isNotEmpty()
|
||||
val shouldAuthenticate = authentication != null
|
||||
|
||||
fun update(fingerprint: Fingerprint, timestamp: Long? = null, etag: String? = null): Repo {
|
||||
return copy(
|
||||
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 name: String,
|
||||
val icon: String = "",
|
||||
val description: String = ""
|
||||
val description: String = "",
|
||||
)
|
||||
|
||||
data class Category(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val icon: String = "",
|
||||
val description: String = ""
|
||||
val description: String = "",
|
||||
)
|
||||
|
||||
data class Authentication(
|
||||
val username: String,
|
||||
val password: String
|
||||
val password: String,
|
||||
)
|
||||
|
||||
data class VersionInfo(
|
||||
val timestamp: Long,
|
||||
val etag: String?
|
||||
val etag: String?,
|
||||
)
|
||||
|
@ -82,7 +82,9 @@ internal class KtorDownloader(
|
||||
if (networkResponse !is NetworkResponse.Success) {
|
||||
return@execute networkResponse
|
||||
}
|
||||
response.bodyAsChannel().copyTo(target.outputStream())
|
||||
target.outputStream().use { output ->
|
||||
response.bodyAsChannel().copyTo(output)
|
||||
}
|
||||
validator?.validate(target)
|
||||
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.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> {
|
||||
|
||||
val parser: Parser<T>
|
||||
|
||||
suspend fun sync(
|
||||
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.name
|
||||
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.FeatureV2
|
||||
import com.looker.droidify.sync.v2.model.FileV2
|
||||
@ -94,7 +95,7 @@ private fun AppV1.toV2(preferredSigner: String?): MetadataV2 = MetadataV2(
|
||||
added = added ?: 0L,
|
||||
lastUpdated = lastUpdated ?: 0L,
|
||||
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 },
|
||||
summary = localized?.localizedString(summary) { it.summary },
|
||||
authorEmail = authorEmail,
|
||||
@ -157,7 +158,7 @@ private fun PackageV1.toVersionV2(
|
||||
packageAntiFeatures: List<String>,
|
||||
): VersionV2 = VersionV2(
|
||||
added = added ?: 0L,
|
||||
file = FileV2(
|
||||
file = ApkFileV2(
|
||||
name = "/$apkName",
|
||||
sha256 = hash,
|
||||
size = size,
|
||||
|
@ -1,9 +1,9 @@
|
||||
package com.looker.droidify.sync.common
|
||||
|
||||
import android.content.Context
|
||||
import com.looker.droidify.utility.common.cache.Cache
|
||||
import com.looker.droidify.domain.model.Repo
|
||||
import com.looker.droidify.network.Downloader
|
||||
import com.looker.droidify.utility.common.cache.Cache
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
@ -16,23 +16,25 @@ suspend fun Downloader.downloadIndex(
|
||||
url: String,
|
||||
diff: Boolean = false,
|
||||
): File = withContext(Dispatchers.IO) {
|
||||
val tempFile = Cache.getIndexFile(context, "repo_${repo.id}_$fileName")
|
||||
val indexFile = Cache.getIndexFile(context, "repo_${repo.id}_$fileName")
|
||||
downloadToFile(
|
||||
url = url,
|
||||
target = tempFile,
|
||||
target = indexFile,
|
||||
headers = {
|
||||
if (repo.shouldAuthenticate) {
|
||||
with(requireNotNull(repo.authentication)) {
|
||||
authentication(
|
||||
repo.authentication.username,
|
||||
repo.authentication.password
|
||||
username = username,
|
||||
password = password,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (repo.versionInfo.timestamp > 0L && !diff) {
|
||||
ifModifiedSince(Date(repo.versionInfo.timestamp))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
tempFile
|
||||
indexFile
|
||||
}
|
||||
|
||||
const val INDEX_V1_NAME = "index-v1.jar"
|
||||
|
@ -1,12 +1,11 @@
|
||||
package com.looker.droidify.sync.v2
|
||||
|
||||
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.Repo
|
||||
import com.looker.droidify.network.Downloader
|
||||
import com.looker.droidify.sync.Parser
|
||||
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.INDEX_V2_NAME
|
||||
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.IndexV2
|
||||
import com.looker.droidify.sync.v2.model.IndexV2Diff
|
||||
import com.looker.droidify.utility.common.cache.Cache
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -51,7 +52,7 @@ class EntrySyncable(
|
||||
context = context,
|
||||
repo = repo,
|
||||
url = repo.address.removeSuffix("/") + "/$ENTRY_V2_NAME",
|
||||
fileName = ENTRY_V2_NAME
|
||||
fileName = ENTRY_V2_NAME,
|
||||
)
|
||||
val (fingerprint, entry) = parser.parse(jar, repo)
|
||||
jar.delete()
|
||||
@ -61,7 +62,6 @@ class EntrySyncable(
|
||||
val indexPath = repo.address.removeSuffix("/") + index.name
|
||||
val indexFile = Cache.getIndexFile(context, "repo_${repo.id}_$INDEX_V2_NAME")
|
||||
val indexV2 = if (index != entry.index && indexFile.exists()) {
|
||||
// example https://apt.izzysoft.de/fdroid/repo/diff/1725372028000.json
|
||||
val diffFile = downloader.downloadIndex(
|
||||
context = context,
|
||||
repo = repo,
|
||||
@ -69,17 +69,12 @@ class EntrySyncable(
|
||||
fileName = "diff_${repo.versionInfo.timestamp}.json",
|
||||
diff = true,
|
||||
)
|
||||
// TODO: Maybe parse in parallel
|
||||
diffParser.parse(diffFile, repo).second.let {
|
||||
val diff = async { diffParser.parse(diffFile, repo).second }
|
||||
val oldIndex = async { indexParser.parse(indexFile, repo).second }
|
||||
diff.await().patchInto(oldIndex.await()) { index ->
|
||||
diffFile.delete()
|
||||
it.patchInto(
|
||||
indexParser.parse(
|
||||
indexFile,
|
||||
repo
|
||||
).second) { index ->
|
||||
Json.encodeToStream(index, indexFile.outputStream())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// example https://apt.izzysoft.de/fdroid/repo/index-v2.json
|
||||
val newIndexFile = downloader.downloadIndex(
|
||||
|
@ -12,3 +12,10 @@ data class FileV2(
|
||||
val sha256: String? = 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
|
||||
|
||||
import androidx.core.os.LocaleListCompat
|
||||
|
||||
typealias LocalizedString = Map<String, String>
|
||||
typealias NullableLocalizedString = Map<String, String?>
|
||||
typealias LocalizedIcon = Map<String, FileV2>
|
||||
typealias LocalizedList = Map<String, List<String>>
|
||||
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,
|
||||
lastUpdated = metadata?.lastUpdated ?: 0L,
|
||||
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
|
||||
?.mapNotNull { (key, value) -> value?.let { key to value } }?.toMap(),
|
||||
description = metadata?.description
|
||||
@ -116,7 +116,7 @@ data class PackageV2Diff(
|
||||
|
||||
@Serializable
|
||||
data class MetadataV2(
|
||||
val name: LocalizedString? = null,
|
||||
val name: LocalizedString,
|
||||
val summary: LocalizedString? = null,
|
||||
val description: LocalizedString? = null,
|
||||
val icon: LocalizedIcon? = null,
|
||||
@ -129,7 +129,7 @@ data class MetadataV2(
|
||||
val bitcoin: String? = null,
|
||||
val categories: List<String> = emptyList(),
|
||||
val changelog: String? = null,
|
||||
val donate: List<String> = emptyList(),
|
||||
val donate: List<String>? = null,
|
||||
val featureGraphic: LocalizedIcon? = null,
|
||||
val flattrID: String? = null,
|
||||
val issueTracker: String? = null,
|
||||
@ -183,25 +183,25 @@ data class MetadataV2Diff(
|
||||
@Serializable
|
||||
data class VersionV2(
|
||||
val added: Long,
|
||||
val file: FileV2,
|
||||
val file: ApkFileV2,
|
||||
val src: FileV2? = null,
|
||||
val whatsNew: LocalizedString = emptyMap(),
|
||||
val manifest: ManifestV2,
|
||||
val antiFeatures: Map<String, LocalizedString> = emptyMap(),
|
||||
val antiFeatures: Map<Tag, AntiFeatureReason> = emptyMap(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VersionV2Diff(
|
||||
val added: Long? = null,
|
||||
val file: FileV2? = null,
|
||||
val file: ApkFileV2? = null,
|
||||
val src: FileV2? = null,
|
||||
val whatsNew: LocalizedString? = null,
|
||||
val manifest: ManifestV2? = null,
|
||||
val antiFeatures: Map<String, LocalizedString>? = null,
|
||||
val antiFeatures: Map<Tag, AntiFeatureReason>? = null,
|
||||
) {
|
||||
fun toVersion() = VersionV2(
|
||||
added = added ?: 0,
|
||||
file = file ?: FileV2(""),
|
||||
file = file ?: ApkFileV2("", "", -1L),
|
||||
src = src ?: FileV2(""),
|
||||
whatsNew = whatsNew ?: emptyMap(),
|
||||
manifest = manifest ?: ManifestV2(
|
||||
|
@ -10,8 +10,8 @@ data class RepoV2(
|
||||
val icon: LocalizedIcon? = null,
|
||||
val name: LocalizedString = emptyMap(),
|
||||
val description: LocalizedString = emptyMap(),
|
||||
val antiFeatures: Map<String, AntiFeatureV2> = emptyMap(),
|
||||
val categories: Map<String, CategoryV2> = emptyMap(),
|
||||
val antiFeatures: Map<Tag, AntiFeatureV2> = emptyMap(),
|
||||
val categories: Map<DefaultName, CategoryV2> = emptyMap(),
|
||||
val mirrors: List<MirrorV2> = emptyList(),
|
||||
val timestamp: Long,
|
||||
)
|
||||
@ -22,8 +22,8 @@ data class RepoV2Diff(
|
||||
val icon: LocalizedIcon? = null,
|
||||
val name: LocalizedString? = null,
|
||||
val description: LocalizedString? = null,
|
||||
val antiFeatures: Map<String, AntiFeatureV2?>? = null,
|
||||
val categories: Map<String, CategoryV2?>? = null,
|
||||
val antiFeatures: Map<Tag, AntiFeatureV2?>? = null,
|
||||
val categories: Map<DefaultName, CategoryV2?>? = null,
|
||||
val mirrors: List<MirrorV2>? = null,
|
||||
val timestamp: Long,
|
||||
) {
|
||||
@ -69,7 +69,7 @@ data class RepoV2Diff(
|
||||
data class MirrorV2(
|
||||
val url: String,
|
||||
val isPrimary: Boolean? = null,
|
||||
val location: String? = null
|
||||
val countryCode: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
@ -31,6 +31,7 @@ import com.google.android.material.shape.ShapeAppearanceModel
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.looker.droidify.R
|
||||
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.model.SortOrder
|
||||
import com.looker.droidify.model.ProductItem
|
||||
@ -212,7 +213,7 @@ class TabsFragment : ScreenFragment() {
|
||||
.setIcon(toolbar.context.getMutatedIcon(R.drawable.ic_sort))
|
||||
.let { menu ->
|
||||
menu.item.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
||||
val menuItems = SortOrder.entries.map { sortOrder ->
|
||||
val menuItems = supportedSortOrders().map { sortOrder ->
|
||||
menu.add(context.sortOrderName(sortOrder))
|
||||
.setOnMenuItemClickListener {
|
||||
viewModel.setSortOrder(sortOrder)
|
||||
@ -224,9 +225,7 @@ class TabsFragment : ScreenFragment() {
|
||||
}
|
||||
|
||||
favouritesItem = add(1, 0, 0, stringRes.favourites)
|
||||
.setIcon(
|
||||
toolbar.context.getMutatedIcon(R.drawable.ic_favourite_checked)
|
||||
)
|
||||
.setIcon(toolbar.context.getMutatedIcon(R.drawable.ic_favourite_checked))
|
||||
.setOnMenuItemClickListener {
|
||||
view.post { mainActivity.navigateFavourites() }
|
||||
true
|
||||
|
@ -7,6 +7,7 @@ import com.looker.droidify.database.Database
|
||||
import com.looker.droidify.datastore.SettingsRepository
|
||||
import com.looker.droidify.datastore.get
|
||||
import com.looker.droidify.datastore.model.SortOrder
|
||||
import com.looker.droidify.domain.model.Fingerprint
|
||||
import com.looker.droidify.model.ProductItem
|
||||
import com.looker.droidify.ui.tabsFragment.TabsFragment.BackAction
|
||||
import com.looker.droidify.utility.common.extension.asStateFlow
|
||||
@ -20,7 +21,9 @@ import javax.inject.Inject
|
||||
@HiltViewModel
|
||||
class TabsViewModel @Inject constructor(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val savedStateHandle: SavedStateHandle
|
||||
private val indexDao: IndexDao,
|
||||
private val syncable: Syncable<IndexV2>,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel() {
|
||||
|
||||
val currentSection =
|
||||
@ -37,7 +40,7 @@ class TabsViewModel @Inject constructor(
|
||||
val sections =
|
||||
combine(
|
||||
Database.CategoryAdapter.getAllStream(),
|
||||
Database.RepositoryAdapter.getEnabledStream()
|
||||
Database.RepositoryAdapter.getEnabledStream(),
|
||||
) { categories, repos ->
|
||||
val productCategories = categories
|
||||
.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 {
|
||||
private const val STATE_SECTION = "section"
|
||||
}
|
||||
|
@ -1,5 +1,18 @@
|
||||
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> {
|
||||
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 kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.File
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
@ -32,19 +33,19 @@ class KtorDownloaderTest {
|
||||
|
||||
private val downloader = KtorDownloader(engine, dispatcher)
|
||||
|
||||
@org.junit.jupiter.api.Test
|
||||
@Test
|
||||
fun `head call success`() = runTest(dispatcher) {
|
||||
val response = downloader.headCall("https://success.com")
|
||||
assertIs<NetworkResponse.Success>(response)
|
||||
}
|
||||
|
||||
@org.junit.jupiter.api.Test
|
||||
@Test
|
||||
fun `head call if path not found`() = runTest(dispatcher) {
|
||||
val response = downloader.headCall("https://notfound.com")
|
||||
assertIs<NetworkResponse.Error.Http>(response)
|
||||
}
|
||||
|
||||
@org.junit.jupiter.api.Test
|
||||
@Test
|
||||
fun `save text to file success`() = runTest(dispatcher) {
|
||||
val file = File.createTempFile("test", "success")
|
||||
val response = downloader.downloadToFile(
|
||||
@ -55,7 +56,7 @@ class KtorDownloaderTest {
|
||||
assertEquals("success", file.readText())
|
||||
}
|
||||
|
||||
@org.junit.jupiter.api.Test
|
||||
@Test
|
||||
fun `save text to read-only file`() = runTest(dispatcher) {
|
||||
val file = File.createTempFile("test", "success")
|
||||
file.setReadOnly()
|
||||
@ -66,7 +67,7 @@ class KtorDownloaderTest {
|
||||
assertIs<NetworkResponse.Error.IO>(response)
|
||||
}
|
||||
|
||||
@org.junit.jupiter.api.Test
|
||||
@Test
|
||||
fun `save text to file with slow connection`() = runTest(dispatcher) {
|
||||
val file = File.createTempFile("test", "success")
|
||||
val response = downloader.downloadToFile(
|
||||
@ -76,7 +77,7 @@ class KtorDownloaderTest {
|
||||
assertIs<NetworkResponse.Error.ConnectionTimeout>(response)
|
||||
}
|
||||
|
||||
@org.junit.jupiter.api.Test
|
||||
@Test
|
||||
fun `save text to file with socket error`() = runTest(dispatcher) {
|
||||
val file = File.createTempFile("test", "success")
|
||||
val response = downloader.downloadToFile(
|
||||
@ -86,7 +87,7 @@ class KtorDownloaderTest {
|
||||
assertIs<NetworkResponse.Error.SocketTimeout>(response)
|
||||
}
|
||||
|
||||
@org.junit.jupiter.api.Test
|
||||
@Test
|
||||
fun `save text to file if not modifier`() = runTest(dispatcher) {
|
||||
val file = File.createTempFile("test", "success")
|
||||
val response = downloader.downloadToFile(
|
||||
@ -100,7 +101,7 @@ class KtorDownloaderTest {
|
||||
assertEquals("", file.readText())
|
||||
}
|
||||
|
||||
@org.junit.jupiter.api.Test
|
||||
@Test
|
||||
fun `save text to file with wrong authentication`() =
|
||||
runTest(dispatcher) {
|
||||
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.kotlin.serialization) 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.jvmargs=-Xmx6g -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g
|
||||
android.useAndroidX=true
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.configuration-cache=true
|
||||
android.defaults.buildfeatures.resvalues=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" }
|
||||
recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recycler-view" }
|
||||
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-rules = { group = "androidx.test", name = "rules", version.ref = "test-rules" }
|
||||
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-testing = { group = "androidx.work", name = "work-testing", version.ref = "work" }
|
||||
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" }
|
||||
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-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-ext-work = { group = "androidx.hilt", name = "hilt-work", 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-ktx = { group = "androidx.room", name = "room-ktx", 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-provider = { group = "dev.rikka.shizuku", name = "provider", version.ref = "shizuku" }
|
||||
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" }
|
||||
|
||||
[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"]
|
||||
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