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 42e3647e5..aa5d9c329 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/AppListItemsTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/AppListItemsTest.kt @@ -20,6 +20,7 @@ import kotlin.random.Random import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue import kotlin.test.fail @@ -426,6 +427,45 @@ internal class AppListItemsTest : AppTest() { } } + @Test + fun testGetInstalledAppListItemsMaxVars() { + // insert an app + val repoId = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId, packageName, app1, locales) + + val packageInfoCreator = { name: String -> + @Suppress("DEPRECATION") + PackageInfo().apply { + packageName = name + versionName = name + versionCode = Random.nextInt(1, Int.MAX_VALUE) + } + } + val packageInfo = packageInfoCreator(packageName) + + // sqlite has a maximum number of 999 variables that can be used in a query + val listPackageInfo = listOf(packageInfo) + val packageInfoOk = MutableList(999) { packageInfoCreator(getRandomString()) } + val packageInfoNotOk1 = MutableList(1000) { packageInfoCreator(getRandomString()) } + val packageInfoNotOk2 = MutableList(5000) { packageInfoCreator(getRandomString()) } + + // app gets returned no matter how many packages are installed + every { pm.getInstalledPackages(0) } returns packageInfoOk + listPackageInfo + assertEquals(1, appDao.getInstalledAppListItems(pm).getOrFail().size) + every { pm.getInstalledPackages(0) } returns packageInfoNotOk1 + listPackageInfo + assertEquals(1, appDao.getInstalledAppListItems(pm).getOrFail().size) + every { pm.getInstalledPackages(0) } returns packageInfoNotOk2 + listPackageInfo + assertEquals(1, appDao.getInstalledAppListItems(pm).getOrFail().size) + + // ensure they have version info set + every { pm.getInstalledPackages(0) } returns packageInfoOk + listPackageInfo + assertNotNull(appDao.getInstalledAppListItems(pm).getOrFail()[0].installedVersionName) + every { pm.getInstalledPackages(0) } returns packageInfoNotOk1 + listPackageInfo + assertNotNull(appDao.getInstalledAppListItems(pm).getOrFail()[0].installedVersionName) + every { pm.getInstalledPackages(0) } returns packageInfoNotOk2 + listPackageInfo + assertNotNull(appDao.getInstalledAppListItems(pm).getOrFail()[0].installedVersionName) + } + /** * Runs the given block on all getAppListItems* methods. * Uses category "A" as all apps should be in that. 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 c86b56050..c62c324a0 100644 --- a/libs/database/src/main/java/org/fdroid/database/AppDao.kt +++ b/libs/database/src/main/java/org/fdroid/database/AppDao.kt @@ -8,6 +8,7 @@ import androidx.core.content.pm.PackageInfoCompat import androidx.core.os.ConfigurationCompat.getLocales import androidx.core.os.LocaleListCompat import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.map import androidx.room.Dao import androidx.room.Insert @@ -480,6 +481,9 @@ internal interface AppDaoInt : AppDao { ORDER BY localizedName COLLATE NOCASE ASC""") fun getAppListItemsByName(category: String): LiveData> + /** + * Warning: Can not be called with more than 999 [packageNames]. + */ @Transaction @SuppressWarnings(CURSOR_MISMATCH) // no anti-features needed here @Query("""SELECT repoId, packageName, localizedName, localizedSummary, app.lastUpdated, @@ -497,7 +501,40 @@ internal interface AppDaoInt : AppDao { val installedPackages = packageManager.getInstalledPackages(0) .associateBy { packageInfo -> packageInfo.packageName } val packageNames = installedPackages.keys.toList() - return getAppListItems(packageNames).map(packageManager, installedPackages) + return if (packageNames.size <= 999) { + getAppListItems(packageNames).map(packageManager, installedPackages) + } else { + AppListLiveData().apply { + packageNames.chunked(999) { addSource(getAppListItems(it)) } + }.map(packageManager, installedPackages) + } + } + + private class AppListLiveData : MediatorLiveData>() { + private val list = ArrayList>>() + + /** + * Adds the given [liveData] and updates [getValue] with a union of all lists + * once all added [liveData]s changed to a non-null list value. + */ + fun addSource(liveData: LiveData>) { + list.add(liveData) + addSource(liveData) { + var shouldUpdate = true + val result = list.flatMap { + it.value ?: run { + shouldUpdate = false + emptyList() + } + } + if (shouldUpdate) value = result.sortedWith { i1, i2 -> + // we need to re-sort the result, because each liveData is only sorted in itself + val n1 = i1.name ?: "" + val n2 = i2.name ?: "" + n1.compareTo(n2, ignoreCase = true) + } + } + } } //