diff --git a/app/schemas/com.looker.droidify.data.local.DroidifyDatabase/1.json b/app/schemas/com.looker.droidify.data.local.DroidifyDatabase/1.json index c1739d2a..a3e29658 100644 --- a/app/schemas/com.looker.droidify.data.local.DroidifyDatabase/1.json +++ b/app/schemas/com.looker.droidify.data.local.DroidifyDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "099d07ab258fe12cb0a660545bd36e63", + "identityHash": "e64dac21017b34894f6d9d7890184178", "entities": [ { "tableName": "anti_feature", @@ -142,7 +142,7 @@ }, { "tableName": "authentication", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`password` TEXT NOT NULL, `username` TEXT NOT NULL, `initializationVector` TEXT NOT NULL, `repoId` INTEGER NOT NULL, PRIMARY KEY(`repoId`), FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`password` TEXT NOT NULL, `username` TEXT NOT NULL, `initializationVector` TEXT NOT NULL, `repoId` INTEGER NOT NULL, PRIMARY KEY(`repoId`), FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "password", @@ -179,7 +179,7 @@ "foreignKeys": [ { "table": "repository", - "onDelete": "NO ACTION", + "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ "repoId" @@ -1078,7 +1078,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '099d07ab258fe12cb0a660545bd36e63')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e64dac21017b34894f6d9d7890184178')" ] } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/looker/droidify/RoomTesting.kt b/app/src/androidTest/kotlin/com/looker/droidify/RoomTesting.kt index 7af09d74..29ef11a2 100644 --- a/app/src/androidTest/kotlin/com/looker/droidify/RoomTesting.kt +++ b/app/src/androidTest/kotlin/com/looker/droidify/RoomTesting.kt @@ -3,32 +3,25 @@ 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.database.Database +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.index.RepositoryUpdater -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.JsonParser -import com.looker.droidify.sync.common.assets -import com.looker.droidify.sync.common.benchmark 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.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule -import java.io.File import javax.inject.Inject -import kotlin.system.measureTimeMillis import kotlin.test.Test +import kotlin.test.assertTrue @HiltAndroidTest class RoomTesting { @@ -53,98 +46,60 @@ class RoomTesting { @Before fun before() = runTest { hiltRule.inject() - setupLegacy() + launch { + val izzy = izzyLegacy.toRepo(1) + val izzyFile = FakeDownloader.downloadIndex(context, izzy, "i2", "index-v2.json") + val izzyIndex = + JsonParser.decodeFromString(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(fdroidFile.readBytes().decodeToString()) +// indexDao.insertIndex( +// fingerprint = fdroid.fingerprint!!, +// index = fdroidIndex, +// expectedRepoId = fdroid.id, +// ) +// } } @Test - fun roomBenchmark() = runTest { - val izzy = izzyLegacy.toRepo(1) - val insert = benchmark(1) { - val v2File = FakeDownloader - .downloadIndex(context, izzy, "izzy-v2", "index-v2.json") - measureTimeMillis { - val index = JsonParser.decodeFromString( - v2File.readBytes().decodeToString(), - ) - indexDao.insertIndex( - fingerprint = izzy.fingerprint!!, - index = index, - expectedRepoId = izzy.id, - ) - } + 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 fdroid = fdroidLegacy.toRepo(2) - val insertFDroid = benchmark(1) { - val v2File = FakeDownloader - .downloadIndex(context, fdroid, "fdroid-v2", "fdroid-index-v2.json") - measureTimeMillis { - val index = JsonParser.decodeFromString( - v2File.readBytes().decodeToString(), - ) - indexDao.insertIndex( - fingerprint = fdroid.fingerprint!!, - index = index, - expectedRepoId = fdroid.id, - ) - } + + 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 } - val query = appDao.queryAppEntity("com.looker.droidify") - println(query.first().joinToString("\n")) - println(insert) - println(insertFDroid) } @Test - fun legacyBenchmark() { - val insert = benchmark(1) { - val createFile = File.createTempFile("index", "entry") - val mergerFile = File.createTempFile("index", "merger") - val jarStream = assets("izzy_index_v1.jar") - jarStream.copyTo(createFile.outputStream()) - measureTimeMillis { - RepositoryUpdater.processFile( - context = context, - repository = izzyLegacy, - indexType = IndexType.INDEX_V1, - unstable = false, - file = createFile, - mergerFile = mergerFile, - lastModified = "", - entityTag = "", - callback = { _, _, _ -> }, - ) - createFile.delete() - mergerFile.delete() - } - } - val insertFDroid = benchmark(1) { - val createFile = File.createTempFile("index", "entry") - val mergerFile = File.createTempFile("index", "merger") - val jarStream = assets("fdroid_index_v1.jar") - jarStream.copyTo(createFile.outputStream()) - measureTimeMillis { - RepositoryUpdater.processFile( - context = context, - repository = fdroidLegacy, - indexType = IndexType.INDEX_V1, - unstable = false, - file = createFile, - mergerFile = mergerFile, - lastModified = "", - entityTag = "", - callback = { _, _, _ -> }, - ) - createFile.delete() - mergerFile.delete() - } - } - println(insert) - println(insertFDroid) - } - - private fun setupLegacy() { - Database.init(context) - RepositoryUpdater.init(CoroutineScope(Dispatchers.Default), FakeDownloader) + 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"), + ) } } diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/dao/AppDao.kt b/app/src/main/kotlin/com/looker/droidify/data/local/dao/AppDao.kt index d729f328..f032ee65 100644 --- a/app/src/main/kotlin/com/looker/droidify/data/local/dao/AppDao.kt +++ b/app/src/main/kotlin/com/looker/droidify/data/local/dao/AppDao.kt @@ -18,67 +18,129 @@ import kotlinx.coroutines.flow.Flow @Dao interface AppDao { - @RawQuery(observedEntities = [ - AppEntity::class, - VersionEntity::class, - CategoryAppRelation::class, - AntiFeatureAppRelation::class, - ]) - fun _rawQueryAppEntities(query: SimpleSQLiteQuery): Flow> + @RawQuery( + observedEntities = [ + AppEntity::class, + VersionEntity::class, + CategoryAppRelation::class, + AntiFeatureAppRelation::class, + ], + ) + fun _rawStreamAppEntities(query: SimpleSQLiteQuery): Flow> + + @RawQuery + suspend fun _rawQueryAppEntities(query: SimpleSQLiteQuery): List - /** - * @param category: get by default name and use the boolean to ignore the app category - * @param antiFeature: get by tag and use the boolean to ignore the anti feature - * - * Add `canUpdate` and `isInstalled` is the next layer - * */ fun stream( + sortOrder: SortOrder, + searchQuery: String? = null, + repoId: Int? = null, + categoriesToInclude: List? = null, + categoriesToExclude: List? = null, + antiFeaturesToInclude: List? = null, + antiFeaturesToExclude: List? = null, + ): Flow> = _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? = null, + categoriesToExclude: List? = null, + antiFeaturesToInclude: List? = null, + antiFeaturesToExclude: List? = null, + ): List = _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?, - category: Pair?, - antiFeature: Pair?, - ): Flow> { + categoriesToInclude: List?, + categoriesToExclude: List?, + antiFeaturesToInclude: List?, + antiFeaturesToExclude: List?, + ): SimpleSQLiteQuery { val args = arrayListOf() val query = buildString(1024) { - append("SELECT app.* FROM app") + 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.appId") - append(" LEFT JOIN anti_feature_app_relation ON app.id = anti_feature_app_relation.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 (category != null) { - val (name, hide) = category - if (!hide) { - append(" AND category_app_relation.defaultName = ?") - } else { - append(" AND category_app_relation.defaultName != ?") - } - args.add(name) + if (categoriesToInclude != null) { + append(" AND category_app_relation.defaultName IN (") + append(categoriesToInclude.joinToString(", ") { "?" }) + append(")") + args.addAll(categoriesToInclude) } - if (antiFeature != null) { - val (tag, hide) = antiFeature - if (!hide) { - append(" AND anti_feature_app_relation.tag = ?") - } else { - append(" AND anti_feature_app_relation.tag != ?") - } - args.add(tag) + 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 ?)") + 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 + 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) + ") @@ -86,12 +148,8 @@ interface AppDao { args.addAll(listOf(searchPattern, searchPattern, searchPattern, searchPattern)) } - if (searchQuery == null) { - append(" ORDER BY ") - } - when (sortOrder) { - SortOrder.UPDATED -> append("app,lastUpdated DESC, ") + SortOrder.UPDATED -> append("app.lastUpdated DESC, ") SortOrder.ADDED -> append("app.added DESC, ") SortOrder.SIZE -> append("version.apk_size DESC, ") SortOrder.NAME -> Unit @@ -99,7 +157,7 @@ interface AppDao { append("app.name COLLATE LOCALIZED ASC") } - return _rawQueryAppEntities(SimpleSQLiteQuery(query, args.toTypedArray())) + return SimpleSQLiteQuery(query, args.toTypedArray()) } @Query(