diff --git a/database/src/main/java/org/fdroid/database/App.kt b/database/src/main/java/org/fdroid/database/App.kt index 0dc01f3b0..983b76c7a 100644 --- a/database/src/main/java/org/fdroid/database/App.kt +++ b/database/src/main/java/org/fdroid/database/App.kt @@ -5,7 +5,10 @@ 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 import org.fdroid.index.v2.FileV2 @@ -126,6 +129,48 @@ public data class AppOverviewItem( localizedIcon?.toLocalizedFileV2().getBestLocale(localeList)?.name } +public data class AppListItem @JvmOverloads constructor( + public val repoId: Long, + public val packageId: String, + internal val name: String?, + internal val summary: String?, + internal val antiFeatures: String?, + @Relation( + parentColumn = "packageId", + entityColumn = "packageId", + ) + internal val localizedIcon: List?, + /** + * If true, this this app has at least one version that is compatible with this device. + */ + @Ignore // TODO actually get this from the DB (probably needs post-processing). + public val isCompatible: Boolean = true, + /** + * The name of the installed version, null if this app is not installed. + */ + @Ignore + public val installedVersionName: String? = null, + @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 { + return fromStringToMapOfLocalizedTextV2(antiFeatures)?.map { it.key } ?: emptyList() + } + + public fun getIcon(localeList: LocaleListCompat) = + localizedIcon?.toLocalizedFileV2().getBestLocale(localeList)?.name +} + public data class UpdatableApp( public val packageId: String, public val installedVersionCode: Long, diff --git a/database/src/main/java/org/fdroid/database/AppDao.kt b/database/src/main/java/org/fdroid/database/AppDao.kt index 73056f7ad..2337f3dbc 100644 --- a/database/src/main/java/org/fdroid/database/AppDao.kt +++ b/database/src/main/java/org/fdroid/database/AppDao.kt @@ -1,5 +1,7 @@ package org.fdroid.database +import android.content.pm.PackageInfo +import android.content.pm.PackageManager import androidx.annotation.VisibleForTesting import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -11,6 +13,7 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.RoomWarnings.CURSOR_MISMATCH import androidx.room.Transaction import org.fdroid.database.FDroidDatabaseHolder.dispatcher import org.fdroid.index.v2.LocalizedFileListV2 @@ -29,6 +32,14 @@ public interface AppDao { fun getApp(repoId: Long, packageId: String): App? fun getAppOverviewItems(limit: Int = 200): LiveData> fun getAppOverviewItems(category: String, limit: Int = 50): LiveData> + fun getAppListItems(packageManager: PackageManager): LiveData> + fun getAppListItems( + packageManager: PackageManager, + category: String, + ): LiveData> + + fun getInstalledAppListItems(packageManager: PackageManager): LiveData> + fun getNumberOfAppsInCategory(category: String): Int } @@ -197,8 +208,69 @@ internal interface AppDaoInt : AppDao { LIMIT :limit""") override fun getAppOverviewItems(category: String, limit: Int): LiveData> - // FIXME don't over report the same app twice (e.g. in several repos) - @Query("""SELECT COUNT(*) FROM AppMetadata + override fun getAppListItems(packageManager: PackageManager): LiveData> { + return getAppListItems().map(packageManager) + } + + private fun LiveData>.map( + packageManager: PackageManager, + installedPackages: Map = packageManager.getInstalledPackages(0) + .associateBy { packageInfo -> packageInfo.packageName }, + ) = map { items -> + items.map { item -> + val packageInfo = installedPackages[item.packageId] + if (packageInfo == null) item else item.copy( + installedVersionName = packageInfo.versionName, + installedVersionCode = packageInfo.getVersionCode(), + ) + } + } + + @Transaction + @Query("""SELECT repoId, packageId, app.name, summary, version.antiFeatures + 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> + + override fun getAppListItems( + packageManager: PackageManager, + category: String, + ): LiveData> { + return getAppListItems(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 + 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> + + @Transaction + @SuppressWarnings(CURSOR_MISMATCH) // no anti-features needed here + @Query("""SELECT repoId, packageId, app.name, summary + 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)""") + fun getAppListItems(packageNames: List): LiveData> + + override fun getInstalledAppListItems(packageManager: PackageManager): LiveData> { + val installedPackages = packageManager.getInstalledPackages(0) + .associateBy { packageInfo -> packageInfo.packageName } + val packageNames = installedPackages.keys.toList() + return getAppListItems(packageNames).map(packageManager, installedPackages) + } + + @Query("""SELECT COUNT(DISTINCT packageId) FROM AppMetadata JOIN RepositoryPreferences AS pref USING (repoId) WHERE pref.enabled = 1 AND categories LIKE '%' || :category || '%'""") override fun getNumberOfAppsInCategory(category: String): Int diff --git a/database/src/main/java/org/fdroid/database/Converters.kt b/database/src/main/java/org/fdroid/database/Converters.kt index f5c263fc2..80a06b62d 100644 --- a/database/src/main/java/org/fdroid/database/Converters.kt +++ b/database/src/main/java/org/fdroid/database/Converters.kt @@ -6,7 +6,7 @@ import kotlinx.serialization.builtins.serializer import org.fdroid.index.IndexParser.json import org.fdroid.index.v2.LocalizedTextV2 -internal class Converters { +internal object Converters { private val localizedTextV2Serializer = MapSerializer(String.serializer(), String.serializer()) private val mapOfLocalizedTextV2Serializer = diff --git a/database/src/main/java/org/fdroid/database/RepositoryDao.kt b/database/src/main/java/org/fdroid/database/RepositoryDao.kt index 91b925288..1a724d5bc 100644 --- a/database/src/main/java/org/fdroid/database/RepositoryDao.kt +++ b/database/src/main/java/org/fdroid/database/RepositoryDao.kt @@ -48,8 +48,6 @@ public interface RepositoryDao { fun updateUserMirrors(repoId: Long, mirrors: List) fun updateUsernameAndPassword(repoId: Long, username: String?, password: String?) fun updateDisabledMirrors(repoId: Long, disabledMirrors: List) - - // FIXME: We probably want unique categories here flattened by repo weight fun getLiveCategories(): LiveData> }