From 88f817e5caf73d92876bda7b923a26380d7122c9 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 26 Nov 2025 18:45:37 -0300 Subject: [PATCH 001/180] [db] allow providing PackageInfo maps to avoid hammering PackageManager The app may maintain PackageInfo for apps already, so there's no need to keep asking the system for it. We could instead just work with what we already have. Also, we discovered that chunking the package names isn't needed for newer Android versions. This is only relevant for whom has more than 999 apps installed. --- .../org/fdroid/database/AppListItemsTest.kt | 136 +++++++++++++++--- .../main/java/org/fdroid/database/AppDao.kt | 36 ++++- .../java/org/fdroid/database/AppPrefsDao.kt | 11 +- .../java/org/fdroid/database/VersionDao.kt | 4 +- 4 files changed, 161 insertions(+), 26 deletions(-) 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 460a1f462..d25bae15f 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/AppListItemsTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/AppListItemsTest.kt @@ -6,6 +6,8 @@ import androidx.core.content.pm.PackageInfoCompat import androidx.test.ext.junit.runners.AndroidJUnit4 import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import org.fdroid.LocaleChooser.getBestLocale import org.fdroid.database.AppListSortOrder.LAST_UPDATED import org.fdroid.database.AppListSortOrder.NAME @@ -17,7 +19,6 @@ import org.fdroid.test.TestUtils.getRandomString import org.fdroid.test.TestVersionUtils.getRandomPackageVersionV2 import org.junit.Test import org.junit.runner.RunWith -import kotlin.collections.map import kotlin.random.Random import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -74,8 +75,10 @@ internal class AppListItemsTest : AppTest() { appDao.getAppListItems(pm, "Two", NAME).getOrFail().let { apps -> assertEquals(1, apps.size) assertEquals(app2, apps[0]) - assertEquals(PackageInfoCompat.getLongVersionCode(packageInfo2), - apps[0].installedVersionCode) + assertEquals( + PackageInfoCompat.getLongVersionCode(packageInfo2), + apps[0].installedVersionCode + ) assertEquals(packageInfo2.versionName, apps[0].installedVersionName) } @@ -127,8 +130,10 @@ internal class AppListItemsTest : AppTest() { appDao.getAppListItems(pm, "A", "Two", NAME).getOrFail().let { apps -> assertEquals(1, apps.size) assertEquals(app2, apps[0]) - assertEquals(PackageInfoCompat.getLongVersionCode(packageInfo2), - apps[0].installedVersionCode) + assertEquals( + PackageInfoCompat.getLongVersionCode(packageInfo2), + apps[0].installedVersionCode + ) assertEquals(packageInfo2.versionName, apps[0].installedVersionName) } @@ -190,8 +195,10 @@ internal class AppListItemsTest : AppTest() { assertEquals(2, apps.size) assertEquals(app3a, apps[0]) assertEquals(app2, apps[1]) - assertEquals(PackageInfoCompat.getLongVersionCode(packageInfo2), - apps[1].installedVersionCode) + assertEquals( + PackageInfoCompat.getLongVersionCode(packageInfo2), + apps[1].installedVersionCode + ) assertEquals(packageInfo2.versionName, apps[1].installedVersionName) } @@ -201,8 +208,10 @@ internal class AppListItemsTest : AppTest() { val sortedApps = apps.sortedBy { it.lastUpdated } assertEquals(app2, sortedApps[0]) assertEquals(app3a, sortedApps[1]) - assertEquals(PackageInfoCompat.getLongVersionCode(packageInfo2), - sortedApps[0].installedVersionCode) + assertEquals( + PackageInfoCompat.getLongVersionCode(packageInfo2), + sortedApps[0].installedVersionCode + ) assertEquals(packageInfo2.versionName, sortedApps[0].installedVersionName) } @@ -577,6 +586,50 @@ internal class AppListItemsTest : AppTest() { } } + @Test + fun testGetInstalledAppListItemsFlow() = 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) + + // define packageInfo for each test + val packageInfo1 = PackageInfo().apply { + packageName = packageName1 + versionName = getRandomString() + versionCode = Random.nextInt(1, Int.MAX_VALUE) + } + val packageInfo2 = PackageInfo().apply { packageName = packageName2 } + val packageInfo3 = PackageInfo().apply { packageName = packageName3 } + + // all apps get returned, if we consider all of them installed + val pmInfoMap1 = mapOf( + packageName1 to packageInfo1, + packageName2 to packageInfo2, + packageName3 to packageInfo3, + ) + assertEquals(3, appDao.getInstalledAppListItems(pmInfoMap1).first().size) + + // one apps get returned, if we consider only that one installed + val pmInfoMap2 = mapOf(packageName1 to packageInfo1) + appDao.getInstalledAppListItems(pmInfoMap2).first().let { apps -> + assertEquals(1, apps.size) + assertEquals(app1, apps[0]) + // version code and version name gets taken from supplied packageInfo + assertEquals( + PackageInfoCompat.getLongVersionCode(packageInfo1), + apps[0].installedVersionCode + ) + assertEquals(packageInfo1.versionName, apps[0].installedVersionName) + } + + // no app gets returned, if we consider none installed + appDao.getInstalledAppListItems(emptyMap()).first().let { apps -> + assertEquals(0, apps.size) + } + } + @Test fun testGetInstalledAppListItemsMaxVars() { // insert an app @@ -592,12 +645,12 @@ internal class AppListItemsTest : AppTest() { } val packageInfo = packageInfoCreator(packageName) - // sqlite has a maximum number of 999 variables that can be used in a query + // sqlite (before 3.32.0) has a maximum number of 999 variables that can be used in a query // one additional package info is added to the package lists with each test case val listPackageInfo = listOf(packageInfo) - val packageInfoOk = MutableList(998) { packageInfoCreator(getRandomString()) } - val packageInfoNotOk1 = MutableList(999) { packageInfoCreator(getRandomString()) } - val packageInfoNotOk2 = MutableList(5000) { packageInfoCreator(getRandomString()) } + val packageInfoOk = MutableList(998) { packageInfoCreator("${it + 1}") } + val packageInfoNotOk1 = MutableList(999) { packageInfoCreator("${it + 1}") } + val packageInfoNotOk2 = MutableList(5000) { packageInfoCreator("${it + 1}") } // app gets returned no matter how many packages are installed every { pm.getInstalledPackages(0) } returns packageInfoOk + listPackageInfo @@ -616,6 +669,51 @@ internal class AppListItemsTest : AppTest() { assertNotNull(appDao.getInstalledAppListItems(pm).getOrFail()[0].installedVersionName) } + @Test + fun testGetInstalledAppListItemsFlowMaxVars(): Unit = runBlocking { + // insert an app + val repoId = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId, packageName, app1, locales) + + val packageInfoCreator = { name: String -> + PackageInfo().apply { + packageName = name + versionName = name + versionCode = Random.nextInt(1, Int.MAX_VALUE) + } + } + val packageInfo = packageInfoCreator(packageName) + // sqlite (before 3.32.0) has a maximum number of 999 variables that can be used in a query + // one additional package info is added to the package lists with each test case + val packageInfoOk = mutableMapOf().apply { + for (i in 2..999) set("$i", packageInfoCreator("$i")) + set(packageName, packageInfo) + } + val packageInfoNotOk1 = mutableMapOf().apply { + for (i in 2..1000) set("$i", packageInfoCreator("$i")) + set(packageName, packageInfo) + } + val packageInfoNotOk2 = mutableMapOf().apply { + for (i in 2..5000) set("$i", packageInfoCreator("$i")) + set(packageName, packageInfo) + } + // app gets returned no matter how many packages are installed + assertEquals(1, appDao.getInstalledAppListItems(packageInfoOk).first().size) + assertEquals(1, appDao.getInstalledAppListItems(packageInfoNotOk1).first().size) + assertEquals(1, appDao.getInstalledAppListItems(packageInfoNotOk2).first().size) + + // ensure they have version info set + assertNotNull( + appDao.getInstalledAppListItems(packageInfoOk).first()[0].installedVersionName + ) + assertNotNull( + appDao.getInstalledAppListItems(packageInfoNotOk1).first()[0].installedVersionName + ) + assertNotNull( + appDao.getInstalledAppListItems(packageInfoNotOk2).first()[0].installedVersionName + ) + } + // region author tests @Test fun testAuthor_NoApp() { @@ -625,8 +723,10 @@ internal class AppListItemsTest : AppTest() { assertFalse(appDao.hasAuthorMoreThanOneApp(author).getOrFail()) assertTrue(appDao.getAppListItemsForAuthor(pm, author, null, NAME).getOrFail().isEmpty()) - assertTrue(appDao.getAppListItemsForAuthor(pm, author, null, LAST_UPDATED) - .getOrFail().isEmpty()) + assertTrue( + appDao.getAppListItemsForAuthor(pm, author, null, LAST_UPDATED) + .getOrFail().isEmpty() + ) } @Test @@ -640,8 +740,10 @@ internal class AppListItemsTest : AppTest() { assertFalse(appDao.hasAuthorMoreThanOneApp(author).getOrFail()) val appsForAuthor = appDao.getAppListItemsForAuthor(pm, author, null, NAME).getOrFail() assertEquals(1, appsForAuthor.size) - assertEquals(1, appDao.getAppListItemsForAuthor(pm, author, null, LAST_UPDATED) - .getOrFail().size) + assertEquals( + 1, appDao.getAppListItemsForAuthor(pm, author, null, LAST_UPDATED) + .getOrFail().size + ) assertEquals(packageName, appsForAuthor[0].packageName) } 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 9541bdfae..125e72bcb 100644 --- a/libs/database/src/main/java/org/fdroid/database/AppDao.kt +++ b/libs/database/src/main/java/org/fdroid/database/AppDao.kt @@ -3,12 +3,14 @@ package org.fdroid.database import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.res.Resources +import android.os.Build.VERSION.SDK_INT import androidx.annotation.VisibleForTesting 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.asFlow import androidx.lifecycle.map import androidx.room.Dao import androidx.room.Insert @@ -189,6 +191,9 @@ public interface AppDao { public fun hasAuthorMoreThanOneApp(author: String): LiveData public fun getInstalledAppListItems(packageManager: PackageManager): LiveData> + public fun getInstalledAppListItems( + packageInfoMap: Map, + ): Flow> public suspend fun getAppSearchItems(searchQuery: String): List @@ -682,8 +687,14 @@ internal interface AppDaoInt : AppDao { private fun LiveData>.map( packageManager: PackageManager, - installedPackages: Map = packageManager.getInstalledPackages(0) - .associateBy { packageInfo -> packageInfo.packageName }, + ): LiveData> { + val installedPackages = packageManager.getInstalledPackages(0) + .associateBy { packageInfo -> packageInfo.packageName } + return map(installedPackages) + } + + private fun LiveData>.map( + installedPackages: Map, ) = map { items -> items.map { item -> val packageInfo = installedPackages[item.packageName] @@ -823,15 +834,30 @@ internal interface AppDaoInt : AppDao { val installedPackages = packageManager.getInstalledPackages(0) .associateBy { packageInfo -> packageInfo.packageName } val packageNames = installedPackages.keys.toList() - return if (packageNames.size <= 999) { - getAppListItems(packageNames).map(packageManager, installedPackages) + // since sqlite 3.32.0 the max variables number was increased to 32766 + return if (packageNames.size <= 999 || SDK_INT >= 31) { + getAppListItems(packageNames).map(installedPackages) } else { AppListLiveData().apply { packageNames.chunked(999) { addSource(getAppListItems(it)) } - }.map(packageManager, installedPackages) + }.map(installedPackages) } } + override fun getInstalledAppListItems( + packageInfoMap: Map, + ): Flow> { + val packageNames = packageInfoMap.keys.toList() + // since sqlite 3.32.0 the max variables number was increased to 32766 + return if (packageNames.size <= 999 || SDK_INT >= 31) { + getAppListItems(packageNames).map(packageInfoMap) + } else { + AppListLiveData().apply { + packageNames.chunked(999) { addSource(getAppListItems(it)) } + }.map(packageInfoMap) + }.asFlow() + } + private class AppListLiveData : MediatorLiveData>() { private val list = ArrayList>>() diff --git a/libs/database/src/main/java/org/fdroid/database/AppPrefsDao.kt b/libs/database/src/main/java/org/fdroid/database/AppPrefsDao.kt index 8a55d9cf2..cca5fdd0e 100644 --- a/libs/database/src/main/java/org/fdroid/database/AppPrefsDao.kt +++ b/libs/database/src/main/java/org/fdroid/database/AppPrefsDao.kt @@ -1,5 +1,6 @@ package org.fdroid.database +import android.os.Build.VERSION.SDK_INT import androidx.lifecycle.LiveData import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.map @@ -30,9 +31,13 @@ internal interface AppPrefsDaoInt : AppPrefsDao { fun getAppPrefsOrNull(packageName: String): AppPrefs? fun getPreferredRepos(packageNames: List): Map { - return if (packageNames.size <= 999) getPreferredReposInternal(packageNames) - else HashMap(packageNames.size).also { map -> - packageNames.chunked(999).forEach { map.putAll(getPreferredReposInternal(it)) } + // since sqlite 3.32.0 the max variables number was increased to 32766 + return if (packageNames.size <= 999 || SDK_INT >= 31) { + getPreferredReposInternal(packageNames) + } else { + HashMap(packageNames.size).also { map -> + packageNames.chunked(999).forEach { map.putAll(getPreferredReposInternal(it)) } + } } } diff --git a/libs/database/src/main/java/org/fdroid/database/VersionDao.kt b/libs/database/src/main/java/org/fdroid/database/VersionDao.kt index a67493972..71be99dc3 100644 --- a/libs/database/src/main/java/org/fdroid/database/VersionDao.kt +++ b/libs/database/src/main/java/org/fdroid/database/VersionDao.kt @@ -1,5 +1,6 @@ package org.fdroid.database +import android.os.Build.VERSION.SDK_INT import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Insert @@ -196,7 +197,8 @@ internal interface VersionDaoInt : VersionDao { * so takes [AppPrefs.ignoreVersionCodeUpdate] into account. */ fun getVersions(packageNames: List): List { - return if (packageNames.size <= 999) getVersionsInternal(packageNames) + // since sqlite 3.32.0 (in SDK 31 the max variables number was increased to 32766 + return if (packageNames.size <= 999 || SDK_INT >= 31) getVersionsInternal(packageNames) else packageNames.chunked(999).flatMap { getVersionsInternal(it) } } From 8b4e42935ee5e84c53899023cfc607881ba20afd Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 28 Nov 2025 16:14:37 -0300 Subject: [PATCH 002/180] [db] Add new DbAppChecker which finds updates and apps with issues This is similar to DbUpdateChecker which only finds updates. --- .../src/main/java/org/fdroid/database/App.kt | 2 + .../org/fdroid/database/AppCheckResult.kt | 54 ++++ .../java/org/fdroid/database/DbAppChecker.kt | 242 ++++++++++++++++++ .../org/fdroid/database/DbUpdateChecker.kt | 1 + .../main/java/org/fdroid/index/RepoManager.kt | 5 +- libs/index/api/android/index.api | 2 + .../kotlin/org/fdroid/UpdateChecker.kt | 35 ++- 7 files changed, 332 insertions(+), 9 deletions(-) create mode 100644 libs/database/src/main/java/org/fdroid/database/AppCheckResult.kt create mode 100644 libs/database/src/main/java/org/fdroid/database/DbAppChecker.kt diff --git a/libs/database/src/main/java/org/fdroid/database/App.kt b/libs/database/src/main/java/org/fdroid/database/App.kt index fed489aa6..a9609f9f2 100644 --- a/libs/database/src/main/java/org/fdroid/database/App.kt +++ b/libs/database/src/main/java/org/fdroid/database/App.kt @@ -397,11 +397,13 @@ public data class UpdatableApp internal constructor( public val installedVersionCode: Long, public val installedVersionName: String, public val update: AppVersion, + @Deprecated("Use AppWithIssue instead: UpdateInOtherRepo") public val isFromPreferredRepo: Boolean, /** * If true, this is not necessarily an update (contrary to the class name), * but an app with the `KnownVuln` anti-feature. */ + @Deprecated("Use AppWithIssue instead: KnownVulnerability") public val hasKnownVulnerability: Boolean, public override val name: String? = null, public override val summary: String? = null, diff --git a/libs/database/src/main/java/org/fdroid/database/AppCheckResult.kt b/libs/database/src/main/java/org/fdroid/database/AppCheckResult.kt new file mode 100644 index 000000000..e642cf47b --- /dev/null +++ b/libs/database/src/main/java/org/fdroid/database/AppCheckResult.kt @@ -0,0 +1,54 @@ +package org.fdroid.database + +public data class AppCheckResult( + val updates: List, + val issues: List, +) + +public sealed interface AppWithIssue { + public val packageName: String + public val installVersionName: String + public val issue: AppIssue +} + +public data class AvailableAppWithIssue( + val app: AppOverviewItem, + override val installVersionName: String, + val installVersionCode: Long, + override val issue: AppIssue, +) : AppWithIssue { + override val packageName: String = app.packageName +} + +public data class UnavailableAppWithIssue( + override val packageName: String, + val name: CharSequence?, + override val installVersionName: String, + val installVersionCode: Long, +) : AppWithIssue { + override val issue: AppIssue = NotAvailable +} + +public sealed interface AppIssue + +/** + * An app that we installed in the past, but is no longer available in any (enabled) repository. + */ +public data object NotAvailable : AppIssue + +/** + * An app that can not get updated, because all versions have an incompatible signer. + * There may be compatible versions in another repo. + */ +public data class NoCompatibleSigner(val repoIdWithCompatibleSigner: Long? = null) : AppIssue + +/** + * An app that could get updated, but only from another repo. + */ +public data class UpdateInOtherRepo(val repoIdWithUpdate: Long) : AppIssue + +/** + * Has a known vulnerability and should either get updated or uninstalled. + * @param fromPreferredRepo true if the preferred repo had marked the app with known vulnerability. + */ +public data class KnownVulnerability(val fromPreferredRepo: Boolean) : AppIssue diff --git a/libs/database/src/main/java/org/fdroid/database/DbAppChecker.kt b/libs/database/src/main/java/org/fdroid/database/DbAppChecker.kt new file mode 100644 index 000000000..3a149abe1 --- /dev/null +++ b/libs/database/src/main/java/org/fdroid/database/DbAppChecker.kt @@ -0,0 +1,242 @@ +package org.fdroid.database + +import android.content.Context +import android.content.pm.ApplicationInfo.FLAG_SYSTEM +import android.content.pm.PackageInfo +import android.os.Build.VERSION.SDK_INT +import androidx.core.content.pm.PackageInfoCompat.getLongVersionCode +import org.fdroid.CompatibilityChecker +import org.fdroid.CompatibilityCheckerImpl +import org.fdroid.UpdateChecker +import org.fdroid.index.IndexUtils.getPackageSigner + +public class DbAppChecker( + db: FDroidDatabase, + private val context: Context, + compatibilityChecker: CompatibilityChecker = CompatibilityCheckerImpl(context.packageManager), + private val updateChecker: UpdateChecker = UpdateChecker(compatibilityChecker), +) { + private val appDao = db.getAppDao() as AppDaoInt + private val versionDao = db.getVersionDao() as VersionDaoInt + private val appPrefsDao = db.getAppPrefsDao() as AppPrefsDaoInt + + /** + * Gets all apps that somehow have a special status that warrants the user's attention. + * These can include apps that: + * * have updates available ([UpdatableApp]) + * * can not be updated, because we don't have those apps anymore ([NotAvailable]) + * * can not be updated, because all versions have incompatible signer ([NoCompatibleSigner]) + * * could get updated from another repo ([UpdateInOtherRepo]) + * * have known vulnerabilities ([KnownVulnerability]) + */ + public fun getApps(packageInfoMap: Map): AppCheckResult { + val updatableApps = ArrayList() + val appsWithIssue = ArrayList() + + // get all versions for all packages (irrespective of preferred repo) + // and make them accessible per packageName + val packageNames = packageInfoMap.keys.toList() + val versionsByPackage = HashMap>(packageNames.size) + // TODO add test for an app ignoring all updates, this won't return versions here + versionDao.getVersions(packageNames).forEach { version -> + val versions = versionsByPackage.getOrPut(version.packageName) { ArrayList() } + versions.add(version) + } + // go through all apps (packages) and check for updates + val preferredRepos = appPrefsDao.getPreferredRepos(packageNames) + packageInfoMap.forEach packages@{ (packageName, packageInfo) -> + // get versions for this app and try to find an update in them + val versions = versionsByPackage[packageName] + val flags = packageInfo.applicationInfo?.flags ?: 0 + if (versions.isNullOrEmpty() && flags and FLAG_SYSTEM == 0) { + // we have no versions and no system app, + // so check if we maybe had installed this app in the past + getUnavailableApp(packageInfo, preferredRepos)?.let { unavailableApp -> + appsWithIssue.add(unavailableApp) + } + return@packages // continue + } + // we ignore system apps without version + if (versions == null) return@packages // continue + // get all updates from the versions we found + // these can be from other repos, have incompatible signers or just are KnownVuln + val updates = updateChecker.getUpdates( + versions = versions, + allowedSignersGetter = null, // all signers are allowed + installedVersionCode = getLongVersionCode(packageInfo), + allowedReleaseChannels = null, + includeKnownVulnerabilities = true, + preferencesGetter = { appPrefsDao.getAppPrefsOrNull(packageName) }, + ).toList() + // if there are no updates available, there's nothing left to do for us + if (updates.isEmpty()) return@packages + // we have updates, so now get some data for us to judge those updates + + // get preferred repo for the current app + val preferredRepoId = preferredRepos[packageName] + ?: error("No preferred repo for $packageName") + + // get allowed signers for current app + // always gives us the oldest signer, even if they rotated certs by now + @Suppress("DEPRECATION") + val allowedSigners = packageInfo.signatures?.map { + getPackageSigner(it.toByteArray()) + }?.toSet() ?: error("Got no signatures for $packageName") + + // happy path is a preferred and compatible update, so we look for those first + // for simplicity and safety, we tell the user to make those updates first + updates.forEach { update -> + if (update.isOk(preferredRepoId, allowedSigners)) { + getUpdatableApp( + version = update, + installedVersionCode = getLongVersionCode(packageInfo), + installedVersionName = packageInfo.versionName ?: "???", + )?.let { app -> updatableApps.add(app) } + return@packages + } + } + + // we do have update(s), but there's an issue with them, find out what + // for simplicity, we only consider the issue of the most recent version + val update = updates[0] + val updateSigners = update.signer?.sha256?.toSet() + val hasCompatibleSigner = + updateSigners == null || updateSigners.intersect(allowedSigners).isNotEmpty() + val app = appDao.getAppOverviewItem(preferredRepoId, packageName) ?: return@packages + + // find out the specific issue + val appWithIssue = if (update.hasKnownVulnerability) { + AvailableAppWithIssue( + app = app, + installVersionName = packageInfo.versionName ?: "???", + installVersionCode = getLongVersionCode(packageInfo), + issue = KnownVulnerability(preferredRepoId == update.repoId), + ) + } else if (hasCompatibleSigner) { + // the signer is compatible, so the update must come from a non-preferred repo + AvailableAppWithIssue( + app = app, + installVersionName = packageInfo.versionName ?: "???", + installVersionCode = getLongVersionCode(packageInfo), + issue = UpdateInOtherRepo(update.repoId), + ) + } else { + // no update with compatible signer available, + // check if there's a compatible signer available in a non-preferred repo + val repoIdWithCompatibleSigner = updates.find { + val signers = it.signer?.sha256?.toSet() + signers == null || signers.intersect(allowedSigners).isNotEmpty() + }?.repoId + if (repoIdWithCompatibleSigner == null) { + // all updates are not compatible, we only warn about this, + // if all versions in the preferred repo aren't compatible + val allIncompatible = versions.all { version -> + version.repoId != preferredRepoId || + !version.isOk(preferredRepoId, allowedSigners) + } + if (allIncompatible) { + // most likely the wrong repo was preferred, try to find the right one + val repoId = versions.find { + // treat the current repo as preferred, so we only look at signers + it.isOk(it.repoId, allowedSigners) + }?.repoId + AvailableAppWithIssue( + app = app, + installVersionName = packageInfo.versionName ?: "???", + installVersionCode = getLongVersionCode(packageInfo), + issue = NoCompatibleSigner(repoId), + ) + } else null + } else { + AvailableAppWithIssue( + app = app, + installVersionName = packageInfo.versionName ?: "???", + installVersionCode = getLongVersionCode(packageInfo), + issue = NoCompatibleSigner(repoIdWithCompatibleSigner), + ) + } + } + appWithIssue?.let { appsWithIssue.add(it) } + } + return AppCheckResult( + updates = updatableApps, + issues = appsWithIssue, + ) + } + + /** + * Returns a [UnavailableAppWithIssue], in case the app provided with [packageInfo] + * was installed by us in the past. + */ + private fun getUnavailableApp( + packageInfo: PackageInfo, + preferredRepos: Map, + ): UnavailableAppWithIssue? { + // check if we installed the app or are the current update owner of this app + val weInstalledApp = if (SDK_INT >= 30) { + val installInfo = context.packageManager.getInstallSourceInfo(packageInfo.packageName) + context.packageName == installInfo.initiatingPackageName || + context.packageName == installInfo.installingPackageName || + (SDK_INT >= 34 && context.packageName == installInfo.updateOwnerPackageName) + } else { + @Suppress("DEPRECATION") // no other choice to use this for old API versions + val installer = context.packageManager.getInstallerPackageName(packageInfo.packageName) + context.packageName == installer + } + if (weInstalledApp) { + // we had installed this app, check if we maybe just got no versions + val app = preferredRepos[packageInfo.packageName]?.let { repoId -> + appDao.getAppOverviewItem(repoId, packageInfo.packageName) + } + // we still have the app, so we just didn't get versions for it, + // like when the user was ignoring all updates for the app + if (app != null) return null + // warn the user that this app isn't available anymore + val notAvailable = UnavailableAppWithIssue( + packageName = packageInfo.packageName, + name = packageInfo.applicationInfo?.loadLabel(context.packageManager), + installVersionName = packageInfo.versionName ?: "???", + installVersionCode = getLongVersionCode(packageInfo), + ) + return notAvailable + } + return null + } + + /** + * @return true if this version is an update from the preferred repo with a compatible signer + * and not a known vulnerable version. + */ + private fun Version.isOk(preferredRepoId: Long, signers: Set): Boolean { + val ourSigners = signer?.sha256?.toSet() + return preferredRepoId == repoId && + !hasKnownVulnerability && + (ourSigners == null || ourSigners.intersect(signers).isNotEmpty()) + } + + private fun getUpdatableApp( + version: Version, + installedVersionCode: Long, + installedVersionName: String, + ): UpdatableApp? { + val versionedStrings = versionDao.getVersionedStrings( + repoId = version.repoId, + packageName = version.packageName, + versionId = version.versionId, + ) + val appOverviewItem = + appDao.getAppOverviewItem(version.repoId, version.packageName) ?: return null + return UpdatableApp( + repoId = version.repoId, + packageName = version.packageName, + installedVersionCode = installedVersionCode, + installedVersionName = installedVersionName, + update = version.toAppVersion(versionedStrings), + isFromPreferredRepo = true, + hasKnownVulnerability = version.hasKnownVulnerability, + name = appOverviewItem.name, + summary = appOverviewItem.summary, + localizedIcon = appOverviewItem.localizedIcon, + ) + } +} diff --git a/libs/database/src/main/java/org/fdroid/database/DbUpdateChecker.kt b/libs/database/src/main/java/org/fdroid/database/DbUpdateChecker.kt index 9a04ce1cf..7af7fde9a 100644 --- a/libs/database/src/main/java/org/fdroid/database/DbUpdateChecker.kt +++ b/libs/database/src/main/java/org/fdroid/database/DbUpdateChecker.kt @@ -10,6 +10,7 @@ import org.fdroid.CompatibilityCheckerImpl import org.fdroid.PackagePreference import org.fdroid.UpdateChecker +@Deprecated("Use DbAppChecker instead") public class DbUpdateChecker @JvmOverloads constructor( db: FDroidDatabase, private val packageManager: PackageManager, diff --git a/libs/database/src/main/java/org/fdroid/index/RepoManager.kt b/libs/database/src/main/java/org/fdroid/index/RepoManager.kt index 1c47a150a..273324e98 100644 --- a/libs/database/src/main/java/org/fdroid/index/RepoManager.kt +++ b/libs/database/src/main/java/org/fdroid/index/RepoManager.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.asLiveData import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -203,8 +204,8 @@ public class RepoManager @JvmOverloads constructor( } @AnyThread - public fun setPreferredRepoId(packageName: String, repoId: Long) { - GlobalScope.launch(coroutineContext) { + public fun setPreferredRepoId(packageName: String, repoId: Long): Job { + return GlobalScope.launch(coroutineContext) { db.runInTransaction { val appPrefs = appPrefsDao.getAppPrefsOrNull(packageName) ?: AppPrefs(packageName) appPrefsDao.update(appPrefs.copy(preferredRepoId = repoId)) diff --git a/libs/index/api/android/index.api b/libs/index/api/android/index.api index 0ac289286..de1c69fc2 100644 --- a/libs/index/api/android/index.api +++ b/libs/index/api/android/index.api @@ -36,6 +36,8 @@ public final class org/fdroid/UpdateChecker { public final fun getUpdate (Ljava/util/List;Lkotlin/jvm/functions/Function0;JLjava/util/List;ZLkotlin/jvm/functions/Function0;)Lorg/fdroid/index/v2/PackageVersion; public static synthetic fun getUpdate$default (Lorg/fdroid/UpdateChecker;Ljava/util/List;Landroid/content/pm/PackageInfo;Ljava/util/List;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lorg/fdroid/index/v2/PackageVersion; public static synthetic fun getUpdate$default (Lorg/fdroid/UpdateChecker;Ljava/util/List;Lkotlin/jvm/functions/Function0;JLjava/util/List;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lorg/fdroid/index/v2/PackageVersion; + public final fun getUpdates (Ljava/util/List;Lkotlin/jvm/functions/Function0;JLjava/util/List;ZLkotlin/jvm/functions/Function0;)Lkotlin/sequences/Sequence; + public static synthetic fun getUpdates$default (Lorg/fdroid/UpdateChecker;Ljava/util/List;Lkotlin/jvm/functions/Function0;JLjava/util/List;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/sequences/Sequence; } public final class org/fdroid/index/IndexConverter { diff --git a/libs/index/src/androidMain/kotlin/org/fdroid/UpdateChecker.kt b/libs/index/src/androidMain/kotlin/org/fdroid/UpdateChecker.kt index dbee9c3f3..cfb415df1 100644 --- a/libs/index/src/androidMain/kotlin/org/fdroid/UpdateChecker.kt +++ b/libs/index/src/androidMain/kotlin/org/fdroid/UpdateChecker.kt @@ -93,7 +93,29 @@ public class UpdateChecker( allowedReleaseChannels: List? = null, includeKnownVulnerabilities: Boolean = false, preferencesGetter: (() -> PackagePreference?)? = null, - ): T? { + ): T? = getUpdates( + versions = versions, + allowedSignersGetter = allowedSignersGetter, + installedVersionCode = installedVersionCode, + allowedReleaseChannels = allowedReleaseChannels, + includeKnownVulnerabilities = includeKnownVulnerabilities, + preferencesGetter = preferencesGetter, + ).firstOrNull() // just return matching update with highest version code, don't look at others + + /** + * Same as [getUpdate], but gets a list of all possible updates + * beginning from highest version code. + * + * This usually isn't useful unless you need to pick a certain update with your own criteria. + */ + public fun getUpdates( + versions: List, + allowedSignersGetter: (() -> Set?)? = null, + installedVersionCode: Long = 0, + allowedReleaseChannels: List? = null, + includeKnownVulnerabilities: Boolean = false, + preferencesGetter: (() -> PackagePreference?)? = null, + ): Sequence = sequence { // getting signers is rather expensive, so we only do that when there's update candidates val allowedSigners by lazy { allowedSignersGetter?.let { it() } } versions.iterator().forEach versions@{ version -> @@ -101,9 +123,9 @@ public class UpdateChecker( if (includeKnownVulnerabilities && version.versionCode == installedVersionCode && version.hasKnownVulnerability - ) return version + ) yield(version) // if version code is not higher than installed skip package as list is sorted - if (version.versionCode <= installedVersionCode) return null + if (version.versionCode <= installedVersionCode) return@sequence // we don't support versions that have multiple signers if (version.signer?.hasMultipleSigners == true) return@versions // skip incompatible versions @@ -115,7 +137,7 @@ public class UpdateChecker( // check if release channel of version is allowed val hasAllowedReleaseChannel = hasAllowedReleaseChannel( allowedReleaseChannels = allowedReleaseChannels?.toMutableSet() ?: LinkedHashSet(), - versionReleaseChannels = version.releaseChannels, + versionReleaseChannels = version.releaseChannels?.toSet(), packagePreference = packagePreference, ) if (!hasAllowedReleaseChannel) return@versions @@ -126,14 +148,13 @@ public class UpdateChecker( if (versionSigners.intersect(allowedSigners!!).isEmpty()) return@versions } // no need to see other versions, we got the highest version code per sorting - return version + yield(version) } - return null } private fun hasAllowedReleaseChannel( allowedReleaseChannels: MutableSet, - versionReleaseChannels: List?, + versionReleaseChannels: Set?, packagePreference: PackagePreference?, ): Boolean { // no channels (aka stable version) is always allowed From 41653a80a0739ad610ba2d1cb5eb6803749478f3 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 9 Dec 2025 10:21:51 -0300 Subject: [PATCH 003/180] [db] add a way to get apps for a list of package names this will be used to get the apps most downloaded --- .../main/java/org/fdroid/database/AppDao.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) 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 125e72bcb..8dc055689 100644 --- a/libs/database/src/main/java/org/fdroid/database/AppDao.kt +++ b/libs/database/src/main/java/org/fdroid/database/AppDao.kt @@ -134,6 +134,11 @@ public interface AppDao { */ public suspend fun getAppsByRepository(repoId: Long): List + /** + * Returns apps for the given [packageNames]. + */ + public suspend fun getApps(packageNames: List): List + /** * Same as [getNewApps], but returns an observable [Flow]. */ @@ -144,6 +149,11 @@ public interface AppDao { */ public fun getRecentlyUpdatedAppsFlow(limit: Int = 200): Flow> + /** + * Returns apps for the given [packageNames]. + */ + public fun getAppsFlow(packageNames: List): Flow> + /** * 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. @@ -520,6 +530,20 @@ internal interface AppDaoInt : AppDao { WHERE repoId = :repoId""") override suspend fun getAppsByRepository(repoId: Long): List + override suspend fun getApps(packageNames: List): List { + val placeholders = buildString { + repeat(packageNames.size) { append("?,") } + }.trimEnd(',') + val query = getAppsQuery( + "packageName IN ($placeholders) ORDER BY app.lastUpdated DESC" + ) { statement -> + packageNames.forEachIndexed { i, packageName -> + statement.bindText(i + 1, packageName) + } + } + return getApps(query) + } + override fun getNewAppsFlow(maxAgeInDays: Long): Flow> { val query = getAppsQuery( @@ -542,6 +566,20 @@ internal interface AppDaoInt : AppDao { return getAppsFlow(query) } + override fun getAppsFlow(packageNames: List): Flow> { + val placeholders = buildString { + repeat(packageNames.size) { append("?,") } + }.trimEnd(',') + val query = getAppsQuery( + "packageName IN ($placeholders) ORDER BY app.lastUpdated DESC" + ) { statement -> + packageNames.forEachIndexed { i, packageName -> + statement.bindText(i + 1, packageName) + } + } + return getAppsFlow(query) + } + private fun getAppsQuery( whereQuery: String, onBindStatement: (SQLiteStatement) -> Unit, From 29e2d15386ece40002f499d23df6ea5b8df5bad4 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 10 Dec 2025 11:14:19 -0300 Subject: [PATCH 004/180] [db] Update repositoriesState atomically in RepoManager Otherwise, concurrency issues may cause a broken state such as the same repo being in the list two times. --- .../src/main/java/org/fdroid/index/RepoManager.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/libs/database/src/main/java/org/fdroid/index/RepoManager.kt b/libs/database/src/main/java/org/fdroid/index/RepoManager.kt index 273324e98..5a8cdf2da 100644 --- a/libs/database/src/main/java/org/fdroid/index/RepoManager.kt +++ b/libs/database/src/main/java/org/fdroid/index/RepoManager.kt @@ -87,13 +87,13 @@ public class RepoManager @JvmOverloads constructor( init { // we need to load the repositories first off the UiThread, so it doesn't deadlock GlobalScope.launch(coroutineContext) { - _repositoriesState.value = repositoryDao.getRepositories() + _repositoriesState.update { repositoryDao.getRepositories() } repoCountDownLatch.countDown() withContext(Dispatchers.Main) { // keep observing the repos from the DB // and update internal cache when changes happen db.getRepositoryDao().getLiveRepositories().observeForever { repositories -> - _repositoriesState.value = repositories + _repositoriesState.update { repositories } } } } @@ -149,7 +149,7 @@ public class RepoManager @JvmOverloads constructor( // while this will get updated automatically, getting the update may be slow, // so to speed up the UI, we emit the state change right away (deletion is unlikely to fail) _repositoriesState.update { - _repositoriesState.value.filter { repo -> + it.filter { repo -> // keep only repos that are not the deleted one repo.repoId != repoId } @@ -186,8 +186,10 @@ public class RepoManager @JvmOverloads constructor( val addedRepo = repoAdder.addFetchedRepository() // if repo was added, update state right away, so it becomes available asap if (addedRepo != null) withContext(Dispatchers.Main) { - _repositoriesState.value = _repositoriesState.value.toMutableList().apply { - add(addedRepo) + _repositoriesState.update { + it.toMutableList().apply { + add(addedRepo) + } } } } From 15ad121fc8e5b07cde8fd7d3bbce7717a10b48d9 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 13 Feb 2025 11:52:30 -0300 Subject: [PATCH 005/180] next --- basic/.gitignore | 1 + basic/build.gradle.kts | 66 ++++++ basic/proguard-rules.pro | 21 ++ basic/src/main/AndroidManifest.xml | 23 ++ .../java/org/fdroid/basic/MainActivity.kt | 17 ++ .../fdroid/basic/ui/icons/PackageVariant.kt | 94 ++++++++ .../java/org/fdroid/basic/ui/main/Apps.kt | 134 +++++++++++ .../java/org/fdroid/basic/ui/main/Main.kt | 97 ++++++++ .../fdroid/basic/ui/main/MainOverflowMenu.kt | 51 +++++ .../fdroid/basic/ui/main/apps/AppDetails.kt | 55 +++++ .../org/fdroid/basic/ui/main/apps/AppList.kt | 111 +++++++++ .../basic/ui/main/apps/AppNavigationItem.kt | 11 + .../basic/ui/main/apps/AppSearchInputField.kt | 90 ++++++++ .../fdroid/basic/ui/main/apps/AppsFilter.kt | 212 ++++++++++++++++++ .../fdroid/basic/ui/main/apps/AppsSearch.kt | 89 ++++++++ .../java/org/fdroid/basic/ui/theme/Color.kt | 1 + .../java/org/fdroid/basic/ui/theme/Theme.kt | 1 + .../src/main/java/org/fdroid/fdroid/Compat.kt | 9 + .../drawable-v24/ic_launcher_foreground.xml | 30 +++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + basic/src/main/res/values/colors.xml | 10 + basic/src/main/res/values/strings-next.xml | 6 + basic/src/main/res/values/strings.xml | 1 + basic/src/main/res/values/themes.xml | 5 + gradle/libs.versions.toml | 13 +- settings.gradle | 2 + 28 files changed, 1331 insertions(+), 1 deletion(-) create mode 100644 basic/.gitignore create mode 100644 basic/build.gradle.kts create mode 100644 basic/proguard-rules.pro create mode 100644 basic/src/main/AndroidManifest.xml create mode 100644 basic/src/main/java/org/fdroid/basic/MainActivity.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/icons/PackageVariant.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/Apps.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/Main.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/MainOverflowMenu.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/apps/AppDetails.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/apps/AppList.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/apps/AppNavigationItem.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/apps/AppSearchInputField.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/apps/AppsFilter.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/apps/AppsSearch.kt create mode 120000 basic/src/main/java/org/fdroid/basic/ui/theme/Color.kt create mode 120000 basic/src/main/java/org/fdroid/basic/ui/theme/Theme.kt create mode 100644 basic/src/main/java/org/fdroid/fdroid/Compat.kt create mode 100644 basic/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 basic/src/main/res/drawable/ic_launcher_background.xml create mode 100644 basic/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 basic/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 basic/src/main/res/values/colors.xml create mode 100644 basic/src/main/res/values/strings-next.xml create mode 120000 basic/src/main/res/values/strings.xml create mode 100644 basic/src/main/res/values/themes.xml diff --git a/basic/.gitignore b/basic/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/basic/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/basic/build.gradle.kts b/basic/build.gradle.kts new file mode 100644 index 000000000..0df56bbb3 --- /dev/null +++ b/basic/build.gradle.kts @@ -0,0 +1,66 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.jetbrains.kotlin.parcelize) + alias(libs.plugins.jetbrains.compose.compiler) +} + +android { + namespace = "org.fdroid.basic" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + applicationId = "org.fdroid.next" + minSdk = 23 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material3.adaptive) + implementation(libs.androidx.compose.material3.adaptive.layout) + implementation(libs.androidx.compose.material3.adaptive.navigation) + implementation(libs.androidx.compose.material3.adaptive.navigation.suite.android) + + testImplementation(libs.junit) + + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} diff --git a/basic/proguard-rules.pro b/basic/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/basic/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/basic/src/main/AndroidManifest.xml b/basic/src/main/AndroidManifest.xml new file mode 100644 index 000000000..dde1136bb --- /dev/null +++ b/basic/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/basic/src/main/java/org/fdroid/basic/MainActivity.kt b/basic/src/main/java/org/fdroid/basic/MainActivity.kt new file mode 100644 index 000000000..97e4d9c75 --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/MainActivity.kt @@ -0,0 +1,17 @@ +package org.fdroid.basic + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import org.fdroid.basic.ui.main.Main + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + Main() + } + } +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/icons/PackageVariant.kt b/basic/src/main/java/org/fdroid/basic/ui/icons/PackageVariant.kt new file mode 100644 index 000000000..d05c34437 --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/icons/PackageVariant.kt @@ -0,0 +1,94 @@ +package org.fdroid.basic.ui.icons + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.fdroid.fdroid.ui.theme.FDroidContent + +val PackageVariant: ImageVector + get() { + if (_PackageVariant != null) { + return _PackageVariant!! + } + _PackageVariant = Builder( + name = "packageVariant", defaultWidth = 24.0.dp, defaultHeight + = 24.0.dp, viewportWidth = 24.0f, viewportHeight = 24.0f + ).apply { + path( + fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero + ) { + moveTo(2.0f, 10.96f) + curveTo(1.5f, 10.68f, 1.35f, 10.07f, 1.63f, 9.59f) + lineTo(3.13f, 7.0f) + curveTo(3.24f, 6.8f, 3.41f, 6.66f, 3.6f, 6.58f) + lineTo(11.43f, 2.18f) + curveTo(11.59f, 2.06f, 11.79f, 2.0f, 12.0f, 2.0f) + curveTo(12.21f, 2.0f, 12.41f, 2.06f, 12.57f, 2.18f) + lineTo(20.47f, 6.62f) + curveTo(20.66f, 6.72f, 20.82f, 6.88f, 20.91f, 7.08f) + lineTo(22.36f, 9.6f) + curveTo(22.64f, 10.08f, 22.47f, 10.69f, 22.0f, 10.96f) + lineTo(21.0f, 11.54f) + verticalLineTo(16.5f) + curveTo(21.0f, 16.88f, 20.79f, 17.21f, 20.47f, 17.38f) + lineTo(12.57f, 21.82f) + curveTo(12.41f, 21.94f, 12.21f, 22.0f, 12.0f, 22.0f) + curveTo(11.79f, 22.0f, 11.59f, 21.94f, 11.43f, 21.82f) + lineTo(3.53f, 17.38f) + curveTo(3.21f, 17.21f, 3.0f, 16.88f, 3.0f, 16.5f) + verticalLineTo(10.96f) + curveTo(2.7f, 11.13f, 2.32f, 11.14f, 2.0f, 10.96f) + moveTo(12.0f, 4.15f) + verticalLineTo(4.15f) + lineTo(12.0f, 10.85f) + verticalLineTo(10.85f) + lineTo(17.96f, 7.5f) + lineTo(12.0f, 4.15f) + moveTo(5.0f, 15.91f) + lineTo(11.0f, 19.29f) + verticalLineTo(12.58f) + lineTo(5.0f, 9.21f) + verticalLineTo(15.91f) + moveTo(19.0f, 15.91f) + verticalLineTo(12.69f) + lineTo(14.0f, 15.59f) + curveTo(13.67f, 15.77f, 13.3f, 15.76f, 13.0f, 15.6f) + verticalLineTo(19.29f) + lineTo(19.0f, 15.91f) + moveTo(13.85f, 13.36f) + lineTo(20.13f, 9.73f) + lineTo(19.55f, 8.72f) + lineTo(13.27f, 12.35f) + lineTo(13.85f, 13.36f) + close() + } + } + .build() + return _PackageVariant!! + } + +private var _PackageVariant: ImageVector? = null + +@Preview +@Composable +private fun Preview() { + FDroidContent { + Box(modifier = Modifier.padding(12.dp)) { + Image(imageVector = PackageVariant, contentDescription = "") + } + } +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/Apps.kt b/basic/src/main/java/org/fdroid/basic/ui/main/Apps.kt new file mode 100644 index 000000000..730710ccb --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/Apps.kt @@ -0,0 +1,134 @@ +package org.fdroid.basic.ui.main + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.AnimatedPane +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole.Detail +import androidx.compose.material3.adaptive.layout.PaneAdaptedValue +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewScreenSizes +import kotlinx.coroutines.launch +import org.fdroid.basic.R +import org.fdroid.basic.ui.main.apps.AppDetails +import org.fdroid.basic.ui.main.apps.AppList +import org.fdroid.basic.ui.main.apps.AppNavigationItem +import org.fdroid.basic.ui.main.apps.AppsFilter +import org.fdroid.basic.ui.main.apps.AppsSearch +import org.fdroid.fdroid.ui.theme.FDroidContent + +enum class Sort { + NAME, + LATEST, +} + +const val NUM_ITEMS = 42 + +@Composable +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +fun Apps(modifier: Modifier) { + val navigator = rememberListDetailPaneScaffoldNavigator() + val scope = rememberCoroutineScope() + BackHandler(enabled = navigator.canNavigateBack()) { + scope.launch { + navigator.navigateBack() + } + } + val isDetailVisible = navigator.scaffoldValue[Detail] == PaneAdaptedValue.Expanded + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective, + value = navigator.scaffoldValue, + listPane = { + AnimatedPane { + Column( + modifier.fillMaxSize() + ) { + var filterExpanded by rememberSaveable { mutableStateOf(true) } + var sortBy by rememberSaveable { mutableStateOf(Sort.NAME) } + var onlyInstalledApps by rememberSaveable { mutableStateOf(false) } + val addedCategories = remember { mutableStateListOf() } + val addedRepos = remember { mutableStateListOf() } + val categories = listOf( + stringResource(R.string.category_Time), + stringResource(R.string.category_Games), + stringResource(R.string.category_Money), + stringResource(R.string.category_Reading), + stringResource(R.string.category_Theming), + stringResource(R.string.category_Connectivity), + stringResource(R.string.category_Internet), + stringResource(R.string.category_Navigation), + stringResource(R.string.category_Multimedia), + stringResource(R.string.category_Phone_SMS), + stringResource(R.string.category_Science_Education), + stringResource(R.string.category_Security), + stringResource(R.string.category_Sports_Health), + stringResource(R.string.category_System), + stringResource(R.string.category_Writing), + ) + AppsSearch( + onlyInstalledApps = onlyInstalledApps, + addedCategories = addedCategories, + addedRepos = addedRepos, + toggleFilter = { filterExpanded = !filterExpanded }, + ) + AppsFilter( + filterExpanded = filterExpanded, + sortBy = sortBy, + onlyInstalledApps = onlyInstalledApps, + addedCategories = addedCategories, + addedRepos = addedRepos, + categories = categories, + onSortByChanged = { sortBy = it }, + toggleOnlyInstalledApps = { + onlyInstalledApps = !onlyInstalledApps + }, + ) + AppList( + onlyInstalledApps = onlyInstalledApps, + sortBy = sortBy, + addedCategories = addedCategories, + categories = categories, + currentItem = if (isDetailVisible) { + navigator.currentDestination?.contentKey + } else { + null + }, + ) { + scope.launch { navigator.navigateTo(Detail, it) } + } + } + } + }, + detailPane = { + AnimatedPane { + navigator.currentDestination?.contentKey?.let { + AppDetails( + appItem = it, + ) + } + } + }, + ) +} + +@Preview +@PreviewScreenSizes +@Composable +fun AppsPreview() { + FDroidContent { + Apps(Modifier) + } +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/Main.kt b/basic/src/main/java/org/fdroid/basic/ui/main/Main.kt new file mode 100644 index 000000000..4f5673b99 --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/Main.kt @@ -0,0 +1,97 @@ +package org.fdroid.basic.ui.main + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Apps +import androidx.compose.material.icons.filled.Update +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewScreenSizes +import androidx.window.core.layout.WindowWidthSizeClass +import org.fdroid.basic.R +import org.fdroid.fdroid.ui.theme.FDroidContent + +enum class AppDestinations( + @StringRes val label: Int, + val icon: ImageVector, +) { + APPS(R.string.apps, Icons.Filled.Apps), + UPDATES(R.string.updates, Icons.Filled.Update), +} + +@Composable +fun Main() { + var currentDestination by rememberSaveable { mutableStateOf(AppDestinations.APPS) } + FDroidContent { + val adaptiveInfo = currentWindowAdaptiveInfo() + val customNavSuiteType = with(adaptiveInfo) { + when (windowSizeClass.windowWidthSizeClass) { + WindowWidthSizeClass.COMPACT -> NavigationSuiteType.NavigationBar + else -> NavigationSuiteType.NavigationRail + } + } + NavigationSuiteScaffold( + modifier = Modifier.fillMaxSize(), + layoutType = customNavSuiteType, + navigationSuiteColors = NavigationSuiteDefaults.colors( + navigationBarContainerColor = Color.Transparent, + ), + navigationSuiteItems = { + AppDestinations.entries.forEach { dest -> + item( + icon = { + BadgedBox( + badge = { + if (dest == AppDestinations.UPDATES) { + Badge { + Text(text = "13") + } + } + } + ) { + Icon( + dest.icon, + contentDescription = stringResource(dest.label) + ) + } + }, + label = { Text(stringResource(dest.label)) }, + selected = dest == currentDestination, + onClick = { currentDestination = dest } + ) + } + } + ) { + if (currentDestination == AppDestinations.APPS) Apps(Modifier) + else Text( + text = "TODO", + modifier = Modifier.safeDrawingPadding(), + ) + } + } +} + +@Preview +@PreviewScreenSizes +@Composable +fun MainPreview() { + Main() +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/MainOverflowMenu.kt b/basic/src/main/java/org/fdroid/basic/ui/main/MainOverflowMenu.kt new file mode 100644 index 000000000..ad24ba461 --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/MainOverflowMenu.kt @@ -0,0 +1,51 @@ +package org.fdroid.basic.ui.main + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import org.fdroid.basic.R +import org.fdroid.basic.ui.icons.PackageVariant + +@Composable +fun MainOverFlowMenu(menuExpanded: Boolean, onDismissRequest: () -> Unit) { + DropdownMenu( + expanded = menuExpanded, + onDismissRequest = onDismissRequest) { + DropdownMenuItem( + text = { Text(stringResource(R.string.app_details_repositories)) }, + onClick = { }, + leadingIcon = { + Icon( + PackageVariant, + contentDescription = null + ) + } + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.menu_settings)) }, + onClick = { }, + leadingIcon = { + Icon( + Icons.Filled.Settings, + contentDescription = null + ) + } + ) + DropdownMenuItem( + text = { Text("About") }, + onClick = { }, + leadingIcon = { + Icon( + Icons.Filled.Info, + contentDescription = null + ) + } + ) + } +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppDetails.kt b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppDetails.kt new file mode 100644 index 000000000..1671dc9ac --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppDetails.kt @@ -0,0 +1,55 @@ +package org.fdroid.basic.ui.main.apps + +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Android +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.fdroid.fdroid.ui.theme.FDroidContent + +@Composable +fun AppDetails( + appItem: AppNavigationItem, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .safeDrawingPadding() + .padding(16.dp), + horizontalArrangement = spacedBy(8.dp), + ) { + Icon( + Icons.Filled.Android, + tint = MaterialTheme.colorScheme.secondary, + contentDescription = null, + modifier = Modifier.size(48.dp), + ) + Column { + Text(appItem.name, style = MaterialTheme.typography.headlineMedium) + Text(appItem.summary, style = MaterialTheme.typography.bodyMedium) + } + } +} + +@Preview +@Composable +fun AppDetailsPreview() { + FDroidContent { + val item = AppNavigationItem( + packageName = "foo", + name = "bar", + summary = "This is a nice app!", + ) + AppDetails(item) + } +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppList.kt b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppList.kt new file mode 100644 index 000000000..3d74d8ab4 --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppList.kt @@ -0,0 +1,111 @@ +package org.fdroid.basic.ui.main.apps + +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Android +import androidx.compose.material.icons.filled.NewReleases +import androidx.compose.material3.BadgedBox +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import org.fdroid.basic.ui.main.NUM_ITEMS +import org.fdroid.basic.ui.main.Sort + +@Composable +fun AppList( + onlyInstalledApps: Boolean, + sortBy: Sort, + addedCategories: List, + categories: List, + currentItem: AppNavigationItem?, + onItemClick: (AppNavigationItem) -> Unit, +) { + LazyColumn( + contentPadding = PaddingValues(vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.then( + if (currentItem == null) Modifier + else Modifier.selectableGroup() + ), + ) { + repeat(NUM_ITEMS) { idx -> + if (onlyInstalledApps && idx % 2 > 0) return@repeat + val i = if (sortBy == Sort.NAME) idx else NUM_ITEMS - idx + val category = categories.getOrElse(i) { categories.random() } + if (addedCategories.isNotEmpty() && category !in addedCategories) return@repeat + item { + val navItem = AppNavigationItem( + packageName = "$i", + name = "App $i", + summary = "Summary of the app • $category", + ) + val isSelected = currentItem?.packageName == navItem.packageName + val interactionModifier = if (currentItem == null) { + Modifier.clickable( + onClick = { onItemClick(navItem) } + ) + } else { + Modifier.selectable( + selected = isSelected, + onClick = { onItemClick(navItem) } + ) + } + ListItem( + headlineContent = { Text(navItem.name) }, + supportingContent = { Text(navItem.summary) }, + leadingContent = { + BadgedBox(badge = { + if (i <= 3) Icon( + imageVector = Icons.Filled.NewReleases, + tint = MaterialTheme.colorScheme.primary, + contentDescription = null, modifier = Modifier.size(24.dp), + ) + }) { + Icon( + Icons.Filled.Android, + tint = MaterialTheme.colorScheme.secondary, + contentDescription = null, + ) + } + }, + colors = ListItemDefaults.colors( + containerColor = if (isSelected) { + MaterialTheme.colorScheme.surfaceVariant + } else { + Color.Transparent + } + ), + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 8.dp, + vertical = 4.dp + ) + .then(interactionModifier) + ) + } + } + item { + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) + } + } +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppNavigationItem.kt b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppNavigationItem.kt new file mode 100644 index 000000000..7a7e9f0f9 --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppNavigationItem.kt @@ -0,0 +1,11 @@ +package org.fdroid.basic.ui.main.apps + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +class AppNavigationItem( + val packageName: String, + val name: String, + val summary: String, +): Parcelable diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppSearchInputField.kt b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppSearchInputField.kt new file mode 100644 index 000000000..3cd638025 --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppSearchInputField.kt @@ -0,0 +1,90 @@ +package org.fdroid.basic.ui.main.apps + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.FilterList +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.SearchBarState +import androidx.compose.material3.SearchBarValue +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import kotlinx.coroutines.launch +import org.fdroid.basic.ui.main.MainOverFlowMenu + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun AppSearchInputField( + searchBarState: SearchBarState, + textFieldState: TextFieldState, + toggleFilter: () -> Unit, + showFilterBadge: Boolean +) { + val scope = rememberCoroutineScope() + var menuExpanded by remember { mutableStateOf(false) } + SearchBarDefaults.InputField( + modifier = Modifier, + searchBarState = searchBarState, + textFieldState = textFieldState, + onSearch = { + scope.launch { searchBarState.animateToCollapsed() } + }, + placeholder = { Text("Search...") }, + leadingIcon = { + if (searchBarState.currentValue == SearchBarValue.Expanded) { + IconButton( + onClick = { scope.launch { searchBarState.animateToCollapsed() } } + ) { + Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back") + } + } else { + Icon(Icons.Default.Search, contentDescription = null) + } + }, + trailingIcon = { + if (searchBarState.currentValue == SearchBarValue.Expanded) { + IconButton(onClick = { textFieldState.setTextAndPlaceCursorAtEnd("") }) { + Icon( + Icons.Filled.Clear, + contentDescription = null, + ) + } + } else Row { + IconButton(onClick = toggleFilter) { + BadgedBox(badge = { + if (showFilterBadge) Badge(containerColor = MaterialTheme.colorScheme.secondary) + }) { + Icon( + Icons.Filled.FilterList, + contentDescription = null, + ) + } + } + IconButton(onClick = { menuExpanded = true }) { + Icon( + Icons.Default.MoreVert, + contentDescription = null, + ) + } + MainOverFlowMenu(menuExpanded) { menuExpanded = false } + } + } + ) +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppsFilter.kt b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppsFilter.kt new file mode 100644 index 000000000..267c8c29a --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppsFilter.kt @@ -0,0 +1,212 @@ +package org.fdroid.basic.ui.main.apps + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccessTime +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Done +import androidx.compose.material.icons.filled.SortByAlpha +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.fdroid.basic.R +import org.fdroid.basic.ui.main.Sort + +@Composable +fun ColumnScope.AppsFilter( + filterExpanded: Boolean, + sortBy: Sort, + onlyInstalledApps: Boolean, + addedCategories: MutableList, + addedRepos: MutableList, + categories: List, + onSortByChanged: (Sort) -> Unit, + toggleOnlyInstalledApps: () -> Unit, +) { + AnimatedVisibility(filterExpanded) { + FlowRow( + modifier = Modifier + .padding(horizontal = 16.dp), + horizontalArrangement = spacedBy(16.dp), + ) { + var sortByMenuExpanded by remember { mutableStateOf(false) } + var repoMenuExpanded by remember { mutableStateOf(false) } + var categoryMenuExpanded by remember { mutableStateOf(false) } + addedCategories.forEach { category -> + FilterChip( + selected = true, + trailingIcon = { + Icon( + Icons.Filled.Clear, + null, + modifier = Modifier.size(FilterChipDefaults.IconSize) + ) + }, + label = { + Text(category) + }, + onClick = { + addedCategories.remove(category) + } + ) + } + addedRepos.forEach { repo -> + FilterChip( + selected = true, + trailingIcon = { + Icon( + Icons.Filled.Clear, + null, + modifier = Modifier.size(FilterChipDefaults.IconSize) + ) + }, + label = { + Text(repo) + }, + onClick = { + addedRepos.remove(repo) + } + ) + } + FilterChip( + selected = false, + leadingIcon = { + val vector = when (sortBy) { + Sort.NAME -> Icons.Filled.SortByAlpha + Sort.LATEST -> Icons.Filled.AccessTime + } + Icon(vector, null, modifier = Modifier.size(FilterChipDefaults.IconSize)) + }, + trailingIcon = { + Icon(Icons.Filled.ArrowDropDown, null) + }, + label = { + val s = when (sortBy) { + Sort.NAME -> "Sort by name" + Sort.LATEST -> "Sort by latest" + } + Text(s) + DropdownMenu( + expanded = sortByMenuExpanded, + onDismissRequest = { sortByMenuExpanded = false }, + ) { + DropdownMenuItem( + text = { Text("Sort by name") }, + leadingIcon = { + Icon(Icons.Filled.SortByAlpha, null) + }, + onClick = { + onSortByChanged(Sort.NAME) + sortByMenuExpanded = false + }, + ) + DropdownMenuItem( + text = { Text("Sort by latest") }, + leadingIcon = { + Icon(Icons.Filled.AccessTime, null) + }, + onClick = { + onSortByChanged(Sort.LATEST) + sortByMenuExpanded = false + }, + ) + } + }, + onClick = { sortByMenuExpanded = !sortByMenuExpanded }, + ) + FilterChip( + selected = onlyInstalledApps, + leadingIcon = if (onlyInstalledApps) { + { + Icon( + imageVector = Icons.Filled.Done, + contentDescription = null, + modifier = Modifier.size(FilterChipDefaults.IconSize), + ) + } + } else null, + label = { Text(stringResource(R.string.app_installed)) }, + onClick = toggleOnlyInstalledApps, + ) + FilterChip( + selected = false, + leadingIcon = { + Icon( + Icons.Filled.Add, + null, + modifier = Modifier.size(FilterChipDefaults.IconSize) + ) + }, + label = { + Text("Category") + DropdownMenu( + expanded = categoryMenuExpanded, + onDismissRequest = { categoryMenuExpanded = false }, + ) { + categories.forEach { category -> + DropdownMenuItem( + text = { Text(category) }, + onClick = { + addedCategories.add(category) + categoryMenuExpanded = false + }, + ) + } + } + }, + onClick = { categoryMenuExpanded = !categoryMenuExpanded }, + ) + FilterChip( + selected = false, + leadingIcon = { + Icon( + Icons.Filled.Add, + null, + modifier = Modifier.size(FilterChipDefaults.IconSize) + ) + }, + label = { + Text("Repository") + DropdownMenu( + expanded = repoMenuExpanded, + onDismissRequest = { repoMenuExpanded = false }, + ) { + val repos = listOf( + "F-Droid", + "Guardian Project", + "IzzyOnDroid", + ) + repos.forEach { category -> + DropdownMenuItem( + text = { Text(category) }, + onClick = { + addedRepos.add(category) + repoMenuExpanded = false + }, + ) + } + } + }, + onClick = { repoMenuExpanded = !repoMenuExpanded }, + ) + } + } +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppsSearch.kt b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppsSearch.kt new file mode 100644 index 000000000..78cfdeaff --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppsSearch.kt @@ -0,0 +1,89 @@ +package org.fdroid.basic.ui.main.apps + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.ExpandedFullScreenSearchBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TopSearchBar +import androidx.compose.material3.rememberSearchBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun AppsSearch( + onlyInstalledApps: Boolean, + addedCategories: List, + addedRepos: List, + toggleFilter: () -> Unit, +) { + val textFieldState = rememberTextFieldState() + val scrollBehavior = SearchBarDefaults.enterAlwaysSearchBarScrollBehavior() + val searchBarState = rememberSearchBarState() + val scope = rememberCoroutineScope() + val inputField = @Composable { + AppSearchInputField( + searchBarState = searchBarState, + textFieldState = textFieldState, + toggleFilter = toggleFilter, + showFilterBadge = addedRepos.isNotEmpty() || addedCategories.isNotEmpty() || + onlyInstalledApps, + ) + } + TopSearchBar( + modifier = Modifier, + state = searchBarState, + scrollBehavior = scrollBehavior, + windowInsets = WindowInsets.systemBars, + inputField = inputField, + ) + ExpandedFullScreenSearchBar( + state = searchBarState, + inputField = inputField, + ) { + Column(Modifier.verticalScroll(rememberScrollState())) { + repeat(4) { idx -> + val resultText = "Suggestion $idx" + ListItem(headlineContent = { Text(resultText) }, + supportingContent = { Text("Additional info") }, + leadingContent = { + Icon( + Icons.Filled.Star, + contentDescription = null + ) + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + modifier = Modifier + .clickable { + textFieldState.setTextAndPlaceCursorAtEnd(resultText) + scope.launch { searchBarState.animateToCollapsed() } + } + .fillMaxWidth() + .padding( + horizontal = 16.dp, + vertical = 4.dp + ) + ) + } + } + } +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/theme/Color.kt b/basic/src/main/java/org/fdroid/basic/ui/theme/Color.kt new file mode 120000 index 000000000..9d0fa521d --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/theme/Color.kt @@ -0,0 +1 @@ +../../../../../../../../../app/src/main/java/org/fdroid/fdroid/ui/theme/Color.kt \ No newline at end of file diff --git a/basic/src/main/java/org/fdroid/basic/ui/theme/Theme.kt b/basic/src/main/java/org/fdroid/basic/ui/theme/Theme.kt new file mode 120000 index 000000000..895916611 --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/theme/Theme.kt @@ -0,0 +1 @@ +../../../../../../../../../app/src/main/java/org/fdroid/fdroid/ui/theme/Theme.kt \ No newline at end of file diff --git a/basic/src/main/java/org/fdroid/fdroid/Compat.kt b/basic/src/main/java/org/fdroid/fdroid/Compat.kt new file mode 100644 index 000000000..dc4e8d31d --- /dev/null +++ b/basic/src/main/java/org/fdroid/fdroid/Compat.kt @@ -0,0 +1,9 @@ +package org.fdroid.fdroid + +object Preferences { + + fun get(): Preferences = Preferences + + val isPureBlack: Boolean = true + +} diff --git a/basic/src/main/res/drawable-v24/ic_launcher_foreground.xml b/basic/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..7706ab9e6 --- /dev/null +++ b/basic/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/basic/src/main/res/drawable/ic_launcher_background.xml b/basic/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/basic/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/basic/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/basic/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..b3e26b4c6 --- /dev/null +++ b/basic/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/basic/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/basic/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..b3e26b4c6 --- /dev/null +++ b/basic/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/basic/src/main/res/values/colors.xml b/basic/src/main/res/values/colors.xml new file mode 100644 index 000000000..ca1931bca --- /dev/null +++ b/basic/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + diff --git a/basic/src/main/res/values/strings-next.xml b/basic/src/main/res/values/strings-next.xml new file mode 100644 index 000000000..c5faad631 --- /dev/null +++ b/basic/src/main/res/values/strings-next.xml @@ -0,0 +1,6 @@ + + + + F-Droid Next + + diff --git a/basic/src/main/res/values/strings.xml b/basic/src/main/res/values/strings.xml new file mode 120000 index 000000000..83c4d8695 --- /dev/null +++ b/basic/src/main/res/values/strings.xml @@ -0,0 +1 @@ +../../../../../app/src/main/res/values/strings.xml \ No newline at end of file diff --git a/basic/src/main/res/values/themes.xml b/basic/src/main/res/values/themes.xml new file mode 100644 index 000000000..859ec354d --- /dev/null +++ b/basic/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + + - - - - - - + + + + + + + + diff --git a/app/src/main/res/xml/apk_file_provider.xml b/legacy/src/main/res/xml/apk_file_provider.xml similarity index 100% rename from app/src/main/res/xml/apk_file_provider.xml rename to legacy/src/main/res/xml/apk_file_provider.xml diff --git a/app/src/main/res/xml/backup_extraction_rules.xml b/legacy/src/main/res/xml/backup_extraction_rules.xml similarity index 100% rename from app/src/main/res/xml/backup_extraction_rules.xml rename to legacy/src/main/res/xml/backup_extraction_rules.xml diff --git a/app/src/main/res/xml/backup_rules.xml b/legacy/src/main/res/xml/backup_rules.xml similarity index 100% rename from app/src/main/res/xml/backup_rules.xml rename to legacy/src/main/res/xml/backup_rules.xml diff --git a/app/src/main/res/xml/installer_file_provider.xml b/legacy/src/main/res/xml/installer_file_provider.xml similarity index 100% rename from app/src/main/res/xml/installer_file_provider.xml rename to legacy/src/main/res/xml/installer_file_provider.xml diff --git a/next/src/main/res/xml/locales_config.xml b/legacy/src/main/res/xml/locales_config.xml similarity index 100% rename from next/src/main/res/xml/locales_config.xml rename to legacy/src/main/res/xml/locales_config.xml diff --git a/legacy/src/main/res/xml/network_security_config.xml b/legacy/src/main/res/xml/network_security_config.xml new file mode 100644 index 000000000..cfcccb746 --- /dev/null +++ b/legacy/src/main/res/xml/network_security_config.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + amazonaws.com + + + + f-droid.org + + + + github.com + + + + githubusercontent.com + + + + github.io + + + + gitlab.com + + + + gitlab.io + + + diff --git a/app/src/main/res/xml/preferences.xml b/legacy/src/main/res/xml/preferences.xml similarity index 100% rename from app/src/main/res/xml/preferences.xml rename to legacy/src/main/res/xml/preferences.xml diff --git a/app/src/main/res/xml/searchable.xml b/legacy/src/main/res/xml/searchable.xml similarity index 100% rename from app/src/main/res/xml/searchable.xml rename to legacy/src/main/res/xml/searchable.xml diff --git a/app/src/main/scripts/update-binary b/legacy/src/main/scripts/update-binary similarity index 100% rename from app/src/main/scripts/update-binary rename to legacy/src/main/scripts/update-binary diff --git a/app/src/test/assets/urzip.apk b/legacy/src/test/assets/urzip.apk similarity index 100% rename from app/src/test/assets/urzip.apk rename to legacy/src/test/assets/urzip.apk diff --git a/app/src/test/java/org/fdroid/fdroid/AppUpdateManagerTest.kt b/legacy/src/test/java/org/fdroid/fdroid/AppUpdateManagerTest.kt similarity index 100% rename from app/src/test/java/org/fdroid/fdroid/AppUpdateManagerTest.kt rename to legacy/src/test/java/org/fdroid/fdroid/AppUpdateManagerTest.kt diff --git a/app/src/test/java/org/fdroid/fdroid/PreferencesTest.java b/legacy/src/test/java/org/fdroid/fdroid/PreferencesTest.java similarity index 100% rename from app/src/test/java/org/fdroid/fdroid/PreferencesTest.java rename to legacy/src/test/java/org/fdroid/fdroid/PreferencesTest.java diff --git a/app/src/test/java/org/fdroid/fdroid/RepoUpdateManagerTest.kt b/legacy/src/test/java/org/fdroid/fdroid/RepoUpdateManagerTest.kt similarity index 100% rename from app/src/test/java/org/fdroid/fdroid/RepoUpdateManagerTest.kt rename to legacy/src/test/java/org/fdroid/fdroid/RepoUpdateManagerTest.kt diff --git a/app/src/test/java/org/fdroid/fdroid/RepoUrlsTest.java b/legacy/src/test/java/org/fdroid/fdroid/RepoUrlsTest.java similarity index 100% rename from app/src/test/java/org/fdroid/fdroid/RepoUrlsTest.java rename to legacy/src/test/java/org/fdroid/fdroid/RepoUrlsTest.java diff --git a/app/src/test/java/org/fdroid/fdroid/TestFDroidApp.java b/legacy/src/test/java/org/fdroid/fdroid/TestFDroidApp.java similarity index 100% rename from app/src/test/java/org/fdroid/fdroid/TestFDroidApp.java rename to legacy/src/test/java/org/fdroid/fdroid/TestFDroidApp.java diff --git a/app/src/test/java/org/fdroid/fdroid/TestUtils.java b/legacy/src/test/java/org/fdroid/fdroid/TestUtils.java similarity index 100% rename from app/src/test/java/org/fdroid/fdroid/TestUtils.java rename to legacy/src/test/java/org/fdroid/fdroid/TestUtils.java diff --git a/app/src/test/java/org/fdroid/fdroid/UtilsTest.java b/legacy/src/test/java/org/fdroid/fdroid/UtilsTest.java similarity index 100% rename from app/src/test/java/org/fdroid/fdroid/UtilsTest.java rename to legacy/src/test/java/org/fdroid/fdroid/UtilsTest.java diff --git a/app/src/test/java/org/fdroid/fdroid/data/ApkTest.java b/legacy/src/test/java/org/fdroid/fdroid/data/ApkTest.java similarity index 100% rename from app/src/test/java/org/fdroid/fdroid/data/ApkTest.java rename to legacy/src/test/java/org/fdroid/fdroid/data/ApkTest.java diff --git a/app/src/test/java/org/fdroid/fdroid/data/DBHelperTest.java b/legacy/src/test/java/org/fdroid/fdroid/data/DBHelperTest.java similarity index 100% rename from app/src/test/java/org/fdroid/fdroid/data/DBHelperTest.java rename to legacy/src/test/java/org/fdroid/fdroid/data/DBHelperTest.java diff --git a/app/src/test/java/org/fdroid/fdroid/data/SanitizedFileTest.java b/legacy/src/test/java/org/fdroid/fdroid/data/SanitizedFileTest.java similarity index 100% rename from app/src/test/java/org/fdroid/fdroid/data/SanitizedFileTest.java rename to legacy/src/test/java/org/fdroid/fdroid/data/SanitizedFileTest.java diff --git a/app/src/test/java/org/fdroid/fdroid/data/SuggestedVersionTest.java b/legacy/src/test/java/org/fdroid/fdroid/data/SuggestedVersionTest.java similarity index 100% rename from app/src/test/java/org/fdroid/fdroid/data/SuggestedVersionTest.java rename to legacy/src/test/java/org/fdroid/fdroid/data/SuggestedVersionTest.java diff --git a/app/src/test/java/org/fdroid/fdroid/installer/ApkCacheTest.java b/legacy/src/test/java/org/fdroid/fdroid/installer/ApkCacheTest.java similarity index 100% rename from app/src/test/java/org/fdroid/fdroid/installer/ApkCacheTest.java rename to legacy/src/test/java/org/fdroid/fdroid/installer/ApkCacheTest.java diff --git a/app/src/test/java/org/fdroid/fdroid/installer/FileInstallerTest.java b/legacy/src/test/java/org/fdroid/fdroid/installer/FileInstallerTest.java similarity index 100% rename from app/src/test/java/org/fdroid/fdroid/installer/FileInstallerTest.java rename to legacy/src/test/java/org/fdroid/fdroid/installer/FileInstallerTest.java diff --git a/app/src/test/java/org/fdroid/fdroid/installer/InstallerFactoryTest.java b/legacy/src/test/java/org/fdroid/fdroid/installer/InstallerFactoryTest.java similarity index 100% rename from app/src/test/java/org/fdroid/fdroid/installer/InstallerFactoryTest.java rename to legacy/src/test/java/org/fdroid/fdroid/installer/InstallerFactoryTest.java diff --git a/app/src/test/java/org/fdroid/fdroid/views/AppDetailsAdapterTest.java b/legacy/src/test/java/org/fdroid/fdroid/views/AppDetailsAdapterTest.java similarity index 100% rename from app/src/test/java/org/fdroid/fdroid/views/AppDetailsAdapterTest.java rename to legacy/src/test/java/org/fdroid/fdroid/views/AppDetailsAdapterTest.java diff --git a/app/src/test/java/org/fdroid/fdroid/views/main/MainActivityTest.java b/legacy/src/test/java/org/fdroid/fdroid/views/main/MainActivityTest.java similarity index 100% rename from app/src/test/java/org/fdroid/fdroid/views/main/MainActivityTest.java rename to legacy/src/test/java/org/fdroid/fdroid/views/main/MainActivityTest.java diff --git a/app/src/test/java/org/fdroid/fdroid/work/CleanCacheWorkerTest.java b/legacy/src/test/java/org/fdroid/fdroid/work/CleanCacheWorkerTest.java similarity index 100% rename from app/src/test/java/org/fdroid/fdroid/work/CleanCacheWorkerTest.java rename to legacy/src/test/java/org/fdroid/fdroid/work/CleanCacheWorkerTest.java diff --git a/app/src/test/java/org/fdroid/fdroid/work/FDroidMetricsWorkerTest.java b/legacy/src/test/java/org/fdroid/fdroid/work/FDroidMetricsWorkerTest.java similarity index 100% rename from app/src/test/java/org/fdroid/fdroid/work/FDroidMetricsWorkerTest.java rename to legacy/src/test/java/org/fdroid/fdroid/work/FDroidMetricsWorkerTest.java diff --git a/app/src/test/resources/Norway_bouvet_europe_2.obf.zip b/legacy/src/test/resources/Norway_bouvet_europe_2.obf.zip similarity index 100% rename from app/src/test/resources/Norway_bouvet_europe_2.obf.zip rename to legacy/src/test/resources/Norway_bouvet_europe_2.obf.zip diff --git a/app/src/test/resources/additional_repos.xml b/legacy/src/test/resources/additional_repos.xml similarity index 100% rename from app/src/test/resources/additional_repos.xml rename to legacy/src/test/resources/additional_repos.xml diff --git a/app/src/test/resources/all_fields_index-v1.json b/legacy/src/test/resources/all_fields_index-v1.json similarity index 100% rename from app/src/test/resources/all_fields_index-v1.json rename to legacy/src/test/resources/all_fields_index-v1.json diff --git a/app/src/test/resources/install_history_all b/legacy/src/test/resources/install_history_all similarity index 100% rename from app/src/test/resources/install_history_all rename to legacy/src/test/resources/install_history_all diff --git a/app/src/test/resources/org.fdroid.fdroid.privileged.ota_2110.zip b/legacy/src/test/resources/org.fdroid.fdroid.privileged.ota_2110.zip similarity index 100% rename from app/src/test/resources/org.fdroid.fdroid.privileged.ota_2110.zip rename to legacy/src/test/resources/org.fdroid.fdroid.privileged.ota_2110.zip diff --git a/app/src/test/resources/ugly_additional_repos.xml b/legacy/src/test/resources/ugly_additional_repos.xml similarity index 100% rename from app/src/test/resources/ugly_additional_repos.xml rename to legacy/src/test/resources/ugly_additional_repos.xml diff --git a/app/src/testFull/java/kellinwood/security/zipsigner/ZipSignerTest.java b/legacy/src/testFull/java/kellinwood/security/zipsigner/ZipSignerTest.java similarity index 100% rename from app/src/testFull/java/kellinwood/security/zipsigner/ZipSignerTest.java rename to legacy/src/testFull/java/kellinwood/security/zipsigner/ZipSignerTest.java diff --git a/app/src/testFull/java/org/fdroid/fdroid/nearby/LocalHTTPDManagerTest.java b/legacy/src/testFull/java/org/fdroid/fdroid/nearby/LocalHTTPDManagerTest.java similarity index 100% rename from app/src/testFull/java/org/fdroid/fdroid/nearby/LocalHTTPDManagerTest.java rename to legacy/src/testFull/java/org/fdroid/fdroid/nearby/LocalHTTPDManagerTest.java diff --git a/app/src/testFull/java/org/fdroid/fdroid/nearby/LocalHTTPDTest.java b/legacy/src/testFull/java/org/fdroid/fdroid/nearby/LocalHTTPDTest.java similarity index 100% rename from app/src/testFull/java/org/fdroid/fdroid/nearby/LocalHTTPDTest.java rename to legacy/src/testFull/java/org/fdroid/fdroid/nearby/LocalHTTPDTest.java diff --git a/app/src/testFull/java/org/fdroid/fdroid/nearby/LocalRepoKeyStoreTest.java b/legacy/src/testFull/java/org/fdroid/fdroid/nearby/LocalRepoKeyStoreTest.java similarity index 100% rename from app/src/testFull/java/org/fdroid/fdroid/nearby/LocalRepoKeyStoreTest.java rename to legacy/src/testFull/java/org/fdroid/fdroid/nearby/LocalRepoKeyStoreTest.java diff --git a/app/src/testFull/java/org/fdroid/fdroid/nearby/WifiStateChangeServiceTest.java b/legacy/src/testFull/java/org/fdroid/fdroid/nearby/WifiStateChangeServiceTest.java similarity index 100% rename from app/src/testFull/java/org/fdroid/fdroid/nearby/WifiStateChangeServiceTest.java rename to legacy/src/testFull/java/org/fdroid/fdroid/nearby/WifiStateChangeServiceTest.java diff --git a/app/src/testFull/resources/icon.png b/legacy/src/testFull/resources/icon.png similarity index 100% rename from app/src/testFull/resources/icon.png rename to legacy/src/testFull/resources/icon.png diff --git a/app/src/testFull/resources/index.html b/legacy/src/testFull/resources/index.html similarity index 100% rename from app/src/testFull/resources/index.html rename to legacy/src/testFull/resources/index.html diff --git a/app/src/testFull/resources/test.html b/legacy/src/testFull/resources/test.html similarity index 100% rename from app/src/testFull/resources/test.html rename to legacy/src/testFull/resources/test.html diff --git a/app/src/testFull/resources/urzip.apk b/legacy/src/testFull/resources/urzip.apk similarity index 100% rename from app/src/testFull/resources/urzip.apk rename to legacy/src/testFull/resources/urzip.apk diff --git a/app/tools/download-material-icon.sh b/legacy/tools/download-material-icon.sh similarity index 100% rename from app/tools/download-material-icon.sh rename to legacy/tools/download-material-icon.sh diff --git a/app/tools/svg-to-drawables.sh b/legacy/tools/svg-to-drawables.sh similarity index 100% rename from app/tools/svg-to-drawables.sh rename to legacy/tools/svg-to-drawables.sh diff --git a/app/tools/test-search-intents.sh b/legacy/tools/test-search-intents.sh similarity index 100% rename from app/tools/test-search-intents.sh rename to legacy/tools/test-search-intents.sh diff --git a/next/proguard-rules.pro b/next/proguard-rules.pro deleted file mode 100644 index b37172aee..000000000 --- a/next/proguard-rules.pro +++ /dev/null @@ -1,17 +0,0 @@ --dontobfuscate --keepattributes SourceFile,LineNumberTable,Exceptions - -# Anything less causes issues like not finding primary constructor in ReflectionDiffer --keep class org.fdroid.** {*;} - -# Logging --keep class ch.qos.logback.classic.android.LogcatAppender --keepclassmembers class ch.qos.logback.** { *; } --keepclassmembers class org.slf4j.impl.** { *; } - -# Needed for instrumentation tests (for some werid inexplicable reason) --keep class kotlin.LazyKt --keep class kotlin.collections.CollectionsKt - -# for debugging (comment in when needed) -#-printconfiguration build/outputs/logs/r8-configuration.txt diff --git a/next/src/main/AndroidManifest.xml b/next/src/main/AndroidManifest.xml deleted file mode 100644 index cf22d92b6..000000000 --- a/next/src/main/AndroidManifest.xml +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/next/src/main/kotlin/org/fdroid/fdroid/Compat.kt b/next/src/main/kotlin/org/fdroid/fdroid/Compat.kt deleted file mode 100644 index 1275413f1..000000000 --- a/next/src/main/kotlin/org/fdroid/fdroid/Compat.kt +++ /dev/null @@ -1,11 +0,0 @@ -@file:Suppress("ktlint:standard:filename") - -package org.fdroid.fdroid - -object Preferences { - - fun get(): Preferences = Preferences - - val isPureBlack: Boolean = true - -} diff --git a/next/src/main/kotlin/org/fdroid/fdroid/ui/theme/Color.kt b/next/src/main/kotlin/org/fdroid/fdroid/ui/theme/Color.kt deleted file mode 120000 index 9d0fa521d..000000000 --- a/next/src/main/kotlin/org/fdroid/fdroid/ui/theme/Color.kt +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../../../app/src/main/java/org/fdroid/fdroid/ui/theme/Color.kt \ No newline at end of file diff --git a/next/src/main/res/values/colors.xml b/next/src/main/res/values/colors.xml deleted file mode 100644 index ca1931bca..000000000 --- a/next/src/main/res/values/colors.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF - diff --git a/next/src/main/res/values/themes.xml b/next/src/main/res/values/themes.xml deleted file mode 100644 index 6e59254a1..000000000 --- a/next/src/main/res/values/themes.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - -