diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d0f5163a7..82893a609 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -20,20 +20,17 @@ 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
- 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}.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
@@ -61,17 +58,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 +86,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 +95,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 +135,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 +151,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
@@ -265,12 +186,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
@@ -297,40 +215,18 @@ 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:
+ <<: *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 "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
- - git lfs install
script:
- ./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
@@ -348,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}'`
@@ -357,15 +252,14 @@ 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"
+ - $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
- - 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 +318,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
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 9ef3aafd3..0e976f839 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()
@@ -17,9 +19,9 @@ android {
defaultConfig {
applicationId = "org.fdroid"
minSdk = 24
- targetSdk = 36
- versionCode = 2000009
- versionName = "2.0-alpha9"
+ targetSdk = 37
+ versionCode = 2000010
+ versionName = "2.0-alpha10"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -28,6 +30,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") {
@@ -49,7 +52,7 @@ android {
create("default") { dimension = "release" }
create("nightly") {
dimension = "release"
- versionCode = (System.currentTimeMillis() / 1000 / 60).toInt()
+ versionCode = nightlyVersionCode
versionNameSuffix = "-$gitHash"
applicationIdSuffix = ".nightly"
}
@@ -84,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 {
@@ -102,6 +109,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 +140,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/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/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/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;
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 000000000..64201978c
Binary files /dev/null and b/app/src/full/res/mipmap-xhdpi/ic_banner.png differ
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/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/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/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/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/install/AppUpdateReceiver.kt b/app/src/main/kotlin/org/fdroid/install/AppUpdateReceiver.kt
new file mode 100644
index 000000000..75bd1d37a
--- /dev/null
+++ b/app/src/main/kotlin/org/fdroid/install/AppUpdateReceiver.kt
@@ -0,0 +1,44 @@
+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 android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+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 {}
+
+ @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}" }
+ 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) {
+ 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/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) } }
+ }
}
}
diff --git a/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt b/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt
index 1495e8a9e..257a0c897 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
@@ -25,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
@@ -38,6 +40,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
@@ -70,8 +73,24 @@ 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
}
+
+ /**
+ * 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 {
@@ -104,6 +123,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 (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) {
log.info { "Requesting pre-approval for ${app.packageName}..." }
try {
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/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/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..bef22f6e7 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,26 @@ 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 =
+ 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),
+ )
+ 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 ->
@@ -157,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)
}
}
@@ -288,7 +304,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/MyAppsPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt
index 7301a0794..d5e5ef8fc 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 = 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)
+ } else {
+ lastAddedCompare
+ }
+ }
+ }
}
}
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/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/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 5dfbf364f..f76acfcce 100644
--- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt
+++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt
@@ -17,28 +17,41 @@ 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.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.Games
+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
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
@@ -54,20 +67,29 @@ 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.School
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
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
+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
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
@@ -82,30 +104,41 @@ 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
+ "Casual Game" -> Icons.Default.Gamepad
"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
+ "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
"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
"Icon Pack" -> Icons.Default.Collections
"Internet" -> Icons.Default.Language
"Inventory" -> Icons.Default.AllInbox
@@ -113,6 +146,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
@@ -122,34 +156,47 @@ 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
+ "Platformer 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
"Text Editor" -> Icons.Default.EditNote
+ "Text to Speech" -> Icons.Default.RecordVoiceOver
"Theming" -> Icons.Default.Style
"Time" -> Icons.Default.AccessTime
+ "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
"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
@@ -161,30 +208,41 @@ 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
+ "Casual 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
+ "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
"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
"Icon Pack" -> CategoryGroups.device
"Internet" -> CategoryGroups.network
"Inventory" -> CategoryGroups.tools
@@ -192,6 +250,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
@@ -201,34 +260,47 @@ 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
+ "Platformer 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
"Text Editor" -> CategoryGroups.productivity
+ "Text to Speech" -> CategoryGroups.device
"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
"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/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..0c2d3916a 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
@@ -12,7 +14,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
@@ -43,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
@@ -66,7 +68,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 +80,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
@@ -100,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
@@ -146,7 +158,7 @@ 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))
}
@@ -186,6 +198,20 @@ fun AppDetails(
}
}
}
+ // Screenshots
+ if (item.phoneScreenshots.isNotEmpty()) {
+ Screenshots(item.networkState.showWarningDialog, item.phoneScreenshots)
+ }
+ // 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 +236,7 @@ fun AppDetails(
SelectionContainer {
Text(
text = htmlDescription,
+ lineHeight = 22.sp,
modifier = Modifier.padding(horizontal = 16.dp).padding(top = 8.dp),
)
}
@@ -218,6 +245,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 +267,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 +405,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),
@@ -431,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
@@ -468,6 +464,12 @@ fun AppDetailsLoadingPreview() {
@Preview
@Composable
-fun AppDetailsPreview() {
- FDroidContent { AppDetails(testApp, {}, {}) }
+fun AppDetailsNotFoundPreview() {
+ FDroidContent { AppDetails(NotFoundAppDetailsItem, {}, {}) }
+}
+
+@Preview
+@Composable
+fun AppDetailsPreview() {
+ 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..65c41c5a9 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
@@ -79,7 +93,8 @@ 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(),
) {
@@ -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/details/AppDetailsItem.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt
index 98b7e5395..2e53d545e 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,
@@ -181,7 +185,9 @@ data class AppDetailsItem(
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/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 1e713da20..a9215f1d8 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,12 +28,12 @@ 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"
-// TODO write tests for this function
-// see: https://github.com/cashapp/molecule?tab=readme-ov-file#testing
@Composable
fun DetailsPresenter(
db: FDroidDatabase,
@@ -57,18 +56,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 null
+ .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) {
@@ -100,6 +105,7 @@ 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) {
@@ -109,15 +115,23 @@ 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)
+ },
)
}
}
- 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 null
+ .value
+ val repo =
+ when (loadableRepo) {
+ is Loading -> return null
+ is Loaded -> loadableRepo.value ?: return NotFoundAppDetailsItem
+ }
val repositories =
produceState(emptyList(), packageName) {
withContext(dispatcher) {
@@ -151,27 +165,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
@@ -192,7 +204,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,
@@ -210,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
@@ -257,7 +269,7 @@ fun DetailsPresenter(
if (!signerCompatible || installState.showProgress) {
false
} else {
- (installedVersion?.versionCode ?: 0) < version.versionCode
+ (installedVersionCode ?: 0) < version.versionCode
},
)
},
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/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/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()) {
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/ui/utils/PreviewUtils.kt b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt
index 21b55121f..12623b5e0 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,7 @@ 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
import org.fdroid.ui.lists.AppListActions
@@ -143,14 +142,24 @@ 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 =
- AppDetailsItem(
+ LoadedAppDetailsItem(
app =
AppMetadata(
repoId = 1,
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",
@@ -179,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(
@@ -411,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() {}
@@ -519,7 +525,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,
@@ -541,7 +547,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,
@@ -620,13 +626,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/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/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()
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/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 提供
課金
此應用程式包含負面功能
聯絡開發者
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6eb095730..2529c1861 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
@@ -58,32 +59,33 @@
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
- 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
-
- By %1$s
+ Calendars, tasks, clocks & notes
+ Navigation, weather, calculators & converters
+ Finance, money & pass managers
+ Music, podcasts & 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)
@@ -102,6 +104,7 @@
App incompatible with your device, install anyway?
Version
+
by %s
Delete
Prompt to send crash reports
@@ -183,6 +186,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
@@ -190,6 +194,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
@@ -956,6 +961,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
@@ -979,6 +986,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
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/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..5ed7b89fb 100644
--- a/app/src/screenshotTest/kotlin/org/fdroid/ui/details/DetailsTest.kt
+++ b/app/src/screenshotTest/kotlin/org/fdroid/ui/details/DetailsTest.kt
@@ -14,15 +14,31 @@ 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 = {},
)
}
+@Preview
+@Composable
+@PreviewTest
+fun AppDetailsNotFoundTest() =
+ ScreenshotTest(showBottomBar = false) { AppDetails(NotFoundAppDetailsItem, {}, {}) }
+
@Composable
@PreviewTest
@Preview(showBackground = true, showSystemUi = true, heightDp = 3000)
@@ -31,19 +47,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 +72,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 +103,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/apps/MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png
index 95c8a682c..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:9e8e34dc95d9b2b264fafa4bcf601936dfcb707d11abe3cc05000ae5d0bd0d14
-size 114468
+oid sha256:789e8b83fa632ca8110a67c9424759d51c9e1903348483fbdf7dbfa07eeed839
+size 132588
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/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
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..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:837fc43bc5799bdc30a27560718434cc07ceb36c7b2c8f80b68fcdfd8e3d924a
-size 339991
+oid sha256:1bb5854c4ca879caa91c00d719747503d0f44116e0f0d6ee5fc39739bb5ea7c5
+size 328351
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..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:e47c65e00ed022537d09355e361156365472742074ebc2614604e684545ba207
-size 82392
+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 e04531ca3..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:abbedbf99ef0960a5f5e3a3219bfb9ad920e49f72e7f90398226df94661bc274
-size 86216
+oid sha256:41b129d0cd0e4350030b10c6304c106b21dcc52301706d88230088306fbb48d0
+size 86255
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)
+ }
}
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].
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..616ffc3f8
--- /dev/null
+++ b/app/src/test/java/org/fdroid/ui/details/DetailsPresenterTest.kt
@@ -0,0 +1,730 @@
+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 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
+ 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 UPDATE button, but still a suggested version.
+ setupInstalledApp(versionCode = versionCode - 1)
+ every { appPrefsDao.getAppPrefs(packageName) } returns
+ MutableLiveData(AppPrefs(packageName).toggleIgnoreAllUpdates())
+
+ presenterFlow.test {
+ val item = awaitNonNullItem()
+ assertIs(item)
+ assertTrue(item.ignoresAllUpdates)
+ // 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()
+ }
+ }
+
+ @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)
+ 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)
+ 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/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
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 2d11b55cf..1824ab4fe 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,28 +1,28 @@
[versions]
-compileSdk = "36"
-kotlin = "2.3.20"
-androidGradlePlugin = "9.1.1"
-androidKspPlugin = "2.3.6"
+compileSdk = "37"
+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"
kotlinxSerializationCore = "1.11.0"
kotlinxSerializationJson = "1.11.0"
-kotlinxCoroutinesTest = "1.10.2"
+kotlinxCoroutinesTest = "1.11.0"
-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,25 @@ androidxConstraintlayout = "2.2.1"
androidxSwipeRefreshLayout = "1.2.0"
androidxVectordrawable = "1.2.0"
androidxGridlayout = "1.1.0"
-androidxComposeBom = "2026.03.01"
+androidxComposeBom = "2026.05.00"
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"
+material = "1.14.0"
+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"
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
@@ -65,7 +67,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"
@@ -114,7 +116,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 +124,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..934df7413 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -313,6 +313,11 @@
+
+
+
+
+
@@ -328,11 +333,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -348,11 +373,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -368,11 +413,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -388,11 +453,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -408,6 +493,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -433,6 +528,11 @@
+
+
+
+
+
@@ -448,6 +548,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -463,6 +578,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -473,6 +598,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -483,6 +618,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -493,6 +638,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -503,11 +658,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -523,11 +693,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -543,11 +733,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -563,11 +773,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -583,11 +813,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -603,11 +853,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -623,11 +893,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -643,11 +933,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -663,11 +973,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -683,11 +1013,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -703,11 +1053,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -723,11 +1093,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -743,11 +1133,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -763,11 +1173,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -783,11 +1213,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -803,11 +1253,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -823,6 +1293,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -918,6 +1403,11 @@
+
+
+
+
+
@@ -938,6 +1428,11 @@
+
+
+
+
+
@@ -953,6 +1448,11 @@
+
+
+
+
+
@@ -1223,6 +1723,11 @@
+
+
+
+
+
@@ -1238,6 +1743,11 @@
+
+
+
+
+
@@ -1648,6 +2158,11 @@
+
+
+
+
+
@@ -1663,6 +2178,11 @@
+
+
+
+
+
@@ -1683,6 +2203,11 @@
+
+
+
+
+
@@ -1698,6 +2223,11 @@
+
+
+
+
+
@@ -1713,6 +2243,11 @@
+
+
+
+
+
@@ -1728,6 +2263,11 @@
+
+
+
+
+
@@ -1743,6 +2283,11 @@
+
+
+
+
+
@@ -1758,6 +2303,11 @@
+
+
+
+
+
@@ -1773,6 +2323,11 @@
+
+
+
+
+
@@ -1788,6 +2343,11 @@
+
+
+
+
+
@@ -1803,6 +2363,11 @@
+
+
+
+
+
@@ -1818,6 +2383,11 @@
+
+
+
+
+
@@ -1833,6 +2403,11 @@
+
+
+
+
+
@@ -1848,6 +2423,11 @@
+
+
+
+
+
@@ -1863,6 +2443,11 @@
+
+
+
+
+
@@ -1878,6 +2463,11 @@
+
+
+
+
+
@@ -1893,6 +2483,11 @@
+
+
+
+
+
@@ -1908,6 +2503,11 @@
+
+
+
+
+
@@ -1923,6 +2523,11 @@
+
+
+
+
+
@@ -1938,6 +2543,11 @@
+
+
+
+
+
@@ -1953,6 +2563,11 @@
+
+
+
+
+
@@ -1968,6 +2583,11 @@
+
+
+
+
+
@@ -1988,6 +2608,11 @@
+
+
+
+
+
@@ -2003,6 +2628,11 @@
+
+
+
+
+
@@ -2018,6 +2648,11 @@
+
+
+
+
+
@@ -2033,6 +2668,11 @@
+
+
+
+
+
@@ -2048,6 +2688,11 @@
+
+
+
+
+
@@ -2083,6 +2728,11 @@
+
+
+
+
+
@@ -2098,6 +2748,11 @@
+
+
+
+
+
@@ -2113,6 +2768,11 @@
+
+
+
+
+
@@ -2128,6 +2788,11 @@
+
+
+
+
+
@@ -2143,6 +2808,11 @@
+
+
+
+
+
@@ -2163,6 +2833,11 @@
+
+
+
+
+
@@ -2188,6 +2863,11 @@
+
+
+
+
+
@@ -2203,6 +2883,11 @@
+
+
+
+
+
@@ -2218,6 +2903,11 @@
+
+
+
+
+
@@ -2233,6 +2923,11 @@
+
+
+
+
+
@@ -2248,6 +2943,11 @@
+
+
+
+
+
@@ -2263,6 +2963,11 @@
+
+
+
+
+
@@ -2288,6 +2993,11 @@
+
+
+
+
+
@@ -2303,6 +3013,11 @@
+
+
+
+
+
@@ -2318,6 +3033,11 @@
+
+
+
+
+
@@ -2333,6 +3053,11 @@
+
+
+
+
+
@@ -2348,6 +3073,11 @@
+
+
+
+
+
@@ -2363,6 +3093,11 @@
+
+
+
+
+
@@ -2378,6 +3113,11 @@
+
+
+
+
+
@@ -2393,6 +3133,11 @@
+
+
+
+
+
@@ -2408,6 +3153,11 @@
+
+
+
+
+
@@ -2423,6 +3173,11 @@
+
+
+
+
+
@@ -2438,6 +3193,11 @@
+
+
+
+
+
@@ -2453,6 +3213,11 @@
+
+
+
+
+
@@ -2468,6 +3233,11 @@
+
+
+
+
+
@@ -2483,6 +3253,11 @@
+
+
+
+
+
@@ -2498,6 +3273,11 @@
+
+
+
+
+
@@ -2513,6 +3293,11 @@
+
+
+
+
+
@@ -2528,6 +3313,11 @@
+
+
+
+
+
@@ -2543,6 +3333,11 @@
+
+
+
+
+
@@ -2558,6 +3353,11 @@
+
+
+
+
+
@@ -2603,26 +3403,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -2633,16 +3458,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -2693,6 +3533,11 @@
+
+
+
+
+
@@ -2838,6 +3683,11 @@
+
+
+
+
+
@@ -2858,6 +3708,11 @@
+
+
+
+
+
@@ -2868,6 +3723,11 @@
+
+
+
+
+
@@ -2878,6 +3738,11 @@
+
+
+
+
+
@@ -3298,6 +4163,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -3318,16 +4193,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -3343,11 +4233,21 @@
+
+
+
+
+
+
+
+
+
+
@@ -3363,6 +4263,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -3393,6 +4303,11 @@
+
+
+
+
+
@@ -3593,6 +4508,11 @@
+
+
+
+
+
@@ -3608,6 +4528,11 @@
+
+
+
+
+
@@ -3623,6 +4548,11 @@
+
+
+
+
+
@@ -3638,16 +4568,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -3663,6 +4608,11 @@
+
+
+
+
+
@@ -3678,6 +4628,11 @@
+
+
+
+
+
@@ -3693,6 +4648,11 @@
+
+
+
+
+
@@ -3708,6 +4668,11 @@
+
+
+
+
+
@@ -3723,6 +4688,11 @@
+
+
+
+
+
@@ -3738,6 +4708,11 @@
+
+
+
+
+
@@ -3753,6 +4728,11 @@
+
+
+
+
+
@@ -3768,6 +4748,11 @@
+
+
+
+
+
@@ -3783,6 +4768,11 @@
+
+
+
+
+
@@ -3798,6 +4788,11 @@
+
+
+
+
+
@@ -3813,6 +4808,11 @@
+
+
+
+
+
@@ -3828,6 +4828,11 @@
+
+
+
+
+
@@ -3843,6 +4848,11 @@
+
+
+
+
+
@@ -3858,6 +4868,11 @@
+
+
+
+
+
@@ -3873,6 +4888,11 @@
+
+
+
+
+
@@ -3888,6 +4908,11 @@
+
+
+
+
+
@@ -3903,6 +4928,11 @@
+
+
+
+
+
@@ -3918,6 +4948,11 @@
+
+
+
+
+
@@ -3933,6 +4968,11 @@
+
+
+
+
+
@@ -3948,6 +4988,11 @@
+
+
+
+
+
@@ -3963,6 +5008,11 @@
+
+
+
+
+
@@ -3978,6 +5028,11 @@
+
+
+
+
+
@@ -3993,6 +5048,11 @@
+
+
+
+
+
@@ -4008,6 +5068,11 @@
+
+
+
+
+
@@ -4023,6 +5088,11 @@
+
+
+
+
+
@@ -4038,6 +5108,11 @@
+
+
+
+
+
@@ -4818,6 +5893,11 @@
+
+
+
+
+
@@ -4828,6 +5908,11 @@
+
+
+
+
+
@@ -4838,6 +5923,11 @@
+
+
+
+
+
@@ -4848,6 +5938,11 @@
+
+
+
+
+
@@ -4858,6 +5953,11 @@
+
+
+
+
+
@@ -4868,6 +5968,11 @@
+
+
+
+
+
@@ -4878,6 +5983,11 @@
+
+
+
+
+
@@ -4888,11 +5998,21 @@
+
+
+
+
+
+
+
+
+
+
@@ -4903,6 +6023,11 @@
+
+
+
+
+
@@ -4918,6 +6043,11 @@
+
+
+
+
+
@@ -4928,6 +6058,11 @@
+
+
+
+
+
@@ -4938,6 +6073,11 @@
+
+
+
+
+
@@ -4948,6 +6088,11 @@
+
+
+
+
+
@@ -4963,6 +6108,11 @@
+
+
+
+
+
@@ -4973,6 +6123,11 @@
+
+
+
+
+
@@ -4983,6 +6138,11 @@
+
+
+
+
+
@@ -4993,6 +6153,11 @@
+
+
+
+
+
@@ -5003,6 +6168,11 @@
+
+
+
+
+
@@ -5013,6 +6183,11 @@
+
+
+
+
+
@@ -5023,6 +6198,11 @@
+
+
+
+
+
@@ -5033,6 +6213,11 @@
+
+
+
+
+
@@ -5043,6 +6228,11 @@
+
+
+
+
+
@@ -5063,6 +6253,11 @@
+
+
+
+
+
@@ -5073,6 +6268,11 @@
+
+
+
+
+
@@ -5118,6 +6318,11 @@
+
+
+
+
+
@@ -5133,6 +6338,11 @@
+
+
+
+
+
@@ -5143,6 +6353,11 @@
+
+
+
+
+
@@ -5153,6 +6368,11 @@
+
+
+
+
+
@@ -5163,6 +6383,11 @@
+
+
+
+
+
@@ -5173,6 +6398,11 @@
+
+
+
+
+
@@ -5183,6 +6413,11 @@
+
+
+
+
+
@@ -5193,6 +6428,11 @@
+
+
+
+
+
@@ -5257,6 +6497,14 @@
+
+
+
+
+
+
+
+
@@ -5348,6 +6596,14 @@
+
+
+
+
+
+
+
+
@@ -5358,6 +6614,11 @@
+
+
+
+
+
@@ -5368,6 +6629,11 @@
+
+
+
+
+
@@ -5378,6 +6644,11 @@
+
+
+
+
+
@@ -5388,6 +6659,11 @@
+
+
+
+
+
@@ -5398,6 +6674,11 @@
+
+
+
+
+
@@ -5413,6 +6694,11 @@
+
+
+
+
+
@@ -5448,6 +6734,11 @@
+
+
+
+
+
@@ -5478,11 +6769,21 @@
+
+
+
+
+
+
+
+
+
+
@@ -5843,6 +7144,11 @@
+
+
+
+
+
@@ -5853,6 +7159,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
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
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/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
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..6c49e69ad 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 =
@@ -530,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
}
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
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
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
}
}
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