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