[db] New AppOverviewItems AppDao methods

This commit is contained in:
Torsten Grote committed 2025-11-04 08:50:52 -03:00
1 parent a45c9763e5
commit 290cfcee34
2 files changed
+256 -32

No files matched your search

@@ -1,16 +1,20 @@
package org.fdroid.database package org.fdroid.database
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import org.fdroid.LocaleChooser.getBestLocale import org.fdroid.LocaleChooser.getBestLocale
import org.fdroid.database.TestUtils.getOrAwaitValue import org.fdroid.database.TestUtils.getOrAwaitValue
import org.fdroid.database.TestUtils.getOrFail import org.fdroid.database.TestUtils.getOrFail
import org.fdroid.index.v2.MetadataV2 import org.fdroid.index.v2.MetadataV2
import org.fdroid.test.TestAppUtils.getRandomMetadataV2
import org.fdroid.test.TestRepoUtils.getRandomRepo import org.fdroid.test.TestRepoUtils.getRandomRepo
import org.fdroid.test.TestUtils.getRandomString
import org.fdroid.test.TestVersionUtils.getRandomPackageVersionV2 import org.fdroid.test.TestVersionUtils.getRandomPackageVersionV2
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
import kotlin.test.assertNull import kotlin.test.assertNull
import kotlin.test.assertTrue import kotlin.test.assertTrue
@@ -19,13 +23,13 @@ import kotlin.test.assertTrue
internal class AppOverviewItemsTest : AppTest() { internal class AppOverviewItemsTest : AppTest() {
@Test @Test
fun testAntiFeatures() { fun testAntiFeatures() = runBlocking {
// insert one apps with without version // insert one app with without version
val repoId = repoDao.insertOrReplace(getRandomRepo()) val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, app1, locales) appDao.insert(repoId, packageName, app1, locales)
// without version, anti-features are empty // without version, anti-features are empty
appDao.getAppOverviewItems().getOrFail().let { apps -> getItems().forEach { apps ->
assertEquals(1, apps.size) assertEquals(1, apps.size)
assertNull(apps[0].antiFeatures) assertNull(apps[0].antiFeatures)
} }
@@ -33,7 +37,7 @@ internal class AppOverviewItemsTest : AppTest() {
// with one version, the app has those anti-features // with one version, the app has those anti-features
val version = getRandomPackageVersionV2(versionCode = 42) val version = getRandomPackageVersionV2(versionCode = 42)
versionDao.insert(repoId, packageName, "1", version, true) versionDao.insert(repoId, packageName, "1", version, true)
appDao.getAppOverviewItems().getOrFail().let { apps -> getItems().forEach { apps ->
assertEquals(1, apps.size) assertEquals(1, apps.size)
assertEquals(version.antiFeatures, apps[0].antiFeatures) assertEquals(version.antiFeatures, apps[0].antiFeatures)
} }
@@ -41,7 +45,7 @@ internal class AppOverviewItemsTest : AppTest() {
// with two versions, the app has the anti-features of the highest version // with two versions, the app has the anti-features of the highest version
val version2 = getRandomPackageVersionV2(versionCode = 23) val version2 = getRandomPackageVersionV2(versionCode = 23)
versionDao.insert(repoId, packageName, "2", version2, true) versionDao.insert(repoId, packageName, "2", version2, true)
appDao.getAppOverviewItems().getOrFail().let { apps -> getItems().forEach { apps ->
assertEquals(1, apps.size) assertEquals(1, apps.size)
assertEquals(version.antiFeatures, apps[0].antiFeatures) assertEquals(version.antiFeatures, apps[0].antiFeatures)
} }
@@ -49,20 +53,20 @@ internal class AppOverviewItemsTest : AppTest() {
// with three versions, the app has the anti-features of the highest version // with three versions, the app has the anti-features of the highest version
val version3 = getRandomPackageVersionV2(versionCode = 1337) val version3 = getRandomPackageVersionV2(versionCode = 1337)
versionDao.insert(repoId, packageName, "3", version3, true) versionDao.insert(repoId, packageName, "3", version3, true)
appDao.getAppOverviewItems().getOrFail().let { apps -> getItems().forEach { apps ->
assertEquals(1, apps.size) assertEquals(1, apps.size)
assertEquals(version3.antiFeatures, apps[0].antiFeatures) assertEquals(version3.antiFeatures, apps[0].antiFeatures)
} }
} }
@Test @Test
fun testIcons() { fun testIcons() = runBlocking {
// insert one app // insert one app
val repoId = repoDao.insertOrReplace(getRandomRepo()) val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, app1, locales) appDao.insert(repoId, packageName, app1, locales)
// icon is returned correctly // icon is returned correctly
appDao.getAppOverviewItems().getOrFail().let { apps -> getItems().forEach { apps ->
assertEquals(1, apps.size) assertEquals(1, apps.size)
assertEquals(app1.icon.getBestLocale(locales), apps[0].getIcon(locales)) assertEquals(app1.icon.getBestLocale(locales), apps[0].getIcon(locales))
} }
@@ -72,14 +76,14 @@ internal class AppOverviewItemsTest : AppTest() {
appDao.insert(repoId2, packageName, app2, locales) appDao.insert(repoId2, packageName, app2, locales)
// app is still returned as before // app is still returned as before
appDao.getAppOverviewItems().getOrFail().let { apps -> getItems().forEach { apps ->
assertEquals(1, apps.size) assertEquals(1, apps.size)
assertEquals(app1.icon.getBestLocale(locales), apps[0].getIcon(locales)) assertEquals(app1.icon.getBestLocale(locales), apps[0].getIcon(locales))
} }
// after preferring second repo, icon is returned from app in second repo // after preferring second repo, icon is returned from app in second repo
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2)) appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2))
appDao.getAppOverviewItems().getOrFail().let { apps -> getItems().forEach { apps ->
assertEquals(1, apps.size) assertEquals(1, apps.size)
assertEquals(app2.icon.getBestLocale(locales), apps[0].getIcon(locales)) assertEquals(app2.icon.getBestLocale(locales), apps[0].getIcon(locales))
} }
@@ -99,17 +103,18 @@ internal class AppOverviewItemsTest : AppTest() {
} }
@Test @Test
fun testIncompatibleFlag() { fun testIncompatibleFlag() = runBlocking {
// insert two apps // insert two apps
val repoId = repoDao.insertOrReplace(getRandomRepo()) val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales) appDao.insert(repoId, packageName1, app1, locales)
appDao.insert(repoId, packageName2, app2, locales) appDao.insert(repoId, packageName2, app2, locales)
// both apps are not compatible // both apps are not compatible
appDao.getAppOverviewItems().getOrFail().also { getItems().forEach { apps ->
assertEquals(2, it.size) assertEquals(2, apps.size)
}.forEach { apps.forEach {
assertFalse(it.isCompatible) assertFalse(it.isCompatible)
}
} }
// both apps, in the same category, are not compatible // both apps, in the same category, are not compatible
appDao.getAppOverviewItems("A").getOrFail().also { appDao.getAppOverviewItems("A").getOrFail().also {
@@ -128,10 +133,10 @@ internal class AppOverviewItemsTest : AppTest() {
appDao.updateCompatibility(repoId) appDao.updateCompatibility(repoId)
// now only one is not compatible // now only one is not compatible
appDao.getAppOverviewItems().getOrFail().also { getItems().forEach { apps ->
assertEquals(2, it.size) assertEquals(2, apps.size)
assertFalse(it[0].isCompatible) assertFalse(apps[0].isCompatible)
assertTrue(it[1].isCompatible) assertTrue(apps[1].isCompatible)
} }
appDao.getAppOverviewItems("A").getOrFail().also { appDao.getAppOverviewItems("A").getOrFail().also {
assertEquals(2, it.size) assertEquals(2, it.size)
@@ -143,14 +148,14 @@ internal class AppOverviewItemsTest : AppTest() {
} }
@Test @Test
fun testGetByRepoWeight() { fun testGetByRepoWeight() = runBlocking {
// insert one app with one version // insert one app with one version
val repoId = repoDao.insertOrReplace(getRandomRepo()) val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, app1, locales) appDao.insert(repoId, packageName, app1, locales)
versionDao.insert(repoId, packageName, "1", getRandomPackageVersionV2(2), true) versionDao.insert(repoId, packageName, "1", getRandomPackageVersionV2(2), true)
// app is returned correctly // app is returned correctly
appDao.getAppOverviewItems().getOrFail().let { apps -> getItems().forEach { apps ->
assertEquals(1, apps.size) assertEquals(1, apps.size)
assertEquals(app1, apps[0]) assertEquals(app1, apps[0])
} }
@@ -160,21 +165,21 @@ internal class AppOverviewItemsTest : AppTest() {
appDao.insert(repoId2, packageName, app2, locales) appDao.insert(repoId2, packageName, app2, locales)
// app is still returned as before, new repo doesn't override old one // app is still returned as before, new repo doesn't override old one
appDao.getAppOverviewItems().getOrFail().let { apps -> getItems().forEach { apps ->
assertEquals(1, apps.size) assertEquals(1, apps.size)
assertEquals(app1, apps[0]) assertEquals(app1, apps[0])
} }
// now second app from second repo is returned after preferring it explicitly // now second app from second repo is returned after preferring it explicitly
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2)) appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2))
appDao.getAppOverviewItems().getOrFail().let { apps -> getItems().forEach { apps ->
assertEquals(1, apps.size) assertEquals(1, apps.size)
assertEquals(app2, apps[0]) assertEquals(app2, apps[0])
} }
} }
@Test @Test
fun testGetByRepoPref() { fun testGetByRepoPref() = runBlocking {
// insert same app into three repos (repoId1 has highest weight) // insert same app into three repos (repoId1 has highest weight)
val repoId1 = repoDao.insertOrReplace(getRandomRepo()) val repoId1 = repoDao.insertOrReplace(getRandomRepo())
val repoId3 = repoDao.insertOrReplace(getRandomRepo()) val repoId3 = repoDao.insertOrReplace(getRandomRepo())
@@ -184,7 +189,7 @@ internal class AppOverviewItemsTest : AppTest() {
appDao.insert(repoId3, packageName, app3, locales) appDao.insert(repoId3, packageName, app3, locales)
// app is returned correctly from repo1 // app is returned correctly from repo1
appDao.getAppOverviewItems().getOrFail().let { apps -> getItems().forEach { apps ->
assertEquals(1, apps.size) assertEquals(1, apps.size)
assertEquals(app1, apps[0]) assertEquals(app1, apps[0])
} }
@@ -195,7 +200,7 @@ internal class AppOverviewItemsTest : AppTest() {
// prefer repo3 for this app // prefer repo3 for this app
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId3)) appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId3))
appDao.getAppOverviewItems().getOrFail().let { apps -> getItems().forEach { apps ->
assertEquals(1, apps.size) assertEquals(1, apps.size)
assertEquals(app3, apps[0]) assertEquals(app3, apps[0])
} }
@@ -206,7 +211,7 @@ internal class AppOverviewItemsTest : AppTest() {
// prefer repo2 for this app // prefer repo2 for this app
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2)) appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2))
appDao.getAppOverviewItems().getOrFail().let { apps -> getItems().forEach { apps ->
assertEquals(1, apps.size) assertEquals(1, apps.size)
assertEquals(app2, apps[0]) assertEquals(app2, apps[0])
} }
@@ -220,7 +225,7 @@ internal class AppOverviewItemsTest : AppTest() {
// prefer repo1 for this app // prefer repo1 for this app
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1)) appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1))
appDao.getAppOverviewItems().getOrFail().let { apps -> getItems().forEach { apps ->
assertEquals(1, apps.size) assertEquals(1, apps.size)
assertEquals(app1, apps[0]) assertEquals(app1, apps[0])
} }
@@ -341,7 +346,7 @@ internal class AppOverviewItemsTest : AppTest() {
} }
@Test @Test
fun testOnlyFromEnabledRepos() { fun testOnlyFromEnabledRepos() = runBlocking {
val repoId = repoDao.insertOrReplace(getRandomRepo()) val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales) appDao.insert(repoId, packageName1, app1, locales)
appDao.insert(repoId, packageName2, app2, locales) appDao.insert(repoId, packageName2, app2, locales)
@@ -349,18 +354,24 @@ internal class AppOverviewItemsTest : AppTest() {
appDao.insert(repoId2, packageName3, app3, locales) appDao.insert(repoId2, packageName3, app3, locales)
// 3 apps from 2 repos // 3 apps from 2 repos
assertEquals(3, appDao.getAppOverviewItems().getOrAwaitValue()?.size) getItems().forEach { apps ->
assertEquals(3, apps.size)
}
assertEquals(3, appDao.getAppOverviewItems("A").getOrAwaitValue()?.size) assertEquals(3, appDao.getAppOverviewItems("A").getOrAwaitValue()?.size)
// only 1 app after disabling first repo // only 1 app after disabling first repo
repoDao.setRepositoryEnabled(repoId, false) repoDao.setRepositoryEnabled(repoId, false)
assertEquals(1, appDao.getAppOverviewItems().getOrAwaitValue()?.size) getItems().forEach { apps ->
assertEquals(1, apps.size)
}
assertEquals(1, appDao.getAppOverviewItems("A").getOrAwaitValue()?.size) assertEquals(1, appDao.getAppOverviewItems("A").getOrAwaitValue()?.size)
assertEquals(1, appDao.getAppOverviewItems("B").getOrAwaitValue()?.size) assertEquals(1, appDao.getAppOverviewItems("B").getOrAwaitValue()?.size)
// no more apps after disabling all repos // no more apps after disabling all repos
repoDao.setRepositoryEnabled(repoId2, false) repoDao.setRepositoryEnabled(repoId2, false)
assertEquals(0, appDao.getAppOverviewItems().getOrAwaitValue()?.size) getItems().forEach { apps ->
assertEquals(0, apps.size)
}
assertEquals(0, appDao.getAppOverviewItems("A").getOrAwaitValue()?.size) assertEquals(0, appDao.getAppOverviewItems("A").getOrAwaitValue()?.size)
assertEquals(0, appDao.getAppOverviewItems("B").getOrAwaitValue()?.size) assertEquals(0, appDao.getAppOverviewItems("B").getOrAwaitValue()?.size)
} }
@@ -405,6 +416,65 @@ internal class AppOverviewItemsTest : AppTest() {
assertEquals(app2, appDao.getAppOverviewItem(repoId2, packageName)) assertEquals(app2, appDao.getAppOverviewItem(repoId2, packageName))
} }
@Test
fun testByAuthor() = runBlocking {
val author = getRandomString()
val packageName = getRandomString()
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, getRandomMetadataV2(author), locales)
// author has only one app
assertFalse(appDao.hasAuthorMoreThanOneApp(author).getOrFail())
assertEquals(0, appDao.getAppsByAuthor("foo bar").size)
appDao.getAppsByAuthor(author).let { apps ->
assertEquals(1, apps.size)
assertEquals(packageName, apps[0].packageName)
}
// now add 49 more apps
(1 until 50).forEach { _ ->
appDao.insert(repoId, getRandomString(), getRandomMetadataV2(author), locales)
}
assertTrue(appDao.hasAuthorMoreThanOneApp(author).getOrFail())
assertEquals(50, appDao.getAppsByAuthor(author).size)
}
@Test
fun testByCategory() = runBlocking {
// insert three apps
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales)
appDao.insert(repoId, packageName2, app2, locales)
appDao.insert(repoId, packageName3, app3, locales)
// only two apps are in category B
appDao.getAppsByCategory("B").let { apps ->
assertEquals(2, apps.size)
assertNotEquals(packageName2, apps[0].packageName)
assertNotEquals(packageName2, apps[1].packageName)
}
// no app is in category C
assertEquals(0, appDao.getAppsByCategory("C").size)
// we'll add app1 as a variant of app2, but its repo has lower weight, so no effect
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId2, packageName2, app1, locales)
appDao.getAppsByCategory("B").let { apps ->
assertEquals(2, apps.size)
assertNotEquals(packageName2, apps[0].packageName)
assertNotEquals(packageName2, apps[1].packageName)
}
}
private suspend fun getItems(): List<List<AppOverviewItem>> {
return listOf(
appDao.getAppOverviewItems().getOrFail(),
// manually sort the second list, so both results are comparable
appDao.getAllApps().sortedByDescending { it.lastUpdated },
)
}
private fun assertEquals(expected: MetadataV2, actual: AppOverviewItem?) { private fun assertEquals(expected: MetadataV2, actual: AppOverviewItem?) {
assertNotNull(actual) assertNotNull(actual)
assertEquals(expected.added, actual.added) assertEquals(expected.added, actual.added)
@@ -19,6 +19,8 @@ import androidx.room.RoomRawQuery
import androidx.room.RoomWarnings.Companion.QUERY_MISMATCH import androidx.room.RoomWarnings.Companion.QUERY_MISMATCH
import androidx.room.Transaction import androidx.room.Transaction
import androidx.room.Update import androidx.room.Update
import androidx.sqlite.SQLiteStatement
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
@@ -34,6 +36,7 @@ import org.fdroid.index.v2.LocalizedFileListV2
import org.fdroid.index.v2.LocalizedFileV2 import org.fdroid.index.v2.LocalizedFileV2
import org.fdroid.index.v2.MetadataV2 import org.fdroid.index.v2.MetadataV2
import org.fdroid.index.v2.ReflectionDiffer.applyDiff import org.fdroid.index.v2.ReflectionDiffer.applyDiff
import java.util.concurrent.TimeUnit
public interface AppDao { public interface AppDao {
/** /**
@@ -83,16 +86,62 @@ public interface AppDao {
* Apps without name, icon or summary are at the end (or excluded if limit is too small). * Apps without name, icon or summary are at the end (or excluded if limit is too small).
* Includes anti-features from the version with the highest version code. * Includes anti-features from the version with the highest version code.
*/ */
@Deprecated("Use getNewAppsFlow and getRecentlyUpdatedAppsFlow instead")
public fun getAppOverviewItems(limit: Int = 200): LiveData<List<AppOverviewItem>> public fun getAppOverviewItems(limit: Int = 200): LiveData<List<AppOverviewItem>>
/** /**
* Returns a limited number of apps with limited data within the given [category]. * Returns a limited number of apps with limited data within the given [category].
*/ */
@Deprecated("Use getAppsByCategory instead")
public fun getAppOverviewItems( public fun getAppOverviewItems(
category: String, category: String,
limit: Int = 50, limit: Int = 50,
): LiveData<List<AppOverviewItem>> ): LiveData<List<AppOverviewItem>>
/**
* Returns all apps from the database.
*/
public suspend fun getAllApps(): List<AppOverviewItem>
/**
* Returns all apps whose author is set exactly to [authorName].
*/
public suspend fun getAppsByAuthor(authorName: String): List<AppOverviewItem>
/**
* Returns all apps that are in the category with [categoryId].
*/
public suspend fun getAppsByCategory(categoryId: String): List<AppOverviewItem>
/**
* Returns apps that are new. This means that they were added and last updated at the same time.
* @param maxAgeInDays the number of days that is still considered "new".
* Apps older than this won't be returned.
*/
public suspend fun getNewApps(maxAgeInDays: Long = 14): List<AppOverviewItem>
/**
* Get apps that were recently updated.
* This excludes apps returned by [getNewApps].
* @param limit only return that many apps and not more.
*/
public suspend fun getRecentlyUpdatedApps(limit: Int = 200): List<AppOverviewItem>
/**
* Get all apps from the repository identified by [repoId].
*/
public suspend fun getAppsByRepository(repoId: Long): List<AppOverviewItem>
/**
* Same as [getNewApps], but returns an observable [Flow].
*/
public fun getNewAppsFlow(maxAgeInDays: Long = 14): Flow<List<AppOverviewItem>>
/**
* Same as [getRecentlyUpdatedApps], but returns an observable [Flow].
*/
public fun getRecentlyUpdatedAppsFlow(limit: Int = 200): Flow<List<AppOverviewItem>>
/** /**
* Returns a list of all [AppListItem] sorted by the given [sortOrder], * Returns a list of all [AppListItem] sorted by the given [sortOrder],
* or a subset of [AppListItem]s filtered by the given [searchQuery] if it is non-null. * or a subset of [AppListItem]s filtered by the given [searchQuery] if it is non-null.
@@ -413,6 +462,111 @@ internal interface AppDaoInt : AppDao {
FROM ${AppMetadata.TABLE} AS app WHERE repoId = :repoId AND packageName = :packageName""") FROM ${AppMetadata.TABLE} AS app WHERE repoId = :repoId AND packageName = :packageName""")
fun getAppOverviewItem(repoId: Long, packageName: String): AppOverviewItem? fun getAppOverviewItem(repoId: Long, packageName: String): AppOverviewItem?
@Transaction
override suspend fun getAllApps(): List<AppOverviewItem> {
val query = getAppsQuery("") {}
return getApps(query)
}
@Transaction
override suspend fun getAppsByAuthor(authorName: String): List<AppOverviewItem> {
val query = getAppsQuery("authorName = ?") { statement ->
statement.bindText(1, authorName)
}
return getApps(query)
}
@Transaction
override suspend fun getAppsByCategory(categoryId: String): List<AppOverviewItem> {
val query = getAppsQuery("categories LIKE '%,' || ? || ',%'") { statement ->
statement.bindText(1, categoryId)
}
return getApps(query)
}
@Transaction
override suspend fun getNewApps(maxAgeInDays: Long): List<AppOverviewItem> {
val query =
getAppsQuery("app.added = app.lastUpdated AND app.lastUpdated > ?") { statement ->
statement.bindLong(
1,
System.currentTimeMillis() - TimeUnit.DAYS.toMillis(maxAgeInDays)
)
}
return getApps(query)
}
@Transaction
override suspend fun getRecentlyUpdatedApps(limit: Int): List<AppOverviewItem> {
val query = getAppsQuery(
"app.added != app.lastUpdated ORDER BY app.lastUpdated DESC LIMIT ?"
) { statement ->
statement.bindInt(1, limit)
}
return getApps(query)
}
@Transaction
@Query("""SELECT repoId, packageName, app.added, app.lastUpdated, localizedName,
localizedSummary, categories, version.antiFeatures, app.isCompatible
FROM ${AppMetadata.TABLE} AS app
LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName)
WHERE repoId = :repoId""")
override suspend fun getAppsByRepository(repoId: Long): List<AppOverviewItem>
override fun getNewAppsFlow(maxAgeInDays: Long): Flow<List<AppOverviewItem>> {
val query =
getAppsQuery(
"app.added = app.lastUpdated AND app.lastUpdated > ? ORDER BY app.added DESC"
) { statement ->
statement.bindLong(
1,
System.currentTimeMillis() - TimeUnit.DAYS.toMillis(maxAgeInDays)
)
}
return getAppsFlow(query)
}
override fun getRecentlyUpdatedAppsFlow(limit: Int): Flow<List<AppOverviewItem>> {
val query = getAppsQuery(
"app.added != app.lastUpdated ORDER BY app.lastUpdated DESC LIMIT ?"
) { statement ->
statement.bindInt(1, limit)
}
return getAppsFlow(query)
}
private fun getAppsQuery(
whereQuery: String,
onBindStatement: (SQLiteStatement) -> Unit,
): RoomRawQuery {
val queryBuilder = StringBuilder(
"""
SELECT repoId, packageName, app.added, app.lastUpdated, localizedName,
localizedSummary, categories, version.antiFeatures, app.isCompatible
FROM ${AppMetadata.TABLE} AS app
JOIN PreferredRepo USING (packageName)
LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName)
WHERE repoId = preferredRepoId"""
)
if (whereQuery.isNotEmpty()) queryBuilder.append(" AND ").append(whereQuery)
return RoomRawQuery(
sql = queryBuilder.toString().trimIndent(),
onBindStatement = onBindStatement,
)
}
@RawQuery
suspend fun getApps(query: RoomRawQuery): List<AppOverviewItem>
@Transaction
@RawQuery(
observedEntities = [
AppMetadata::class, Version::class, Repository::class, RepositoryPreferences::class,
]
)
fun getAppsFlow(query: RoomRawQuery): Flow<List<AppOverviewItem>>
// //
// AppListItems // AppListItems
// //