diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 82bdcb1d6..d0f5163a7 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -103,6 +103,7 @@ app assembleRelease test:
paths:
- app/build/reports
- app/build/outputs/apk
+ - app/build/outputs/mapping
- libs/*/build/reports
- legacy/build/reports
- legacy/build/outputs/apk
@@ -434,16 +435,4 @@ deploy_nightly:
- 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
-
- # taken from fdroiddata/.gitlab-ci.yml as a tmp workaround until this is released:
- # https://gitlab.com/fdroid/fdroidserver/-/merge_requests/1666
- - rm -rf $fdroidserver
- - mkdir $fdroidserver
- - git ls-remote https://gitlab.com/fdroid/fdroidserver.git master
- - curl --silent https://gitlab.com/fdroid/fdroidserver/-/archive/master/fdroidserver-master.tar.gz
- | tar -xz --directory=$fdroidserver --strip-components=1
- - export PATH="$fdroidserver:$PATH"
- - export PYTHONPATH="$fdroidserver:$fdroidserver/examples"
- - export PYTHONUNBUFFERED=true
-
- fdroid nightly -v
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 697dbc602..9ef3aafd3 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -18,8 +18,8 @@ android {
applicationId = "org.fdroid"
minSdk = 24
targetSdk = 36
- versionCode = 2000008
- versionName = "2.0-alpha8"
+ versionCode = 2000009
+ versionName = "2.0-alpha9"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 2d269191a..0d81ad1a6 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -1,4 +1,5 @@
-dontobfuscate
+-dontoptimize
-keepattributes SourceFile,LineNumberTable,Exceptions
# Anything less causes issues like not finding primary constructor in ReflectionDiffer
diff --git a/app/src/androidTest/java/org/fdroid/download/DnsCacheTest.kt b/app/src/androidTest/java/org/fdroid/download/DnsCacheTest.kt
index 13bdd9eff..ddad92633 100644
--- a/app/src/androidTest/java/org/fdroid/download/DnsCacheTest.kt
+++ b/app/src/androidTest/java/org/fdroid/download/DnsCacheTest.kt
@@ -9,7 +9,6 @@ import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
import org.fdroid.settings.SettingsManager
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@@ -82,7 +81,6 @@ class DnsCacheTest {
}
@Test
- @Ignore("failing") // TODO
fun preloadCacheTest() {
// test setup
settings.useDnsCache = true
diff --git a/app/src/androidTest/java/org/fdroid/ui/screenshots/MyAppsScreenshotTest.kt b/app/src/androidTest/java/org/fdroid/ui/screenshots/MyAppsScreenshotTest.kt
index 26b2c952a..07ceaefda 100644
--- a/app/src/androidTest/java/org/fdroid/ui/screenshots/MyAppsScreenshotTest.kt
+++ b/app/src/androidTest/java/org/fdroid/ui/screenshots/MyAppsScreenshotTest.kt
@@ -30,6 +30,7 @@ class MyAppsScreenshotTest(localeName: String) : LocalizedScreenshotTest(localeN
appUpdates = getUpdates(localeList),
installingApps = emptyList(),
installedApps = getInstalledApps(localeList),
+ showUpdatesHint = false,
showAppIssueHint = false,
networkState = NetworkState(isOnline = true, isMetered = false),
)
diff --git a/app/src/androidTest/java/org/fdroid/ui/screenshots/SearchScreenshotTest.kt b/app/src/androidTest/java/org/fdroid/ui/screenshots/SearchScreenshotTest.kt
index efba3c953..949e08ffc 100644
--- a/app/src/androidTest/java/org/fdroid/ui/screenshots/SearchScreenshotTest.kt
+++ b/app/src/androidTest/java/org/fdroid/ui/screenshots/SearchScreenshotTest.kt
@@ -7,6 +7,7 @@ import org.fdroid.ui.lists.AppListItem
import org.fdroid.ui.navigation.NavigationKey
import org.fdroid.ui.search.GlobalSearch
import org.fdroid.ui.search.SearchResults
+import org.fdroid.ui.utils.getSearchInfo
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@@ -28,14 +29,10 @@ class SearchScreenshotTest(localeName: String) : LocalizedScreenshotTest(localeN
apps = getSearchResultItems(localeList),
)
GlobalSearch(
+ info = getSearchInfo(results, emptyList()),
textFieldState = rememberTextFieldState("work"),
- searchResults = results,
- savedSearches = emptyList(),
- onSearch = {},
- onClearSavedSearches = {},
onNav = {},
onBack = {},
- onSearchCleared = {},
)
}
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 867162373..d6f6e0fc1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -25,6 +25,12 @@
+
+
+
+
+
+
+
+
+
+
= 35) caller.`package` else caller.toString()
+ "onNewIntent: $intent, caller: $callerInfo"
+ }
+ // if the app got killed and restarted via an intent,
+ // this may run before the IntentListener is attached, so it would miss this intent
+ if (SDK_INT >= 35) setIntent(intent, caller) else setIntent(intent)
+
+ // calling super seems to be needed, so the IntentListener gets informed
+ super.onNewIntent(intent, caller)
+ }
}
diff --git a/app/src/main/kotlin/org/fdroid/download/DnsWithCache.kt b/app/src/main/kotlin/org/fdroid/download/DnsWithCache.kt
index 04aaa068b..8dada7485 100644
--- a/app/src/main/kotlin/org/fdroid/download/DnsWithCache.kt
+++ b/app/src/main/kotlin/org/fdroid/download/DnsWithCache.kt
@@ -26,8 +26,8 @@ constructor(private val settingsManager: SettingsManager, private val cache: Dns
}
/**
- * these methods provide a way to pre-load the cache with values from other sources such as
- * the mirror info from the index, further reducing the needs to do DNS lookups
+ * these methods provide a way to preload the cache with values from other sources such as the
+ * mirror info from the index, further reducing the needs to do DNS lookups
*/
fun populateCacheWithStrings(hostname: String, ipv4List: List, ipv6List: List) {
val ipv4ConvertedList = stringListToIpList(ipv4List)
@@ -35,7 +35,11 @@ constructor(private val settingsManager: SettingsManager, private val cache: Dns
populateCacheWithIps(hostname, ipv4ConvertedList, ipv6ConvertedList)
}
- fun populateCacheWithIps(hostname: String, ipv4List: List, ipv6List: List) {
+ fun populateCacheWithIps(
+ hostname: String,
+ ipv4List: List,
+ ipv6List: List,
+ ) {
// at this time, the DNS cache only supports a single collection because that's what the DNS
// lookup method returns. since these values are being inserted manually, it's possible that
// the cache might end up with values that wouldn't have been returned by the lookup method.
@@ -55,12 +59,12 @@ constructor(private val settingsManager: SettingsManager, private val cache: Dns
try {
InetAddress.getByName(it)
} catch (e: UnknownHostException) {
- log.warn { "Unexpected format for IP address: $it" }
+ log.error(e) { "Unexpected format for IP address: $it" }
null
}
}
} catch (e: Exception) {
- log.warn { "Failed to parse list of IP addresses, returning empty list" }
+ log.error(e) { "Failed to parse list of IP addresses, returning empty list" }
return emptyList()
}
}
diff --git a/app/src/main/kotlin/org/fdroid/download/NetworkMonitor.kt b/app/src/main/kotlin/org/fdroid/download/NetworkMonitor.kt
index 3f4f648c1..af8b28058 100644
--- a/app/src/main/kotlin/org/fdroid/download/NetworkMonitor.kt
+++ b/app/src/main/kotlin/org/fdroid/download/NetworkMonitor.kt
@@ -9,11 +9,14 @@ import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.update
-import org.fdroid.settings.SettingsConstants.AutoUpdateValues
+import kotlinx.coroutines.launch
import org.fdroid.settings.SettingsManager
+import org.fdroid.utils.IoDispatcher
@Singleton
class NetworkMonitor
@@ -21,18 +24,19 @@ class NetworkMonitor
constructor(
@param:ApplicationContext private val context: Context,
private val settingsManager: SettingsManager,
+ @param:IoDispatcher private val coroutineScope: CoroutineScope,
) : ConnectivityManager.NetworkCallback() {
private val connectivityManager =
context.getSystemService(ConnectivityManager::class.java) as ConnectivityManager
- private val neverMetered
- get() = settingsManager.autoUpdateApps == AutoUpdateValues.Always
+ private val warnWhenMetered
+ get() = settingsManager.warnWhenMeteredFlow.value
private val _networkState =
MutableStateFlow(
connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)?.let {
- NetworkState(it, neverMetered)
- } ?: NetworkState(isOnline = false, isMetered = false)
+ NetworkState(it, warnWhenMetered)
+ } ?: NetworkState(isOnline = false, isMetered = false, warnWhenMetered = warnWhenMetered)
)
val networkState = _networkState.asStateFlow()
@@ -43,24 +47,41 @@ constructor(
* we are doing instead is simpler and seems to work better.
*/
connectivityManager.registerDefaultNetworkCallback(this)
+ // observe the setting for whether to warn when on a metered network
+ coroutineScope.launch {
+ // drop first emission as that is the initial value, and we only want to react to changes
+ settingsManager.warnWhenMeteredFlow.drop(1).collect { warnWhenMetered ->
+ if (!warnWhenMetered) {
+ _networkState.update { it.copy(warnWhenMetered = false) }
+ }
+ }
+ }
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
- _networkState.update { NetworkState(networkCapabilities, neverMetered) }
+ _networkState.update { NetworkState(networkCapabilities, warnWhenMetered) }
}
override fun onLost(network: Network) {
- _networkState.update { NetworkState(isOnline = false, isMetered = false) }
+ _networkState.update {
+ NetworkState(isOnline = false, isMetered = false, warnWhenMetered = warnWhenMetered)
+ }
}
}
-data class NetworkState(val isOnline: Boolean, val isMetered: Boolean) {
+data class NetworkState(
+ val isOnline: Boolean,
+ private val isMetered: Boolean,
+ val warnWhenMetered: Boolean = true,
+) {
+ val showWarningDialog: Boolean = isMetered && warnWhenMetered
+
constructor(
networkCapabilities: NetworkCapabilities,
- neverMetered: Boolean,
+ warnWhenMetered: Boolean,
) : this(
isOnline = networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET),
- isMetered =
- if (neverMetered) false else !networkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED),
+ isMetered = !networkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED),
+ warnWhenMetered = warnWhenMetered,
)
}
diff --git a/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt b/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt
index ea9b9edca..1495e8a9e 100644
--- a/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt
+++ b/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt
@@ -25,6 +25,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.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
@@ -122,7 +123,7 @@ constructor(
app: AppMetadata,
icon: Bitmap?,
canRequestUserConfirmationNow: Boolean,
- ): PreApprovalResult = suspendCancellableCoroutine { cont ->
+ ): PreApprovalResult = safeSuspendCoroutine { cont ->
val params = getSessionParams(app.packageName)
val sessionId = installer.createSession(params)
log.info { "Opened session $sessionId for ${app.packageName}" }
@@ -133,7 +134,7 @@ constructor(
when (status) {
PackageInstaller.STATUS_SUCCESS -> {
cont.resume(PreApprovalResult.Success(sessionId))
- context.unregisterReceiver(this)
+ unregisterReceiver(this)
}
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val flags = FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
@@ -149,13 +150,13 @@ constructor(
log.error(e) { "Error sending pre-approval intent: " }
val s = PreApprovalResult.UserConfirmationRequired(sessionId, pendingIntent)
cont.resume(s)
- context.unregisterReceiver(this)
+ unregisterReceiver(this)
}
} else {
log.info { "Can not ask pre-approval for ${app.packageName}: $intent" }
val s = PreApprovalResult.UserConfirmationRequired(sessionId, pendingIntent)
cont.resume(s)
- context.unregisterReceiver(this)
+ unregisterReceiver(this)
}
}
else -> {
@@ -166,14 +167,14 @@ constructor(
else -> PreApprovalResult.Error(msg)
}
cont.resume(result)
- context.unregisterReceiver(this)
+ unregisterReceiver(this)
}
}
}
registerReceiver(context, receiver, IntentFilter(ACTION_INSTALL), RECEIVER_NOT_EXPORTED)
cont.invokeOnCancellation {
log.info { "Pre-approval for ${app.packageName} cancelled." }
- context.unregisterReceiver(receiver)
+ unregisterReceiver(receiver)
}
installer.openSession(sessionId).use { session ->
@@ -197,7 +198,7 @@ constructor(
packageName: String,
state: InstallStateWithInfo,
apkFile: File,
- ): InstallState = suspendCancellableCoroutine { cont ->
+ ): InstallState = safeSuspendCoroutine { cont ->
val size = apkFile.length()
log.info { "Installing ${apkFile.name} with size $size bytes" }
@@ -213,12 +214,12 @@ constructor(
log.error(e) { "Error when creating session: " }
val s = InstallState.Error("${e::class.java.simpleName} ${e.message}", state)
cont.resume(s)
- return@suspendCancellableCoroutine
+ return@safeSuspendCoroutine
}
// set-up receiver for install result
val receiver =
receiverFactory.create(sessionId) { status, intent, msg ->
- context.unregisterReceiver(this)
+ unregisterReceiver(this)
when (status) {
PackageInstaller.STATUS_SUCCESS -> {
val newState =
@@ -278,7 +279,7 @@ constructor(
registerReceiver(context, receiver, IntentFilter(ACTION_INSTALL), RECEIVER_NOT_EXPORTED)
cont.invokeOnCancellation {
log.info { "App installation was cancelled, unregistering broadcast receiver..." }
- context.unregisterReceiver(receiver)
+ unregisterReceiver(receiver)
try {
installer.abandonSession(sessionId)
} catch (e: SecurityException) {
@@ -306,11 +307,11 @@ constructor(
}
suspend fun requestUserConfirmation(state: InstallConfirmationState): InstallState =
- suspendCancellableCoroutine { cont ->
+ safeSuspendCoroutine { cont ->
val isPreApproval = state is InstallState.PreApprovalConfirmationNeeded
val receiver =
receiverFactory.create(state.sessionId) { status, _, msg ->
- context.unregisterReceiver(this)
+ unregisterReceiver(this)
when (status) {
PackageInstaller.STATUS_SUCCESS -> {
val newState =
@@ -346,12 +347,12 @@ constructor(
}
}
registerReceiver(context, receiver, IntentFilter(ACTION_INSTALL), RECEIVER_NOT_EXPORTED)
- cont.invokeOnCancellation { context.unregisterReceiver(receiver) }
+ cont.invokeOnCancellation { unregisterReceiver(receiver) }
try {
state.intent.send()
} catch (e: Exception) {
log.error(e) { "Error sending user confirmation intent: " }
- context.unregisterReceiver(receiver)
+ unregisterReceiver(receiver)
cont.resume(InstallState.Error("${e::class.java.simpleName} ${e.message}", state))
}
}
@@ -423,4 +424,47 @@ constructor(
val pendingIntent = PendingIntent.getBroadcast(context, sessionId, broadcastIntent, flags)
return pendingIntent.intentSender
}
+
+ private fun unregisterReceiver(receiver: InstallBroadcastReceiver) {
+ try {
+ context.unregisterReceiver(receiver)
+ } catch (e: IllegalArgumentException) {
+ // Duplicate callbacks can race with unregistration
+ log.debug(e) { "Receiver already unregistered" }
+ }
+ }
+
+ /**
+ * A helper function to create a suspend function that can be resumed only once. This shouldn't be
+ * needed, but we have seen crash reports where we received [PackageInstaller.STATUS_SUCCESS] and
+ * then [PackageInstaller.STATUS_FAILURE_ABORTED] for the same sessionId.
+ */
+ private suspend fun safeSuspendCoroutine(block: (SafeContinuation) -> Unit): T =
+ suspendCancellableCoroutine { continuation ->
+ val safeCont =
+ object : SafeContinuation {
+ private val isCompleted = AtomicBoolean(false)
+ private var resumedValue: T? = null
+
+ override fun resume(value: T) {
+ if (isCompleted.compareAndSet(false, true)) {
+ resumedValue = value
+ continuation.resume(value)
+ } else {
+ log.warn { "Already resumed with $resumedValue, not trying to resume with $value" }
+ }
+ }
+
+ override fun invokeOnCancellation(handler: () -> Unit) {
+ continuation.invokeOnCancellation { handler() }
+ }
+ }
+ block(safeCont)
+ }
+
+ private interface SafeContinuation {
+ fun resume(value: T)
+
+ fun invokeOnCancellation(handler: () -> Unit)
+ }
}
diff --git a/app/src/main/kotlin/org/fdroid/repo/RepoUpdateWorker.kt b/app/src/main/kotlin/org/fdroid/repo/RepoUpdateWorker.kt
index 1e3e717f1..792dd4fa8 100644
--- a/app/src/main/kotlin/org/fdroid/repo/RepoUpdateWorker.kt
+++ b/app/src/main/kotlin/org/fdroid/repo/RepoUpdateWorker.kt
@@ -127,6 +127,7 @@ constructor(
}
try {
if (canStartForegroundService(applicationContext)) setForeground(getForegroundInfo())
+ else log.info { "Not using foreground service" }
} catch (e: Exception) {
log.error(e) { "Error while running setForeground: " }
}
diff --git a/app/src/main/kotlin/org/fdroid/search/SearchHelper.kt b/app/src/main/kotlin/org/fdroid/search/SearchHelper.kt
index 5471848cd..a90b1c46d 100644
--- a/app/src/main/kotlin/org/fdroid/search/SearchHelper.kt
+++ b/app/src/main/kotlin/org/fdroid/search/SearchHelper.kt
@@ -6,6 +6,9 @@ import java.text.Normalizer.Form.NFKD
object SearchHelper {
private val normalizerRegex = "\\p{M}".toRegex()
+ fun CharSequence.isSearchable(): Boolean =
+ isNotBlank() && (length >= 3 || any { Character.isIdeographic(it.code) })
+
/** Normalizes the string by removing any diacritics that may appear. */
fun String.normalize(): String {
if (Normalizer.isNormalized(this, NFKD)) return this
diff --git a/app/src/main/kotlin/org/fdroid/search/SearchManager.kt b/app/src/main/kotlin/org/fdroid/search/SearchManager.kt
index 2b60d747a..862bc0bed 100644
--- a/app/src/main/kotlin/org/fdroid/search/SearchManager.kt
+++ b/app/src/main/kotlin/org/fdroid/search/SearchManager.kt
@@ -28,16 +28,13 @@ import org.fdroid.download.PackageName
import org.fdroid.download.getImageModel
import org.fdroid.index.RepoManager
import org.fdroid.install.InstalledAppsCache
+import org.fdroid.search.SearchHelper.normalize
import org.fdroid.settings.SettingsManager
import org.fdroid.ui.categories.CategoryItem
import org.fdroid.ui.lists.AppListItem
-import org.fdroid.search.SearchHelper.normalize
import org.fdroid.ui.search.SearchResults
import org.fdroid.utils.IoDispatcher
-/** The minimum amount of characters we start auto-searching for. */
-const val SEARCH_THRESHOLD = 2
-
@Singleton
class SearchManager
@Inject
@@ -55,16 +52,20 @@ constructor(
private val collator = Collator.getInstance(Locale.getDefault())
private val _searchResults = MutableStateFlow(null)
private val _savedSearches = MutableStateFlow?>(null)
- private val categories =
+ private var searchJob: SearchJob? = null
+
+ val categories =
db.getRepositoryDao().getLiveCategories().asFlow().map { categories ->
categories
.map { category ->
- CategoryItem(id = category.id, name = category.getName(localeList) ?: "Unknown Category")
+ CategoryItem(
+ id = category.id,
+ name = category.getName(localeList) ?: "Unknown Category",
+ description = category.getDescription(localeList),
+ )
}
.sortedWith { c1, c2 -> collator.compare(c1.name, c2.name) }
}
- private var searchJob: SearchJob? = null
-
val searchResults = _searchResults.asStateFlow()
val savedSearches = _savedSearches.asStateFlow()
@@ -116,7 +117,9 @@ constructor(
val timedCategories = measureTimedValue {
categories.first().filter {
// normalization removed diacritics, so searches without them work
- it.name.normalize().contains(sanitized.normalize(), ignoreCase = true)
+ val normalized = sanitized.normalize()
+ it.name.normalize().contains(normalized, ignoreCase = true) ||
+ (it.description?.normalize()?.contains(normalized, ignoreCase = true) ?: false)
}
}
_searchResults.value = SearchResults(timedApps.value, timedCategories.value)
diff --git a/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt b/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt
index 26d9fa574..2f7b946d1 100644
--- a/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt
+++ b/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt
@@ -18,14 +18,19 @@ class OnboardingManager
constructor(@param:ApplicationContext private val context: Context) {
private companion object {
+ const val KEY_MY_APPS_UPDATES = "myAppsUpdates"
const val KEY_FILTER = "appFilter"
const val KEY_REPO_LIST = "repoList"
const val KEY_REPO_DETAILS = "repoDetails"
const val KEY_APP_ISSUE_HINT = "appIssueHint"
+ const val KEY_ANTI_FEATURES = "antiFeatures"
}
private val prefs = context.getSharedPreferences("onboarding", MODE_PRIVATE)
+ private val _showMyAppsUpdatesHint = Onboarding(KEY_MY_APPS_UPDATES, prefs)
+ val showMyAppsUpdatesHint = _showMyAppsUpdatesHint.flow
+
private val _showFilterOnboarding = Onboarding(KEY_FILTER, prefs)
val showFilterOnboarding = _showFilterOnboarding.flow
@@ -38,6 +43,13 @@ constructor(@param:ApplicationContext private val context: Context) {
private val _showAppIssueHint = Onboarding(KEY_APP_ISSUE_HINT, prefs)
val showAppIssueHint = _showAppIssueHint.flow
+ private val _showAntiFeaturesOnboarding = Onboarding(KEY_ANTI_FEATURES, prefs)
+ val showAntiFeaturesOnboarding = _showAntiFeaturesOnboarding.flow
+
+ fun onMyAppsUpdateHintSeen() {
+ _showMyAppsUpdatesHint.onSeen(prefs)
+ }
+
fun onFilterOnboardingSeen() {
_showFilterOnboarding.onSeen(prefs)
}
@@ -53,6 +65,10 @@ constructor(@param:ApplicationContext private val context: Context) {
fun onAppIssueHintSeen() {
_showAppIssueHint.onSeen(prefs)
}
+
+ fun onAntiFeaturesOnboardingSeen() {
+ _showAntiFeaturesOnboarding.onSeen(prefs)
+ }
}
private data class Onboarding(val key: String, private val _flow: MutableStateFlow) {
diff --git a/app/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt b/app/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt
index 531b90735..51e4a0ee0 100644
--- a/app/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt
+++ b/app/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt
@@ -55,6 +55,12 @@ object SettingsConstants {
const val PREF_KEY_PREVENT_SCREENSHOTS = "preventScreenshots"
const val PREF_DEFAULT_PREVENT_SCREENSHOTS = false
+ const val PREF_KEY_WARN_WHEN_METERED = "warnWhenMetered"
+ const val PREF_DEFAULT_WARN_WHEN_METERED = true
+
+ const val PREF_KEY_SHOW_SEARCH_KEYBOARD = "showSearchKeyboard"
+ const val PREF_DEFAULT_SHOW_SEARCH_KEYBOARD = false
+
const val PREF_KEY_SHOW_INCOMPATIBLE = "incompatibleVersions"
const val PREF_DEFAULT_SHOW_INCOMPATIBLE = true
diff --git a/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt b/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt
index 8aaa35e71..215eeb172 100644
--- a/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt
+++ b/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt
@@ -33,7 +33,9 @@ import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_PREVENT_SCREENSHOTS
import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_PROXY
import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_REPO_UPDATES
import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_SHOW_INCOMPATIBLE
+import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_SHOW_SEARCH_KEYBOARD
import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_THEME
+import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_WARN_WHEN_METERED
import org.fdroid.settings.SettingsConstants.PREF_DNS_CACHE
import org.fdroid.settings.SettingsConstants.PREF_DNS_CACHE_DEFAULT
import org.fdroid.settings.SettingsConstants.PREF_KEY_APP_LIST_SORT_ORDER
@@ -49,7 +51,9 @@ import org.fdroid.settings.SettingsConstants.PREF_KEY_PREVENT_SCREENSHOTS
import org.fdroid.settings.SettingsConstants.PREF_KEY_PROXY
import org.fdroid.settings.SettingsConstants.PREF_KEY_REPO_UPDATES
import org.fdroid.settings.SettingsConstants.PREF_KEY_SHOW_INCOMPATIBLE
+import org.fdroid.settings.SettingsConstants.PREF_KEY_SHOW_SEARCH_KEYBOARD
import org.fdroid.settings.SettingsConstants.PREF_KEY_THEME
+import org.fdroid.settings.SettingsConstants.PREF_KEY_WARN_WHEN_METERED
import org.fdroid.settings.SettingsConstants.PREF_USE_DNS_CACHE
import org.fdroid.settings.SettingsConstants.PREF_USE_DNS_CACHE_DEFAULT
import org.fdroid.settings.SettingsConstants.getAppListSortOrder
@@ -188,6 +192,14 @@ class SettingsManager @Inject constructor(@param:ApplicationContext private val
return prefs.edit { putString(PREF_DNS_CACHE, value) }
}
+ private val _warnWhenMeteredFlow =
+ MutableStateFlow(prefs.getBoolean(PREF_KEY_WARN_WHEN_METERED, PREF_DEFAULT_WARN_WHEN_METERED))
+ val warnWhenMeteredFlow = _warnWhenMeteredFlow.asStateFlow()
+
+ private val _showSearchKeyboardFlow =
+ MutableStateFlow(prefs.getBoolean(PREF_KEY_SHOW_SEARCH_KEYBOARD, PREF_DEFAULT_SHOW_SEARCH_KEYBOARD))
+ val showSearchKeyboardFlow = _showSearchKeyboardFlow.asStateFlow()
+
val filterIncompatible: Boolean
get() = !prefs.getBoolean(PREF_KEY_SHOW_INCOMPATIBLE, PREF_DEFAULT_SHOW_INCOMPATIBLE)
@@ -206,6 +218,16 @@ class SettingsManager @Inject constructor(@param:ApplicationContext private val
prefs.edit { putString(PREF_KEY_MY_APPS_SORT_ORDER, value.toSettings()) }
}
+ fun onDontWarnOnMeteredNetwork() {
+ prefs.edit { putBoolean(PREF_KEY_WARN_WHEN_METERED, false) }
+ _warnWhenMeteredFlow.update { false }
+ }
+
+ fun setShowSearchKeyboard(show: Boolean) {
+ prefs.edit { putBoolean(PREF_KEY_SHOW_SEARCH_KEYBOARD, show) }
+ _showSearchKeyboardFlow.update { show }
+ }
+
fun saveAppListFilter(sortOrder: AppListSortOrder, filterIncompatible: Boolean) {
prefs.edit {
putBoolean(PREF_KEY_SHOW_INCOMPATIBLE, !filterIncompatible)
diff --git a/app/src/main/kotlin/org/fdroid/ui/Main.kt b/app/src/main/kotlin/org/fdroid/ui/Main.kt
index 59f4e0c30..99b9bd33a 100644
--- a/app/src/main/kotlin/org/fdroid/ui/Main.kt
+++ b/app/src/main/kotlin/org/fdroid/ui/Main.kt
@@ -8,14 +8,19 @@ import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
+import org.fdroid.search.SavedSearch
import org.fdroid.ui.apps.myAppsEntry
+import org.fdroid.ui.categories.CategoryItem
import org.fdroid.ui.details.NoAppSelected
import org.fdroid.ui.details.appDetailsEntry
import org.fdroid.ui.discover.discoverEntry
@@ -32,6 +37,9 @@ import org.fdroid.ui.navigation.toEntries
import org.fdroid.ui.navigation.topLevelRoutes
import org.fdroid.ui.repositories.repoEntry
import org.fdroid.ui.search.GlobalSearch
+import org.fdroid.ui.search.SearchActions
+import org.fdroid.ui.search.SearchInfo
+import org.fdroid.ui.search.SearchResults
import org.fdroid.ui.search.SearchViewModel
import org.fdroid.ui.settings.Settings
import org.fdroid.ui.settings.SettingsViewModel
@@ -57,6 +65,7 @@ fun Main(onListeningForIntent: () -> Unit = {}) {
calculatePaneScaffoldDirective(windowAdaptiveInfo).copy(horizontalPartitionSpacerSize = 2.dp)
}
val isBigScreen = directive.maxHorizontalPartitions > 1
+ var showSearchKeyboard by remember { mutableStateOf(false) }
val entryProvider: (NavKey) -> NavEntry = entryProvider {
discoverEntry(navigator)
@@ -69,13 +78,25 @@ fun Main(onListeningForIntent: () -> Unit = {}) {
) {
val viewModel = hiltViewModel()
GlobalSearch(
- searchResults = viewModel.searchResults.collectAsStateWithLifecycle().value,
- savedSearches = viewModel.savedSearchesFlow.collectAsStateWithLifecycle().value,
- onSearch = viewModel::search,
- onClearSavedSearches = viewModel::onClearSearchHistory,
+ info =
+ object : SearchInfo {
+ override val searchResults: SearchResults? =
+ viewModel.searchResults.collectAsStateWithLifecycle().value
+ override val savedSearches: List? =
+ viewModel.savedSearchesFlow.collectAsStateWithLifecycle().value
+ override val categories: List? =
+ viewModel.categories.collectAsStateWithLifecycle(null).value
+ override val autoShowKeyboard: Boolean =
+ viewModel.autoShowKeyboard.collectAsStateWithLifecycle().value
+ override val showKeyboard: Boolean = showSearchKeyboard
+ override val actions: SearchActions = viewModel
+
+ override fun onKeyboardShown() {
+ showSearchKeyboard = false
+ }
+ },
onNav = { navKey -> navigator.navigate(navKey) },
onBack = { navigator.goBack() },
- onSearchCleared = viewModel::onSearchCleared,
)
}
entry(NavigationKey.Settings) {
@@ -119,7 +140,17 @@ fun Main(onListeningForIntent: () -> Unit = {}) {
isBigScreen = isBigScreen,
showBottomBar = !isBigScreen && navigator.last is MainNavKey,
currentNavKey = navigationState.topLevelRoute,
- onNav = navigator::navigate,
+ onNav = {
+ // don't navigate, if we are already on the selected top level route
+ if (navigationState.topLevelRoute == it) {
+ if (navigationState.topLevelRoute == NavigationKey.Search) {
+ // double tapping the search brings up the keyboard
+ showSearchKeyboard = true
+ }
+ } else {
+ navigator.navigate(it)
+ }
+ },
onBack = navigator::goBack,
)
}
diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt
index e4d6fdfcd..edd354359 100644
--- a/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt
+++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt
@@ -7,9 +7,9 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ManageSearch
import androidx.compose.material.icons.automirrored.filled.Sort
import androidx.compose.material.icons.filled.AccessTime
-import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.SortByAlpha
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
@@ -96,8 +96,8 @@ fun MyApps(
title = { Text(stringResource(R.string.menu_apps_my)) },
actions = {
TopAppBarButton(
- imageVector = Icons.Filled.Search,
- contentDescription = stringResource(R.string.menu_search),
+ imageVector = Icons.AutoMirrored.Default.ManageSearch,
+ contentDescription = stringResource(R.string.search_my_apps_hint),
onClick = { searchActive = true },
)
var sortByMenuExpanded by remember { mutableStateOf(false) }
@@ -186,10 +186,8 @@ fun MyAppsLoadingPreview() {
val model =
MyAppsModel(
installingApps = emptyList(),
- appUpdates = null,
- installedApps = null,
+ showUpdatesHint = false,
showAppIssueHint = false,
- sortOrder = AppListSortOrder.NAME,
networkState = NetworkState(isOnline = false, isMetered = false),
)
FDroidContent {
@@ -223,11 +221,11 @@ fun MyAppsEmptyPreview() {
FDroidContent {
val model =
MyAppsModel(
- installingApps = emptyList(),
appUpdates = emptyList(),
+ installingApps = emptyList(),
installedApps = emptyList(),
+ showUpdatesHint = false,
showAppIssueHint = false,
- sortOrder = AppListSortOrder.NAME,
networkState = NetworkState(isOnline = false, isMetered = false),
)
MyApps(
diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt
index 78c00c2f8..b9b9759f3 100644
--- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt
+++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt
@@ -15,9 +15,11 @@ data class MyAppsModel(
val installingApps: List,
val appsWithIssue: List? = null,
val installedApps: List? = null,
+ val showUpdatesHint: Boolean,
val showAppIssueHint: Boolean,
val sortOrder: AppListSortOrder = AppListSortOrder.NAME,
val networkState: NetworkState,
+ val isSearching: Boolean = false,
val appUpdatesBytes: Long? = null,
)
@@ -32,7 +34,11 @@ interface MyAppsActions {
fun ignoreAppIssue(item: AppWithIssueItem)
+ fun onUpdatesHintSeen()
+
fun onAppIssueHintSeen()
+ fun onNotWarnWhenMetered()
+
fun exportInstalledApps()
}
diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt
index 949645d99..b4c259650 100644
--- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt
+++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt
@@ -37,6 +37,7 @@ import org.fdroid.database.NotAvailable
import org.fdroid.ui.FDroidContent
import org.fdroid.ui.utils.MeteredConnectionDialog
import org.fdroid.ui.utils.OfflineBar
+import org.fdroid.ui.utils.OnboardingCard
import org.fdroid.ui.utils.getMyAppsInfo
import org.fdroid.ui.utils.myAppsModel
@@ -74,7 +75,22 @@ fun MyAppsList(
modifier.then(if (currentPackageName == null) Modifier else Modifier.selectableGroup()),
) {
// Updates header with Update all button
- if (!updatableApps.isNullOrEmpty()) {
+ if (
+ updatableApps != null &&
+ updatableApps.isEmpty() &&
+ myAppsInfo.model.showUpdatesHint &&
+ !myAppsInfo.model.isSearching
+ ) {
+ item(key = "A-hint", contentType = "hint") {
+ OnboardingCard(
+ title = stringResource(R.string.apps_updates_hint_title),
+ message = stringResource(R.string.apps_updates_hint_text),
+ buttonText = stringResource(R.string.hide),
+ onGotIt = { myAppsInfo.actions.onUpdatesHintSeen() },
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
+ )
+ }
+ } else if (!updatableApps.isNullOrEmpty()) {
if (!myAppsInfo.model.networkState.isOnline) {
item(key = "OfflineBar", contentType = "offlineBar") { OfflineBar() }
}
@@ -92,7 +108,7 @@ fun MyAppsList(
myAppsInfo.actions.updateAll()
showUpdateAllButton = false
}
- if (myAppsInfo.model.networkState.isMetered) {
+ if (myAppsInfo.model.networkState.showWarningDialog) {
showMeteredDialog = installLambda
} else {
installLambda()
@@ -211,12 +227,8 @@ fun MyAppsList(
NotAvailableDialog(app.packageName) { showNotAvailableDialog = false }
}
}
- // Installed apps header (only show when we have non-empty lists above)
- val aboveNonEmpty =
- installingApps.isNotEmpty() ||
- !updatableApps.isNullOrEmpty() ||
- !appsWithIssue.isNullOrEmpty()
- if (aboveNonEmpty && !installedApps.isNullOrEmpty()) {
+ // List of installed apps
+ if (!installedApps.isNullOrEmpty()) {
item(key = "D", contentType = "header") {
Text(
text = stringResource(R.string.installed_apps__activity_title),
@@ -224,9 +236,6 @@ fun MyAppsList(
modifier = Modifier.padding(16.dp),
)
}
- }
- // List of installed apps
- if (installedApps != null)
items(items = installedApps, key = { it.packageName }, contentType = { "D" }) { app ->
val isSelected = app.packageName == currentPackageName
val interactionModifier =
@@ -241,12 +250,16 @@ fun MyAppsList(
val modifier = Modifier.animateItem().then(interactionModifier)
InstalledAppRow(app, isSelected, modifier)
}
+ }
}
val meteredLambda = showMeteredDialog
if (meteredLambda != null)
MeteredConnectionDialog(
numBytes = myAppsInfo.model.appUpdatesBytes,
- onConfirm = { meteredLambda() },
+ onConfirm = { notWarnWhenMetered ->
+ if (notWarnWhenMetered) myAppsInfo.actions.onNotWarnWhenMetered()
+ meteredLambda()
+ },
onDismiss = { showMeteredDialog = null },
)
}
@@ -264,3 +277,24 @@ private fun MyAppsListPreview() {
)
}
}
+
+@Preview
+@Composable
+@RestrictTo(RestrictTo.Scope.TESTS)
+private fun Preview() {
+ FDroidContent {
+ MyApps(
+ myAppsInfo =
+ getMyAppsInfo(
+ myAppsModel.copy(
+ appUpdates = emptyList(),
+ installingApps = emptyList(),
+ appsWithIssue = emptyList(),
+ )
+ ),
+ currentPackageName = null,
+ onAppItemClick = {},
+ onNav = {},
+ )
+ }
+}
diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt
index 0ccc3cc73..7301a0794 100644
--- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt
+++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt
@@ -21,6 +21,7 @@ fun MyAppsPresenter(
appInstallStatesFlow: StateFlow
Toon gearchiveerde apps en verouderde versies van apps
- Uitschakelen van deze gegevensbank zal de \"geprefereerde\" status verwijderen van alle apps.
- Download apps van IPFS web eindpunten: %1$d ingeschakeld
- Download apps van IPFS web eindpunten: %1$d ingeschakeld
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index e0bc1605f..22d5134d1 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -596,7 +596,6 @@
Preferuj repozytorium
Archiwalne repozytoria nie mogą być dodawane bezpośrednio. Naciśnij repozytorium na liście i włącz tam archiwum.
Ostatnia aktualizacja: %s
- Wyłączenie tego repozytorium spowoduje usunięcie jego statusu „preferowane” ze wszystkich aplikacji.
Dodaj repozytorium i serwer lustrzany
Wyświetl repozytorium
Zeskanuj kod QR
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 6eb9ffff5..e5211b485 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -568,7 +568,6 @@
Arquivo do repositório
Mostrar apps arquivados e versões desatualizadas de apps
O arquivo do repositório não está disponível no momento
- Desativar este repositório removerá o estado de \"preferido\" dele de todos os apps.
Desativar
(preferido)
Adicionar repositório e espelho
diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml
index 81c9fc8e8..ccf14c408 100644
--- a/app/src/main/res/values-pt-rPT/strings.xml
+++ b/app/src/main/res/values-pt-rPT/strings.xml
@@ -543,7 +543,6 @@
Um repositório é uma fonte de aplicações. Esta lista mostra todos os repositórios atualmente adicionados. Os repositórios desativados não são utilizados.\n\nSe uma aplicação estiver em mais do que um repositório, o repositório no topo da lista é automaticamente preferido. Pode reordenar os repositórios premindo longamente e arrastando-os.
Arquivo do repositório
O repositório de arquivos não está atualmente disponível
- A desativação deste repositório removerá o seu estatdo de \"preferido\" de todas as apps.
Desativar
F-Droid pode procurar por repositórios e espelhos no seu Cartão de Memória, mas precisa da sua permissão para fazer isso.
Para trocar aplicações, inicie o seu Wi-Fi e ligue-se à mesma rede que o seu amigo.
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 90459002c..32ba51497 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -571,7 +571,6 @@
Mostrar aplicações arquivadas e versões desatualizadas de aplicações
O repositório de arquivos não está atualmente disponível
Desativar
- A desativação deste repositório removerá o seu estatuto de \"preferido\" de todas as aplicações.
Gateways IPFS
Repositório
Repositórios
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index 7727e6103..91a759cfc 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -570,7 +570,6 @@
Lista de depozite
Arhivă depozit
Arhivă depozit momentan indisponibilă
- Dezactivarea acestui depozit îl va elimina ca fiind \"preferat\" pentru toate aplicațiile.
Ultima actualizare: %s
niciodată
Un depozit este o sursă de aplicații. Această listă afișează toate depozitele adăugate în prezent. Depozitele dezactivate nu sunt utilizate.
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index cfc69dbf0..42a2b193d 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -576,7 +576,6 @@
(выбран)
Архивные репозитории не могут быть добавлены напрямую. Нажмите на репозиторий из списка и включите в нем архив.
Репозиторий — это источник приложений. В этом списке отображаются все добавленные репозитории. Отключенные репозитории не используются.\n\nЕсли приложение находится в нескольких репозиториях, предпочтение автоматически отдается репозиторию, расположенному выше в списке. Изменить порядок репозиториев можно долгим нажатием и перетаскиванием.
- Отключение этого репозитория удалит его статус \"предпочтительный\" из всех приложений.
Отключить
Не удалось найти адрес репозитория в общем тексте.
Добавить репозиторий и зеркало
diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml
index 30fc35738..cb5ec2b5e 100644
--- a/app/src/main/res/values-sc/strings.xml
+++ b/app/src/main/res/values-sc/strings.xml
@@ -538,7 +538,6 @@
- Iscàrriga aplicatziones dae sos puntos finales IPFS: %1$d abilitadu
- Iscàrriga aplicatziones dae sos puntos finales IPFS: %1$d abilitados
- Disabilitende custu depòsitu nd\'as a bogare s\'istadu de \"preferidu\" suo dae totu sas aplicatziones.
Ùrtima atualizatzione: %s
mai
Lista de depòsitos
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index 74449f224..f63ae2b2d 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -638,7 +638,6 @@
Archív repozitára nie je momentálne k dispozícii
Odstrániť zrkadlo?
Aplikácie z tohto repozitára môžete naďalej inštalovať prostredníctvom iných zrkadiel.
- Deaktiváciou tohto repozitára sa zruší jeho status „predvoleného“ vo všetkých aplikáciách.
Vypnúť
Zrkadlo na zdieľanie
Zobraziť QR kód repozitára
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml
index 040588b96..160e6c1eb 100644
--- a/app/src/main/res/values-sl/strings.xml
+++ b/app/src/main/res/values-sl/strings.xml
@@ -634,7 +634,6 @@
Arhivski repozitorij trenutno ni na voljo
Izbriši zrcalo?
Aplikacije iz tega repozitorija lahko še naprej nameščate z uporabo drugih zrcalnih strežnikov.
- Če onemogočite to skladišče, boste odstranili njegov status \"prednostno\" iz vseh aplikacij.
Onemogoči
Deli zrcalo
Prikaži QR kodo repozitorija
diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml
index 028f46d39..e5f40f5e3 100644
--- a/app/src/main/res/values-sq/strings.xml
+++ b/app/src/main/res/values-sq/strings.xml
@@ -520,7 +520,6 @@
Listë deposh
Arkiv deposh
Shfaq aplikacione të arkivuar dhe versione të vjetruar të aplikacioneves
- Çaktivizimi i kësaj depoje do t’i heqë asaj gjendjen “e parapëlqyer” nga krejt aplikacionet.
Kanale IPFS
Përditësimi i fundit më: %s
kurrë
diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml
index bedf1eafc..1efb8dc81 100644
--- a/app/src/main/res/values-sr/strings.xml
+++ b/app/src/main/res/values-sr/strings.xml
@@ -562,7 +562,6 @@
Листа репозиторијума
Прошири листу репозиторијума
Приказује архивиране апликације и застареле верзије апликација
- Онемогућавањем овог репозиторијума уклањате његов статус „префериран” из свих апликација.
(префериран)
Преферирај репозиторијум
Последње ажурирање: %s
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 4127b5c88..f6aa08b7a 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -573,7 +573,6 @@
Förrådets arkiv är inte tillgängligt just nu
Tidigast nästkommande uppdatering: %s
Inaktivera
- Att inaktivera förrådet kommer ta bort dess status som \"föredragen\" från alla appar.
IPFS-gateways
Det valda förvaret har inga kompatibla appversioner. För att ta emot uppdateringar, föredra ett förvar med kompatibla versioner.
Hämta appar med IPFS-webbändpunkter: inaktiverad
diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml
index ec706d4c2..4429586d2 100644
--- a/app/src/main/res/values-sw/strings.xml
+++ b/app/src/main/res/values-sw/strings.xml
@@ -559,7 +559,6 @@
Kumbukumbu za ghala haiwezi kuongezwa moja kwa moja. Gusa ghala kwenye orodha na uwezeshe kumbukumbu hio hapo.
Ghala la kumbukumbu halipatikani kwa sasa.
Milango za IPFS
- Kulemaza ghala hili litaondoa hali yake ya \"Iliyopendekezwa\" kutoka kwa apu zote.
Apu zilizojumuishwa:
URL hio unayojaribu kuongeza (%s) ni kioo cha ghala iliyopo.
URL unayojaribu kuongeza (%s) ni kioo cha ghala mpya. Zote mbili ghala asili na kioo zitaongezwa.
diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml
index cdef44a45..0732965a3 100644
--- a/app/src/main/res/values-ta/strings.xml
+++ b/app/src/main/res/values-ta/strings.xml
@@ -529,7 +529,6 @@
களஞ்சிய பட்டியலை விரிவாக்கு
டிஎன்எச் வினவல்களைக் குறைக்க தற்காலிக சேமிப்பு முடிவுகளைப் பயன்படுத்தவும்.
களஞ்சிய காப்பகம்
- இந்த களஞ்சியத்தை முடக்குவது அனைத்து பயன்பாடுகளிலிருந்தும் அதன் \"விருப்பமான\" நிலையை அகற்றும்.
முயற்சி செய்யுங்கள்
ஒருபோதும்
களஞ்சியத்தையும் கண்ணாடியையும் சேர்க்கவும்
diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml
index 93f4405c2..6225f30aa 100644
--- a/app/src/main/res/values-th/strings.xml
+++ b/app/src/main/res/values-th/strings.xml
@@ -534,7 +534,6 @@
รายการพื้นที่เก็บข้อมูล
ที่เก็บถาวร
ที่เก็บถาวรไม่พร้อมใช้งานในขณะนี้
- การปิดใช้งานที่เก็บนี้จะลบสถานะ \"ที่ต้องการ\" ออกจากแอปทั้งหมด
ปิดการใช้งาน
แปะ
เกตเวย์ IPFS แบบกำหนดเอง
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index ed48c559f..159aca14e 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -572,7 +572,6 @@
Son Güncelleme: %s
yok
Arşiv deposu şu anda kullanılabilir değil
- Bu deponun devre dışı bırakılması, tüm uygulamalardan \"tercih edilen\" durumunu kaldıracaktır.
Devre dışı bırak
(tercih edilen)
Depo ve yansı ekle
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index a6bdd2fc7..0365920b9 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -568,7 +568,6 @@
Розгорнути список репозиторіїв
Показувати заархівовані застосунки та застарілі версії застосунків
Вимкнути
- Вимкнення цього репозиторію призведе до вилучення його стану \"бажаного\" з усіх застосунків.
Востаннє оновлено: %s
ніколи
Список репозиторіїв
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index 088ee4a70..bda4dcd6c 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -526,7 +526,6 @@
Kho này đã được thêm từ trước.
Cập nhật lần cuối: %s
chưa từng cập nhật
- Vô hiệu hóa kho này sẽ gỡ bỏ trạng thái \"ưu tiên\" của nó khỏi tất cả ứng dụng.
mục tiêu %s
Kho ứng dụng (repository) lưu giữ thông tin cài đặt và cập nhật cho các ứng dụng, Danh sách này hiển thị tất cả các kho đã thêm. Các kho bị vô hiệu hóa sẽ không cung cấp được thông tin cài đặt và cập nhật cho các ứng dụng trong kho ấy.\n \nNếu một ứng dụng tồn tại trong nhiều kho, kho có vị trí cao hơn trong danh sách sẽ được ưu tiên. Bạn có thể sắp xếp vị trí kho bằng cách nhấn giữ và kéo thả chúng.
Dùng trình cài đặt cũ
diff --git a/app/src/main/res/values-yue/strings.xml b/app/src/main/res/values-yue/strings.xml
index b0fd2dc7a..f6afe9886 100644
--- a/app/src/main/res/values-yue/strings.xml
+++ b/app/src/main/res/values-yue/strings.xml
@@ -557,7 +557,6 @@
從來沒有
存庫清單
封印存庫目前欠奉
- 若然停用呢個存庫,所有 app 都唔會再「揀選」佢個囉噃。
停用
存庫
揀選存庫
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 8dfdfe130..45cbf0726 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -555,7 +555,6 @@
上次更新:%s
从未
存储库列表
- 停用此存储库会从所有应用去除其“首选”状态。
(首选)
包含的应用:
你正尝试添加的 URL (%s) 是一个新存储库的镜像。原始存储库和此镜像都将被添加。
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index bc6dedc36..7f1882d74 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -561,7 +561,6 @@
軟件庫封存
顯示已封存的應用程式與各應用程式的過時版本
封存軟件庫目前不可用
- 若停用此軟件庫,所有應用程式均將不再「喜好」此軟件庫。
停用
軟件庫
(喜好)
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 8b7ca20e8..26b33b752 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -554,7 +554,6 @@
從不
軟體庫是應用程式的來源。此清單顯示所有目前新增的軟體庫。不會使用已停用的軟體庫。 \n \n若一個應用程式位於多個軟體庫中,則自動偏好清單中較高的軟體庫。您可以透過長按並拖曳軟體庫來重新排序。
封存軟體庫目前無法使用
- 停用此軟體庫將從所有應用程式中刪除其「偏好」狀態。
新增軟體庫與鏡像
掃描 QR Code
包含的應用程式:
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c93feae15..aec885105 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -38,6 +38,7 @@
No apps found\n\nTry using fewer search terms or add more repositories
No matching apps\n\nTry using fewer search terms or remove filters
No matching apps\n\nTry using fewer search terms
+ Automatically open keyboard
Sorting and compatibility
Sort by name
Sort by last update
@@ -56,6 +57,18 @@
Use filters to only show apps from specific categories or repositories. You can also change the sort order.
Got it
Clear
+ Hide
+
+ Calendars, tasks, clocks & notes
+ Navigation, weather, calculators & converters
+ Finance, money & pass managers
+ Music, podcasts, games & ebook readers
+ Email, messaging & social networks
+ Launcher, keyboard, security & theming
+ Browser, VPNs, proxies & firewall
+ Files, gallery, cloud sync & encryption
+ Science, sports, drawing & recipes
+ Uncategorized
Productivity
Tools
@@ -225,6 +238,10 @@ This often occurs with apps installed via Google Play or other sources, if they
This might also be interesting to you:
More apps by %1$s
+ What are Anti-Features?
+ Some apps come with behaviors you didn’t ask for — and might not want. Things like ads, tracking, or dependence on proprietary services. We call these Anti-Features.
+\n\nApps in F-Droid are clearly labeled when they contain Anti-Features, so you can make an informed choice.
+
About F-Droid
About F-Droid Basic
Apps on F-Droid are free and open source software (FOSS), they respect your privacy, and they\'re available for anyone to see the source code, build on and improve.\n\nApp distribution is transparent, privacy-respecting, and accountable, setting a standard that challenges the mobile ecosystem to do better.\n\nF-Droid\'s impact is measured not just in apps published, but in the freedom, trust, and digital rights it helps protect.\n\nHelp F-Droid to protect community-powered innovation and continue our mission of openness, privacy, and user empowerment.
@@ -239,6 +256,7 @@ This often occurs with apps installed via Google Play or other sources, if they
Downloading %1$s may incur charges depending on your data plan
Downloading may incur charges depending on your data plan
+ Don\'t show this again
Download
Tap to load screenshots via mobile data
@@ -295,6 +313,8 @@ This often occurs with apps installed via Google Play or other sources, if they
Download canceled
Installed apps
+ Congratulations 🥳
+ All your apps are up to date
Share installed apps
Apps installed by F-Droid as CSV file
@@ -488,6 +508,7 @@ This often occurs with apps installed via Google Play or other sources, if they
Show extra info and enable extra settings
Search apps
+ Search installed apps
App compatibility
Include incompatible versions
@@ -639,7 +660,9 @@ This often occurs with apps installed via Google Play or other sources, if they
Disabled "%1$s".\n\nYou will
need to re-enable this repository to install apps from it.
- Disabling this repository will remove its \"preferred\" status from all apps.
+ Disable repository
+ If you\'ve set this repository as preferred for any apps, that preference will be lost
+ All apps from this repository will disappear. Already installed apps won\'t be affected.
Disable
Saved package repository %1$s.
Looking for package repository at\n%1$s
diff --git a/app/src/screenshotTest/kotlin/org/fdroid/ui/apps/MyAppsTest.kt b/app/src/screenshotTest/kotlin/org/fdroid/ui/apps/MyAppsTest.kt
index 8b2795a5f..42f08c6ec 100644
--- a/app/src/screenshotTest/kotlin/org/fdroid/ui/apps/MyAppsTest.kt
+++ b/app/src/screenshotTest/kotlin/org/fdroid/ui/apps/MyAppsTest.kt
@@ -11,6 +11,31 @@ import org.fdroid.ui.ScreenshotTest
import org.fdroid.ui.utils.getMyAppsInfo
import org.fdroid.ui.utils.getPreviewVersion
+@Composable
+@PreviewTest
+@Preview(showBackground = true, showSystemUi = true)
+fun MyAppsEmpty() {
+ ScreenshotTest(numUpdates = 0, hasAppIssues = false) {
+ val model =
+ MyAppsModel(
+ appUpdates = emptyList(),
+ installingApps = emptyList(),
+ appsWithIssue = null,
+ installedApps = emptyList(),
+ showUpdatesHint = true,
+ showAppIssueHint = false,
+ sortOrder = AppListSortOrder.NAME,
+ networkState = NetworkState(isOnline = true, isMetered = false),
+ )
+ MyApps(
+ myAppsInfo = getMyAppsInfo(model),
+ currentPackageName = null,
+ onAppItemClick = {},
+ onNav = {},
+ )
+ }
+}
+
@Composable
@PreviewTest
@Preview(showBackground = true, showSystemUi = true)
@@ -22,6 +47,7 @@ fun MyAppsOnlyInstalledAppsAndNoInternet() {
installingApps = emptyList(),
appsWithIssue = null,
installedApps = getInstalledApps(),
+ showUpdatesHint = true,
showAppIssueHint = false,
sortOrder = AppListSortOrder.NAME,
networkState = NetworkState(isOnline = true, isMetered = false),
@@ -46,6 +72,7 @@ fun MyAppsInstalledAndIssues() {
installingApps = emptyList(),
appsWithIssue = getAppIssues(),
installedApps = getInstalledApps(),
+ showUpdatesHint = false,
showAppIssueHint = true,
sortOrder = AppListSortOrder.NAME,
networkState = NetworkState(isOnline = false, isMetered = false),
@@ -70,6 +97,7 @@ fun MyAppsUpdatesOnlyAndNoInternet() {
installingApps = emptyList(),
appsWithIssue = null,
installedApps = emptyList(),
+ showUpdatesHint = false,
showAppIssueHint = false,
sortOrder = AppListSortOrder.LAST_UPDATED,
networkState = NetworkState(isOnline = false, isMetered = false),
@@ -119,6 +147,7 @@ fun MyAppsFullList() {
installingApps = installingApps,
appsWithIssue = getAppIssues(),
installedApps = getInstalledApps(),
+ showUpdatesHint = false,
showAppIssueHint = false,
sortOrder = AppListSortOrder.NAME,
networkState = NetworkState(isOnline = true, isMetered = false),
diff --git a/app/src/screenshotTest/kotlin/org/fdroid/ui/repositories/AddRepoTest.kt b/app/src/screenshotTest/kotlin/org/fdroid/ui/repositories/AddRepoTest.kt
index ec6696fc4..4e7d71027 100644
--- a/app/src/screenshotTest/kotlin/org/fdroid/ui/repositories/AddRepoTest.kt
+++ b/app/src/screenshotTest/kotlin/org/fdroid/ui/repositories/AddRepoTest.kt
@@ -29,6 +29,7 @@ fun AddRepoIntroTest() =
networkStateFlow = networkStateFlow,
proxyConfig = null,
onFetchRepo = {},
+ onNotWarnWhenMetered = {},
onAddRepo = {},
onExistingRepo = {},
onRepoAdded = { _, _ -> },
@@ -50,6 +51,7 @@ fun AddRepoIntroNightTest() =
networkStateFlow = MutableStateFlow(NetworkState(isOnline = false, isMetered = false)),
proxyConfig = null,
onFetchRepo = {},
+ onNotWarnWhenMetered = {},
onAddRepo = {},
onExistingRepo = {},
onRepoAdded = { _, _ -> },
@@ -73,6 +75,7 @@ fun AddRepoFetchingProgressTest() =
networkStateFlow = networkStateFlow,
proxyConfig = null,
onFetchRepo = {},
+ onNotWarnWhenMetered = {},
onAddRepo = {},
onExistingRepo = {},
onRepoAdded = { _, _ -> },
@@ -97,6 +100,7 @@ fun AddRepoFetchingPreviewTest() =
networkStateFlow = networkStateFlow,
proxyConfig = null,
onFetchRepo = {},
+ onNotWarnWhenMetered = {},
onAddRepo = {},
onExistingRepo = {},
onRepoAdded = { _, _ -> },
@@ -114,6 +118,7 @@ fun AddRepoAddingTest() =
networkStateFlow = networkStateFlow,
proxyConfig = null,
onFetchRepo = {},
+ onNotWarnWhenMetered = {},
onAddRepo = {},
onExistingRepo = {},
onRepoAdded = { _, _ -> },
@@ -131,6 +136,7 @@ fun AddRepoErrorTest() =
networkStateFlow = networkStateFlow,
proxyConfig = null,
onFetchRepo = {},
+ onNotWarnWhenMetered = {},
onAddRepo = {},
onExistingRepo = {},
onRepoAdded = { _, _ -> },
diff --git a/app/src/screenshotTest/kotlin/org/fdroid/ui/search/GlobalSearchTest.kt b/app/src/screenshotTest/kotlin/org/fdroid/ui/search/GlobalSearchTest.kt
index 2d9fb24ca..06bea13e6 100644
--- a/app/src/screenshotTest/kotlin/org/fdroid/ui/search/GlobalSearchTest.kt
+++ b/app/src/screenshotTest/kotlin/org/fdroid/ui/search/GlobalSearchTest.kt
@@ -9,116 +9,92 @@ import org.fdroid.search.SavedSearch
import org.fdroid.ui.ScreenshotTest
import org.fdroid.ui.utils.appListItems
import org.fdroid.ui.utils.categoryItems
+import org.fdroid.ui.utils.getSearchInfo
@Composable
@PreviewTest
@Preview(showBackground = true, showSystemUi = true)
-fun GlobalSearchEmptyTest() =
- ScreenshotTest {
- val textFieldState = rememberTextFieldState()
- GlobalSearch(
- textFieldState = textFieldState,
- searchResults = null,
- savedSearches = emptyList(),
- onSearch = {},
- onClearSavedSearches = {},
- onNav = {},
- onBack = {},
- onSearchCleared = {},
- )
- }
+fun GlobalSearchEmptyTest() = ScreenshotTest {
+ val textFieldState = rememberTextFieldState()
+ GlobalSearch(
+ info = getSearchInfo(savedSearches = emptyList()),
+ textFieldState = textFieldState,
+ onNav = {},
+ onBack = {},
+ )
+}
@Composable
@PreviewTest
@Preview(showBackground = true, showSystemUi = true)
-fun GlobalSearchEmptyStateTest() =
- ScreenshotTest {
- val savedSearches =
- listOf(SavedSearch(1, "Browser"), SavedSearch(2, "media player"), SavedSearch(3, "email app"))
- val textFieldState = rememberTextFieldState()
- GlobalSearch(
- textFieldState = textFieldState,
- searchResults = null,
- savedSearches = savedSearches,
- onSearch = {},
- onClearSavedSearches = {},
- onNav = {},
- onBack = {},
- onSearchCleared = {},
- )
- }
+fun GlobalSearchEmptyStateTest() = ScreenshotTest {
+ val savedSearches =
+ listOf(SavedSearch(1, "Browser"), SavedSearch(2, "media player"), SavedSearch(3, "email app"))
+ val textFieldState = rememberTextFieldState()
+ GlobalSearch(
+ info =
+ getSearchInfo(
+ searchResults = null,
+ savedSearches = savedSearches,
+ categories = categoryItems,
+ ),
+ textFieldState = textFieldState,
+ onNav = {},
+ onBack = {},
+ )
+}
@Composable
@PreviewTest
@Preview(showBackground = true, showSystemUi = true)
-fun GlobalSearchLoadingTest() =
- ScreenshotTest {
- val textFieldState = rememberTextFieldState("foo bar")
- GlobalSearch(
- textFieldState = textFieldState,
- searchResults = null,
- savedSearches = emptyList(),
- onSearch = {},
- onClearSavedSearches = {},
- onNav = {},
- onBack = {},
- onSearchCleared = {},
- )
- }
+fun GlobalSearchLoadingTest() = ScreenshotTest {
+ val textFieldState = rememberTextFieldState("foo bar")
+ GlobalSearch(
+ info = getSearchInfo(savedSearches = emptyList()),
+ textFieldState = textFieldState,
+ onNav = {},
+ onBack = {},
+ )
+}
@Composable
@PreviewTest
@Preview(showBackground = true, showSystemUi = true)
-fun GlobalSearchNoResultsTest() =
- ScreenshotTest {
- val textFieldState = rememberTextFieldState("foo")
- GlobalSearch(
- textFieldState = textFieldState,
- searchResults = SearchResults(emptyList(), emptyList()),
- savedSearches = emptyList(),
- onSearch = {},
- onClearSavedSearches = {},
- onNav = {},
- onBack = {},
- onSearchCleared = {},
- )
- }
+fun GlobalSearchNoResultsTest() = ScreenshotTest {
+ val textFieldState = rememberTextFieldState("foo")
+ GlobalSearch(
+ info = getSearchInfo(searchResults = SearchResults(emptyList(), emptyList())),
+ textFieldState = textFieldState,
+ onNav = {},
+ onBack = {},
+ )
+}
@Composable
@PreviewTest
@Preview(showBackground = true, showSystemUi = true)
-fun GlobalSearchResultsTest() =
- ScreenshotTest {
- val textFieldState = rememberTextFieldState("foo bar")
- GlobalSearch(
- textFieldState = textFieldState,
- searchResults = SearchResults(appListItems, categoryItems.subList(0, 4)),
- savedSearches = emptyList(),
- onSearch = {},
- onClearSavedSearches = {},
- onNav = {},
- onBack = {},
- onSearchCleared = {},
- )
- }
+fun GlobalSearchResultsTest() = ScreenshotTest {
+ val textFieldState = rememberTextFieldState("foo bar")
+ GlobalSearch(
+ info = getSearchInfo(searchResults = SearchResults(appListItems, categoryItems.subList(0, 4))),
+ textFieldState = textFieldState,
+ onNav = {},
+ onBack = {},
+ )
+}
@Composable
@PreviewTest
@Preview(showBackground = true, showSystemUi = true)
-fun GlobalSearchOnlyCategoriesTest() =
- ScreenshotTest {
- val textFieldState = rememberTextFieldState("foo bar")
- GlobalSearch(
- textFieldState = textFieldState,
- searchResults = SearchResults(emptyList(), categoryItems.subList(5, 9)),
- savedSearches = emptyList(),
- onSearch = {},
- onClearSavedSearches = {},
- onNav = {},
- onBack = {},
- onSearchCleared = {},
- )
- }
+fun GlobalSearchOnlyCategoriesTest() = ScreenshotTest {
+ val textFieldState = rememberTextFieldState("foo bar")
+ GlobalSearch(
+ info = getSearchInfo(searchResults = SearchResults(emptyList(), categoryItems.subList(5, 9))),
+ textFieldState = textFieldState,
+ onNav = {},
+ onBack = {},
+ )
+}
@Composable
@PreviewTest
@@ -127,17 +103,12 @@ fun GlobalSearchOnlyCategoriesTest() =
showSystemUi = true,
uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL,
)
-fun GlobalSearchResultsNightTest() =
- ScreenshotTest {
- val textFieldState = rememberTextFieldState("foo bar")
- GlobalSearch(
- textFieldState = textFieldState,
- searchResults = SearchResults(appListItems, categoryItems.subList(0, 4)),
- savedSearches = emptyList(),
- onSearch = {},
- onClearSavedSearches = {},
- onNav = {},
- onBack = {},
- onSearchCleared = {},
- )
- }
+fun GlobalSearchResultsNightTest() = ScreenshotTest {
+ val textFieldState = rememberTextFieldState("foo bar")
+ GlobalSearch(
+ info = getSearchInfo(searchResults = SearchResults(appListItems, categoryItems.subList(0, 4))),
+ textFieldState = textFieldState,
+ onNav = {},
+ onBack = {},
+ )
+}
diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsEmpty_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsEmpty_2ed8e27d_0.png
new file mode 100644
index 000000000..dc032ca1c
--- /dev/null
+++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsEmpty_2ed8e27d_0.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:17943eea5fc8b02e415e2bbd5c4db5477cb9b2168ca2018cfc3db7193c483fe7
+size 44031
diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsFullList_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsFullList_2ed8e27d_0.png
index 65f12ee8b..aae4197da 100644
--- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsFullList_2ed8e27d_0.png
+++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsFullList_2ed8e27d_0.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1501c756d19f294b3f71ee089a1e728b3ade04b91372617248f68cb60aecfe1f
-size 158114
+oid sha256:11d1141d017dd1bc3013d8aa16dc466e0fad8863fec8084acd8e49549fa78ae6
+size 157993
diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png
index 309a6b33d..95c8a682c 100644
--- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png
+++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:619eb7e9478b45a8b1ada0b65335fad718cb728f500ab04c77bb292933df5c84
-size 114633
+oid sha256:9e8e34dc95d9b2b264fafa4bcf601936dfcb707d11abe3cc05000ae5d0bd0d14
+size 114468
diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsOnlyInstalledAppsAndNoInternet_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsOnlyInstalledAppsAndNoInternet_2ed8e27d_0.png
index 2505a0bb8..62f886c83 100644
--- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsOnlyInstalledAppsAndNoInternet_2ed8e27d_0.png
+++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsOnlyInstalledAppsAndNoInternet_2ed8e27d_0.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:037f0d24d270c8fb52ac88bff5f40c999ea11035d03e270b6fd7968ad24506a5
-size 67807
+oid sha256:9c23a46a628d49fe90c7bf494bb0deb7a86121780d8e608e7ff8bf75c7cb63af
+size 88320
diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsUpdatesOnlyAndNoInternet_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsUpdatesOnlyAndNoInternet_2ed8e27d_0.png
index 8774b310a..d45d6c05e 100644
--- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsUpdatesOnlyAndNoInternet_2ed8e27d_0.png
+++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsUpdatesOnlyAndNoInternet_2ed8e27d_0.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8c6ea083fcbe5e69a47e88498ab197b4eab85ab129750290bb634815cf9166b7
-size 83745
+oid sha256:5684aa8762d69b4ce448a526e6d8f0ad2c65dd690a40c8764384e596eaf0ef7a
+size 83590
diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingNightTest_3534351a_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingNightTest_3534351a_0.png
index b86ae526c..fa4d0e568 100644
--- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingNightTest_3534351a_0.png
+++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingNightTest_3534351a_0.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:92066e2671a0fa2733feb5701d540b612714b7f8f23b69133b0e4fc06a5b4a64
-size 46451
+oid sha256:e7f3085f7ba43f765a1fd85cb3abcffb7d6e559da92fc9c71a353a239786600b
+size 46387
diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingTest_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingTest_0.png
index aa655ebe4..1e386d0df 100644
--- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingTest_0.png
+++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingTest_0.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b7c985dfe300bc834da2b8bfc874c79ebcb5bbfc9adc42f19391f4c719a9ee4c
-size 45829
+oid sha256:03bd16dda3867f2fa18f603d71b9a7d7ac49acf5b8d5e819433226e6dbd315e2
+size 45636
diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderStartingTest_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderStartingTest_0.png
index 940bfebc0..a4fef3c50 100644
--- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderStartingTest_0.png
+++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderStartingTest_0.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6491de8f2575bb81b4de9c098ebfc0b4727efd936cd3e57fb0368c35d9a5f7c3
-size 40858
+oid sha256:d56fab1b947d2fc36723e406d56016e36c1beb9c1e35a5f66751c6bc7759d27e
+size 40685
diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsInstallTest_9f4e7551_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsInstallTest_9f4e7551_0.png
index c22a8e40a..900497091 100644
--- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsInstallTest_9f4e7551_0.png
+++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsInstallTest_9f4e7551_0.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d43c4f0e7edc8dc6df20e67e6833b9d6351b01d8e0d28aa95965fd8474874298
-size 306138
+oid sha256:716e489ba77720da9e1fe56a7d34a3aea8e9a8907def61eb4479447cf1f7c78d
+size 306602
diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsNightTest_150baa59_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsNightTest_150baa59_0.png
index 33c3b667d..d3e9f4634 100644
--- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsNightTest_150baa59_0.png
+++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsNightTest_150baa59_0.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:13101051c48afbeba93de01bec1927e26e7644935e944f235305a2630e4f316d
-size 362639
+oid sha256:1dba67602ed49df8ee22fe0d53193e17812dcfc8663a8c3da2cf937522287b3e
+size 346513
diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/discover/DiscoverTestKt/DiscoverTest_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/discover/DiscoverTestKt/DiscoverTest_2ed8e27d_0.png
index 2a344adb7..3e42ba9f7 100644
--- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/discover/DiscoverTestKt/DiscoverTest_2ed8e27d_0.png
+++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/discover/DiscoverTestKt/DiscoverTest_2ed8e27d_0.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3bffaab8294637b504398af638e91187ff23640466f9775668ec00490436effe
-size 151906
+oid sha256:3e6fec5c784a921bf2e1f6d3460e8b9815d437cd28f0b23c5ff0c0c5164418f8
+size 163366
diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListLoadingTest_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListLoadingTest_2ed8e27d_0.png
index 6e3ea2dd7..284615bf6 100644
--- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListLoadingTest_2ed8e27d_0.png
+++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListLoadingTest_2ed8e27d_0.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3e0ccc94cd164fd9b0b5b5419d4961f4ad3f5d7467f656ed2564426266edf424
-size 30001
+oid sha256:b4be452a3d0bf27035f605f143e409c2a62f5f19bcde05d53b25d5f176ac7d5c
+size 29881
diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListNightTest_1def2e7f_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListNightTest_1def2e7f_0.png
index 4ba2abea2..26ceedb37 100644
--- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListNightTest_1def2e7f_0.png
+++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListNightTest_1def2e7f_0.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:eedc635af9bb23df02184f47f4359eb5c9a73d62e972c224c4791a3af61b6b49
-size 81100
+oid sha256:700f140725b0f3e42ef8fe40ed2193bb1235ec65e0ef4504a8e4af365230b566
+size 80956
diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListTest_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListTest_2ed8e27d_0.png
index 596d60e99..6c941d4a7 100644
--- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListTest_2ed8e27d_0.png
+++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListTest_2ed8e27d_0.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2890befec321390ce0fd43965f1495b45550a6f258b38429c432100823e2370b
-size 78311
+oid sha256:03a251fcbc37a99def2b5544e0e245e587e3bfdc507495a9adcf3548c6f247c6
+size 78178
diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyStateTest_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyStateTest_2ed8e27d_0.png
index 0737be26d..3c1e33fd3 100644
--- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyStateTest_2ed8e27d_0.png
+++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyStateTest_2ed8e27d_0.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6eb1c82199e7d0ace698bdc95b523bef51a761eaa22698dfefae3ff5f2419ca3
-size 56180
+oid sha256:9d61085047b8397d4f99513c930fac8bf4b5e6fcdd6f38a5c6aa177eb8a294be
+size 119701
diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyTest_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyTest_2ed8e27d_0.png
index 90a354891..262aeb078 100644
--- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyTest_2ed8e27d_0.png
+++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyTest_2ed8e27d_0.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b84731273d6b914154b20cd44c6a0ef56b6d5fd0815d0585e6d39da75b75760b
-size 31966
+oid sha256:119b1d10a0e9926c96197938152d5ef6e2e15c71017064e48af612a794e82082
+size 32593
diff --git a/app/src/test/java/org/fdroid/install/SessionInstallManagerTest.kt b/app/src/test/java/org/fdroid/install/SessionInstallManagerTest.kt
index ccc838d93..19355e207 100644
--- a/app/src/test/java/org/fdroid/install/SessionInstallManagerTest.kt
+++ b/app/src/test/java/org/fdroid/install/SessionInstallManagerTest.kt
@@ -259,7 +259,8 @@ internal class SessionInstallManagerTest {
every { packageInstaller.openSession(999) } returns mockk(relaxed = true)
val preapprovalReceiver: InstallBroadcastReceiver = mockk(relaxed = true)
every { receiverFactory.create(999, any()) } returns preapprovalReceiver
- every { registerReceiver(context, preapprovalReceiver, any(), any()) } returns mockk()
+ every { registerReceiver(context, preapprovalReceiver, any(), any()) } returns
+ mockk()
val iconError =
sessionInstallManager.requestPreapproval(
@@ -352,6 +353,42 @@ internal class SessionInstallManagerTest {
verify(atLeast = 1) { context.unregisterReceiver(any()) }
}
+ @Test
+ fun `install ignores duplicate callbacks after completion`() = runBlocking {
+ val apkFile: File =
+ tmpFolder.newFile("app-duplicate-callback.apk").apply { writeBytes(byteArrayOf(7)) }
+
+ val listenerSlot = slot()
+ every { receiverFactory.create(sessionId, capture(listenerSlot)) } returns receiver
+
+ val sender: IntentSender = mockk(relaxed = true)
+ every { PendingIntent.getBroadcast(any(), any(), any(), any()) } returns pendingIntent
+ every { pendingIntent.intentSender } returns sender
+
+ every { packageInstaller.openSession(sessionId) } returns session
+ every { session.openWrite(packageName, 0, apkFile.length()) } returns ByteArrayOutputStream()
+ every { session.fsync(any()) } just runs
+ every { session.commit(any()) } answers
+ {
+ listenerSlot.captured.invoke(receiver, PackageInstaller.STATUS_SUCCESS, null, null)
+ listenerSlot.captured.invoke(receiver, PackageInstaller.STATUS_FAILURE_ABORTED, null, null)
+ }
+ every { session.close() } just Runs
+ every { registerReceiver(context, receiver, any(), any()) } returns mockk()
+
+ val result =
+ sessionInstallManager.install(
+ sessionId = sessionId,
+ packageName = packageName,
+ state = installingState,
+ apkFile = apkFile,
+ )
+
+ // first state wins, even though we got STATUS_FAILURE_ABORTED afterward
+ assertIs(result)
+ verify(atLeast = 1) { context.unregisterReceiver(any()) }
+ }
+
@Test
fun `install returns UserConfirmationNeeded when STATUS_PENDING_USER_ACTION`() {
runBlocking {
@@ -419,15 +456,14 @@ internal class SessionInstallManagerTest {
// start the installation in the background so we can cancel() it
val installScope = CoroutineScope(Dispatchers.Unconfined)
- val installJob =
- installScope.async {
- sessionInstallManager.install(
- sessionId = sessionId,
- packageName = packageName,
- state = installingState,
- apkFile = apkFile,
- )
- }
+ val installJob = installScope.async {
+ sessionInstallManager.install(
+ sessionId = sessionId,
+ packageName = packageName,
+ state = installingState,
+ apkFile = apkFile,
+ )
+ }
installJob.cancelAndJoin()
verify(atLeast = 1) { context.unregisterReceiver(receiver) }
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index a19e13c91..2d11b55cf 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,7 +1,7 @@
[versions]
compileSdk = "36"
kotlin = "2.3.20"
-androidGradlePlugin = "9.1.0"
+androidGradlePlugin = "9.1.1"
androidKspPlugin = "2.3.6"
hilt = "2.59.2"
hiltWork = "1.3.0"
@@ -11,8 +11,8 @@ mavenPublish = "0.35.0"
screenshot = "0.0.1-alpha13"
ktfmt = "0.26.0"
-kotlinxSerializationCore = "1.10.0"
-kotlinxSerializationJson = "1.10.0"
+kotlinxSerializationCore = "1.11.0"
+kotlinxSerializationJson = "1.11.0"
kotlinxCoroutinesTest = "1.10.2"
ktor = "3.4.2"
@@ -22,7 +22,7 @@ glide = "5.0.5"
glideCompose = "1.0.0-beta08"
coilCompose = "3.4.0"
molecule = "2.2.0"
-hints = "2.0.1"
+hints = "2.2.1"
composePreference = "2.2.0"
androidxCoreKtx = "1.18.0"
@@ -43,7 +43,7 @@ androidxActivityCompose = "1.13.0"
accompanistDrawablepainter = "0.37.3"
# navigation3
-nav3Core = "1.1.0-rc01"
+nav3Core = "1.1.0"
lifecycleViewmodelNav3 = "2.10.0"
material3AdaptiveNav3 = "1.0.0-alpha03"
@@ -54,14 +54,13 @@ guardianprojectNetcipher = "2.2.0-alpha"
guardianprojectPanic = "1.0"
acra = "5.13.1"
adapterdelegates4 = "4.3.2"
-#noinspection GradleDependency Commons IO > 2.5 uses java.nio.file, which requires desugaring
-commonsIo = "2.6"
-commonsNet = "3.6"
-bouncycastle = "1.83"
+commonsIo = "2.21.0"
+commonsNet = "3.13.0"
+bouncycastle = "1.84"
#noinspection NewerVersionAvailable upgrade to 3.6.1 failed BonjourManagerTest
jmdns = "3.5.5"
nanohttpd = "2.3.1"
-guava = "33.5.0-android"
+guava = "33.6.0-android"
rxjava = "3.1.12"
rxandroid = "3.0.2"
@@ -87,7 +86,7 @@ mockitoCore = "5.1.1"
hamcrest = "2.2"
goncalossilvaResources = "0.15.0"
turbine = "1.2.1"
-json = "20220320"
+json = "20251224"
[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
@@ -172,6 +171,7 @@ ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
+ktor-client-encoding = { module = "io.ktor:ktor-client-encoding", version.ref = "ktor" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
goncalossilva-resources = { module = "com.goncalossilva:resources", version.ref = "goncalossilvaResources" }
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index efecf23a6..1a14e07d8 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -933,6 +933,11 @@
+
+
+
+
+
@@ -943,6 +948,11 @@
+
+
+
+
+
@@ -1203,6 +1213,11 @@
+
+
+
+
+
@@ -1213,6 +1228,11 @@
+
+
+
+
+
@@ -1623,6 +1643,11 @@
+
+
+
+
+
@@ -1633,6 +1658,11 @@
+
+
+
+
+
@@ -1648,6 +1678,11 @@
+
+
+
+
+
@@ -1658,6 +1693,11 @@
+
+
+
+
+
@@ -1668,6 +1708,11 @@
+
+
+
+
+
@@ -1678,6 +1723,11 @@
+
+
+
+
+
@@ -1688,6 +1738,11 @@
+
+
+
+
+
@@ -1698,6 +1753,11 @@
+
+
+
+
+
@@ -1708,6 +1768,11 @@
+
+
+
+
+
@@ -1718,6 +1783,11 @@
+
+
+
+
+
@@ -1728,6 +1798,11 @@
+
+
+
+
+
@@ -1738,6 +1813,11 @@
+
+
+
+
+
@@ -1748,6 +1828,11 @@
+
+
+
+
+
@@ -1758,6 +1843,11 @@
+
+
+
+
+
@@ -1768,6 +1858,11 @@
+
+
+
+
+
@@ -1778,6 +1873,11 @@
+
+
+
+
+
@@ -1788,6 +1888,11 @@
+
+
+
+
+
@@ -1798,6 +1903,11 @@
+
+
+
+
+
@@ -1808,6 +1918,11 @@
+
+
+
+
+
@@ -1818,6 +1933,11 @@
+
+
+
+
+
@@ -1828,6 +1948,11 @@
+
+
+
+
+
@@ -1838,6 +1963,11 @@
+
+
+
+
+
@@ -1853,6 +1983,11 @@
+
+
+
+
+
@@ -1863,6 +1998,11 @@
+
+
+
+
+
@@ -1873,6 +2013,11 @@
+
+
+
+
+
@@ -1883,6 +2028,11 @@
+
+
+
+
+
@@ -1893,6 +2043,11 @@
+
+
+
+
+
@@ -1923,6 +2078,11 @@
+
+
+
+
+
@@ -1933,6 +2093,11 @@
+
+
+
+
+
@@ -1943,6 +2108,11 @@
+
+
+
+
+
@@ -1953,6 +2123,11 @@
+
+
+
+
+
@@ -1963,6 +2138,11 @@
+
+
+
+
+
@@ -1978,6 +2158,11 @@
+
+
+
+
+
@@ -1998,6 +2183,11 @@
+
+
+
+
+
@@ -2008,6 +2198,11 @@
+
+
+
+
+
@@ -2018,6 +2213,11 @@
+
+
+
+
+
@@ -2028,6 +2228,11 @@
+
+
+
+
+
@@ -2038,6 +2243,11 @@
+
+
+
+
+
@@ -2048,6 +2258,11 @@
+
+
+
+
+
@@ -2068,6 +2283,11 @@
+
+
+
+
+
@@ -2078,6 +2298,11 @@
+
+
+
+
+
@@ -2088,6 +2313,11 @@
+
+
+
+
+
@@ -2098,6 +2328,11 @@
+
+
+
+
+
@@ -2108,6 +2343,11 @@
+
+
+
+
+
@@ -2118,6 +2358,11 @@
+
+
+
+
+
@@ -2128,6 +2373,11 @@
+
+
+
+
+
@@ -2138,6 +2388,11 @@
+
+
+
+
+
@@ -2148,6 +2403,11 @@
+
+
+
+
+
@@ -2158,6 +2418,11 @@
+
+
+
+
+
@@ -2168,6 +2433,11 @@
+
+
+
+
+
@@ -2178,6 +2448,11 @@
+
+
+
+
+
@@ -2188,6 +2463,11 @@
+
+
+
+
+
@@ -2198,6 +2478,11 @@
+
+
+
+
+
@@ -2208,6 +2493,11 @@
+
+
+
+
+
@@ -2218,6 +2508,11 @@
+
+
+
+
+
@@ -2228,6 +2523,11 @@
+
+
+
+
+
@@ -2238,6 +2538,11 @@
+
+
+
+
+
@@ -2248,6 +2553,11 @@
+
+
+
+
+
@@ -2628,6 +2938,11 @@
+
+
+
+
+
@@ -2708,6 +3023,11 @@
+
+
+
+
+
@@ -3033,6 +3353,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -3058,6 +3388,11 @@
+
+
+
+
+
@@ -3068,6 +3403,11 @@
+
+
+
+
+
@@ -3298,6 +3638,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -4138,6 +4488,11 @@
+
+
+
+
+
@@ -4153,6 +4508,11 @@
+
+
+
+
+
@@ -4173,6 +4533,11 @@
+
+
+
+
+
@@ -4353,11 +4718,21 @@
+
+
+
+
+
+
+
+
+
+
@@ -5138,6 +5513,11 @@
+
+
+
+
+
@@ -5148,6 +5528,11 @@
+
+
+
+
+
@@ -5168,11 +5553,21 @@
+
+
+
+
+
+
+
+
+
+
@@ -5193,6 +5588,11 @@
+
+
+
+
+
diff --git a/legacy/build.gradle b/legacy/build.gradle
index 7d6d87c97..8dfb58968 100644
--- a/legacy/build.gradle
+++ b/legacy/build.gradle
@@ -62,6 +62,8 @@ android {
}
debug {
getIsDefault().set(true)
+ minifyEnabled false
+ shrinkResources false
manifestPlaceholders = [applicationLabel: APP_NAME_DEBUG]
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
diff --git a/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/CopyUtils.kt b/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/CopyUtils.kt
new file mode 100644
index 000000000..a185ed728
--- /dev/null
+++ b/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/CopyUtils.kt
@@ -0,0 +1,12 @@
+package org.fdroid.fdroid.nearby
+
+import java.io.File
+import java.io.InputStream
+
+object CopyUtils {
+
+ @JvmStatic
+ fun copyInputStreamToFile(inputStream: InputStream, file: File) {
+ inputStream.use { input -> file.outputStream().use { output -> input.copyTo(output) } }
+ }
+}
diff --git a/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/PublicSourceDirProviderTest.java b/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/PublicSourceDirProviderTest.java
index 59d89f387..93c29bfe6 100644
--- a/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/PublicSourceDirProviderTest.java
+++ b/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/PublicSourceDirProviderTest.java
@@ -1,5 +1,6 @@
package org.fdroid.fdroid.nearby;
+import static org.fdroid.fdroid.nearby.CopyUtils.copyInputStreamToFile;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@@ -14,7 +15,6 @@ import android.provider.MediaStore;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
-import org.apache.commons.io.FileUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -57,7 +57,8 @@ public class PublicSourceDirProviderTest {
File f = null;
is = context.getContentResolver().openInputStream(uri);
f = File.createTempFile("received", ".apk");
- FileUtils.copyInputStreamToFile(is, f);
+ assertNotNull(is);
+ copyInputStreamToFile(is, f);
assertTrue("dest file " + f + " should exist", f.exists());
assertEquals(f + " should equal " + apk, apk.length(), f.length());
f.delete();
diff --git a/legacy/src/main/java/org/fdroid/fdroid/views/repos/ManageReposActivity.java b/legacy/src/main/java/org/fdroid/fdroid/views/repos/ManageReposActivity.java
index be561936a..d28dd8f8a 100644
--- a/legacy/src/main/java/org/fdroid/fdroid/views/repos/ManageReposActivity.java
+++ b/legacy/src/main/java/org/fdroid/fdroid/views/repos/ManageReposActivity.java
@@ -213,7 +213,7 @@ public class ManageReposActivity extends AppCompatActivity implements RepoAdapte
public void onToggleEnabled(Repository repo) {
if (repo.getEnabled()) {
new MaterialAlertDialogBuilder(this)
- .setMessage(R.string.repo_disable_warning)
+ .setMessage(R.string.repo_disable_dialog_warning)
.setPositiveButton(R.string.repo_disable_warning_button, (dialog, id) -> {
disableRepo(repo);
dialog.dismiss();
diff --git a/libs/database/schemas/org.fdroid.database.FDroidDatabaseInt/13.json b/libs/database/schemas/org.fdroid.database.FDroidDatabaseInt/13.json
new file mode 100644
index 000000000..e26e79617
--- /dev/null
+++ b/libs/database/schemas/org.fdroid.database.FDroidDatabaseInt/13.json
@@ -0,0 +1,1136 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 13,
+ "identityHash": "447cc6c706ec5aa97a6983c3c4a0d55b",
+ "entities": [
+ {
+ "tableName": "CoreRepository",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon` TEXT, `address` TEXT NOT NULL, `webBaseUrl` TEXT, `timestamp` INTEGER NOT NULL, `version` INTEGER, `formatVersion` TEXT, `maxAge` INTEGER, `description` TEXT NOT NULL, `certificate` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "icon",
+ "columnName": "icon",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "address",
+ "columnName": "address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "webBaseUrl",
+ "columnName": "webBaseUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "formatVersion",
+ "columnName": "formatVersion",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "maxAge",
+ "columnName": "maxAge",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "certificate",
+ "columnName": "certificate",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "repoId"
+ ]
+ }
+ },
+ {
+ "tableName": "Mirror",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `url` TEXT NOT NULL, `countryCode` TEXT, `dnsA` TEXT, `dnsAAAA` TEXT, `isPrimary` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`repoId`, `url`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "countryCode",
+ "columnName": "countryCode",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "dnsA",
+ "columnName": "dnsA",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "dnsAAAA",
+ "columnName": "dnsAAAA",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "isPrimary",
+ "columnName": "isPrimary",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId",
+ "url"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "CoreRepository",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "repoId"
+ ],
+ "referencedColumns": [
+ "repoId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "AntiFeature",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `id` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, PRIMARY KEY(`repoId`, `id`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "icon",
+ "columnName": "icon",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId",
+ "id"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "CoreRepository",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "repoId"
+ ],
+ "referencedColumns": [
+ "repoId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "Category",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `id` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, PRIMARY KEY(`repoId`, `id`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "icon",
+ "columnName": "icon",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId",
+ "id"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "CoreRepository",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "repoId"
+ ],
+ "referencedColumns": [
+ "repoId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "ReleaseChannel",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `id` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, PRIMARY KEY(`repoId`, `id`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "icon",
+ "columnName": "icon",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId",
+ "id"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "CoreRepository",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "repoId"
+ ],
+ "referencedColumns": [
+ "repoId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "RepositoryPreferences",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `lastUpdated` INTEGER, `lastETag` TEXT, `userMirrors` TEXT, `disabledMirrors` TEXT, `username` TEXT, `password` TEXT, `errorCount` INTEGER NOT NULL DEFAULT 0, `lastError` TEXT, PRIMARY KEY(`repoId`))",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "weight",
+ "columnName": "weight",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enabled",
+ "columnName": "enabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastUpdated",
+ "columnName": "lastUpdated",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "lastETag",
+ "columnName": "lastETag",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "userMirrors",
+ "columnName": "userMirrors",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "disabledMirrors",
+ "columnName": "disabledMirrors",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "username",
+ "columnName": "username",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "password",
+ "columnName": "password",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "errorCount",
+ "columnName": "errorCount",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "lastError",
+ "columnName": "lastError",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId"
+ ]
+ }
+ },
+ {
+ "tableName": "AppMetadata",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `added` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, `name` TEXT, `summary` TEXT, `description` TEXT, `localizedName` TEXT, `localizedSummary` TEXT, `webSite` TEXT, `changelog` TEXT, `license` TEXT, `sourceCode` TEXT, `issueTracker` TEXT, `translation` TEXT, `preferredSigner` TEXT, `video` TEXT, `authorName` TEXT, `authorEmail` TEXT, `authorWebSite` TEXT, `authorPhone` TEXT, `donate` TEXT, `liberapayID` TEXT, `liberapay` TEXT, `openCollective` TEXT, `bitcoin` TEXT, `litecoin` TEXT, `flattrID` TEXT, `categories` TEXT, `isCompatible` INTEGER NOT NULL, PRIMARY KEY(`repoId`, `packageName`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packageName",
+ "columnName": "packageName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "added",
+ "columnName": "added",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastUpdated",
+ "columnName": "lastUpdated",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "summary",
+ "columnName": "summary",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "localizedName",
+ "columnName": "localizedName",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "localizedSummary",
+ "columnName": "localizedSummary",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "webSite",
+ "columnName": "webSite",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "changelog",
+ "columnName": "changelog",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "license",
+ "columnName": "license",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "sourceCode",
+ "columnName": "sourceCode",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "issueTracker",
+ "columnName": "issueTracker",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "translation",
+ "columnName": "translation",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "preferredSigner",
+ "columnName": "preferredSigner",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "video",
+ "columnName": "video",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "authorName",
+ "columnName": "authorName",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "authorEmail",
+ "columnName": "authorEmail",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "authorWebSite",
+ "columnName": "authorWebSite",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "authorPhone",
+ "columnName": "authorPhone",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "donate",
+ "columnName": "donate",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "liberapayID",
+ "columnName": "liberapayID",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "liberapay",
+ "columnName": "liberapay",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "openCollective",
+ "columnName": "openCollective",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "bitcoin",
+ "columnName": "bitcoin",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "litecoin",
+ "columnName": "litecoin",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "flattrID",
+ "columnName": "flattrID",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "categories",
+ "columnName": "categories",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "isCompatible",
+ "columnName": "isCompatible",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId",
+ "packageName"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_AppMetadata_authorName",
+ "unique": false,
+ "columnNames": [
+ "authorName"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AppMetadata_authorName` ON `${TABLE_NAME}` (`authorName`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "CoreRepository",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "repoId"
+ ],
+ "referencedColumns": [
+ "repoId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "AppMetadataFts",
+ "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`repoId` INTEGER NOT NULL, `name` TEXT, `summary` TEXT, `description` TEXT, `authorName` TEXT, `packageName` TEXT NOT NULL, tokenize=unicode61 `remove_diacritics=1` `separators=.` `tokenchars=-`, content=`AppMetadata`, notindexed=`repoId`)",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "summary",
+ "columnName": "summary",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "authorName",
+ "columnName": "authorName",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "packageName",
+ "columnName": "packageName",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": []
+ },
+ "ftsVersion": "FTS4",
+ "ftsOptions": {
+ "tokenizer": "unicode61",
+ "tokenizerArgs": [
+ "remove_diacritics=1",
+ "separators=.",
+ "tokenchars=-"
+ ],
+ "contentTable": "AppMetadata",
+ "languageIdColumnName": "",
+ "matchInfo": "FTS4",
+ "notIndexedColumns": [
+ "repoId"
+ ],
+ "prefixSizes": [],
+ "preferredOrder": "ASC"
+ },
+ "contentSyncTriggers": [
+ "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_BEFORE_UPDATE BEFORE UPDATE ON `AppMetadata` BEGIN DELETE FROM `AppMetadataFts` WHERE `docid`=OLD.`rowid`; END",
+ "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_BEFORE_DELETE BEFORE DELETE ON `AppMetadata` BEGIN DELETE FROM `AppMetadataFts` WHERE `docid`=OLD.`rowid`; END",
+ "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_AFTER_UPDATE AFTER UPDATE ON `AppMetadata` BEGIN INSERT INTO `AppMetadataFts`(`docid`, `repoId`, `name`, `summary`, `description`, `authorName`, `packageName`) VALUES (NEW.`rowid`, NEW.`repoId`, NEW.`name`, NEW.`summary`, NEW.`description`, NEW.`authorName`, NEW.`packageName`); END",
+ "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_AFTER_INSERT AFTER INSERT ON `AppMetadata` BEGIN INSERT INTO `AppMetadataFts`(`docid`, `repoId`, `name`, `summary`, `description`, `authorName`, `packageName`) VALUES (NEW.`rowid`, NEW.`repoId`, NEW.`name`, NEW.`summary`, NEW.`description`, NEW.`authorName`, NEW.`packageName`); END"
+ ]
+ },
+ {
+ "tableName": "LocalizedFile",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `type` TEXT NOT NULL, `locale` TEXT NOT NULL, `name` TEXT NOT NULL, `sha256` TEXT, `size` INTEGER, `ipfsCidV1` TEXT, PRIMARY KEY(`repoId`, `packageName`, `type`, `locale`), FOREIGN KEY(`repoId`, `packageName`) REFERENCES `AppMetadata`(`repoId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packageName",
+ "columnName": "packageName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "locale",
+ "columnName": "locale",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sha256",
+ "columnName": "sha256",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "ipfsCidV1",
+ "columnName": "ipfsCidV1",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId",
+ "packageName",
+ "type",
+ "locale"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_LocalizedFile_packageName",
+ "unique": false,
+ "columnNames": [
+ "packageName"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LocalizedFile_packageName` ON `${TABLE_NAME}` (`packageName`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AppMetadata",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "repoId",
+ "packageName"
+ ],
+ "referencedColumns": [
+ "repoId",
+ "packageName"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LocalizedFileList",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `type` TEXT NOT NULL, `locale` TEXT NOT NULL, `name` TEXT NOT NULL, `sha256` TEXT, `size` INTEGER, `ipfsCidV1` TEXT, PRIMARY KEY(`repoId`, `packageName`, `type`, `locale`, `name`), FOREIGN KEY(`repoId`, `packageName`) REFERENCES `AppMetadata`(`repoId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packageName",
+ "columnName": "packageName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "locale",
+ "columnName": "locale",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sha256",
+ "columnName": "sha256",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "ipfsCidV1",
+ "columnName": "ipfsCidV1",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId",
+ "packageName",
+ "type",
+ "locale",
+ "name"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_LocalizedFileList_packageName_repoId",
+ "unique": false,
+ "columnNames": [
+ "packageName",
+ "repoId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LocalizedFileList_packageName_repoId` ON `${TABLE_NAME}` (`packageName`, `repoId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AppMetadata",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "repoId",
+ "packageName"
+ ],
+ "referencedColumns": [
+ "repoId",
+ "packageName"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "Version",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `versionId` TEXT NOT NULL, `added` INTEGER NOT NULL, `releaseChannels` TEXT, `antiFeatures` TEXT, `whatsNew` TEXT, `appLabel` TEXT, `isCompatible` INTEGER NOT NULL, `file_name` TEXT NOT NULL, `file_sha256` TEXT NOT NULL, `file_size` INTEGER, `file_ipfsCidV1` TEXT, `src_name` TEXT, `src_sha256` TEXT, `src_size` INTEGER, `src_ipfsCidV1` TEXT, `manifest_versionName` TEXT NOT NULL, `manifest_versionCode` INTEGER NOT NULL, `manifest_maxSdkVersion` INTEGER, `manifest_nativecode` TEXT, `manifest_features` TEXT, `manifest_usesSdk_minSdkVersion` INTEGER, `manifest_usesSdk_targetSdkVersion` INTEGER, `manifest_signer_sha256` TEXT, `manifest_signer_hasMultipleSigners` INTEGER, PRIMARY KEY(`repoId`, `packageName`, `versionId`), FOREIGN KEY(`repoId`, `packageName`) REFERENCES `AppMetadata`(`repoId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packageName",
+ "columnName": "packageName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "versionId",
+ "columnName": "versionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "added",
+ "columnName": "added",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "releaseChannels",
+ "columnName": "releaseChannels",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "antiFeatures",
+ "columnName": "antiFeatures",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "whatsNew",
+ "columnName": "whatsNew",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "appLabel",
+ "columnName": "appLabel",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "isCompatible",
+ "columnName": "isCompatible",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "file.name",
+ "columnName": "file_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "file.sha256",
+ "columnName": "file_sha256",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "file.size",
+ "columnName": "file_size",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "file.ipfsCidV1",
+ "columnName": "file_ipfsCidV1",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "src.name",
+ "columnName": "src_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "src.sha256",
+ "columnName": "src_sha256",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "src.size",
+ "columnName": "src_size",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "src.ipfsCidV1",
+ "columnName": "src_ipfsCidV1",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "manifest.versionName",
+ "columnName": "manifest_versionName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "manifest.versionCode",
+ "columnName": "manifest_versionCode",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "manifest.maxSdkVersion",
+ "columnName": "manifest_maxSdkVersion",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "manifest.nativecode",
+ "columnName": "manifest_nativecode",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "manifest.features",
+ "columnName": "manifest_features",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "manifest.usesSdk.minSdkVersion",
+ "columnName": "manifest_usesSdk_minSdkVersion",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "manifest.usesSdk.targetSdkVersion",
+ "columnName": "manifest_usesSdk_targetSdkVersion",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "manifest.signer.sha256",
+ "columnName": "manifest_signer_sha256",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "manifest.signer.hasMultipleSigners",
+ "columnName": "manifest_signer_hasMultipleSigners",
+ "affinity": "INTEGER"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId",
+ "packageName",
+ "versionId"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "AppMetadata",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "repoId",
+ "packageName"
+ ],
+ "referencedColumns": [
+ "repoId",
+ "packageName"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "VersionedString",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `versionId` TEXT NOT NULL, `type` TEXT NOT NULL, `name` TEXT NOT NULL, `version` INTEGER, PRIMARY KEY(`repoId`, `packageName`, `versionId`, `type`, `name`), FOREIGN KEY(`repoId`, `packageName`, `versionId`) REFERENCES `Version`(`repoId`, `packageName`, `versionId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "repoId",
+ "columnName": "repoId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packageName",
+ "columnName": "packageName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "versionId",
+ "columnName": "versionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "repoId",
+ "packageName",
+ "versionId",
+ "type",
+ "name"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_VersionedString_versionId",
+ "unique": false,
+ "columnNames": [
+ "versionId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_VersionedString_versionId` ON `${TABLE_NAME}` (`versionId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "Version",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "repoId",
+ "packageName",
+ "versionId"
+ ],
+ "referencedColumns": [
+ "repoId",
+ "packageName",
+ "versionId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "AppPrefs",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `ignoreVersionCodeUpdate` INTEGER NOT NULL, `preferredRepoId` INTEGER, `appPrefReleaseChannels` TEXT, PRIMARY KEY(`packageName`))",
+ "fields": [
+ {
+ "fieldPath": "packageName",
+ "columnName": "packageName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ignoreVersionCodeUpdate",
+ "columnName": "ignoreVersionCodeUpdate",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "preferredRepoId",
+ "columnName": "preferredRepoId",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "appPrefReleaseChannels",
+ "columnName": "appPrefReleaseChannels",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "packageName"
+ ]
+ }
+ },
+ {
+ "tableName": "DbMetadata",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))",
+ "fields": [
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "key"
+ ]
+ }
+ }
+ ],
+ "views": [
+ {
+ "viewName": "LocalizedIcon",
+ "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM LocalizedFile WHERE type='icon'"
+ },
+ {
+ "viewName": "HighestVersion",
+ "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT repoId, packageName, antiFeatures FROM Version\n GROUP BY repoId, packageName HAVING MAX(manifest_versionCode)"
+ },
+ {
+ "viewName": "PreferredRepo",
+ "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT packageName, repoId AS preferredRepoId FROM AppMetadata\n JOIN RepositoryPreferences AS pref USING (repoId)\n LEFT JOIN AppPrefs USING (packageName)\n WHERE pref.enabled = 1 AND (repoId = COALESCE(preferredRepoId, repoId) OR\n NOT EXISTS (SELECT 1 FROM AppMetadata WHERE repoId=AppPrefs.preferredRepoId AND packageName=AppPrefs.packageName)\n )\n GROUP BY packageName HAVING MAX(pref.weight)"
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '447cc6c706ec5aa97a6983c3c4a0d55b')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/libs/database/src/main/java/org/fdroid/database/DbMetadata.kt b/libs/database/src/main/java/org/fdroid/database/DbMetadata.kt
new file mode 100644
index 000000000..148e12de0
--- /dev/null
+++ b/libs/database/src/main/java/org/fdroid/database/DbMetadata.kt
@@ -0,0 +1,23 @@
+package org.fdroid.database
+
+import androidx.room.Dao
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.PrimaryKey
+import androidx.room.Query
+
+@Entity
+internal data class DbMetadata(
+ @PrimaryKey val key: String,
+ val value: String
+)
+
+@Dao
+internal interface DbMetadataDao {
+ @Query("SELECT value FROM DbMetadata WHERE `key` = :key")
+ suspend fun get(key: String): String?
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun set(vararg entries: DbMetadata)
+}
diff --git a/libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt b/libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt
index 343bbcdca..058f2c5fc 100644
--- a/libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt
+++ b/libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt
@@ -18,7 +18,7 @@ import org.fdroid.LocaleChooser.getBestLocale
// When bumping this version, please make sure to add one (or more) migration(s) below!
// Consider also providing tests for that migration.
// Don't forget to commit the new schema to the git repo as well.
- version = 12,
+ version = 13,
entities =
[
// repo
@@ -38,6 +38,8 @@ import org.fdroid.LocaleChooser.getBestLocale
VersionedString::class,
// app user preferences
AppPrefs::class,
+ // internal metadata
+ DbMetadata::class,
],
views = [LocalizedIcon::class, HighestVersion::class, PreferredRepo::class],
exportSchema = true,
@@ -54,6 +56,7 @@ import org.fdroid.LocaleChooser.getBestLocale
AutoMigration(9, 10),
AutoMigration(10, 11),
AutoMigration(11, 12),
+ AutoMigration(12, 13, DbMetadataMigration::class),
// add future migrations above!
],
)
@@ -69,6 +72,8 @@ internal abstract class FDroidDatabaseInt : RoomDatabase(), FDroidDatabase, Clos
abstract override fun getAppPrefsDao(): AppPrefsDaoInt
+ abstract fun getDbMetadataDao(): DbMetadataDao
+
@Deprecated("Will be removed in future version")
override fun afterLocalesChanged(locales: LocaleListCompat) {
val appDao = getAppDao()
diff --git a/libs/database/src/main/java/org/fdroid/database/FDroidDatabaseHolder.kt b/libs/database/src/main/java/org/fdroid/database/FDroidDatabaseHolder.kt
index c68178a09..e961417f0 100644
--- a/libs/database/src/main/java/org/fdroid/database/FDroidDatabaseHolder.kt
+++ b/libs/database/src/main/java/org/fdroid/database/FDroidDatabaseHolder.kt
@@ -4,23 +4,18 @@ import android.content.Context
import android.util.Log
import androidx.annotation.GuardedBy
import androidx.room.Room
-import androidx.room.RoomDatabase
-import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.room.withTransaction
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
-import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
/**
* A way to pre-populate the database with a fixture. This can be supplied to
* [FDroidDatabaseHolder.getDb] and will then be called when a new database is created.
*/
public fun interface FDroidFixture {
- /**
- * Called when a new database gets created. Multiple DB operations should use
- * [FDroidDatabase.runInTransaction].
- */
+ /** Called when a new database gets created. This gets run inside a transaction. */
public fun prePopulateDb(db: FDroidDatabase)
}
@@ -61,8 +56,6 @@ public object FDroidDatabaseHolder {
// We allow destructive migration (if no real migration was provided),
// so we have the option to nuke the DB in production (if that will ever be needed).
fallbackToDestructiveMigration(false)
- // Add our [FixtureCallback] if a fixture was provided
- if (fixture != null) addCallback(FixtureCallback(fixture))
if (logSlowQueries) {
openHelperFactory(
@@ -73,27 +66,33 @@ public object FDroidDatabaseHolder {
)
}
}
- val instance = builder.build()
- this.instance = instance
- // return instance
- instance
+ val db = builder.build()
+ runBlocking { runFixtureIfNeeded(db, fixture) }
+ instance = db
+ db
}
}
- private class FixtureCallback(private val fixture: FDroidFixture) : RoomDatabase.Callback() {
- override fun onCreate(db: SupportSQLiteDatabase) {
- super.onCreate(db)
- @OptIn(DelicateCoroutinesApi::class)
- GlobalScope.launch(dispatcher) {
- val database: FDroidDatabase
- synchronized(lock) { database = instance ?: error("DB not yet initialized") }
- fixture.prePopulateDb(database)
- Log.d(TAG, "Loaded fixtures")
+ /**
+ * Inserts the given [fixture] into the DB if it has not been run before. We are using a flag in
+ * the [DbMetadata] table set in the same transaction to ensure fixtures get inserted only once
+ * when the DB is created.
+ *
+ * This is an ugly hack allowing us to insert fixtures into the DB while it is being constructed
+ * on the UiThread without having to allow general UiThread access to the DB.
+ */
+ private suspend fun runFixtureIfNeeded(db: FDroidDatabaseInt, fixture: FDroidFixture?) {
+ withContext(dispatcher) {
+ db.withTransaction {
+ val isSetup = db.getDbMetadataDao().get("setup")
+ if (isSetup != "true") {
+ fixture?.prePopulateDb(db)
+ db.getDbMetadataDao().set(DbMetadata("setup", "true"))
+ Log.d(TAG, "Finished initial setup")
+ } else {
+ Log.d(TAG, "DB already set up")
+ }
}
}
-
- override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
- onCreate(db)
- }
}
}
diff --git a/libs/database/src/main/java/org/fdroid/database/Migrations.kt b/libs/database/src/main/java/org/fdroid/database/Migrations.kt
index 7fbaea3d4..972bc48d0 100644
--- a/libs/database/src/main/java/org/fdroid/database/Migrations.kt
+++ b/libs/database/src/main/java/org/fdroid/database/Migrations.kt
@@ -233,3 +233,16 @@ internal val MIGRATION_8_9 =
db.execSQL("INSERT INTO AppMetadataFts(AppMetadataFts) VALUES('rebuild')")
}
}
+
+/** Raise the setup flag in the [DbMetadata] table, so we know that initial setup is done. */
+internal class DbMetadataMigration : AutoMigrationSpec {
+ override fun onPostMigrate(db: SupportSQLiteDatabase) {
+ db.beginTransaction()
+ try {
+ db.execSQL("INSERT INTO DbMetadata (key, value) VALUES ('setup', 'true')")
+ db.setTransactionSuccessful()
+ } finally {
+ db.endTransaction()
+ }
+ }
+}
diff --git a/libs/download/build.gradle.kts b/libs/download/build.gradle.kts
index 227e2ce27..e4e637b60 100644
--- a/libs/download/build.gradle.kts
+++ b/libs/download/build.gradle.kts
@@ -42,6 +42,7 @@ kotlin {
dependencies {
api(project(":libs:core"))
api(libs.ktor.client.core)
+ implementation(libs.ktor.client.encoding)
implementation(libs.microutils.kotlin.logging)
}
}
diff --git a/libs/download/src/commonMain/kotlin/org/fdroid/download/HttpManager.kt b/libs/download/src/commonMain/kotlin/org/fdroid/download/HttpManager.kt
index 20c8d7dd6..82896c80a 100644
--- a/libs/download/src/commonMain/kotlin/org/fdroid/download/HttpManager.kt
+++ b/libs/download/src/commonMain/kotlin/org/fdroid/download/HttpManager.kt
@@ -6,6 +6,7 @@ import io.ktor.client.engine.ProxyConfig
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.ResponseException
import io.ktor.client.plugins.UserAgent
+import io.ktor.client.plugins.compression.ContentEncoding
import io.ktor.client.plugins.timeout
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.basicAuth
@@ -84,6 +85,10 @@ constructor(
proxy = proxyConfig
}
install(UserAgent) { agent = userAgent }
+ install(ContentEncoding) {
+ deflate()
+ gzip()
+ }
install(HttpTimeout) {
if (highTimeouts || proxyConfig.isTor()) {
connectTimeoutMillis = TIMEOUT_MILLIS_HIGH
diff --git a/libs/download/src/commonTest/kotlin/org/fdroid/download/HttpManagerTest.kt b/libs/download/src/commonTest/kotlin/org/fdroid/download/HttpManagerTest.kt
index b061973b4..5e4d1b9e6 100644
--- a/libs/download/src/commonTest/kotlin/org/fdroid/download/HttpManagerTest.kt
+++ b/libs/download/src/commonTest/kotlin/org/fdroid/download/HttpManagerTest.kt
@@ -13,6 +13,7 @@ import io.ktor.client.engine.mock.respondRedirect
import io.ktor.client.plugins.ClientRequestException
import io.ktor.client.plugins.RedirectResponseException
import io.ktor.client.plugins.ServerResponseException
+import io.ktor.http.HttpHeaders.AcceptEncoding
import io.ktor.http.HttpHeaders.Authorization
import io.ktor.http.HttpHeaders.ETag
import io.ktor.http.HttpHeaders.UserAgent
@@ -50,7 +51,7 @@ internal class HttpManagerTest {
private val downloadRequest = DownloadRequest("foo", mirrors)
@Test
- fun testUserAgent() = runSuspend {
+ fun testUserAgentAndCompression() = runSuspend {
val mockEngine = MockEngine { respondOk() }
val httpManager = HttpManager(userAgent, null, httpClientEngineFactory = get(mockEngine))
@@ -59,6 +60,7 @@ internal class HttpManagerTest {
mockEngine.requestHistory.forEach { request ->
assertEquals(userAgent, request.headers[UserAgent])
+ assertEquals("deflate,gzip", request.headers[AcceptEncoding])
}
}
diff --git a/src/basic/fastlane/metadata/android/ar/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/ar/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/ar/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/ar/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/be/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/be/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/be/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/be/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/cs/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/cs/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/cs/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/cs/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/de/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/de/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/de/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/de/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/en-GB/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/en-GB/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/en-GB/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/en-GB/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/en-US/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/en-US/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/en-US/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/en-US/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/eo/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/eo/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/eo/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/eo/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/es/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/es/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/es/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/es/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/et/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/et/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/et/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/et/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/fa-IR/full_description.txt b/src/basic/fastlane/metadata/android/fa/full_description.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/fa-IR/full_description.txt
rename to src/basic/fastlane/metadata/android/fa/full_description.txt
diff --git a/src/basic/fastlane/metadata/android/fa-IR/short_description.txt b/src/basic/fastlane/metadata/android/fa/short_description.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/fa-IR/short_description.txt
rename to src/basic/fastlane/metadata/android/fa/short_description.txt
diff --git a/src/basic/fastlane/metadata/android/fa-IR/title.txt b/src/basic/fastlane/metadata/android/fa/title.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/fa-IR/title.txt
rename to src/basic/fastlane/metadata/android/fa/title.txt
diff --git a/src/basic/fastlane/metadata/android/fr/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/fr/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/fr/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/fr/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/ga/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/ga/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/ga/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/ga/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/hu-HU/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/hu/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/hu-HU/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/hu/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/hu-HU/full_description.txt b/src/basic/fastlane/metadata/android/hu/full_description.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/hu-HU/full_description.txt
rename to src/basic/fastlane/metadata/android/hu/full_description.txt
diff --git a/src/basic/fastlane/metadata/android/hu-HU/short_description.txt b/src/basic/fastlane/metadata/android/hu/short_description.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/hu-HU/short_description.txt
rename to src/basic/fastlane/metadata/android/hu/short_description.txt
diff --git a/src/basic/fastlane/metadata/android/hu-HU/title.txt b/src/basic/fastlane/metadata/android/hu/title.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/hu-HU/title.txt
rename to src/basic/fastlane/metadata/android/hu/title.txt
diff --git a/src/basic/fastlane/metadata/android/id/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/id/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/id/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/id/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/is-IS/full_description.txt b/src/basic/fastlane/metadata/android/is/full_description.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/is-IS/full_description.txt
rename to src/basic/fastlane/metadata/android/is/full_description.txt
diff --git a/src/basic/fastlane/metadata/android/is-IS/short_description.txt b/src/basic/fastlane/metadata/android/is/short_description.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/is-IS/short_description.txt
rename to src/basic/fastlane/metadata/android/is/short_description.txt
diff --git a/src/basic/fastlane/metadata/android/is-IS/title.txt b/src/basic/fastlane/metadata/android/is/title.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/is-IS/title.txt
rename to src/basic/fastlane/metadata/android/is/title.txt
diff --git a/src/basic/fastlane/metadata/android/ja/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/ja/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/ja/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/ja/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/nb/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/nb/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/nb/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/nb/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/nl/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/nl/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/nl/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/nl/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/pl/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/pl/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/pl/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/pl/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/pt-BR/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/pt-BR/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/pt-BR/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/pt-BR/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/pt-PT/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/pt-PT/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/pt-PT/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/pt-PT/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/pt/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/pt/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/pt/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/pt/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/ru/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/ru/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/ru/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/ru/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/sv-SE/short_description.txt b/src/basic/fastlane/metadata/android/sv-SE/short_description.txt
deleted file mode 100644
index 879b0dca3..000000000
--- a/src/basic/fastlane/metadata/android/sv-SE/short_description.txt
+++ /dev/null
@@ -1 +0,0 @@
-Den minimala klient-appen för app-butiken som respekterar frihet och privatliv
diff --git a/src/basic/fastlane/metadata/android/sv-SE/title.txt b/src/basic/fastlane/metadata/android/sv-SE/title.txt
deleted file mode 100644
index 996a1c6b2..000000000
--- a/src/basic/fastlane/metadata/android/sv-SE/title.txt
+++ /dev/null
@@ -1 +0,0 @@
-F-Droid Basic
diff --git a/src/basic/fastlane/metadata/android/sv/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/sv/changelogs/2000008.txt
deleted file mode 100644
index 898797168..000000000
--- a/src/basic/fastlane/metadata/android/sv/changelogs/2000008.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-* Användargränssnittet har skrivits om från grunden med Kotlin Compose
-* förbättrad sök, som nu även söker i beskrivningar och översättningar
-* enklare att upptäcka nya appar, med markering av de mest nedladdade
-* godkännande av installation *innan* nedladdning
-* flera uppdateringar/nedladdningar samtidigt
-* meddelanden till användaren om problem med appar (t.ex. ändrad signeringsnyckel)
-* valfritt färgtema i Material You
-* förbättrad filtrering av listor
diff --git a/src/basic/fastlane/metadata/android/sv-SE/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/sv/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/sv-SE/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/sv/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/sv-SE/full_description.txt b/src/basic/fastlane/metadata/android/sv/full_description.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/sv-SE/full_description.txt
rename to src/basic/fastlane/metadata/android/sv/full_description.txt
diff --git a/src/basic/fastlane/metadata/android/sv/short_description.txt b/src/basic/fastlane/metadata/android/sv/short_description.txt
index 40e8147f6..879b0dca3 100644
--- a/src/basic/fastlane/metadata/android/sv/short_description.txt
+++ b/src/basic/fastlane/metadata/android/sv/short_description.txt
@@ -1 +1 @@
-Den minimalistiska klient-appen för app-butiken som respekterar frihet och privatliv
+Den minimala klient-appen för app-butiken som respekterar frihet och privatliv
diff --git a/src/basic/fastlane/metadata/android/sv/title.txt b/src/basic/fastlane/metadata/android/sv/title.txt
index a97be6208..996a1c6b2 100644
--- a/src/basic/fastlane/metadata/android/sv/title.txt
+++ b/src/basic/fastlane/metadata/android/sv/title.txt
@@ -1 +1 @@
-Grundläggande F-Droid
+F-Droid Basic
diff --git a/src/basic/fastlane/metadata/android/sw/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/sw/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/sw/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/sw/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/ta-IN/full_description.txt b/src/basic/fastlane/metadata/android/ta/full_description.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/ta-IN/full_description.txt
rename to src/basic/fastlane/metadata/android/ta/full_description.txt
diff --git a/src/basic/fastlane/metadata/android/ta-IN/short_description.txt b/src/basic/fastlane/metadata/android/ta/short_description.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/ta-IN/short_description.txt
rename to src/basic/fastlane/metadata/android/ta/short_description.txt
diff --git a/src/basic/fastlane/metadata/android/ta-IN/title.txt b/src/basic/fastlane/metadata/android/ta/title.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/ta-IN/title.txt
rename to src/basic/fastlane/metadata/android/ta/title.txt
diff --git a/src/basic/fastlane/metadata/android/tr/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/tr/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/tr/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/tr/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/uk/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/uk/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/uk/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/uk/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/zh-CN/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/zh-CN/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/zh-CN/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/zh-CN/changelogs/2000009.txt
diff --git a/src/basic/fastlane/metadata/android/zh-TW/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/zh-TW/changelogs/2000009.txt
similarity index 100%
rename from src/basic/fastlane/metadata/android/zh-TW/changelogs/2000008.txt
rename to src/basic/fastlane/metadata/android/zh-TW/changelogs/2000009.txt