From a5ec22fa6c79c575e3326e70fabf5ee730c48381 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 21 Sep 2023 14:30:53 +0200 Subject: [PATCH] [db] add queries for getting app list items by repo --- .../org/fdroid/database/AppListItemsTest.kt | 68 +++++++++++++++++++ .../main/java/org/fdroid/database/AppDao.kt | 60 ++++++++++++++++ .../java/org/fdroid/repo/RepoAdderTest.kt | 7 +- 3 files changed, 132 insertions(+), 3 deletions(-) diff --git a/libs/database/src/dbTest/java/org/fdroid/database/AppListItemsTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/AppListItemsTest.kt index 3f8a29bcc..a989cc401 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/AppListItemsTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/AppListItemsTest.kt @@ -161,6 +161,74 @@ internal class AppListItemsTest : AppTest() { } } + @Test + fun testSearchQueryPerRepo() { + val app1 = app1.copy(name = mapOf("en-US" to "One"), summary = mapOf("en-US" to "Onearry")) + val app2 = app2.copy(name = mapOf("en-US" to "Two"), summary = mapOf("de" to "Zfassung")) + val app3a = app3.copy(name = mapOf("de-DE" to "Drei"), summary = mapOf("de" to "Zfassung")) + val app3b = app3.copy(name = mapOf("en-US" to "Three"), summary = mapOf("en" to "summary")) + // insert three apps in a random order + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + val repoId3 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId1, packageName1, app1, locales) + appDao.insert(repoId2, packageName2, app2, locales) + appDao.insert(repoId2, packageName3, app3a, locales) + appDao.insert(repoId3, packageName3, app3b, locales) + + // one of the apps is installed + @Suppress("DEPRECATION") + val packageInfo2 = PackageInfo().apply { + packageName = packageName2 + versionName = getRandomString() + versionCode = Random.nextInt(1, Int.MAX_VALUE) + } + every { pm.getInstalledPackages(0) } returns listOf(packageInfo2) + + // get all apps in repo2 sorted by last updated + appDao.getAppListItems(pm, repoId2, null, LAST_UPDATED).getOrFail().let { apps -> + assertEquals(2, apps.size) + assertEquals(app3a, apps[0]) + assertEquals(app2, apps[1]) + assertEquals(PackageInfoCompat.getLongVersionCode(packageInfo2), + apps[1].installedVersionCode) + assertEquals(packageInfo2.versionName, apps[1].installedVersionName) + } + + // get all apps in repo2 searching for summary + appDao.getAppListItems(pm, repoId2, "Zfassung", NAME).getOrFail().let { apps -> + assertEquals(2, apps.size) + val sortedApps = apps.sortedBy { it.lastUpdated } + assertEquals(app2, sortedApps[0]) + assertEquals(app3a, sortedApps[1]) + assertEquals(PackageInfoCompat.getLongVersionCode(packageInfo2), + sortedApps[0].installedVersionCode) + assertEquals(packageInfo2.versionName, sortedApps[0].installedVersionName) + } + + // get first app by searching for summary + appDao.getAppListItems(pm, repoId1, "One", LAST_UPDATED).getOrFail().let { apps -> + assertEquals(1, apps.size) + assertEquals(app1, apps[0]) + } + + // get third app by searching for summary in repo3 only + appDao.getAppListItems(pm, repoId3, "summary", LAST_UPDATED).getOrFail().let { apps -> + assertEquals(1, apps.size) + assertEquals(app3b, apps[0]) + } + + // empty search for unknown category + appDao.getAppListItems(pm, 1337L, "Zfassung", LAST_UPDATED).getOrFail().let { apps -> + assertEquals(0, apps.size) + } + + // empty search for unknown search term + appDao.getAppListItems(pm, repoId2, "foo bar", LAST_UPDATED).getOrFail().let { apps -> + assertEquals(0, apps.size) + } + } + @Test fun testMalformedSearchQuery() { every { pm.getInstalledPackages(0) } returns emptyList() diff --git a/libs/database/src/main/java/org/fdroid/database/AppDao.kt b/libs/database/src/main/java/org/fdroid/database/AppDao.kt index 2d2f5ea3a..5f5e17c37 100644 --- a/libs/database/src/main/java/org/fdroid/database/AppDao.kt +++ b/libs/database/src/main/java/org/fdroid/database/AppDao.kt @@ -106,6 +106,16 @@ public interface AppDao { sortOrder: AppListSortOrder, ): LiveData> + /** + * Like [getAppListItems], but further filter items by the given [repoId]. + */ + public fun getAppListItems( + packageManager: PackageManager, + repoId: Long, + searchQuery: String?, + sortOrder: AppListSortOrder, + ): LiveData> + public fun getInstalledAppListItems(packageManager: PackageManager): LiveData> public fun getNumberOfAppsInCategory(category: String): Int @@ -389,6 +399,18 @@ internal interface AppDaoInt : AppDao { } else getAppListItems(category, escapeQuery(searchQuery)).map(packageManager) } + override fun getAppListItems( + packageManager: PackageManager, + repoId: Long, + searchQuery: String?, + sortOrder: AppListSortOrder, + ): LiveData> { + return if (searchQuery.isNullOrEmpty()) when (sortOrder) { + LAST_UPDATED -> getAppListItemsByLastUpdated(repoId).map(packageManager) + NAME -> getAppListItemsByName(repoId).map(packageManager) + } else getAppListItems(repoId, escapeQuery(searchQuery)).map(packageManager) + } + private fun escapeQuery(searchQuery: String): String { val sanitized = searchQuery.replace(Regex.fromLiteral("\""), "\"\"") return "\"*$sanitized*\"" @@ -439,6 +461,24 @@ internal interface AppDaoInt : AppDao { GROUP BY packageName HAVING MAX(pref.weight)""") fun getAppListItems(category: String, searchQuery: String): LiveData> + /** + * Warning: Run [escapeQuery] on the given [searchQuery] before. + * + * This query is structured differently than the sister query above, + * because of abysmal performance that we couldn't explain. + */ + @Transaction + @Query(""" + SELECT repoId, packageName, app.localizedName, app.localizedSummary, app.lastUpdated, + version.antiFeatures, app.isCompatible, app.preferredSigner + FROM ${AppMetadata.TABLE} AS app + LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName) + WHERE repoId = :repoId AND app.rowid IN ( + SELECT rowid FROM ${AppMetadataFts.TABLE} + WHERE repoId = :repoId AND ${AppMetadataFts.TABLE} MATCH :searchQuery + )""") + fun getAppListItems(repoId: Long, searchQuery: String): LiveData> + @Transaction @Query(""" SELECT repoId, packageName, localizedName, localizedSummary, app.lastUpdated, @@ -487,6 +527,26 @@ internal interface AppDaoInt : AppDao { ORDER BY localizedName COLLATE NOCASE ASC""") fun getAppListItemsByName(category: String): LiveData> + @Transaction + @Query(""" + SELECT repoId, packageName, localizedName, localizedSummary, app.lastUpdated, + version.antiFeatures, app.isCompatible, app.preferredSigner + FROM ${AppMetadata.TABLE} AS app + LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName) + WHERE repoId = :repoId + ORDER BY app.lastUpdated DESC""") + fun getAppListItemsByLastUpdated(repoId: Long): LiveData> + + @Transaction + @Query(""" + SELECT repoId, packageName, localizedName, localizedSummary, app.lastUpdated, + version.antiFeatures, app.isCompatible, app.preferredSigner + FROM ${AppMetadata.TABLE} AS app + LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName) + WHERE repoId = :repoId + ORDER BY localizedName COLLATE NOCASE ASC""") + fun getAppListItemsByName(repoId: Long): LiveData> + /** * Warning: Can not be called with more than 999 [packageNames]. */ diff --git a/libs/database/src/test/java/org/fdroid/repo/RepoAdderTest.kt b/libs/database/src/test/java/org/fdroid/repo/RepoAdderTest.kt index 7f714c367..cbf126665 100644 --- a/libs/database/src/test/java/org/fdroid/repo/RepoAdderTest.kt +++ b/libs/database/src/test/java/org/fdroid/repo/RepoAdderTest.kt @@ -16,6 +16,7 @@ import io.mockk.just import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.slot +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest @@ -652,10 +653,10 @@ internal class RepoAdderTest { repoAdder.addRepoState.test { assertIs(awaitItem()) - launch { + launch(Dispatchers.IO) { // FIXME executing this block may emit items too fast, so we might miss one - // causing flaky tests. 50ms may fix it, let's see. - delay(50) + // causing flaky tests. A short delay may fix it, let's see. + delay(250) block() }