From 76934c8ae3611178625721994d54d93da43166ea Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 18 Feb 2026 16:56:31 -0300 Subject: [PATCH 01/44] Stop checking legacy in CI It is going away soon anyway --- .gitlab-ci.yml | 109 ++++--------------------------------------------- 1 file changed, 8 insertions(+), 101 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d0f5163a7..6d25d309b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -61,17 +61,11 @@ workflow: - app/build/reports - app/build/outputs/*ml - app/build/outputs/apk - - legacy/core* - - legacy/*.log - - legacy/build/reports - - legacy/build/outputs/*ml - - legacy/build/outputs/apk - libs/*/build/reports - build/reports reports: junit: - app/build/**/TEST-*.xml - - legacy/build/**/TEST-*.xml - libs/*/build/**/TEST-*.xml expire_in: 1 week when: on_failure @@ -95,9 +89,8 @@ app assembleRelease test: - changes: - app/**/* - libs/**/* - - legacy/**/* script: - - ./gradlew :app:assembleDefault :app:testFullDebugUnitTest :legacy:assemble :legacy:testFullDebugUnitTest + - ./gradlew :app:assembleDefault :app:testFullDebugUnitTest artifacts: name: "${CI_PROJECT_PATH}_${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA}" paths: @@ -105,12 +98,9 @@ app assembleRelease test: - app/build/outputs/apk - app/build/outputs/mapping - libs/*/build/reports - - legacy/build/reports - - legacy/build/outputs/apk reports: junit: - app/build/test-results/*/TEST-*.xml - - legacy/build/test-results/*/TEST-*.xml expire_in: 1 week when: always @@ -148,26 +138,9 @@ app lint: - <<: *always-on-these-changes - changes: - app/**/* - - legacy/**/* script: - # always report on lint errors to the build log - - sed -i -e 's,textReport .*,textReport true,' legacy/build.gradle # the tasks "lint", "test", etc don't always include everything - - ./gradlew :app:lint :app:ktfmtCheck :legacy:lint - -legacy checkstyle: - <<: *test-template - stage: lint - rules: - - <<: *always-on-these-changes - - changes: - - legacy/**/* - script: - - ./gradlew :legacy:checkstyle - - python3 tools/checkstyle-to-codeclimate.py --input legacy/build/reports/checkstyle/checkstyle.xml --output gl-checkstyle.json - artifacts: - reports: - codequality: gl-checkstyle.json + - ./gradlew :app:lint :app:ktfmtCheck libs lint: <<: *test-template @@ -181,55 +154,6 @@ libs lint: - ./gradlew :libs:core:ktfmtCheck :libs:database:ktfmtCheck :libs:download:ktfmtCheck :libs:index:ktfmtCheck :libs:sharedTest:ktfmtCheck - ./gradlew checkLegacyAbi -# Reference: https://gitlab.com/components/code-quality-oss/codequality-os-scanners-integration/-/blob/4121970daed111dda84cab4547e1f2951684653c/templates/pmd.yml#L52-92 -legacy lint pmd: - stage: lint - image: - name: registry.gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd:latest - entrypoint: [ "" ] - rules: - - <<: *always-on-these-changes - - changes: - - legacy/**/* - parallel: - matrix: - - PMD_VARIANT: main - PMD_RULESETS: "config/pmd/rules.xml,config/pmd/rules-main.xml" - PMD_FILE_PATHS: - - "legacy/src/main/java" - - PMD_VARIANT: test - PMD_RULESETS: "config/pmd/rules.xml,config/pmd/rules-test.xml" - PMD_FILE_PATHS: - - "legacy/src/test/java" - - "legacy/src/androidTest/java" - before_script: - - apt-get update - - apt-get -qy install --no-install-recommends jq - script: - - find ${PMD_FILE_PATHS[@]} -type f -name '*.java' ! -path '/vendored/*' > .pmd-files.txt - - pmd check --file-list .pmd-files.txt -R ${PMD_RULESETS} -f codeclimate -r gl-code-quality-not-formatted.json --no-fail-on-violation - after_script: - ## Fingerprint is required for reading Codequality: https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool - ## PMD output does not contain fingerprint. Following snippet generates a fingerprint with md5sum for each code quality item and adds a comma at end of each line to format as JSON array - ## This returns true always. PMD rarely outputs codequality items which are not JSON conformant, because they are not properly escaped. - ## For example \w+ instead of \\w+ Therefore following snippet ignores those issues, so that all other lines are represented in the report - - | - sed 's/\\/\\\\/g' gl-code-quality-not-formatted.json | while IFS= read -r line; do - fingerprint=$(echo -n "$line" | md5sum | awk '{print $1}'); - echo "$line" | jq -c --arg fp "$fingerprint" '. + {fingerprint: $fp}' | sed 's/$/,/'; - done > gl-pmd-${PMD_VARIANT}-report.json || true - - # adds square bracket at the beginning for JSON array - - sed -i '1s/^/[/' gl-pmd-${PMD_VARIANT}-report.json - # adds square bracket at the end for JSON array - - sed -i '$s/,$/]/' gl-pmd-${PMD_VARIANT}-report.json - artifacts: - paths: - - gl-pmd-${PMD_VARIANT}-report.json - reports: - codequality: gl-pmd-${PMD_VARIANT}-report.json - when: always - app screenshots: <<: *test-template stage: lint @@ -297,18 +221,6 @@ app weblate merge conflict: - git diff --exit-code - exit $EXITVALUE -legacy errorprone: - extends: .base - stage: lint - rules: - - <<: *always-on-these-changes - - changes: - - legacy/**/* - script: - - sed -i "s@plugins {@plugins{\nid 'net.ltgt.errorprone' version '3.1.0'@" legacy/build.gradle - - cat config/errorprone.gradle >> legacy/build.gradle - - ./gradlew -Dorg.gradle.dependency.verification=lenient :legacy:assembleDebug - libs database schema: stage: lint image: debian:trixie-backports @@ -361,11 +273,12 @@ libs database schema: - echo no | $ANDROID_HOME/cmdline-tools/latest/bin/avdmanager --verbose create avd --name "$NAME_AVD" --package "$AVD" --device "pixel" - df -h - start-emulator.sh + - ./gradlew :app:installBasicDefaultDebug :legacy:installFullDebug - adb shell am start -n org.fdroid.fdroid.debug/org.fdroid.fdroid.views.main.MainActivity - export FLAG="-Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.LargeTest,androidx.test.filters.FlakyTest" - - ./gradlew $FLAG :app:connectedBasicDebugAndroidTest :legacy:connectedFullDebugAndroidTest :libs:database:connectedCheck :libs:download:connectedAndroidTest :libs:index:connectedAndroidTest + - ./gradlew $FLAG :app:connectedAndroidTest :libs:database:connectedCheck :libs:download:connectedAndroidTest :libs:index:connectedAndroidTest - export FLAG="-Pandroid.testInstrumentationRunnerArguments.annotation=androidx.test.filters.FlakyTest" - - for i in {1..5}; do echo "$i" && ./gradlew $FLAG :app:connectedBasicDebugAndroidTest :legacy:connectedFullDebugAndroidTest :libs:database:connectedCheck :libs:download:connectedAndroidTest :libs:index:connectedAndroidTest && break; done || exit 137 + - for i in {1..5}; do echo "$i" && ./gradlew $FLAG :app:connectedAndroidTest :libs:database:connectedCheck :libs:download:connectedAndroidTest :libs:index:connectedAndroidTest && break; done || exit 137 allow_failure: exit_codes: 137 @@ -424,15 +337,9 @@ deploy_nightly: app/src/main/res/values*/strings.xml # add this nightly repo as a enabled repo - jq --slurpfile new_dict config/nightly-repo/repo.json "[(\$new_dict[0] | .address = \"${CI_PROJECT_URL}-nightly/-/raw/master/fdroid/repo\")] + ." app/src/main/assets/default_repos.json | sponge app/src/main/assets/default_repos.json - - sed -i -e '/<\/string-array>/d' -e '/<\/resources>/d' legacy/src/main/res/values/default_repos.xml - - echo "${CI_PROJECT_PATH}-nightly" >> legacy/src/main/res/values/default_repos.xml - - echo "${CI_PROJECT_URL}-nightly/-/raw/master/fdroid/repo" >> legacy/src/main/res/values/default_repos.xml - - cat config/nightly-repo/repo.xml >> legacy/src/main/res/values/default_repos.xml - - export DB=`sed -n 's,.*version *= *\([0-9][0-9]*\).*,\1,p' libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt` - - export versionCode=`printf '%d%05d' $DB $(date '+%s'| cut -b1-8)` - - sed -i "s,^\(\s*versionCode\) *[0-9].*,\1 $versionCode," legacy/build.gradle # build the APKs! - rm -rf app/build/outputs/apk legacy/build/outputs/apk - - ./gradlew :app:assembleBasicNightly :legacy:assembleFullDebug - - mv app/build/outputs/apk/basicNightly/release/app-basic-nightly-release-unsigned.apk app/build/outputs/apk/basicNightly/release/app-debug.apk + - ./gradlew :app:assembleBasicNightly :app:assembleFullNightly + - mv app/build/outputs/apk/basicNightly/release/app-basic-nightly-release-unsigned.apk app/build/outputs/apk/basicNightly/release/app-basic-debug.apk + - mv app/build/outputs/apk/fullNightly/release/app-full-nightly-release-unsigned.apk app/build/outputs/apk/fullNightly/release/app-full-debug.apk - fdroid nightly -v From fdd5dd65bfa0c84223fd33da4d4fff4f1ffbf4a5 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 29 Apr 2026 11:00:23 -0300 Subject: [PATCH 02/44] Fix suggested and installable versions in app details For both we weren't considering the actual version code of the already installed app in case the repository did not have that version --- .../org/fdroid/ui/details/DetailsPresenter.kt | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt index 1e713da20..083486aa0 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt @@ -100,15 +100,21 @@ fun DetailsPresenter( @Suppress("DEPRECATION") // so far we had issues with the new way of getting sigs packageInfo?.signatures?.get(0)?.let { sha256(it.toByteArray()) } } + val installedVersionCode = packageInfo?.let { getLongVersionCode(packageInfo) } val suggestedVersion = remember(versions, appPrefs, installedSigner) { if (versions == null || appPrefs == null) { null } else { - updateChecker.getSuggestedVersion( + // Use getUpdate() instead of getSuggestedVersion() to consider `installedVersionCode`. + // Otherwise, we would default to `installedVersionCode = 0` and get a suggested version + // that can have a smaller version code than the installed one. + updateChecker.getUpdate( versions = versions, - preferredSigner = installedSigner ?: app.metadata.preferredSigner, - releaseChannels = appPrefs.releaseChannels, + installedVersionCode = installedVersionCode ?: 0, + allowedSignersGetter = + (installedSigner ?: app.metadata.preferredSigner)?.let { { setOf(it) } }, + allowedReleaseChannels = appPrefs.releaseChannels, preferencesGetter = { appPrefs }, ) } @@ -151,27 +157,25 @@ fun DetailsPresenter( ) } } - val installedVersionCode = packageInfo?.let { getLongVersionCode(packageInfo) } - val installedVersion = - packageInfo?.let { - val installedVersions = versions?.filter { it.versionCode == installedVersionCode } - when (installedVersions?.size) { - null -> null - 0 -> null - 1 -> installedVersions.first() - // more than version with the same version code, find a matching signer - else -> - installedVersions.find { - val versionSigners = it.signer?.sha256?.toSet() - // F-Droid allows versions without a signer entry, allow those - if (versionSigners != null && installedSigner != null) { - versionSigners.intersect(setOf(installedSigner)).isNotEmpty() - } else { - true - } + val installedVersion = packageInfo?.let { + val installedVersions = versions?.filter { it.versionCode == installedVersionCode } + when (installedVersions?.size) { + null -> null + 0 -> null + 1 -> installedVersions.first() + // more than version with the same version code, find a matching signer + else -> + installedVersions.find { + val versionSigners = it.signer?.sha256?.toSet() + // F-Droid allows versions without a signer entry, allow those + if (versionSigners != null && installedSigner != null) { + versionSigners.intersect(setOf(installedSigner)).isNotEmpty() + } else { + true } - } + } } + } val authorName = app.authorName val authorHasMoreThanOneApp = if (authorName == null) false @@ -257,7 +261,7 @@ fun DetailsPresenter( if (!signerCompatible || installState.showProgress) { false } else { - (installedVersion?.versionCode ?: 0) < version.versionCode + (installedVersionCode ?: 0) < version.versionCode }, ) }, From 01b85e6dbe3281d6a2b234e32d93dde99db1678a Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 4 May 2026 11:46:08 -0300 Subject: [PATCH 03/44] Log when we updated ourselves so we can test if it is feasible to auto-restart the app --- app/src/main/AndroidManifest.xml | 8 +++++++ .../org/fdroid/install/AppUpdateReceiver.kt | 22 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 app/src/main/kotlin/org/fdroid/install/AppUpdateReceiver.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dd96e6967..f898a8c63 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -161,6 +161,14 @@ + + + + + + diff --git a/app/src/main/kotlin/org/fdroid/install/AppUpdateReceiver.kt b/app/src/main/kotlin/org/fdroid/install/AppUpdateReceiver.kt new file mode 100644 index 000000000..12c638acd --- /dev/null +++ b/app/src/main/kotlin/org/fdroid/install/AppUpdateReceiver.kt @@ -0,0 +1,22 @@ +package org.fdroid.install + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.Intent.ACTION_MY_PACKAGE_REPLACED +import androidx.annotation.RequiresApi +import mu.KotlinLogging + +class AppUpdateReceiver : BroadcastReceiver() { + + private val log = KotlinLogging.logger {} + + @RequiresApi(35) + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != ACTION_MY_PACKAGE_REPLACED) { + log.warn { "Unknown action: ${intent.action}" } + return + } + log.info { "Intent received, we just updated ourselves!" } + } +} From 2e1f938bce7262c67c10b1cca7987ef0a45fbb8b Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 4 May 2026 11:52:29 -0300 Subject: [PATCH 04/44] Close keyboard on global search action --- .../kotlin/org/fdroid/ui/search/AppSearchInputField.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/search/AppSearchInputField.kt b/app/src/main/kotlin/org/fdroid/ui/search/AppSearchInputField.kt index 74c2a1b1b..979a5134e 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/AppSearchInputField.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/AppSearchInputField.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.collectLatest @@ -49,12 +50,18 @@ fun AppSearchInputField( } } } + val keyboardController = LocalSoftwareKeyboardController.current SearchBarDefaults.InputField( modifier = modifier, searchBarState = searchBarState, textFieldState = textFieldState, textStyle = MaterialTheme.typography.bodyLarge, - onSearch = { if (it.isSearchable()) scope.launch { onSearch(it) } }, + onSearch = { + if (it.isSearchable()) { + keyboardController?.hide() + scope.launch { onSearch(it) } + } + }, placeholder = { Text(stringResource(R.string.search_placeholder)) }, trailingIcon = { if (textFieldState.text.isNotEmpty()) { From b785bcc874076a4fc9a010dcc5ad2186fd3751a3 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 4 May 2026 14:16:52 -0300 Subject: [PATCH 05/44] [db] Don't crash when server returns error codes when adding repo --- libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt b/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt index c0ee209b7..264cfb719 100644 --- a/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt +++ b/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt @@ -200,6 +200,10 @@ internal class RepoAdder( log.error(e) { "Error fetching repo." } onError(AddRepoError(INVALID_INDEX, e)) return + } catch (e: Exception) { // all other exceptions also need to get caught + log.error(e) { "Error fetching repo." } + onError(AddRepoError(IO_ERROR, e)) + return } // set final result val finalRepo = receivedRepo From 7b42b91b418d2b123125092821cf309f8929c2f3 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 4 May 2026 18:18:20 -0300 Subject: [PATCH 06/44] Visual changes to app details suggested by Ura Design --- .../org/fdroid/ui/categories/CategoryChip.kt | 6 +- .../org/fdroid/ui/details/AntiFeatures.kt | 2 +- .../org/fdroid/ui/details/AppDetails.kt | 58 ++++-------- .../org/fdroid/ui/details/AppDetailsHeader.kt | 89 ++++++++++++++++-- .../repositories/add/AddRepoPreviewScreen.kt | 2 +- .../ui/repositories/add/RepoPreviewHeader.kt | 8 +- .../org/fdroid/ui/utils/PreviewUtils.kt | 3 +- app/src/main/res/values/strings.xml | 3 +- .../fdroid/ui/details/DetailsHeaderTest.kt | 23 +++-- .../org/fdroid/ui/details/DetailsTest.kt | 93 +++++++++++-------- ...sHeaderDownloadingNightTest_3534351a_0.png | 4 +- .../DetailsHeaderDownloadingTest_0.png | 4 +- .../DetailsHeaderInstallTest_0.png | 4 +- .../DetailsHeaderLoadingTest_0.png | 4 +- .../DetailsHeaderOpenTest_0.png | 4 +- .../DetailsHeaderStartingTest_0.png | 4 +- .../DetailsHeaderUpdateTest_0.png | 4 +- .../DetailsInstallTest_9f4e7551_0.png | 4 +- .../DetailsNightTest_150baa59_0.png | 4 +- .../DetailsTestKt/DetailsTest_9f4e7551_0.png | 4 +- .../DetailsUpdateTest_2ed8e27d_0.png | 4 +- .../RepositoriesListNightTest_1def2e7f_0.png | 4 +- .../RepositoriesListTest_2ed8e27d_0.png | 4 +- 23 files changed, 212 insertions(+), 127 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryChip.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryChip.kt index d1848b3da..02afdcaeb 100644 --- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryChip.kt +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryChip.kt @@ -52,7 +52,11 @@ fun CategoryChip( } @Composable -fun CategoryChip(categoryItem: CategoryItem, onClick: () -> Unit, modifier: Modifier = Modifier) { +fun CategoryChip( + categoryItem: CategoryItem, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { AssistChip( onClick = onClick, leadingIcon = { diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt b/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt index fc18fdd08..9c5d6ec32 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt @@ -76,7 +76,7 @@ fun AntiFeatures( ExpandableSection( icon = rememberVectorPainter(Icons.Default.WarningAmber), title = stringResource(R.string.anti_features_title), - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier.padding(start = 16.dp), ) { Column { antiFeatures.forEach { antiFeature -> diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt index 765bba6f7..0dd0d5fd2 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AppSettingsAlt -import androidx.compose.material.icons.filled.Category import androidx.compose.material.icons.filled.ChangeHistory import androidx.compose.material.icons.filled.Code import androidx.compose.material.icons.filled.CurrencyBitcoin @@ -66,7 +65,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.core.os.LocaleListCompat +import com.viktormykhailiv.compose.hints.HintHost import com.viktormykhailiv.compose.hints.HintProperties import com.viktormykhailiv.compose.hints.rememberHint import com.viktormykhailiv.compose.hints.rememberHintAnchorState @@ -76,8 +77,6 @@ import org.fdroid.LocaleChooser.getBestLocale import org.fdroid.R import org.fdroid.install.InstallState import org.fdroid.ui.FDroidContent -import org.fdroid.ui.categories.CategoryChip -import org.fdroid.ui.categories.ChipFlowRow import org.fdroid.ui.icons.License import org.fdroid.ui.icons.Litecoin import org.fdroid.ui.lists.AppListType @@ -146,10 +145,14 @@ fun AppDetails( .onGloballyPositioned { coordinates -> size = coordinates.size } ) { // Header is taking care of top innerPadding - AppDetailsHeader(item, innerPadding) + AppDetailsHeader(item, onNav, innerPadding) AnimatedVisibility(item.showWarnings) { AppDetailsWarnings(item, Modifier.padding(horizontal = 16.dp)) } + // Screenshots + if (item.phoneScreenshots.isNotEmpty()) { + Screenshots(item.networkState.showWarningDialog, item.phoneScreenshots) + } // What's New if ( item.installedVersionCode != null && (item.whatsNew != null || item.app.changelog != null) @@ -186,6 +189,16 @@ fun AppDetails( } } } + // Anti-features + if (!item.antiFeatures.isNullOrEmpty()) { + AntiFeatures( + antiFeatures = item.antiFeatures, + hintAnchor = hintAnchor, + showOnboarding = item.showAntiFeaturesOnboarding, + ) { + coroutineScope.launch { hintController.show(hintAnchor) } + } + } // Description item.description?.let { description -> val maxLines = 3 @@ -210,6 +223,7 @@ fun AppDetails( SelectionContainer { Text( text = htmlDescription, + lineHeight = 22.sp, modifier = Modifier.padding(horizontal = 16.dp).padding(top = 8.dp), ) } @@ -218,6 +232,7 @@ fun AppDetails( AnimatedVisibility(!descriptionExpanded) { Text( text = htmlDescription, + lineHeight = 22.sp, maxLines = maxLines, overflow = TextOverflow.Ellipsis, modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).padding(top = 8.dp), @@ -239,20 +254,6 @@ fun AppDetails( } } } - // Anti-features - if (!item.antiFeatures.isNullOrEmpty()) { - AntiFeatures( - antiFeatures = item.antiFeatures, - hintAnchor = hintAnchor, - showOnboarding = item.showAntiFeaturesOnboarding, - ) { - coroutineScope.launch { hintController.show(hintAnchor) } - } - } - // Screenshots - if (item.phoneScreenshots.isNotEmpty()) { - Screenshots(item.networkState.showWarningDialog, item.phoneScreenshots) - } // Donate card if (item.showDonate) ElevatedCard( @@ -391,25 +392,6 @@ fun AppDetails( } } } - if (!item.categories.isNullOrEmpty()) - ExpandableSection( - icon = rememberVectorPainter(Icons.Default.Category), - title = stringResource(R.string.main_menu__categories), - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - initiallyExpanded = true, - ) { - ChipFlowRow(modifier = Modifier.padding(start = 8.dp)) { - item.categories.forEach { item -> - CategoryChip( - item, - onClick = { - val categoryNav = AppListType.Category(item.name, item.id) - onNav(NavigationKey.AppList(categoryNav)) - }, - ) - } - } - } ExpandableSection( icon = rememberVectorPainter(Icons.Default.AppSettingsAlt), title = stringResource(R.string.technical_info), @@ -469,5 +451,5 @@ fun AppDetailsLoadingPreview() { @Preview @Composable fun AppDetailsPreview() { - FDroidContent { AppDetails(testApp, {}, {}) } + HintHost { FDroidContent { AppDetails(testApp, {}, {}) } } } diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt index 5ad830255..f076e9c2c 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt @@ -21,8 +21,10 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Cancel +import androidx.compose.material3.AssistChip import androidx.compose.material3.BadgedBox import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon @@ -55,9 +57,16 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner @@ -67,11 +76,16 @@ import org.fdroid.R import org.fdroid.download.NetworkState import org.fdroid.install.InstallState import org.fdroid.ui.FDroidContent +import org.fdroid.ui.categories.ChipFlowRow +import org.fdroid.ui.categories.chipHeight +import org.fdroid.ui.lists.AppListType +import org.fdroid.ui.navigation.NavigationKey import org.fdroid.ui.utils.AsyncShimmerImage import org.fdroid.ui.utils.InstalledBadge import org.fdroid.ui.utils.MeteredConnectionDialog import org.fdroid.ui.utils.OfflineBar import org.fdroid.ui.utils.asRelativeTimeString +import org.fdroid.ui.utils.getRepository import org.fdroid.ui.utils.startActivitySafe import org.fdroid.ui.utils.testApp @@ -80,6 +94,7 @@ import org.fdroid.ui.utils.testApp /** Timestamp [now] gets passed in for screenshot tests to have a stable download speed. */ fun AppDetailsHeader( item: AppDetailsItem, + onNav: (NavigationKey) -> Unit, innerPadding: PaddingValues, now: Long = System.currentTimeMillis(), ) { @@ -136,14 +151,48 @@ fun AppDetailsHeader( } Column { SelectionContainer { - Text(item.name, style = MaterialTheme.typography.headlineMediumEmphasized) + Text( + item.name, + style = MaterialTheme.typography.headlineSmallEmphasized, + fontWeight = FontWeight.Medium, + lineHeight = 26.sp, + ) } item.app.authorName?.let { authorName -> SelectionContainer { - Text( - text = stringResource(R.string.author_by, authorName), - style = MaterialTheme.typography.bodyMedium, - ) + if (item.authorHasMoreThanOneApp) { + val title = stringResource(R.string.app_list_author, authorName) + val authorString = stringResource(R.string.by_author_format, authorName) + val startIndex = authorString.indexOf(authorName) + val annotatedString = buildAnnotatedString { + append(authorString) + addLink( + clickable = + LinkAnnotation.Clickable( + tag = "author", + linkInteractionListener = { + onNav(NavigationKey.AppList(AppListType.Author(title, authorName))) + }, + styles = + TextLinkStyles( + style = + SpanStyle( + color = ButtonDefaults.textButtonColors().contentColor, + textDecoration = TextDecoration.None, + ) + ), + ), + start = startIndex, + end = startIndex + authorName.length, + ) + } + Text(text = annotatedString, style = MaterialTheme.typography.bodyMedium) + } else { + Text( + text = stringResource(R.string.by_author_format, authorName), + style = MaterialTheme.typography.bodyMedium, + ) + } } } val lastUpdated = item.app.lastUpdated.asRelativeTimeString() @@ -171,6 +220,22 @@ fun AppDetailsHeader( ) } } + // Category chips + if (!item.categories.isNullOrEmpty()) + ChipFlowRow(modifier = Modifier.padding(start = 8.dp)) { + item.categories.forEach { categoryItem -> + AssistChip( + label = { + Text(text = categoryItem.name, maxLines = 2, overflow = TextOverflow.Ellipsis) + }, + onClick = { + val categoryNav = AppListType.Category(categoryItem.name, categoryItem.id) + onNav(NavigationKey.AppList(categoryNav)) + }, + modifier = Modifier.height(chipHeight), + ) + } + } // Repo Chooser RepoChooser( repos = item.repositories, @@ -340,7 +405,7 @@ fun AppDetailsHeader( @Preview @Composable fun AppDetailsHeaderPreview() { - FDroidContent { Column { AppDetailsHeader(testApp, PaddingValues(top = 8.dp)) } } + FDroidContent { Column { AppDetailsHeader(testApp, {}, PaddingValues(top = 8.dp)) } } } @Preview @@ -353,9 +418,12 @@ private fun InstallPreview() { installedVersion = null, installedVersionCode = null, installedVersionName = null, + repositories = listOf(getRepository(testApp.app.repoId), getRepository()), + preferredRepoId = 42L, suggestedVersion = testApp.versions?.first()?.version, networkState = NetworkState(true, isMetered = true), ), + {}, PaddingValues(top = 8.dp), ) } @@ -372,6 +440,7 @@ private fun UpdatePreview() { suggestedVersion = testApp.versions?.first()?.version, networkState = NetworkState(true, isMetered = true), ), + {}, PaddingValues(top = 8.dp), ) } @@ -384,7 +453,7 @@ private fun PreviewLoading() { FDroidContent { Column { val app = testApp.copy(versions = null) - AppDetailsHeader(app, PaddingValues(top = 8.dp)) + AppDetailsHeader(app, {}, PaddingValues(top = 8.dp)) } } } @@ -409,7 +478,7 @@ private fun PreviewDownloading() { ), networkState = NetworkState(true, isMetered = true), ) - AppDetailsHeader(app, PaddingValues(top = 8.dp)) + AppDetailsHeader(app, {}, PaddingValues(top = 8.dp)) } } } @@ -434,7 +503,7 @@ private fun PreviewDownloadingNight() { ), networkState = NetworkState(true, isMetered = true), ) - AppDetailsHeader(app, PaddingValues(top = 8.dp)) + AppDetailsHeader(app, {}, PaddingValues(top = 8.dp)) } } } @@ -449,7 +518,7 @@ private fun PreviewProgress() { installState = InstallState.Starting("", "", "", 23), networkState = NetworkState(true, isMetered = true), ) - AppDetailsHeader(app, PaddingValues(top = 16.dp)) + AppDetailsHeader(app, {}, PaddingValues(top = 16.dp)) } } } diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoPreviewScreen.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoPreviewScreen.kt index 430c92629..a93bccc19 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoPreviewScreen.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoPreviewScreen.kt @@ -105,7 +105,7 @@ fun AddRepoPreviewScreen( @Composable private fun Preview() { val address = "https://example.org" - val repo = getRepository(address) + val repo = getRepository(address = address) val app1 = object : MinimalApp { override val repoId = 0L diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/add/RepoPreviewHeader.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/add/RepoPreviewHeader.kt index f85476f10..4b6f11c62 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/add/RepoPreviewHeader.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/add/RepoPreviewHeader.kt @@ -140,7 +140,7 @@ fun RepoPreviewHeader( @Composable @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 720, heightDp = 360) fun RepoPreviewScreenNewMirrorPreview() { - val repo = getRepository("https://example.org") + val repo = getRepository(address = "https://example.org") FDroidContent { RepoPreviewHeader( Fetching("https://mirror.example.org", repo, emptyList(), IsNewMirror(0L)), @@ -155,7 +155,7 @@ fun RepoPreviewScreenNewMirrorPreview() { @Composable @Preview fun RepoPreviewScreenNewRepoAndNewMirrorPreview() { - val repo = getRepository("https://example.org") + val repo = getRepository(address = "https://example.org") FDroidContent { RepoPreviewHeader( state = @@ -177,7 +177,7 @@ fun RepoPreviewScreenNewRepoAndNewMirrorPreview() { @Composable fun RepoPreviewScreenExistingRepoPreview() { val address = "https://example.org" - val repo = getRepository(address) + val repo = getRepository(address = address) FDroidContent { RepoPreviewHeader( Fetching(address, repo, emptyList(), IsExistingRepository(0L)), @@ -192,7 +192,7 @@ fun RepoPreviewScreenExistingRepoPreview() { @Preview @Composable fun RepoPreviewScreenExistingMirrorPreview() { - val repo = getRepository("https://example.org") + val repo = getRepository(address = "https://example.org") FDroidContent { RepoPreviewHeader( Fetching("https://mirror.example.org", repo, emptyList(), IsExistingMirror(0L)), diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt index 21b55121f..604933da5 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt @@ -620,13 +620,14 @@ fun getRepoDetailsInfo( } fun getRepository( + repoId: Long = 42L, address: String = "https://example.org/repo", username: String? = "foo", password: String? = "bar", lastError: String? = "NotFoundException FooBar technical blabla", ) = Repository( - repoId = 42L, + repoId = repoId, address = address, timestamp = System.currentTimeMillis() - DAYS.toMillis(4), formatVersion = IndexFormatVersion.ONE, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6eb095730..2ebfb085d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -82,8 +82,6 @@ Interests Miscellaneous - - By %1$s Last updated: %1$s Last updated: %1$s (%2$s) @@ -102,6 +100,7 @@ App incompatible with your device, install anyway? Version + by %s Delete Prompt to send crash reports diff --git a/app/src/screenshotTest/kotlin/org/fdroid/ui/details/DetailsHeaderTest.kt b/app/src/screenshotTest/kotlin/org/fdroid/ui/details/DetailsHeaderTest.kt index 37bf9be54..8e795a8f7 100644 --- a/app/src/screenshotTest/kotlin/org/fdroid/ui/details/DetailsHeaderTest.kt +++ b/app/src/screenshotTest/kotlin/org/fdroid/ui/details/DetailsHeaderTest.kt @@ -10,13 +10,15 @@ import com.android.tools.screenshot.PreviewTest import org.fdroid.download.NetworkState import org.fdroid.install.InstallState import org.fdroid.ui.FDroidContent +import org.fdroid.ui.utils.getRepository import org.fdroid.ui.utils.testApp @Preview @Composable @PreviewTest fun DetailsHeaderOpenTest() { - FDroidContent { Column { AppDetailsHeader(testApp, PaddingValues(top = 8.dp)) } } + FDroidContent { Column { AppDetailsHeader(testApp.copy( + categories = testApp.categories?.subList(0, 2),), {}, PaddingValues(top = 8.dp)) } } } @Preview @@ -30,9 +32,13 @@ private fun DetailsHeaderInstallTest() { installedVersion = null, installedVersionCode = null, installedVersionName = null, + repositories = listOf(getRepository(testApp.app.repoId), getRepository()), + preferredRepoId = 42L, + categories = testApp.categories?.subList(0, 2), suggestedVersion = testApp.versions?.first()?.version, networkState = NetworkState(true, isMetered = true), ), + {}, PaddingValues(top = 8.dp), ) } @@ -47,9 +53,11 @@ private fun DetailsHeaderUpdateTest() { Column { AppDetailsHeader( testApp.copy( + categories = testApp.categories?.subList(0, 2), suggestedVersion = testApp.versions?.first()?.version, networkState = NetworkState(true, isMetered = true), ), + {}, PaddingValues(top = 8.dp), ) } @@ -62,8 +70,8 @@ private fun DetailsHeaderUpdateTest() { private fun DetailsHeaderLoadingTest() { FDroidContent { Column { - val app = testApp.copy(versions = null) - AppDetailsHeader(app, PaddingValues(top = 8.dp)) + val app = testApp.copy(categories = testApp.categories?.subList(0, 2), versions = null) + AppDetailsHeader(app, {}, PaddingValues(top = 8.dp)) } } } @@ -88,9 +96,10 @@ private fun DetailsHeaderDownloadingTest() { totalBytes = 1024 * 1024 * 8, startMillis = now - 2000, ), + categories = testApp.categories?.subList(0, 2), networkState = NetworkState(true, isMetered = true), ) - AppDetailsHeader(app, PaddingValues(top = 8.dp), now) + AppDetailsHeader(app, {}, PaddingValues(top = 8.dp), now) } } } @@ -104,6 +113,7 @@ private fun DetailsHeaderDownloadingNightTest() { val now = System.currentTimeMillis() val app = testApp.copy( + categories = testApp.categories?.subList(0, 2), installState = InstallState.Downloading( name = "", @@ -117,7 +127,7 @@ private fun DetailsHeaderDownloadingNightTest() { ), networkState = NetworkState(true, isMetered = true), ) - AppDetailsHeader(app, PaddingValues(top = 8.dp), now) + AppDetailsHeader(app, {}, PaddingValues(top = 8.dp), now) } } } @@ -130,10 +140,11 @@ private fun DetailsHeaderStartingTest() { Column { val app = testApp.copy( + categories = testApp.categories?.subList(0, 2), installState = InstallState.Starting("", "", "", 23), networkState = NetworkState(true, isMetered = true), ) - AppDetailsHeader(app, PaddingValues(top = 16.dp)) + AppDetailsHeader(app, {}, PaddingValues(top = 16.dp)) } } } diff --git a/app/src/screenshotTest/kotlin/org/fdroid/ui/details/DetailsTest.kt b/app/src/screenshotTest/kotlin/org/fdroid/ui/details/DetailsTest.kt index 464546d0f..e8d4b4f13 100644 --- a/app/src/screenshotTest/kotlin/org/fdroid/ui/details/DetailsTest.kt +++ b/app/src/screenshotTest/kotlin/org/fdroid/ui/details/DetailsTest.kt @@ -14,12 +14,21 @@ import org.fdroid.ui.utils.testApp fun DetailsTest() = ScreenshotTest(showBottomBar = false) { AppDetails( - testApp.copy( - versions = emptyList(), - networkState = NetworkState(isOnline = true, isMetered = false), - ), - {}, - {}, + item = + testApp.copy( + versions = emptyList(), + categories = testApp.categories?.subList(0, 2), + phoneScreenshots = listOf( + "https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png", + "https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png", + "https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png", + "https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png", + "https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png", + ), + networkState = NetworkState(isOnline = true, isMetered = false), + ), + onNav = {}, + onBackNav = {}, ) } @@ -31,19 +40,21 @@ fun DetailsInstallTest() = // reduces information, so we can see the bottom of the expanded page // shows install button instead of open button AppDetails( - testApp.copy( - versions = listOf(testApp.versions!!.first()), - whatsNew = null, - antiFeatures = null, - installedVersion = null, - installedVersionCode = null, - installedVersionName = null, - suggestedVersion = testApp.versions.first().version, - actions = testApp.actions.copy(launchIntent = null), - networkState = NetworkState(isOnline = true, isMetered = false), - ), - {}, - {}, + item = + testApp.copy( + versions = listOf(testApp.versions!!.first()), + whatsNew = null, + antiFeatures = null, + installedVersion = null, + installedVersionCode = null, + installedVersionName = null, + suggestedVersion = testApp.versions.first().version, + categories = testApp.categories?.subList(0, 2), + actions = testApp.actions.copy(launchIntent = null), + networkState = NetworkState(isOnline = true, isMetered = false), + ), + onNav = {}, + onBackNav = {}, ) } @@ -54,23 +65,25 @@ fun DetailsUpdateTest() = ScreenshotTest(showBottomBar = false) { // show update and open button, hide donation options AppDetails( - testApp.copy( - whatsNew = null, - antiFeatures = null, - app = - testApp.app.copy( - donate = null, - bitcoin = null, - litecoin = null, - liberapay = null, - liberapayID = null, - openCollective = null, - ), - suggestedVersion = testApp.versions?.first()?.version, - networkState = NetworkState(isOnline = true, isMetered = false), - ), - {}, - {}, + item = + testApp.copy( + whatsNew = null, + antiFeatures = null, + app = + testApp.app.copy( + donate = null, + bitcoin = null, + litecoin = null, + liberapay = null, + liberapayID = null, + openCollective = null, + ), + suggestedVersion = testApp.versions?.first()?.version, + categories = testApp.categories?.subList(0, 2), + networkState = NetworkState(isOnline = true, isMetered = false), + ), + onNav = {}, + onBackNav = {}, ) } @@ -83,4 +96,10 @@ fun DetailsUpdateTest() = heightDp = 3000, ) fun DetailsNightTest() = - ScreenshotTest(showBottomBar = false) { AppDetails(testApp.copy(whatsNew = "foo bar"), {}, {}) } + ScreenshotTest(showBottomBar = false) { + AppDetails( + item = testApp.copy(categories = testApp.categories?.subList(0, 2), whatsNew = "foo bar"), + onNav = {}, + onBackNav = {}, + ) + } diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingNightTest_3534351a_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingNightTest_3534351a_0.png index fa4d0e568..f21632510 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingNightTest_3534351a_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingNightTest_3534351a_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e7f3085f7ba43f765a1fd85cb3abcffb7d6e559da92fc9c71a353a239786600b -size 46387 +oid sha256:ebd84b700c3d452f4ddaba1f925d4ec8ffb3a3793cf9b78aa0fc8a2ea2f3d20a +size 52414 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingTest_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingTest_0.png index 1e386d0df..425659289 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingTest_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingTest_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03bd16dda3867f2fa18f603d71b9a7d7ac49acf5b8d5e819433226e6dbd315e2 -size 45636 +oid sha256:fad5e0b600e3c5012d73183c31923b02a800f52ffbfdff2e4c4039496702e75c +size 51607 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderInstallTest_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderInstallTest_0.png index 3a56b3e17..505bf8c8f 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderInstallTest_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderInstallTest_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:463781ce40f2043fcf06b9dd2bab783f4a1bd09953a43aa589d9900a572116a2 -size 40879 +oid sha256:d1ceb9b169af1fce879d721d787af65829e1af8875de70ab6ab26a4117c971b3 +size 66478 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderLoadingTest_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderLoadingTest_0.png index 38226478c..7a820c18c 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderLoadingTest_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderLoadingTest_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c9ab0c2a8332ca31f5439ea6b48a8fbf1a6bace086c9a78d2d8581725872fa7 -size 33657 +oid sha256:6988ea4b51c9f032d64e8e574f1e2072812609ab30b851ecf006caf59a593185 +size 39826 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderOpenTest_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderOpenTest_0.png index bf6899c45..2b4b8fc83 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderOpenTest_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderOpenTest_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fb1cf422698465681699ba808216af589264d5994b4863453c33ef513078aef -size 41290 +oid sha256:55cf18d9e5631e166e889ef63370b4bd77b26f626ef127efd42b6267964eaeb5 +size 47337 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderStartingTest_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderStartingTest_0.png index a4fef3c50..1a27d9de6 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderStartingTest_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderStartingTest_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d56fab1b947d2fc36723e406d56016e36c1beb9c1e35a5f66751c6bc7759d27e -size 40685 +oid sha256:1428678f2395d70ec1ad993be926ef83ec168ef3605e0ab2f924e27a9bb9ca9d +size 46678 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderUpdateTest_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderUpdateTest_0.png index b6404e61d..b21302f06 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderUpdateTest_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderUpdateTest_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ef3aad20d6b0b08c5de32ff97f1f634594413934ee4dd144221e47148a79eb2 -size 42769 +oid sha256:2b4cde2904300a76a18ff34da948e4899cb4b4e5e453d2f4c857cde548a7e5c8 +size 48825 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsInstallTest_9f4e7551_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsInstallTest_9f4e7551_0.png index 900497091..9354f2f08 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsInstallTest_9f4e7551_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsInstallTest_9f4e7551_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:716e489ba77720da9e1fe56a7d34a3aea8e9a8907def61eb4479447cf1f7c78d -size 306602 +oid sha256:e717f954f26ade786ebd034a57efffac4da7496dec60d62df4df9d10638ce1a8 +size 280535 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsNightTest_150baa59_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsNightTest_150baa59_0.png index d3e9f4634..7fa8bf40d 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsNightTest_150baa59_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsNightTest_150baa59_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1dba67602ed49df8ee22fe0d53193e17812dcfc8663a8c3da2cf937522287b3e -size 346513 +oid sha256:207181cdf34a52cbb3770540ac912bad1d1c873c90a7df1622126903c9e525d4 +size 346709 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsTest_9f4e7551_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsTest_9f4e7551_0.png index 6cc315a89..6625234cb 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsTest_9f4e7551_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsTest_9f4e7551_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:837fc43bc5799bdc30a27560718434cc07ceb36c7b2c8f80b68fcdfd8e3d924a -size 339991 +oid sha256:bbd1604f0c6daea3b851ab8cf1a54e8072851c64b6a7ce244d3f8425737a1814 +size 328503 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsUpdateTest_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsUpdateTest_2ed8e27d_0.png index c94013b32..e30971499 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsUpdateTest_2ed8e27d_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsUpdateTest_2ed8e27d_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0b8851bd2ddf94fce8d024deaa628418670e741f4afd2e50cb6d8829e603559 -size 158449 +oid sha256:6941a28c801900f78542a6fcb57f61731eb356e48cdaa00e30735f4c51983502 +size 159962 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/repositories/RepositoriesListTestKt/RepositoriesListNightTest_1def2e7f_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/repositories/RepositoriesListTestKt/RepositoriesListNightTest_1def2e7f_0.png index b10113985..ebd19ce4f 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/repositories/RepositoriesListTestKt/RepositoriesListNightTest_1def2e7f_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/repositories/RepositoriesListTestKt/RepositoriesListNightTest_1def2e7f_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e47c65e00ed022537d09355e361156365472742074ebc2614604e684545ba207 -size 82392 +oid sha256:3a61579c31d71a86cb2966aa756f75e51d46e9d748d33320db2b68cd10a2c12e +size 82465 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/repositories/RepositoriesListTestKt/RepositoriesListTest_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/repositories/RepositoriesListTestKt/RepositoriesListTest_2ed8e27d_0.png index e04531ca3..88db9d6f0 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/repositories/RepositoriesListTestKt/RepositoriesListTest_2ed8e27d_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/repositories/RepositoriesListTestKt/RepositoriesListTest_2ed8e27d_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abbedbf99ef0960a5f5e3a3219bfb9ad920e49f72e7f90398226df94661bc274 -size 86216 +oid sha256:e9cfd57bd48a75c62780a75bc20e3702f7e2cc43344a0c698618d21b3199563d +size 86299 From 05d5e73959a0baaa68fcd7ba1a819ff8e6eba026 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 5 May 2026 09:02:14 -0300 Subject: [PATCH 07/44] Stop checking for unused or blank translations as weblate is now doing this for us and leaving this check in place is just causing CI churn --- .gitlab-ci.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6d25d309b..de270c259 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -189,12 +189,9 @@ app tools scripts: - app/**/* script: - apt-get update - - apt-get -qy install --no-install-recommends git python3 + - apt-get -qy install --no-install-recommends python3 - ./tools/check-format-strings.py - ./tools/check-fastlane-whitespace.py - - ./tools/remove-unused-and-blank-translations.py - - echo "These are unused or blank translations that should be removed:" - - git --no-pager diff --ignore-all-space --name-only --exit-code app/src/*/res/values*/strings.xml app weblate merge conflict: stage: lint From eebb8a58b073f90bf2420f4cf33bbfbd958a1034 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 5 May 2026 09:52:29 -0300 Subject: [PATCH 08/44] Handle apps not found in app details --- .../ui/screenshots/DetailsScreenshotTest.kt | 4 +- .../org/fdroid/ui/details/AppDetails.kt | 72 ++++++++++++------- .../org/fdroid/ui/details/AppDetailsHeader.kt | 2 +- .../org/fdroid/ui/details/AppDetailsItem.kt | 8 ++- .../org/fdroid/ui/details/AppDetailsMenu.kt | 2 +- .../fdroid/ui/details/AppDetailsTopAppBar.kt | 34 ++++----- .../fdroid/ui/details/AppDetailsViewModel.kt | 13 ++-- .../fdroid/ui/details/AppDetailsWarnings.kt | 2 +- .../org/fdroid/ui/details/DetailsPresenter.kt | 6 +- .../org/fdroid/ui/details/TechnicalInfo.kt | 2 +- .../kotlin/org/fdroid/ui/details/Versions.kt | 2 +- .../org/fdroid/ui/utils/PreviewUtils.kt | 3 +- .../org/fdroid/ui/details/DetailsTest.kt | 21 ++++-- .../AppDetailsNotFoundTest_0.png | 3 + 14 files changed, 106 insertions(+), 68 deletions(-) create mode 100644 app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/AppDetailsNotFoundTest_0.png diff --git a/app/src/androidTest/java/org/fdroid/ui/screenshots/DetailsScreenshotTest.kt b/app/src/androidTest/java/org/fdroid/ui/screenshots/DetailsScreenshotTest.kt index 16a249076..4c105dc23 100644 --- a/app/src/androidTest/java/org/fdroid/ui/screenshots/DetailsScreenshotTest.kt +++ b/app/src/androidTest/java/org/fdroid/ui/screenshots/DetailsScreenshotTest.kt @@ -4,7 +4,7 @@ import org.fdroid.LocaleChooser.getBestLocale import org.fdroid.download.NetworkState import org.fdroid.install.InstallState import org.fdroid.ui.details.AppDetails -import org.fdroid.ui.details.AppDetailsItem +import org.fdroid.ui.details.LoadedAppDetailsItem import org.fdroid.ui.details.VersionItem import org.fdroid.ui.utils.getAppDetailsActions import org.fdroid.ui.utils.testVersion1 @@ -23,7 +23,7 @@ class DetailsScreenshotTest(localeName: String) : LocalizedScreenshotTest(locale fun appDetails() = screenshotTest("4_Details", showBottomBar = false, dark = true) { localeList -> val item = - AppDetailsItem( + LoadedAppDetailsItem( app = appMetadata, actions = getAppDetailsActions(), installState = InstallState.Unknown, diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt index 0dd0d5fd2..45b2143eb 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt @@ -2,9 +2,11 @@ package org.fdroid.ui.details import android.util.Log import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState @@ -42,6 +44,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.rememberVectorPainter @@ -99,11 +102,21 @@ fun AppDetails( var showInstallError by remember { mutableStateOf(false) } val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarState) if (item == null) BigLoadingIndicator() - else + else { Scaffold( topBar = { AppDetailsTopAppBar(item, topAppBarState, scrollBehavior, onBackNav) }, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), ) { innerPadding -> + if (item is NotFoundAppDetailsItem) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize().padding(innerPadding), + ) { + Text(stringResource(R.string.no_such_app)) + } + return@Scaffold + } + item as LoadedAppDetailsItem // react to install state changes LaunchedEffect(item.installState) { val state = item.installState @@ -413,33 +426,34 @@ fun AppDetails( } } } - if (showInstallError && item != null && item.installState is InstallState.Error) - AlertDialog( - onDismissRequest = { showInstallError = false }, - containerColor = MaterialTheme.colorScheme.errorContainer, - title = { Text(stringResource(R.string.install_error_notify_title, item.name)) }, - text = { - if (item.installState.msg == null) { - Text(stringResource(R.string.app_details_install_error_text)) - } else { - ExpandableSection( - icon = null, - title = stringResource(R.string.app_details_install_error_text), - ) { - SelectionContainer { - Text( - text = item.installState.msg, - fontFamily = FontFamily.Monospace, - modifier = Modifier.padding(top = 8.dp), - ) + if (item is LoadedAppDetailsItem && showInstallError && item.installState is InstallState.Error) + AlertDialog( + onDismissRequest = { showInstallError = false }, + containerColor = MaterialTheme.colorScheme.errorContainer, + title = { Text(stringResource(R.string.install_error_notify_title, item.name)) }, + text = { + if (item.installState.msg == null) { + Text(stringResource(R.string.app_details_install_error_text)) + } else { + ExpandableSection( + icon = null, + title = stringResource(R.string.app_details_install_error_text), + ) { + SelectionContainer { + Text( + text = item.installState.msg, + fontFamily = FontFamily.Monospace, + modifier = Modifier.padding(top = 8.dp), + ) + } } } - } - }, - confirmButton = { - TextButton(onClick = { showInstallError = false }) { Text(stringResource(R.string.ok)) } - }, - ) + }, + confirmButton = { + TextButton(onClick = { showInstallError = false }) { Text(stringResource(R.string.ok)) } + }, + ) + } } @Preview @@ -448,6 +462,12 @@ fun AppDetailsLoadingPreview() { FDroidContent { AppDetails(null, {}, {}) } } +@Preview +@Composable +fun AppDetailsNotFoundPreview() { + FDroidContent { AppDetails(NotFoundAppDetailsItem, {}, {}) } +} + @Preview @Composable fun AppDetailsPreview() { diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt index f076e9c2c..65c41c5a9 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt @@ -93,7 +93,7 @@ import org.fdroid.ui.utils.testApp @OptIn(ExperimentalMaterial3ExpressiveApi::class) /** Timestamp [now] gets passed in for screenshot tests to have a stable download speed. */ fun AppDetailsHeader( - item: AppDetailsItem, + item: LoadedAppDetailsItem, onNav: (NavigationKey) -> Unit, innerPadding: PaddingValues, now: Long = System.currentTimeMillis(), diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt index 98b7e5395..ff88e3147 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt @@ -24,7 +24,11 @@ import org.fdroid.install.SessionInstallManager import org.fdroid.search.SearchHelper.removeZeroWhiteSpace import org.fdroid.ui.categories.CategoryItem -data class AppDetailsItem( +sealed class AppDetailsItem + +data object NotFoundAppDetailsItem : AppDetailsItem() + +data class LoadedAppDetailsItem( val app: AppMetadata, val actions: AppDetailsActions, val installState: InstallState, @@ -66,7 +70,7 @@ data class AppDetailsItem( val issue: AppIssue? = null, val authorHasMoreThanOneApp: Boolean = false, val proxy: ProxyConfig? = null, -) { +) : AppDetailsItem() { constructor( repository: Repository, preferredRepoId: Long, diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsMenu.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsMenu.kt index 9a17b7337..e6c0e9851 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsMenu.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsMenu.kt @@ -28,7 +28,7 @@ import org.fdroid.ui.utils.testApp @Composable fun AppDetailsMenu( - item: AppDetailsItem, + item: LoadedAppDetailsItem, uninstallLauncher: ActivityResultLauncher, onDismiss: () -> Unit, ) { diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsTopAppBar.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsTopAppBar.kt index 43d8f2f8b..05441c084 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsTopAppBar.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsTopAppBar.kt @@ -32,28 +32,30 @@ fun AppDetailsTopAppBar( TopAppBar( colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), title = { - if (topAppBarState.overlappedFraction == 1f) { + if (item is LoadedAppDetailsItem && topAppBarState.overlappedFraction == 1f) { Text(item.name, maxLines = 1, overflow = TextOverflow.Ellipsis) } }, navigationIcon = { if (onBackNav != null) BackButton(onClick = onBackNav) }, actions = { - val context = LocalContext.current - item.actions.shareIntent?.let { shareIntent -> - TopAppBarButton( - imageVector = Icons.Filled.Share, - contentDescription = stringResource(R.string.menu_share), - onClick = { context.startActivitySafe(shareIntent) }, - ) - } - // the launcher needs to be at least here, - // because if the menu is dismissed we don't get the result - val uninstallLauncher = - rememberLauncherForActivityResult(StartActivityForResult()) { - item.actions.onUninstallResult(it) + if (item is LoadedAppDetailsItem) { + val context = LocalContext.current + item.actions.shareIntent?.let { shareIntent -> + TopAppBarButton( + imageVector = Icons.Filled.Share, + contentDescription = stringResource(R.string.menu_share), + onClick = { context.startActivitySafe(shareIntent) }, + ) + } + // the launcher needs to be at least here, + // because if the menu is dismissed we don't get the result + val uninstallLauncher = + rememberLauncherForActivityResult(StartActivityForResult()) { + item.actions.onUninstallResult(it) + } + TopAppBarOverflowButton { onDismissRequest -> + AppDetailsMenu(item, uninstallLauncher, onDismissRequest) } - TopAppBarOverflowButton { onDismissRequest -> - AppDetailsMenu(item, uninstallLauncher, onDismissRequest) } }, scrollBehavior = scrollBehavior, diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt index 11b310122..de11a692a 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt @@ -165,7 +165,7 @@ constructor( @UiThread fun onUninstallResult(activityResult: ActivityResult) { - val name = appDetails.value?.name + val name = (appDetails.value as? LoadedAppDetailsItem)?.name val result = appInstallManager.onUninstallResult(packageName, name, activityResult) log.info { "Uninstall result was: $result" } } @@ -195,7 +195,7 @@ constructor( val diskCache = SingletonImageLoader.get(application).diskCache if (diskCache != null) scope.launch { - appDetails.value?.phoneScreenshots?.forEach { screenshot -> + (appDetails.value as? LoadedAppDetailsItem)?.phoneScreenshots?.forEach { screenshot -> if (screenshot is DownloadRequest) { try { diskCache.remove(screenshot.getCacheKey()) @@ -209,7 +209,7 @@ constructor( @UiThread fun allowBetaUpdates() { - val appPrefs = appDetails.value?.appPrefs ?: return + val appPrefs = (appDetails.value as? LoadedAppDetailsItem)?.appPrefs ?: return scope.launch { db.getAppPrefsDao().update(appPrefs.toggleReleaseChannel(RELEASE_CHANNEL_BETA)) updatesManager.loadUpdates() @@ -218,7 +218,7 @@ constructor( @UiThread fun ignoreAllUpdates() { - val appPrefs = appDetails.value?.appPrefs ?: return + val appPrefs = (appDetails.value as? LoadedAppDetailsItem)?.appPrefs ?: return scope.launch { db.getAppPrefsDao().update(appPrefs.toggleIgnoreAllUpdates()) updatesManager.loadUpdates() @@ -227,8 +227,9 @@ constructor( @UiThread fun ignoreThisUpdate() { - val appPrefs = appDetails.value?.appPrefs ?: return - val versionCode = appDetails.value?.possibleUpdate?.versionCode ?: return + val appPrefs = (appDetails.value as? LoadedAppDetailsItem)?.appPrefs ?: return + val versionCode = + (appDetails.value as? LoadedAppDetailsItem)?.possibleUpdate?.versionCode ?: return scope.launch { db.getAppPrefsDao().update(appPrefs.toggleIgnoreVersionCodeUpdate(versionCode)) updatesManager.loadUpdates() diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsWarnings.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsWarnings.kt index 561ca2b85..f69616219 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsWarnings.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsWarnings.kt @@ -30,7 +30,7 @@ import org.fdroid.ui.FDroidContent import org.fdroid.ui.utils.testApp @Composable -fun AppDetailsWarnings(item: AppDetailsItem, modifier: Modifier = Modifier) { +fun AppDetailsWarnings(item: LoadedAppDetailsItem, modifier: Modifier = Modifier) { val (color, string) = when { // app issues take priority diff --git a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt index 083486aa0..86f0fd8fa 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt @@ -68,7 +68,7 @@ fun DetailsPresenter( } } } - .value ?: return null + .value ?: return NotFoundAppDetailsItem val versions = produceState?>(null, currentRepoId) { withContext(dispatcher) { @@ -123,7 +123,7 @@ fun DetailsPresenter( produceState(null, app) { withContext(dispatcher) { value = repoManager.getRepository(app.repoId) } } - .value ?: return null + .value ?: return NotFoundAppDetailsItem val repositories = produceState(emptyList(), packageName) { withContext(dispatcher) { @@ -196,7 +196,7 @@ fun DetailsPresenter( Log.d(TAG, " versions: ${versions?.size}") Log.d(TAG, " appPrefs: $appPrefs") Log.d(TAG, " installState: $installState") - return AppDetailsItem( + return LoadedAppDetailsItem( repository = repo, preferredRepoId = preferredRepoId, repositories = repositories, diff --git a/app/src/main/kotlin/org/fdroid/ui/details/TechnicalInfo.kt b/app/src/main/kotlin/org/fdroid/ui/details/TechnicalInfo.kt index a6a470be9..643117b7c 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/TechnicalInfo.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/TechnicalInfo.kt @@ -17,7 +17,7 @@ import org.fdroid.ui.FDroidContent import org.fdroid.ui.utils.testApp @Composable -fun TechnicalInfo(item: AppDetailsItem) { +fun TechnicalInfo(item: LoadedAppDetailsItem) { val items = mutableMapOf(stringResource(R.string.package_name) to item.app.packageName) if (item.installedVersionCode != null) { items[stringResource(R.string.installed_version)] = diff --git a/app/src/main/kotlin/org/fdroid/ui/details/Versions.kt b/app/src/main/kotlin/org/fdroid/ui/details/Versions.kt index 9b86974d8..a48d7a33d 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/Versions.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/Versions.kt @@ -47,7 +47,7 @@ import org.fdroid.ui.utils.testApp @Composable @OptIn(ExperimentalMaterial3ExpressiveApi::class) -fun Versions(item: AppDetailsItem, scrollUp: suspend () -> Unit) { +fun Versions(item: LoadedAppDetailsItem, scrollUp: suspend () -> Unit) { ExpandableSection( icon = rememberVectorPainter(Icons.Default.AccessTime), title = stringResource(R.string.versions), diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt index 604933da5..fc7d46b22 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt @@ -34,6 +34,7 @@ import org.fdroid.ui.categories.CategoryItem import org.fdroid.ui.details.AntiFeature import org.fdroid.ui.details.AppDetailsActions import org.fdroid.ui.details.AppDetailsItem +import org.fdroid.ui.details.LoadedAppDetailsItem import org.fdroid.ui.details.VersionItem import org.fdroid.ui.lists.AntiFeatureItem import org.fdroid.ui.lists.AppListActions @@ -144,7 +145,7 @@ val categoryItems = ) val testApp = - AppDetailsItem( + LoadedAppDetailsItem( app = AppMetadata( repoId = 1, diff --git a/app/src/screenshotTest/kotlin/org/fdroid/ui/details/DetailsTest.kt b/app/src/screenshotTest/kotlin/org/fdroid/ui/details/DetailsTest.kt index e8d4b4f13..5ed7b89fb 100644 --- a/app/src/screenshotTest/kotlin/org/fdroid/ui/details/DetailsTest.kt +++ b/app/src/screenshotTest/kotlin/org/fdroid/ui/details/DetailsTest.kt @@ -18,13 +18,14 @@ fun DetailsTest() = testApp.copy( versions = emptyList(), categories = testApp.categories?.subList(0, 2), - phoneScreenshots = listOf( - "https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png", - "https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png", - "https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png", - "https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png", - "https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png", - ), + phoneScreenshots = + listOf( + "https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png", + "https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png", + "https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png", + "https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png", + "https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png", + ), networkState = NetworkState(isOnline = true, isMetered = false), ), onNav = {}, @@ -32,6 +33,12 @@ fun DetailsTest() = ) } +@Preview +@Composable +@PreviewTest +fun AppDetailsNotFoundTest() = + ScreenshotTest(showBottomBar = false) { AppDetails(NotFoundAppDetailsItem, {}, {}) } + @Composable @PreviewTest @Preview(showBackground = true, showSystemUi = true, heightDp = 3000) diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/AppDetailsNotFoundTest_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/AppDetailsNotFoundTest_0.png new file mode 100644 index 000000000..ea45d134e --- /dev/null +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/AppDetailsNotFoundTest_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9bc256fb0c2f4e7736bda965bff6fcfd2004cca3ac6bd307235ad068aef821af +size 21381 From 6e382550564318750a0f1b541c209256e44387d8 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 5 May 2026 10:04:09 -0300 Subject: [PATCH 09/44] Make RepositoriesList screenshot test less flaky --- app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt | 6 ++---- .../RepositoriesListNightTest_1def2e7f_0.png | 4 ++-- .../RepositoriesListTest_2ed8e27d_0.png | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt index fc7d46b22..62e53ee50 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt @@ -5,7 +5,6 @@ import androidx.annotation.RestrictTo import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import java.util.concurrent.TimeUnit.DAYS import java.util.concurrent.TimeUnit.HOURS -import java.util.concurrent.TimeUnit.MINUTES import org.fdroid.database.AppListSortOrder import org.fdroid.database.AppMetadata import org.fdroid.database.AppPrefs @@ -33,7 +32,6 @@ import org.fdroid.ui.apps.MyAppsModel import org.fdroid.ui.categories.CategoryItem import org.fdroid.ui.details.AntiFeature import org.fdroid.ui.details.AppDetailsActions -import org.fdroid.ui.details.AppDetailsItem import org.fdroid.ui.details.LoadedAppDetailsItem import org.fdroid.ui.details.VersionItem import org.fdroid.ui.lists.AntiFeatureItem @@ -520,7 +518,7 @@ val repoItems = address = "http://example.org", name = "F-Droid", icon = null, - timestamp = System.currentTimeMillis() - MINUTES.toMillis(18), + timestamp = System.currentTimeMillis() - HOURS.toMillis(18), lastUpdated = System.currentTimeMillis() - 9_999_999, weight = 1, enabled = true, @@ -542,7 +540,7 @@ val repoItems = address = "http://example.net", name = "My first Repo", icon = null, - timestamp = System.currentTimeMillis() - MINUTES.toMillis(14), + timestamp = System.currentTimeMillis() - DAYS.toMillis(4), lastUpdated = System.currentTimeMillis(), weight = 3, enabled = true, diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/repositories/RepositoriesListTestKt/RepositoriesListNightTest_1def2e7f_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/repositories/RepositoriesListTestKt/RepositoriesListNightTest_1def2e7f_0.png index ebd19ce4f..32080684d 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/repositories/RepositoriesListTestKt/RepositoriesListNightTest_1def2e7f_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/repositories/RepositoriesListTestKt/RepositoriesListNightTest_1def2e7f_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a61579c31d71a86cb2966aa756f75e51d46e9d748d33320db2b68cd10a2c12e -size 82465 +oid sha256:2596dab7b00165c8738d1f38b33f708b09328d8d51afa9f9a18885eb0ef0bd07 +size 82404 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/repositories/RepositoriesListTestKt/RepositoriesListTest_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/repositories/RepositoriesListTestKt/RepositoriesListTest_2ed8e27d_0.png index 88db9d6f0..936263cbb 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/repositories/RepositoriesListTestKt/RepositoriesListTest_2ed8e27d_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/repositories/RepositoriesListTestKt/RepositoriesListTest_2ed8e27d_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9cfd57bd48a75c62780a75bc20e3702f7e2cc43344a0c698618d21b3199563d -size 86299 +oid sha256:41b129d0cd0e4350030b10c6304c106b21dcc52301706d88230088306fbb48d0 +size 86255 From fb60c8d554c3e6f647e4f577017cf749e1ae11c8 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 5 May 2026 11:36:07 -0300 Subject: [PATCH 10/44] Try to auto-start after updating ourselves --- .../kotlin/org/fdroid/install/AppUpdateReceiver.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/src/main/kotlin/org/fdroid/install/AppUpdateReceiver.kt b/app/src/main/kotlin/org/fdroid/install/AppUpdateReceiver.kt index 12c638acd..b85cf3e9a 100644 --- a/app/src/main/kotlin/org/fdroid/install/AppUpdateReceiver.kt +++ b/app/src/main/kotlin/org/fdroid/install/AppUpdateReceiver.kt @@ -4,6 +4,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.Intent.ACTION_MY_PACKAGE_REPLACED +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import androidx.annotation.RequiresApi import mu.KotlinLogging @@ -18,5 +19,16 @@ class AppUpdateReceiver : BroadcastReceiver() { return } log.info { "Intent received, we just updated ourselves!" } + val intent = + context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply { + addFlags(FLAG_ACTIVITY_NEW_TASK) + } + if (intent != null) { + try { + context.startActivity(intent) + } catch (e: Exception) { + log.error(e) { "Failed to start activity after update" } + } + } } } From 911a6f3ea94185796a684b2c9c94cb73c0d0e65b Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 5 May 2026 14:39:56 -0300 Subject: [PATCH 11/44] Update dependencies --- app/build.gradle.kts | 4 +- .../org/fdroid/settings/SettingsManager.kt | 1 - gradle/libs.versions.toml | 30 +- gradle/verification-metadata.xml | 1031 +++++++++++++++++ gradle/wrapper/gradle-wrapper.properties | 4 +- 5 files changed, 1053 insertions(+), 17 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9ef3aafd3..4a0f0b1f2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -28,6 +28,7 @@ android { all { buildConfigField("String", "ACRA_REPORT_EMAIL", "\"reports@f-droid.org\"") } getByName("release") { isMinifyEnabled = true + isShrinkResources = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } getByName("debug") { @@ -102,6 +103,7 @@ dependencies { implementation(libs.androidx.ui.graphics) implementation(libs.androidx.compose.material.icons.extended) implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.compose.material3.adaptive.navigation) implementation(libs.androidx.compose.material3.adaptive.navigation3) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) @@ -132,7 +134,7 @@ dependencies { ksp(libs.hilt.android.compiler) ksp(libs.androidx.hilt.compiler) // https://github.com/google/dagger/issues/5001 - ksp("org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.0") + ksp("org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.21") debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) diff --git a/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt b/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt index 215eeb172..3fcbe3b99 100644 --- a/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt +++ b/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt @@ -158,7 +158,6 @@ class SettingsManager @Inject constructor(@param:ApplicationContext private val prefs.getString(PREF_KEY_MIRROR_CHOOSER, PREF_DEFAULT_MIRROR_CHOOSER).toMirrorChooserValue() val proxyConfig: ProxyConfig? - @UiThread get() { val proxyStr = prefs.getString(PREF_KEY_PROXY, PREF_DEFAULT_PROXY) return if (proxyStr.isNullOrBlank()) null diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2d11b55cf..99bcb2ead 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,13 @@ [versions] compileSdk = "36" -kotlin = "2.3.20" -androidGradlePlugin = "9.1.1" -androidKspPlugin = "2.3.6" +kotlin = "2.3.21" +androidGradlePlugin = "9.2.1" +androidKspPlugin = "2.3.7" hilt = "2.59.2" hiltWork = "1.3.0" hiltNavigationCompose = "1.3.0" dokka = "2.2.0" -mavenPublish = "0.35.0" +mavenPublish = "0.36.0" screenshot = "0.0.1-alpha13" ktfmt = "0.26.0" @@ -15,14 +15,14 @@ kotlinxSerializationCore = "1.11.0" kotlinxSerializationJson = "1.11.0" kotlinxCoroutinesTest = "1.10.2" -ktor = "3.4.2" +ktor = "3.4.3" okhttp = "4.12.0" room = "2.8.4" -glide = "5.0.5" -glideCompose = "1.0.0-beta08" +glide = "5.0.7" +glideCompose = "1.0.0-beta09" coilCompose = "3.4.0" molecule = "2.2.0" -hints = "2.2.1" +hints = "3.0.1" composePreference = "2.2.0" androidxCoreKtx = "1.18.0" @@ -38,23 +38,26 @@ androidxConstraintlayout = "2.2.1" androidxSwipeRefreshLayout = "1.2.0" androidxVectordrawable = "1.2.0" androidxGridlayout = "1.1.0" -androidxComposeBom = "2026.03.01" +androidxComposeBom = "2026.04.01" androidxActivityCompose = "1.13.0" accompanistDrawablepainter = "0.37.3" # navigation3 -nav3Core = "1.1.0" +nav3Core = "1.1.1" lifecycleViewmodelNav3 = "2.10.0" material3AdaptiveNav3 = "1.0.0-alpha03" material = "1.13.0" +material3 = "1.5.0-alpha18" # should be possible to remove once 1.5.0 is stable and in bom +#noinspection GradleDependency 1.3.0-alpha10 depends on SDK 37 +material3AdaptiveNav = "1.3.0-alpha09" zxingCore = "3.5.4" guardianprojectNetcipher = "2.2.0-alpha" guardianprojectPanic = "1.0" acra = "5.13.1" adapterdelegates4 = "4.3.2" -commonsIo = "2.21.0" +commonsIo = "2.22.0" commonsNet = "3.13.0" bouncycastle = "1.84" #noinspection NewerVersionAvailable upgrade to 3.6.1 failed BonjourManagerTest @@ -114,7 +117,7 @@ androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } -androidx-compose-bom = { group = "androidx.compose", name = "compose-bom-alpha", version.ref = "androidxComposeBom" } # TODO remove -alpha +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose" } @@ -122,7 +125,8 @@ androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecy androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" } androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" } androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } -androidx-compose-material3 = { module = "androidx.compose.material3:material3" } +androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } +androidx-compose-material3-adaptive-navigation = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation", version.ref = "material3AdaptiveNav" } androidx-compose-material3-adaptive-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "material3AdaptiveNav3" } androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "androidxHiltCompiler" } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 1a14e07d8..edca3b1e5 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -313,6 +313,11 @@ + + + + + @@ -328,11 +333,21 @@ + + + + + + + + + + @@ -348,11 +363,21 @@ + + + + + + + + + + @@ -368,11 +393,21 @@ + + + + + + + + + + @@ -388,11 +423,21 @@ + + + + + + + + + + @@ -408,6 +453,11 @@ + + + + + @@ -433,6 +483,11 @@ + + + + + @@ -448,6 +503,11 @@ + + + + + @@ -463,6 +523,11 @@ + + + + + @@ -473,6 +538,11 @@ + + + + + @@ -483,6 +553,11 @@ + + + + + @@ -493,6 +568,11 @@ + + + + + @@ -503,11 +583,21 @@ + + + + + + + + + + @@ -523,11 +613,21 @@ + + + + + + + + + + @@ -543,11 +643,21 @@ + + + + + + + + + + @@ -563,11 +673,21 @@ + + + + + + + + + + @@ -583,11 +703,21 @@ + + + + + + + + + + @@ -603,11 +733,21 @@ + + + + + + + + + + @@ -623,11 +763,21 @@ + + + + + + + + + + @@ -643,11 +793,21 @@ + + + + + + + + + + @@ -663,11 +823,21 @@ + + + + + + + + + + @@ -683,11 +853,21 @@ + + + + + + + + + + @@ -703,11 +883,21 @@ + + + + + + + + + + @@ -723,11 +913,21 @@ + + + + + + + + + + @@ -743,11 +943,21 @@ + + + + + + + + + + @@ -763,11 +973,21 @@ + + + + + + + + + + @@ -783,11 +1003,21 @@ + + + + + + + + + + @@ -803,11 +1033,21 @@ + + + + + + + + + + @@ -823,6 +1063,11 @@ + + + + + @@ -938,6 +1183,11 @@ + + + + + @@ -953,6 +1203,11 @@ + + + + + @@ -1223,6 +1478,11 @@ + + + + + @@ -1238,6 +1498,11 @@ + + + + + @@ -1648,6 +1913,11 @@ + + + + + @@ -1663,6 +1933,11 @@ + + + + + @@ -1683,6 +1958,11 @@ + + + + + @@ -1698,6 +1978,11 @@ + + + + + @@ -1713,6 +1998,11 @@ + + + + + @@ -1728,6 +2018,11 @@ + + + + + @@ -1743,6 +2038,11 @@ + + + + + @@ -1758,6 +2058,11 @@ + + + + + @@ -1773,6 +2078,11 @@ + + + + + @@ -1788,6 +2098,11 @@ + + + + + @@ -1803,6 +2118,11 @@ + + + + + @@ -1818,6 +2138,11 @@ + + + + + @@ -1833,6 +2158,11 @@ + + + + + @@ -1848,6 +2178,11 @@ + + + + + @@ -1863,6 +2198,11 @@ + + + + + @@ -1878,6 +2218,11 @@ + + + + + @@ -1893,6 +2238,11 @@ + + + + + @@ -1908,6 +2258,11 @@ + + + + + @@ -1923,6 +2278,11 @@ + + + + + @@ -1938,6 +2298,11 @@ + + + + + @@ -1953,6 +2318,11 @@ + + + + + @@ -1968,6 +2338,11 @@ + + + + + @@ -1988,6 +2363,11 @@ + + + + + @@ -2003,6 +2383,11 @@ + + + + + @@ -2018,6 +2403,11 @@ + + + + + @@ -2033,6 +2423,11 @@ + + + + + @@ -2048,6 +2443,11 @@ + + + + + @@ -2083,6 +2483,11 @@ + + + + + @@ -2098,6 +2503,11 @@ + + + + + @@ -2113,6 +2523,11 @@ + + + + + @@ -2128,6 +2543,11 @@ + + + + + @@ -2143,6 +2563,11 @@ + + + + + @@ -2163,6 +2588,11 @@ + + + + + @@ -2188,6 +2618,11 @@ + + + + + @@ -2203,6 +2638,11 @@ + + + + + @@ -2218,6 +2658,11 @@ + + + + + @@ -2233,6 +2678,11 @@ + + + + + @@ -2248,6 +2698,11 @@ + + + + + @@ -2263,6 +2718,11 @@ + + + + + @@ -2288,6 +2748,11 @@ + + + + + @@ -2303,6 +2768,11 @@ + + + + + @@ -2318,6 +2788,11 @@ + + + + + @@ -2333,6 +2808,11 @@ + + + + + @@ -2348,6 +2828,11 @@ + + + + + @@ -2363,6 +2848,11 @@ + + + + + @@ -2378,6 +2868,11 @@ + + + + + @@ -2393,6 +2888,11 @@ + + + + + @@ -2408,6 +2908,11 @@ + + + + + @@ -2423,6 +2928,11 @@ + + + + + @@ -2438,6 +2948,11 @@ + + + + + @@ -2453,6 +2968,11 @@ + + + + + @@ -2468,6 +2988,11 @@ + + + + + @@ -2483,6 +3008,11 @@ + + + + + @@ -2498,6 +3028,11 @@ + + + + + @@ -2513,6 +3048,11 @@ + + + + + @@ -2528,6 +3068,11 @@ + + + + + @@ -2543,6 +3088,11 @@ + + + + + @@ -2558,6 +3108,11 @@ + + + + + @@ -2603,26 +3158,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2633,16 +3213,31 @@ + + + + + + + + + + + + + + + @@ -2838,6 +3433,11 @@ + + + + + @@ -2858,6 +3458,11 @@ + + + + + @@ -2868,6 +3473,11 @@ + + + + + @@ -2878,6 +3488,11 @@ + + + + + @@ -3298,6 +3913,16 @@ + + + + + + + + + + @@ -3318,16 +3943,31 @@ + + + + + + + + + + + + + + + @@ -3343,11 +3983,21 @@ + + + + + + + + + + @@ -3363,6 +4013,16 @@ + + + + + + + + + + @@ -3393,6 +4053,11 @@ + + + + + @@ -3593,6 +4258,11 @@ + + + + + @@ -3608,6 +4278,11 @@ + + + + + @@ -3623,6 +4298,11 @@ + + + + + @@ -3638,16 +4318,31 @@ + + + + + + + + + + + + + + + @@ -3663,6 +4358,11 @@ + + + + + @@ -3678,6 +4378,11 @@ + + + + + @@ -3693,6 +4398,11 @@ + + + + + @@ -3708,6 +4418,11 @@ + + + + + @@ -3723,6 +4438,11 @@ + + + + + @@ -3738,6 +4458,11 @@ + + + + + @@ -3753,6 +4478,11 @@ + + + + + @@ -3768,6 +4498,11 @@ + + + + + @@ -3783,6 +4518,11 @@ + + + + + @@ -3798,6 +4538,11 @@ + + + + + @@ -3813,6 +4558,11 @@ + + + + + @@ -3828,6 +4578,11 @@ + + + + + @@ -3843,6 +4598,11 @@ + + + + + @@ -3858,6 +4618,11 @@ + + + + + @@ -3873,6 +4638,11 @@ + + + + + @@ -3888,6 +4658,11 @@ + + + + + @@ -3903,6 +4678,11 @@ + + + + + @@ -3918,6 +4698,11 @@ + + + + + @@ -3933,6 +4718,11 @@ + + + + + @@ -3948,6 +4738,11 @@ + + + + + @@ -3963,6 +4758,11 @@ + + + + + @@ -3978,6 +4778,11 @@ + + + + + @@ -3993,6 +4798,11 @@ + + + + + @@ -4008,6 +4818,11 @@ + + + + + @@ -4023,6 +4838,11 @@ + + + + + @@ -4038,6 +4858,11 @@ + + + + + @@ -4818,6 +5643,11 @@ + + + + + @@ -4828,6 +5658,11 @@ + + + + + @@ -4838,6 +5673,11 @@ + + + + + @@ -4848,6 +5688,11 @@ + + + + + @@ -4858,6 +5703,11 @@ + + + + + @@ -4868,6 +5718,11 @@ + + + + + @@ -4878,6 +5733,11 @@ + + + + + @@ -4888,11 +5748,21 @@ + + + + + + + + + + @@ -4903,6 +5773,11 @@ + + + + + @@ -4918,6 +5793,11 @@ + + + + + @@ -4928,6 +5808,11 @@ + + + + + @@ -4938,6 +5823,11 @@ + + + + + @@ -4948,6 +5838,11 @@ + + + + + @@ -4963,6 +5858,11 @@ + + + + + @@ -4973,6 +5873,11 @@ + + + + + @@ -4983,6 +5888,11 @@ + + + + + @@ -4993,6 +5903,11 @@ + + + + + @@ -5003,6 +5918,11 @@ + + + + + @@ -5013,6 +5933,11 @@ + + + + + @@ -5023,6 +5948,11 @@ + + + + + @@ -5033,6 +5963,11 @@ + + + + + @@ -5043,6 +5978,11 @@ + + + + + @@ -5063,6 +6003,11 @@ + + + + + @@ -5073,6 +6018,11 @@ + + + + + @@ -5118,6 +6068,11 @@ + + + + + @@ -5133,6 +6088,11 @@ + + + + + @@ -5143,6 +6103,11 @@ + + + + + @@ -5153,6 +6118,11 @@ + + + + + @@ -5163,6 +6133,11 @@ + + + + + @@ -5173,6 +6148,11 @@ + + + + + @@ -5183,6 +6163,11 @@ + + + + + @@ -5193,6 +6178,11 @@ + + + + + @@ -5257,6 +6247,14 @@ + + + + + + + + @@ -5348,6 +6346,14 @@ + + + + + + + + @@ -5358,6 +6364,11 @@ + + + + + @@ -5368,6 +6379,11 @@ + + + + + @@ -5378,6 +6394,11 @@ + + + + + @@ -5388,6 +6409,11 @@ + + + + + @@ -5398,6 +6424,11 @@ + + + + + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7abf69a2f..87bbfa18b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ #Tue Oct 22 15:45:27 PDT 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=17f277867f6914d61b1aa02efab1ba7bb439ad652ca485cd8ca6842fccec6e43 -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip +distributionSha256Sum=708d2c6ecc97ca9a11838ef64a6c2301151b8dd10387e22dc1a12c30557cab5b +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 4da7bafbee7ddfd5dc08a11b5af705ea1ee2d784 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 6 May 2026 10:18:46 -0300 Subject: [PATCH 12/44] Show notification after self-update because we aren't allowed to launch the app. So the user can at least re-launch us easily. --- .../main/kotlin/org/fdroid/MainActivity.kt | 6 ++++ .../kotlin/org/fdroid/NotificationManager.kt | 31 +++++++++++++++++++ .../org/fdroid/install/AppUpdateReceiver.kt | 16 ++++++++-- app/src/main/res/values/strings.xml | 4 +++ 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/MainActivity.kt b/app/src/main/kotlin/org/fdroid/MainActivity.kt index 54f1550c4..5ec0ccba7 100644 --- a/app/src/main/kotlin/org/fdroid/MainActivity.kt +++ b/app/src/main/kotlin/org/fdroid/MainActivity.kt @@ -31,6 +31,7 @@ class MainActivity : AppCompatActivity() { val requestPermissionLauncher = registerForActivityResult(RequestPermission()) {} @Inject lateinit var settingsManager: SettingsManager + @Inject lateinit var notificationManager: NotificationManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -78,4 +79,9 @@ class MainActivity : AppCompatActivity() { // calling super seems to be needed, so the IntentListener gets informed super.onNewIntent(intent, caller) } + + override fun onResume() { + super.onResume() + notificationManager.cancelSelfUpdateNotification() + } } diff --git a/app/src/main/kotlin/org/fdroid/NotificationManager.kt b/app/src/main/kotlin/org/fdroid/NotificationManager.kt index 4ed97784f..cf12f74a8 100644 --- a/app/src/main/kotlin/org/fdroid/NotificationManager.kt +++ b/app/src/main/kotlin/org/fdroid/NotificationManager.kt @@ -12,11 +12,14 @@ import androidx.annotation.StringRes import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.BigTextStyle +import androidx.core.app.NotificationCompat.CATEGORY_REMINDER import androidx.core.app.NotificationCompat.CATEGORY_SERVICE import androidx.core.app.NotificationCompat.PRIORITY_HIGH +import androidx.core.app.NotificationCompat.PRIORITY_MAX import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat.IMPORTANCE_DEFAULT import androidx.core.app.NotificationManagerCompat.IMPORTANCE_LOW +import androidx.core.app.NotificationManagerCompat.IMPORTANCE_MAX import androidx.core.content.ContextCompat.checkSelfPermission import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject @@ -40,10 +43,12 @@ constructor(@param:ApplicationContext private val context: Context) { const val NOTIFICATION_ID_APP_INSTALLS: Int = 1 const val NOTIFICATION_ID_APP_INSTALL_SUCCESS: Int = 2 const val NOTIFICATION_ID_APP_UPDATES_AVAILABLE: Int = 3 + const val NOTIFICATION_ID_SELF_UPDATE: Int = 4 private const val CHANNEL_UPDATES = "update-channel" private const val CHANNEL_INSTALLS = "install-channel" private const val CHANNEL_INSTALL_SUCCESS = "install-success-channel" private const val CHANNEL_UPDATES_AVAILABLE = "updates-available-channel" + private const val CHANNEL_SELF_UPDATE = "self-update-channel" } init { @@ -69,6 +74,10 @@ constructor(@param:ApplicationContext private val context: Context) { .setName(s(R.string.notification_channel_updates_available_title)) .setDescription(s(R.string.notification_channel_updates_available_description)) .build(), + NotificationChannelCompat.Builder(CHANNEL_SELF_UPDATE, IMPORTANCE_MAX) + .setName(s(R.string.notification_channel_self_update_title)) + .setDescription(s(R.string.notification_channel_self_update_description)) + .build(), ) nm.createNotificationChannelsCompat(channels) } @@ -181,6 +190,28 @@ constructor(@param:ApplicationContext private val context: Context) { return builder } + fun showSelfUpdateNotification() { + val pi = getMyAppsPendingIntent(context) + val app = context.getString(R.string.app_name) + val title = context.getString(R.string.notification_self_update_title, app) + val builder = + NotificationCompat.Builder(context, CHANNEL_SELF_UPDATE) + .setSmallIcon(R.drawable.ic_notification) + .setCategory(CATEGORY_REMINDER) + .setContentTitle(title) + .setPriority(PRIORITY_MAX) + .setContentIntent(pi) + .setAutoCancel(true) + val n = builder.build() + if (checkSelfPermission(context, POST_NOTIFICATIONS) == PERMISSION_GRANTED) { + nm.notify(NOTIFICATION_ID_SELF_UPDATE, n) + } + } + + fun cancelSelfUpdateNotification() { + nm.cancel(NOTIFICATION_ID_SELF_UPDATE) + } + private fun getMainActivityPendingIntent(context: Context): PendingIntent { val i = Intent(ACTION_MAIN).apply { setClass(context, MainActivity::class.java) } val flags = FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE diff --git a/app/src/main/kotlin/org/fdroid/install/AppUpdateReceiver.kt b/app/src/main/kotlin/org/fdroid/install/AppUpdateReceiver.kt index b85cf3e9a..75bd1d37a 100644 --- a/app/src/main/kotlin/org/fdroid/install/AppUpdateReceiver.kt +++ b/app/src/main/kotlin/org/fdroid/install/AppUpdateReceiver.kt @@ -5,14 +5,19 @@ import android.content.Context import android.content.Intent import android.content.Intent.ACTION_MY_PACKAGE_REPLACED import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import androidx.annotation.RequiresApi +import android.os.Build.VERSION.SDK_INT +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import mu.KotlinLogging +import org.fdroid.NotificationManager +@AndroidEntryPoint class AppUpdateReceiver : BroadcastReceiver() { private val log = KotlinLogging.logger {} - @RequiresApi(35) + @Inject lateinit var notificationManager: NotificationManager + override fun onReceive(context: Context, intent: Intent) { if (intent.action != ACTION_MY_PACKAGE_REPLACED) { log.warn { "Unknown action: ${intent.action}" } @@ -23,12 +28,17 @@ class AppUpdateReceiver : BroadcastReceiver() { context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply { addFlags(FLAG_ACTIVITY_NEW_TASK) } - if (intent != null) { + if (intent == null) { + log.error { "Could not get launch intent for ourselves" } + } else { try { context.startActivity(intent) } catch (e: Exception) { log.error(e) { "Failed to start activity after update" } } } + // show notification on Android 10+, because we aren't allowed to start activity from background + // see: https://developer.android.com/guide/components/activities/background-starts + if (SDK_INT >= 29) notificationManager.showSelfUpdateNotification() } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2ebfb085d..0570b9a1e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -955,6 +955,8 @@ This often occurs with apps installed via Google Play or other sources, if they Displays a notification after apps were installed automatically Available app updates Displays a notification after repositories were updated and app updates were found + Self-update + Notifies when the app updated itself and was closed, so you can launch it again easily Connecting to %1$s… Downloaded %1$s from %2$s @@ -978,6 +980,8 @@ This often occurs with apps installed via Google Play or other sources, if they Needs user confirmation: Installed: Tap to confirm + + %s was updated. Tap to open Category %1$s From 454be70d59fcefaeb592b9e53fb4259b1936596a Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 6 May 2026 10:39:33 -0300 Subject: [PATCH 13/44] Use separate TV banners for full and basic --- .../res/mipmap-xhdpi/ic_banner.png | Bin app/src/full/res/mipmap-xhdpi/ic_banner.png | Bin 0 -> 7796 bytes 2 files changed, 0 insertions(+), 0 deletions(-) rename app/src/{main => basic}/res/mipmap-xhdpi/ic_banner.png (100%) create mode 100644 app/src/full/res/mipmap-xhdpi/ic_banner.png diff --git a/app/src/main/res/mipmap-xhdpi/ic_banner.png b/app/src/basic/res/mipmap-xhdpi/ic_banner.png similarity index 100% rename from app/src/main/res/mipmap-xhdpi/ic_banner.png rename to app/src/basic/res/mipmap-xhdpi/ic_banner.png diff --git a/app/src/full/res/mipmap-xhdpi/ic_banner.png b/app/src/full/res/mipmap-xhdpi/ic_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..64201978cb014f16dc65b44520ac816cf2c522b9 GIT binary patch literal 7796 zcmeHsRalf=)b`K}tw`6PC@3A$9RiAU<4BBj%)kHxLk|cjA)=IYNtYnqAc#oE&?yo_ zN;3oe^Ztk5*?0aOJaO%7?PsrNt+nrc-+RB)*VUw^V5I;60My!A>V^OSfinKRj+_*K z-}rNS2mb)Vv@GBN08P(-7ePO?_zk|1#aqMN+X(95jj;8y2Otm#A!iR)IK&oaF9h{+ zOy8Gh#Tyy^XQT#&`FlA#I(Y-s)IciU-d@gjKHkny4?!a*d{-4WXIFcWleafaR#+I~ zWpC^4>}wB!y1PR?;QxI|*v%PY?*X@m3)}e%!fk~mghYgUjO|DO0FHcZbroa3jJ-vF zZ{v+cvcm(g!$6j@Dc=k8Zbnz@u=jU~3z=!3Fw)wU*YD)|9#wqa7&R*RZu}uyq1}C~ zPNP?~L~uT!c(24jJ)w|cJi<&}{j(qs4-a?RZrX;>2)6%CnvTQYrL={C?&~0V8Q$MP z`|#|vUg&<%{;_-tBvzD8H5zzCvb<&A?KVVN`iKMniRsYZTLBmn^= zB+OR7w=MS`e%weE@u!HSW!q+w>?%C~N0KaXs>n=Z(m@0>aO;L5>g*s(RH+yZrN>f4 z(*zgoxUcHZxSSCJazHj%K4OG*dYzwseeCNiR4ilh?}?fzsZ>G|!<@gzV`D);DuQ zf^ZaJ&6fSvYV9_HY90qVNTTz~k9K+#tt3^Z$**`G@Dz68FV8;rrCeX0UD`lHCj*u0 zC!;mgR%-zbxo;#>C-EL8M^YTyfWw`^Jf%pTFn45I zujijV9O;+3087uonA~a-m#6oe?MrS64}X4qVBUuGnfxH`xu(9mz;bo`bS{jDtlD?a za&beDk#cQ)od9rjbfl%Nt@8Xi#rXJmLS|;1=XxJEFE5$4wzh%WQ&D;~LVeRf!UvrS z45X1I1N_Lh*;U(Vm_yqh4_MyqY{B$H&yhXPo!HP-^kaTc#_{w6HToOIy4XBsn6*|> zU*EF~d4I8;{*N0r*X3jDFlnj~sQkzSDJdqzf+tzBfCWkh{x@{f*+l9ti&`*T+M77r z`l2TO761~zIZN>2nR(XL7YA$xV|Pbd6c!aVYui{v;Q_n!OP)WK4NVw|^s=AX+H(IL zN{`yzb-TyN*uCLJ?LG06?#*cS@Jbh2dqv>OcXRc!_}Y$@eiFdP1{Y4vd$_@ihU_aq z)cb$`yyTC6i>Kl}q+yp986DzQi_JyTgrP>*gs5mnwiB?(&koYF8Xeco80r&b4%#v& zXke-!IdY*=?Dj@QYzExqMRIw0Xveuls7IS+t=3R6TL6MN?+mOO7;&X)o{rl+$V z-B?&y$Udiz)reuV{q=dD#1SmS#LC9jvVY&q#5rz$*L@^QAyv|yH7j6WCfw?U2Tvq% zPUpNWVkngIVKw5VD`nuj*+^lS_VvRRpz))HL(2BKm|qLUed)!?^`r}&#qNpensBgu z-^;y@u56^j$*7ti?uZTU^F@=>pO}P%M5Q4mB?W^2dp-ey-XAec;mOD6xZ{xrN=jS} zOUN=6LF$T{nme(gGkd7JTP6tt0k7ZDxCnqCLoioFt6V2=LaDU||g1_LN!=bG}s`m|`+m{3rUxgu-)rgUN-!)D?IF zRlrcWymik3{E-Gde#CbB^s3|Irz@=l$xmT@$MRXhSI&PnHf(Qjm&Ije6AMKJ1;iQ} z8jbjhP)o*k(FW(tIySn^ijkuL+KJV+J)~|6jiOF;4Rv)+^)F3Kc4uqJs#^cO{~PM5 zov3eQ6#nH4>*vp(SEM>FsqO6S^!4>aeK6CuxQnAN{l^RyB``VP-Of^zGJR`nE*SQ^ zE4muGthhVhK#Q5K=qamee~;a75iW-?zyC;ZdNvQmE7nn1{=Z@7`P?rfqMb zNqo2Y1E<2Ta?`TPD<%Q+91E@k@FTuIDFTD#g@tbg&8wAb-H=G+&-F|>MEuZ~r!UN~ z-`fQkDXlt!6+ZE3pKqGIdR1<5@Hb4=ILmXOGNjQ!cUqmj4gAf-H%=1Q8oDq?nAE4aE6TecNYdp zkA-53vmRO{bugI2(8vhutz(f~mDq3aJ@(o(tyL^*>e-xi9F@j9i06i zxcZmc0@8R?k|gI>Xg95^=bykbgz2kl z6oKSP^VVLCpUWXiML62x0D3wpv!+B--XTnxZ|H=Z(XU3Hy5NEKY%73LJ=vaJ8V2|v zKi;q($7CIN=b#}AZ4Gqxc3^e~jE)1o&AOM+C;zqsrWYGSBs zW@zLgDcsOqkoAQvRxZ~%(w^g9?*i|lYFl2 zn^#|T(oi^D;2Q^ryow5HJb*}hZ;ghQ8&3`-0Ni{ZONT5@G05g#;RxL+?ghUe1Qs>{ zy&&|d{nXQ*QI)jLNqY=_`yUYC-TkuRCdnoVU5e$+^2-7t!zUJSi3wrPHIqv|;+<-2QgZUwvW%Wpk=@i<$B{nkn}1BPEcAYt^-o==bMOe+ z?}{f~2Tw5@(li0*`x?Q)!JT{W-o2ZtfsnD&kSzrqv?cgtN_!IW>SgtwY)?G>z*eG! zTcvIOdAZOT!_1@8~Av zY9d?2u9sYG{vJ&mB`0%@OC5-Oraee*ZEdZ4uBxu?UR+$AjM!Uheee026LZnJrwCH~ttF)+NY1G{*zQ+hC_$@Sw`5xixZW(p zb1Bi7nB@Qa8H)E!srlD`7;n}zGh>cly0&G@BobE%ihWEsI%dHJOANuPuL#{BeJJ`1 z&JBDHzqfJdn05l?ZN94=QcJy#2%2Q}bMy5pEd!Xm#r%8N`{xe;HNvqjGAA<=%OPia zzLC{|=cqmN;^tc%tOX5KBA>Og6TTKfUB?TwZHHpow!fxJ4Eo7)O0&l+(gJAJM*@ER z{!N9MX=-XxlLGWmD*_VO*b~^=dI*>*!4a1a(9;okmTHbuRz?GB|BI3@gqf)NwbHjRykl>*~N55uq+Z`t#*W{kgfwz>AM7inv2H@Wx4o zsPjAT(&Q|;(bY44Uq!{qXozMvqVS7NcaR=Y&7Txof?@{0t*rwLE|HD3Q+9ubqGzJ( zGUKLYu&UF@n(N=N`=;d72I_d+pU-R4uAIHx;z9GXp2o5&mQ%(7kdc8wE>^ebjw|7# zM~|M>WoLm7FJfPhsvWo`!ql{c6)+r;*eueufnbA>oKoo4Wg;6}nit>)zgiN(*)! zgO#K0$^BhS?FUwBZpGaE;DbXZggr^Y*L_+YXV>t_Nr32Bi*i9~B2`H)Cm?)blGro# z#+q1s;}00IScvs{lAty6{%Qvrf@Q0Bo6RfRMUhb6e=R*pjwC#c5pE|h)4ff56eQ!z zb3(k5#Ba9Z+hSWg>pjJ??`T6z1Vu%}Q!iIM4+ub02C5pO)6&N0NTcsd8xTX5CX51n zCH>Od`e)f3@7S`XQ=hJp@Kp$M^pm}bBC@mlCMAtmj9FyH3|_8Q<^;vXf8TZ54<=JL z`FBUra8O`!HTg};qmk*Vmpf)1r51gwu$4jK4s~7K_)1y54C!2pN4&heE@jmFI8j2%3Q1@`h-8(aqy6Rc_HcHJ`qMe8=zgzRW_+3>YEbas}?x zIyfM27mrX2<8un(5$j!66&#MSs|uTF`6H=1=}cYu^96V#o@V&are=ye(zGANLDk#b zbAP8x>7h4{E5R+^Q=48*5-GK0bW`Wo->ZUNnklX9FShthA#fL-J)5qWu_`H*QGXym zg6uAn-L6sRW4PinrKhLD-y1O#f@83S>dEv_!J#ZJ`DD>+9!5#O>#Lu4#T*-&JV2fJYah~mhV4R`zt()?V#;Et5P%Q zYf%wowfpUz2D1;m*@1>|=ET8`?mUf{!!9x|Vs@tD(kG@>ujB9wkY-I`7Yl1>vzli0 z3G+xlLU-5Hq_`9+?G?IQPIo~Q<|*4{{GA^s9e=Q+MBFSKBtuzoD4&WKk)wxEFk!2D zw@&O$V9=)Aa#tiZo4$d;oo@>@kp6`9^qBt`JKlw#zkZ?m*Spfh9S{4JZtmcFjQNnZ z;^R?VMbU;QwdI!IpC6Z>PUtE9sB@mEcA0we|H+4p7Y|-C8WuKou-k$B}9dp7vwSir!rXemQ670i>pG`zrOzd@S1q|bJF6hcmNgr z0#$l^H-<`APyUGbb;z~Pkf!Zf5gzp2=IVg7iWfN!v;1b2D*?+`&e0n6bjRxy_fX%su`*fhA4}XYGw2aw`HPvQ6~PqS8`acs6$Y8s z^ha-Cbi@Q~l5U;#?(^P)OjFDF-Kbcu+lzkNJ$jFehYugxp6@SKTVgj$9WVcFL@EqI zehw1@HywMKTh+j_`=G}!UoJ94-xnxR*d%%)BsDjj8GgK`(OC8RE#?L<0 zy}29-vD=v}F(^@@%i65L>nFvL^-RSe+36~4sCmoDPE;C5p+nXpv6aY(;RCT=5cqsF z`9y~tcoe+B@_CxkZ>LJl#rvdjVlWjUR1>7*ixy;fgv+GfB!ym zp9Mrn49?*eXzyJu30>j%x>iy`irGRyY^p!7kaZoD$6YQ`Y?o)(TH10c5wwmt!4dZM zXmNG7G>d0#8e-&l?f*MVflcXhKo9osk9OeIc9Gw1O+V?zL#POQzt8uMDum0YPoIv~ zVubVR>gudVa5x`QSR<_+awUumhF7B1X7QrWC!@N&caq-9)4Do5S}^19e8V#Qf(buh z*SzP@arD4&zeW$@q55qs*oNPZ z^QJcE_-T^zAnD`BJ7+cv5fQ}EdC`rH(gg(tpob4hN=izYr99Zb)z*H@%;cDE_D2q7 zD_PfKdbZM^;8}y;W+no5wrjCFQ$wWrfm6fT`7uvi7}nX@86U$QyxPF43c4}lzJdat zq3odo?N8mM@a_m8UP#a_Au)n(i_Hx9>~|0@apCI`zB&9KPxD+3I`?T)^!lSUB)ReT zZ{*Y?L3Z(+9!$)RufU9V31%6|e^N;r1bkk5{wz8*gK)msV8w4aSD4DBh5mVfoUsKd z@8OEO@1f@r7z6N4WMhXVSs%5Fswle+|A`g~Ifj?d)C;t7DRMt^fXbahfZWI$uq zp-4|m;33i*%Nc$~xtZJiDuImWYkX!9VM+NCbS?kDo>yP&{KpB|HXEhJE(9%*oW9v4u zqEwV*vw!jB@TNF#}DyvoUSlREug zn7_~_k#)YeP7nI4gBl;FXezn$98N!}jQHWlX{SoqTkwYbowmY0@i}!*>gQbX0u~(Txpv3^(N+r=0Rtx0@j@+KN`@sxRRw)^m~%(f0$pWQovwti z(f0W4s{YklA15E`%!h?;_oFh()djNBNYdYSf1s-sk;fnAy(!L_;P}6Ty#HMUu9YB2 WGrA=7d@}wp7@)18t6r&UgZh7+!^)We literal 0 HcmV?d00001 From c57cca33f408a796074eea37cbaf018754e33e58 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 8 May 2026 15:08:25 -0300 Subject: [PATCH 14/44] Add new categories --- .../org/fdroid/ui/categories/CategoryItem.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt index 5dfbf364f..5278f77f8 100644 --- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt @@ -29,6 +29,7 @@ import androidx.compose.material.icons.filled.Dns import androidx.compose.material.icons.filled.Draw import androidx.compose.material.icons.filled.EditNote import androidx.compose.material.icons.filled.EnhancedEncryption +import androidx.compose.material.icons.filled.Fastfood import androidx.compose.material.icons.filled.FitnessCenter import androidx.compose.material.icons.filled.FlashlightOn import androidx.compose.material.icons.filled.Games @@ -54,9 +55,11 @@ import androidx.compose.material.icons.filled.PermPhoneMsg import androidx.compose.material.icons.filled.PhotoSizeSelectActual import androidx.compose.material.icons.filled.Podcasts import androidx.compose.material.icons.filled.Radio +import androidx.compose.material.icons.filled.RecordVoiceOver import androidx.compose.material.icons.filled.RestaurantMenu import androidx.compose.material.icons.filled.Science import androidx.compose.material.icons.filled.Security +import androidx.compose.material.icons.filled.SelfImprovement import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.SettingsRemote import androidx.compose.material.icons.filled.ShoppingCart @@ -64,6 +67,7 @@ import androidx.compose.material.icons.filled.SignalCellularAlt import androidx.compose.material.icons.filled.Storefront import androidx.compose.material.icons.filled.Style import androidx.compose.material.icons.filled.TaskAlt +import androidx.compose.material.icons.filled.Timer import androidx.compose.material.icons.filled.TrackChanges import androidx.compose.material.icons.filled.Translate import androidx.compose.material.icons.filled.UploadFile @@ -92,6 +96,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Connectivity" -> Icons.Default.SignalCellularAlt "Contact" -> Icons.Default.Contacts "Development" -> Icons.Default.DeveloperMode + "Diet" -> Icons.Default.Fastfood "DNS & Hosts" -> Icons.Default.Dns "Draw" -> Icons.Default.Draw "Ebook Reader" -> AutoMirrored.Default.MenuBook @@ -106,6 +111,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Games" -> Icons.Default.Games "Graphics" -> Icons.Default.Brush "Habit Tracker" -> Icons.Default.TrackChanges + "Health Manager" -> Icons.Default.HealthAndSafety "Icon Pack" -> Icons.Default.Collections "Internet" -> Icons.Default.Language "Inventory" -> Icons.Default.AllInbox @@ -113,6 +119,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Launcher" -> Icons.Default.Home "Local Media Player" -> Icons.Default.LocalPlay "Location Tracker & Sharer" -> Icons.Default.MyLocation + "Meditation" -> Icons.Default.SelfImprovement "Messaging" -> AutoMirrored.Default.Message "Money" -> Icons.Default.Money "Multimedia" -> Icons.Default.MusicVideo @@ -133,6 +140,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Religion" -> Icons.Default.Church "Remote Access" -> Icons.Default.BrowserUpdated "Remote Controller" -> Icons.Default.SettingsRemote + "Schedule" -> Icons.Default.CalendarMonth "Science & Education" -> Icons.Default.Science "Security" -> Icons.Default.Security "Shopping List" -> Icons.Default.ShoppingCart @@ -141,8 +149,10 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "System" -> Icons.Default.Settings "Task" -> Icons.Default.TaskAlt "Text Editor" -> Icons.Default.EditNote + "Text to Speech" -> Icons.Default.RecordVoiceOver "Theming" -> Icons.Default.Style "Time" -> Icons.Default.AccessTime + "Time Tracker" -> Icons.Default.Timer "Translation & Dictionary" -> Icons.Default.Translate "Voice & Video Chat" -> Icons.Default.VideoChat "Unit Convertor" -> Icons.Default.CurrencyExchange @@ -171,6 +181,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Connectivity" -> CategoryGroups.network "Contact" -> CategoryGroups.communication "Development" -> CategoryGroups.interests + "Diet" -> CategoryGroups.interests "DNS & Hosts" -> CategoryGroups.network "Draw" -> CategoryGroups.interests "Ebook Reader" -> CategoryGroups.media @@ -185,6 +196,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Games" -> CategoryGroups.media "Graphics" -> CategoryGroups.interests "Habit Tracker" -> CategoryGroups.productivity + "Health Manager" -> CategoryGroups.productivity "Icon Pack" -> CategoryGroups.device "Internet" -> CategoryGroups.network "Inventory" -> CategoryGroups.tools @@ -192,6 +204,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Launcher" -> CategoryGroups.device "Local Media Player" -> CategoryGroups.media "Location Tracker & Sharer" -> CategoryGroups.tools + "Meditation" -> CategoryGroups.interests "Messaging" -> CategoryGroups.communication "Money" -> CategoryGroups.wallets "Multimedia" -> CategoryGroups.media @@ -212,6 +225,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Religion" -> CategoryGroups.interests "Remote Access" -> CategoryGroups.network "Remote Controller" -> CategoryGroups.tools + "Schedule" -> CategoryGroups.productivity "Science & Education" -> CategoryGroups.interests "Security" -> CategoryGroups.device "Shopping List" -> CategoryGroups.tools @@ -220,8 +234,10 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "System" -> CategoryGroups.device "Task" -> CategoryGroups.productivity "Text Editor" -> CategoryGroups.productivity + "Text to Speech" -> CategoryGroups.device "Theming" -> CategoryGroups.device "Time" -> CategoryGroups.productivity + "Time Tracker" -> CategoryGroups.productivity "Translation & Dictionary" -> CategoryGroups.tools "Voice & Video Chat" -> CategoryGroups.communication "Unit Convertor" -> CategoryGroups.tools From ce09ad713d7bef461ffb6aa2f4a31f2ab81d25ec Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 11 May 2026 11:14:23 -0300 Subject: [PATCH 15/44] Move changelog above whatsNew in app details --- app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt | 8 ++++---- .../ui/details/DetailsTestKt/DetailsTest_9f4e7551_0.png | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt index 45b2143eb..0c2d3916a 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt @@ -162,10 +162,6 @@ fun AppDetails( AnimatedVisibility(item.showWarnings) { AppDetailsWarnings(item, Modifier.padding(horizontal = 16.dp)) } - // Screenshots - if (item.phoneScreenshots.isNotEmpty()) { - Screenshots(item.networkState.showWarningDialog, item.phoneScreenshots) - } // What's New if ( item.installedVersionCode != null && (item.whatsNew != null || item.app.changelog != null) @@ -202,6 +198,10 @@ fun AppDetails( } } } + // Screenshots + if (item.phoneScreenshots.isNotEmpty()) { + Screenshots(item.networkState.showWarningDialog, item.phoneScreenshots) + } // Anti-features if (!item.antiFeatures.isNullOrEmpty()) { AntiFeatures( diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsTest_9f4e7551_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsTest_9f4e7551_0.png index 6625234cb..31cdd4fb7 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsTest_9f4e7551_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsTest_9f4e7551_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbd1604f0c6daea3b851ab8cf1a54e8072851c64b6a7ce244d3f8425737a1814 -size 328503 +oid sha256:1bb5854c4ca879caa91c00d719747503d0f44116e0f0d6ee5fc39739bb5ea7c5 +size 328351 From 9d602ee64226c6dd0638d347a8991dec0d3a5f4e Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 11 May 2026 15:48:56 -0300 Subject: [PATCH 16/44] Don't show brief "No app found" if opening app details via external intent we do this by differentiating between null as not yet retrieved a value and null as no value found by introducing a Loadable helper class. --- .../org/fdroid/ui/details/DetailsPresenter.kt | 32 +++++++++++++------ .../main/kotlin/org/fdroid/utils/Loadable.kt | 7 ++++ 2 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 app/src/main/kotlin/org/fdroid/utils/Loadable.kt diff --git a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt index 86f0fd8fa..b3b82eb7d 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt @@ -16,7 +16,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext import org.fdroid.LocaleChooser.getBestLocale import org.fdroid.UpdateChecker -import org.fdroid.database.App import org.fdroid.database.AppPrefs import org.fdroid.database.AppVersion import org.fdroid.database.FDroidDatabase @@ -29,6 +28,8 @@ import org.fdroid.install.InstallState import org.fdroid.repo.RepoPreLoader import org.fdroid.settings.SettingsManager import org.fdroid.ui.apps.AppWithIssueItem +import org.fdroid.utils.Loaded +import org.fdroid.utils.Loading import org.fdroid.utils.sha256 private const val TAG = "DetailsPresenter" @@ -57,18 +58,24 @@ fun DetailsPresenter( val currentRepoId = currentRepoIdFlow.collectAsState().value val appsWithIssues = appsWithIssuesFlow.collectAsState().value val appDao = db.getAppDao() - val app = - produceState(null, currentRepoId) { + val loadableApp = + produceState(Loading(), currentRepoId) { withContext(dispatcher) { if (currentRepoId == null) { val flow = appDao.getApp(packageName).asFlow() - flow.collect { value = it } + flow.collect { value = Loaded(it) } } else { - value = appDao.getApp(currentRepoId, packageName) + value = Loaded(appDao.getApp(currentRepoId, packageName)) } } } - .value ?: return NotFoundAppDetailsItem + .value + val app = + when (loadableApp) { + is Loading -> return null + is Loaded if loadableApp.value != null -> loadableApp.value + else -> return NotFoundAppDetailsItem + } val versions = produceState?>(null, currentRepoId) { withContext(dispatcher) { @@ -119,11 +126,16 @@ fun DetailsPresenter( ) } } - val repo = - produceState(null, app) { - withContext(dispatcher) { value = repoManager.getRepository(app.repoId) } + val loadableRepo = + produceState(Loading(), app) { + withContext(dispatcher) { value = Loaded(repoManager.getRepository(app.repoId)) } } - .value ?: return NotFoundAppDetailsItem + .value + val repo = + when (loadableRepo) { + is Loading -> return null + is Loaded -> loadableRepo.value ?: return NotFoundAppDetailsItem + } val repositories = produceState(emptyList(), packageName) { withContext(dispatcher) { diff --git a/app/src/main/kotlin/org/fdroid/utils/Loadable.kt b/app/src/main/kotlin/org/fdroid/utils/Loadable.kt new file mode 100644 index 000000000..342cfe5fb --- /dev/null +++ b/app/src/main/kotlin/org/fdroid/utils/Loadable.kt @@ -0,0 +1,7 @@ +package org.fdroid.utils + +sealed class Loadable + +class Loading : Loadable() + +class Loaded(val value: T) : Loadable() From 63ca340f974067eff46110a8dad01ffd90a5df83 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 11 May 2026 17:51:46 -0300 Subject: [PATCH 17/44] Add unit tests for DetailsPresenter --- .../org/fdroid/ui/details/DetailsPresenter.kt | 2 - .../org/fdroid/ui/utils/PreviewUtils.kt | 17 +- .../fdroid/ui/details/DetailsPresenterTest.kt | 680 ++++++++++++++++++ .../org/fdroid/database/DbAppCheckerTest.kt | 3 +- 4 files changed, 693 insertions(+), 9 deletions(-) create mode 100644 app/src/test/java/org/fdroid/ui/details/DetailsPresenterTest.kt diff --git a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt index b3b82eb7d..55d523edd 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt @@ -34,8 +34,6 @@ import org.fdroid.utils.sha256 private const val TAG = "DetailsPresenter" -// TODO write tests for this function -// see: https://github.com/cashapp/molecule?tab=readme-ov-file#testing @Composable fun DetailsPresenter( db: FDroidDatabase, diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt index 62e53ee50..a1671baea 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt @@ -142,6 +142,13 @@ val categoryItems = CategoryItem("doesn't exist", "Foo bar"), ) +private val description = + "NewPipe does not use any Google framework libraries, or the YouTube API. " + + "It only parses the website in order to gain the information it needs. " + + "Therefore this app can be used on devices without Google Services installed. " + + "Also, you don't need a YouTube account to use NewPipe, and it's FLOSS.\n\n" + + LoremIpsum(128).values.joinToString(" ") + val testApp = LoadedAppDetailsItem( app = @@ -150,6 +157,9 @@ val testApp = packageName = "org.schabi.newpipe", added = 1441756800000, lastUpdated = 1747214796000, + name = mapOf("en-US" to "New Pipe"), + summary = mapOf("en-US" to "Lightweight YouTube frontend"), + description = mapOf("en-US" to description), webSite = "https://newpipe.net", changelog = "https://github.com/TeamNewPipe/NewPipe/releases", license = "GPL-3.0-or-later", @@ -178,12 +188,7 @@ val testApp = appPrefs = AppPrefs("org.schabi.newpipe"), name = "New Pipe", summary = "Lightweight YouTube frontend", - description = - "NewPipe does not use any Google framework libraries, or the YouTube API. " + - "It only parses the website in order to gain the information it needs. " + - "Therefore this app can be used on devices without Google Services installed. " + - "Also, you don't need a YouTube account to use NewPipe, and it's FLOSS.\n\n" + - LoremIpsum(128).values.joinToString(" "), + description = description, categories = categoryItems.subList(0, 5), antiFeatures = listOf( diff --git a/app/src/test/java/org/fdroid/ui/details/DetailsPresenterTest.kt b/app/src/test/java/org/fdroid/ui/details/DetailsPresenterTest.kt new file mode 100644 index 000000000..11b4462b1 --- /dev/null +++ b/app/src/test/java/org/fdroid/ui/details/DetailsPresenterTest.kt @@ -0,0 +1,680 @@ +package org.fdroid.ui.details + +import android.content.Intent +import android.content.pm.PackageInfo +import android.content.pm.Signature +import androidx.core.content.pm.PackageInfoCompat +import androidx.core.content.pm.PackageInfoCompat.getLongVersionCode +import androidx.lifecycle.MutableLiveData +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.ReceiveTurbine +import app.cash.turbine.test +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.spyk +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.fdroid.CompatibilityChecker +import org.fdroid.UpdateChecker +import org.fdroid.database.App +import org.fdroid.database.AppDao +import org.fdroid.database.AppIssue +import org.fdroid.database.AppManifest +import org.fdroid.database.AppPrefs +import org.fdroid.database.AppPrefsDao +import org.fdroid.database.AppVersion +import org.fdroid.database.FDroidDatabase +import org.fdroid.database.NotAvailable +import org.fdroid.database.Repository +import org.fdroid.database.VersionDao +import org.fdroid.download.NetworkState +import org.fdroid.index.IndexFormatVersion +import org.fdroid.index.RELEASE_CHANNEL_BETA +import org.fdroid.index.RepoManager +import org.fdroid.index.v2.FileV1 +import org.fdroid.index.v2.SignerV2 +import org.fdroid.index.v2.UsesSdkV2 +import org.fdroid.install.AppInstallManager +import org.fdroid.install.InstallState +import org.fdroid.repo.RepoPreLoader +import org.fdroid.settings.SettingsManager +import org.fdroid.ui.apps.AppWithIssueItem +import org.fdroid.ui.utils.testApp +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@Config(sdk = [34]) // needed for oldTargetSdk assertion +@RunWith(RobolectricTestRunner::class) +internal class DetailsPresenterTest { + + private val packageName = testApp.app.packageName + private val repoId = testApp.app.repoId + + private val db: FDroidDatabase = mockk() + private val appDao: AppDao = mockk() + private val versionDao: VersionDao = mockk() + private val appPrefsDao: AppPrefsDao = mockk() + private val repoManager: RepoManager = mockk() + private val repoPreLoader: RepoPreLoader = mockk() + private val settingsManager: SettingsManager = mockk() + private val appInstallManager: AppInstallManager = mockk() + private val viewModel: AppDetailsViewModel = mockk(relaxed = true) + private val compatibilityChecker: CompatibilityChecker = mockk() + private val updateChecker = UpdateChecker(compatibilityChecker) + + private val repository = + Repository( + repoId = repoId, + address = "https://example.org/fdroid/repo", + timestamp = 123L, + formatVersion = IndexFormatVersion.TWO, + certificate = "abcd", + version = 1L, + weight = 100, + lastUpdated = 456L, + ) + + private val app: App = mockk() + private val version: AppVersion = mockk() + private val versionCode = 42L + + init { + every { db.getAppDao() } returns appDao + every { db.getVersionDao() } returns versionDao + every { db.getAppPrefsDao() } returns appPrefsDao + every { repoManager.getRepository(repoId) } returns repository + every { repoPreLoader.defaultRepoAddresses } returns emptySet() + every { settingsManager.proxyConfig } returns null + every { appInstallManager.getAppFlow(packageName) } returns flowOf(InstallState.Unknown) + + every { appDao.getApp(repoId, packageName) } returns app + every { app.metadata } returns testApp.app + every { app.repoId } returns repoId + every { app.authorName } returns null + every { app.packageName } returns packageName + every { app.getDescription(any()) } returns testApp.description + every { app.getIcon(any()) } returns null + every { app.getFeatureGraphic(any()) } returns null + every { app.getPhoneScreenshots(any()) } returns emptyList() + + every { versionDao.getAppVersions(repoId, packageName) } returns + MutableLiveData(listOf(version)) + every { version.versionCode } returns versionCode + every { version.versionName } returns "1.0" + every { version.file } returns FileV1(name = "test.apk", sha256 = "abcd", size = 123L) + every { version.added } returns 300L + every { version.size } returns 123L + every { version.signer } returns null + every { version.releaseChannels } returns emptyList() + every { version.packageManifest } returns AppManifest(versionName = "1.0", versionCode = 42L) + every { version.hasKnownVulnerability } returns false + every { version.isCompatible } returns true + every { version.getWhatsNew(any()) } returns "Bug fixes" + every { version.antiFeatureKeys } returns emptyList() + + every { appDao.getRepositoryIdsForApp(packageName) } returns listOf(repoId) + every { appPrefsDao.getAppPrefs(packageName) } returns MutableLiveData(AppPrefs(packageName)) + every { compatibilityChecker.isCompatible(any()) } returns true + } + + private val appInfoFlow = MutableStateFlow(AppInfo(packageName = packageName)) + private val currentRepoIdFlow = MutableStateFlow(repoId) + private val showAntiFeaturesOnboardingFlow = MutableStateFlow(false) + private val appsWithIssuesFlow = MutableStateFlow?>(emptyList()) + private val networkStateFlow = MutableStateFlow(NetworkState(isOnline = true, isMetered = false)) + + val presenterFlow = + moleculeFlow(RecompositionMode.Immediate) { + DetailsPresenter( + db = db, + dispatcher = Dispatchers.Unconfined, + repoManager = repoManager, + repoPreLoader = repoPreLoader, + updateChecker = updateChecker, + settingsManager = settingsManager, + appInstallManager = appInstallManager, + viewModel = viewModel, + packageInfoFlow = appInfoFlow, + currentRepoIdFlow = currentRepoIdFlow, + showAntiFeaturesOnboardingFlow = showAntiFeaturesOnboardingFlow, + appsWithIssuesFlow = appsWithIssuesFlow, + networkStateFlow = networkStateFlow, + ) + } + + // Not found cases + + @Test + fun emitsNotFoundWhenAppIsNull() = runTest { + // App does not exist in the database for this repo + every { appDao.getApp(repoId, packageName) } returns null + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + + cancelAndPrintRemainingEvents() + } + } + + @Test + fun emitsNotFoundWhenRepoIsNull() = runTest { + // Repo has been removed (unlikely here) + every { repoManager.getRepository(repoId) } returns null + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + + cancelAndPrintRemainingEvents() + } + } + + // App that can be installed + + @Test + fun emitsInstallableLoadedItem() = runTest { + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + assertEquals(MainButtonState.INSTALL, item.mainButtonState) + assertNotNull(item.versions) + + assertEquals(packageName, item.app.packageName) + assertEquals(testApp.name, item.name) + assertEquals(testApp.summary, item.summary) + assertEquals(getHtmlDescription(testApp.description), item.description) + assertEquals(repoId, item.preferredRepoId) + assertEquals(InstallState.Unknown, item.installState) + assertEquals(MainButtonState.INSTALL, item.mainButtonState) + assertEquals(version, item.suggestedVersion) + assertFalse(item.showAntiFeaturesOnboarding) + + val versionItem = item.versions.single() + assertNotNull(versionItem) + assertEquals(version, versionItem.version) + assertTrue(versionItem.isSuggested) + assertTrue(versionItem.isCompatible) + assertTrue(versionItem.isSignerCompatible) + assertFalse(versionItem.isInstalled) + assertTrue(versionItem.showInstallButton) + + assertEquals(NetworkState(isOnline = true, isMetered = false), item.networkState) + assertNull(item.installedVersionCode) + assertNull(item.installedVersion) + assertNull(item.installedSigner) + assertEquals("Bug fixes", item.whatsNew) + assertTrue(item.showDonate) + assertTrue(item.showAuthorContact) + assertEquals(testApp.liberapayUri, item.liberapayUri) + assertEquals(testApp.openCollectiveUri, item.openCollectiveUri) + assertEquals(testApp.bitcoinUri, item.bitcoinUri) + assertEquals(testApp.litecoinUri, item.litecoinUri) + assertFalse(item.showWarnings) + assertFalse(item.ignoresCurrentUpdate) + assertFalse(item.ignoresAllUpdates) + assertFalse(item.allowsBetaVersions) + assertFalse(item.oldTargetSdk) + assertEquals(emptyList(), item.categories) + + cancelAndPrintRemainingEvents() + } + } + + // MainButtonState + + @Test + fun showsLoadingStateWhenVersionsNotYetAvailable() = runTest { + setupInstalledApp(versionCode) + + // Return a LiveData with no initial value so versions stay null in the presenter + every { versionDao.getAppVersions(repoId, packageName) } returns MutableLiveData() + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + assertEquals(MainButtonState.LOADING, item.mainButtonState) + assertEquals(null, item.versions) + assertEquals(versionCode, item.installedVersionCode) + + cancelAndPrintRemainingEvents() + } + } + + @Test + fun showsLoadingStateWhenNotInstalledAndVersionsNotYetAvailable() = runTest { + // similar as above, but with no installed version, so different code path + every { versionDao.getAppVersions(repoId, packageName) } returns MutableLiveData() + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + assertEquals(MainButtonState.LOADING, item.mainButtonState) + assertNull(item.versions) + assertNull(item.installedVersionCode) + + cancelAndPrintRemainingEvents() + } + } + + @Test + fun showsProgressStateWhenInstallationIsInProgress() = runTest { + val progressState = + InstallState.Starting(name = testApp.name, versionName = "1.0", lastUpdated = 300L) + every { appInstallManager.getAppFlow(packageName) } returns flowOf(progressState) + + presenterFlow.test { + // after first load state is still unknown + val item1 = awaitNonNullItem() + assertIs(item1) + assertEquals(InstallState.Unknown, item1.installState) + + // then second emission reflects proper progress state + val item2 = awaitNonNullItem() + assertIs(item2) + assertEquals(MainButtonState.PROGRESS, item2.mainButtonState) + assertEquals(progressState, item2.installState) + + cancelAndPrintRemainingEvents() + } + } + + @Test + fun emitsUpdateButtonWhenInstalledAndUpdateAvailable() = runTest { + setupInstalledApp(versionCode = versionCode - 1) + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + assertEquals(MainButtonState.UPDATE, item.mainButtonState) + assertNotNull(item.versions) + assertEquals(packageName, item.app.packageName) + assertNotNull(item.installedSigner) + + cancelAndPrintRemainingEvents() + } + } + + @Test + fun emitsOpenButtonWhenInstalledWithNoUpdate() = runTest { + setupInstalledApp(versionCode = versionCode, isInstalled = true) + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + assertEquals(MainButtonState.NONE, item.mainButtonState) + assertTrue(item.showOpenButton) + assertNotNull(item.versions) + assertEquals(version, item.installedVersion) + assertEquals(versionCode, item.installedVersionCode) + assertEquals("0.1", item.installedVersionName) + + val versionItem = item.versions.single() + assertNotNull(versionItem) + assertTrue(versionItem.isInstalled) + + cancelAndPrintRemainingEvents() + } + } + + @Test + fun emitsNoneButtonWhenNoCompatibleVersions() = runTest { + // all versions are not compatible with this device + every { version.isCompatible } returns false + every { compatibilityChecker.isCompatible(any()) } returns false + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + assertEquals(MainButtonState.NONE, item.mainButtonState) + assertTrue(item.isIncompatible) + assertTrue(item.showWarnings) + assertTrue(item.versions?.all { !it.isCompatible } ?: false) + assertFalse(item.showOpenButton) + + cancelAndPrintRemainingEvents() + } + } + + // Signer compatibility + + @Test + fun nullSignerVersionFoundWhenMultipleVersionsShareVersionCode() = runTest { + // The presenter finds the installed version by signer when multiple versions share the same + // version code. Versions without a signer are explicitly allowed by F-Droid, so a + // null signer version must still be recognized as the installed one. + every { version.signer } returns null + + val version2: AppVersion = mockk() + every { version2.versionCode } returns versionCode // same code as default version + every { version2.signer } returns SignerV2(listOf("a_different_signer_hash")) + every { version2.isCompatible } returns true + every { version2.antiFeatureKeys } returns emptyList() + every { versionDao.getAppVersions(repoId, packageName) } returns + MutableLiveData(listOf(version, version2)) + + // Install at the same version code so both versions appear in installedVersions + setupInstalledApp(versionCode = versionCode, isInstalled = true) + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + // version (null signer, first in list) is matched, version2 (mismatched signer) is skipped + assertEquals(version, item.installedVersion) + + cancelAndPrintRemainingEvents() + } + } + + @Test + fun signerMismatchHidesInstallButtonForVersion() = runTest { + // When the installed app's signer does not match a version's signer, the version must + // not show an installation button even if its version code is higher than the installed one. + every { version.signer } returns SignerV2(listOf("a_different_signer_hash")) + + setupInstalledApp(versionCode = versionCode - 1) + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + val versionItem = item.versions?.single() + assertNotNull(versionItem) + assertFalse(versionItem.isSignerCompatible) + assertFalse(versionItem.showInstallButton) // hidden because of signer mismatch + // Since the signer doesn't match allowedSigners, no update is suggested either + assertNull(item.suggestedVersion) + assertEquals(MainButtonState.NONE, item.mainButtonState) + + cancelAndPrintRemainingEvents() + } + } + + // Misc + + @Test + fun showsAntiFeaturesOnboardingWhenFlagIsSet() = runTest { + showAntiFeaturesOnboardingFlow.value = true + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + assertTrue(item.showAntiFeaturesOnboarding) + + cancelAndPrintRemainingEvents() + } + } + + @Test + fun showsWarningWhenIssueIsPresent() = runTest { + val issue: AppIssue = NotAvailable + appsWithIssuesFlow.value = + listOf( + AppWithIssueItem( + packageName = packageName, + name = testApp.name, + installedVersionName = "1.0", + installedVersionCode = versionCode, + issue = issue, + lastUpdated = 200L, + ) + ) + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + assertEquals(issue, item.issue) + assertTrue(item.showWarnings) + assertFalse(item.isIncompatible) + + cancelAndPrintRemainingEvents() + } + } + + @Test + fun showsWarningWhenTargetSdkIsTooOld() = runTest { + // targetSdk 28 on SDK 34 means isAutoUpdateSupported() = false, so oldTargetSdk = true + every { version.packageManifest } returns + AppManifest( + versionName = "1.0", + versionCode = versionCode, + usesSdk = UsesSdkV2(minSdkVersion = 21, targetSdkVersion = 28), + ) + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + assertTrue(item.oldTargetSdk) + assertTrue(item.showWarnings) + assertFalse(item.isIncompatible) + assertNull(item.issue) + + cancelAndPrintRemainingEvents() + } + } + + @Test + fun hidesAuthorContactWhenNoEmailNorWebSite() = runTest { + every { app.metadata } returns testApp.app.copy(authorEmail = null, authorWebSite = null) + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + assertFalse(item.showAuthorContact) + + cancelAndPrintRemainingEvents() + } + } + + @Test + fun showsAuthorHasMoreThanOneApp() = runTest { + val authorName = "Test Dev" + every { app.authorName } returns authorName + every { appDao.hasAuthorMoreThanOneApp(authorName) } returns MutableLiveData(true) + + presenterFlow.test { + // First non-null item has the initial produceState value of false + val item1 = awaitNonNullItem() + assertIs(item1) + assertFalse(item1.authorHasMoreThanOneApp) + + // Second emission reflects the actual DB result + val item2 = awaitNonNullItem() + assertIs(item2) + assertTrue(item2.authorHasMoreThanOneApp) + + cancelAndPrintRemainingEvents() + } + } + + // Repository visibility + + @Test + fun hidesRepositoriesWhenAppIsInDefaultRepoOnly() = runTest { + // The app's repo address is listed as a default address -> repo chooser should stay hidden + every { repoPreLoader.defaultRepoAddresses } returns setOf(repository.address) + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + assertTrue(item.repositories.isEmpty()) + + cancelAndPrintRemainingEvents() + } + } + + @Test + fun showsRepositoriesWhenAppIsInNonDefaultRepo() = runTest { + // The repo address is NOT a default address -> repo chooser must be shown + every { repoPreLoader.defaultRepoAddresses } returns emptySet() + + presenterFlow.test { + // Initially repositories are empty because of async load + val item1 = awaitNonNullItem() + assertIs(item1) + assertEquals(emptyList(), item1.repositories) + + // After the async load the single non-default repo becomes visible + val item2 = awaitNonNullItem() + assertIs(item2) + assertEquals(repository, item2.repositories.single()) + + cancelAndPrintRemainingEvents() + } + } + + @Test + fun showsMultipleRepositoriesWhenAppInMultipleRepos() = runTest { + // set up a second repo this app is in + val repoId2 = 2L + val repo2 = + Repository( + repoId = repoId2, + address = "https://example.com/second/repo", + timestamp = 200L, + formatVersion = IndexFormatVersion.TWO, + certificate = "abcde", + version = 2L, + weight = 50, + lastUpdated = 500L, + ) + every { appDao.getRepositoryIdsForApp(packageName) } returns listOf(repoId, repoId2) + every { repoManager.getRepository(repoId2) } returns repo2 + + presenterFlow.test { + // repos are loaded async, so the list is empty by default (don't show repo chooser) + val item1 = awaitNonNullItem() + assertIs(item1) + assertEquals(emptyList(), item1.repositories) + + // now the app is in two repos + val item2 = awaitNonNullItem() + assertIs(item2) + assertEquals(2, item2.repositories.size) + + cancelAndPrintRemainingEvents() + } + } + + // App preferences + + @Test + fun usesPreferredRepoIdFromAppPrefs() = runTest { + // When the user has chosen a preferred repo, preferredRepoId must reflect that choice + // rather than defaulting to the app's own repoId. + val customPreferredRepoId = 99L + every { appPrefsDao.getAppPrefs(packageName) } returns + MutableLiveData(AppPrefs(packageName, preferredRepoId = customPreferredRepoId)) + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + assertEquals(customPreferredRepoId, item.preferredRepoId) + + cancelAndPrintRemainingEvents() + } + } + + @Test + fun ignoresAllUpdatesFromAppPrefs() = runTest { + // When the user ignores all updates, ignoresAllUpdates must be true and there should be + // no suggested version, resulting in a NONE button. + every { appPrefsDao.getAppPrefs(packageName) } returns + MutableLiveData(AppPrefs(packageName).toggleIgnoreAllUpdates()) + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + assertTrue(item.ignoresAllUpdates) + assertNull(item.suggestedVersion) + assertEquals(MainButtonState.NONE, item.mainButtonState) + + cancelAndPrintRemainingEvents() + } + } + + @Test + fun ignoresCurrentUpdateFromAppPrefs() = runTest { + setupInstalledApp(versionCode = versionCode - 1) + every { appPrefsDao.getAppPrefs(packageName) } returns + MutableLiveData(AppPrefs(packageName, ignoreVersionCodeUpdate = versionCode)) + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + // possibleUpdate exists, but ignores, so suggestedVersion is null + assertNull(item.suggestedVersion) + assertEquals(MainButtonState.NONE, item.mainButtonState) + assertNotNull(item.actions.ignoreThisUpdate) // there is a version to un-ignore + assertTrue(item.ignoresCurrentUpdate) + assertFalse(item.ignoresAllUpdates) + + cancelAndPrintRemainingEvents() + } + } + + @Test + fun allowsBetaVersionsFromAppPrefs() = runTest { + // When the user opts into beta versions, allowsBetaVersions must be reflected in the item. + every { appPrefsDao.getAppPrefs(packageName) } returns + MutableLiveData(AppPrefs(packageName).toggleReleaseChannel(RELEASE_CHANNEL_BETA)) + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + assertTrue(item.allowsBetaVersions) + + cancelAndPrintRemainingEvents() + } + } + + private fun setupInstalledApp(versionCode: Long, isInstalled: Boolean = false) { + val signature = mockk() + every { signature.toByteArray() } returns byteArrayOf(0xAB.toByte(), 0xCD.toByte()) + + val packageInfo = + spyk(PackageInfo()).also { + it.packageName = packageName + it.versionName = "0.1" + @Suppress("DEPRECATION") + it.signatures = arrayOf(signature) + } + mockkStatic(PackageInfoCompat::getLongVersionCode) + every { getLongVersionCode(packageInfo) } returns versionCode + + appInfoFlow.value = + AppInfo( + packageName = packageName, + packageInfo = packageInfo, + launchIntent = if (isInstalled) Intent() else null, + ) + } + + private suspend fun ReceiveTurbine.awaitNonNullItem(): AppDetailsItem { + var item: AppDetailsItem? = null + var count = 0 + while (item == null) { + item = awaitItem() + count++ + } + println("Received non-null item after $count emissions") + return item + } + + private suspend fun ReceiveTurbine.cancelAndPrintRemainingEvents() { + val lastItems = cancelAndConsumeRemainingEvents() + if (!lastItems.isEmpty()) println("Received additional items after cancellation") + lastItems.forEach { item -> println(" $item") } + } +} diff --git a/libs/database/src/test/java/org/fdroid/database/DbAppCheckerTest.kt b/libs/database/src/test/java/org/fdroid/database/DbAppCheckerTest.kt index 5a829c20f..f397a0a1b 100644 --- a/libs/database/src/test/java/org/fdroid/database/DbAppCheckerTest.kt +++ b/libs/database/src/test/java/org/fdroid/database/DbAppCheckerTest.kt @@ -5,6 +5,7 @@ import android.content.pm.ApplicationInfo import android.content.pm.InstallSourceInfo import android.content.pm.PackageInfo import android.content.pm.PackageManager +import android.content.pm.Signature import android.os.Build import androidx.core.content.pm.PackageInfoCompat.getLongVersionCode import io.mockk.every @@ -493,7 +494,7 @@ internal class DbAppCheckerTest { } every { appInfo.loadLabel(packageManager) } returns appName - val sig = mockk() + val sig = mockk() every { sig.toByteArray() } returns signerBytes val packageInfo = From b30e4b07b99d45b8d5800c6a3a6cb68efa4c5710 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 13 May 2026 16:20:05 -0300 Subject: [PATCH 18/44] Update dependencies --- gradle/libs.versions.toml | 9 +- gradle/verification-metadata.xml | 260 +++++++++++++++++++++++++++++++ 2 files changed, 265 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 99bcb2ead..9926b1589 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ ktfmt = "0.26.0" kotlinxSerializationCore = "1.11.0" kotlinxSerializationJson = "1.11.0" -kotlinxCoroutinesTest = "1.10.2" +kotlinxCoroutinesTest = "1.11.0" ktor = "3.4.3" okhttp = "4.12.0" @@ -38,7 +38,7 @@ androidxConstraintlayout = "2.2.1" androidxSwipeRefreshLayout = "1.2.0" androidxVectordrawable = "1.2.0" androidxGridlayout = "1.1.0" -androidxComposeBom = "2026.04.01" +androidxComposeBom = "2026.05.00" androidxActivityCompose = "1.13.0" accompanistDrawablepainter = "0.37.3" @@ -47,7 +47,8 @@ nav3Core = "1.1.1" lifecycleViewmodelNav3 = "2.10.0" material3AdaptiveNav3 = "1.0.0-alpha03" -material = "1.13.0" +material = "1.14.0" +#noinspection GradleDependency 1.5.0-alpha19 depends on SDK 37 material3 = "1.5.0-alpha18" # should be possible to remove once 1.5.0 is stable and in bom #noinspection GradleDependency 1.3.0-alpha10 depends on SDK 37 material3AdaptiveNav = "1.3.0-alpha09" @@ -68,7 +69,7 @@ guava = "33.6.0-android" rxjava = "3.1.12" rxandroid = "3.0.2" -slf4jApi = "2.0.17" +slf4jApi = "2.0.18" microutilsKotlinLogging = "3.0.5" logbackClassic = "1.5.32" logbackAndroid = "3.0.0" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index edca3b1e5..bfa231173 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -333,11 +333,21 @@ + + + + + + + + + + @@ -363,11 +373,21 @@ + + + + + + + + + + @@ -393,11 +413,21 @@ + + + + + + + + + + @@ -423,11 +453,21 @@ + + + + + + + + + + @@ -453,6 +493,11 @@ + + + + + @@ -503,11 +548,21 @@ + + + + + + + + + + @@ -528,6 +583,11 @@ + + + + + @@ -613,11 +673,21 @@ + + + + + + + + + + @@ -643,11 +713,21 @@ + + + + + + + + + + @@ -673,11 +753,21 @@ + + + + + + + + + + @@ -703,11 +793,21 @@ + + + + + + + + + + @@ -733,11 +833,21 @@ + + + + + + + + + + @@ -763,11 +873,21 @@ + + + + + + + + + + @@ -793,11 +913,21 @@ + + + + + + + + + + @@ -823,11 +953,21 @@ + + + + + + + + + + @@ -853,11 +993,21 @@ + + + + + + + + + + @@ -883,11 +1033,21 @@ + + + + + + + + + + @@ -913,11 +1073,21 @@ + + + + + + + + + + @@ -943,11 +1113,21 @@ + + + + + + + + + + @@ -973,11 +1153,21 @@ + + + + + + + + + + @@ -1003,11 +1193,21 @@ + + + + + + + + + + @@ -1033,11 +1233,21 @@ + + + + + + + + + + @@ -1063,11 +1273,21 @@ + + + + + + + + + + @@ -1163,6 +1383,11 @@ + + + + + @@ -3288,6 +3513,11 @@ + + + + + @@ -6444,6 +6674,11 @@ + + + + + @@ -6479,6 +6714,11 @@ + + + + + @@ -6509,11 +6749,21 @@ + + + + + + + + + + @@ -6874,6 +7124,11 @@ + + + + + @@ -6884,6 +7139,11 @@ + + + + + From 8ef37ec5ad4142af6230fea865f3c1b0db596e11 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 29 Apr 2026 16:48:20 +0200 Subject: [PATCH 19/44] gitlab-ci: use fdroid/sdkmanager for all SDK package installs sdkmanager!29 --- .gitlab-ci.yml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index de270c259..d176cc369 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -27,13 +27,13 @@ workflow: - apt-get -qy install -t trixie-backports --no-install-recommends git git-lfs sdkmanager openjdk-21-jdk-headless - test -n "$ANDROID_HOME" || source /etc/profile.d/bsenv.sh - - - export cmdline_tools_latest="$ANDROID_HOME/cmdline-tools/latest/bin" - - test -e $cmdline_tools_latest && export PATH="$cmdline_tools_latest:$PATH" - - export GRADLE_USER_HOME=$PWD/.gradle - export ANDROID_COMPILE_SDK=`sed -n 's,.*compileSdk = "\([0-9][0-9]*\)".*,\1,p' gradle/libs.versions.toml` - - echo y | sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" > /dev/null + - sdkmanager --install + "build-tools;${ANDROID_COMPILE_SDK}.0.0" + "platform-tools" + "platforms;android-${ANDROID_COMPILE_SDK}" + "platforms;android-37.0" # index-v1.jar tests need SHA1 support still, TODO use apksig to validate JAR sigs - sed -i 's,SHA1 denyAfter 20[0-9][0-9],SHA1 denyAfter 2027,' /usr/lib/jvm/java-21-openjdk-amd64/conf/security/java.security @@ -232,9 +232,11 @@ libs database schema: - apt-get -qy --no-install-recommends install openjdk-21-jdk-headless git git-lfs sdkmanager - export ANDROID_HOME=/opt/android-sdk - export ANDROID_COMPILE_SDK=`sed -n 's,.*compileSdk = "\([0-9][0-9]*\)".*,\1,p' gradle/libs.versions.toml` - - sdkmanager "platforms;android-$ANDROID_COMPILE_SDK" "build-tools;$ANDROID_COMPILE_SDK.0.0" - - sdkmanager "build-tools;34.0.0" # something (AGP?) still pulls in old build-tools - - sdkmanager "build-tools;35.0.0" # something (AGP?) still pulls in old build-tools + - sdkmanager --install + "build-tools;${ANDROID_COMPILE_SDK}.0.0" + "platform-tools" + "platforms;android-${ANDROID_COMPILE_SDK}" + - git lfs install script: - ./gradlew :libs:database:kspDebugKotlin @@ -266,7 +268,7 @@ libs database schema: - $ANDROID_HOME/cmdline-tools/latest/bin/avdmanager --verbose delete avd --name "$NAME_AVD" - export AVD="$AVD_PACKAGE" - - echo y | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install "$AVD" + - sdkmanager --install "$AVD" - echo no | $ANDROID_HOME/cmdline-tools/latest/bin/avdmanager --verbose create avd --name "$NAME_AVD" --package "$AVD" --device "pixel" - df -h - start-emulator.sh From 940c90f026602423f864dc465a31f1c984e3f414 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 13 May 2026 16:37:38 -0300 Subject: [PATCH 20/44] Set compileSdk to 37 and remove custom CI logic for `libs database schema` job as it just causes problems --- .gitlab-ci.yml | 22 +++++-------------- gradle/libs.versions.toml | 8 +++---- gradle/verification-metadata.xml | 20 +++++++++++++++++ .../src/test/resources/robolectric.properties | 2 ++ 4 files changed, 30 insertions(+), 22 deletions(-) create mode 100644 libs/database/src/test/resources/robolectric.properties diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d176cc369..95beecbb0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,15 +25,13 @@ workflow: || echo "deb http://deb.debian.org/debian trixie-backports main" >> /etc/apt/sources.list - apt update - apt-get -qy install -t trixie-backports --no-install-recommends git git-lfs sdkmanager openjdk-21-jdk-headless - - test -n "$ANDROID_HOME" || source /etc/profile.d/bsenv.sh - export GRADLE_USER_HOME=$PWD/.gradle - export ANDROID_COMPILE_SDK=`sed -n 's,.*compileSdk = "\([0-9][0-9]*\)".*,\1,p' gradle/libs.versions.toml` - sdkmanager --install "build-tools;${ANDROID_COMPILE_SDK}.0.0" "platform-tools" - "platforms;android-${ANDROID_COMPILE_SDK}" - "platforms;android-37.0" + "platforms;android-${ANDROID_COMPILE_SDK}.0" # index-v1.jar tests need SHA1 support still, TODO use apksig to validate JAR sigs - sed -i 's,SHA1 denyAfter 20[0-9][0-9],SHA1 denyAfter 2027,' /usr/lib/jvm/java-21-openjdk-amd64/conf/security/java.security @@ -219,29 +217,19 @@ app weblate merge conflict: - exit $EXITVALUE libs database schema: + <<: *test-template stage: lint - image: debian:trixie-backports rules: - <<: *always-on-these-changes - changes: - libs/database/**/* - variables: - JAVA_HOME: /usr/lib/jvm/java-21-openjdk-amd64 - before_script: - - apt-get update - - apt-get -qy --no-install-recommends install openjdk-21-jdk-headless git git-lfs sdkmanager - - export ANDROID_HOME=/opt/android-sdk - - export ANDROID_COMPILE_SDK=`sed -n 's,.*compileSdk = "\([0-9][0-9]*\)".*,\1,p' gradle/libs.versions.toml` - - sdkmanager --install - "build-tools;${ANDROID_COMPILE_SDK}.0.0" - "platform-tools" - "platforms;android-${ANDROID_COMPILE_SDK}" - - - git lfs install script: + - git lfs install # somehow required for git restore below + - git restore gradle.properties # gets touched by base and diff check below fail - ./gradlew :libs:database:kspDebugKotlin - git status - git --no-pager diff --exit-code + cache: [] # Run the tests in the emulator. Each step is broken out to run on # its own since the CI runner can have limited RAM, and the emulator diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9926b1589..1824ab4fe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -compileSdk = "36" +compileSdk = "37" kotlin = "2.3.21" androidGradlePlugin = "9.2.1" androidKspPlugin = "2.3.7" @@ -48,10 +48,8 @@ lifecycleViewmodelNav3 = "2.10.0" material3AdaptiveNav3 = "1.0.0-alpha03" material = "1.14.0" -#noinspection GradleDependency 1.5.0-alpha19 depends on SDK 37 -material3 = "1.5.0-alpha18" # should be possible to remove once 1.5.0 is stable and in bom -#noinspection GradleDependency 1.3.0-alpha10 depends on SDK 37 -material3AdaptiveNav = "1.3.0-alpha09" +material3 = "1.5.0-alpha19" # should be possible to remove once 1.5.0 is stable and in bom +material3AdaptiveNav = "1.3.0-beta01" zxingCore = "3.5.4" guardianprojectNetcipher = "2.2.0-alpha" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index bfa231173..934df7413 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -603,6 +603,11 @@ + + + + + @@ -618,6 +623,11 @@ + + + + + @@ -633,6 +643,11 @@ + + + + + @@ -648,6 +663,11 @@ + + + + + diff --git a/libs/database/src/test/resources/robolectric.properties b/libs/database/src/test/resources/robolectric.properties new file mode 100644 index 000000000..c562666dc --- /dev/null +++ b/libs/database/src/test/resources/robolectric.properties @@ -0,0 +1,2 @@ +# can be updated once roboelectric supports higher SDK versions +sdk=36 From 7b59f28fbfd2e03474d2348d5abad6ce127c9eda Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 13 May 2026 18:09:32 -0300 Subject: [PATCH 21/44] remove unnecessary steps from KVM CI test --- .gitlab-ci.yml | 8 +------- gradle.properties | 1 + 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 95beecbb0..82893a609 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,7 +20,6 @@ workflow: variables: JAVA_HOME: /usr/lib/jvm/java-21-openjdk-amd64 before_script: - - echo "org.gradle.caching=true" >> gradle.properties - test -e /etc/apt/sources.list.d/trixie-backports.list || echo "deb http://deb.debian.org/debian trixie-backports main" >> /etc/apt/sources.list - apt update @@ -224,8 +223,6 @@ libs database schema: - changes: - libs/database/**/* script: - - git lfs install # somehow required for git restore below - - git restore gradle.properties # gets touched by base and diff check below fail - ./gradlew :libs:database:kspDebugKotlin - git status - git --no-pager diff --exit-code @@ -247,7 +244,6 @@ libs database schema: # Cache hits the storage limits in kvm runners quickly cache: [] script: - - ./gradlew assembleFullDebug - export AVD_SDK=`echo $CI_JOB_NAME | awk '{print $2}'` - export AVD_TAG=`echo $CI_JOB_NAME | awk '{print $3}'` - export AVD_ARCH=`echo $CI_JOB_NAME | awk '{print $4}'` @@ -256,12 +252,10 @@ libs database schema: - $ANDROID_HOME/cmdline-tools/latest/bin/avdmanager --verbose delete avd --name "$NAME_AVD" - export AVD="$AVD_PACKAGE" - - sdkmanager --install "$AVD" + - $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install "$AVD" - echo no | $ANDROID_HOME/cmdline-tools/latest/bin/avdmanager --verbose create avd --name "$NAME_AVD" --package "$AVD" --device "pixel" - df -h - start-emulator.sh - - ./gradlew :app:installBasicDefaultDebug :legacy:installFullDebug - - adb shell am start -n org.fdroid.fdroid.debug/org.fdroid.fdroid.views.main.MainActivity - export FLAG="-Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.LargeTest,androidx.test.filters.FlakyTest" - ./gradlew $FLAG :app:connectedAndroidTest :libs:database:connectedCheck :libs:download:connectedAndroidTest :libs:index:connectedAndroidTest - export FLAG="-Pandroid.testInstrumentationRunnerArguments.annotation=androidx.test.filters.FlakyTest" diff --git a/gradle.properties b/gradle.properties index ff6ee5956..63e21be75 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,5 @@ org.gradle.jvmargs=-Xms2g -Xmx4g +org.gradle.caching=true android.enableJetifier=false android.useAndroidX=true android.experimental.enableScreenshotTest=true From 9ccc81b96dc475aa19037ecfaa8b0f73cb4667e5 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 15 May 2026 15:06:13 -0300 Subject: [PATCH 22/44] Add setting to only update repos when app is opened if enabled, this auto updates repos, when user opens app and if last check was more than 12h ago --- .../org/fdroid/repo/RepoUpdateManager.kt | 13 ++++++++++ .../org/fdroid/repo/RepoUpdateWorker.kt | 2 +- .../org/fdroid/settings/SettingsConstants.kt | 9 ++++--- .../kotlin/org/fdroid/ui/settings/Settings.kt | 25 +++++++++++++------ .../org/fdroid/updates/AppUpdateWorker.kt | 2 +- app/src/main/res/values/strings.xml | 2 ++ .../org/fdroid/repo/RepoUpdateManagerTest.kt | 23 +++++++++++++++++ 7 files changed, 62 insertions(+), 14 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/repo/RepoUpdateManager.kt b/app/src/main/kotlin/org/fdroid/repo/RepoUpdateManager.kt index d03434b15..6ed95fc5b 100644 --- a/app/src/main/kotlin/org/fdroid/repo/RepoUpdateManager.kt +++ b/app/src/main/kotlin/org/fdroid/repo/RepoUpdateManager.kt @@ -23,10 +23,12 @@ import org.fdroid.download.DownloaderFactory import org.fdroid.index.IndexUpdateResult import org.fdroid.index.RepoManager import org.fdroid.index.RepoUpdater +import org.fdroid.settings.SettingsConstants import org.fdroid.settings.SettingsManager import org.fdroid.updates.UpdatesManager private const val MIN_UPDATE_INTERVAL_MILLIS = 15_000 +private const val MAX_UPDATE_INTERVAL_MILLIS = 12 * 60 * 60 * 1000L // 12 hours @Singleton class RepoUpdateManager @@ -87,6 +89,17 @@ internal constructor( workInfo?.nextScheduleTimeMillis ?: Long.MAX_VALUE } + init { + log.info { "RepoUpdateManager initialized" } + if (settingsManager.repoUpdates == SettingsConstants.AutoUpdateValues.OnlyWhenOpenApp) { + val now = System.currentTimeMillis() + if (now - settingsManager.lastRepoUpdate > MAX_UPDATE_INTERVAL_MILLIS) { + log.info { "Last repo update was more than 12h ago, triggering update..." } + RepoUpdateWorker.updateNow(context) + } + } + } + /** * Updates all enabled repositories. * diff --git a/app/src/main/kotlin/org/fdroid/repo/RepoUpdateWorker.kt b/app/src/main/kotlin/org/fdroid/repo/RepoUpdateWorker.kt index 792dd4fa8..b56f1c757 100644 --- a/app/src/main/kotlin/org/fdroid/repo/RepoUpdateWorker.kt +++ b/app/src/main/kotlin/org/fdroid/repo/RepoUpdateWorker.kt @@ -74,7 +74,7 @@ constructor( @JvmStatic fun scheduleOrCancel(context: Context, autoUpdate: AutoUpdateValues) { val workManager = WorkManager.getInstance(context) - if (autoUpdate != AutoUpdateValues.Never) { + if (autoUpdate.workerEnabled) { Log.i(TAG, "scheduleOrCancel: enqueueUniquePeriodicWork") val networkType = if (autoUpdate == AutoUpdateValues.Always) { diff --git a/app/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt b/app/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt index 51e4a0ee0..a38fd0cfe 100644 --- a/app/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt +++ b/app/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt @@ -17,10 +17,11 @@ object SettingsConstants { const val PREF_KEY_DYNAMIC_COLORS = "dynamicColors" const val PREF_DEFAULT_DYNAMIC_COLORS = false - enum class AutoUpdateValues { - OnlyWifi, - Always, - Never, + enum class AutoUpdateValues(val workerEnabled: Boolean) { + OnlyWifi(true), + Always(true), + OnlyWhenOpenApp(false), + Never(false), } const val PREF_KEY_REPO_UPDATES = "repoAutoUpdates" diff --git a/app/src/main/kotlin/org/fdroid/ui/settings/Settings.kt b/app/src/main/kotlin/org/fdroid/ui/settings/Settings.kt index b0297444f..20ec99e3b 100644 --- a/app/src/main/kotlin/org/fdroid/ui/settings/Settings.kt +++ b/app/src/main/kotlin/org/fdroid/ui/settings/Settings.kt @@ -59,6 +59,7 @@ import org.fdroid.R import org.fdroid.settings.SettingsConstants.AutoUpdateValues import org.fdroid.settings.SettingsConstants.AutoUpdateValues.Always import org.fdroid.settings.SettingsConstants.AutoUpdateValues.Never +import org.fdroid.settings.SettingsConstants.AutoUpdateValues.OnlyWhenOpenApp import org.fdroid.settings.SettingsConstants.AutoUpdateValues.OnlyWifi import org.fdroid.settings.SettingsConstants.MirrorChooserValues import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_AUTO_UPDATES @@ -214,7 +215,8 @@ fun Settings(model: SettingsModel, onSaveLogcat: (Uri?) -> Unit, onBackClicked: ) }, summary = { strValue -> - if (strValue != Never.name) { + val value = strValue.toAutoUpdateValue() + if (value.workerEnabled) { val nextUpdate = model.nextRepoUpdateFlow.collectAsState(Long.MAX_VALUE).value val nextUpdateStr = if (nextUpdate == Long.MAX_VALUE) { @@ -228,17 +230,20 @@ fun Settings(model: SettingsModel, onSaveLogcat: (Uri?) -> Unit, onBackClicked: stringResource(R.string.auto_update_time, nextUpdate.asRelativeTimeString()) } val s = - if (strValue == OnlyWifi.name) { + if (value == OnlyWifi) { stringResource(R.string.pref_repo_updates_summary_only_wifi) - } else if (strValue == Always.name) { + } else if (value == Always) { stringResource(R.string.pref_repo_updates_summary_always) } else error("Unknown value: $strValue") Text(s + "\n" + nextUpdateStr) } else { - Text( - text = stringResource(R.string.pref_repo_updates_summary_never), - color = MaterialTheme.colorScheme.error, - ) + val s = + if (value == OnlyWhenOpenApp) { + stringResource(R.string.pref_repo_updates_summary_only_when_open_app) + } else { + stringResource(R.string.pref_repo_updates_summary_never) + } + Text(text = s, color = MaterialTheme.colorScheme.error) } }, values = AutoUpdateValues.entries.map { it.name }, @@ -247,6 +252,7 @@ fun Settings(model: SettingsModel, onSaveLogcat: (Uri?) -> Unit, onBackClicked: when (value.toAutoUpdateValue()) { OnlyWifi -> res.getString(R.string.pref_auto_updates_only_wifi) Always -> res.getString(R.string.pref_auto_updates_only_always) + OnlyWhenOpenApp -> res.getString(R.string.pref_auto_updates_only_only_when_open_app) Never -> res.getString(R.string.pref_auto_updates_only_never) } ) @@ -295,12 +301,15 @@ fun Settings(model: SettingsModel, onSaveLogcat: (Uri?) -> Unit, onBackClicked: } Text(s) }, - values = AutoUpdateValues.entries.map { it.name }, + // Exclude the OnlyWhenOpenApp option here + values = + AutoUpdateValues.entries.mapNotNull { if (it == OnlyWhenOpenApp) null else it.name }, valueToText = { value: String -> AnnotatedString( when (value.toAutoUpdateValue()) { OnlyWifi -> res.getString(R.string.pref_auto_updates_only_wifi) Always -> res.getString(R.string.pref_auto_updates_only_always) + OnlyWhenOpenApp -> res.getString(R.string.pref_auto_updates_only_only_when_open_app) Never -> res.getString(R.string.pref_auto_updates_only_never) } ) diff --git a/app/src/main/kotlin/org/fdroid/updates/AppUpdateWorker.kt b/app/src/main/kotlin/org/fdroid/updates/AppUpdateWorker.kt index ae34db7ae..42892c5f0 100644 --- a/app/src/main/kotlin/org/fdroid/updates/AppUpdateWorker.kt +++ b/app/src/main/kotlin/org/fdroid/updates/AppUpdateWorker.kt @@ -49,7 +49,7 @@ constructor( @JvmStatic fun scheduleOrCancel(context: Context, autoUpdate: AutoUpdateValues) { val workManager = WorkManager.getInstance(context) - if (autoUpdate != AutoUpdateValues.Never) { + if (autoUpdate.workerEnabled) { Log.i(TAG, "scheduleOrCancel: enqueueUniquePeriodicWork") val networkType = if (autoUpdate == AutoUpdateValues.Always) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0570b9a1e..d97d5379b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -182,6 +182,7 @@ Open system language settings Only on Wi-Fi Always (even on mobile data) + Only when opening the app Never Download and update apps daily when on Wi-Fi and the device isn\'t being used Download and update apps daily even on mobile data when the device isn\'t being used @@ -189,6 +190,7 @@ Check for updates Periodically fetch app updates from repositories only when on Wi-Fi Periodically fetch app updates from repositories even when on mobile data + Check for updates only when opened • Apps will become outdated Don\'t check for updates • Apps will become outdated Network Download mirror selection diff --git a/app/src/test/java/org/fdroid/repo/RepoUpdateManagerTest.kt b/app/src/test/java/org/fdroid/repo/RepoUpdateManagerTest.kt index b59469dbe..ca40f229b 100644 --- a/app/src/test/java/org/fdroid/repo/RepoUpdateManagerTest.kt +++ b/app/src/test/java/org/fdroid/repo/RepoUpdateManagerTest.kt @@ -29,6 +29,7 @@ import org.fdroid.index.IndexUpdateResult import org.fdroid.index.RepoManager import org.fdroid.index.RepoUpdater import org.fdroid.install.InstalledAppsCache +import org.fdroid.settings.SettingsConstants import org.fdroid.settings.SettingsManager import org.fdroid.updates.AppUpdateWorker import org.fdroid.updates.UpdatesManager @@ -57,6 +58,7 @@ internal class RepoUpdateManagerTest { every { db.getRepositoryDao() } returns repositoryDao every { context.getString(any(), any()) } returns "repo update" every { settingsManager.isFirstStart } returns false + every { settingsManager.repoUpdates } returns SettingsConstants.AutoUpdateValues.OnlyWifi every { installedAppsCache.installedApps } returns MutableStateFlow(emptyMap()) } @@ -324,6 +326,27 @@ internal class RepoUpdateManagerTest { } } + @Test + fun `triggers updateNow on init when OnlyWhenOpenApp and last update is stale`() { + every { settingsManager.repoUpdates } returns SettingsConstants.AutoUpdateValues.OnlyWhenOpenApp + every { settingsManager.lastRepoUpdate } returns 0L + every { RepoUpdateWorker.updateNow(any()) } just runs + + RepoUpdateManager( + context = context, + db = db, + repoManager = repoManager, + updatesManager = updatesManager, + settingsManager = settingsManager, + downloaderFactory = mockk(relaxed = true), + notificationManager = notificationManager, + compatibilityChecker = compatibilityChecker, + repoUpdater = repoUpdater, + ) + + verify(exactly = 1) { RepoUpdateWorker.updateNow(context) } + } + /** * Workaround for [verify] calls trying to take installedAppsCache into account for * [UpdatesManager.loadUpdates]. From 02fef43ec171a070a0067bcff87a5973b3fef938 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 15 May 2026 15:53:44 -0300 Subject: [PATCH 23/44] Target SDK 37 --- app/build.gradle.kts | 2 +- app/src/full/AndroidManifest.xml | 1 + .../fdroid/nearby/BluetoothManager.java | 2 +- .../fdroid/nearby/SwapWorkflowActivity.java | 44 ++++++++++++++++--- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4a0f0b1f2..2c76716ec 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,7 +17,7 @@ android { defaultConfig { applicationId = "org.fdroid" minSdk = 24 - targetSdk = 36 + targetSdk = 37 versionCode = 2000009 versionName = "2.0-alpha9" diff --git a/app/src/full/AndroidManifest.xml b/app/src/full/AndroidManifest.xml index a6b88dcc3..e9521c85d 100644 --- a/app/src/full/AndroidManifest.xml +++ b/app/src/full/AndroidManifest.xml @@ -36,6 +36,7 @@ + requestLocalAccessLauncher = + registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { + if (isGranted) sendFDroidBluetooth(); + }); + public static void requestSwap(Context context, String repo) { requestSwap(context, Uri.parse(repo)); } @@ -1092,18 +1100,25 @@ public class SwapWorkflowActivity extends AppCompatActivity { wifiSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { Context context = getApplicationContext(); - if (isChecked) { + boolean granted = SDK_INT < 37 || + ContextCompat.checkSelfPermission(context, ACCESS_LOCAL_NETWORK) == PERMISSION_GRANTED; + if (isChecked && !granted) { + requestLocalAccessLauncher.launch(ACCESS_LOCAL_NETWORK); + Toast.makeText(this, R.string.swap_bluetooth_permissions, Toast.LENGTH_LONG).show(); + } + boolean activate = isChecked && granted; + if (activate) { if (wifiApControl != null && wifiApControl.isEnabled()) { setupWifiAP(); } else { - if (Build.VERSION.SDK_INT <= 28) { + if (SDK_INT <= 28) { wifiManager.setWifiEnabled(true); } } BonjourManager.start(context); } - BonjourManager.setVisible(context, isChecked); - SwapService.putWifiVisibleUserPreference(isChecked); + BonjourManager.setVisible(context, activate); + SwapService.putWifiVisibleUserPreference(activate); }); scanQrButton.setOnClickListener(v -> inflateSwapView(R.layout.swap_wifi_qr)); @@ -1143,7 +1158,12 @@ public class SwapWorkflowActivity extends AppCompatActivity { break; case BonjourManager.STATUS_STARTED: textWifiVisible.setText(R.string.swap_not_visible_wifi); - peopleNearbyText.setText(R.string.swap_scanning_for_peers); + if (SDK_INT < 37 || + ContextCompat.checkSelfPermission(context, ACCESS_LOCAL_NETWORK) == PERMISSION_GRANTED) { + peopleNearbyText.setText(R.string.swap_scanning_for_peers); + } else { + peopleNearbyText.setText(R.string.swap_bluetooth_permissions); + } peopleNearbyText.setVisibility(View.VISIBLE); peopleNearbyProgress.setVisibility(View.VISIBLE); break; @@ -1152,7 +1172,12 @@ public class SwapWorkflowActivity extends AppCompatActivity { break; case BonjourManager.STATUS_NOT_VISIBLE: textWifiVisible.setText(R.string.swap_not_visible_wifi); - peopleNearbyText.setText(R.string.swap_scanning_for_peers); + if (SDK_INT < 37 || + ContextCompat.checkSelfPermission(context, ACCESS_LOCAL_NETWORK) == PERMISSION_GRANTED) { + peopleNearbyText.setText(R.string.swap_scanning_for_peers); + } else { + peopleNearbyText.setText(R.string.swap_bluetooth_permissions); + } peopleNearbyText.setVisibility(View.VISIBLE); peopleNearbyProgress.setVisibility(View.VISIBLE); break; @@ -1162,7 +1187,12 @@ public class SwapWorkflowActivity extends AppCompatActivity { } else { textWifiVisible.setText(R.string.swap_visible_wifi); } - peopleNearbyText.setText(R.string.swap_scanning_for_peers); + if (SDK_INT < 37 || + ContextCompat.checkSelfPermission(context, ACCESS_LOCAL_NETWORK) == PERMISSION_GRANTED) { + peopleNearbyText.setText(R.string.swap_scanning_for_peers); + } else { + peopleNearbyText.setText(R.string.swap_bluetooth_permissions); + } peopleNearbyText.setVisibility(View.VISIBLE); peopleNearbyProgress.setVisibility(View.VISIBLE); break; From 7d0e314a196e54a200f2be7a4b8183ae65d902b9 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Sat, 16 May 2026 16:03:51 -0300 Subject: [PATCH 24/44] Detect when apps get archived, so they can be listed as uninstalled Otherwise, they would still be shown as installed and appear in My Apps, but can't be opened, nor installed. --- .../kotlin/org/fdroid/install/InstalledAppsCache.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/install/InstalledAppsCache.kt b/app/src/main/kotlin/org/fdroid/install/InstalledAppsCache.kt index 61fb9474d..d48fbb33c 100644 --- a/app/src/main/kotlin/org/fdroid/install/InstalledAppsCache.kt +++ b/app/src/main/kotlin/org/fdroid/install/InstalledAppsCache.kt @@ -3,11 +3,13 @@ package org.fdroid.install import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.Intent.EXTRA_ARCHIVAL import android.content.Intent.EXTRA_REPLACING import android.content.IntentFilter import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.pm.PackageManager.GET_SIGNATURES +import android.os.Build.VERSION.SDK_INT import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED import androidx.core.content.ContextCompat.registerReceiver import dagger.hilt.android.qualifiers.ApplicationContext @@ -88,10 +90,14 @@ constructor( private fun onPackageRemoved(intent: Intent) { val replacing = intent.getBooleanExtra(EXTRA_REPLACING, false) - log.info { "onPackageRemoved($intent) ${intent.data} replacing: $replacing" } + val archival = if (SDK_INT >= 35) intent.getBooleanExtra(EXTRA_ARCHIVAL, false) else false + log.info { + "onPackageRemoved($intent) ${intent.data} replacing: $replacing ${intent.extras?.keySet()?.toList()}" + } val packageName = intent.data?.schemeSpecificPart ?: error("No package name in ACTION_PACKAGE_REMOVED") - if (!replacing) + if (!replacing || archival) { _installedApps.update { apps -> apps.toMutableMap().apply { remove(packageName) } } + } } } From fc7727cf85616bfbd28636bd4b3d767ce113e5ec Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 18 May 2026 09:59:20 -0300 Subject: [PATCH 25/44] Add a button to clear all installing apps, if they are not progressing anymore --- .../org/fdroid/install/AppInstallManager.kt | 17 +++++++++++ .../kotlin/org/fdroid/ui/apps/MyAppsInfo.kt | 6 +++- .../kotlin/org/fdroid/ui/apps/MyAppsList.kt | 28 +++++++++++++++---- .../org/fdroid/ui/apps/MyAppsViewModel.kt | 6 ++++ .../org/fdroid/ui/utils/PreviewUtils.kt | 2 ++ app/src/main/res/values/strings.xml | 1 + .../kotlin/org/fdroid/ui/apps/MyAppsTest.kt | 8 +++++- .../MyAppsInstalledAndIssues_2ed8e27d_0.png | 4 +-- 8 files changed, 62 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/install/AppInstallManager.kt b/app/src/main/kotlin/org/fdroid/install/AppInstallManager.kt index 96aae5f43..120cb7473 100644 --- a/app/src/main/kotlin/org/fdroid/install/AppInstallManager.kt +++ b/app/src/main/kotlin/org/fdroid/install/AppInstallManager.kt @@ -488,6 +488,23 @@ constructor( return result } + @UiThread + fun clearInstallingApps() { + apps.update { oldApps -> + oldApps.toMutableMap().apply { + val iterator = entries.iterator() + while (iterator.hasNext()) { + val app = iterator.next() + if (!app.value.showProgress) { + val packageName = app.key + jobs.remove(packageName)?.cancel() + iterator.remove() + } + } + } + } + } + @UiThread fun cleanUp(packageName: String) { val state = apps.value[packageName] ?: return diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt index b9b9759f3..9e5fddefd 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt @@ -21,7 +21,9 @@ data class MyAppsModel( val networkState: NetworkState, val isSearching: Boolean = false, val appUpdatesBytes: Long? = null, -) +) { + val showClearInstallingAppsButton: Boolean = installingApps.all { !it.installState.showProgress } +} interface MyAppsActions { fun updateAll() @@ -32,6 +34,8 @@ interface MyAppsActions { fun confirmAppInstall(packageName: String, state: InstallConfirmationState) + fun clearInstallingApps() + fun ignoreAppIssue(item: AppWithIssueItem) fun onUpdatesHintSeen() diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt index b4c259650..41459cf30 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.fdroid.R import org.fdroid.database.NotAvailable +import org.fdroid.install.InstallState import org.fdroid.ui.FDroidContent import org.fdroid.ui.utils.MeteredConnectionDialog import org.fdroid.ui.utils.OfflineBar @@ -139,11 +140,21 @@ fun MyAppsList( // Apps currently installing header if (installingApps.isNotEmpty()) { item(key = "B", contentType = "header") { - Text( - text = stringResource(R.string.notification_title_summary_installing), - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(16.dp), - ) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.notification_title_summary_installing), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(16.dp).weight(1f), + ) + if (myAppsInfo.model.showClearInstallingAppsButton) { + TextButton( + onClick = myAppsInfo.actions::clearInstallingApps, + modifier = Modifier.padding(end = 16.dp), + ) { + Text(stringResource(R.string.clear_all)) + } + } + } } // List of currently installing apps items(items = installingApps, key = { it.packageName }, contentType = { "B" }) { app -> @@ -288,7 +299,12 @@ private fun Preview() { getMyAppsInfo( myAppsModel.copy( appUpdates = emptyList(), - installingApps = emptyList(), + installingApps = + listOf( + myAppsModel.installingApps[0].copy( + installState = InstallState.Installed("Installed App", "0.5.2", null, 1337L, null) + ) + ), appsWithIssue = emptyList(), ) ), diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt index 53509b0df..4ce3ced17 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt @@ -3,6 +3,7 @@ package org.fdroid.ui.apps import android.app.Application import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import androidx.annotation.UiThread import androidx.core.app.ShareCompat import androidx.core.os.LocaleListCompat import androidx.lifecycle.AndroidViewModel @@ -135,6 +136,11 @@ constructor( } } + @UiThread + override fun clearInstallingApps() { + appInstallManager.clearInstallingApps() + } + override fun ignoreAppIssue(item: AppWithIssueItem) { settingsManager.ignoreAppIssue(item.packageName, item.installedVersionCode) updatesManager.loadUpdates() diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt index a1671baea..12623b5e0 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt @@ -415,6 +415,8 @@ fun getMyAppsInfo(model: MyAppsModel): MyAppsInfo = override fun confirmAppInstall(packageName: String, state: InstallConfirmationState) {} + override fun clearInstallingApps() {} + override fun ignoreAppIssue(item: AppWithIssueItem) {} override fun onUpdatesHintSeen() {} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d97d5379b..889dcd6d5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -58,6 +58,7 @@ Use filters to only show apps from specific categories or repositories. You can also change the sort order. Got it Clear + Clear all Hide Calendars, tasks, clocks & notes diff --git a/app/src/screenshotTest/kotlin/org/fdroid/ui/apps/MyAppsTest.kt b/app/src/screenshotTest/kotlin/org/fdroid/ui/apps/MyAppsTest.kt index 42f08c6ec..13d739b26 100644 --- a/app/src/screenshotTest/kotlin/org/fdroid/ui/apps/MyAppsTest.kt +++ b/app/src/screenshotTest/kotlin/org/fdroid/ui/apps/MyAppsTest.kt @@ -10,6 +10,7 @@ import org.fdroid.install.InstallState import org.fdroid.ui.ScreenshotTest import org.fdroid.ui.utils.getMyAppsInfo import org.fdroid.ui.utils.getPreviewVersion +import org.fdroid.ui.utils.myAppsModel @Composable @PreviewTest @@ -69,7 +70,12 @@ fun MyAppsInstalledAndIssues() { val model = MyAppsModel( appUpdates = emptyList(), - installingApps = emptyList(), + installingApps = + listOf( + myAppsModel.installingApps[0].copy( + installState = InstallState.Installed("Installed App", "0.5.2", null, 1337L, null) + ) + ), appsWithIssue = getAppIssues(), installedApps = getInstalledApps(), showUpdatesHint = false, diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png index 95c8a682c..f09001233 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e8e34dc95d9b2b264fafa4bcf601936dfcb707d11abe3cc05000ae5d0bd0d14 -size 114468 +oid sha256:fbc5717f991caad84edea313031b33dfced41af8ed779112d44ec5bbf5acd8cc +size 130690 From 31ad8a287acf435246a21d9af44d2686818f7226 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 18 May 2026 16:42:10 -0300 Subject: [PATCH 26/44] Rename "Installing" apps in "Recently installed" when all installs have finished --- app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt | 9 +++++++-- app/src/main/res/values/strings.xml | 1 + .../MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt index 41459cf30..bef22f6e7 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt @@ -142,7 +142,12 @@ fun MyAppsList( item(key = "B", contentType = "header") { Row(verticalAlignment = Alignment.CenterVertically) { Text( - text = stringResource(R.string.notification_title_summary_installing), + text = + if (myAppsInfo.model.showClearInstallingAppsButton) { + stringResource(R.string.my_apps_header_recently_installed_apps) + } else { + stringResource(R.string.notification_title_summary_installing) + }, style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(16.dp).weight(1f), ) @@ -168,7 +173,7 @@ fun MyAppsList( onClick = { onAppItemClick(app.packageName) }, ) } - val modifier = Modifier.Companion.animateItem().then(interactionModifier) + val modifier = Modifier.animateItem().then(interactionModifier) InstallingAppRow(app, isSelected, modifier) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 889dcd6d5..6f9956f0f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -22,6 +22,7 @@ Only apps available in %1$s\'s repositories are shown here + Recently installed Apps with issues Hide issue The issue with this version of \"%1$s\" will get ignored diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png index f09001233..c2e6e2a33 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbc5717f991caad84edea313031b33dfced41af8ed779112d44ec5bbf5acd8cc -size 130690 +oid sha256:789e8b83fa632ca8110a67c9424759d51c9e1903348483fbdf7dbfa07eeed839 +size 132588 From 652c8545337dd2b00d57a0738a837cbe42b56b09 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 20 May 2026 09:15:03 -0300 Subject: [PATCH 27/44] Update targetSdk for 37 that can get auto updated and installed --- app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt | 1 + .../src/androidMain/kotlin/org/fdroid/CompatibilityChecker.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt b/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt index 1495e8a9e..9b252af1a 100644 --- a/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt +++ b/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt @@ -70,6 +70,7 @@ constructor( // https://developer.android.com/reference/android/content/pm/PackageInstaller.SessionParams#setRequireUserAction(int) // https://cs.android.com/android/platform/superproject/+/android-16.0.0_r2:frameworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.java;l=329;drc=73caa0299d9196ddeefe4f659f557fb880f6536d // current code requires targetSdk 34 on SDK 36+ + // SDK 37 beta 4 also auto updates targetSdk 34 apps return SDK_INT >= 36 && targetSdk >= 34 } } diff --git a/libs/index/src/androidMain/kotlin/org/fdroid/CompatibilityChecker.kt b/libs/index/src/androidMain/kotlin/org/fdroid/CompatibilityChecker.kt index 34ea304a2..31a847be0 100644 --- a/libs/index/src/androidMain/kotlin/org/fdroid/CompatibilityChecker.kt +++ b/libs/index/src/androidMain/kotlin/org/fdroid/CompatibilityChecker.kt @@ -62,6 +62,7 @@ public object CompatibilityCheckerUtils { 34 -> 23 // Android 6.0, M 35 -> 24 // Android 7.0, N 36 -> 24 // Android 7.0, N (didn't change) + 37 -> 24 // Android 7.0, N (didn't change in SDK 37 beta 4) else -> 1 // Android 1.0, BASE } } From 4b70298ea2a3316595c9da2bd66e87f38a850580 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 20 May 2026 15:08:49 -0300 Subject: [PATCH 28/44] Stabilize apps sort order in MyApps because many apps (currently) have the same lastUpdated timestamp which may lead to random re-sorting in the UI --- .../kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt index 7301a0794..76123982d 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt @@ -113,11 +113,22 @@ fun MyAppsPresenter( private fun List.sort(sortOrder: AppListSortOrder): List { val collator = Collator.getInstance(Locale.getDefault()) return when (sortOrder) { - AppListSortOrder.NAME -> + AppListSortOrder.NAME -> { sortedWith { a1, a2 -> // storing collator.getCollationKey() and using that could be an optimization collator.compare(a1.name, a2.name) } - AppListSortOrder.LAST_UPDATED -> sortedByDescending { it.lastUpdated } + } + AppListSortOrder.LAST_UPDATED -> { + sortedWith { a1, a2 -> + val lastAddedCompare = a1.lastUpdated.compareTo(a2.lastUpdated) + if (lastAddedCompare == 0) { + // fall-back to name if last updated is the same, to ensure stable sorting + collator.compare(a1.name, a2.name) + } else { + lastAddedCompare + } + } + } } } From 34476aeb64bdf059cc17bd4490823880c3b9fbf5 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 21 May 2026 08:14:58 -0300 Subject: [PATCH 29/44] Remove unused strings --- app/src/main/res/values-ar/strings.xml | 1 - app/src/main/res/values-be/strings.xml | 1 - app/src/main/res/values-bg/strings.xml | 1 - app/src/main/res/values-ca/strings.xml | 1 - app/src/main/res/values-cs/strings.xml | 1 - app/src/main/res/values-da/strings.xml | 1 - app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-el/strings.xml | 1 - app/src/main/res/values-en-rGB/strings.xml | 1 - app/src/main/res/values-eo/strings.xml | 1 - app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-et/strings.xml | 1 - app/src/main/res/values-eu/strings.xml | 1 - app/src/main/res/values-fa/strings.xml | 1 - app/src/main/res/values-fi/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-ga/strings.xml | 1 - app/src/main/res/values-he/strings.xml | 1 - app/src/main/res/values-hr/strings.xml | 1 - app/src/main/res/values-hu/strings.xml | 1 - app/src/main/res/values-id/strings.xml | 1 - app/src/main/res/values-is/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-ja/strings.xml | 1 - app/src/main/res/values-kab/strings.xml | 1 - app/src/main/res/values-ko/strings.xml | 1 - app/src/main/res/values-lt/strings.xml | 1 - app/src/main/res/values-lv/strings.xml | 1 - app/src/main/res/values-mk/strings.xml | 1 - app/src/main/res/values-nb/strings.xml | 1 - app/src/main/res/values-nl/strings.xml | 1 - app/src/main/res/values-pl/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 1 - app/src/main/res/values-pt-rPT/strings.xml | 1 - app/src/main/res/values-pt/strings.xml | 1 - app/src/main/res/values-ro/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-sk/strings.xml | 1 - app/src/main/res/values-sl/strings.xml | 1 - app/src/main/res/values-sq/strings.xml | 1 - app/src/main/res/values-sr/strings.xml | 1 - app/src/main/res/values-sv/strings.xml | 1 - app/src/main/res/values-sw/strings.xml | 1 - app/src/main/res/values-tr/strings.xml | 1 - app/src/main/res/values-uk/strings.xml | 1 - app/src/main/res/values-vi/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 1 - app/src/main/res/values-zh-rTW/strings.xml | 1 - 48 files changed, 48 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 81dc956c2..c1b53c937 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -695,7 +695,6 @@ الملفات والتخزين اهتمامات متنوّع - بواسطة %1$s آخر تحديث: %1$s آخر تحديث: %1$s (%2$s) يجهز التثبيت… diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index adcd2dc6e..6b1e76da5 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -672,7 +672,6 @@ Файлы і Сховішча Інтарэсы Розныя - Ад %1$s Абноўлена: %1$s Абноўлена: %1$s (%2$s) Падрыхтоўка ўсталёўкі… diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 4d93f982f..39af4dd81 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -639,7 +639,6 @@ Файлове и хранилища Интереси Разни - От %1$s Последно обновяване: %1$s Последно обновяване: %1$s (%2$s) Подготовка за инсталиране… diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 2e9b156bd..6ca6eb089 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -650,7 +650,6 @@ Arxius i emmagatzematge Interessos Miscel·lània - Per %1$s Últimes actualitzacions: %1$s Últimes actualitzacions: %1$s (%2$s) Preparant la instal·lació… diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 32d773cde..076735b60 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -653,7 +653,6 @@ Soubory a úložiště Zájmy Různé - Od %1$s Naposledy aktualizováno: %1$s Naposledy aktualizováno: %1$s (%2$s) Příprava instalace… diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 04844a1da..ee2b90bee 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -624,7 +624,6 @@ Nye apps Eksportér appliste Værktøjer - Af %1$s Kommunikation Netværk Donér diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8d90b39db..ab4b44dc4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -649,7 +649,6 @@ Dateien und Speicher Interessen Verschiedenes - Von %1$s Zuletzt aktualisiert: %1$s Zuletzt aktualisiert: %1$s (%2$s) Installation wird vorbereitet … diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 502fac6b5..b67451b25 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -638,7 +638,6 @@ Αρχεία & Αποθηκευτικός Χώρος Ενδιαφέροντα Διάφορα - Από %1$s Τελευταία ενημέρωση: %1$s Τελευταία ενημέρωση: %1$s (%2$s) Προετοιμασία εγκατάστασης… diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index f1a1aee44..9333280bc 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -658,7 +658,6 @@ Files & Storage Interests Miscellaneous - By %1$s Last updated: %1$s Last updated: %1$s (%2$s) Preparing installation… diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index a516e9b08..b454d20e5 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -638,7 +638,6 @@ Dosieroj kaj konservado Interesiĝoj Diversaĵoj - Fare de %1$s Lasta ĝisdatigo: %1$s Lasta ĝisdatigo: %1$s (%2$s) Preparado de instalado… diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 5c681e591..5842b3886 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -676,7 +676,6 @@ Archivos y Almacenamiento Intereses Misceláneo - Por %1$s Últimamente actualizado: %1$s Últimamente actualizado: %1$s (%2$s) Preparando instalación… diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 968f5e8f0..baa823171 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -652,7 +652,6 @@ Failid ja andmeruum Huvid ja hobid Varia - Arendajalt %1$s Viimati uuendatud: %1$s Viimati uuendatud: %1$s (%2$s) Valmistun paigaldama… diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index d9f0598c9..f8b27c723 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -664,7 +664,6 @@ Fitxategiak eta biltegiratzea Interesak Denetarik - %1$s-ek egina Azken eguneraketa: %1$s Azken eguneraketa: %1$s (%2$s) Instalazioa prestatzen… diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 632dadd86..334800435 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -640,7 +640,6 @@ شبکه پرونده‌ها و ذخیره‌سازی متفرّقه - به دست %1$s آخرین به‌روز رسانی: %1$s آخرین به‌روز رسانی: %1$s‏ (%2$s) آماده سازی نصب… diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index dacdc3f3f..e82b2e67b 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -625,7 +625,6 @@ Verkko Tiedostot ja tallennus Sekalaiset - Tekijältä %1$s Viimeksi päivitetty: %1$s Viimeksi päivitetty: %1$s (%2$s) Valmistellaan asennusta… diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d8db1b832..dafb789d9 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -666,7 +666,6 @@ Fichiers & Stockage Intérêts Divers - De %1$s Dernière mise à jour : %1$s Dernière mise à jour : %1$s (%2$s) Préparation de l\'installation… diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index 95b0e2e4c..1c6ea7b19 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -695,7 +695,6 @@ Comhaid & Stóráil Leasanna Ilghnéitheach - Faoi %1$s Nuashonraithe go deireanach: %1$s Nuashonraithe go deireanach: %1$s (%2$s) Ag ullmhú suiteála… diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index b1397aa75..d151e645c 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -684,7 +684,6 @@ קבצים ואחסון תחומי עניין שונות - על ידי %1$s עדכון אחרון: %1$s עדכון אחרון: %1$s (%2$s) בהכנות להתקנה… diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 485a12308..bfac81a1f 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -658,7 +658,6 @@ Datoteke i spremišta Interesi Razno - Autor: %1$s Zadnje aktualiziranje: %1$s Zadnje aktualiziranje: %1$s (%2$s) Pripremanje instalacije … diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index d96d71f17..091e48ed6 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -641,7 +641,6 @@ Fájlok és tárolók Érdeklődési kör Egyéb - %1$s -től Utolsó frissítés: %1$s Utolsó frissítés: %1$s (%2$s) Telepítés előkészítése… diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 49314f21a..a0d15f194 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -644,7 +644,6 @@ Fail & Penyimpanan Minat Lain-Lain - Oleh %1$s Terakhir diperbarui: %1$s Terakhir diperbarui: %1$s (%2$s) Mempersiapkan instalasi… diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 6de8124bf..f52e8b786 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -640,7 +640,6 @@ Skrár og geymslurými Áhugamál Ýmislegt - Frá %1$s Síðast uppfært: %1$s Síðast uppfært: %1$s (%2$s) Undirbý uppsetningu… diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 03e60a296..4d248521e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -664,7 +664,6 @@ Archiviazione Interessi Miscellanea - Da %1$s Ultimo aggiornamento: %1$s Ultimo aggiornamento: %1$s (%2$s) Preparando l\'installazione… diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 7a8d42585..ad919d67c 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -645,7 +645,6 @@ ファイル&ストレージ 趣味 その他 - %1$s による 最終更新: %1$s 最終更新: %1$s (%2$s) インストールの準備中… diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index 2123d1285..a9cc6c48d 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -419,7 +419,6 @@ Ameskar %1$s Isnasen imaynuten Akk isnasen - Sɣur %1$s Aheyyi n usbeddi… D acu i yellan d amaynut Nɣel aseɣwen diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index ae409ab26..73785d90d 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -630,7 +630,6 @@ 파일 & 저장소 관심분야 기타 - 개발자: %1$s 마지막 업데이트: %1$s 마지막 업데이트: %1$s (%2$s) 설치 준비중… diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index cae87bfcb..31a8a38eb 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -611,7 +611,6 @@ Failai ir saugykla Pomėgiai Kiti - Sukurta %1$s Paskutinį kartą atnaujinta: %1$s Paskutinį kartą atnaujinta: %1$s (%2$s) Įdiegimas ruošiamas… diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index be1e2af05..4fa3bc4c5 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -649,7 +649,6 @@ Tīkls Datnes un krātuve Dažādi - No %1$s Pēdējoreiz atjaunināta: %1$s Pēdējoreiz atjaunināta: %1$s (%2$s) Sagatavo uzstādīšanu… diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index c600e7853..da8554091 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -307,7 +307,6 @@ Датотеки и меморија Интереси Останато - Направено од %1$s Последно надоградено: %1$s Последно надоградено: %1$s­%2$s Подготовка на инсталација… diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 6f0bb1765..9df6f8146 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -655,7 +655,6 @@ Filer & Lagring Interesser Diverse - Fra %1$s Sist oppdatert: %1$s Sist oppdatert: %1$s (%2$s) Forbereder installasjon… diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index c1487c4df..6bae2c23f 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -646,7 +646,6 @@ Bestanden & Opslag Interesses Diversen - Door %1$s Laatst geüpdatet: %1$s Laatst geüpdatet: %1$s (%2$s) Installatie voorbereiden… diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index b545b118c..3be6ca53c 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -684,7 +684,6 @@ Pliki i pamięć Zainteresowania Różne - Przez %1$s Ostatnia aktualizacja: %1$s Ostatnia aktualizacja: %1$s (%2$s) Przygotowywanie instalacji… diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index aa0ad2545..aa279fc64 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -654,7 +654,6 @@ Dispositivo Arquivos & Armazenamento Interesses - Por %1$s Última atualização: %1$s (%2$s) Preparando instalação… O que há de novo diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index abf9a3d45..0307d3821 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -651,7 +651,6 @@ Ficheiros & armazenamento Interesses Diversos - Por %1$s Última atualização: %1$s Última atualização: %1$s (%2$s) A preparar instalação… diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 5d37081a8..96f7563bb 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -665,7 +665,6 @@ Ficheiros & armazenamento Interesses Diversos - Por %1$s Última atualização: %1$s Última atualização: %1$s (%2$s) A preparar instalação… diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index b7d107e4f..e862b6f61 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -661,7 +661,6 @@ Fișiere și stocare Pasiuni Diverse - De către %1$s Ultima actualizare: %1$s Ultima actualizare: %1$s (%2$s) Pregătire instalare… diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 9d176e35e..6b6bf1641 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -665,7 +665,6 @@ Файлы и хранилище Интересы Разное - От %1$s Последнее обновление: %1$s Последнее обновление: %1$s (%2$s) Подготовка к установке… diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 2121d0ccb..d4202c2b4 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -589,7 +589,6 @@ Súbory a uložisko Záujmy Rôzne - Od %1$s Naposledy aktualizované: %1$s Naposledy aktualizované: %1$s (%2$s) Príprava inštalácie… diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 94ba5d785..6d2339d8e 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -563,7 +563,6 @@ Datoteke in shranjevanje Interesi Razno - Od %1$s Zadnja posodobitev: %1$s Zadnja posodobitev: %1$s (%2$s) Priprava namestitve … diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index 9cab8ead9..67d38be5a 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -637,7 +637,6 @@ Kartela & Depozitim Interesa Të ndryshme - Nga %1$s Përditësuar së fundi më: %1$s Ky aplikacion përmban anti-veçori Kontakt zhvilluesi diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 872c9981a..f1c8793d7 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -637,7 +637,6 @@ Датотеке и складиште Интересовања Разно - Од %1$s Последње ажурирање: %1$s Последње ажурирано: %1$s (%2$s) Припрема се инсталација… diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 89fb7e8aa..3dcd8e57d 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -655,7 +655,6 @@ Filer & lagring Intressen Diverse - Av %1$s Senast uppdaterad: %1$s Senast uppdaterad: %1$s (%2$s) Förbereder installationen… diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml index 630f4d159..85129446b 100644 --- a/app/src/main/res/values-sw/strings.xml +++ b/app/src/main/res/values-sw/strings.xml @@ -624,7 +624,6 @@ Iliyopakuliwa zaidi Programu na %s Hakuna programu zilizopatikana\n\nJaribu kutumia maneno machache ya utafutaji au uongeze hazina zaidi - Na %1$s Ilisasishwa mwisho: %1$s (%2$s) Inatayarisha usakinishaji… Programu hii ina vipengele vya kupinga diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 1628fd0bd..09516cddf 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -636,7 +636,6 @@ Eğlence & Medya İletişim İlgi Alanları - %1$s tarafından F-Nightly F-Nightly Temel Uygulama depoları getiriliyor… diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index ff19c6923..f2901ec9c 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -666,7 +666,6 @@ Файли та зберігання Інтереси Різне - Від %1$s Останні оновлено: %1$s Останні оновлено: %1$s (%2$s) Підготовка до встановлення… diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 5aba2d9d1..729343ace 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -613,7 +613,6 @@ Tệp & Lưu trữ Sở thích Linh tinh - Bởi %1$s Lần cuối cập nhật: %1$s Lần cuối cập nhật: %1$s (%2$s) Chuẩn bị cài đặt… diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 6c893b62f..f9ed752cd 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -638,7 +638,6 @@ 文件和存储 兴趣 杂项 - 由 %1$s 开发 上次更新:%1$s 上次更新:%1$s (%2$s) 正在准备安装… diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 8361a65a0..f940186ca 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -636,7 +636,6 @@ 最後更新: %1$s (%2$s) 正在準備安裝… 有什麼新事物 - 由 %1$s 提供 課金 此應用程式包含負面功能 聯絡開發者 From b8c702869f4a8c13ba13293122bdd108729d9737 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 21 May 2026 08:15:32 -0300 Subject: [PATCH 30/44] Update most downloaded apps and default repos --- app/src/main/assets/default_repos.json | 5 ++++- app/src/main/assets/most_downloaded_apps.json | 12 ++++++------ legacy/src/main/res/values/default_repos.xml | 3 +++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/src/main/assets/default_repos.json b/app/src/main/assets/default_repos.json index c3dceba13..d981689d0 100644 --- a/app/src/main/assets/default_repos.json +++ b/app/src/main/assets/default_repos.json @@ -56,7 +56,10 @@ "https://opencolo.mm.fcix.net/fdroid/archive", "https://plug-mirror.rcac.purdue.edu/fdroid/archive", "https://mirror.init7.net/fdroid/archive", - "https://mirror.freedif.org/fdroid/archive" + "https://mirror.freedif.org/fdroid/archive", + "https://mirrors.hostico.ro/fdroid/archive", + "https://ftp.lug.ro/fdroid/archive", + "https://mirrors.chroot.ro/fdroid/archive" ], "description": "The archive repository of the F-Droid client. This contains older versions of applications from the main repository.", "certificate": "3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef", diff --git a/app/src/main/assets/most_downloaded_apps.json b/app/src/main/assets/most_downloaded_apps.json index f4fe556a9..2fa216da5 100644 --- a/app/src/main/assets/most_downloaded_apps.json +++ b/app/src/main/assets/most_downloaded_apps.json @@ -1,15 +1,14 @@ [ + "app.comaps.fdroid", "app.organicmaps", "at.bitfire.davdroid", "ch.protonvpn.android", - "com.artifex.mupdf.mini.app", "com.artifex.mupdf.viewer.app", "com.aurora.store", "com.duckduckgo.mobile.android", "com.foobnix.pro.pdf.reader", "com.fsck.k9", "com.github.andreyasadchy.xtra", - "com.github.libretube", "com.inspiredandroid.linuxcommandbibliotheca", "com.junkfood.seal", "com.kunzisoft.keepass.libre", @@ -17,31 +16,32 @@ "com.machiav3lli.fdroid", "com.maxrave.simpmusic", "com.nextcloud.client", - "com.nononsenseapps.feeder", "com.termux", "com.termux.api", "com.unciv.app", "de.danoeh.antennapod", + "de.markusfisch.android.binaryeye", "de.schildbach.oeffi", "de.tutao.tutanota", - "deckers.thibault.aves.libre", "dev.imranr.obtainium.fdroid", "eu.faircode.email", + "eu.siacs.conversations", "helium314.keyboard", "InfinityLoop1309.NewPipeEnhanced", "io.element.android.x", "jp.nonbili.noutube", - "net.cozic.joplin", "net.osmand.plus", "net.thunderbird.android", + "net.typeblog.shelter", + "org.adaway", "org.breezyweather", "org.documentfoundation.libreoffice", + "org.fairscan.app", "org.fdroid.fdroid", "org.fossify.calendar", "org.fossify.filemanager", "org.fossify.gallery", "org.fossify.messages", - "org.fossify.phone", "org.kde.kdeconnect_tp", "org.mozilla.fennec_fdroid", "org.schabi.newpipe", diff --git a/legacy/src/main/res/values/default_repos.xml b/legacy/src/main/res/values/default_repos.xml index f626a5246..fbfbc7d86 100644 --- a/legacy/src/main/res/values/default_repos.xml +++ b/legacy/src/main/res/values/default_repos.xml @@ -73,6 +73,9 @@ https://plug-mirror.rcac.purdue.edu/fdroid/archive https://mirror.init7.net/fdroid/archive https://mirror.freedif.org/fdroid/archive + https://mirrors.hostico.ro/fdroid/archive + https://ftp.lug.ro/fdroid/archive + https://mirrors.chroot.ro/fdroid/archive The archive repository of the F-Droid client. This contains older versions of From 3e3fc846aa8fbf81f0902e1ff8bf756d6b4a27df Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 21 May 2026 09:13:37 -0300 Subject: [PATCH 31/44] Disable pre-approval for Chinese users as it is broken in many Chinese ROMs. In the future, we can gradually do extra checks for Chinese users and re-enable it for some of them. This is just way easier than trying to detect Chinese ROMs which is a research project of its own. --- .../download/FDroidMirrorParameterManager.kt | 16 +++------------ .../fdroid/install/SessionInstallManager.kt | 4 ++++ app/src/main/kotlin/org/fdroid/utils/Utils.kt | 17 ++++++++++++++++ .../install/SessionInstallManagerTest.kt | 20 +++++++++++++++++++ 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/download/FDroidMirrorParameterManager.kt b/app/src/main/kotlin/org/fdroid/download/FDroidMirrorParameterManager.kt index 5fd8dfcf8..f361991f0 100644 --- a/app/src/main/kotlin/org/fdroid/download/FDroidMirrorParameterManager.kt +++ b/app/src/main/kotlin/org/fdroid/download/FDroidMirrorParameterManager.kt @@ -1,15 +1,13 @@ package org.fdroid.download import android.content.Context -import android.telephony.TelephonyManager -import androidx.core.os.LocaleListCompat import dagger.hilt.android.qualifiers.ApplicationContext -import java.util.Locale import javax.inject.Inject import javax.inject.Singleton import kotlin.concurrent.atomics.ExperimentalAtomicApi import org.fdroid.settings.SettingsConstants import org.fdroid.settings.SettingsManager +import org.fdroid.utils.getCurrentLocation @OptIn(ExperimentalAtomicApi::class) @Singleton @@ -24,7 +22,7 @@ constructor( override fun cacheMirrorIpAddresses( mirrorUrl: String, ipv4Addresses: List, - ipv6Addresses: List + ipv6Addresses: List, ) { dnsWithCache.populateCacheWithStrings(mirrorUrl, ipv4Addresses, ipv6Addresses) } @@ -42,13 +40,5 @@ constructor( return settingsManager.mirrorChooser == SettingsConstants.MirrorChooserValues.PreferForeign } - override fun getCurrentLocation(): String { - val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager - return tm.simCountryIso - ?: tm.networkCountryIso - ?: run { - val localeList = LocaleListCompat.getDefault() - localeList.get(0)?.country ?: Locale.getDefault().country - } - } + override fun getCurrentLocation(): String = getCurrentLocation(context) } diff --git a/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt b/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt index 9b252af1a..88e7cda95 100644 --- a/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt +++ b/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt @@ -38,6 +38,7 @@ import org.fdroid.database.AppMetadata import org.fdroid.index.v2.PackageVersion import org.fdroid.ui.utils.isAppInForeground import org.fdroid.utils.IoDispatcher +import org.fdroid.utils.isChina @Singleton class SessionInstallManager @@ -105,6 +106,9 @@ constructor( // should not be needed, so we say not supported log.info { "Can do auto-update pre-approval for ${app.packageName} not needed." } PreApprovalResult.NotSupported + } else if (isChina(context)) { + log.info { "Device is in China, pre-approval is broken." } + PreApprovalResult.NotSupported } else if (SDK_INT >= 34) { log.info { "Requesting pre-approval for ${app.packageName}..." } try { diff --git a/app/src/main/kotlin/org/fdroid/utils/Utils.kt b/app/src/main/kotlin/org/fdroid/utils/Utils.kt index 307e7a085..14283a06b 100644 --- a/app/src/main/kotlin/org/fdroid/utils/Utils.kt +++ b/app/src/main/kotlin/org/fdroid/utils/Utils.kt @@ -1,6 +1,8 @@ package org.fdroid.utils import android.content.Context +import android.telephony.TelephonyManager +import androidx.core.os.LocaleListCompat import java.security.MessageDigest import java.security.NoSuchAlgorithmException import java.text.SimpleDateFormat @@ -30,6 +32,21 @@ fun getLogName(context: Context): String { return "${context.packageName}-$time" } +fun getCurrentLocation(context: Context): String { + val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + return tm.simCountryIso + ?: tm.networkCountryIso + ?: run { + val localeList = LocaleListCompat.getDefault() + localeList.get(0)?.country ?: Locale.getDefault().country + } +} + +fun isChina(context: Context): Boolean { + val country = getCurrentLocation(context) + return country.equals("cn", ignoreCase = true) +} + val isFull: Boolean get() = FLAVOR.startsWith("full") val isBasic: Boolean diff --git a/app/src/test/java/org/fdroid/install/SessionInstallManagerTest.kt b/app/src/test/java/org/fdroid/install/SessionInstallManagerTest.kt index 19355e207..ac8386a18 100644 --- a/app/src/test/java/org/fdroid/install/SessionInstallManagerTest.kt +++ b/app/src/test/java/org/fdroid/install/SessionInstallManagerTest.kt @@ -10,6 +10,7 @@ import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller.Session import android.content.pm.PackageManager import android.os.Build.VERSION.SDK_INT +import android.telephony.TelephonyManager import androidx.core.content.ContextCompat.registerReceiver import io.mockk.Runs import io.mockk.every @@ -62,6 +63,7 @@ internal class SessionInstallManagerTest { private val receiver: InstallBroadcastReceiver = mockk() private val pendingIntent: PendingIntent = mockk() private val session: Session = mockk() + private val telephonyManager: TelephonyManager = mockk(relaxed = true) private val sessionId = 123 private val packageName = "com.example.app" @@ -110,6 +112,7 @@ internal class SessionInstallManagerTest { every { anyConstructed().addFlags(any()) } returns mockk() every { context.packageManager } returns packageManager + every { context.getSystemService(any()) } returns telephonyManager every { packageManager.packageInstaller } returns packageInstaller every { packageInstaller.mySessions } returns emptyList() every { context.unregisterReceiver(any()) } just runs @@ -604,4 +607,21 @@ internal class SessionInstallManagerTest { listenerSlot.captured.invoke(receiver, packageInstallerResult, Intent("confirm"), msg) } } + + @Test + fun `requestPreapproval not supported in China`(): Unit = runBlocking { + every { telephonyManager.simCountryIso } returns "CN" + every { telephonyManager.networkCountryIso } returns null + every { context.isAppInForeground() } returns true + + val notSupportedResult = + sessionInstallManager.requestPreapproval( + app = appMetadata, + iconGetter = { null }, + isUpdate = false, + version = appVersion, + canRequestUserConfirmationNow = true, + ) + assertIs(notSupportedResult) + } } From 526cb80e8dda05207baa1964ae400ff41e5a54ab Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 21 May 2026 09:24:31 -0300 Subject: [PATCH 32/44] Enable pre-approval for Chinese users on userdebug builds This automatically captures LineageOS and maybe other ROMs. --- .../main/kotlin/org/fdroid/install/SessionInstallManager.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt b/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt index 88e7cda95..c113f5d0f 100644 --- a/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt +++ b/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt @@ -17,6 +17,7 @@ import android.content.pm.PackageInstaller.SessionParams import android.content.pm.PackageManager import android.graphics.Bitmap import android.icu.util.ULocale +import android.os.Build import android.os.Build.VERSION.SDK_INT import androidx.annotation.RequiresApi import androidx.annotation.WorkerThread @@ -106,8 +107,8 @@ constructor( // should not be needed, so we say not supported log.info { "Can do auto-update pre-approval for ${app.packageName} not needed." } PreApprovalResult.NotSupported - } else if (isChina(context)) { - log.info { "Device is in China, pre-approval is broken." } + } else if (isChina(context) && Build.TYPE != "userdebug") { + log.info { "Device is in China and not a userdebug build, so pre-approval is likely broken." } PreApprovalResult.NotSupported } else if (SDK_INT >= 34) { log.info { "Requesting pre-approval for ${app.packageName}..." } From c02281a14dbce9c4b7dbae97b38e4bdb00d43c33 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 21 May 2026 09:54:44 -0300 Subject: [PATCH 33/44] Fix sorting by lastUpdated in MyApps --- app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt index 76123982d..d5e5ef8fc 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt @@ -121,7 +121,7 @@ private fun List.sort(sortOrder: AppListSortOrder): List { } AppListSortOrder.LAST_UPDATED -> { sortedWith { a1, a2 -> - val lastAddedCompare = a1.lastUpdated.compareTo(a2.lastUpdated) + val lastAddedCompare = a2.lastUpdated.compareTo(a1.lastUpdated) if (lastAddedCompare == 0) { // fall-back to name if last updated is the same, to ensure stable sorting collator.compare(a1.name, a2.name) From 8afa8664f2065f797e545b588fff6780f6197499 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 21 May 2026 10:21:57 -0300 Subject: [PATCH 34/44] [db] Fix localization of updates --- .../src/main/java/org/fdroid/database/DbAppChecker.kt | 6 ++++-- .../src/main/java/org/fdroid/database/DbUpdateChecker.kt | 6 ++++-- .../src/test/java/org/fdroid/database/DbAppCheckerTest.kt | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/libs/database/src/main/java/org/fdroid/database/DbAppChecker.kt b/libs/database/src/main/java/org/fdroid/database/DbAppChecker.kt index f73eb1b21..929beeff8 100644 --- a/libs/database/src/main/java/org/fdroid/database/DbAppChecker.kt +++ b/libs/database/src/main/java/org/fdroid/database/DbAppChecker.kt @@ -5,6 +5,7 @@ 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 androidx.core.os.LocaleListCompat import java.util.concurrent.TimeUnit import org.fdroid.CompatibilityChecker import org.fdroid.CompatibilityCheckerImpl @@ -22,6 +23,7 @@ public class DbAppChecker( private val appDao = db.getAppDao() as AppDaoInt private val versionDao = db.getVersionDao() as VersionDaoInt private val appPrefsDao = db.getAppPrefsDao() as AppPrefsDaoInt + private val localeList = LocaleListCompat.getDefault() /** * Gets all apps that somehow have a special status that warrants the user's attention. These can @@ -282,8 +284,8 @@ public class DbAppChecker( update = version.toAppVersion(versionedStrings), isFromPreferredRepo = true, hasKnownVulnerability = version.hasKnownVulnerability, - name = appOverviewItem.name, - summary = appOverviewItem.summary, + name = appOverviewItem.getName(localeList), + summary = appOverviewItem.getSummary(localeList), 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 25d1757f8..271bc53e7 100644 --- a/libs/database/src/main/java/org/fdroid/database/DbUpdateChecker.kt +++ b/libs/database/src/main/java/org/fdroid/database/DbUpdateChecker.kt @@ -5,6 +5,7 @@ import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.pm.PackageManager.GET_SIGNATURES import androidx.core.content.pm.PackageInfoCompat.getLongVersionCode +import androidx.core.os.LocaleListCompat import org.fdroid.CompatibilityChecker import org.fdroid.CompatibilityCheckerImpl import org.fdroid.PackagePreference @@ -23,6 +24,7 @@ constructor( private val appDao = db.getAppDao() as AppDaoInt private val versionDao = db.getVersionDao() as VersionDaoInt private val appPrefsDao = db.getAppPrefsDao() as AppPrefsDaoInt + private val localeList = LocaleListCompat.getDefault() /** * Returns a list of apps that can be updated. @@ -190,8 +192,8 @@ constructor( update = version.toAppVersion(versionedStrings), isFromPreferredRepo = isFromPreferredRepo, hasKnownVulnerability = version.hasKnownVulnerability, - name = appOverviewItem.name, - summary = appOverviewItem.summary, + name = appOverviewItem.getName(localeList), + summary = appOverviewItem.getSummary(localeList), localizedIcon = appOverviewItem.localizedIcon, ) } diff --git a/libs/database/src/test/java/org/fdroid/database/DbAppCheckerTest.kt b/libs/database/src/test/java/org/fdroid/database/DbAppCheckerTest.kt index f397a0a1b..6c49e69ad 100644 --- a/libs/database/src/test/java/org/fdroid/database/DbAppCheckerTest.kt +++ b/libs/database/src/test/java/org/fdroid/database/DbAppCheckerTest.kt @@ -531,8 +531,8 @@ internal class DbAppCheckerTest { private fun makeAppOverviewItem(packageName: String = this.packageName): AppOverviewItem = mockk().also { every { it.packageName } returns packageName - every { it.name } returns appName - every { it.summary } returns "summary" + every { it.getName(any()) } returns appName + every { it.getSummary(any()) } returns "summary" every { it.localizedIcon } returns null } From eaf265436b2cf78e9d47c073f4a4780ffbd10ae2 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 21 May 2026 11:06:28 -0300 Subject: [PATCH 35/44] Add new games categories --- .../org/fdroid/ui/categories/CategoryGroup.kt | 105 ++++++++++-------- .../org/fdroid/ui/categories/CategoryItem.kt | 50 ++++++++- app/src/main/res/values/strings.xml | 24 ++-- 3 files changed, 116 insertions(+), 63 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroup.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroup.kt index dc9959a73..adba266ef 100644 --- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroup.kt +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroup.kt @@ -10,6 +10,7 @@ import androidx.compose.material.icons.filled.Forum import androidx.compose.material.icons.filled.NetworkCheck import androidx.compose.material.icons.filled.PhoneAndroid import androidx.compose.material.icons.filled.SdStorage +import androidx.compose.material.icons.filled.SportsEsports import androidx.compose.material.icons.filled.VideogameAsset import androidx.compose.material.icons.filled.Wallet import androidx.compose.ui.graphics.vector.ImageVector @@ -23,6 +24,54 @@ data class CategoryGroup( ) object CategoryGroups { + val communication = + CategoryGroup( + id = "communication", + name = R.string.category_group_communication, + summary = R.string.category_group_summary_communication, + imageVector = Icons.Default.Forum, + ) + val device = + CategoryGroup( + id = "device", + name = R.string.category_group_device, + summary = R.string.category_group_summary_device, + imageVector = Icons.Default.PhoneAndroid, + ) + val games = CategoryGroup( + id = "games", + name = R.string.category_group_games, + summary = R.string.category_group_summary_games, + imageVector = Icons.Default.SportsEsports, + ) + val interests = + CategoryGroup( + id = "interests", + name = R.string.category_group_interests, + summary = R.string.category_group_summary_interests, + imageVector = Icons.Default.FavoriteBorder, + ) + val media = + CategoryGroup( + id = "media", + name = R.string.category_group_media, + summary = R.string.category_group_summary_media, + imageVector = Icons.Default.VideogameAsset, + ) + val misc = + CategoryGroup( + id = "misc", + name = R.string.category_group_misc, + summary = R.string.category_group_summary_misc, + imageVector = Icons.Default.Category, + ) + val network = + CategoryGroup( + id = "network", + name = R.string.category_group_network, + summary = R.string.category_group_summary_network, + imageVector = Icons.Default.NetworkCheck, + ) val productivity = CategoryGroup( id = "productivity", @@ -30,6 +79,13 @@ object CategoryGroups { summary = R.string.category_group_summary_productivity, imageVector = Icons.Default.Factory, ) + val storage = + CategoryGroup( + id = "storage", + name = R.string.category_group_storage, + summary = R.string.category_group_summary_storage, + imageVector = Icons.Default.SdStorage, + ) val tools = CategoryGroup( id = "tools", @@ -44,53 +100,4 @@ object CategoryGroups { summary = R.string.category_group_summary_wallets, imageVector = Icons.Default.Wallet, ) - val media = - CategoryGroup( - id = "media", - name = R.string.category_group_media, - summary = R.string.category_group_summary_media, - imageVector = Icons.Default.VideogameAsset, - ) - val communication = - CategoryGroup( - id = "communication", - name = R.string.category_group_communication, - summary = R.string.category_group_summary_communication, - imageVector = Icons.Default.Forum, - ) - val device = - CategoryGroup( - id = "device", - name = R.string.category_group_device, - summary = R.string.category_group_summary_device, - imageVector = Icons.Default.PhoneAndroid, - ) - val network = - CategoryGroup( - id = "network", - name = R.string.category_group_network, - summary = R.string.category_group_summary_network, - imageVector = Icons.Default.NetworkCheck, - ) - val storage = - CategoryGroup( - id = "storage", - name = R.string.category_group_storage, - summary = R.string.category_group_summary_storage, - imageVector = Icons.Default.SdStorage, - ) - val interests = - CategoryGroup( - id = "interests", - name = R.string.category_group_interests, - summary = R.string.category_group_summary_interests, - imageVector = Icons.Default.FavoriteBorder, - ) - val misc = - CategoryGroup( - id = "misc", - name = R.string.category_group_misc, - summary = R.string.category_group_summary_misc, - imageVector = Icons.Default.Category, - ) } diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt index 5278f77f8..cc26b6830 100644 --- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt @@ -17,29 +17,38 @@ import androidx.compose.material.icons.filled.BrowserUpdated import androidx.compose.material.icons.filled.Brush import androidx.compose.material.icons.filled.Calculate import androidx.compose.material.icons.filled.CalendarMonth +import androidx.compose.material.icons.filled.Casino +import androidx.compose.material.icons.filled.Castle import androidx.compose.material.icons.filled.Category +import androidx.compose.material.icons.filled.Celebration +import androidx.compose.material.icons.filled.CenterFocusWeak import androidx.compose.material.icons.filled.Church import androidx.compose.material.icons.filled.Cloud import androidx.compose.material.icons.filled.Collections import androidx.compose.material.icons.filled.Contacts +import androidx.compose.material.icons.filled.CrueltyFree import androidx.compose.material.icons.filled.CurrencyExchange +import androidx.compose.material.icons.filled.DeveloperBoard import androidx.compose.material.icons.filled.DeveloperMode import androidx.compose.material.icons.filled.DirectionsBus +import androidx.compose.material.icons.filled.Diversity3 import androidx.compose.material.icons.filled.Dns import androidx.compose.material.icons.filled.Draw import androidx.compose.material.icons.filled.EditNote import androidx.compose.material.icons.filled.EnhancedEncryption +import androidx.compose.material.icons.filled.Extension import androidx.compose.material.icons.filled.Fastfood import androidx.compose.material.icons.filled.FitnessCenter import androidx.compose.material.icons.filled.FlashlightOn -import androidx.compose.material.icons.filled.Games import androidx.compose.material.icons.filled.Groups +import androidx.compose.material.icons.filled.Handyman import androidx.compose.material.icons.filled.HealthAndSafety import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Image import androidx.compose.material.icons.filled.Keyboard import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.LocalPlay +import androidx.compose.material.icons.filled.ModeComment import androidx.compose.material.icons.filled.MonetizationOn import androidx.compose.material.icons.filled.Money import androidx.compose.material.icons.filled.MusicNote @@ -57,6 +66,7 @@ import androidx.compose.material.icons.filled.Podcasts import androidx.compose.material.icons.filled.Radio import androidx.compose.material.icons.filled.RecordVoiceOver import androidx.compose.material.icons.filled.RestaurantMenu +import androidx.compose.material.icons.filled.School import androidx.compose.material.icons.filled.Science import androidx.compose.material.icons.filled.Security import androidx.compose.material.icons.filled.SelfImprovement @@ -64,6 +74,9 @@ import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.SettingsRemote import androidx.compose.material.icons.filled.ShoppingCart import androidx.compose.material.icons.filled.SignalCellularAlt +import androidx.compose.material.icons.filled.Sos +import androidx.compose.material.icons.filled.SportsMartialArts +import androidx.compose.material.icons.filled.SportsSoccer import androidx.compose.material.icons.filled.Storefront import androidx.compose.material.icons.filled.Style import androidx.compose.material.icons.filled.TaskAlt @@ -72,6 +85,7 @@ import androidx.compose.material.icons.filled.TrackChanges import androidx.compose.material.icons.filled.Translate import androidx.compose.material.icons.filled.UploadFile import androidx.compose.material.icons.filled.VideoChat +import androidx.compose.material.icons.filled.VideogameAsset import androidx.compose.material.icons.filled.VoiceChat import androidx.compose.material.icons.filled.VpnLock import androidx.compose.material.icons.filled.Wallet @@ -86,21 +100,27 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "AI Chat" -> Icons.Default.VoiceChat "App Manager" -> Icons.Default.Apps "App Store & Updater" -> Icons.Default.Storefront + "Action Game" -> Icons.Default.SportsMartialArts "Battery" -> Icons.Default.BatteryChargingFull + "Board Game" -> Icons.Default.DeveloperBoard "Bookmark" -> Icons.Default.Bookmarks "Browser" -> Icons.Default.OpenInBrowser "Calculator" -> Icons.Default.Calculate "Calendar & Agenda" -> Icons.Default.CalendarMonth + "Card Game" -> Icons.Default.Style "Clock" -> Icons.Default.AccessTime "Cloud Storage & File Sync" -> Icons.Default.Cloud "Connectivity" -> Icons.Default.SignalCellularAlt "Contact" -> Icons.Default.Contacts "Development" -> Icons.Default.DeveloperMode + "Dice" -> Icons.Default.Casino "Diet" -> Icons.Default.Fastfood "DNS & Hosts" -> Icons.Default.Dns "Draw" -> Icons.Default.Draw "Ebook Reader" -> AutoMirrored.Default.MenuBook + "Educational Game" -> Icons.Default.School "Email" -> Icons.Default.AlternateEmail + "Emulator" -> Icons.Default.VideogameAsset "File Encryption & Vault" -> Icons.Default.EnhancedEncryption "File Transfer" -> Icons.Default.UploadFile "Finance Manager" -> Icons.Default.MonetizationOn @@ -108,7 +128,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Flashlight" -> Icons.Default.FlashlightOn "Forum" -> Icons.Default.Image "Gallery" -> Icons.Default.PhotoSizeSelectActual - "Games" -> Icons.Default.Games + "Game Helper" -> Icons.Default.Handyman "Graphics" -> Icons.Default.Brush "Habit Tracker" -> Icons.Default.TrackChanges "Health Manager" -> Icons.Default.HealthAndSafety @@ -129,22 +149,29 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "News" -> Icons.Default.Newspaper "Note" -> Icons.Default.NoteAlt "Online Media Player" -> Icons.Default.Airplay + "Party Game" -> Icons.Default.Celebration "Pass Wallet" -> Icons.Default.AccountBalanceWallet "Password & 2FA" -> Icons.Default.Password "Phone & SMS" -> Icons.Default.PermPhoneMsg + "Platform Game" -> Icons.Default.CrueltyFree "Podcast" -> Icons.Default.Podcasts "Public Transport" -> Icons.Default.DirectionsBus + "Puzzle Game" -> Icons.Default.Extension "Radio" -> Icons.Default.Radio "Reading" -> AutoMirrored.Default.MenuBook "Recipe Manager" -> Icons.Default.RestaurantMenu "Religion" -> Icons.Default.Church + "Role-Playing Game" -> Icons.Default.Diversity3 "Remote Access" -> Icons.Default.BrowserUpdated "Remote Controller" -> Icons.Default.SettingsRemote "Schedule" -> Icons.Default.CalendarMonth "Science & Education" -> Icons.Default.Science "Security" -> Icons.Default.Security + "Shooter Game" -> Icons.Default.CenterFocusWeak + "Strategy Game" -> Icons.Default.Castle "Shopping List" -> Icons.Default.ShoppingCart "Social Network" -> Icons.Default.Groups + "Sport Game" -> Icons.Default.SportsSoccer "Sports & Health" -> Icons.Default.HealthAndSafety "System" -> Icons.Default.Settings "Task" -> Icons.Default.TaskAlt @@ -154,12 +181,14 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Time" -> Icons.Default.AccessTime "Time Tracker" -> Icons.Default.Timer "Translation & Dictionary" -> Icons.Default.Translate + "Visual Novel" -> Icons.Default.ModeComment "Voice & Video Chat" -> Icons.Default.VideoChat "Unit Convertor" -> Icons.Default.CurrencyExchange "VPN & Proxy" -> Icons.Default.VpnLock "Wallet" -> Icons.Default.Wallet "Wallpaper" -> Icons.Default.Wallpaper "Weather" -> Icons.Default.WbSunny + "Word Game" -> Icons.Default.Sos "Workout" -> Icons.Default.FitnessCenter "Writing" -> Icons.Default.EditNote else -> Icons.Default.Category @@ -171,21 +200,27 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "AI Chat" -> CategoryGroups.tools "App Manager" -> CategoryGroups.device "App Store & Updater" -> CategoryGroups.device + "Action Game" -> CategoryGroups.games "Battery" -> CategoryGroups.device + "Board Game" -> CategoryGroups.games "Bookmark" -> CategoryGroups.storage "Browser" -> CategoryGroups.network "Calculator" -> CategoryGroups.tools "Calendar & Agenda" -> CategoryGroups.productivity + "Card Game" -> CategoryGroups.games "Clock" -> CategoryGroups.productivity "Cloud Storage & File Sync" -> CategoryGroups.storage "Connectivity" -> CategoryGroups.network "Contact" -> CategoryGroups.communication "Development" -> CategoryGroups.interests + "Dice" -> CategoryGroups.games "Diet" -> CategoryGroups.interests "DNS & Hosts" -> CategoryGroups.network "Draw" -> CategoryGroups.interests "Ebook Reader" -> CategoryGroups.media + "Educational Game" -> CategoryGroups.games "Email" -> CategoryGroups.communication + "Emulator" -> CategoryGroups.games "File Encryption & Vault" -> CategoryGroups.storage "File Transfer" -> CategoryGroups.storage "Finance Manager" -> CategoryGroups.wallets @@ -193,7 +228,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Flashlight" -> CategoryGroups.tools "Forum" -> CategoryGroups.communication "Gallery" -> CategoryGroups.storage - "Games" -> CategoryGroups.media + "Game Helper" -> CategoryGroups.games "Graphics" -> CategoryGroups.interests "Habit Tracker" -> CategoryGroups.productivity "Health Manager" -> CategoryGroups.productivity @@ -214,22 +249,29 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "News" -> CategoryGroups.interests "Note" -> CategoryGroups.storage "Online Media Player" -> CategoryGroups.media + "Party Game" -> CategoryGroups.games "Pass Wallet" -> CategoryGroups.wallets "Password & 2FA" -> CategoryGroups.device "Phone & SMS" -> CategoryGroups.communication + "Platform Game" -> CategoryGroups.games "Podcast" -> CategoryGroups.media "Public Transport" -> CategoryGroups.tools + "Puzzle Game" -> CategoryGroups.games "Radio" -> CategoryGroups.media "Reading" -> CategoryGroups.media "Recipe Manager" -> CategoryGroups.interests "Religion" -> CategoryGroups.interests + "Role-Playing Game" -> CategoryGroups.games "Remote Access" -> CategoryGroups.network "Remote Controller" -> CategoryGroups.tools "Schedule" -> CategoryGroups.productivity "Science & Education" -> CategoryGroups.interests "Security" -> CategoryGroups.device + "Shooter Game" -> CategoryGroups.games + "Strategy Game" -> CategoryGroups.games "Shopping List" -> CategoryGroups.tools "Social Network" -> CategoryGroups.communication + "Sport Game" -> CategoryGroups.games "Sports & Health" -> CategoryGroups.interests "System" -> CategoryGroups.device "Task" -> CategoryGroups.productivity @@ -239,12 +281,14 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Time" -> CategoryGroups.productivity "Time Tracker" -> CategoryGroups.productivity "Translation & Dictionary" -> CategoryGroups.tools + "Visual Novel" -> CategoryGroups.games "Voice & Video Chat" -> CategoryGroups.communication "Unit Convertor" -> CategoryGroups.tools "VPN & Proxy" -> CategoryGroups.network "Wallet" -> CategoryGroups.wallets "Wallpaper" -> CategoryGroups.device "Weather" -> CategoryGroups.tools + "Word Game" -> CategoryGroups.games "Workout" -> CategoryGroups.interests "Writing" -> CategoryGroups.productivity else -> CategoryGroups.misc diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6f9956f0f..8655621e2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -62,28 +62,30 @@ Clear all Hide - Calendars, tasks, clocks & notes - Navigation, weather, calculators & converters - Finance, money & pass managers - Music, podcasts, games & ebook readers - Email, messaging & social networks - Launcher, keyboard, security & theming - Browser, VPNs, proxies & firewall - Files, gallery, cloud sync & encryption - Science, sports, drawing & recipes - Uncategorized - Productivity Tools Finances & Wallets Entertainment & Media Communication Device + Games Network Files & Storage Interests Miscellaneous + Calendars, tasks, clocks & notes + Navigation, weather, calculators & converters + Finance, money & pass managers + Music, podcasts, games & ebook readers + Email, messaging & social networks + Launcher, keyboard, security & theming + Board, card & party games + Browser, VPNs, proxies & firewall + Files, gallery, cloud sync & encryption + Science, sports, drawing & recipes + Uncategorized + Last updated: %1$s Last updated: %1$s (%2$s) From 7792401a4872fd77dc492b120be6ae6d9c64ae4a Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 21 May 2026 18:51:26 -0300 Subject: [PATCH 36/44] Fix version code of basicNightly --- app/build.gradle.kts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2c76716ec..bc3516055 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -10,6 +10,8 @@ plugins { alias(libs.plugins.ktfmt) } +val nightlyVersionCode = (System.currentTimeMillis() / 1000 / 60).toInt() + android { namespace = "org.fdroid" compileSdk = libs.versions.compileSdk.get().toInt() @@ -50,7 +52,7 @@ android { create("default") { dimension = "release" } create("nightly") { dimension = "release" - versionCode = (System.currentTimeMillis() / 1000 / 60).toInt() + versionCode = nightlyVersionCode versionNameSuffix = "-$gitHash" applicationIdSuffix = ".nightly" } @@ -85,6 +87,10 @@ androidComponents { variantBuilder.enable = false } } + // only needed while basic flavor has its own version code + onVariants(selector().withFlavor("release" to "nightly")) { variant -> + variant.outputs.forEach { output -> output.versionCode.set(nightlyVersionCode) } + } } dependencies { From 619753af11a821e46551c2282a6ab5a87adc3245 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 22 May 2026 09:06:20 -0300 Subject: [PATCH 37/44] Fix new game categories --- .../main/kotlin/org/fdroid/ui/categories/CategoryItem.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt index cc26b6830..0e3ab97c3 100644 --- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt @@ -40,6 +40,7 @@ import androidx.compose.material.icons.filled.Extension import androidx.compose.material.icons.filled.Fastfood import androidx.compose.material.icons.filled.FitnessCenter import androidx.compose.material.icons.filled.FlashlightOn +import androidx.compose.material.icons.filled.Gamepad import androidx.compose.material.icons.filled.Groups import androidx.compose.material.icons.filled.Handyman import androidx.compose.material.icons.filled.HealthAndSafety @@ -108,6 +109,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Calculator" -> Icons.Default.Calculate "Calendar & Agenda" -> Icons.Default.CalendarMonth "Card Game" -> Icons.Default.Style + "Casual Game" -> Icons.Default.Gamepad "Clock" -> Icons.Default.AccessTime "Cloud Storage & File Sync" -> Icons.Default.Cloud "Connectivity" -> Icons.Default.SignalCellularAlt @@ -153,7 +155,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Pass Wallet" -> Icons.Default.AccountBalanceWallet "Password & 2FA" -> Icons.Default.Password "Phone & SMS" -> Icons.Default.PermPhoneMsg - "Platform Game" -> Icons.Default.CrueltyFree + "Platformer Game" -> Icons.Default.CrueltyFree "Podcast" -> Icons.Default.Podcasts "Public Transport" -> Icons.Default.DirectionsBus "Puzzle Game" -> Icons.Default.Extension @@ -208,6 +210,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Calculator" -> CategoryGroups.tools "Calendar & Agenda" -> CategoryGroups.productivity "Card Game" -> CategoryGroups.games + "Casual Game" -> CategoryGroups.games "Clock" -> CategoryGroups.productivity "Cloud Storage & File Sync" -> CategoryGroups.storage "Connectivity" -> CategoryGroups.network @@ -253,7 +256,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Pass Wallet" -> CategoryGroups.wallets "Password & 2FA" -> CategoryGroups.device "Phone & SMS" -> CategoryGroups.communication - "Platform Game" -> CategoryGroups.games + "Platformer Game" -> CategoryGroups.games "Podcast" -> CategoryGroups.media "Public Transport" -> CategoryGroups.tools "Puzzle Game" -> CategoryGroups.games From fc252cc84581349cfaf69abe08c90211b17c5609 Mon Sep 17 00:00:00 2001 From: Torsten Grote <26331-grote@users.noreply.gitlab.com> Date: Fri, 22 May 2026 13:00:38 +0000 Subject: [PATCH 38/44] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: linsui <2873532-linsui@users.noreply.gitlab.com> --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8655621e2..2529c1861 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -77,7 +77,7 @@ Calendars, tasks, clocks & notes Navigation, weather, calculators & converters Finance, money & pass managers - Music, podcasts, games & ebook readers + Music, podcasts & ebook readers Email, messaging & social networks Launcher, keyboard, security & theming Board, card & party games From 5fe00f6a3e06135085f394f6c97aafa0d14e9a2d Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 22 May 2026 10:38:13 -0300 Subject: [PATCH 39/44] Also disable pre-approval for Chinese locales --- .../fdroid/install/SessionInstallManager.kt | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt b/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt index c113f5d0f..257a0c897 100644 --- a/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt +++ b/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt @@ -26,6 +26,7 @@ import androidx.core.content.ContextCompat.registerReceiver import androidx.core.os.LocaleListCompat import dagger.hilt.android.qualifiers.ApplicationContext import java.io.File +import java.util.Locale import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import javax.inject.Singleton @@ -75,6 +76,21 @@ constructor( // SDK 37 beta 4 also auto updates targetSdk 34 apps return SDK_INT >= 36 && targetSdk >= 34 } + + /** + * Many Chinese ROMs have as of 2026 not properly implemented pre-approval. They either return + * `3: INSTALL_FAILED_ABORTED: User rejected permissions` or not return anything at all. Since + * it is hard to handle this and Chinese ROMs can't easily be detected, we turn pre-approval off + * for all devices that are likely affected by this. + * + * See: https://gitlab.com/fdroid/fdroidclient/-/work_items/3254 + */ + fun isPreApprovalLikelyBroken(context: Context): Boolean { + val localeList = LocaleListCompat.getDefault() + val country = localeList.get(0)?.country ?: Locale.getDefault().country + return (isChina(context) || country.equals("cn", ignoreCase = true)) && + Build.TYPE != "userdebug" + } } init { @@ -107,7 +123,7 @@ constructor( // should not be needed, so we say not supported log.info { "Can do auto-update pre-approval for ${app.packageName} not needed." } PreApprovalResult.NotSupported - } else if (isChina(context) && Build.TYPE != "userdebug") { + } else if (isPreApprovalLikelyBroken(context)) { log.info { "Device is in China and not a userdebug build, so pre-approval is likely broken." } PreApprovalResult.NotSupported } else if (SDK_INT >= 34) { From 8bde3876c55654c40c2d26aca920e4ff8a1ac5b5 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 27 May 2026 15:31:02 -0300 Subject: [PATCH 40/44] Show suggested version even if already installed or a newer version is already installed --- .../org/fdroid/ui/details/DetailsPresenter.kt | 11 +++-------- .../fdroid/ui/details/DetailsPresenterTest.kt | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt index 55d523edd..00f50cb3e 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt @@ -111,15 +111,10 @@ fun DetailsPresenter( if (versions == null || appPrefs == null) { null } else { - // Use getUpdate() instead of getSuggestedVersion() to consider `installedVersionCode`. - // Otherwise, we would default to `installedVersionCode = 0` and get a suggested version - // that can have a smaller version code than the installed one. - updateChecker.getUpdate( + updateChecker.getSuggestedVersion( versions = versions, - installedVersionCode = installedVersionCode ?: 0, - allowedSignersGetter = - (installedSigner ?: app.metadata.preferredSigner)?.let { { setOf(it) } }, - allowedReleaseChannels = appPrefs.releaseChannels, + preferredSigner = installedSigner ?: app.metadata.preferredSigner, + releaseChannels = appPrefs.releaseChannels, preferencesGetter = { appPrefs }, ) } diff --git a/app/src/test/java/org/fdroid/ui/details/DetailsPresenterTest.kt b/app/src/test/java/org/fdroid/ui/details/DetailsPresenterTest.kt index 11b4462b1..d98d92b09 100644 --- a/app/src/test/java/org/fdroid/ui/details/DetailsPresenterTest.kt +++ b/app/src/test/java/org/fdroid/ui/details/DetailsPresenterTest.kt @@ -329,6 +329,25 @@ internal class DetailsPresenterTest { } } + @Test + fun suggestedVersionIsStillSetWhenInstalledVersionMatchesSuggestedVersion() = runTest { + // Even if the installed version code equals the suggested version's code, the presenter + // must still expose a non-null suggestedVersion + setupInstalledApp(versionCode = versionCode, isInstalled = true) + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + // The app is up to date, so no update button, but suggestedVersion must still be populated + assertEquals(MainButtonState.NONE, item.mainButtonState) + assertNotNull(item.suggestedVersion) + assertEquals(version, item.suggestedVersion) + assertEquals(versionCode, item.installedVersionCode) + + cancelAndPrintRemainingEvents() + } + } + @Test fun emitsNoneButtonWhenNoCompatibleVersions() = runTest { // all versions are not compatible with this device From b89f10d8d23ec7d6333d92c9a71f7a71341711b5 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 27 May 2026 16:55:53 -0300 Subject: [PATCH 41/44] Fix bug with ignore all updates which would not show a suggested version and prevent installation for previously uninstalled apps --- .../org/fdroid/ui/details/AppDetailsItem.kt | 4 +- .../org/fdroid/ui/details/DetailsPresenter.kt | 7 +++- .../fdroid/ui/details/DetailsPresenterTest.kt | 39 +++++++++++++++++-- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt index ff88e3147..2e53d545e 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt @@ -185,7 +185,9 @@ data class LoadedAppDetailsItem( if (versions == null) { MainButtonState.LOADING } else if ( - suggestedVersion == null || suggestedVersion.versionCode <= installedVersionCode + suggestedVersion == null || + suggestedVersion.versionCode <= installedVersionCode || + suggestedVersion.versionCode <= (appPrefs?.ignoreVersionCodeUpdate ?: 0) ) { MainButtonState.NONE } else { diff --git a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt index 00f50cb3e..a9215f1d8 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt @@ -115,7 +115,10 @@ fun DetailsPresenter( versions = versions, preferredSigner = installedSigner ?: app.metadata.preferredSigner, releaseChannels = appPrefs.releaseChannels, - preferencesGetter = { appPrefs }, + preferencesGetter = { + // the suggested version shouldn't be affected by ignored versions + appPrefs.copy(ignoreVersionCodeUpdate = 0) + }, ) } } @@ -219,7 +222,7 @@ fun DetailsPresenter( allowBetaVersions = viewModel::allowBetaUpdates, onAntiFeaturesOnboardingSeen = viewModel::onAntiFeaturesOnboardingSeen, ignoreAllUpdates = - if (installedVersionCode == null) { + if (installedVersionCode == null && (appPrefs == null || !appPrefs.ignoreAllUpdates)) { null } else { viewModel::ignoreAllUpdates diff --git a/app/src/test/java/org/fdroid/ui/details/DetailsPresenterTest.kt b/app/src/test/java/org/fdroid/ui/details/DetailsPresenterTest.kt index d98d92b09..616ffc3f8 100644 --- a/app/src/test/java/org/fdroid/ui/details/DetailsPresenterTest.kt +++ b/app/src/test/java/org/fdroid/ui/details/DetailsPresenterTest.kt @@ -608,7 +608,8 @@ internal class DetailsPresenterTest { @Test fun ignoresAllUpdatesFromAppPrefs() = runTest { // When the user ignores all updates, ignoresAllUpdates must be true and there should be - // no suggested version, resulting in a NONE button. + // no UPDATE button, but still a suggested version. + setupInstalledApp(versionCode = versionCode - 1) every { appPrefsDao.getAppPrefs(packageName) } returns MutableLiveData(AppPrefs(packageName).toggleIgnoreAllUpdates()) @@ -616,8 +617,38 @@ internal class DetailsPresenterTest { val item = awaitNonNullItem() assertIs(item) assertTrue(item.ignoresAllUpdates) - assertNull(item.suggestedVersion) + // The repo still has a suggested version, it must be shown as suggested + assertNotNull(item.suggestedVersion) + assertEquals(version, item.suggestedVersion) + // No update button because updates are ignored assertEquals(MainButtonState.NONE, item.mainButtonState) + // The user must be able to toggle ignoreAllUpdates back off + assertNotNull(item.actions.ignoreAllUpdates) + + cancelAndPrintRemainingEvents() + } + } + + @Test + fun ignoringAllUpdatesAfterUninstallingApp() = runTest { + // If the user set ignoreAllUpdates while the app was installed, then uninstalled the app, + // the AppPrefs entry with ignoreVersionCodeUpdate = Long.MAX_VALUE persists in the DB. + every { appPrefsDao.getAppPrefs(packageName) } returns + MutableLiveData(AppPrefs(packageName).toggleIgnoreAllUpdates()) + + presenterFlow.test { + val item = awaitNonNullItem() + assertIs(item) + assertTrue(item.ignoresAllUpdates) + assertNull(item.installedVersionCode) // not installed + // The repo still has a version, it must be suggested for fresh installation + assertNotNull(item.suggestedVersion) + assertEquals(version, item.suggestedVersion) + // Install button must be shown even though updates were previously ignored + assertEquals(MainButtonState.INSTALL, item.mainButtonState) + // The user should see ignoreAllUpdates toggle even without the app installed, + // so they can undo it + assertNotNull(item.actions.ignoreAllUpdates) cancelAndPrintRemainingEvents() } @@ -632,8 +663,8 @@ internal class DetailsPresenterTest { presenterFlow.test { val item = awaitNonNullItem() assertIs(item) - // possibleUpdate exists, but ignores, so suggestedVersion is null - assertNull(item.suggestedVersion) + assertNotNull(item.suggestedVersion) + // possibleUpdate exists, but ignored, so no update button assertEquals(MainButtonState.NONE, item.mainButtonState) assertNotNull(item.actions.ignoreThisUpdate) // there is a version to un-ignore assertTrue(item.ignoresCurrentUpdate) From 4e8549d3ce9356115bdf54715c1988c14b4e69cc Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 4 Jun 2026 10:04:07 -0300 Subject: [PATCH 42/44] [download] fix MirrorChooser always picking f-droid.org if there's no domestic mirror --- .../kotlin/org/fdroid/download/MirrorChooser.kt | 4 ++-- .../kotlin/org/fdroid/download/MirrorChooserTest.kt | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/libs/download/src/commonMain/kotlin/org/fdroid/download/MirrorChooser.kt b/libs/download/src/commonMain/kotlin/org/fdroid/download/MirrorChooser.kt index 042dce168..f02ad36e6 100644 --- a/libs/download/src/commonMain/kotlin/org/fdroid/download/MirrorChooser.kt +++ b/libs/download/src/commonMain/kotlin/org/fdroid/download/MirrorChooser.kt @@ -127,7 +127,7 @@ internal class MirrorChooserWithParameters( error1.compareTo(error2) } - val mirrorList: MutableList = mutableListOf() + val mirrorList: MutableList = mutableListOf() if ( mirrorParameterManager != null && mirrorParameterManager.getCurrentLocation().isNotEmpty() @@ -186,8 +186,8 @@ internal class MirrorChooserWithParameters( mirrorList.addAll(domesticList) } else { mirrorList.addAll(domesticList) - mirrorList.addAll(unknownList) mirrorList.addAll(foreignList) + mirrorList.addAll(unknownList) } return mirrorList } diff --git a/libs/download/src/commonTest/kotlin/org/fdroid/download/MirrorChooserTest.kt b/libs/download/src/commonTest/kotlin/org/fdroid/download/MirrorChooserTest.kt index 8107dc2dc..04e723e4a 100644 --- a/libs/download/src/commonTest/kotlin/org/fdroid/download/MirrorChooserTest.kt +++ b/libs/download/src/commonTest/kotlin/org/fdroid/download/MirrorChooserTest.kt @@ -9,7 +9,6 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertTrue import kotlinx.io.IOException -import org.fdroid.getIndexFile import org.fdroid.runSuspend class MirrorChooserTest { @@ -29,15 +28,13 @@ class MirrorChooserTest { ) private val downloadRequest = DownloadRequest("foo", mirrors) private val downloadRequestLocation = DownloadRequest("location", mirrorsLocation) - private val downloadRequestTryFIrst = + private val downloadRequestTryFirst = DownloadRequest( path = "location", mirrors = mirrorsLocation, tryFirstMirror = Mirror(baseUrl = "remote_1", countryCode = "THERE"), ) - private val ipfsIndexFile = getIndexFile(name = "foo", ipfsCidV1 = "CIDv1") - @Test fun testMirrorChooserDefaultImpl() = runSuspend { val mirrorChooser = MirrorChooserRandom() @@ -177,7 +174,9 @@ class MirrorChooserTest { assertEquals("HERE", domesticList[0].countryCode) assertEquals("HERE", domesticList[1].countryCode) assertEquals("HERE", domesticList[2].countryCode) - assertEquals(null, domesticList[3].countryCode) + // unknown mirrors should be last, + // because otherwise they will always be at the front if there are no domestic mirrors + assertEquals(null, domesticList.last().countryCode) } @Test @@ -229,7 +228,7 @@ class MirrorChooserTest { val mirrorChooser = MirrorChooserWithParameters(mockManager) // test tryfirst mirror parameter - val tryFirstList = mirrorChooser.orderMirrors(downloadRequestTryFIrst) + val tryFirstList = mirrorChooser.orderMirrors(downloadRequestTryFirst) // confirm the list contains all mirrors assertEquals(9, tryFirstList.size) // tryfirst mirror should be included before local mirrors From 876407e30f3ebcb1bd0b3f09e1512d61802803bd Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 4 Jun 2026 10:51:55 -0300 Subject: [PATCH 43/44] Add new categories: Timer, Download and File Manager --- .../kotlin/org/fdroid/ui/categories/CategoryItem.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt index 0e3ab97c3..f76acfcce 100644 --- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt @@ -33,11 +33,13 @@ import androidx.compose.material.icons.filled.DeveloperMode import androidx.compose.material.icons.filled.DirectionsBus import androidx.compose.material.icons.filled.Diversity3 import androidx.compose.material.icons.filled.Dns +import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Draw import androidx.compose.material.icons.filled.EditNote import androidx.compose.material.icons.filled.EnhancedEncryption import androidx.compose.material.icons.filled.Extension import androidx.compose.material.icons.filled.Fastfood +import androidx.compose.material.icons.filled.FileCopy import androidx.compose.material.icons.filled.FitnessCenter import androidx.compose.material.icons.filled.FlashlightOn import androidx.compose.material.icons.filled.Gamepad @@ -81,6 +83,7 @@ import androidx.compose.material.icons.filled.SportsSoccer import androidx.compose.material.icons.filled.Storefront import androidx.compose.material.icons.filled.Style import androidx.compose.material.icons.filled.TaskAlt +import androidx.compose.material.icons.filled.Timelapse import androidx.compose.material.icons.filled.Timer import androidx.compose.material.icons.filled.TrackChanges import androidx.compose.material.icons.filled.Translate @@ -118,12 +121,14 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Dice" -> Icons.Default.Casino "Diet" -> Icons.Default.Fastfood "DNS & Hosts" -> Icons.Default.Dns + "Download" -> Icons.Default.Download "Draw" -> Icons.Default.Draw "Ebook Reader" -> AutoMirrored.Default.MenuBook "Educational Game" -> Icons.Default.School "Email" -> Icons.Default.AlternateEmail "Emulator" -> Icons.Default.VideogameAsset "File Encryption & Vault" -> Icons.Default.EnhancedEncryption + "File Manager" -> Icons.Default.FileCopy "File Transfer" -> Icons.Default.UploadFile "Finance Manager" -> Icons.Default.MonetizationOn "Firewall" -> Icons.Default.AppBlocking @@ -181,7 +186,8 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Text to Speech" -> Icons.Default.RecordVoiceOver "Theming" -> Icons.Default.Style "Time" -> Icons.Default.AccessTime - "Time Tracker" -> Icons.Default.Timer + "Time Tracker" -> Icons.Default.Timelapse + "Timer" -> Icons.Default.Timer "Translation & Dictionary" -> Icons.Default.Translate "Visual Novel" -> Icons.Default.ModeComment "Voice & Video Chat" -> Icons.Default.VideoChat @@ -219,12 +225,14 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Dice" -> CategoryGroups.games "Diet" -> CategoryGroups.interests "DNS & Hosts" -> CategoryGroups.network + "Download" -> CategoryGroups.network "Draw" -> CategoryGroups.interests "Ebook Reader" -> CategoryGroups.media "Educational Game" -> CategoryGroups.games "Email" -> CategoryGroups.communication "Emulator" -> CategoryGroups.games "File Encryption & Vault" -> CategoryGroups.storage + "File Manager" -> CategoryGroups.storage "File Transfer" -> CategoryGroups.storage "Finance Manager" -> CategoryGroups.wallets "Firewall" -> CategoryGroups.network @@ -283,6 +291,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Theming" -> CategoryGroups.device "Time" -> CategoryGroups.productivity "Time Tracker" -> CategoryGroups.productivity + "Timer" -> CategoryGroups.productivity "Translation & Dictionary" -> CategoryGroups.tools "Visual Novel" -> CategoryGroups.games "Voice & Video Chat" -> CategoryGroups.communication From b58c8039c8c6a3c738f1c8ce487861501ae2ac8a Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 4 Jun 2026 09:57:12 -0300 Subject: [PATCH 44/44] Release 2.0-alpha10 --- app/build.gradle.kts | 4 ++-- metadata/ar/changelogs/2000008.txt | 8 -------- .../2000009.txt => metadata/ar/changelogs/2000010.txt | 0 .../2000009.txt => metadata/be/changelogs/2000010.txt | 0 metadata/ca/changelogs/2000008.txt | 8 -------- .../2000009.txt => metadata/ca/changelogs/2000010.txt | 0 metadata/cs/changelogs/2000008.txt | 8 -------- .../2000009.txt => metadata/cs/changelogs/2000010.txt | 0 metadata/de/changelogs/2000008.txt | 8 -------- .../2000009.txt => metadata/de/changelogs/2000010.txt | 0 metadata/en-GB/changelogs/2000008.txt | 8 -------- .../2000009.txt => metadata/en-GB/changelogs/2000010.txt | 0 metadata/en-US/changelogs/2000008.txt | 8 -------- .../2000009.txt => metadata/en-US/changelogs/2000010.txt | 0 metadata/eo/changelogs/2000008.txt | 8 -------- .../2000009.txt => metadata/eo/changelogs/2000010.txt | 0 metadata/es/changelogs/2000008.txt | 8 -------- .../2000009.txt => metadata/es/changelogs/2000010.txt | 0 .../2000009.txt => metadata/et/changelogs/2000010.txt | 0 metadata/fr/changelogs/2000008.txt | 8 -------- .../2000009.txt => metadata/fr/changelogs/2000010.txt | 0 metadata/ga/changelogs/2000008.txt | 8 -------- .../2000009.txt => metadata/ga/changelogs/2000010.txt | 0 .../2000009.txt => metadata/hr/changelogs/2000010.txt | 0 .../2000009.txt => metadata/hu/changelogs/2000010.txt | 0 .../2000009.txt => metadata/id/changelogs/2000010.txt | 0 .../2000009.txt => metadata/ja/changelogs/2000010.txt | 0 metadata/nb/changelogs/{2000008.txt => 2000010.txt} | 0 metadata/nl/changelogs/{2000008.txt => 2000010.txt} | 0 metadata/pl/changelogs/2000008.txt | 8 -------- .../2000009.txt => metadata/pl/changelogs/2000010.txt | 0 metadata/pt-BR/changelogs/2000008.txt | 8 -------- .../2000009.txt => metadata/pt-BR/changelogs/2000010.txt | 0 metadata/pt-PT/changelogs/2000008.txt | 8 -------- .../2000009.txt => metadata/pt-PT/changelogs/2000010.txt | 0 metadata/pt/changelogs/2000008.txt | 8 -------- .../2000009.txt => metadata/pt/changelogs/2000010.txt | 0 metadata/ru/changelogs/{2000008.txt => 2000010.txt} | 0 .../2000009.txt => metadata/sk/changelogs/2000010.txt | 0 .../2000008.txt => metadata/sl/changelogs/2000010.txt | 0 .../2000009.txt => metadata/sq/changelogs/2000010.txt | 0 .../2000009.txt => metadata/sv/changelogs/2000010.txt | 0 .../2000009.txt => metadata/sw/changelogs/2000010.txt | 0 .../2000009.txt => metadata/tr/changelogs/2000010.txt | 0 .../2000009.txt => metadata/uk/changelogs/2000010.txt | 0 .../2000009.txt => metadata/vi/changelogs/2000010.txt | 0 metadata/zh-CN/changelogs/2000008.txt | 8 -------- .../2000009.txt => metadata/zh-CN/changelogs/2000010.txt | 0 .../2000009.txt => metadata/zh-TW/changelogs/2000010.txt | 0 .../fastlane/metadata/android/nb/changelogs/2000009.txt | 8 -------- .../fastlane/metadata/android/nl/changelogs/2000009.txt | 8 -------- .../fastlane/metadata/android/ru/changelogs/2000009.txt | 8 -------- .../fastlane/metadata/android/sl/changelogs/2000009.txt | 8 -------- 53 files changed, 2 insertions(+), 154 deletions(-) delete mode 100644 metadata/ar/changelogs/2000008.txt rename src/basic/fastlane/metadata/android/ar/changelogs/2000009.txt => metadata/ar/changelogs/2000010.txt (100%) rename src/basic/fastlane/metadata/android/be/changelogs/2000009.txt => metadata/be/changelogs/2000010.txt (100%) delete mode 100644 metadata/ca/changelogs/2000008.txt rename src/basic/fastlane/metadata/android/ca/changelogs/2000009.txt => metadata/ca/changelogs/2000010.txt (100%) delete mode 100644 metadata/cs/changelogs/2000008.txt rename src/basic/fastlane/metadata/android/cs/changelogs/2000009.txt => metadata/cs/changelogs/2000010.txt (100%) delete mode 100644 metadata/de/changelogs/2000008.txt rename src/basic/fastlane/metadata/android/de/changelogs/2000009.txt => metadata/de/changelogs/2000010.txt (100%) delete mode 100644 metadata/en-GB/changelogs/2000008.txt rename src/basic/fastlane/metadata/android/en-GB/changelogs/2000009.txt => metadata/en-GB/changelogs/2000010.txt (100%) delete mode 100644 metadata/en-US/changelogs/2000008.txt rename src/basic/fastlane/metadata/android/en-US/changelogs/2000009.txt => metadata/en-US/changelogs/2000010.txt (100%) delete mode 100644 metadata/eo/changelogs/2000008.txt rename src/basic/fastlane/metadata/android/eo/changelogs/2000009.txt => metadata/eo/changelogs/2000010.txt (100%) delete mode 100644 metadata/es/changelogs/2000008.txt rename src/basic/fastlane/metadata/android/es/changelogs/2000009.txt => metadata/es/changelogs/2000010.txt (100%) rename src/basic/fastlane/metadata/android/et/changelogs/2000009.txt => metadata/et/changelogs/2000010.txt (100%) delete mode 100644 metadata/fr/changelogs/2000008.txt rename src/basic/fastlane/metadata/android/fr/changelogs/2000009.txt => metadata/fr/changelogs/2000010.txt (100%) delete mode 100644 metadata/ga/changelogs/2000008.txt rename src/basic/fastlane/metadata/android/ga/changelogs/2000009.txt => metadata/ga/changelogs/2000010.txt (100%) rename src/basic/fastlane/metadata/android/hr/changelogs/2000009.txt => metadata/hr/changelogs/2000010.txt (100%) rename src/basic/fastlane/metadata/android/hu/changelogs/2000009.txt => metadata/hu/changelogs/2000010.txt (100%) rename src/basic/fastlane/metadata/android/id/changelogs/2000009.txt => metadata/id/changelogs/2000010.txt (100%) rename src/basic/fastlane/metadata/android/ja/changelogs/2000009.txt => metadata/ja/changelogs/2000010.txt (100%) rename metadata/nb/changelogs/{2000008.txt => 2000010.txt} (100%) rename metadata/nl/changelogs/{2000008.txt => 2000010.txt} (100%) delete mode 100644 metadata/pl/changelogs/2000008.txt rename src/basic/fastlane/metadata/android/pl/changelogs/2000009.txt => metadata/pl/changelogs/2000010.txt (100%) delete mode 100644 metadata/pt-BR/changelogs/2000008.txt rename src/basic/fastlane/metadata/android/pt-BR/changelogs/2000009.txt => metadata/pt-BR/changelogs/2000010.txt (100%) delete mode 100644 metadata/pt-PT/changelogs/2000008.txt rename src/basic/fastlane/metadata/android/pt-PT/changelogs/2000009.txt => metadata/pt-PT/changelogs/2000010.txt (100%) delete mode 100644 metadata/pt/changelogs/2000008.txt rename src/basic/fastlane/metadata/android/pt/changelogs/2000009.txt => metadata/pt/changelogs/2000010.txt (100%) rename metadata/ru/changelogs/{2000008.txt => 2000010.txt} (100%) rename src/basic/fastlane/metadata/android/sk/changelogs/2000009.txt => metadata/sk/changelogs/2000010.txt (100%) rename src/basic/fastlane/metadata/android/sl/changelogs/2000008.txt => metadata/sl/changelogs/2000010.txt (100%) rename src/basic/fastlane/metadata/android/sq/changelogs/2000009.txt => metadata/sq/changelogs/2000010.txt (100%) rename src/basic/fastlane/metadata/android/sv/changelogs/2000009.txt => metadata/sv/changelogs/2000010.txt (100%) rename src/basic/fastlane/metadata/android/sw/changelogs/2000009.txt => metadata/sw/changelogs/2000010.txt (100%) rename src/basic/fastlane/metadata/android/tr/changelogs/2000009.txt => metadata/tr/changelogs/2000010.txt (100%) rename src/basic/fastlane/metadata/android/uk/changelogs/2000009.txt => metadata/uk/changelogs/2000010.txt (100%) rename src/basic/fastlane/metadata/android/vi/changelogs/2000009.txt => metadata/vi/changelogs/2000010.txt (100%) delete mode 100644 metadata/zh-CN/changelogs/2000008.txt rename src/basic/fastlane/metadata/android/zh-CN/changelogs/2000009.txt => metadata/zh-CN/changelogs/2000010.txt (100%) rename src/basic/fastlane/metadata/android/zh-TW/changelogs/2000009.txt => metadata/zh-TW/changelogs/2000010.txt (100%) delete mode 100644 src/basic/fastlane/metadata/android/nb/changelogs/2000009.txt delete mode 100644 src/basic/fastlane/metadata/android/nl/changelogs/2000009.txt delete mode 100644 src/basic/fastlane/metadata/android/ru/changelogs/2000009.txt delete mode 100644 src/basic/fastlane/metadata/android/sl/changelogs/2000009.txt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bc3516055..0e976f839 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,8 +20,8 @@ android { applicationId = "org.fdroid" minSdk = 24 targetSdk = 37 - versionCode = 2000009 - versionName = "2.0-alpha9" + versionCode = 2000010 + versionName = "2.0-alpha10" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/metadata/ar/changelogs/2000008.txt b/metadata/ar/changelogs/2000008.txt deleted file mode 100644 index 85c9e24f2..000000000 --- a/metadata/ar/changelogs/2000008.txt +++ /dev/null @@ -1,8 +0,0 @@ -* إعادة كتابة واجهة المستخدم بالكامل باستخدام Kotlin Compose (باستثناء ميزة "الأجهزة القريبة") -* تحسين ميزة البحث، لتشمل الأوصاف والترجمات أيضاً -* تسهيل اكتشاف تطبيقات جديدة، مع تسليط الضوء على الأكثر تنزيلاً -* الموافقة على التثبيت *قبل* التنزيل (في حال كان ذلك مدعومًا) -* إمكانية إجراء تحديثات أو تنزيلات متعددة في وقت واحد -* إشعار المستخدم في حال وجود مشاكل في التطبيقات (مثل تغيير مفتاح التوقيع) -* سمة ألوان Material You اختيارية -* تحسين تصفية قوائم التطبيقات diff --git a/src/basic/fastlane/metadata/android/ar/changelogs/2000009.txt b/metadata/ar/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/ar/changelogs/2000009.txt rename to metadata/ar/changelogs/2000010.txt diff --git a/src/basic/fastlane/metadata/android/be/changelogs/2000009.txt b/metadata/be/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/be/changelogs/2000009.txt rename to metadata/be/changelogs/2000010.txt diff --git a/metadata/ca/changelogs/2000008.txt b/metadata/ca/changelogs/2000008.txt deleted file mode 100644 index 4b3c01ef4..000000000 --- a/metadata/ca/changelogs/2000008.txt +++ /dev/null @@ -1,8 +0,0 @@ -* interfície d'usuari completament reescrita amb Kotlin Compose (excepte el «nearby») -* cerca millorada, també cerca a les descripcions i traduccions -* més facilitat per a descobrir aplicacions, també destaca les més baixades -* aprovació de la instal·lació *abans* de fer la baixada -* actualitzacions/baixades múltiples a la vegada -* notificació a usuaris de problemes en les aplicacions (p. ex. canvis en la clau de signatura) -* tema de color Material You opcional -* filtratge de llistes millorat diff --git a/src/basic/fastlane/metadata/android/ca/changelogs/2000009.txt b/metadata/ca/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/ca/changelogs/2000009.txt rename to metadata/ca/changelogs/2000010.txt diff --git a/metadata/cs/changelogs/2000008.txt b/metadata/cs/changelogs/2000008.txt deleted file mode 100644 index ca96b7317..000000000 --- a/metadata/cs/changelogs/2000008.txt +++ /dev/null @@ -1,8 +0,0 @@ -* UI přepracováno od základu pomocí Kotlin Compose -* vylepšené vyhledávání, včetně vyhledávání v popisech a překladech -* snazší objevování nových aplikací, včetně zvýraznění nejstahovanějších aplikací -* schválení instalace *před* stažením -* více aktualizací/stažení současně -* upozornění uživatele na problémy s aplikacemi (např. změna podpisového klíče) -* volitelný barevný motiv Material You -* vylepšené filtrování seznamů aplikací diff --git a/src/basic/fastlane/metadata/android/cs/changelogs/2000009.txt b/metadata/cs/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/cs/changelogs/2000009.txt rename to metadata/cs/changelogs/2000010.txt diff --git a/metadata/de/changelogs/2000008.txt b/metadata/de/changelogs/2000008.txt deleted file mode 100644 index eff89d79b..000000000 --- a/metadata/de/changelogs/2000008.txt +++ /dev/null @@ -1,8 +0,0 @@ -* Bedienoberfläche von Grund auf neu geschrieben mit Kotlin Compose (außer „Umfeld“) -* Verbesserte Suche, auch in Beschreibungen/Übersetzungen -* Einfacheres Entdecken neuer Apps, Hervorheben der am häufigsten heruntergeladenen Apps -* Installationsfreigabe *vor* dem Herunterladen (sofern unterstützt) -* Mehrere Downloads gleichzeitig -* Nutzerbenachrichtigung bei Problemen mit Apps (z. B. Änderung des Signierschlüssels) -* Optionales Material-You-Farbschema -* Verbesserte Filterung von App-Listen diff --git a/src/basic/fastlane/metadata/android/de/changelogs/2000009.txt b/metadata/de/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/de/changelogs/2000009.txt rename to metadata/de/changelogs/2000010.txt diff --git a/metadata/en-GB/changelogs/2000008.txt b/metadata/en-GB/changelogs/2000008.txt deleted file mode 100644 index 5e1e47b33..000000000 --- a/metadata/en-GB/changelogs/2000008.txt +++ /dev/null @@ -1,8 +0,0 @@ -* UI rewritten from scratch with Kotlin Compose (except nearby) -* improved search, also searching in descriptions and translations -* easier to discover new apps, also highlighting the most downloaded ones -* installation approval *before* downloading (if supported) -* multiple updates/downloads at the same time -* notifying user of issues with apps (e.g. signing key changed) -* optional Material You colour theme -* improved filtering of app lists diff --git a/src/basic/fastlane/metadata/android/en-GB/changelogs/2000009.txt b/metadata/en-GB/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/en-GB/changelogs/2000009.txt rename to metadata/en-GB/changelogs/2000010.txt diff --git a/metadata/en-US/changelogs/2000008.txt b/metadata/en-US/changelogs/2000008.txt deleted file mode 100644 index fe298a61e..000000000 --- a/metadata/en-US/changelogs/2000008.txt +++ /dev/null @@ -1,8 +0,0 @@ -* UI rewritten from scratch with Kotlin compose (except nearby) -* improved search, also searching in descriptions and translations -* easier to discover new apps, also highlighting the most downloaded ones -* installation approval *before* downloading (if supported) -* multiple updates/downloads at the same time -* notifying user of issues with apps (e.g. signing key changed) -* optional Material You color theme -* improved filtering of app lists diff --git a/src/basic/fastlane/metadata/android/en-US/changelogs/2000009.txt b/metadata/en-US/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/en-US/changelogs/2000009.txt rename to metadata/en-US/changelogs/2000010.txt diff --git a/metadata/eo/changelogs/2000008.txt b/metadata/eo/changelogs/2000008.txt deleted file mode 100644 index 4260ed227..000000000 --- a/metadata/eo/changelogs/2000008.txt +++ /dev/null @@ -1,8 +0,0 @@ -• tute refarita uzant-fasado per Kotlin Compose (escepte de “Proksime”) -• plibonigita serĉo, ankaŭ priserĉante priskribojn kaj tradukojn -• plifaciligita esplorado de novaj apoj, emfazante la plej popularajn -• montri konfirm-peton antaŭ ol instalado (se subtenata) -• pluraj ĝisdatigadoj/elŝutoj samtempe -• informi uzanton pri aplikaĵ-rilataj problemoj (ekz. ŝanĝo de subskriba ŝlosilo) -• (malnepra) kolora haŭto Material You -• plibonigita filtrado de listoj diff --git a/src/basic/fastlane/metadata/android/eo/changelogs/2000009.txt b/metadata/eo/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/eo/changelogs/2000009.txt rename to metadata/eo/changelogs/2000010.txt diff --git a/metadata/es/changelogs/2000008.txt b/metadata/es/changelogs/2000008.txt deleted file mode 100644 index 53d8e8d32..000000000 --- a/metadata/es/changelogs/2000008.txt +++ /dev/null @@ -1,8 +0,0 @@ -* interfaz reescrita de cero con Kotlin compose (excepto cercano) -* mejorada busqueda, también buscar en descripciones y traducciones -* más facil descubrir apps nuevas, también destacando las más descargadas -* confirmación de instalación *antes* de descargar (si es soportado) -* múltiples actualizaciones/descargas a la vez -* notificar usuario de problemas con apps (p.e. clave de firma cambiada) -* tema Material You opcional) -* filtración de listas de apps mejoradas diff --git a/src/basic/fastlane/metadata/android/es/changelogs/2000009.txt b/metadata/es/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/es/changelogs/2000009.txt rename to metadata/es/changelogs/2000010.txt diff --git a/src/basic/fastlane/metadata/android/et/changelogs/2000009.txt b/metadata/et/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/et/changelogs/2000009.txt rename to metadata/et/changelogs/2000010.txt diff --git a/metadata/fr/changelogs/2000008.txt b/metadata/fr/changelogs/2000008.txt deleted file mode 100644 index 215f4216c..000000000 --- a/metadata/fr/changelogs/2000008.txt +++ /dev/null @@ -1,8 +0,0 @@ -* Interface utilisateur entièrement réécrite avec Kotlin Compose (à l'exception de la fonctionnalité « À proximité ») -* Recherche améliorée, incluant désormais les descriptions et les traductions -* Découverte plus facile de nouvelles applications, avec mise en avant des plus téléchargées -* Validation de l'installation *avant* le téléchargement (si pris en charge) -* Mises à jour et téléchargements multiples simultanés -* Notification à l'utilisateur en cas de problèmes avec les applications (par exemple, modification de la clé de signature) -* Thème de couleurs Material You en option -* Filtrage amélioré des listes d'applications diff --git a/src/basic/fastlane/metadata/android/fr/changelogs/2000009.txt b/metadata/fr/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/fr/changelogs/2000009.txt rename to metadata/fr/changelogs/2000010.txt diff --git a/metadata/ga/changelogs/2000008.txt b/metadata/ga/changelogs/2000008.txt deleted file mode 100644 index fbb87db46..000000000 --- a/metadata/ga/changelogs/2000008.txt +++ /dev/null @@ -1,8 +0,0 @@ -* Athscríobh an comhéadan úsáideora ón tús le Kotlin compose -* feabhas ar an gcuardach, ag cuardach i dtuairiscí agus in aistriúcháin freisin -* níos éasca aipeanna nua a aimsiú, ag aird a tharraingt ar na cinn is mó a íoslódáladh freisin -* ceadú suiteála *roimh* íoslódáil (más tacaítear leis) -* ilnuashonruithe/íoslódálacha ag an am céanna -* fógra a thabhairt don úsáideoir faoi fhadhbanna le haipeanna -* téama roghnach Material You -* scagadh feabhsaithe ar liostaí aipeanna diff --git a/src/basic/fastlane/metadata/android/ga/changelogs/2000009.txt b/metadata/ga/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/ga/changelogs/2000009.txt rename to metadata/ga/changelogs/2000010.txt diff --git a/src/basic/fastlane/metadata/android/hr/changelogs/2000009.txt b/metadata/hr/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/hr/changelogs/2000009.txt rename to metadata/hr/changelogs/2000010.txt diff --git a/src/basic/fastlane/metadata/android/hu/changelogs/2000009.txt b/metadata/hu/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/hu/changelogs/2000009.txt rename to metadata/hu/changelogs/2000010.txt diff --git a/src/basic/fastlane/metadata/android/id/changelogs/2000009.txt b/metadata/id/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/id/changelogs/2000009.txt rename to metadata/id/changelogs/2000010.txt diff --git a/src/basic/fastlane/metadata/android/ja/changelogs/2000009.txt b/metadata/ja/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/ja/changelogs/2000009.txt rename to metadata/ja/changelogs/2000010.txt diff --git a/metadata/nb/changelogs/2000008.txt b/metadata/nb/changelogs/2000010.txt similarity index 100% rename from metadata/nb/changelogs/2000008.txt rename to metadata/nb/changelogs/2000010.txt diff --git a/metadata/nl/changelogs/2000008.txt b/metadata/nl/changelogs/2000010.txt similarity index 100% rename from metadata/nl/changelogs/2000008.txt rename to metadata/nl/changelogs/2000010.txt diff --git a/metadata/pl/changelogs/2000008.txt b/metadata/pl/changelogs/2000008.txt deleted file mode 100644 index b123d664b..000000000 --- a/metadata/pl/changelogs/2000008.txt +++ /dev/null @@ -1,8 +0,0 @@ -* UI napisany od podstaw w Kotlin Compose (z wyjątkiem „w pobliżu”) -* ulepszona wyszukiwarka, przeszukująca również opisy i tłumaczenia -* łatwiejsze odkrywanie nowych aplikacji, w tym wyróżnianie najczęściej pobieranych -* zatwierdzanie instalacji *przed* pobraniem (jeśli obsługiwane) -* wiele aktualizacji/pobierań naraz -* powiadamianie użytkownika o problemach z aplikacjami (np. zmiana klucza podpisu) -* opcjonalny motyw kolorystyczny Material You -* ulepszone filtrowanie list aplikacji diff --git a/src/basic/fastlane/metadata/android/pl/changelogs/2000009.txt b/metadata/pl/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/pl/changelogs/2000009.txt rename to metadata/pl/changelogs/2000010.txt diff --git a/metadata/pt-BR/changelogs/2000008.txt b/metadata/pt-BR/changelogs/2000008.txt deleted file mode 100644 index 290a75083..000000000 --- a/metadata/pt-BR/changelogs/2000008.txt +++ /dev/null @@ -1,8 +0,0 @@ -* Interface reescrita por completo com Kotlin compose (exceto "nas proximidades") -* Pesquisa melhorada, também buscando em descrições e traduções -* Mais fácil descobrir novos aplicativos, destacando também os mais baixados -* Aprovação da instalação *antes* de baixar (se suportado) -* Várias atualizações/downloads ao mesmo tempo -* Notificação do usuário de problemas com os aplicativos (por exemplo, chave de assinatura alterada) -* Tema de cor opcional Material You -* Filtragem melhorada das listas de aplicativos diff --git a/src/basic/fastlane/metadata/android/pt-BR/changelogs/2000009.txt b/metadata/pt-BR/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/pt-BR/changelogs/2000009.txt rename to metadata/pt-BR/changelogs/2000010.txt diff --git a/metadata/pt-PT/changelogs/2000008.txt b/metadata/pt-PT/changelogs/2000008.txt deleted file mode 100644 index 0c249f42f..000000000 --- a/metadata/pt-PT/changelogs/2000008.txt +++ /dev/null @@ -1,8 +0,0 @@ -* Interface reescrita completamente com Kotlin compose (exceto "nas proximidades") -* Pesquisa melhorada, também pesquisar em descrições e traduções -* Mais fácil descobrir novas apps, destacando também as mais descarregadas -* Aprovação da instalação *antes* de descarregar (se suportado) -* Várias atualizações/descargas simultâneas -* Notificação do utilizador sobre problemas com apps (por exemplo, chave de assinatura alterada) -* Tema de cor opcional Material You -* Filtrar melhor a listas de apps diff --git a/src/basic/fastlane/metadata/android/pt-PT/changelogs/2000009.txt b/metadata/pt-PT/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/pt-PT/changelogs/2000009.txt rename to metadata/pt-PT/changelogs/2000010.txt diff --git a/metadata/pt/changelogs/2000008.txt b/metadata/pt/changelogs/2000008.txt deleted file mode 100644 index 0c249f42f..000000000 --- a/metadata/pt/changelogs/2000008.txt +++ /dev/null @@ -1,8 +0,0 @@ -* Interface reescrita completamente com Kotlin compose (exceto "nas proximidades") -* Pesquisa melhorada, também pesquisar em descrições e traduções -* Mais fácil descobrir novas apps, destacando também as mais descarregadas -* Aprovação da instalação *antes* de descarregar (se suportado) -* Várias atualizações/descargas simultâneas -* Notificação do utilizador sobre problemas com apps (por exemplo, chave de assinatura alterada) -* Tema de cor opcional Material You -* Filtrar melhor a listas de apps diff --git a/src/basic/fastlane/metadata/android/pt/changelogs/2000009.txt b/metadata/pt/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/pt/changelogs/2000009.txt rename to metadata/pt/changelogs/2000010.txt diff --git a/metadata/ru/changelogs/2000008.txt b/metadata/ru/changelogs/2000010.txt similarity index 100% rename from metadata/ru/changelogs/2000008.txt rename to metadata/ru/changelogs/2000010.txt diff --git a/src/basic/fastlane/metadata/android/sk/changelogs/2000009.txt b/metadata/sk/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/sk/changelogs/2000009.txt rename to metadata/sk/changelogs/2000010.txt diff --git a/src/basic/fastlane/metadata/android/sl/changelogs/2000008.txt b/metadata/sl/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/sl/changelogs/2000008.txt rename to metadata/sl/changelogs/2000010.txt diff --git a/src/basic/fastlane/metadata/android/sq/changelogs/2000009.txt b/metadata/sq/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/sq/changelogs/2000009.txt rename to metadata/sq/changelogs/2000010.txt diff --git a/src/basic/fastlane/metadata/android/sv/changelogs/2000009.txt b/metadata/sv/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/sv/changelogs/2000009.txt rename to metadata/sv/changelogs/2000010.txt diff --git a/src/basic/fastlane/metadata/android/sw/changelogs/2000009.txt b/metadata/sw/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/sw/changelogs/2000009.txt rename to metadata/sw/changelogs/2000010.txt diff --git a/src/basic/fastlane/metadata/android/tr/changelogs/2000009.txt b/metadata/tr/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/tr/changelogs/2000009.txt rename to metadata/tr/changelogs/2000010.txt diff --git a/src/basic/fastlane/metadata/android/uk/changelogs/2000009.txt b/metadata/uk/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/uk/changelogs/2000009.txt rename to metadata/uk/changelogs/2000010.txt diff --git a/src/basic/fastlane/metadata/android/vi/changelogs/2000009.txt b/metadata/vi/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/vi/changelogs/2000009.txt rename to metadata/vi/changelogs/2000010.txt diff --git a/metadata/zh-CN/changelogs/2000008.txt b/metadata/zh-CN/changelogs/2000008.txt deleted file mode 100644 index 7b9cef8ff..000000000 --- a/metadata/zh-CN/changelogs/2000008.txt +++ /dev/null @@ -1,8 +0,0 @@ -* 用 Kotlin compose 重写用户界面 (除“附近”功能) -* 改进搜索,搜索范围包括描述和翻译 -* 更易发现新应用,并高亮下载最多的应用 -* 下载*前*批准安装 (如支持) -* 同时下载/更新多个应用 -* 通知用户应用存在的问题(如签名密钥更改) -* 可选的 Material You 颜色主题 -* 改进应用列表筛选 diff --git a/src/basic/fastlane/metadata/android/zh-CN/changelogs/2000009.txt b/metadata/zh-CN/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/zh-CN/changelogs/2000009.txt rename to metadata/zh-CN/changelogs/2000010.txt diff --git a/src/basic/fastlane/metadata/android/zh-TW/changelogs/2000009.txt b/metadata/zh-TW/changelogs/2000010.txt similarity index 100% rename from src/basic/fastlane/metadata/android/zh-TW/changelogs/2000009.txt rename to metadata/zh-TW/changelogs/2000010.txt diff --git a/src/basic/fastlane/metadata/android/nb/changelogs/2000009.txt b/src/basic/fastlane/metadata/android/nb/changelogs/2000009.txt deleted file mode 100644 index 67197750f..000000000 --- a/src/basic/fastlane/metadata/android/nb/changelogs/2000009.txt +++ /dev/null @@ -1,8 +0,0 @@ -* Omskrevet brukergrensesnittet med Kotlin compose (unntatt «I nærheten») -* Forbedret søkefunksjon, nå søkes det også i beskrivelser og oversettelser -* Lettere å oppdage nye apper, nå fremheves de som har blitt lastet ned mest -* Godkjenning av installasjon skjer nå *før* nedlasting (hvis det støttes) -* Flere oppdateringer/nedlastinger kan nå utføres samtidig -* Brukeren varsles ved problemer med apper (f.eks. at signeringsnøkkelen har endret seg) -* Valgfritt Material You-drakt -* Forbedret appliste-filtrering diff --git a/src/basic/fastlane/metadata/android/nl/changelogs/2000009.txt b/src/basic/fastlane/metadata/android/nl/changelogs/2000009.txt deleted file mode 100644 index cee477347..000000000 --- a/src/basic/fastlane/metadata/android/nl/changelogs/2000009.txt +++ /dev/null @@ -1,8 +0,0 @@ -* UI vanuit niets herschreven met Kotlin compose -* verbeterde zoekfunctie, die ook zoekt in beschrijvingen en vertalingen -* Eenvoudiger om nieuwe apps te ontdekken, daarnaast worden de meest gedownloadde uitgelicht -* toestemming voor installatie *voor* downloaden -* meerdere updates/downloads tegelijkertijd -* melding aan gebruikers als er problemen met apps zijn (bvb tekensleutel veranderd) -* optioneel "Material You" kleurthema -* verbeterde applijst filtering diff --git a/src/basic/fastlane/metadata/android/ru/changelogs/2000009.txt b/src/basic/fastlane/metadata/android/ru/changelogs/2000009.txt deleted file mode 100644 index bff0d239a..000000000 --- a/src/basic/fastlane/metadata/android/ru/changelogs/2000009.txt +++ /dev/null @@ -1,8 +0,0 @@ -* Полностью переписан интерфейс с использованием Kotlin -* Улучшен поиск, который теперь работает и в описаниях, и в переводах -* Упрощен поиск новых приложений, в частности, выделены самые скачиваемые -* Согласие на установку *до* загрузки (если поддерживается) -* Одновременное обновление и загрузка нескольких приложений -* Уведомление пользователя о проблемах с приложениями (например, об изменении ключа подписи) -* Дополнительная цветовая тема Material You -* Улучшенная фильтрация списков приложений diff --git a/src/basic/fastlane/metadata/android/sl/changelogs/2000009.txt b/src/basic/fastlane/metadata/android/sl/changelogs/2000009.txt deleted file mode 100644 index 8fa9d6e11..000000000 --- a/src/basic/fastlane/metadata/android/sl/changelogs/2000009.txt +++ /dev/null @@ -1,8 +0,0 @@ -* Uporabniški vmesnik, popolnoma prepisan s Kotlin compose -* izboljšano iskanje, tudi iskanje po opisih in prevodih -* lažje odkrivanje novih aplikacij, tudi označevanje najpogosteje prenesenih -* odobritev namestitve *pred* prenosom -* več posodobitev/prenosov hkrati -* obveščanje uporabnika o težavah z aplikacijami (npr. sprememba ključa za podpisovanje) -* izbirna barvna tema Material You -* izboljšano filtriranje seznamov