Merge branch '2.0' into 'master'

Release 2.0-alpha10

Closes #3278, #3276, #3273, #3269, #3254, and #3284

See merge request fdroid/fdroidclient!1673
This commit is contained in:
linsui
2026-06-04 14:37:00 +00:00
183 changed files with 3008 additions and 720 deletions

View File

@@ -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 "<item>${CI_PROJECT_PATH}-nightly</item>" >> legacy/src/main/res/values/default_repos.xml
- echo "<item>${CI_PROJECT_URL}-nightly/-/raw/master/fdroid/repo</item>" >> 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

View File

@@ -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)

View File

@@ -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,

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -36,6 +36,7 @@
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
<uses-permission-sdk-23
android:name="android.permission.ACCESS_COARSE_LOCATION"

View File

@@ -78,7 +78,7 @@ public class BluetoothManager {
* so make sure {@link BroadcastReceiver}s handle duplicates.
*/
public static void start(final Context context) {
if (checkSelfPermission(context, BLUETOOTH_CONNECT) != PERMISSION_GRANTED &&
if (checkSelfPermission(context, BLUETOOTH_CONNECT) != PERMISSION_GRANTED ||
checkSelfPermission(context, BLUETOOTH_SCAN) != PERMISSION_GRANTED) {
// TODO we either throw away that Bluetooth code or properly request permissions
return;

View File

@@ -1,5 +1,8 @@
package org.fdroid.fdroid.nearby;
import static android.Manifest.permission.ACCESS_LOCAL_NETWORK;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Build.VERSION.SDK_INT;
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
import static org.fdroid.fdroid.nearby.SwapService.ACTION_REQUEST_SWAP;
@@ -149,6 +152,11 @@ public class SwapWorkflowActivity extends AppCompatActivity {
if (isGranted) sendFDroidBluetooth();
});
private final ActivityResultLauncher<String> 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;

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -161,6 +161,14 @@
<action android:name="android.intent.action.UNARCHIVE_PACKAGE" />
</intent-filter>
</receiver>
<receiver
android:name="org.fdroid.install.AppUpdateReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@@ -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",

View File

@@ -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",

View File

@@ -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()
}
}

View File

@@ -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

View File

@@ -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<String>,
ipv6Addresses: List<String>
ipv6Addresses: List<String>,
) {
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)
}

View File

@@ -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

View File

@@ -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()
}
}

View File

@@ -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) } }
}
}
}

View File

@@ -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 {

View File

@@ -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.
*

View File

@@ -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) {

View File

@@ -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"

View File

@@ -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

View File

@@ -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()

View File

@@ -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(),
)
),

View File

@@ -113,11 +113,22 @@ fun MyAppsPresenter(
private fun <T : MyAppItem> List<T>.sort(sortOrder: AppListSortOrder): List<T> {
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
}
}
}
}
}

View File

@@ -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()

View File

@@ -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 = {

View File

@@ -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,
)
}

View File

@@ -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

View File

@@ -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 ->

View File

@@ -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, {}, {}) } }
}

View File

@@ -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))
}
}
}

View File

@@ -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 {

View File

@@ -28,7 +28,7 @@ import org.fdroid.ui.utils.testApp
@Composable
fun AppDetailsMenu(
item: AppDetailsItem,
item: LoadedAppDetailsItem,
uninstallLauncher: ActivityResultLauncher<Intent>,
onDismiss: () -> Unit,
) {

View File

@@ -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,

View File

@@ -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()

View File

@@ -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

View File

@@ -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<App?>(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<List<AppVersion>?>(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<Repository?>(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
},
)
},

View File

@@ -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)] =

View File

@@ -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),

View File

@@ -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

View File

@@ -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)),

View File

@@ -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()) {

View File

@@ -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)
}
)

View File

@@ -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,

View File

@@ -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) {

View File

@@ -0,0 +1,7 @@
package org.fdroid.utils
sealed class Loadable<T>
class Loading<T> : Loadable<T>()
class Loaded<T>(val value: T) : Loadable<T>()

View File

@@ -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

View File

@@ -695,7 +695,6 @@
<string name="category_group_storage">الملفات والتخزين</string>
<string name="category_group_interests">اهتمامات</string>
<string name="category_group_misc">متنوّع</string>
<string name="author_by">بواسطة %1$s</string>
<string name="last_updated">آخر تحديث: %1$s</string>
<string name="last_updated_with_size">آخر تحديث: %1$s (%2$s)</string>
<string name="status_install_preparing">يجهز التثبيت…</string>

View File

@@ -672,7 +672,6 @@
<string name="category_group_storage">Файлы і Сховішча</string>
<string name="category_group_interests">Інтарэсы</string>
<string name="category_group_misc">Розныя</string>
<string name="author_by">Ад %1$s</string>
<string name="last_updated">Абноўлена: %1$s</string>
<string name="last_updated_with_size">Абноўлена: %1$s (%2$s)</string>
<string name="status_install_preparing">Падрыхтоўка ўсталёўкі…</string>

View File

@@ -639,7 +639,6 @@
<string name="category_group_storage">Файлове и хранилища</string>
<string name="category_group_interests">Интереси</string>
<string name="category_group_misc">Разни</string>
<string name="author_by">От %1$s</string>
<string name="last_updated">Последно обновяване: %1$s</string>
<string name="last_updated_with_size">Последно обновяване: %1$s (%2$s)</string>
<string name="status_install_preparing">Подготовка за инсталиране…</string>

View File

@@ -650,7 +650,6 @@
<string name="category_group_storage">Arxius i emmagatzematge</string>
<string name="category_group_interests">Interessos</string>
<string name="category_group_misc">Miscel·lània</string>
<string name="author_by">Per %1$s</string>
<string name="last_updated">Últimes actualitzacions: %1$s</string>
<string name="last_updated_with_size">Últimes actualitzacions: %1$s (%2$s)</string>
<string name="status_install_preparing">Preparant la instal·lació…</string>

View File

@@ -653,7 +653,6 @@
<string name="category_group_storage">Soubory a úložiště</string>
<string name="category_group_interests">Zájmy</string>
<string name="category_group_misc">Různé</string>
<string name="author_by">Od %1$s</string>
<string name="last_updated">Naposledy aktualizováno: %1$s</string>
<string name="last_updated_with_size">Naposledy aktualizováno: %1$s (%2$s)</string>
<string name="status_install_preparing">Příprava instalace…</string>

View File

@@ -624,7 +624,6 @@
<string name="app_list_new">Nye apps</string>
<string name="my_apps_export_installed_apps">Eksportér appliste</string>
<string name="category_group_tools">Værktøjer</string>
<string name="author_by">Af %1$s</string>
<string name="category_group_communication">Kommunikation</string>
<string name="category_group_network">Netværk</string>
<string name="donate_title">Donér</string>

View File

@@ -649,7 +649,6 @@
<string name="category_group_storage">Dateien und Speicher</string>
<string name="category_group_interests">Interessen</string>
<string name="category_group_misc">Verschiedenes</string>
<string name="author_by">Von %1$s</string>
<string name="last_updated">Zuletzt aktualisiert: %1$s</string>
<string name="last_updated_with_size">Zuletzt aktualisiert: %1$s (%2$s)</string>
<string name="status_install_preparing">Installation wird vorbereitet </string>

View File

@@ -638,7 +638,6 @@
<string name="category_group_storage">Αρχεία &amp; Αποθηκευτικός Χώρος</string>
<string name="category_group_interests">Ενδιαφέροντα</string>
<string name="category_group_misc">Διάφορα</string>
<string name="author_by">Από %1$s</string>
<string name="last_updated">Τελευταία ενημέρωση: %1$s</string>
<string name="last_updated_with_size">Τελευταία ενημέρωση: %1$s (%2$s)</string>
<string name="status_install_preparing">Προετοιμασία εγκατάστασης…</string>

View File

@@ -658,7 +658,6 @@
<string name="category_group_storage">Files &amp; Storage</string>
<string name="category_group_interests">Interests</string>
<string name="category_group_misc">Miscellaneous</string>
<string name="author_by">By %1$s</string>
<string name="last_updated">Last updated: %1$s</string>
<string name="last_updated_with_size">Last updated: %1$s (%2$s)</string>
<string name="status_install_preparing">Preparing installation…</string>

View File

@@ -638,7 +638,6 @@
<string name="category_group_storage">Dosieroj kaj konservado</string>
<string name="category_group_interests">Interesiĝoj</string>
<string name="category_group_misc">Diversaĵoj</string>
<string name="author_by">Fare de %1$s</string>
<string name="last_updated">Lasta ĝisdatigo: %1$s</string>
<string name="last_updated_with_size">Lasta ĝisdatigo: %1$s (%2$s)</string>
<string name="status_install_preparing">Preparado de instalado…</string>

View File

@@ -676,7 +676,6 @@
<string name="category_group_storage">Archivos y Almacenamiento</string>
<string name="category_group_interests">Intereses</string>
<string name="category_group_misc">Misceláneo</string>
<string name="author_by">Por %1$s</string>
<string name="last_updated">Últimamente actualizado: %1$s</string>
<string name="last_updated_with_size">Últimamente actualizado: %1$s (%2$s)</string>
<string name="status_install_preparing">Preparando instalación…</string>

View File

@@ -652,7 +652,6 @@
<string name="category_group_storage">Failid ja andmeruum</string>
<string name="category_group_interests">Huvid ja hobid</string>
<string name="category_group_misc">Varia</string>
<string name="author_by">Arendajalt %1$s</string>
<string name="last_updated">Viimati uuendatud: %1$s</string>
<string name="last_updated_with_size">Viimati uuendatud: %1$s (%2$s)</string>
<string name="status_install_preparing">Valmistun paigaldama…</string>

View File

@@ -664,7 +664,6 @@
<string name="category_group_storage">Fitxategiak eta biltegiratzea</string>
<string name="category_group_interests">Interesak</string>
<string name="category_group_misc">Denetarik</string>
<string name="author_by">%1$s-ek egina</string>
<string name="last_updated">Azken eguneraketa: %1$s</string>
<string name="last_updated_with_size">Azken eguneraketa: %1$s (%2$s)</string>
<string name="status_install_preparing">Instalazioa prestatzen…</string>

View File

@@ -640,7 +640,6 @@
<string name="category_group_network">شبکه</string>
<string name="category_group_storage">پرونده‌ها و ذخیره‌سازی</string>
<string name="category_group_misc">متفرّقه</string>
<string name="author_by">به دست %1$s</string>
<string name="last_updated">آخرین به‌روز رسانی: %1$s</string>
<string name="last_updated_with_size">آخرین به‌روز رسانی: %1$s (%2$s)</string>
<string name="status_install_preparing">آماده سازی نصب…</string>

View File

@@ -625,7 +625,6 @@
<string name="category_group_network">Verkko</string>
<string name="category_group_storage">Tiedostot ja tallennus</string>
<string name="category_group_misc">Sekalaiset</string>
<string name="author_by">Tekijältä %1$s</string>
<string name="last_updated">Viimeksi päivitetty: %1$s</string>
<string name="last_updated_with_size">Viimeksi päivitetty: %1$s (%2$s)</string>
<string name="status_install_preparing">Valmistellaan asennusta…</string>

View File

@@ -666,7 +666,6 @@
<string name="category_group_storage">Fichiers &amp; Stockage</string>
<string name="category_group_interests">Intérêts</string>
<string name="category_group_misc">Divers</string>
<string name="author_by">De %1$s</string>
<string name="last_updated">Dernière mise à jour : %1$s</string>
<string name="last_updated_with_size">Dernière mise à jour : %1$s (%2$s)</string>
<string name="status_install_preparing">Préparation de l\'installation…</string>

View File

@@ -695,7 +695,6 @@
<string name="category_group_storage">Comhaid &amp; Stóráil</string>
<string name="category_group_interests">Leasanna</string>
<string name="category_group_misc">Ilghnéitheach</string>
<string name="author_by">Faoi %1$s</string>
<string name="last_updated">Nuashonraithe go deireanach: %1$s</string>
<string name="last_updated_with_size">Nuashonraithe go deireanach: %1$s (%2$s)</string>
<string name="status_install_preparing">Ag ullmhú suiteála…</string>

View File

@@ -684,7 +684,6 @@
<string name="category_group_storage">קבצים ואחסון</string>
<string name="category_group_interests">תחומי עניין</string>
<string name="category_group_misc">שונות</string>
<string name="author_by">על ידי %1$s</string>
<string name="last_updated">עדכון אחרון: %1$s</string>
<string name="last_updated_with_size">עדכון אחרון: %1$s (%2$s)</string>
<string name="status_install_preparing">בהכנות להתקנה…</string>

View File

@@ -658,7 +658,6 @@
<string name="category_group_storage">Datoteke i spremišta</string>
<string name="category_group_interests">Interesi</string>
<string name="category_group_misc">Razno</string>
<string name="author_by">Autor: %1$s</string>
<string name="last_updated">Zadnje aktualiziranje: %1$s</string>
<string name="last_updated_with_size">Zadnje aktualiziranje: %1$s (%2$s)</string>
<string name="status_install_preparing">Pripremanje instalacije …</string>

View File

@@ -641,7 +641,6 @@
<string name="category_group_storage">Fájlok és tárolók</string>
<string name="category_group_interests">Érdeklődési kör</string>
<string name="category_group_misc">Egyéb</string>
<string name="author_by">%1$s -től</string>
<string name="last_updated">Utolsó frissítés: %1$s</string>
<string name="last_updated_with_size">Utolsó frissítés: %1$s (%2$s)</string>
<string name="status_install_preparing">Telepítés előkészítése…</string>

View File

@@ -644,7 +644,6 @@
<string name="category_group_storage">Fail &amp; Penyimpanan</string>
<string name="category_group_interests">Minat</string>
<string name="category_group_misc">Lain-Lain</string>
<string name="author_by">Oleh %1$s</string>
<string name="last_updated">Terakhir diperbarui: %1$s</string>
<string name="last_updated_with_size">Terakhir diperbarui: %1$s (%2$s)</string>
<string name="status_install_preparing">Mempersiapkan instalasi…</string>

View File

@@ -640,7 +640,6 @@
<string name="category_group_storage">Skrár og geymslurými</string>
<string name="category_group_interests">Áhugamál</string>
<string name="category_group_misc">Ýmislegt</string>
<string name="author_by">Frá %1$s</string>
<string name="last_updated">Síðast uppfært: %1$s</string>
<string name="last_updated_with_size">Síðast uppfært: %1$s (%2$s)</string>
<string name="status_install_preparing">Undirbý uppsetningu…</string>

View File

@@ -664,7 +664,6 @@
<string name="category_group_storage">Archiviazione</string>
<string name="category_group_interests">Interessi</string>
<string name="category_group_misc">Miscellanea</string>
<string name="author_by">Da %1$s</string>
<string name="last_updated">Ultimo aggiornamento: %1$s</string>
<string name="last_updated_with_size">Ultimo aggiornamento: %1$s (%2$s)</string>
<string name="status_install_preparing">Preparando l\'installazione…</string>

View File

@@ -645,7 +645,6 @@
<string name="category_group_storage">ファイル&ストレージ</string>
<string name="category_group_interests">趣味</string>
<string name="category_group_misc">その他</string>
<string name="author_by">%1$s による</string>
<string name="last_updated">最終更新: %1$s</string>
<string name="last_updated_with_size">最終更新: %1$s (%2$s)</string>
<string name="status_install_preparing">インストールの準備中…</string>

View File

@@ -419,7 +419,6 @@
<string name="tts_author_name">Ameskar %1$s</string>
<string name="app_list_new">Isnasen imaynuten</string>
<string name="app_list_all">Akk isnasen</string>
<string name="author_by">Sɣur %1$s</string>
<string name="status_install_preparing">Aheyyi n usbeddi…</string>
<string name="whats_new_title">D acu i yellan d amaynut</string>
<string name="copy_link">Nɣel aseɣwen</string>

View File

@@ -630,7 +630,6 @@
<string name="category_group_storage">파일 &amp; 저장소</string>
<string name="category_group_interests">관심분야</string>
<string name="category_group_misc">기타</string>
<string name="author_by">개발자: %1$s</string>
<string name="last_updated">마지막 업데이트: %1$s</string>
<string name="last_updated_with_size">마지막 업데이트: %1$s (%2$s)</string>
<string name="status_install_preparing">설치 준비중…</string>

View File

@@ -611,7 +611,6 @@
<string name="category_group_storage">Failai ir saugykla</string>
<string name="category_group_interests">Pomėgiai</string>
<string name="category_group_misc">Kiti</string>
<string name="author_by">Sukurta %1$s</string>
<string name="last_updated">Paskutinį kartą atnaujinta: %1$s</string>
<string name="last_updated_with_size">Paskutinį kartą atnaujinta: %1$s (%2$s)</string>
<string name="status_install_preparing">Įdiegimas ruošiamas…</string>

View File

@@ -649,7 +649,6 @@
<string name="category_group_network">Tīkls</string>
<string name="category_group_storage">Datnes un krātuve</string>
<string name="category_group_misc">Dažādi</string>
<string name="author_by">No %1$s</string>
<string name="last_updated">Pēdējoreiz atjaunināta: %1$s</string>
<string name="last_updated_with_size">Pēdējoreiz atjaunināta: %1$s (%2$s)</string>
<string name="status_install_preparing">Sagatavo uzstādīšanu…</string>

View File

@@ -307,7 +307,6 @@
<string name="category_group_storage">Датотеки и меморија</string>
<string name="category_group_interests">Интереси</string>
<string name="category_group_misc">Останато</string>
<string name="author_by">Направено од %1$s</string>
<string name="last_updated">Последно надоградено: %1$s</string>
<string name="last_updated_with_size">Последно надоградено: %1$s­%2$s</string>
<string name="status_install_preparing">Подготовка на инсталација…</string>

View File

@@ -655,7 +655,6 @@
<string name="category_group_storage">Filer &amp; Lagring</string>
<string name="category_group_interests">Interesser</string>
<string name="category_group_misc">Diverse</string>
<string name="author_by">Fra %1$s</string>
<string name="last_updated">Sist oppdatert: %1$s</string>
<string name="last_updated_with_size">Sist oppdatert: %1$s (%2$s)</string>
<string name="status_install_preparing">Forbereder installasjon…</string>

View File

@@ -646,7 +646,6 @@
<string name="category_group_storage">Bestanden &amp; Opslag</string>
<string name="category_group_interests">Interesses</string>
<string name="category_group_misc">Diversen</string>
<string name="author_by">Door %1$s</string>
<string name="last_updated">Laatst geüpdatet: %1$s</string>
<string name="last_updated_with_size">Laatst geüpdatet: %1$s (%2$s)</string>
<string name="status_install_preparing">Installatie voorbereiden…</string>

View File

@@ -684,7 +684,6 @@
<string name="category_group_storage">Pliki i pamięć</string>
<string name="category_group_interests">Zainteresowania</string>
<string name="category_group_misc">Różne</string>
<string name="author_by">Przez %1$s</string>
<string name="last_updated">Ostatnia aktualizacja: %1$s</string>
<string name="last_updated_with_size">Ostatnia aktualizacja: %1$s (%2$s)</string>
<string name="status_install_preparing">Przygotowywanie instalacji…</string>

View File

@@ -654,7 +654,6 @@
<string name="category_group_device">Dispositivo</string>
<string name="category_group_storage">Arquivos &amp; Armazenamento</string>
<string name="category_group_interests">Interesses</string>
<string name="author_by">Por %1$s</string>
<string name="last_updated_with_size">Última atualização: %1$s (%2$s)</string>
<string name="status_install_preparing">Preparando instalação…</string>
<string name="whats_new_title">O que há de novo</string>

View File

@@ -651,7 +651,6 @@
<string name="category_group_storage">Ficheiros &amp; armazenamento</string>
<string name="category_group_interests">Interesses</string>
<string name="category_group_misc">Diversos</string>
<string name="author_by">Por %1$s</string>
<string name="last_updated">Última atualização: %1$s</string>
<string name="last_updated_with_size">Última atualização: %1$s (%2$s)</string>
<string name="status_install_preparing">A preparar instalação…</string>

View File

@@ -665,7 +665,6 @@
<string name="category_group_storage">Ficheiros &amp; armazenamento</string>
<string name="category_group_interests">Interesses</string>
<string name="category_group_misc">Diversos</string>
<string name="author_by">Por %1$s</string>
<string name="last_updated">Última atualização: %1$s</string>
<string name="last_updated_with_size">Última atualização: %1$s (%2$s)</string>
<string name="status_install_preparing">A preparar instalação…</string>

View File

@@ -661,7 +661,6 @@
<string name="category_group_storage">Fișiere și stocare</string>
<string name="category_group_interests">Pasiuni</string>
<string name="category_group_misc">Diverse</string>
<string name="author_by">De către %1$s</string>
<string name="last_updated">Ultima actualizare: %1$s</string>
<string name="last_updated_with_size">Ultima actualizare: %1$s (%2$s)</string>
<string name="status_install_preparing">Pregătire instalare…</string>

View File

@@ -665,7 +665,6 @@
<string name="category_group_storage">Файлы и хранилище</string>
<string name="category_group_interests">Интересы</string>
<string name="category_group_misc">Разное</string>
<string name="author_by">От %1$s</string>
<string name="last_updated">Последнее обновление: %1$s</string>
<string name="last_updated_with_size">Последнее обновление: %1$s (%2$s)</string>
<string name="status_install_preparing">Подготовка к установке…</string>

View File

@@ -589,7 +589,6 @@
<string name="category_group_storage">Súbory a uložisko</string>
<string name="category_group_interests">Záujmy</string>
<string name="category_group_misc">Rôzne</string>
<string name="author_by">Od %1$s</string>
<string name="last_updated">Naposledy aktualizované: %1$s</string>
<string name="last_updated_with_size">Naposledy aktualizované: %1$s (%2$s)</string>
<string name="status_install_preparing">Príprava inštalácie…</string>

View File

@@ -563,7 +563,6 @@
<string name="category_group_storage">Datoteke in shranjevanje</string>
<string name="category_group_interests">Interesi</string>
<string name="category_group_misc">Razno</string>
<string name="author_by">Od %1$s</string>
<string name="last_updated">Zadnja posodobitev: %1$s</string>
<string name="last_updated_with_size">Zadnja posodobitev: %1$s (%2$s)</string>
<string name="status_install_preparing">Priprava namestitve …</string>

View File

@@ -637,7 +637,6 @@
<string name="category_group_storage">Kartela &amp; Depozitim</string>
<string name="category_group_interests">Interesa</string>
<string name="category_group_misc">Të ndryshme</string>
<string name="author_by">Nga %1$s</string>
<string name="last_updated">Përditësuar së fundi më: %1$s</string>
<string name="anti_features_title">Ky aplikacion përmban anti-veçori</string>
<string name="developer_contact">Kontakt zhvilluesi</string>

View File

@@ -637,7 +637,6 @@
<string name="category_group_storage">Датотеке и складиште</string>
<string name="category_group_interests">Интересовања</string>
<string name="category_group_misc">Разно</string>
<string name="author_by">Од %1$s</string>
<string name="last_updated">Последње ажурирање: %1$s</string>
<string name="last_updated_with_size">Последње ажурирано: %1$s (%2$s)</string>
<string name="status_install_preparing">Припрема се инсталација…</string>

View File

@@ -655,7 +655,6 @@
<string name="category_group_storage">Filer &amp; lagring</string>
<string name="category_group_interests">Intressen</string>
<string name="category_group_misc">Diverse</string>
<string name="author_by">Av %1$s</string>
<string name="last_updated">Senast uppdaterad: %1$s</string>
<string name="last_updated_with_size">Senast uppdaterad: %1$s (%2$s)</string>
<string name="status_install_preparing">Förbereder installationen…</string>

View File

@@ -624,7 +624,6 @@
<string name="app_list_most_downloaded">Iliyopakuliwa zaidi</string>
<string name="app_list_author">Programu na %s</string>
<string name="search_no_results">Hakuna programu zilizopatikana\n\nJaribu kutumia maneno machache ya utafutaji au uongeze hazina zaidi</string>
<string name="author_by">Na %1$s</string>
<string name="last_updated_with_size">Ilisasishwa mwisho: %1$s (%2$s)</string>
<string name="status_install_preparing">Inatayarisha usakinishaji…</string>
<string name="anti_features_title">Programu hii ina vipengele vya kupinga</string>

View File

@@ -636,7 +636,6 @@
<string name="category_group_media">Eğlence &amp; Medya</string>
<string name="category_group_communication">İletişim</string>
<string name="category_group_interests">İlgi Alanları</string>
<string name="author_by">%1$s tarafından</string>
<string name="app_name_full_nightly">F-Nightly</string>
<string name="app_name_basic_nightly">F-Nightly Temel</string>
<string name="first_start_loading">Uygulama depoları getiriliyor…</string>

View File

@@ -666,7 +666,6 @@
<string name="category_group_storage">Файли та зберігання</string>
<string name="category_group_interests">Інтереси</string>
<string name="category_group_misc">Різне</string>
<string name="author_by">Від %1$s</string>
<string name="last_updated">Останні оновлено: %1$s</string>
<string name="last_updated_with_size">Останні оновлено: %1$s (%2$s)</string>
<string name="status_install_preparing">Підготовка до встановлення…</string>

View File

@@ -613,7 +613,6 @@
<string name="category_group_storage">Tệp &amp; Lưu trữ</string>
<string name="category_group_interests">Sở thích</string>
<string name="category_group_misc">Linh tinh</string>
<string name="author_by">Bởi %1$s</string>
<string name="last_updated">Lần cuối cập nhật: %1$s</string>
<string name="last_updated_with_size">Lần cuối cập nhật: %1$s (%2$s)</string>
<string name="status_install_preparing">Chuẩn bị cài đặt…</string>

View File

@@ -638,7 +638,6 @@
<string name="category_group_storage">文件和存储</string>
<string name="category_group_interests">兴趣</string>
<string name="category_group_misc">杂项</string>
<string name="author_by">由 %1$s 开发</string>
<string name="last_updated">上次更新:%1$s</string>
<string name="last_updated_with_size">上次更新:%1$s (%2$s)</string>
<string name="status_install_preparing">正在准备安装…</string>

View File

@@ -636,7 +636,6 @@
<string name="last_updated_with_size">最後更新: %1$s (%2$s)</string>
<string name="status_install_preparing">正在準備安裝…</string>
<string name="whats_new_title">有什麼新事物</string>
<string name="author_by">由 %1$s 提供</string>
<string name="donate_title">課金</string>
<string name="anti_features_title">此應用程式包含負面功能</string>
<string name="developer_contact">聯絡開發者</string>

View File

@@ -22,6 +22,7 @@
<!-- The %1$s placeholder will be replaced by the app name, e.g. "F-Droid" -->
<string name="my_apps_empty">Only apps available in %1$s\'s repositories are shown here</string>
<string name="my_apps_header_recently_installed_apps">Recently installed</string>
<string name="my_apps_header_apps_with_issue">Apps with issues</string>
<string name="my_apps_ignore_dialog_title">Hide issue</string>
<string name="my_apps_ignore_dialog_text">The issue with this version of \"%1$s\" will get ignored</string>
@@ -58,32 +59,33 @@
<string name="onboarding_app_list_filter_message">Use filters to only show apps from specific categories or repositories. You can also change the sort order.</string>
<string name="got_it">Got it</string>
<string name="clear">Clear</string>
<string name="clear_all">Clear all</string>
<string name="hide">Hide</string>
<string name="category_group_summary_productivity">Calendars, tasks, clocks &amp; notes</string>
<string name="category_group_summary_tools">Navigation, weather, calculators &amp; converters</string>
<string name="category_group_summary_wallets">Finance, money &amp; pass managers</string>
<string name="category_group_summary_media">Music, podcasts, games &amp; ebook readers</string>
<string name="category_group_summary_communication">Email, messaging &amp; social networks</string>
<string name="category_group_summary_device">Launcher, keyboard, security &amp; theming</string>
<string name="category_group_summary_network">Browser, VPNs, proxies &amp; firewall</string>
<string name="category_group_summary_storage">Files, gallery, cloud sync &amp; encryption</string>
<string name="category_group_summary_interests">Science, sports, drawing &amp; recipes</string>
<string name="category_group_summary_misc">Uncategorized</string>
<string name="category_group_productivity">Productivity</string>
<string name="category_group_tools">Tools</string>
<string name="category_group_wallets">Finances &amp; Wallets</string>
<string name="category_group_media">Entertainment &amp; Media</string>
<string name="category_group_communication">Communication</string>
<string name="category_group_device">Device</string>
<string name="category_group_games">Games</string>
<string name="category_group_network">Network</string>
<string name="category_group_storage">Files &amp; Storage</string>
<string name="category_group_interests">Interests</string>
<string name="category_group_misc">Miscellaneous</string>
<!-- Used in app details page to show who is the author of the app -->
<string name="author_by">By %1$s</string>
<string name="category_group_summary_productivity">Calendars, tasks, clocks &amp; notes</string>
<string name="category_group_summary_tools">Navigation, weather, calculators &amp; converters</string>
<string name="category_group_summary_wallets">Finance, money &amp; pass managers</string>
<string name="category_group_summary_media">Music, podcasts &amp; ebook readers</string>
<string name="category_group_summary_communication">Email, messaging &amp; social networks</string>
<string name="category_group_summary_device">Launcher, keyboard, security &amp; theming</string>
<string name="category_group_summary_games">Board, card &amp; party games</string>
<string name="category_group_summary_network">Browser, VPNs, proxies &amp; firewall</string>
<string name="category_group_summary_storage">Files, gallery, cloud sync &amp; encryption</string>
<string name="category_group_summary_interests">Science, sports, drawing &amp; recipes</string>
<string name="category_group_summary_misc">Uncategorized</string>
<string name="last_updated">Last updated: %1$s</string>
<!-- The placeholder in round brackets will be replaced with the size of the app -->
<string name="last_updated_with_size">Last updated: %1$s (%2$s)</string>
@@ -102,6 +104,7 @@
</string>
<string name="installIncompatible">App incompatible with your device, install anyway?</string>
<string name="version">Version</string>
<!-- Used in app details page to show who is the author of the app -->
<string name="by_author_format">by %s</string>
<string name="delete">Delete</string>
<string name="prompt_to_send_crash_reports">Prompt to send crash reports</string>
@@ -183,6 +186,7 @@
<string name="pref_language_summary">Open system language settings</string>
<string name="pref_auto_updates_only_wifi">Only on Wi-Fi</string>
<string name="pref_auto_updates_only_always">Always (even on mobile data)</string>
<string name="pref_auto_updates_only_only_when_open_app">Only when opening the app</string>
<string name="pref_auto_updates_only_never">Never</string>
<string name="pref_auto_updates_summary_only_wifi">Download and update apps daily when on Wi-Fi and the device isn\'t being used</string>
<string name="pref_auto_updates_summary_always">Download and update apps daily even on mobile data when the device isn\'t being used</string>
@@ -190,6 +194,7 @@
<string name="pref_repo_updates_title">Check for updates</string>
<string name="pref_repo_updates_summary_only_wifi">Periodically fetch app updates from repositories only when on Wi-Fi</string>
<string name="pref_repo_updates_summary_always">Periodically fetch app updates from repositories even when on mobile data</string>
<string name="pref_repo_updates_summary_only_when_open_app">Check for updates only when opened • Apps will become outdated</string>
<string name="pref_repo_updates_summary_never">Don\'t check for updates • Apps will become outdated</string>
<string name="pref_category_network">Network</string>
<string name="pref_mirror_chooser_title">Download mirror selection</string>
@@ -956,6 +961,8 @@ This often occurs with apps installed via Google Play or other sources, if they
<string name="notification_channel_install_success_description">Displays a notification after apps were installed automatically</string>
<string name="notification_channel_updates_available_title">Available app updates</string>
<string name="notification_channel_updates_available_description">Displays a notification after repositories were updated and app updates were found</string>
<string name="notification_channel_self_update_title">Self-update</string>
<string name="notification_channel_self_update_description">Notifies when the app updated itself and was closed, so you can launch it again easily</string>
<string name="notification_repo_update_default">Connecting to %1$s…</string>
<string name="notification_repo_update_downloading">Downloaded %1$s from %2$s</string>
@@ -979,6 +986,8 @@ This often occurs with apps installed via Google Play or other sources, if they
<string name="notification_installing_section_confirmation">Needs user confirmation:</string>
<string name="notification_installing_section_installed">Installed:</string>
<string name="notification_installing_confirmation">Tap to confirm</string>
<!-- The placeholder %s will be replaced with the name of this app -->
<string name="notification_self_update_title">%s was updated. Tap to open</string>
<!-- Used by the TTS engine when showing a filter "chip" in the search box -->
<string name="tts_category_name">Category %1$s</string>

View File

@@ -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,

View File

@@ -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))
}
}
}

View File

@@ -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 = {},
)
}

Some files were not shown because too many files have changed in this diff Show More