mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-04-20 06:47:06 -04:00
[db] add search support with FTS4
FTS5 isn't supported by Room because old Android devices ship only with sqlite with FTS4
This commit is contained in:
committed by
Michael Pöhn
parent
01381c268d
commit
28fa5fd46e
@@ -34,6 +34,107 @@ internal class AppListItemsTest : AppTest() {
|
||||
Pair(packageName3, app3),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun testSearchQuery() {
|
||||
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 app3 = app3.copy(name = mapOf("de-DE" to "Drei"), summary = mapOf("de" to "Zfassung"))
|
||||
// insert three apps in a random order
|
||||
val repoId = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId, packageName1, app1, locales)
|
||||
appDao.insert(repoId, packageName3, app3, locales)
|
||||
appDao.insert(repoId, packageName2, app2, locales)
|
||||
|
||||
// nothing is installed
|
||||
every { pm.getInstalledPackages(0) } returns emptyList()
|
||||
|
||||
// get first app by search, sort order doesn't matter
|
||||
appDao.getAppListItems(pm, "One", LAST_UPDATED).getOrFail().let { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(app1, apps[0])
|
||||
}
|
||||
|
||||
// get second app by search, sort order doesn't matter
|
||||
appDao.getAppListItems(pm, "Two", NAME).getOrFail().let { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(app2, apps[0])
|
||||
}
|
||||
|
||||
// get second and third app by searching for summary
|
||||
appDao.getAppListItems(pm, "Zfassung", LAST_UPDATED).getOrFail().let { apps ->
|
||||
assertEquals(2, apps.size)
|
||||
// sort-order isn't fixes, yet
|
||||
if (apps[0].packageId == packageName2) {
|
||||
assertEquals(app2, apps[0])
|
||||
assertEquals(app3, apps[1])
|
||||
} else {
|
||||
assertEquals(app3, apps[0])
|
||||
assertEquals(app2, apps[1])
|
||||
}
|
||||
}
|
||||
|
||||
// empty search for unknown search term
|
||||
appDao.getAppListItems(pm, "foo bar", LAST_UPDATED).getOrFail().let { apps ->
|
||||
assertEquals(0, apps.size)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSearchQueryInCategory() {
|
||||
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 app3 = app3.copy(name = mapOf("de-DE" to "Drei"), summary = mapOf("de" to "Zfassung"))
|
||||
// insert three apps in a random order
|
||||
val repoId = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId, packageName1, app1, locales)
|
||||
appDao.insert(repoId, packageName3, app3, locales)
|
||||
appDao.insert(repoId, packageName2, app2, locales)
|
||||
|
||||
// nothing is installed
|
||||
every { pm.getInstalledPackages(0) } returns emptyList()
|
||||
|
||||
// get first app by search, sort order doesn't matter
|
||||
appDao.getAppListItems(pm, "A", "One", LAST_UPDATED).getOrFail().let { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(app1, apps[0])
|
||||
}
|
||||
|
||||
// get second app by search, sort order doesn't matter
|
||||
appDao.getAppListItems(pm, "A", "Two", NAME).getOrFail().let { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(app2, apps[0])
|
||||
}
|
||||
|
||||
// get second and third app by searching for summary
|
||||
appDao.getAppListItems(pm, "A", "Zfassung", LAST_UPDATED).getOrFail().let { apps ->
|
||||
assertEquals(2, apps.size)
|
||||
// sort-order isn't fixes, yet
|
||||
if (apps[0].packageId == packageName2) {
|
||||
assertEquals(app2, apps[0])
|
||||
assertEquals(app3, apps[1])
|
||||
} else {
|
||||
assertEquals(app3, apps[0])
|
||||
assertEquals(app2, apps[1])
|
||||
}
|
||||
}
|
||||
|
||||
// get third app by searching for summary in category B only
|
||||
appDao.getAppListItems(pm, "B", "Zfassung", LAST_UPDATED).getOrFail().let { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(app3, apps[0])
|
||||
}
|
||||
|
||||
// empty search for unknown category
|
||||
appDao.getAppListItems(pm, "C", "Zfassung", LAST_UPDATED).getOrFail().let { apps ->
|
||||
assertEquals(0, apps.size)
|
||||
}
|
||||
|
||||
// empty search for unknown search term
|
||||
appDao.getAppListItems(pm, "A", "foo bar", LAST_UPDATED).getOrFail().let { apps ->
|
||||
assertEquals(0, apps.size)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSortOrderByLastUpdated() {
|
||||
// insert three apps in a random order
|
||||
@@ -46,7 +147,7 @@ internal class AppListItemsTest : AppTest() {
|
||||
every { pm.getInstalledPackages(0) } returns emptyList()
|
||||
|
||||
// get apps sorted by last updated
|
||||
appDao.getAppListItems(pm, LAST_UPDATED).getOrFail().let { apps ->
|
||||
appDao.getAppListItems(pm, "", LAST_UPDATED).getOrFail().let { apps ->
|
||||
assertEquals(3, apps.size)
|
||||
// we expect apps to be sorted by last updated descending
|
||||
appPairs.sortedByDescending { (_, metadataV2) ->
|
||||
@@ -70,7 +171,7 @@ internal class AppListItemsTest : AppTest() {
|
||||
every { pm.getInstalledPackages(0) } returns emptyList()
|
||||
|
||||
// get apps sorted by name ascending
|
||||
appDao.getAppListItems(pm, NAME).getOrFail().let { apps ->
|
||||
appDao.getAppListItems(pm, null, NAME).getOrFail().let { apps ->
|
||||
assertEquals(3, apps.size)
|
||||
// we expect apps to be sorted by last updated descending
|
||||
appPairs.sortedBy { (_, metadataV2) ->
|
||||
@@ -100,8 +201,8 @@ internal class AppListItemsTest : AppTest() {
|
||||
|
||||
// get apps sorted by name and last update, test on both lists
|
||||
listOf(
|
||||
appDao.getAppListItems(pm, NAME).getOrFail(),
|
||||
appDao.getAppListItems(pm, LAST_UPDATED).getOrFail(),
|
||||
appDao.getAppListItems(pm, "", NAME).getOrFail(),
|
||||
appDao.getAppListItems(pm, null, LAST_UPDATED).getOrFail(),
|
||||
).forEach { apps ->
|
||||
assertEquals(2, apps.size)
|
||||
// the installed app should have app data
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.room.DatabaseView
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Fts4
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.Relation
|
||||
import org.fdroid.LocaleChooser.getBestLocale
|
||||
@@ -105,6 +106,17 @@ internal fun MetadataV2.toAppMetadata(
|
||||
isCompatible = isCompatible,
|
||||
)
|
||||
|
||||
@Entity
|
||||
@Fts4(contentEntity = AppMetadata::class)
|
||||
internal data class AppMetadataFts(
|
||||
val repoId: Long,
|
||||
val packageId: String,
|
||||
@ColumnInfo(name = "localizedName")
|
||||
val name: String? = null,
|
||||
@ColumnInfo(name = "localizedSummary")
|
||||
val summary: String? = null,
|
||||
)
|
||||
|
||||
public data class App internal constructor(
|
||||
@Embedded val metadata: AppMetadata,
|
||||
@Relation(
|
||||
|
||||
@@ -83,14 +83,24 @@ public interface AppDao {
|
||||
limit: Int = 50,
|
||||
): LiveData<List<AppOverviewItem>>
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* In the later case, the [sortOrder] gets ignored.
|
||||
*/
|
||||
public fun getAppListItems(
|
||||
packageManager: PackageManager,
|
||||
searchQuery: String?,
|
||||
sortOrder: AppListSortOrder,
|
||||
): LiveData<List<AppListItem>>
|
||||
|
||||
/**
|
||||
* Like [getAppListItems], but further filter items by the given [category].
|
||||
*/
|
||||
public fun getAppListItems(
|
||||
packageManager: PackageManager,
|
||||
category: String,
|
||||
searchQuery: String?,
|
||||
sortOrder: AppListSortOrder,
|
||||
): LiveData<List<AppListItem>>
|
||||
|
||||
@@ -354,19 +364,25 @@ internal interface AppDaoInt : AppDao {
|
||||
|
||||
override fun getAppListItems(
|
||||
packageManager: PackageManager,
|
||||
searchQuery: String?,
|
||||
sortOrder: AppListSortOrder,
|
||||
): LiveData<List<AppListItem>> = when (sortOrder) {
|
||||
LAST_UPDATED -> getAppListItemsByLastUpdated().map(packageManager)
|
||||
NAME -> getAppListItemsByName().map(packageManager)
|
||||
): LiveData<List<AppListItem>> {
|
||||
return if (searchQuery.isNullOrEmpty()) when (sortOrder) {
|
||||
LAST_UPDATED -> getAppListItemsByLastUpdated().map(packageManager)
|
||||
NAME -> getAppListItemsByName().map(packageManager)
|
||||
} else getAppListItems(searchQuery)
|
||||
}
|
||||
|
||||
override fun getAppListItems(
|
||||
packageManager: PackageManager,
|
||||
category: String,
|
||||
searchQuery: String?,
|
||||
sortOrder: AppListSortOrder,
|
||||
): LiveData<List<AppListItem>> = when (sortOrder) {
|
||||
LAST_UPDATED -> getAppListItemsByLastUpdated(category).map(packageManager)
|
||||
NAME -> getAppListItemsByName(category).map(packageManager)
|
||||
): LiveData<List<AppListItem>> {
|
||||
return if (searchQuery.isNullOrEmpty()) when (sortOrder) {
|
||||
LAST_UPDATED -> getAppListItemsByLastUpdated(category).map(packageManager)
|
||||
NAME -> getAppListItemsByName(category).map(packageManager)
|
||||
} else getAppListItems(category, searchQuery)
|
||||
}
|
||||
|
||||
private fun LiveData<List<AppListItem>>.map(
|
||||
@@ -383,6 +399,31 @@ internal interface AppDaoInt : AppDao {
|
||||
}
|
||||
}
|
||||
|
||||
@Transaction
|
||||
@Query("""
|
||||
SELECT repoId, packageId, app.localizedName, app.localizedSummary, version.antiFeatures,
|
||||
app.isCompatible
|
||||
FROM AppMetadata AS app
|
||||
JOIN AppMetadataFts USING (repoId, packageId)
|
||||
LEFT JOIN HighestVersion AS version USING (repoId, packageId)
|
||||
JOIN RepositoryPreferences AS pref USING (repoId)
|
||||
WHERE pref.enabled = 1 AND AppMetadataFts MATCH '"*' || :searchQuery || '*"'
|
||||
GROUP BY packageId HAVING MAX(pref.weight)""")
|
||||
fun getAppListItems(searchQuery: String): LiveData<List<AppListItem>>
|
||||
|
||||
@Transaction
|
||||
@Query("""
|
||||
SELECT repoId, packageId, app.localizedName, app.localizedSummary, version.antiFeatures,
|
||||
app.isCompatible
|
||||
FROM AppMetadata AS app
|
||||
JOIN AppMetadataFts USING (repoId, packageId)
|
||||
LEFT JOIN HighestVersion AS version USING (repoId, packageId)
|
||||
JOIN RepositoryPreferences AS pref USING (repoId)
|
||||
WHERE pref.enabled = 1 AND categories LIKE '%,' || :category || ',%' AND
|
||||
AppMetadataFts MATCH '"*' || :searchQuery || '*"'
|
||||
GROUP BY packageId HAVING MAX(pref.weight)""")
|
||||
fun getAppListItems(category: String, searchQuery: String): LiveData<List<AppListItem>>
|
||||
|
||||
@Transaction
|
||||
@Query("""
|
||||
SELECT repoId, packageId, localizedName, localizedSummary, version.antiFeatures,
|
||||
|
||||
@@ -18,7 +18,7 @@ import kotlinx.coroutines.launch
|
||||
import org.fdroid.LocaleChooser.getBestLocale
|
||||
|
||||
@Database(
|
||||
version = 8, // TODO set version to 1 before release and wipe old schemas
|
||||
version = 9, // TODO set version to 1 before release and wipe old schemas
|
||||
entities = [
|
||||
// repo
|
||||
CoreRepository::class,
|
||||
@@ -29,6 +29,7 @@ import org.fdroid.LocaleChooser.getBestLocale
|
||||
RepositoryPreferences::class,
|
||||
// packages
|
||||
AppMetadata::class,
|
||||
AppMetadataFts::class,
|
||||
LocalizedFile::class,
|
||||
LocalizedFileList::class,
|
||||
// versions
|
||||
@@ -50,6 +51,7 @@ import org.fdroid.LocaleChooser.getBestLocale
|
||||
AutoMigration(from = 5, to = 6),
|
||||
AutoMigration(from = 6, to = 7),
|
||||
AutoMigration(from = 7, to = 8),
|
||||
AutoMigration(from = 8, to = 9),
|
||||
],
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
||||
Reference in New Issue
Block a user