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>, appsWithIssuesFlow: StateFlow?>, installedAppsFlow: Flow>, + showUpdatesHintFlow: StateFlow, showAppIssueHintFlow: StateFlow, searchQueryFlow: StateFlow, sortOrderFlow: StateFlow, @@ -36,52 +37,48 @@ fun MyAppsPresenter( // we want to show apps currently installing/updating even if they have updates available, // so we need to handle those first - val installingApps = - appInstallStates.mapNotNull { (packageName, state) -> - if (state is InstallStateWithInfo) { - val keep = - searchQuery.isBlank() || state.name.normalize().contains(searchQuery, ignoreCase = true) - if (keep) { - processedPackageNames.add(packageName) - InstallingAppItem(packageName, state) - } else null - } else { - null - } - } - val updates = - appUpdates?.filter { + val installingApps = appInstallStates.mapNotNull { (packageName, state) -> + if (state is InstallStateWithInfo) { val keep = - if (searchQuery.isBlank()) { - it.packageName !in processedPackageNames - } else { - it.packageName !in processedPackageNames && - it.name.normalize().contains(searchQuery, ignoreCase = true) - } - if (keep) processedPackageNames.add(it.packageName) - keep + searchQuery.isBlank() || state.name.normalize().contains(searchQuery, ignoreCase = true) + if (keep) { + processedPackageNames.add(packageName) + InstallingAppItem(packageName, state) + } else null + } else { + null } - val withIssues = - appsWithIssues?.filter { - val keep = - if (searchQuery.isBlank()) { - it.packageName !in processedPackageNames - } else { - it.packageName !in processedPackageNames && - it.name.normalize().contains(searchQuery, ignoreCase = true) - } - if (keep) processedPackageNames.add(it.packageName) - keep - } - val installed = - installedApps?.filter { + } + val updates = appUpdates?.filter { + val keep = if (searchQuery.isBlank()) { it.packageName !in processedPackageNames } else { it.packageName !in processedPackageNames && it.name.normalize().contains(searchQuery, ignoreCase = true) } + if (keep) processedPackageNames.add(it.packageName) + keep + } + val withIssues = appsWithIssues?.filter { + val keep = + if (searchQuery.isBlank()) { + it.packageName !in processedPackageNames + } else { + it.packageName !in processedPackageNames && + it.name.normalize().contains(searchQuery, ignoreCase = true) + } + if (keep) processedPackageNames.add(it.packageName) + keep + } + val installed = installedApps?.filter { + if (searchQuery.isBlank()) { + it.packageName !in processedPackageNames + } else { + it.packageName !in processedPackageNames && + it.name.normalize().contains(searchQuery, ignoreCase = true) } + } var updateBytes: Long? = 0L updates?.forEach { val size = it.update.size @@ -102,9 +99,13 @@ fun MyAppsPresenter( appUpdates = updates?.sort(sortOrder), appsWithIssue = withIssues?.sort(sortOrder), installedApps = installed?.sort(sortOrder), + showUpdatesHint = + showUpdatesHintFlow.collectAsState().value && + installingApps.none { it.installState.showProgress }, showAppIssueHint = showAppIssueHintFlow.collectAsState().value, sortOrder = sortOrder, networkState = networkStateFlow.collectAsState().value, + isSearching = searchQuery.isNotBlank(), appUpdatesBytes = updateBytes, ) } diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt index 6db150a63..53509b0df 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt @@ -99,6 +99,7 @@ constructor( appInstallStatesFlow = appInstallManager.appInstallStates, appsWithIssuesFlow = updatesManager.appsWithIssues, installedAppsFlow = installedAppItems, + showUpdatesHintFlow = onboardingManager.showMyAppsUpdatesHint, showAppIssueHintFlow = onboardingManager.showAppIssueHint, searchQueryFlow = searchQuery, sortOrderFlow = sortOrder, @@ -139,8 +140,14 @@ constructor( updatesManager.loadUpdates() } + override fun onUpdatesHintSeen() = onboardingManager.onMyAppsUpdateHintSeen() + override fun onAppIssueHintSeen() = onboardingManager.onAppIssueHintSeen() + override fun onNotWarnWhenMetered() { + settingsManager.onDontWarnOnMeteredNetwork() + } + override fun exportInstalledApps() { scope.launch { val stringBuilder = diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroup.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroup.kt index fd22601d2..dc9959a73 100644 --- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroup.kt +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroup.kt @@ -1,19 +1,96 @@ package org.fdroid.ui.categories import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Category +import androidx.compose.material.icons.filled.Construction +import androidx.compose.material.icons.filled.Factory +import androidx.compose.material.icons.filled.FavoriteBorder +import androidx.compose.material.icons.filled.Forum +import androidx.compose.material.icons.filled.NetworkCheck +import androidx.compose.material.icons.filled.PhoneAndroid +import androidx.compose.material.icons.filled.SdStorage +import androidx.compose.material.icons.filled.VideogameAsset +import androidx.compose.material.icons.filled.Wallet +import androidx.compose.ui.graphics.vector.ImageVector import org.fdroid.R -data class CategoryGroup(val id: String, @get:StringRes val name: Int) +data class CategoryGroup( + val id: String, + @get:StringRes val name: Int, + @get:StringRes val summary: Int, + val imageVector: ImageVector, +) object CategoryGroups { - val productivity = CategoryGroup("productivity", R.string.category_group_productivity) - val tools = CategoryGroup("tools", R.string.category_group_tools) - val wallets = CategoryGroup("wallets", R.string.category_group_wallets) - val media = CategoryGroup("media", R.string.category_group_media) - val communication = CategoryGroup("communication", R.string.category_group_communication) - val device = CategoryGroup("device", R.string.category_group_device) - val network = CategoryGroup("network", R.string.category_group_network) - val storage = CategoryGroup("storage", R.string.category_group_storage) - val interests = CategoryGroup("interests", R.string.category_group_interests) - val misc = CategoryGroup("misc", R.string.category_group_misc) + val productivity = + CategoryGroup( + id = "productivity", + name = R.string.category_group_productivity, + summary = R.string.category_group_summary_productivity, + imageVector = Icons.Default.Factory, + ) + val tools = + CategoryGroup( + id = "tools", + name = R.string.category_group_tools, + summary = R.string.category_group_summary_tools, + imageVector = Icons.Default.Construction, + ) + val wallets = + CategoryGroup( + id = "wallets", + name = R.string.category_group_wallets, + summary = R.string.category_group_summary_wallets, + imageVector = Icons.Default.Wallet, + ) + val media = + CategoryGroup( + id = "media", + name = R.string.category_group_media, + summary = R.string.category_group_summary_media, + imageVector = Icons.Default.VideogameAsset, + ) + val communication = + CategoryGroup( + id = "communication", + name = R.string.category_group_communication, + summary = R.string.category_group_summary_communication, + imageVector = Icons.Default.Forum, + ) + val device = + CategoryGroup( + id = "device", + name = R.string.category_group_device, + summary = R.string.category_group_summary_device, + imageVector = Icons.Default.PhoneAndroid, + ) + val network = + CategoryGroup( + id = "network", + name = R.string.category_group_network, + summary = R.string.category_group_summary_network, + imageVector = Icons.Default.NetworkCheck, + ) + val storage = + CategoryGroup( + id = "storage", + name = R.string.category_group_storage, + summary = R.string.category_group_summary_storage, + imageVector = Icons.Default.SdStorage, + ) + val interests = + CategoryGroup( + id = "interests", + name = R.string.category_group_interests, + summary = R.string.category_group_summary_interests, + imageVector = Icons.Default.FavoriteBorder, + ) + val misc = + CategoryGroup( + id = "misc", + name = R.string.category_group_misc, + summary = R.string.category_group_summary_misc, + imageVector = Icons.Default.Category, + ) } diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroupRow.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroupRow.kt new file mode 100644 index 000000000..58e3ff5f7 --- /dev/null +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroupRow.kt @@ -0,0 +1,107 @@ +package org.fdroid.ui.categories + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment.Companion.Center +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.hideFromAccessibility +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import androidx.navigation3.runtime.NavKey +import org.fdroid.ui.FDroidContent +import org.fdroid.ui.lists.AppListType +import org.fdroid.ui.navigation.NavigationKey +import org.fdroid.ui.utils.ExpandIconArrow + +@Composable +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +fun CategoryGroupRow( + group: CategoryGroup, + categories: List, + onNav: (NavKey) -> Unit, +) { + val isPreview = LocalInspectionMode.current + var expanded by rememberSaveable { mutableStateOf(isPreview) } + val onClick = { expanded = !expanded } + ListItem( + leadingContent = { + Box( + contentAlignment = Center, + modifier = + Modifier.size(40.dp) + .clip(MaterialTheme.shapes.small) + .background(MaterialTheme.colorScheme.primaryContainer), + ) { + Icon( + imageVector = group.imageVector, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.semantics { hideFromAccessibility() }, + ) + } + }, + supportingContent = { + Text(text = stringResource(group.summary), maxLines = 2, overflow = TextOverflow.Ellipsis) + }, + trailingContent = { IconButton(onClick = onClick) { ExpandIconArrow(expanded) } }, + onClick = onClick, + ) { + Text(text = stringResource(group.name), maxLines = 2, overflow = TextOverflow.Ellipsis) + } + AnimatedVisibility( + visible = expanded, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + ChipFlowRow(modifier = Modifier.padding(8.dp)) { + categories.forEach { categoryItem -> + CategoryChip( + categoryItem = categoryItem, + onClick = { + val type = AppListType.Category(categoryItem.name, categoryItem.id) + val navKey = NavigationKey.AppList(type) + onNav(navKey) + }, + ) + } + } + } +} + +@Composable +@PreviewLightDark +private fun Preview() { + FDroidContent { + val categories = + listOf( + CategoryItem("App Store & Updater", "App Store & Updater"), + CategoryItem("Browser", "Browser"), + CategoryItem("Calendar & Agenda", "Calendar & Agenda"), + ) + Column { CategoryGroupRow(CategoryGroups.productivity, categories) {} } + } +} diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt index 34d9bae11..5dfbf364f 100644 --- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt @@ -13,6 +13,7 @@ import androidx.compose.material.icons.filled.AppBlocking import androidx.compose.material.icons.filled.Apps import androidx.compose.material.icons.filled.BatteryChargingFull import androidx.compose.material.icons.filled.Bookmarks +import androidx.compose.material.icons.filled.BrowserUpdated import androidx.compose.material.icons.filled.Brush import androidx.compose.material.icons.filled.Calculate import androidx.compose.material.icons.filled.CalendarMonth @@ -74,7 +75,7 @@ import androidx.compose.material.icons.filled.Wallpaper import androidx.compose.material.icons.filled.WbSunny import androidx.compose.ui.graphics.vector.ImageVector -data class CategoryItem(val id: String, val name: String) { +data class CategoryItem(val id: String, val name: String, val description: String? = null) { val imageVector: ImageVector get() = when (id) { @@ -130,6 +131,7 @@ data class CategoryItem(val id: String, val name: String) { "Reading" -> AutoMirrored.Default.MenuBook "Recipe Manager" -> Icons.Default.RestaurantMenu "Religion" -> Icons.Default.Church + "Remote Access" -> Icons.Default.BrowserUpdated "Remote Controller" -> Icons.Default.SettingsRemote "Science & Education" -> Icons.Default.Science "Security" -> Icons.Default.Security @@ -208,6 +210,7 @@ data class CategoryItem(val id: String, val name: String) { "Reading" -> CategoryGroups.media "Recipe Manager" -> CategoryGroups.interests "Religion" -> CategoryGroups.interests + "Remote Access" -> CategoryGroups.network "Remote Controller" -> CategoryGroups.tools "Science & Education" -> CategoryGroups.interests "Security" -> CategoryGroups.device diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryList.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryList.kt index 4eb791d9e..5985c4d18 100644 --- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryList.kt +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryList.kt @@ -4,33 +4,37 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.navigation3.runtime.NavKey import org.fdroid.R import org.fdroid.ui.FDroidContent -import org.fdroid.ui.lists.AppListType -import org.fdroid.ui.navigation.NavigationKey @Composable +@OptIn(ExperimentalMaterial3ExpressiveApi::class) fun CategoryList( categoryMap: Map>?, onNav: (NavKey) -> Unit, modifier: Modifier = Modifier, ) { AnimatedVisibility(!categoryMap.isNullOrEmpty()) { - Column(modifier = modifier.padding(top = 20.dp)) { + Column(modifier = modifier) { Text( text = stringResource(R.string.main_menu__categories), - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(bottom = 8.dp, start = 16.dp, end = 16.dp), + style = MaterialTheme.typography.titleMediumEmphasized, + fontSize = 20.sp, + modifier = Modifier.padding(horizontal = 16.dp), ) // we'll sort the groups here, because before we didn't have the context to get names val res = LocalResources.current @@ -39,22 +43,12 @@ fun CategoryList( val comparator = compareBy { res.getString(it.name) } categoryMap?.toSortedMap(comparator) } - sortedMap?.forEach { (group, categories) -> - Text( - text = stringResource(group.name), - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.fillMaxWidth().padding(16.dp, 2.dp), - ) - ChipFlowRow(modifier = Modifier.padding(start = 16.dp, bottom = 12.dp)) { - categories.forEach { category -> - CategoryChip( - categoryItem = category, - onClick = { - val type = AppListType.Category(category.name, category.id) - val navKey = NavigationKey.AppList(type) - onNav(navKey) - }, - ) + OutlinedCard(modifier = Modifier.padding(16.dp)) { + // FIXME: A LazyColumn would be better, but we can't use this inside a scrollable column + Column { + sortedMap?.forEach { (group, categories) -> + CategoryGroupRow(group, categories, onNav) + HorizontalDivider(modifier = Modifier.fillMaxWidth()) } } } @@ -62,8 +56,8 @@ fun CategoryList( } } -@Preview @Composable +@PreviewLightDark fun CategoryListPreview() { FDroidContent { val categories = diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/ChipFlowRow.kt b/app/src/main/kotlin/org/fdroid/ui/categories/ChipFlowRow.kt index 9c7d4f280..7fc4beab9 100644 --- a/app/src/main/kotlin/org/fdroid/ui/categories/ChipFlowRow.kt +++ b/app/src/main/kotlin/org/fdroid/ui/categories/ChipFlowRow.kt @@ -37,7 +37,7 @@ fun ChipFlowRowFewItemsPreview() { CategoryItem("doesn't exist", "Oops"), ) - FDroidContent { ChipFlowRow { categories.map { category -> CategoryChip(category, {}) } } } + FDroidContent { ChipFlowRow { categories.forEach { category -> CategoryChip(category, {}) } } } } @Preview @@ -61,5 +61,5 @@ fun ChipFlowRowManyItemsPreview() { CategoryItem("Science & Education", "Science & Education"), ) - FDroidContent { ChipFlowRow { categories.map { category -> CategoryChip(category, {}) } } } + FDroidContent { ChipFlowRow { categories.forEach { category -> CategoryChip(category, {}) } } } } diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt b/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt index d58fe2f46..fc18fdd08 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt @@ -15,13 +15,23 @@ import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter.Companion.tint import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.layout.onVisibilityChanged import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.viktormykhailiv.compose.hints.HintAnchorState +import com.viktormykhailiv.compose.hints.hintAnchor +import com.viktormykhailiv.compose.hints.rememberHint +import com.viktormykhailiv.compose.hints.rememberHintAnchorState import org.fdroid.R import org.fdroid.ui.FDroidContent import org.fdroid.ui.utils.AsyncShimmerImage @@ -30,11 +40,38 @@ import org.fdroid.ui.utils.testApp @Composable @OptIn(ExperimentalMaterial3ExpressiveApi::class) -fun AntiFeatures(antiFeatures: List) { +fun AntiFeatures( + antiFeatures: List, + hintAnchor: HintAnchorState, + showOnboarding: Boolean, + modifier: Modifier = Modifier, + onShowOnboarding: () -> Unit, +) { + var placed by remember { mutableStateOf(false) } + var visible by remember { mutableStateOf(false) } ElevatedCard( colors = CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colorScheme.inverseSurface), - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), + modifier = + modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .hintAnchor(state = hintAnchor, shape = CardDefaults.elevatedShape) + .onPlaced { + if (visible && showOnboarding) { + onShowOnboarding() + } else { + placed = true + } + } + .onVisibilityChanged(minDurationMs = 1000) { isVisible -> + if (isVisible && placed && showOnboarding) { + onShowOnboarding() + } else if (isVisible) { + visible = true + } + } + .padding(8.dp), ) { ExpandableSection( icon = rememberVectorPainter(Icons.Default.WarningAmber), @@ -81,5 +118,7 @@ fun AntiFeatures(antiFeatures: List) { @Preview @Composable fun AntiFeaturesPreview() { - FDroidContent { AntiFeatures(testApp.antiFeatures!!) } + val hint = rememberHint {} + val hintAnchor = rememberHintAnchorState(hint) + FDroidContent { AntiFeatures(testApp.antiFeatures!!, hintAnchor, showOnboarding = false) {} } } diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt index d6bc28fea..765bba6f7 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt @@ -41,6 +41,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier @@ -66,6 +67,11 @@ import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.core.os.LocaleListCompat +import com.viktormykhailiv.compose.hints.HintProperties +import com.viktormykhailiv.compose.hints.rememberHint +import com.viktormykhailiv.compose.hints.rememberHintAnchorState +import com.viktormykhailiv.compose.hints.rememberHintController +import kotlinx.coroutines.launch import org.fdroid.LocaleChooser.getBestLocale import org.fdroid.R import org.fdroid.install.InstallState @@ -78,6 +84,8 @@ import org.fdroid.ui.lists.AppListType import org.fdroid.ui.navigation.NavigationKey import org.fdroid.ui.utils.BigLoadingIndicator import org.fdroid.ui.utils.ExpandableSection +import org.fdroid.ui.utils.OnboardingPopupCard +import org.fdroid.ui.utils.getHintOverlayColor import org.fdroid.ui.utils.testApp @Composable @@ -107,6 +115,22 @@ fun AppDetails( showInstallError = true } } + // onboarding hint plumbing + val hintController = rememberHintController(overlay = getHintOverlayColor()) + val hint = + rememberHint(HintProperties(dismissOnClickOutside = false)) { + OnboardingPopupCard( + title = stringResource(R.string.app_details_anti_features_title), + message = stringResource(R.string.app_details_anti_features_text), + modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp), + onGotIt = { + item.actions.onAntiFeaturesOnboardingSeen() + hintController.dismiss() + }, + ) + } + val hintAnchor = rememberHintAnchorState(hint) + val coroutineScope = rememberCoroutineScope() val scrollState = rememberScrollState() var size by remember { mutableStateOf(IntSize.Zero) } Column( @@ -217,11 +241,17 @@ fun AppDetails( } // Anti-features if (!item.antiFeatures.isNullOrEmpty()) { - AntiFeatures(item.antiFeatures) + AntiFeatures( + antiFeatures = item.antiFeatures, + hintAnchor = hintAnchor, + showOnboarding = item.showAntiFeaturesOnboarding, + ) { + coroutineScope.launch { hintController.show(hintAnchor) } + } } // Screenshots if (item.phoneScreenshots.isNotEmpty()) { - Screenshots(item.networkState.isMetered, item.phoneScreenshots) + Screenshots(item.networkState.showWarningDialog, item.phoneScreenshots) } // Donate card if (item.showDonate) diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt index 86276dc3d..5ad830255 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt @@ -223,6 +223,7 @@ fun AppDetailsHeader( Text( text = stringResource(strRes), style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), overflow = TextOverflow.Ellipsis, maxLines = 1, ) @@ -236,6 +237,7 @@ fun AppDetailsHeader( Text( text = stringResource(R.string.status_downloading, speedStr), style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), overflow = TextOverflow.StartEllipsis, maxLines = 1, modifier = Modifier.padding(end = 8.dp), @@ -243,6 +245,7 @@ fun AppDetailsHeader( Text( text = stringResource(R.string.status_downloading_remaining, remainingStr), style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), overflow = TextOverflow.Ellipsis, maxLines = 1, ) @@ -304,7 +307,7 @@ fun AppDetailsHeader( // button is for either installing or updating Button( onClick = { - if (item.networkState.isMetered) { + if (item.networkState.showWarningDialog) { showMeteredDialog = true } else { require(item.suggestedVersion != null) { "suggestedVersion was null" } @@ -325,7 +328,8 @@ fun AppDetailsHeader( if (showMeteredDialog) MeteredConnectionDialog( numBytes = version?.size, - onConfirm = { + onConfirm = { notWarnWhenMetered -> + if (notWarnWhenMetered) item.actions.onNotWarnWhenMetered() require(item.suggestedVersion != null) { "suggestedVersion was null" } item.actions.installAction(item.app, item.suggestedVersion, item.icon) }, diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt index 53bcfb4a0..98b7e5395 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt @@ -21,8 +21,8 @@ import org.fdroid.index.RELEASE_CHANNEL_BETA import org.fdroid.index.v2.PackageVersion import org.fdroid.install.InstallState import org.fdroid.install.SessionInstallManager -import org.fdroid.ui.categories.CategoryItem import org.fdroid.search.SearchHelper.removeZeroWhiteSpace +import org.fdroid.ui.categories.CategoryItem data class AppDetailsItem( val app: AppMetadata, @@ -62,6 +62,7 @@ data class AppDetailsItem( val appPrefs: AppPrefs? = null, val whatsNew: String? = null, val antiFeatures: List? = null, + val showAntiFeaturesOnboarding: Boolean = false, val issue: AppIssue? = null, val authorHasMoreThanOneApp: Boolean = false, val proxy: ProxyConfig? = null, @@ -82,6 +83,7 @@ data class AppDetailsItem( suggestedVersion: AppVersion?, possibleUpdate: AppVersion?, appPrefs: AppPrefs?, + showAntiFeaturesOnboarding: Boolean, issue: AppIssue?, authorHasMoreThanOneApp: Boolean, localeList: LocaleListCompat, @@ -131,6 +133,7 @@ data class AppDetailsItem( localeList = localeList, proxy = proxy, ), + showAntiFeaturesOnboarding = showAntiFeaturesOnboarding, issue = issue, authorHasMoreThanOneApp = authorHasMoreThanOneApp, proxy = proxy, @@ -239,7 +242,9 @@ data class AppDetailsActions( val onUninstallResult: (ActivityResult) -> Unit, val onRepoChanged: (Long) -> Unit, val onPreferredRepoChanged: (Long) -> Unit, + val onNotWarnWhenMetered: () -> Unit, val allowBetaVersions: () -> Unit, + val onAntiFeaturesOnboardingSeen: () -> Unit, val ignoreAllUpdates: (() -> Unit)? = null, val ignoreThisUpdate: (() -> Unit)? = null, val shareApk: Intent? = null, diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt index 01ae0b543..11b310122 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt @@ -3,11 +3,10 @@ package org.fdroid.ui.details import android.app.Application import android.content.Intent import android.content.pm.PackageInfo -import android.content.pm.PackageManager -import android.content.pm.PackageManager.GET_SIGNATURES import androidx.activity.result.ActivityResult import androidx.annotation.UiThread import androidx.annotation.WorkerThread +import androidx.core.content.pm.PackageInfoCompat.getLongVersionCode import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.application import androidx.lifecycle.viewModelScope @@ -25,9 +24,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import mu.KotlinLogging import org.fdroid.UpdateChecker import org.fdroid.database.AppMetadata @@ -40,7 +40,9 @@ import org.fdroid.index.RepoManager import org.fdroid.index.v2.PackageVersion import org.fdroid.install.AppInstallManager import org.fdroid.install.InstallState +import org.fdroid.install.InstalledAppsCache import org.fdroid.repo.RepoPreLoader +import org.fdroid.settings.OnboardingManager import org.fdroid.settings.SettingsManager import org.fdroid.updates.UpdatesManager import org.fdroid.utils.IoDispatcher @@ -60,7 +62,9 @@ constructor( private val updatesManager: UpdatesManager, private val networkMonitor: NetworkMonitor, private val settingsManager: SettingsManager, + private val onboardingManager: OnboardingManager, private val appInstallManager: AppInstallManager, + private val installedAppsCache: InstalledAppsCache, ) : AndroidViewModel(app) { private val log = KotlinLogging.logger {} private val packageInfoFlow = MutableStateFlow(null) @@ -82,6 +86,7 @@ constructor( viewModel = this, packageInfoFlow = packageInfoFlow, currentRepoIdFlow = currentRepoIdFlow, + showAntiFeaturesOnboardingFlow = onboardingManager.showAntiFeaturesOnboarding, appsWithIssuesFlow = updatesManager.appsWithIssues, networkStateFlow = networkMonitor.networkState, ) @@ -89,18 +94,20 @@ constructor( } init { - viewModelScope.launch(dispatcher) { loadPackageInfoFlow() } + viewModelScope.launch(dispatcher) { + // Collect the cached packageInfo for this app. This way we have a central place + // to react to state changes for installs, updates and uninstalls + installedAppsCache.installedApps + .map { cacheMap -> cacheMap[packageName]?.let { getLongVersionCode(it) } } + .distinctUntilChanged() + .collect { loadPackageInfoFlow() } + } } @WorkerThread private fun loadPackageInfoFlow() { val packageManager = app.packageManager - val packageInfo = - try { - @Suppress("DEPRECATION") packageManager.getPackageInfo(packageName, GET_SIGNATURES) - } catch (_: PackageManager.NameNotFoundException) { - null - } + val packageInfo = installedAppsCache.installedApps.value[packageName] packageInfoFlow.value = if (packageInfo == null) { AppInfo(packageName) @@ -123,15 +130,15 @@ constructor( packageName = packageName, appMetadata = appMetadata, version = version, - currentVersionName = packageInfoFlow.value?.packageInfo?.versionName, // TODO + currentVersionName = packageInfoFlow.value?.packageInfo?.versionName, repo = repoManager.getRepository(appMetadata.repoId) ?: return@launch, iconModel = iconModel, canAskPreApprovalNow = true, ) - if (result is InstallState.Installed) { - // to reload packageInfoFlow with fresh packageInfo - loadPackageInfoFlow() - } + log.info { "Install result was: $result" } + // We are not doing anything with that result, + // because an installation could have been triggered via 'Update all' in My Apps as well. + // So updating states need to happen elsewhere. } } @@ -139,11 +146,7 @@ constructor( fun requestUserConfirmation(installState: InstallState.UserConfirmationNeeded) { scope.launch(Dispatchers.Main) { val result = appInstallManager.requestUserConfirmation(packageName, installState) - if (result is InstallState.Installed) - withContext(Dispatchers.Main) { - // to reload packageInfoFlow with fresh packageInfo - loadPackageInfoFlow() - } + log.info { "requestUserConfirmation result was: $result" } } } @@ -164,10 +167,7 @@ constructor( fun onUninstallResult(activityResult: ActivityResult) { val name = appDetails.value?.name val result = appInstallManager.onUninstallResult(packageName, name, activityResult) - if (result is InstallState.Uninstalled) { - // to reload packageInfoFlow with fresh packageInfo - viewModelScope.launch(dispatcher) { loadPackageInfoFlow() } - } + log.info { "Uninstall result was: $result" } } @UiThread @@ -183,6 +183,11 @@ constructor( } } + @UiThread + fun onNotWarnWhenMetered() { + settingsManager.onDontWarnOnMeteredNetwork() + } + override fun onCleared() { log.info { "App details screen left: $packageName" } appInstallManager.cleanUp(packageName) @@ -230,6 +235,11 @@ constructor( } } + @UiThread + fun onAntiFeaturesOnboardingSeen() { + onboardingManager.onAntiFeaturesOnboardingSeen() + } + @AssistedFactory interface Factory { fun create(packageName: String): AppDetailsViewModel diff --git a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt index d32ced754..1e713da20 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt @@ -47,6 +47,7 @@ fun DetailsPresenter( viewModel: AppDetailsViewModel, packageInfoFlow: StateFlow, currentRepoIdFlow: StateFlow, + showAntiFeaturesOnboardingFlow: StateFlow, appsWithIssuesFlow: StateFlow?>, networkStateFlow: StateFlow, ): AppDetailsItem? { @@ -205,7 +206,9 @@ fun DetailsPresenter( onUninstallResult = viewModel::onUninstallResult, onRepoChanged = viewModel::onRepoChanged, onPreferredRepoChanged = viewModel::onPreferredRepoChanged, + onNotWarnWhenMetered = viewModel::onNotWarnWhenMetered, allowBetaVersions = viewModel::allowBetaUpdates, + onAntiFeaturesOnboardingSeen = viewModel::onAntiFeaturesOnboardingSeen, ignoreAllUpdates = if (installedVersionCode == null) { null @@ -265,6 +268,7 @@ fun DetailsPresenter( suggestedVersion = suggestedVersion, possibleUpdate = possibleUpdate, appPrefs = appPrefs, + showAntiFeaturesOnboarding = showAntiFeaturesOnboardingFlow.collectAsState().value, issue = issue, authorHasMoreThanOneApp = authorHasMoreThanOneApp, localeList = locales, diff --git a/app/src/main/kotlin/org/fdroid/ui/details/Versions.kt b/app/src/main/kotlin/org/fdroid/ui/details/Versions.kt index 91b6c7665..9b86974d8 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/Versions.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/Versions.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons @@ -56,10 +57,11 @@ fun Versions(item: AppDetailsItem, scrollUp: suspend () -> Unit) { item.versions?.forEach { versionItem -> Version( item = versionItem, - isMetered = item.networkState.isMetered, + isMetered = item.networkState.showWarningDialog, installAction = { version: AppVersion -> item.actions.installAction(item.app, version, item.icon) }, + onNotWarnWhenMetered = item.actions.onNotWarnWhenMetered, scrollUp = scrollUp, ) } @@ -72,6 +74,7 @@ fun Version( item: VersionItem, isMetered: Boolean, installAction: (AppVersion) -> Unit, + onNotWarnWhenMetered: () -> Unit, scrollUp: suspend () -> Unit, ) { val isPreview = LocalInspectionMode.current @@ -83,7 +86,7 @@ fun Version( modifier = Modifier.padding(bottom = 8.dp).clickable { expanded = !expanded }, ) { ExpandIconChevron(expanded) - Row { + Row(verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f)) { Text( text = item.version.versionName, @@ -103,14 +106,14 @@ fun Version( containerColor = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(start = 8.dp), ) { - Text(text = stringResource(R.string.app_installed), modifier = Modifier.padding(2.dp)) + Text(text = stringResource(R.string.app_installed), modifier = Modifier.padding(4.dp)) } if (item.isSuggested) Badge( containerColor = MaterialTheme.colorScheme.secondary, modifier = Modifier.padding(start = 8.dp), ) { - Text(text = stringResource(R.string.app_suggested), modifier = Modifier.padding(2.dp)) + Text(text = stringResource(R.string.app_suggested), modifier = Modifier.padding(4.dp)) } } } @@ -120,84 +123,80 @@ fun Version( ) { val coroutineScope = rememberCoroutineScope() var showMeteredDialog by remember { mutableStateOf(false) } - Row(horizontalArrangement = spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) { - Column(modifier = Modifier.weight(1f)) { - if (!item.isCompatible || !item.isSignerCompatible) + Column(modifier = Modifier.fillMaxWidth().padding(start = 40.dp)) { + if (!item.isCompatible || !item.isSignerCompatible) + Text( + text = + if (!item.isCompatible) { + stringResource(R.string.app_details_incompatible_version) + } else { + stringResource(R.string.app_details_incompatible_signer) + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + ) + item.version.size?.let { size -> + Text( + text = + stringResource( + R.string.size_colon, + Formatter.formatFileSize(LocalContext.current, size), + ), + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + SelectionContainer { + Text( + text = stringResource(R.string.version_code_colon, item.version.versionCode.toString()), + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + ) + } + val sdkString = buildString { + item.version.packageManifest.minSdkVersion?.let { sdk -> + append(stringResource(R.string.sdk_min_version, sdk)) + } + item.version.packageManifest.targetSdkVersion?.let { sdk -> + if (isNotEmpty()) append(" ") + append(stringResource(R.string.sdk_target_version, sdk)) + } + item.version.packageManifest.maxSdkVersion?.let { sdk -> + if (isNotEmpty()) append(" ") + append(stringResource(R.string.sdk_max_version, sdk)) + } + } + if (sdkString.isNotEmpty()) + SelectionContainer { Text( - text = - if (!item.isCompatible) { - stringResource(R.string.app_details_incompatible_version) - } else { - stringResource(R.string.app_details_incompatible_signer) - }, + text = stringResource(R.string.sdk_versions_colon, sdkString), style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error, + maxLines = 1, + overflow = TextOverflow.StartEllipsis, ) - item.version.size?.let { size -> + } + item.version.packageManifest.nativecode?.let { nativeCode -> + if (nativeCode.isNotEmpty()) + SelectionContainer { + Text( + text = stringResource(R.string.architectures_colon, nativeCode.joinToString(", ")), + style = MaterialTheme.typography.bodySmall, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } + item.version.signer?.let { signer -> + SelectionContainer { Text( - text = - stringResource( - R.string.size_colon, - Formatter.formatFileSize(LocalContext.current, size), - ), + text = stringResource(R.string.signer_colon, signer.sha256[0].substring(0..15)), style = MaterialTheme.typography.bodySmall, maxLines = 1, overflow = TextOverflow.Ellipsis, ) } - SelectionContainer { - Text( - text = - stringResource(R.string.version_code_colon, item.version.versionCode.toString()), - style = MaterialTheme.typography.bodySmall, - maxLines = 1, - overflow = TextOverflow.MiddleEllipsis, - ) - } - val sdkString = buildString { - item.version.packageManifest.minSdkVersion?.let { sdk -> - append(stringResource(R.string.sdk_min_version, sdk)) - } - item.version.packageManifest.targetSdkVersion?.let { sdk -> - if (isNotEmpty()) append(" ") - append(stringResource(R.string.sdk_target_version, sdk)) - } - item.version.packageManifest.maxSdkVersion?.let { sdk -> - if (isNotEmpty()) append(" ") - append(stringResource(R.string.sdk_max_version, sdk)) - } - } - if (sdkString.isNotEmpty()) - SelectionContainer { - Text( - text = stringResource(R.string.sdk_versions_colon, sdkString), - style = MaterialTheme.typography.bodySmall, - maxLines = 1, - overflow = TextOverflow.StartEllipsis, - ) - } - item.version.packageManifest.nativecode?.let { nativeCode -> - if (nativeCode.isNotEmpty()) - SelectionContainer { - Text( - text = - stringResource(R.string.architectures_colon, nativeCode.joinToString(", ")), - style = MaterialTheme.typography.bodySmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - item.version.signer?.let { signer -> - SelectionContainer { - Text( - text = stringResource(R.string.signer_colon, signer.sha256[0].substring(0..15)), - style = MaterialTheme.typography.bodySmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } } if (item.showInstallButton) { val coroutineScope = rememberCoroutineScope() @@ -211,13 +210,15 @@ fun Version( coroutineScope.launch { scrollUp() } } }, + modifier = Modifier.padding(top = 8.dp, end = 16.dp).fillMaxWidth(), ) } } if (showMeteredDialog) MeteredConnectionDialog( numBytes = item.version.size, - onConfirm = { + onConfirm = { notWarnWhenMetered -> + if (notWarnWhenMetered) onNotWarnWhenMetered() installAction(item.version as AppVersion) coroutineScope.launch { scrollUp() } }, diff --git a/app/src/main/kotlin/org/fdroid/ui/discover/AppCarousel.kt b/app/src/main/kotlin/org/fdroid/ui/discover/AppCarousel.kt index 89cf54f67..b8f2d13b0 100644 --- a/app/src/main/kotlin/org/fdroid/ui/discover/AppCarousel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/discover/AppCarousel.kt @@ -1,6 +1,7 @@ package org.fdroid.ui.discover import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -11,11 +12,14 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material3.BadgedBox import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -27,6 +31,7 @@ import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import org.fdroid.ui.FDroidContent import org.fdroid.ui.utils.AsyncShimmerImage import org.fdroid.ui.utils.InstalledBadge @@ -41,20 +46,28 @@ fun AppCarousel( onTitleTap: () -> Unit, onAppTap: (AppDiscoverItem) -> Unit, ) { - Column(verticalArrangement = spacedBy(8.dp), modifier = modifier) { + Column(verticalArrangement = spacedBy(10.dp), modifier = modifier) { Row( verticalAlignment = CenterVertically, - modifier = - Modifier.fillMaxWidth() - .clickable(onClick = onTitleTap) - .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth().clickable(onClick = onTitleTap).padding(horizontal = 16.dp), ) { - Text(text = title, style = MaterialTheme.typography.headlineSmall) - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - modifier = Modifier.semantics { hideFromAccessibility() }, - contentDescription = null, - ) + Text(text = title, style = MaterialTheme.typography.titleMediumEmphasized, fontSize = 20.sp) + IconButton( + onClick = onTitleTap, + shape = CircleShape, + colors = + IconButtonDefaults.iconButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer + ), + modifier = Modifier.size(48.dp).padding(6.dp), + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + modifier = Modifier.semantics { hideFromAccessibility() }, + contentDescription = null, + ) + } } LazyRow( contentPadding = PaddingValues(horizontal = 16.dp), @@ -82,7 +95,13 @@ fun AppBox(app: AppDiscoverItem, onAppTap: (AppDiscoverItem) -> Unit) { }, ) } - Text(text = app.name, style = MaterialTheme.typography.bodySmall, minLines = 2, maxLines = 2) + Text( + text = app.name, + style = MaterialTheme.typography.bodySmall, + minLines = 2, + maxLines = 2, + lineHeight = 14.sp, + ) } } diff --git a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverContent.kt b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverContent.kt index 70f491a94..b248393ca 100644 --- a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverContent.kt +++ b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverContent.kt @@ -1,19 +1,21 @@ package org.fdroid.ui.discover import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.navigation3.runtime.NavKey import org.fdroid.R import org.fdroid.ui.categories.CategoryList import org.fdroid.ui.lists.AppListType -@OptIn(ExperimentalMaterial3Api::class) @Composable +@OptIn(ExperimentalMaterial3Api::class) fun DiscoverContent( discoverModel: LoadedDiscoverModel, onListTap: (AppListType) -> Unit, @@ -21,7 +23,7 @@ fun DiscoverContent( onNav: (NavKey) -> Unit, modifier: Modifier = Modifier, ) { - Column(modifier = modifier) { + Column(verticalArrangement = spacedBy(18.dp), modifier = modifier) { AnimatedVisibility(discoverModel.newApps.isNotEmpty()) { val listNew = AppListType.New(stringResource(R.string.app_list_new)) AppCarousel( diff --git a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt index bb66e6baf..76fc974e4 100644 --- a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt @@ -2,6 +2,11 @@ package org.fdroid.ui.discover import android.annotation.SuppressLint import android.app.Application +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.Intent.ACTION_LOCALE_CHANGED +import android.content.IntentFilter import androidx.core.os.LocaleListCompat import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.SavedStateHandle @@ -16,10 +21,13 @@ import java.util.Locale import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream @@ -49,8 +57,8 @@ constructor( private val log = KotlinLogging.logger {} private val moleculeScope = CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main) - private val collator = Collator.getInstance(Locale.getDefault()) + private val localeListFlow = MutableStateFlow(LocaleListCompat.getDefault()) private val newAppsFlow = db.getAppDao().getNewAppsFlow() private val recentlyUpdatedAppsFlow = db.getAppDao().getRecentlyUpdatedAppsFlow() private val mostDownloadedApps = @@ -68,8 +76,10 @@ constructor( db.getAppDao().getAppsFlow(packageNames).collect { apps -> emit(apps) } } .flowOn(Dispatchers.IO) + private val dbCategories = db.getRepositoryDao().getLiveCategories().asFlow() private val categories = - db.getRepositoryDao().getLiveCategories().asFlow().map { categories -> + combine(localeListFlow, dbCategories) { localeList, categories -> + val collator = Collator.getInstance(Locale.getDefault()) categories .map { category -> CategoryItem(id = category.id, name = category.getName(localeList) ?: "Unknown Category") @@ -81,7 +91,6 @@ constructor( repos?.any { it.enabled && it.errorCount >= 5 } ?: false } - val localeList = LocaleListCompat.getDefault() val discoverModel: StateFlow by lazy(LazyThreadSafetyMode.NONE) { @SuppressLint("StateFlowValueCalledInComposition") // see comment below @@ -103,4 +112,24 @@ constructor( ) } } + + private val localeChangedReceiver = LocaleChangedReceiver() + + init { + app.registerReceiver(localeChangedReceiver, IntentFilter(ACTION_LOCALE_CHANGED)) + } + + override fun onCleared() { + app.unregisterReceiver(localeChangedReceiver) + } + + private inner class LocaleChangedReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == ACTION_LOCALE_CHANGED) { + val localeList = LocaleListCompat.getDefault() + log.info { "Locale list has changed: $localeList" } + localeListFlow.update { localeList } + } + } + } } diff --git a/app/src/main/kotlin/org/fdroid/ui/discover/FirstStart.kt b/app/src/main/kotlin/org/fdroid/ui/discover/FirstStart.kt index 3ccbfac31..3741ace95 100644 --- a/app/src/main/kotlin/org/fdroid/ui/discover/FirstStart.kt +++ b/app/src/main/kotlin/org/fdroid/ui/discover/FirstStart.kt @@ -52,10 +52,10 @@ fun FirstStart( // TODO it would be nice to surface normal update errors better and also let the user retry } Column(verticalArrangement = Center, modifier = modifier) { - if ((!networkState.isOnline || networkState.isMetered) && !override) { + if ((!networkState.isOnline || networkState.showWarningDialog) && !override) { // offline or metered, not overridden val res = - if (networkState.isMetered) { + if (networkState.showWarningDialog) { stringResource(R.string.first_start_metered) } else { stringResource(R.string.first_start_offline) diff --git a/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt b/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt index 860ecae64..aed48dc13 100644 --- a/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt +++ b/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt @@ -17,8 +17,8 @@ import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ManageSearch import androidx.compose.material.icons.filled.FilterList -import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Badge import androidx.compose.material3.BadgedBox import androidx.compose.material3.ExperimentalMaterial3Api @@ -51,6 +51,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.viktormykhailiv.compose.hints.HintHost +import com.viktormykhailiv.compose.hints.HintProperties import com.viktormykhailiv.compose.hints.hintAnchor import com.viktormykhailiv.compose.hints.rememberHint import com.viktormykhailiv.compose.hints.rememberHintAnchorState @@ -61,7 +62,7 @@ import org.fdroid.ui.FDroidContent import org.fdroid.ui.search.TopSearchBar import org.fdroid.ui.utils.BackButton import org.fdroid.ui.utils.BigLoadingIndicator -import org.fdroid.ui.utils.OnboardingCard +import org.fdroid.ui.utils.OnboardingPopupCard import org.fdroid.ui.utils.TopAppBarButton import org.fdroid.ui.utils.appListItems import org.fdroid.ui.utils.getAppListInfo @@ -80,22 +81,22 @@ fun AppList( val scrollBehavior = enterAlwaysScrollBehavior(rememberTopAppBarState()) val hintController = rememberHintController(overlay = getHintOverlayColor()) - val hint = rememberHint { - OnboardingCard( - title = stringResource(R.string.onboarding_app_list_filter_title), - message = stringResource(R.string.onboarding_app_list_filter_message), - modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp), - onGotIt = { - appListInfo.actions.onOnboardingSeen() - hintController.dismiss() - }, - ) - } + val hint = + rememberHint(HintProperties(dismissOnClickOutside = false)) { + OnboardingPopupCard( + title = stringResource(R.string.onboarding_app_list_filter_title), + message = stringResource(R.string.onboarding_app_list_filter_message), + modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp), + onGotIt = { + appListInfo.actions.onOnboardingSeen() + hintController.dismiss() + }, + ) + } val hintAnchor = rememberHintAnchorState(hint) LaunchedEffect(appListInfo.showOnboarding) { if (appListInfo.showOnboarding) { hintController.show(hintAnchor) - appListInfo.actions.onOnboardingSeen() } } @@ -131,7 +132,7 @@ fun AppList( }, actions = { TopAppBarButton( - imageVector = Icons.Filled.Search, + imageVector = Icons.AutoMirrored.Default.ManageSearch, contentDescription = stringResource(R.string.menu_search), onClick = { searchActive = true }, ) diff --git a/app/src/main/kotlin/org/fdroid/ui/navigation/BottomBar.kt b/app/src/main/kotlin/org/fdroid/ui/navigation/BottomBar.kt index ea5314991..ea35ffdbe 100644 --- a/app/src/main/kotlin/org/fdroid/ui/navigation/BottomBar.kt +++ b/app/src/main/kotlin/org/fdroid/ui/navigation/BottomBar.kt @@ -45,7 +45,7 @@ fun BottomBar(model: MainModel, currentNavKey: NavKey, onNav: (MainNavKey) -> Un selectedTextColor = MaterialTheme.colorScheme.primary, selectedIconColor = contentColorFor(MaterialTheme.colorScheme.primary), ), - onClick = { if (dest != currentNavKey) onNav(dest) }, + onClick = { onNav(dest) }, modifier = Modifier.semantics { if (dest == NavigationKey.MyApps) { @@ -83,7 +83,7 @@ fun NavigationRail( selectedTextColor = MaterialTheme.colorScheme.primary, selectedIconColor = contentColorFor(MaterialTheme.colorScheme.primary), ), - onClick = { if (dest != currentNavKey) onNav(dest) }, + onClick = { onNav(dest) }, modifier = Modifier.semantics { if (dest == NavigationKey.MyApps) { diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/RepoEntry.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/RepoEntry.kt index 4767175ba..d308ab15f 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/RepoEntry.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/RepoEntry.kt @@ -46,6 +46,8 @@ fun EntryProviderScope.repoEntry(navigator: Navigator, isBigScreen: Bool override fun onRepositoryEnabled(repoId: Long, enabled: Boolean) = viewModel.onRepositoryEnabled(repoId, enabled) + override fun onNotWarnWhenMetered() = viewModel.onNotWarnWhenMetered() + override fun onAddRepo() { navigator.navigate(NavigationKey.AddRepo()) } @@ -94,6 +96,7 @@ fun EntryProviderScope.repoEntry(navigator: Navigator, isBigScreen: Bool networkStateFlow = viewModel.networkState, proxyConfig = viewModel.proxyConfig, onFetchRepo = viewModel::onFetchRepo, + onNotWarnWhenMetered = viewModel::onNotWarnWhenMetered, onAddRepo = viewModel::addFetchedRepository, onExistingRepo = { repoId -> navigator.goBack() diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt index e82f4de58..6d8c4dfc1 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt @@ -34,8 +34,10 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import com.viktormykhailiv.compose.hints.HintHost +import com.viktormykhailiv.compose.hints.HintProperties import com.viktormykhailiv.compose.hints.rememberHint import com.viktormykhailiv.compose.hints.rememberHintAnchorState import com.viktormykhailiv.compose.hints.rememberHintController @@ -43,7 +45,7 @@ import org.fdroid.R import org.fdroid.download.NetworkState import org.fdroid.ui.FDroidContent import org.fdroid.ui.utils.BigLoadingIndicator -import org.fdroid.ui.utils.OnboardingCard +import org.fdroid.ui.utils.OnboardingPopupCard import org.fdroid.ui.utils.getHintOverlayColor import org.fdroid.ui.utils.getRepositoriesInfo import org.fdroid.ui.utils.repoItems @@ -52,24 +54,24 @@ import org.fdroid.ui.utils.repoItems @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) fun Repositories(info: RepositoryInfo, isBigScreen: Boolean, onBackClicked: () -> Unit) { val hintController = rememberHintController(overlay = getHintOverlayColor()) - val hint = rememberHint { - Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - OnboardingCard( - title = stringResource(R.string.repo_list_info_title), - message = stringResource(R.string.repo_list_info_text), - modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp), - onGotIt = { - info.onOnboardingSeen() - hintController.dismiss() - }, - ) + val hint = + rememberHint(HintProperties(dismissOnClickOutside = false)) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + OnboardingPopupCard( + title = stringResource(R.string.repo_list_info_title), + message = stringResource(R.string.repo_list_info_text), + modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp), + onGotIt = { + info.onOnboardingSeen() + hintController.dismiss() + }, + ) + } } - } val hintAnchor = rememberHintAnchorState(hint) LaunchedEffect(info.model.showOnboarding) { if (!isBigScreen && info.model.showOnboarding) { hintController.show(hintAnchor) - info.onOnboardingSeen() } } val scrollBehavior = enterAlwaysScrollBehavior(rememberTopAppBarState()) @@ -153,8 +155,8 @@ fun RepositoriesScaffoldLoadingPreview() { } } -@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL) @Composable +@PreviewLightDark private fun RepositoriesScaffoldPreview() { HintHost { FDroidContent { diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesList.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesList.kt index 253b8b0cb..be5398730 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesList.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesList.kt @@ -3,8 +3,11 @@ package org.fdroid.ui.repositories import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.plus import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -12,7 +15,12 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.AlertDialog +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -21,6 +29,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.dropShadow import androidx.compose.ui.graphics.shadow.Shadow @@ -85,7 +94,7 @@ fun RepositoriesList( isSelected = isSelected, onRepoEnabled = { enabled -> if (enabled) { - if (info.model.networkState.isMetered) + if (info.model.networkState.showWarningDialog) showMeteredDialog = { info.onRepositoryEnabled(repoItem.repoId, true) } else info.onRepositoryEnabled(repoItem.repoId, true) } else { @@ -108,7 +117,33 @@ fun RepositoriesList( val repoId = showDisableRepoDialog if (repoId != null) { AlertDialog( - text = { Text(text = stringResource(R.string.repo_disable_warning)) }, + title = { Text(text = stringResource(R.string.repo_disable_dialog_title)) }, + text = { + Column(verticalArrangement = spacedBy(8.dp)) { + ElevatedCard( + colors = + CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer + ) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = spacedBy(8.dp), + ) { + Icon( + Icons.Default.Warning, + contentDescription = null, + modifier = Modifier.padding(start = 8.dp), + ) + Text( + text = stringResource(R.string.repo_disable_dialog_warning), + modifier = Modifier.padding(8.dp), + ) + } + } + Text(stringResource(R.string.repo_disable_dialog_text)) + } + }, onDismissRequest = { showDisableRepoDialog = null }, confirmButton = { TextButton( @@ -135,7 +170,10 @@ fun RepositoriesList( if (meteredLambda != null) MeteredConnectionDialog( numBytes = null, - onConfirm = { meteredLambda() }, + onConfirm = { notWarnWhenMetered -> + if (notWarnWhenMetered) info.onNotWarnWhenMetered() + meteredLambda() + }, onDismiss = { showMeteredDialog = null }, ) } diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt index 1bef2f631..e24dfa320 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt @@ -95,6 +95,10 @@ constructor( } } + fun onNotWarnWhenMetered() { + settingsManager.onDontWarnOnMeteredNetwork() + } + fun onRepositoriesMoved(fromRepoId: Long, toRepoId: Long) { log.info { "onRepositoriesMoved($fromRepoId, $toRepoId)" } repoSortingMap.update { diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoryInfo.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoryInfo.kt index b07c23bb0..c30d8d700 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoryInfo.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoryInfo.kt @@ -12,6 +12,8 @@ interface RepositoryInfo { fun onRepositoryEnabled(repoId: Long, enabled: Boolean) + fun onNotWarnWhenMetered() + fun onAddRepo() fun onRepositoryMoved(fromRepoId: Long, toRepoId: Long) diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/UpdateRepoActivity.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/UpdateRepoActivity.kt new file mode 100644 index 000000000..8d3e6babb --- /dev/null +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/UpdateRepoActivity.kt @@ -0,0 +1,27 @@ +package org.fdroid.ui.repositories + +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import org.fdroid.repo.RepoUpdateWorker + +private val TAG = UpdateRepoActivity::class.java.simpleName + +/** + * This activity allows OS components to trigger a repository update. One known use-case for this is + * to do an initial repository update during SetupWizard, so app data is available when needed, e.g. + * for restoring app backups. + */ +class UpdateRepoActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (intent.action != "org.fdroid.action.UPDATE_REPOS") { + Log.w(TAG, "Unknown action: ${intent.action}") + return + } + Log.i(TAG, "Intent received, updating repos...") + RepoUpdateWorker.updateNow(this) + finish() + } +} diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepo.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepo.kt index 42b09452c..3dc4b1914 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepo.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepo.kt @@ -37,6 +37,7 @@ fun AddRepo( networkStateFlow: StateFlow, proxyConfig: ProxyConfig?, onFetchRepo: (String) -> Unit, + onNotWarnWhenMetered: () -> Unit, onAddRepo: () -> Unit, onExistingRepo: (Long) -> Unit, onRepoAdded: (String, Long) -> Unit, @@ -74,7 +75,7 @@ fun AddRepo( when (state) { None -> { val networkState = networkStateFlow.collectAsStateWithLifecycle().value - AddRepoIntroContent(networkState, onFetchRepo, paddingValues) + AddRepoIntroContent(networkState, onFetchRepo, onNotWarnWhenMetered, paddingValues) } is Fetching -> { if (state.receivedRepo == null) { diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoIntro.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoIntro.kt index e6199b65d..92b784a39 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoIntro.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoIntro.kt @@ -88,6 +88,7 @@ import org.fdroid.ui.utils.startActivitySafe fun AddRepoIntroContent( networkState: NetworkState, onFetchRepo: (String) -> Unit, + onNotWarnWhenMetered: () -> Unit, paddingValues: PaddingValues = PaddingValues(), ) { val scrollState = rememberScrollState() @@ -150,7 +151,7 @@ fun AddRepoIntroContent( Unit } } - if (networkState.isMetered) showMeteredDialog = scanLambda else scanLambda() + if (networkState.showWarningDialog) showMeteredDialog = scanLambda else scanLambda() }, ) AnimatedVisibility( @@ -229,7 +230,7 @@ fun AddRepoIntroContent( FDroidButton( text = stringResource(R.string.repo_add_add), onClick = { - if (networkState.isMetered) + if (networkState.showWarningDialog) showMeteredDialog = { onFetchRepo(textState.value.text) } else onFetchRepo(textState.value.text) }, @@ -244,7 +245,10 @@ fun AddRepoIntroContent( if (meteredLambda != null) MeteredConnectionDialog( numBytes = null, - onConfirm = { meteredLambda() }, + onConfirm = { notWarnWhenMetered -> + if (notWarnWhenMetered) onNotWarnWhenMetered() + meteredLambda() + }, onDismiss = { showMeteredDialog = null }, ) } @@ -254,7 +258,7 @@ fun AddRepoIntroContent( private fun Preview() { FDroidContent { val networkStateFlow = MutableStateFlow(NetworkState(isOnline = true, isMetered = false)) - AddRepo(None, networkStateFlow, null, {}, {}, {}, { _, _ -> }) {} + AddRepo(None, networkStateFlow, null, {}, {}, {}, {}, { _, _ -> }, {}) } } @@ -263,6 +267,6 @@ private fun Preview() { private fun PreviewNight() { FDroidContent { val networkStateFlow = MutableStateFlow(NetworkState(isOnline = false, isMetered = false)) - AddRepo(None, networkStateFlow, null, {}, {}, {}, { _, _ -> }) {} + AddRepo(None, networkStateFlow, null, {}, {}, {}, {}, { _, _ -> }, {}) } } diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoViewModel.kt index b128cf5d7..f15685739 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoViewModel.kt @@ -28,7 +28,7 @@ class AddRepoViewModel constructor( app: Application, networkMonitor: NetworkMonitor, - settingsManager: SettingsManager, + private val settingsManager: SettingsManager, private val repoManager: RepoManager, private val updateManager: UpdatesManager, @param:IoDispatcher private val ioScope: CoroutineScope, @@ -74,4 +74,8 @@ constructor( } } } + + fun onNotWarnWhenMetered() { + settingsManager.onDontWarnOnMeteredNetwork() + } } diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt index a262b3423..dba3c963b 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.viktormykhailiv.compose.hints.HintHost +import com.viktormykhailiv.compose.hints.HintProperties import com.viktormykhailiv.compose.hints.rememberHint import com.viktormykhailiv.compose.hints.rememberHintAnchorState import com.viktormykhailiv.compose.hints.rememberHintController @@ -39,7 +40,7 @@ import org.fdroid.ui.FDroidContent import org.fdroid.ui.utils.BackButton import org.fdroid.ui.utils.BigLoadingIndicator import org.fdroid.ui.utils.MeteredConnectionDialog -import org.fdroid.ui.utils.OnboardingCard +import org.fdroid.ui.utils.OnboardingPopupCard import org.fdroid.ui.utils.TopAppBarButton import org.fdroid.ui.utils.TopAppBarOverflowButton import org.fdroid.ui.utils.getHintOverlayColor @@ -56,24 +57,24 @@ fun RepoDetails( val repo = info.model.repo val hintController = rememberHintController(overlay = getHintOverlayColor()) - val hint = rememberHint { - Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - OnboardingCard( - title = stringResource(R.string.repo_details), - message = stringResource(R.string.repo_details_info_text), - modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp), - onGotIt = { - info.actions.onOnboardingSeen() - hintController.dismiss() - }, - ) + val hint = + rememberHint(HintProperties(dismissOnClickOutside = false)) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + OnboardingPopupCard( + title = stringResource(R.string.repo_details), + message = stringResource(R.string.repo_details_info_text), + modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp), + onGotIt = { + info.actions.onOnboardingSeen() + hintController.dismiss() + }, + ) + } } - } val hintAnchor = rememberHintAnchorState(hint) LaunchedEffect(info.model.showOnboarding) { if (info.model.showOnboarding) { hintController.show(hintAnchor) - info.actions.onOnboardingSeen() } } @@ -95,7 +96,10 @@ fun RepoDetails( if (meteredLambda != null) MeteredConnectionDialog( numBytes = null, - onConfirm = { meteredLambda() }, + onConfirm = { notWarnWhenMetered -> + if (notWarnWhenMetered) info.actions.onNotWarnWhenMetered() + meteredLambda() + }, onDismiss = { showMeteredDialog = null }, ) Scaffold( @@ -120,7 +124,7 @@ fun RepoDetails( contentDescription = stringResource(R.string.repo_force_update), enabled = info.model.isUpdateButtonEnabled, onClick = { - if (info.model.networkState.isMetered) + if (info.model.networkState.showWarningDialog) showMeteredDialog = { RepoUpdateWorker.updateNow(context, repo.repoId) } else RepoUpdateWorker.updateNow(context, repo.repoId) }, diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsInfo.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsInfo.kt index e0ac6f31b..3727505b6 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsInfo.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsInfo.kt @@ -32,6 +32,8 @@ interface RepoDetailsActions { fun setArchiveRepoEnabled(enabled: Boolean) + fun onNotWarnWhenMetered() + fun onOnboardingSeen() suspend fun generateQrCode(repo: Repository): Bitmap? { diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsViewModel.kt index 76acfe406..a828c36be 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsViewModel.kt @@ -154,6 +154,10 @@ constructor( } } + override fun onNotWarnWhenMetered() { + settingsManager.onDontWarnOnMeteredNetwork() + } + override fun onOnboardingSeen() = onboardingManager.onRepoDetailsOnboardingSeen() private fun Repository.archiveState(): ArchiveState { diff --git a/app/src/main/kotlin/org/fdroid/ui/search/AppSearchInputField.kt b/app/src/main/kotlin/org/fdroid/ui/search/AppSearchInputField.kt index 62aad39ee..74c2a1b1b 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/AppSearchInputField.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/AppSearchInputField.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import org.fdroid.R -import org.fdroid.search.SEARCH_THRESHOLD +import org.fdroid.search.SearchHelper.isSearchable @Composable @OptIn(ExperimentalMaterial3Api::class, FlowPreview::class) @@ -41,10 +41,10 @@ fun AppSearchInputField( snapshotFlow { textFieldState.text } .distinctUntilChanged() .debounce(500) - .collectLatest { - if (it.isEmpty()) { + .collectLatest { query -> + if (query.isEmpty()) { onSearchCleared() - } else if (it.length >= SEARCH_THRESHOLD) { + } else if (query.isSearchable()) { onSearch(textFieldState.text.toString()) } } @@ -54,7 +54,7 @@ fun AppSearchInputField( searchBarState = searchBarState, textFieldState = textFieldState, textStyle = MaterialTheme.typography.bodyLarge, - onSearch = { scope.launch { onSearch(it) } }, + onSearch = { if (it.isSearchable()) scope.launch { onSearch(it) } }, placeholder = { Text(stringResource(R.string.search_placeholder)) }, trailingIcon = { if (textFieldState.text.isNotEmpty()) { diff --git a/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt b/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt index 197c5c266..8a489e492 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt @@ -7,42 +7,74 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Keyboard +import androidx.compose.material3.Checkbox +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import org.fdroid.R import org.fdroid.search.SavedSearch import org.fdroid.ui.FDroidContent import org.fdroid.ui.navigation.NavigationKey +import org.fdroid.ui.utils.TopAppBarOverflowButton import org.fdroid.ui.utils.appListItems import org.fdroid.ui.utils.categoryItems +import org.fdroid.ui.utils.getSearchInfo @Composable @OptIn(ExperimentalMaterial3Api::class) fun GlobalSearch( + info: SearchInfo, textFieldState: TextFieldState = rememberTextFieldState(), - searchResults: SearchResults?, - savedSearches: List? = null, - onSearch: suspend (String) -> Unit, - onClearSavedSearches: () -> Unit, onNav: (NavigationKey) -> Unit, onBack: () -> Unit, - onSearchCleared: () -> Unit, ) { + LaunchedEffect(info.showKeyboard) { + // auto-reset showKeyboard, so it functions like an event + if (info.showKeyboard) info.onKeyboardShown() + } + val searchResults = info.searchResults + val showKeyboard = + info.showKeyboard || // either show keyboard because user double tapped the search icon + (info.autoShowKeyboard && // or auto-show is activated + (searchResults == null || // but only if no search results are shown + searchResults.apps.isEmpty() && searchResults.categories.isEmpty())) Scaffold( topBar = { TopSearchBar( searchFieldState = textFieldState, - shouldRequestFocus = // only show keyboard if there are no results to show - searchResults == null || - searchResults.apps.isEmpty() && searchResults.categories.isEmpty(), - onSearch = onSearch, - onSearchCleared = onSearchCleared, + shouldRequestFocus = showKeyboard, + onSearch = info.actions::onSearch, + onSearchCleared = info.actions::onSearchCleared, onHideSearch = onBack, + actions = { + if (textFieldState.text.isEmpty()) { + TopAppBarOverflowButton { onDismiss -> + DropdownMenuItem( + leadingIcon = { Icon(Icons.Default.Keyboard, null) }, + text = { Text(stringResource(R.string.search_auto_keyboard)) }, + trailingIcon = { + Checkbox(checked = info.autoShowKeyboard, onCheckedChange = null) + }, + onClick = { + info.actions.setAutoShowKeyboard(!info.autoShowKeyboard) + onDismiss() + }, + ) + } + } + }, ) } ) { paddingValues -> @@ -59,8 +91,9 @@ fun GlobalSearch( paddingValues = paddingValues, searchResults = searchResults, textFieldState = textFieldState, - savedSearches = savedSearches, - onClearSavedSearches = onClearSavedSearches, + savedSearches = info.savedSearches, + categories = info.categories, + onClearSavedSearches = info.actions::onClearSearchHistory, onNav = onNav, ) } @@ -72,7 +105,9 @@ fun GlobalSearch( private fun AppsSearchLoadingPreview() { FDroidContent { val textFieldState = rememberTextFieldState("foo bar") - Box(Modifier.fillMaxSize()) { GlobalSearch(textFieldState, null, null, {}, {}, {}, {}, {}) } + Box(Modifier.fillMaxSize()) { + GlobalSearch(info = getSearchInfo(), textFieldState = textFieldState, onNav = {}, onBack = {}) + } } } @@ -86,14 +121,10 @@ private fun AppsSearchEmptyStatePreview() { listOf(SavedSearch(1, "foo"), SavedSearch(2, "foo bar"), SavedSearch(3, "foobar")) Box(Modifier.fillMaxSize()) { GlobalSearch( + info = getSearchInfo(null, savedSearches, categoryItems), textFieldState = textFieldState, - searchResults = null, - savedSearches = savedSearches, - onSearch = {}, - onClearSavedSearches = {}, onNav = {}, onBack = {}, - onSearchCleared = {}, ) } } @@ -107,14 +138,10 @@ private fun AppsSearchNoResultsPreview() { val textFieldState = rememberTextFieldState("foo") Box(Modifier.fillMaxSize()) { GlobalSearch( + info = getSearchInfo(SearchResults(emptyList(), emptyList()), emptyList(), emptyList()), textFieldState = textFieldState, - searchResults = SearchResults(emptyList(), emptyList()), - savedSearches = emptyList(), - onSearch = {}, - onClearSavedSearches = {}, onNav = {}, onBack = {}, - onSearchCleared = {}, ) } } @@ -128,14 +155,10 @@ private fun AppsSearchOnlyCategoriesPreview() { val textFieldState = rememberTextFieldState() Box(Modifier.fillMaxSize()) { GlobalSearch( + info = getSearchInfo(SearchResults(emptyList(), categoryItems.subList(3, 5)), emptyList()), textFieldState = textFieldState, - searchResults = SearchResults(emptyList(), categoryItems.subList(3, 5)), - savedSearches = emptyList(), - onSearch = {}, - onClearSavedSearches = {}, onNav = {}, onBack = {}, - onSearchCleared = {}, ) } } @@ -149,14 +172,10 @@ private fun AppsSearchPreview() { val textFieldState = rememberTextFieldState() Box(Modifier.fillMaxSize()) { GlobalSearch( + info = getSearchInfo(SearchResults(appListItems, categoryItems.subList(0, 4)), emptyList()), textFieldState = textFieldState, - searchResults = SearchResults(appListItems, categoryItems.subList(0, 4)), - savedSearches = emptyList(), - onSearch = {}, - onClearSavedSearches = {}, onNav = {}, onBack = {}, - onSearchCleared = {}, ) } } diff --git a/app/src/main/kotlin/org/fdroid/ui/search/PastSearches.kt b/app/src/main/kotlin/org/fdroid/ui/search/PastSearches.kt index 09271a0fd..f6d536017 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/PastSearches.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/PastSearches.kt @@ -1,17 +1,11 @@ package org.fdroid.ui.search import androidx.compose.foundation.background -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons @@ -23,6 +17,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource @@ -32,51 +27,41 @@ import org.fdroid.R import org.fdroid.search.SavedSearch import org.fdroid.ui.FDroidContent -@Composable @OptIn(ExperimentalMaterial3ExpressiveApi::class) -fun PastSearches( +fun LazyListScope.pastSearches( savedSearches: List, onSearch: (String) -> Unit, onClearSavedSearches: () -> Unit, - modifier: Modifier = Modifier, - paddingValues: PaddingValues = PaddingValues(), ) { - LazyColumn(modifier = modifier.fillMaxSize(), contentPadding = paddingValues) { - item { - Row { - Text( - text = stringResource(R.string.search_history), - style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp).weight(1f), + item { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.search_history), + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.padding(horizontal = 16.dp).weight(1f), + ) + TextButton(onClick = onClearSavedSearches, modifier = Modifier.padding(horizontal = 8.dp)) { + Text(stringResource(R.string.clear)) + } + } + } + items(savedSearches) { item -> + ListItem( + leadingContent = { + Icon( + Icons.Default.History, + contentDescription = null, + modifier = + Modifier.clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainer) + .padding(8.dp), ) - TextButton( - onClick = onClearSavedSearches, - modifier = Modifier.padding(horizontal = 8.dp), - ) { - Text(stringResource(R.string.clear)) - } - } + }, + onClick = { onSearch(item.query) }, + modifier = Modifier.fillMaxWidth().animateItem(), + ) { + Text(item.query) } - items(savedSearches) { item -> - ListItem( - leadingContent = { - Icon( - Icons.Default.History, - contentDescription = null, - modifier = - Modifier.clip(CircleShape) - .background(MaterialTheme.colorScheme.surfaceContainer) - .padding(8.dp), - ) - }, - onClick = { onSearch(item.query) }, - modifier = Modifier.fillMaxWidth().animateItem(), - ) { - Text(item.query) - } - } - item { Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) } - item { Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) } } } @@ -86,10 +71,8 @@ private fun Preview() { FDroidContent { val savedSearches = listOf(SavedSearch(1, "foo"), SavedSearch(2, "foo bar"), SavedSearch(3, "foobar")) - PastSearches( - savedSearches = savedSearches, - onClearSavedSearches = {}, - onSearch = {}, - ) + LazyColumn { + pastSearches(savedSearches = savedSearches, onClearSavedSearches = {}, onSearch = {}) + } } } diff --git a/app/src/main/kotlin/org/fdroid/ui/search/SearchInfo.kt b/app/src/main/kotlin/org/fdroid/ui/search/SearchInfo.kt new file mode 100644 index 000000000..6b501fa23 --- /dev/null +++ b/app/src/main/kotlin/org/fdroid/ui/search/SearchInfo.kt @@ -0,0 +1,24 @@ +package org.fdroid.ui.search + +import org.fdroid.search.SavedSearch +import org.fdroid.ui.categories.CategoryItem + +interface SearchInfo { + val searchResults: SearchResults? + val savedSearches: List? + val categories: List? + val autoShowKeyboard: Boolean + val showKeyboard: Boolean + val actions: SearchActions + fun onKeyboardShown() +} + +interface SearchActions { + suspend fun onSearch(term: String) + + fun onSearchCleared() + + fun onClearSearchHistory() + + fun setAutoShowKeyboard(autoShow: Boolean) +} diff --git a/app/src/main/kotlin/org/fdroid/ui/search/SearchResults.kt b/app/src/main/kotlin/org/fdroid/ui/search/SearchResults.kt index 544e22107..1d99d8671 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/SearchResults.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/SearchResults.kt @@ -23,12 +23,13 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import org.fdroid.R -import org.fdroid.search.SEARCH_THRESHOLD import org.fdroid.search.SavedSearch +import org.fdroid.search.SearchHelper.isSearchable import org.fdroid.ui.categories.CategoryChip import org.fdroid.ui.categories.CategoryItem import org.fdroid.ui.categories.ChipFlowRow @@ -47,6 +48,7 @@ fun SearchResults( searchResults: SearchResults?, textFieldState: TextFieldState, savedSearches: List?, + categories: List?, onClearSavedSearches: () -> Unit, onNav: (NavigationKey) -> Unit, modifier: Modifier = Modifier, @@ -55,20 +57,29 @@ fun SearchResults( val listState = rememberSaveable(searchResults, saver = LazyListState.Saver) { LazyListState(0, 0) } if (searchResults == null) { - if (textFieldState.text.length >= SEARCH_THRESHOLD) { + if (textFieldState.text.isSearchable()) { BigLoadingIndicator(modifier.padding(paddingValues).imePadding()) } else { - if (!savedSearches.isNullOrEmpty()) { - PastSearches( - savedSearches = savedSearches, - onSearch = { textFieldState.edit { replace(0, length, it) } }, - onClearSavedSearches = onClearSavedSearches, - modifier = modifier, - paddingValues = paddingValues, - ) + val keyboardController = LocalSoftwareKeyboardController.current + LazyColumn(modifier = modifier.fillMaxSize(), contentPadding = paddingValues) { + if (!savedSearches.isNullOrEmpty()) { + pastSearches( + savedSearches = savedSearches, + onSearch = { + textFieldState.edit { replace(0, length, it) } + keyboardController?.hide() + }, + onClearSavedSearches = onClearSavedSearches, + ) + } + if (!categories.isNullOrEmpty()) { + item { CategoriesFlowRow(categories, onNav, modifier = Modifier.padding(top = 16.dp)) } + } + item { Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) } + item { Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) } } } - } else if (searchResults.apps.isEmpty() && textFieldState.text.length >= SEARCH_THRESHOLD) { + } else if (searchResults.apps.isEmpty() && textFieldState.text.isSearchable()) { Column(modifier = modifier.padding(paddingValues)) { if (searchResults.categories.isNotEmpty()) { CategoriesFlowRow(searchResults.categories, onNav) @@ -121,8 +132,12 @@ fun SearchResults( } @Composable -private fun CategoriesFlowRow(categories: List, onNav: (NavigationKey) -> Unit) { - Column(modifier = Modifier.padding(horizontal = 8.dp)) { +private fun CategoriesFlowRow( + categories: List, + onNav: (NavigationKey) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier.padding(horizontal = 8.dp)) { Text( text = stringResource(R.string.main_menu__categories), style = MaterialTheme.typography.labelLarge, diff --git a/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt index 5ad895b42..4c3c97423 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt @@ -6,19 +6,35 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import jakarta.inject.Inject import kotlinx.coroutines.launch +import org.fdroid.search.SearchHelper.isSearchable import org.fdroid.search.SearchManager +import org.fdroid.settings.SettingsManager @HiltViewModel class SearchViewModel @Inject -constructor(app: Application, private val searchManager: SearchManager) : AndroidViewModel(app) { +constructor( + app: Application, + private val searchManager: SearchManager, + private val settingsManager: SettingsManager, +) : AndroidViewModel(app), SearchActions { val searchResults = searchManager.searchResults val savedSearchesFlow = searchManager.savedSearches + val categories = searchManager.categories + val autoShowKeyboard = settingsManager.showSearchKeyboardFlow - suspend fun search(term: String) = searchManager.search(term) + override suspend fun onSearch(term: String) { + if (term.isSearchable()) searchManager.search(term) + } - fun onSearchCleared() = searchManager.onSearchCleared() + override fun onSearchCleared() = searchManager.onSearchCleared() - fun onClearSearchHistory() = viewModelScope.launch { searchManager.onClearSearchHistory() } + override fun onClearSearchHistory() { + viewModelScope.launch { searchManager.onClearSearchHistory() } + } + + override fun setAutoShowKeyboard(autoShow: Boolean) { + settingsManager.setShowSearchKeyboard(autoShow) + } } diff --git a/app/src/main/kotlin/org/fdroid/ui/settings/SettingsViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/settings/SettingsViewModel.kt index e404f80bb..8b782a542 100644 --- a/app/src/main/kotlin/org/fdroid/ui/settings/SettingsViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/settings/SettingsViewModel.kt @@ -2,7 +2,9 @@ package org.fdroid.ui.settings import android.app.Application import android.net.Uri +import android.os.Build.VERSION.SDK_INT import android.os.Process +import android.os.Process.myUid import android.widget.Toast import android.widget.Toast.LENGTH_SHORT import androidx.annotation.StringRes @@ -68,8 +70,14 @@ constructor( sendToast(R.string.export_log_error) return@launch } - // support for --pid was introduced in SDK 24 - val command = "logcat -d --pid=" + Process.myPid() + " *:V" + val command = if (SDK_INT < 30) { + // support for --pid was introduced in SDK 24 + "logcat -d --pid=${Process.myPid()} *:V" + } else { + // support for --uid was introduced in SDK 30 and is better, + // because it gives logs before process death + "logcat -d --uid=${myUid()} *:V" + } try { application.contentResolver.openOutputStream(uri, "wt")?.use { outputStream -> getRuntime().exec(command).inputStream.use { inputStream -> diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/MeteredConnectionDialog.kt b/app/src/main/kotlin/org/fdroid/ui/utils/MeteredConnectionDialog.kt index d32516784..8aff4347c 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/MeteredConnectionDialog.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/MeteredConnectionDialog.kt @@ -1,18 +1,34 @@ package org.fdroid.ui.utils import android.text.format.Formatter +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Checkbox import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import org.fdroid.R @Composable -fun MeteredConnectionDialog(numBytes: Long?, onConfirm: () -> Unit, onDismiss: () -> Unit) { +fun MeteredConnectionDialog( + numBytes: Long?, + onConfirm: (Boolean) -> Unit, + onDismiss: () -> Unit, +) { + var dontShowAgain by remember { mutableStateOf(false) } AlertDialog( title = { Text(text = stringResource(R.string.dialog_metered_title)) }, text = { @@ -23,14 +39,26 @@ fun MeteredConnectionDialog(numBytes: Long?, onConfirm: () -> Unit, onDismiss: ( val sizeStr = Formatter.formatFileSize(LocalContext.current, numBytes) stringResource(R.string.dialog_metered_text, sizeStr) } - Text(text = s) + Column { + Text(text = s) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 16.dp), + ) { + Checkbox(checked = dontShowAgain, onCheckedChange = { dontShowAgain = it }) + Text( + text = stringResource(R.string.dialog_metered_dont_show), + modifier = Modifier.padding(start = 8.dp), + ) + } + } }, onDismissRequest = onDismiss, confirmButton = { TextButton( onClick = { onDismiss() - onConfirm() + onConfirm(dontShowAgain) } ) { Text( diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/OnboardingCard.kt b/app/src/main/kotlin/org/fdroid/ui/utils/OnboardingCard.kt index df24a65fb..9e6644747 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/OnboardingCard.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/OnboardingCard.kt @@ -1,5 +1,8 @@ package org.fdroid.ui.utils +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.ElevatedCard @@ -20,16 +23,38 @@ import androidx.compose.ui.unit.dp import org.fdroid.R import org.fdroid.ui.FDroidContent +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun OnboardingPopupCard( + title: String, + message: String, + modifier: Modifier = Modifier, + buttonText: String = stringResource(R.string.got_it), + onGotIt: () -> Unit = {}, +) { + val focusRequester = remember { FocusRequester() } + OnboardingCard( + title = title, + message = message, + modifier = modifier.widthIn(max = TooltipDefaults.richTooltipMaxWidth), + buttonText = buttonText, + buttonModifier = Modifier.focusRequester(focusRequester), + onGotIt = onGotIt, + ) + LaunchedEffect(Unit) { focusRequester.requestFocus() } +} + @Composable @OptIn(ExperimentalMaterial3Api::class) fun OnboardingCard( title: String, message: String, modifier: Modifier = Modifier, + buttonText: String = stringResource(R.string.got_it), + buttonModifier: Modifier = Modifier, onGotIt: () -> Unit = {}, ) { - val focusRequester = remember { FocusRequester() } - ElevatedCard(modifier = modifier.widthIn(max = TooltipDefaults.richTooltipMaxWidth)) { + ElevatedCard(modifier = modifier) { Text( text = title, style = MaterialTheme.typography.titleSmall, @@ -40,15 +65,15 @@ fun OnboardingCard( style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(horizontal = 16.dp), ) - TextButton( - onClick = onGotIt, - modifier = - Modifier.focusRequester(focusRequester).padding(vertical = 8.dp, horizontal = 16.dp), - ) { - Text(text = stringResource(R.string.got_it)) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + TextButton( + onClick = onGotIt, + modifier = buttonModifier.padding(vertical = 8.dp, horizontal = 16.dp), + ) { + Text(text = buttonText) + } } } - LaunchedEffect(Unit) { focusRequester.requestFocus() } } @Preview diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt index 58f1c9461..21b55121f 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt @@ -22,6 +22,7 @@ import org.fdroid.index.v2.SignerV2 import org.fdroid.install.InstallConfirmationState import org.fdroid.install.InstallState import org.fdroid.repo.RepoUpdateProgress +import org.fdroid.search.SavedSearch import org.fdroid.ui.apps.AppUpdateItem import org.fdroid.ui.apps.AppWithIssueItem import org.fdroid.ui.apps.InstalledAppItem @@ -49,6 +50,9 @@ import org.fdroid.ui.repositories.details.RepoDetailsActions import org.fdroid.ui.repositories.details.RepoDetailsInfo import org.fdroid.ui.repositories.details.RepoDetailsModel import org.fdroid.ui.repositories.details.UserMirrorItem +import org.fdroid.ui.search.SearchActions +import org.fdroid.ui.search.SearchInfo +import org.fdroid.ui.search.SearchResults object Names { val randomName: String @@ -256,7 +260,9 @@ fun getAppDetailsActions() = onUninstallResult = { _ -> }, onRepoChanged = {}, onPreferredRepoChanged = {}, + onNotWarnWhenMetered = {}, allowBetaVersions = {}, + onAntiFeaturesOnboardingSeen = {}, ignoreAllUpdates = {}, ignoreThisUpdate = {}, shareApk = Intent(), @@ -407,8 +413,12 @@ fun getMyAppsInfo(model: MyAppsModel): MyAppsInfo = override fun ignoreAppIssue(item: AppWithIssueItem) {} + override fun onUpdatesHintSeen() {} + override fun onAppIssueHintSeen() {} + override fun onNotWarnWhenMetered() {} + override fun exportInstalledApps() {} } } @@ -495,6 +505,7 @@ internal val myAppsModel = lastUpdated = System.currentTimeMillis() - DAYS.toMillis(3), ), ), + showUpdatesHint = true, showAppIssueHint = true, sortOrder = AppListSortOrder.NAME, networkState = NetworkState(isOnline = false, isMetered = false), @@ -549,6 +560,8 @@ fun getRepositoriesInfo(model: RepositoryModel, currentRepositoryId: Long? = nul override fun onRepositoryEnabled(repoId: Long, enabled: Boolean) {} + override fun onNotWarnWhenMetered() {} + override fun onAddRepo() {} override fun onRepositoryMoved(fromRepoId: Long, toRepoId: Long) {} @@ -600,6 +613,8 @@ fun getRepoDetailsInfo( override fun setArchiveRepoEnabled(enabled: Boolean) {} + override fun onNotWarnWhenMetered() {} + override fun onOnboardingSeen() {} } } @@ -623,3 +638,31 @@ fun getRepository( password = password, lastError = lastError, ) + +fun getSearchInfo( + searchResults: SearchResults? = null, + savedSearches: List? = null, + categories: List? = null, +) = + object : SearchInfo { + override val searchResults: SearchResults? = searchResults + + override val savedSearches: List? = savedSearches + + override val categories: List? = categories + override val autoShowKeyboard: Boolean = false + override val showKeyboard: Boolean = false + + override val actions: SearchActions = + object : SearchActions { + override suspend fun onSearch(term: String) {} + + override fun onSearchCleared() {} + + override fun onClearSearchHistory() {} + + override fun setAutoShowKeyboard(autoShow: Boolean) {} + } + + override fun onKeyboardShown() {} + } diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/UiUtils.kt b/app/src/main/kotlin/org/fdroid/ui/utils/UiUtils.kt index caea0d7d7..cdad68279 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/UiUtils.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/UiUtils.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.withContext import org.fdroid.database.Repository @Composable -fun getHintOverlayColor() = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f) +fun getHintOverlayColor() = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) fun Context.startActivitySafe(i: Intent?) { if (i == null) return diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 0ee39d949..e3b6c2e80 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -609,7 +609,6 @@ أظهِر التطبيقات المؤرشفة والإصدارات القديمة من التطبيقات عطّل (مفضّل) - سيؤدي تعطيل هذا المستودع إلى إزالة حالته \"المفضلة\" من جميع التطبيقات. عنوان URL الذي تحاول إضافته (%s) هو مرآة لمستودع موجود. أضِف المستودع والمرآة اعرض المستودع diff --git a/app/src/main/res/values-b+hi+Latn/strings.xml b/app/src/main/res/values-b+hi+Latn/strings.xml index 503e9958d..c49d62825 100644 --- a/app/src/main/res/values-b+hi+Latn/strings.xml +++ b/app/src/main/res/values-b+hi+Latn/strings.xml @@ -529,7 +529,6 @@ %3$s se app details save kiye ja rahe haì (%1$d/%2$d) Update karte waqt error: %s Anján - Is bhanđár ko disable kar dene par iski \"cuni hui\" sthiti sabhi apps se haţ jáyegi. Sabhi bhanđárè up-to-date haì Báki kisi bhi bhanđár ne error nahì diya. Theme diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 231009f24..6f8a3e630 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -571,7 +571,6 @@ Архіўныя рэпазітары не могуць быць дададзены напрамую. Націсніце на рэпазітар са спісу і ўключыце ў ім архіў. Адлюстроўваць архіўныя і састарэлыя версіі праграм Архіў рэпазіторыя - Адключэнне гэтага рэпазітара выдаліць яго статус «пераважны» з усіх праграм. IPFS-шлюзы Дадаць шлюз IPFS Дадаць diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index ab467893d..8fb0852fc 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -557,7 +557,6 @@ Предпочитане на хранилището Хранилището е източник на приложения. Този списък показва всички добавени за момента хранилища. Изключените хранилища не се използват.\n\nАко едно приложение е налично в повече от едно хранилище, се използва хранилището, което е по-горе в списъка. Можете да подреждате хранилищата, с дълго задържане и влачене. Архив на хранилище не може да бъде добавен директно. Докоснете хранилище в списъка и го направете архивно. - Изключване на това хранилище ще премахне състоянието „предпочитано“ от всички приложения. Добавяне на хранилище или огледало Преглед Сканиране на QR diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 591d4ac68..2b51c25ff 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -485,7 +485,6 @@ Llista de repositoris Arxiu del repositori L\'arxiu del dipòsit no està disponible actualment - Desactivant aquest dipòsit traureu el seu estat de \"preferit\" a totes les aplicacions. Actualitzada fa %1$d any Actualitzada fa %1$d anys diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 7861278c1..9195620ec 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -565,7 +565,6 @@ Zakázat Zobrazit archivované a zastaralé verze aplikací Archiv repozitáře momentálně není dostupný - Zakázáním tohoto repozitáře jej odeberete z „vybraných“ u všech aplikací. Repozitář je zdroj aplikací. Tento seznam zobrazuje všechny aktuálně přidané repozitáře. Zakázané repozitáře nejsou používány.\n\nPokud se aplikace nachází více než v jednom repozitáři, bude automaticky vybrán repozitář výše v seznamu. Repozitáře můžete přesunout podržením a přesunutím. nikdy Poslední aktualizace: %s diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index b3d860454..48025cf24 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -545,7 +545,6 @@ Ugyldigt repository.\n\nKontakt vedligeholderen og informer dem om problemet. Start dit Wi-Fi og forbind til det samme netværk som din ven for at udveksle apps. Et repository er en kilde til apps. Denne liste viser alle aktuelt tilføjede repositories. Deaktiverede repositories bruges ikke.\n\nHvis en app findes i mere end ét repository, foretrækkes det øverste på listen automatisk. Du kan ændre rækkefølgen af repositories ved at holde og trække dem. - Deaktiverer du dette repository, fjernes dets \"foretrækkes\" status fra alle apps. Kopieret til udklipsholder Indsæt Tilladelsesnavn kopieret til udklipsholder diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8ad984f07..38427ffee 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -562,7 +562,6 @@ Zeig archivierte Apps und veraltete Versionen von Apps Paketquellen-Archiv Archiv-Paketquelle derzeit nicht verfügbar - Das Deaktivieren dieser Paketquelle wird ihren »bevorzugt«-Status von allen Apps entfernen. Paketquellen-Liste Eine Paketquelle ist eine Quelle für Apps. Diese Liste zeigt alle derzeit hinzugefügten Paketquellen an. Deaktivierte Paketquellen werden nicht verwendet. \n \nWenn eine App in mehr als einer Paketquelle enthalten ist, wird die in der Liste weiter oben stehende Paketquelle automatisch bevorzugt. Du kannst Paketquellen durch langes Drücken und Ziehen neu anordnen. Archiv-Paketquellen können nicht direkt hinzugefügt werden. Tippe auf die Paketquelle in der Liste und aktiviere das Archiv dort. diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 23de548ea..612673fc7 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -536,7 +536,6 @@ \nΕάν μια εφαρμογή βρίσκεται σε περισσότερα από ένα αποθετήρια, προτιμάται αυτόματα το αποθετήριο που βρίσκεται ψηλότερα στη λίστα. Μπορείτε να αναδιατάξετε τα αποθετήρια πατώντας παρατεταμένα και σύροντάς τα. Αρχείο αποθετηρίου Εμφάνιση αρχειοθετημένων εφαρμογών και ξεπερασμένων εκδόσεων εφαρμογών - Η απενεργοποίηση αυτού του αποθετηρίου θα αφαιρέσει την κατάσταση \"προτιμώμενη\" από όλες τις εφαρμογές. Απενεργοποίηση Αντιγραφή URL στο πρόχειρο Για να ανταλλάξετε εφαρμογές, ξεκινήστε το Wi-Fi και συνδεθείτε στο ίδιο δίκτυο με τον φίλο σας. diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index d1dfcbd59..d0c622c40 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -561,7 +561,6 @@ never Repository list Archive repo currently not available - Disabling this repository will remove its \"preferred\" status from all apps. Disable Repositories Use cached results to minimise DNS queries. diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 831d1ca4b..a633fe869 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -551,7 +551,6 @@ Ne eblas senpere aldoni arĥivan deponejon. Frapetu deponejon sur la listo kaj tie aktivigi arĥivon. Arĥiva deponejo ne estas nune disponebla Malaktivigi - Per malaktivigi tiun ĉi deponejon, ĝi estos forigita kiel preferata deponejo por ĉiuj ĝiaj aplikaĵoj. Lasta ĝisdatigo: %s neniam Arĥiva deponejo diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 96f33140b..04144e5ed 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -588,7 +588,6 @@ Lista de repositorios Versiones históricas Muestra aplicaciones antiguas y versiones obsoletas de aplicaciones existentes - Deshabilitar este repositorio eliminará su estado \"preferido\" de todas las aplicaciones. Desactivar Un repositorio es una fuente de aplicaciones. Esta lista muestra todos los repositorios que has añadido. Los repositorios desactivados no se utilizan.\n\nSi una aplicación está en más de un repositorio se utilizará la versión del repositorio que esté más alto en la lista. Puedes reordenar el listado manteniendo pulsado y arrastrando cada entrada. La URL que está intentando añadir (%s) es una réplica de un repositorio existente. diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 0dc616b5a..6ddce00b9 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -567,7 +567,6 @@ Viimane uuendus: %s Arhiiv repo praegu mitte saadaval Näita arhiivitud äppe või äppide vanasid versioone - Selle tarkvarahoidla väljalülitamine eemaldab tema \"eelistatud\" oleku kõikide rakenduste jaoks. Lülita välja mitte kunagi Eelista tarkvarahoidlat diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index c0dd403dc..630addd63 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -576,7 +576,6 @@ Gordailuko artxiboa Erakutsi gordetako aplikazioak eta aplikazioen bertsio zaharkituak Artxiboen gordailua une honetan ez dago erabilgarri - Gordailu hau desgaituz gero, aplikazio guztietatik \"hobetsitakoa\" egoera kenduko da. Desgailu Erabili DNS cachea Erabili cachean gordetako emaitzak DNS kontsultak minimizatzeko. diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 5ae2fef61..f4aa4cbd5 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -551,7 +551,6 @@ سیاههٔ مخزن‌ها بایگانی مخزن نمایش کاره‌های بایگانی شده و نگارش‌های از رده‌خارجشان - از کار انداختن این مخزن وضعیت «ترجیحی»اش را از همهٔ کاره‌ها برمی‌دارد. مخزن بایگانی اکنون در دسترس نیست مخزن مخزن‌ها diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 0bc7a765e..3234f25bb 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -558,7 +558,6 @@ Lataa sovelluksia IPFS-verkkopäätepisteistä. (Toimii vain F-Droid-ohjelmavarastoissa, joissa on IPFS-tuki.) Ohjelmavarastoluettelo Ohjelmavarastoarkisto - Tämän ohjelmavaraston poistaminen käytöstä poistaa sen \"ensisijaisen\" tilan kaikista sovelluksista. Lataa sovelluksia IPFS-verkkopäätepisteiden avulla: pois käytöstä Lataa sovelluksia IPFS-verkkopäätepisteen avulla: %1$d käytössä diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2cd1bb9b4..c935b9966 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -576,7 +576,6 @@ Archive des dépôts Afficher les apps archivées et les anciennes versions des apps Dernière mise à jour : %s - Désactiver ce dépôt enlèvera son statut « préféré » de toutes les applications. jamais Un dépôt est un pourvoyeur d\'applis. Cette liste affiche tous les dépôts actuellement ajoutés. Les dépôts désactivés ne sont pas utilisés. \n \nSi une appli est disponible dans plus d\'un dépôt, le dépôt le plus haut dans la liste est préféré. Vous pouvez changer l\'ordre des dépôts avec un appui long puis en les faisant glisser. Les dépôts d\'archive ne peuvent pas être directement ajoutés. Appuyez sur le dépôt dans la liste puis activez l\'archive à cet emplacement. diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index a39a9b45a..2922ad326 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -437,7 +437,6 @@ Ciallaíonn scriosadh stór nach mbeidh feidhmchláir uaidh ar fáil a thuilleadh. \n \nNóta: Fanfaidh gach feidhmchlár a suiteáladh roimhe seo ar do ghléas. - Má dhíchumasaítear an stór seo, bainfear a stádas \"is fearr\" as gach feidhmchlár. Díchumasaigh Stór pacáiste sábháilte %1$s. Ag lorg stór pacáiste ag diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 4ccfd7a70..07f7f59ff 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -537,7 +537,6 @@ Este espello xa foi engadido (%s). A URL que está tentando engadir (%s) é un espello dun repositorio existente. Arquivo de repositorios - Desactivar este repositorio eliminará o seu estado de \"preferente\" de todas as aplicacións. Desactivar nocturno Para compartir aplicacións, conéctate á mesma rede Wi-Fi que a túa amizade. diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index f5dd483c3..35816a64c 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -585,7 +585,6 @@ מאגר הוא מקור של יישומונים. הרשימה הזאת מציגה את כל המאגרים שנוספו. המאגרים המושבתים אינם בשימוש. \n \nאם יישומון הוא יותר במאגר אחד, המאגר שנמצא יותר גבוה ברשימה מקבל עדיפות אוטומטית. אפשר לסדר את המאגרים מחדש בלחיצה ארוכה עליהם וגרירתם. - השבתת המאגר תסיר את מצב ה„העדפה” שלו מכל היישומונים. מאגר ארכיון לא זמין כרגע הורדת יישומונים באמצעות נקודות גישת IPFS: מושבתת הוספת שער גישת IPFS diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index e5d970030..907c8d2d5 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -605,7 +605,6 @@ Arhivski repozitorij trenutačno nije dostupan Izbrisati zrcalni server? Možeš nastaviti instalirati aplikacije iz ovog repozitorija pomoću drugih zrcalnih servera. - Deaktiviranje ovog repozitorija će ukloniti njegov „preferirani” status iz svih aplikacija. Deaktiviraj Dijeli zrcalni server Prikaži QR kod repozitorija diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 345964194..7ab8d8498 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -570,7 +570,6 @@ soha Az archív tároló jelenleg nem érhető el Archivált alkalmazások és elavult verziók megjelenítése - A tároló letiltásával eltávolítja az „előnyben részesített” állapotát az összes alkalmazástól. Adja meg a Bluetooth vagy a Közeli eszköz engedélyt! Következő frissítés a feltételek teljesülése esetén APK-fájl megosztása diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 63707363a..45b35d7ee 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -538,7 +538,6 @@ Tidak dapat mencari alamat repositori dalam teks terbagi. Arsip repositori Repositori arsip saat ini tidak tersedia - Menonaktifkan repositori ini akan menghilangkan status \"lebih disukai\" dari semua aplikasi. sasaran %s Nonaktifkan Unduh aplikasi dari titik akhir web IPFS. (Hanya bekerja pada repositori F-Droid dengan dukungan IPFS.) diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index e71855a3b..37eaab009 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -557,7 +557,6 @@ Bæta við hugbúnaðarsafni og spegli aldrei Slóðin sem þú ert að reyna að bæta við (%s) er spegill fyrir nýtt hugbúnaðarsafn. Bætt verður við bæði upprunalegu hugbúnaðarsafni og spegli þess. - Sé þetta hugbúnaðarsafn gert óvirkt verður staða þess sem \"ákjósanlegt\" fjarlægð af öllum forritum. Hugbúnaðarsafn (ákjósanlegt) Útvíkka lista yfir hugbúnaðarsöfn diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 859b55bd8..97c1f51ec 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -591,7 +591,6 @@ mai Elenco repository Repo di archivio attualmente non disponibile - Disattivando questo repository verrà rimosso il suo stato \"preferito\" da tutte le app. Aggiungi repository e mirror Vedi il repository Scansiona codice QR diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 56b13a351..3966fd893 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -559,7 +559,6 @@ IPFSウェブ終点を使用してアプリをダウンロード: 無効 IPFSウェブ終点からアプリをダウンロードします。 (IPFSに対応するF-Droidレポジトリだけで機能します。) - このリポジトリを無効にすると、すべてのアプリからそれの「優先」状態が削除されます。 IPFSゲートウェイ IPFSゲートウェイを追加 F-DroidはSDカード上からレポジトリとミラーを検索できますが、権限が必要です。 diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index 0ca4448e6..eee9b2cba 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -501,7 +501,6 @@ ಸಕ್ರಿಯ ವಿ.ಪಿ.ಎನ್ ಜೊತೆಗೆ ಸಂಘರ್ಷಿಸುತ್ತದೆ! ಎಫ್-ಡ್ರಾಯ್ಡ್ ಬೇಸಿಕ್ ಕಸ್ಟಮೈಜ್ ಐ ಪಿ ಎಫ್ ಎಸ್ ಗೇಟ್ವೇಸ್ - ಡಿಸೇಬ್ಲಿಂಗ್ ದಿಸ್ ರೆಪೊಸಿಟರಿ ವಿಲ್ ರಿಮೋವ್ ಇಟ್ಸ್ \"ಪ್ರಿಫರ್ಡ್\" ಸ್ಥಿತಿ ಫ್ರಾಮ್ ಆಲ್ ಅಲ್ ಅಪ್ಪ್ಸ್. ಅಪ್ಲಿಕೇಶನ್ ಫೈಲ್ ಹಂಚಿಸಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ ಈ ಅಪ್ಲಿಕೇಶನ್ ಹಳಈ ಅಪ್ಲಿಕೇಶನ್‌ನು ಹಳೆಯ ಆಂಡ್ರಾಯ್ಡ್ ಆವೃತ್ತಿಗಾಗಿ ನಿರ್ಮಿಸಲಾಗಿದೆ ಮತ್ತು ಸ್ವಯಂಮತ್ತಾಯವಾಗಿ ನವೀಕರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ ನೀವು ಹಳೆಯ ಆವೃತ್ತಿಯ F-Droid ಅನ್ನು ಹೊಂದಿರುವ ಯಾರಾದರು ಸಂಪರ್ಕ ಸ್ಥಾಪಿಸಲು ಪ್ರಯತ್ನಿಸುತ್ತಿದ್ದೀರಿ. ಅವರು ತಮ್ಮ F-Droid ಅಪ್ಲಿಕೇಶನ್‌ನನ್ನು ನವೀಕರಿಸಬೇಕಾಗಿರುತ್ತದೆ. ಅವರು ನಿಮ್ಮಿಂದ F-Droid ಅಪ್ಲಿಕೇಶನ್‌ನನ್ನು ನವೀಕರಿಸಲು ಸ್ವಾಪ್ ಬಳಸಬಹುದು. diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index dee78388e..ad887e082 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -548,7 +548,6 @@ 설치에 권장되는 버전이 없습니다. 이 저장소는 이미 추가되었습니다. Archive 저장소는 직접 추가할 수 없습니다. 목록에서 저장소를 탭하고 거기 아카이브를 활성화. - 이 저장소를 비활성화하면 모든 앱의 \"preferred\"상태를 제거합니다. 대상 %s 마지막 업데이트: %s 한 적 없음 diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index a744c697f..48f96dbf6 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -575,7 +575,6 @@ URL, ko tiek mēģināts pievienot (%s), ir jaunas glabātavas spoguļserveris. Tiks pievienota gan sākotnējā glabātava, gan spoguļserveris. Rādīt arhivētās lietotnes un novecojušas lietotņu versijas - Šīs glabātavas atspējošana noņems tās stāvokli \"vēlama\" no visām lietotnēm. Tuvākais nākamais atjauninājums: %s Nākamais atjauninājums, kad ir izpildījušies nosacījumi Piesaistīti tīkla pakalpojumi diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index ffb9d3e0e..5b46c02da 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -581,7 +581,6 @@ Pakkebrønnarkivet er ikke tilgjengelig nå Slett speilet? Du kan fortsette å installere apper fra denne pakkebrønnen ved å bruke andre speil. - Å skru av denne pakkebrønnen vil fjerne den som «foretrukket» fra alle apper. Skru av Del speil Vis QR-kode til pakkebrønn diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index e332b96f4..c5d22d18d 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -570,7 +570,6 @@ \n \nAls een app in meerdere gegevensbanken voorkomt dan wordt de eerste gegevensbank in de lijst gebruikt. U kunt de volgorde veranderen door een gegevensbank ingedrukt te houden en te verplaatsen. 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