[db] store localized name and summary in DB

otherwise we can't really sort *all* apps by name in an efficient manner
This commit is contained in:
Torsten Grote
2022-04-11 16:02:09 -03:00
parent 8f132fc6d0
commit 89a2a50f7c
7 changed files with 159 additions and 53 deletions

View File

@@ -8,6 +8,7 @@ import org.fdroid.test.TestAppUtils.getRandomMetadataV2
import org.fdroid.test.TestRepoUtils.getRandomFileV2
import org.fdroid.test.TestRepoUtils.getRandomRepo
import org.fdroid.test.TestUtils.getRandomString
import org.fdroid.test.TestVersionUtils.getRandomPackageVersionV2
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -64,8 +65,10 @@ internal class AppTest : DbTest() {
val app1 = getRandomMetadataV2().copy(name = name1, icon = icons1)
val app2 = getRandomMetadataV2().copy(name = name2, icon = icons2)
val app3 = getRandomMetadataV2().copy(name = name3, icon = null)
appDao.insert(repoId, packageId1, app1)
appDao.insert(repoId, packageId2, app2)
appDao.insert(repoId, packageId1, app1, locales)
appDao.insert(repoId, packageId2, app2, locales)
versionDao.insert(repoId, packageId1, "1", getRandomPackageVersionV2(), true)
versionDao.insert(repoId, packageId2, "2", getRandomPackageVersionV2(), true)
// icons of both apps are returned correctly
val apps = appDao.getAppOverviewItems().getOrAwaitValue() ?: fail()
@@ -77,6 +80,7 @@ internal class AppTest : DbTest() {
// app without icon is not returned
appDao.insert(repoId, packageId3, app3)
versionDao.insert(repoId, packageId3, "3", getRandomPackageVersionV2(), true)
val apps3 = appDao.getAppOverviewItems().getOrAwaitValue() ?: fail()
assertEquals(2, apps3.size)
assertEquals(icons1,

View File

@@ -1,6 +1,7 @@
package org.fdroid.database
import android.content.Context
import androidx.core.os.LocaleListCompat
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -13,6 +14,7 @@ import org.junit.After
import org.junit.Before
import org.junit.runner.RunWith
import java.io.IOException
import java.util.Locale
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
@@ -24,6 +26,8 @@ internal abstract class DbTest {
internal lateinit var db: FDroidDatabaseInt
private val testCoroutineDispatcher = Dispatchers.Unconfined
protected val locales = LocaleListCompat.create(Locale.US)
@Before
open fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()

View File

@@ -1,13 +1,15 @@
package org.fdroid.database
import android.content.res.Resources
import androidx.core.os.ConfigurationCompat.getLocales
import androidx.core.os.LocaleListCompat
import androidx.room.ColumnInfo
import androidx.room.DatabaseView
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Ignore
import androidx.room.Relation
import org.fdroid.database.Converters.fromStringToLocalizedTextV2
import org.fdroid.database.Converters.fromStringToMapOfLocalizedTextV2
import org.fdroid.index.v2.Author
import org.fdroid.index.v2.Donation
@@ -61,6 +63,7 @@ internal fun MetadataV2.toAppMetadata(
repoId: Long,
packageId: String,
isCompatible: Boolean = false,
locales: LocaleListCompat = getLocales(Resources.getSystem().configuration),
) = AppMetadata(
repoId = repoId,
packageId = packageId,
@@ -69,6 +72,8 @@ internal fun MetadataV2.toAppMetadata(
name = name,
summary = summary,
description = description,
localizedName = name.getBestLocale(locales),
localizedSummary = summary.getBestLocale(locales),
webSite = webSite,
changelog = changelog,
license = license,
@@ -91,12 +96,8 @@ public data class App(
val tvBanner: LocalizedFileV2? = null,
val screenshots: Screenshots? = null,
) {
public fun getName(localeList: LocaleListCompat): String? =
metadata.name.getBestLocale(localeList)
public fun getSummary(localeList: LocaleListCompat): String? =
metadata.summary.getBestLocale(localeList)
public fun getName(): String? = metadata.localizedName
public fun getSummary(): String? = metadata.localizedSummary
public fun getDescription(localeList: LocaleListCompat): String? =
metadata.description.getBestLocale(localeList)
@@ -135,25 +136,30 @@ public data class AppOverviewItem(
public val packageId: String,
public val added: Long,
public val lastUpdated: Long,
internal val name: LocalizedTextV2? = null,
internal val summary: LocalizedTextV2? = null,
@ColumnInfo(name = "localizedName")
public val name: String? = null,
@ColumnInfo(name = "localizedSummary")
public val summary: String? = null,
internal val antiFeatures: Map<String, LocalizedTextV2>? = null,
@Relation(
parentColumn = "packageId",
entityColumn = "packageId",
)
internal val localizedIcon: List<LocalizedIcon>? = null,
) {
public fun getName(localeList: LocaleListCompat): String? = name.getBestLocale(localeList)
public fun getSummary(localeList: LocaleListCompat): String? = summary.getBestLocale(localeList)
public fun getIcon(localeList: LocaleListCompat): String? =
localizedIcon?.toLocalizedFileV2().getBestLocale(localeList)?.name
val antiFeatureNames: List<String> get() = antiFeatures?.map { it.key } ?: emptyList()
}
public data class AppListItem @JvmOverloads constructor(
public data class AppListItem constructor(
public val repoId: Long,
public val packageId: String,
internal val name: String?,
internal val summary: String?,
@ColumnInfo(name = "localizedName")
public val name: String? = null,
@ColumnInfo(name = "localizedSummary")
public val summary: String? = null,
internal val antiFeatures: String?,
@Relation(
parentColumn = "packageId",
@@ -167,21 +173,11 @@ public data class AppListItem @JvmOverloads constructor(
/**
* The name of the installed version, null if this app is not installed.
*/
@Ignore
@get:Ignore
public val installedVersionName: String? = null,
@Ignore
@get:Ignore
public val installedVersionCode: Long? = null,
) {
public fun getName(localeList: LocaleListCompat): String? {
// queries for this class return a larger number, so we convert on demand
return fromStringToLocalizedTextV2(name).getBestLocale(localeList)
}
public fun getSummary(localeList: LocaleListCompat): String? {
// queries for this class return a larger number, so we convert on demand
return fromStringToLocalizedTextV2(summary).getBestLocale(localeList)
}
public fun getAntiFeatureNames(): List<String> {
return fromStringToMapOfLocalizedTextV2(antiFeatures)?.map { it.key } ?: emptyList()
}
@@ -194,7 +190,7 @@ public data class UpdatableApp(
public val packageId: String,
public val installedVersionCode: Long,
public val upgrade: AppVersion,
internal val name: LocalizedTextV2? = null,
public val name: String? = null,
public val summary: String? = null,
@Relation(
parentColumn = "packageId",
@@ -202,7 +198,6 @@ public data class UpdatableApp(
)
internal val localizedIcon: List<LocalizedIcon>? = null,
) {
public fun getName(localeList: LocaleListCompat): String? = name.getBestLocale(localeList)
public fun getIcon(localeList: LocaleListCompat): FileV2? =
localizedIcon?.toLocalizedFileV2().getBestLocale(localeList)
}

View File

@@ -2,7 +2,10 @@ package org.fdroid.database
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.res.Resources
import androidx.annotation.VisibleForTesting
import androidx.core.os.ConfigurationCompat.getLocales
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.distinctUntilChanged
@@ -22,7 +25,12 @@ import org.fdroid.index.v2.MetadataV2
import org.fdroid.index.v2.Screenshots
public interface AppDao {
public fun insert(repoId: Long, packageId: String, app: MetadataV2)
public fun insert(
repoId: Long,
packageId: String,
app: MetadataV2,
locales: LocaleListCompat = getLocales(Resources.getSystem().configuration),
)
/**
* Gets the app from the DB. If more than one app with this [packageId] exists,
@@ -36,10 +44,15 @@ public interface AppDao {
limit: Int = 50,
): LiveData<List<AppOverviewItem>>
public fun getAppListItems(packageManager: PackageManager): LiveData<List<AppListItem>>
public fun getAppListItems(
packageManager: PackageManager,
sortOrder: AppListSortOrder,
): LiveData<List<AppListItem>>
public fun getAppListItems(
packageManager: PackageManager,
category: String,
sortOrder: AppListSortOrder,
): LiveData<List<AppListItem>>
public fun getInstalledAppListItems(packageManager: PackageManager): LiveData<List<AppListItem>>
@@ -47,11 +60,20 @@ public interface AppDao {
public fun getNumberOfAppsInCategory(category: String): Int
}
public enum class AppListSortOrder {
LAST_UPDATED, NAME
}
@Dao
internal interface AppDaoInt : AppDao {
@Transaction
override fun insert(repoId: Long, packageId: String, app: MetadataV2) {
override fun insert(
repoId: Long,
packageId: String,
app: MetadataV2,
locales: LocaleListCompat,
) {
insert(app.toAppMetadata(repoId, packageId, false))
app.icon.insert(repoId, packageId, "icon")
app.featureGraphic.insert(repoId, packageId, "featureGraphic")
@@ -109,6 +131,10 @@ internal interface AppDaoInt : AppDao {
WHERE repoId = :repoId""")
fun updateCompatibility(repoId: Long)
@Query("""UPDATE AppMetadata SET localizedName = :name, localizedSummary = :summary
WHERE repoId = :repoId AND packageId = :packageId""")
fun updateAppMetadata(repoId: Long, packageId: String, name: String?, summary: String?)
override fun getApp(packageId: String): LiveData<App?> {
return getRepoIdForPackage(packageId).distinctUntilChanged().switchMap { repoId ->
if (repoId == null) MutableLiveData(null)
@@ -181,28 +207,40 @@ internal interface AppDaoInt : AppDao {
fun getLocalizedFileLists(): List<LocalizedFileList>
@Transaction
@Query("""SELECT repoId, packageId, added, app.lastUpdated, app.name, summary
@Query("""SELECT repoId, packageId, app.added, app.lastUpdated, localizedName,
localizedSummary, version.antiFeatures
FROM AppMetadata AS app
JOIN RepositoryPreferences AS pref USING (repoId)
JOIN Version AS version USING (repoId, packageId)
JOIN LocalizedIcon AS icon USING (repoId, packageId)
WHERE pref.enabled = 1 GROUP BY packageId
ORDER BY app.name IS NULL ASC, summary IS NULL ASC, app.lastUpdated DESC, added ASC
WHERE pref.enabled = 1
GROUP BY packageId HAVING MAX(pref.weight) AND MAX(version.manifest_versionCode)
ORDER BY localizedName IS NULL ASC, localizedSummary IS NULL ASC, app.lastUpdated DESC, app.added ASC
LIMIT :limit""")
override fun getAppOverviewItems(limit: Int): LiveData<List<AppOverviewItem>>
@Transaction
// TODO maybe it makes sense to split categories into their own table for this?
@Query("""SELECT repoId, packageId, added, app.lastUpdated, app.name, summary
@Query("""SELECT repoId, packageId, app.added, app.lastUpdated, localizedName,
localizedSummary, version.antiFeatures
FROM AppMetadata AS app
JOIN RepositoryPreferences AS pref USING (repoId)
JOIN Version AS version USING (repoId, packageId)
JOIN LocalizedIcon AS icon USING (repoId, packageId)
WHERE pref.enabled = 1 AND categories LIKE '%' || :category || '%' GROUP BY packageId
ORDER BY app.name IS NULL ASC, summary IS NULL ASC, app.lastUpdated DESC, added ASC
WHERE pref.enabled = 1 AND categories LIKE '%' || :category || '%'
GROUP BY packageId HAVING MAX(pref.weight) AND MAX(version.manifest_versionCode)
ORDER BY localizedName IS NULL ASC, localizedSummary IS NULL ASC, app.lastUpdated DESC, app.added ASC
LIMIT :limit""")
override fun getAppOverviewItems(category: String, limit: Int): LiveData<List<AppOverviewItem>>
override fun getAppListItems(packageManager: PackageManager): LiveData<List<AppListItem>> {
return getAppListItems().map(packageManager)
override fun getAppListItems(
packageManager: PackageManager,
sortOrder: AppListSortOrder,
): LiveData<List<AppListItem>> {
return when (sortOrder) {
AppListSortOrder.LAST_UPDATED -> getAppListItemsByLastUpdated().map(packageManager)
AppListSortOrder.NAME -> getAppListItemsByName().map(packageManager)
}
}
private fun LiveData<List<AppListItem>>.map(
@@ -221,41 +259,75 @@ internal interface AppDaoInt : AppDao {
@Transaction
@Query("""
SELECT repoId, packageId, app.name, summary, version.antiFeatures, app.isCompatible
SELECT repoId, packageId, localizedName, localizedSummary, version.antiFeatures,
app.isCompatible
FROM AppMetadata AS app
JOIN Version AS version USING (repoId, packageId)
JOIN RepositoryPreferences AS pref USING (repoId)
WHERE pref.enabled = 1
GROUP BY packageId HAVING MAX(pref.weight) AND MAX(version.manifest_versionCode)
ORDER BY localizedName COLLATE NOCASE ASC""")
fun getAppListItemsByName(): LiveData<List<AppListItem>>
@Transaction
@Query("""
SELECT repoId, packageId, localizedName, localizedSummary, version.antiFeatures,
app.isCompatible
FROM AppMetadata AS app
JOIN Version AS version USING (repoId, packageId)
JOIN RepositoryPreferences AS pref USING (repoId)
WHERE pref.enabled = 1
GROUP BY packageId HAVING MAX(pref.weight) AND MAX(version.manifest_versionCode)
ORDER BY app.lastUpdated DESC""")
fun getAppListItems(): LiveData<List<AppListItem>>
fun getAppListItemsByLastUpdated(): LiveData<List<AppListItem>>
override fun getAppListItems(
packageManager: PackageManager,
category: String,
sortOrder: AppListSortOrder,
): LiveData<List<AppListItem>> {
return getAppListItems(category).map(packageManager)
return when (sortOrder) {
AppListSortOrder.LAST_UPDATED -> {
getAppListItemsByLastUpdated(category).map(packageManager)
}
AppListSortOrder.NAME -> getAppListItemsByName(category).map(packageManager)
}
}
// TODO maybe it makes sense to split categories into their own table for this?
@Transaction
@Query("""
SELECT repoId, packageId, app.name, summary, version.antiFeatures, app.isCompatible
SELECT repoId, packageId, localizedName, localizedSummary, version.antiFeatures,
app.isCompatible
FROM AppMetadata AS app
JOIN Version AS version USING (repoId, packageId)
JOIN RepositoryPreferences AS pref USING (repoId)
WHERE pref.enabled = 1 AND categories LIKE '%' || :category || '%'
GROUP BY packageId HAVING MAX(pref.weight) AND MAX(version.manifest_versionCode)
ORDER BY app.lastUpdated DESC""")
fun getAppListItems(category: String): LiveData<List<AppListItem>>
fun getAppListItemsByLastUpdated(category: String): LiveData<List<AppListItem>>
// TODO maybe it makes sense to split categories into their own table for this?
@Transaction
@Query("""
SELECT repoId, packageId, localizedName, localizedSummary, version.antiFeatures,
app.isCompatible
FROM AppMetadata AS app
JOIN Version AS version USING (repoId, packageId)
JOIN RepositoryPreferences AS pref USING (repoId)
WHERE pref.enabled = 1 AND categories LIKE '%' || :category || '%'
GROUP BY packageId HAVING MAX(pref.weight) AND MAX(version.manifest_versionCode)
ORDER BY localizedName COLLATE NOCASE ASC""")
fun getAppListItemsByName(category: String): LiveData<List<AppListItem>>
@Transaction
@SuppressWarnings(CURSOR_MISMATCH) // no anti-features needed here
@Query("""SELECT repoId, packageId, app.name, summary, app.isCompatible
@Query("""SELECT repoId, packageId, localizedName, localizedSummary, app.isCompatible
FROM AppMetadata AS app
JOIN RepositoryPreferences AS pref USING (repoId)
WHERE pref.enabled = 1 AND packageId IN (:packageNames)
GROUP BY packageId HAVING MAX(pref.weight)""")
GROUP BY packageId HAVING MAX(pref.weight)
ORDER BY localizedName COLLATE NOCASE ASC""")
fun getAppListItems(packageNames: List<String>): LiveData<List<AppListItem>>
override fun getInstalledAppListItems(
@@ -276,7 +348,9 @@ internal interface AppDaoInt : AppDao {
* Used by [UpdateChecker] to get specific apps with available updates.
*/
@Transaction
@Query("""SELECT repoId, packageId, added, app.lastUpdated, app.name, summary
@SuppressWarnings(CURSOR_MISMATCH) // no anti-features needed here
@Query("""SELECT repoId, packageId, added, app.lastUpdated, localizedName,
localizedSummary
FROM AppMetadata AS app WHERE repoId = :repoId AND packageId = :packageId""")
fun getAppOverviewItem(repoId: Long, packageId: String): AppOverviewItem?

View File

@@ -1,5 +1,8 @@
package org.fdroid.database
import android.content.res.Resources
import androidx.core.os.ConfigurationCompat.getLocales
import androidx.core.os.LocaleListCompat
import org.fdroid.CompatibilityChecker
import org.fdroid.index.v2.IndexStreamReceiver
import org.fdroid.index.v2.PackageV2
@@ -10,19 +13,21 @@ internal class DbStreamReceiver(
private val compatibilityChecker: CompatibilityChecker,
) : IndexStreamReceiver {
private val locales: LocaleListCompat = getLocales(Resources.getSystem().configuration)
override fun receive(repoId: Long, repo: RepoV2, version: Int, certificate: String?) {
db.getRepositoryDao().replace(repoId, repo, version, certificate)
}
override fun receive(repoId: Long, packageId: String, p: PackageV2) {
db.getAppDao().insert(repoId, packageId, p.metadata)
db.getAppDao().insert(repoId, packageId, p.metadata, locales)
db.getVersionDao().insert(repoId, packageId, p.versions) {
compatibilityChecker.isCompatible(it.manifest)
}
}
override fun onStreamEnded(repoId: Long) {
db.getAppDao().updateCompatibility(repoId)
db.afterUpdatingRepo(repoId)
}
}

View File

@@ -1,5 +1,8 @@
package org.fdroid.database
import android.content.res.Resources
import androidx.core.os.ConfigurationCompat.getLocales
import androidx.core.os.LocaleListCompat
import org.fdroid.CompatibilityChecker
import org.fdroid.index.v1.IndexV1StreamReceiver
import org.fdroid.index.v2.AntiFeatureV2
@@ -14,12 +17,14 @@ internal class DbV1StreamReceiver(
private val compatibilityChecker: CompatibilityChecker,
) : IndexV1StreamReceiver {
private val locales: LocaleListCompat = getLocales(Resources.getSystem().configuration)
override fun receive(repoId: Long, repo: RepoV2, version: Int, certificate: String?) {
db.getRepositoryDao().replace(repoId, repo, version, certificate)
}
override fun receive(repoId: Long, packageId: String, m: MetadataV2) {
db.getAppDao().insert(repoId, packageId, m)
db.getAppDao().insert(repoId, packageId, m, locales)
}
override fun receive(repoId: Long, packageId: String, v: Map<String, PackageVersionV2>) {
@@ -39,7 +44,7 @@ internal class DbV1StreamReceiver(
repoDao.insertCategories(categories.toRepoCategories(repoId))
repoDao.insertReleaseChannels(releaseChannels.toRepoReleaseChannel(repoId))
db.getAppDao().updateCompatibility(repoId)
db.afterUpdatingRepo(repoId)
}
override fun updateAppMetadata(repoId: Long, packageId: String, preferredSigner: String?) {

View File

@@ -1,7 +1,9 @@
package org.fdroid.database
import android.content.Context
import android.content.res.Resources
import android.util.Log
import androidx.core.os.ConfigurationCompat.getLocales
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@@ -42,12 +44,29 @@ internal abstract class FDroidDatabaseInt internal constructor() : RoomDatabase(
abstract override fun getRepositoryDao(): RepositoryDaoInt
abstract override fun getAppDao(): AppDaoInt
abstract override fun getVersionDao(): VersionDaoInt
fun afterUpdatingRepo(repoId: Long) {
getAppDao().updateCompatibility(repoId)
}
}
public interface FDroidDatabase {
public fun getRepositoryDao(): RepositoryDao
public fun getAppDao(): AppDao
public fun getVersionDao(): VersionDao
public fun afterLocalesChanged() {
val appDao = getAppDao() as AppDaoInt
val locales = getLocales(Resources.getSystem().configuration)
runInTransaction {
appDao.getAppMetadata().forEach { appMetadata ->
appDao.updateAppMetadata(
repoId = appMetadata.repoId,
packageId = appMetadata.packageId,
name = appMetadata.name.getBestLocale(locales),
summary = appMetadata.summary.getBestLocale(locales),
)
}
}
}
public fun runInTransaction(body: Runnable)
}