diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 31b5a5c28..74a62d9f2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,8 +17,8 @@ android { applicationId = "org.fdroid" minSdk = 24 targetSdk = 36 - versionCode = 2000001 - versionName = "2.0-alpha1" + versionCode = 2000002 + versionName = "2.0-alpha2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/kotlin/org/fdroid/download/NetworkMonitor.kt b/app/src/main/kotlin/org/fdroid/download/NetworkMonitor.kt index 305346fc6..12883f60a 100644 --- a/app/src/main/kotlin/org/fdroid/download/NetworkMonitor.kt +++ b/app/src/main/kotlin/org/fdroid/download/NetworkMonitor.kt @@ -10,19 +10,23 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import org.fdroid.settings.SettingsConstants.AutoUpdateValues +import org.fdroid.settings.SettingsManager import javax.inject.Inject import javax.inject.Singleton @Singleton class NetworkMonitor @Inject constructor( @param:ApplicationContext private val context: Context, + private val settingsManager: SettingsManager, ) : ConnectivityManager.NetworkCallback() { private val connectivityManager = context.getSystemService(ConnectivityManager::class.java) as ConnectivityManager + private val neverMetered get() = settingsManager.autoUpdateApps == AutoUpdateValues.Always private val _networkState = MutableStateFlow( connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)?.let { - NetworkState(it) + NetworkState(it, neverMetered) } ?: NetworkState(isOnline = false, isMetered = false) ) val networkState = _networkState.asStateFlow() @@ -37,7 +41,7 @@ class NetworkMonitor @Inject constructor( } override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { - _networkState.update { NetworkState(networkCapabilities) } + _networkState.update { NetworkState(networkCapabilities, neverMetered) } } override fun onLost(network: Network) { @@ -49,8 +53,9 @@ data class NetworkState( val isOnline: Boolean, val isMetered: Boolean, ) { - constructor(networkCapabilities: NetworkCapabilities) : this( + constructor(networkCapabilities: NetworkCapabilities, neverMetered: Boolean) : this( isOnline = networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET), - isMetered = !networkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED), + isMetered = if (neverMetered) false + else !networkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED), ) } diff --git a/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt b/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt index 4e8c5e56f..0bfa07ccb 100644 --- a/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt +++ b/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt @@ -21,6 +21,7 @@ class OnboardingManager @Inject constructor( const val KEY_FILTER = "appFilter" const val KEY_REPO_LIST = "repoList" const val KEY_REPO_DETAILS = "repoDetails" + const val KEY_APP_ISSUE_HINT = "appIssueHint" } private val prefs = context.getSharedPreferences("onboarding", MODE_PRIVATE) @@ -34,6 +35,9 @@ class OnboardingManager @Inject constructor( private val _showRepoDetailsOnboarding = Onboarding(KEY_REPO_DETAILS, prefs) val showRepoDetailsOnboarding = _showRepoDetailsOnboarding.flow + private val _showAppIssueHint = Onboarding(KEY_APP_ISSUE_HINT, prefs) + val showAppIssueHint = _showAppIssueHint.flow + fun onFilterOnboardingSeen() { _showFilterOnboarding.onSeen(prefs) } @@ -45,6 +49,10 @@ class OnboardingManager @Inject constructor( fun onRepoDetailsOnboardingSeen() { _showRepoDetailsOnboarding.onSeen(prefs) } + + fun onAppIssueHintSeen() { + _showAppIssueHint.onSeen(prefs) + } } private data class Onboarding( diff --git a/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt b/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt index 3ae2e943e..fee827df8 100644 --- a/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt +++ b/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt @@ -2,9 +2,9 @@ package org.fdroid.settings import android.content.Context import android.content.Context.MODE_PRIVATE +import androidx.annotation.UiThread import androidx.core.content.edit import dagger.hilt.android.qualifiers.ApplicationContext -import io.ktor.client.engine.ProxyBuilder import io.ktor.client.engine.ProxyConfig import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -35,6 +35,8 @@ import org.fdroid.settings.SettingsConstants.PREF_KEY_SHOW_INCOMPATIBLE import org.fdroid.settings.SettingsConstants.PREF_KEY_THEME import org.fdroid.settings.SettingsConstants.getAppListSortOrder import org.fdroid.settings.SettingsConstants.toSettings +import java.net.InetSocketAddress +import java.net.Proxy import javax.inject.Inject import javax.inject.Singleton @@ -112,12 +114,15 @@ class SettingsManager @Inject constructor( } val proxyConfig: ProxyConfig? + @UiThread get() { val proxyStr = prefs.getString(PREF_KEY_PROXY, PREF_DEFAULT_PROXY) return if (proxyStr.isNullOrBlank()) null else { val (host, port) = proxyStr.split(':') - ProxyBuilder.socks(host, port.toInt()) + // don't resolve hostname here, or we get NetworkOnMainThreadException + val address = InetSocketAddress.createUnresolved(host, port.toInt()) + Proxy(Proxy.Type.SOCKS, address) } } diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/InstalledAppRow.kt b/app/src/main/kotlin/org/fdroid/ui/apps/InstalledAppRow.kt index d89fdc115..5e50654ce 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/InstalledAppRow.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/InstalledAppRow.kt @@ -19,6 +19,9 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.fdroid.R +import org.fdroid.database.AppIssue +import org.fdroid.database.NoCompatibleSigner +import org.fdroid.database.UpdateInOtherRepo import org.fdroid.ui.FDroidContent import org.fdroid.ui.utils.AsyncShimmerImage import org.fdroid.ui.utils.BadgeIcon @@ -29,15 +32,19 @@ fun InstalledAppRow( app: MyInstalledAppItem, isSelected: Boolean, modifier: Modifier = Modifier, - hasIssue: Boolean = false, + appIssue: AppIssue? = null, ) { Column(modifier = modifier) { ListItem( leadingContent = { BadgedBox(badge = { - if (hasIssue) BadgeIcon( + if (appIssue != null) BadgeIcon( icon = Icons.Filled.Error, - color = MaterialTheme.colorScheme.error, + color = if (appIssue is UpdateInOtherRepo) { + MaterialTheme.colorScheme.inverseSurface + } else { + MaterialTheme.colorScheme.error + }, contentDescription = stringResource(R.string.my_apps_header_apps_with_issue), ) @@ -83,7 +90,8 @@ fun InstalledAppRowPreview() { Column { InstalledAppRow(app, false) InstalledAppRow(app, true) - InstalledAppRow(app, false, hasIssue = true) + InstalledAppRow(app, false, appIssue = UpdateInOtherRepo(2L)) + InstalledAppRow(app, false, appIssue = NoCompatibleSigner()) } } } 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 da1e4f9b4..1821f3154 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt @@ -65,7 +65,7 @@ fun MyApps( val app = myAppsModel.appToConfirm if (app != null && lifecycleOwner.lifecycle.currentState.isAtLeast(STARTED)) { val state = app.installState as InstallConfirmationState - myAppsInfo.confirmAppInstall(app.packageName, state) + myAppsInfo.actions.confirmAppInstall(app.packageName, state) } } val installingApps = myAppsModel.installingApps @@ -74,7 +74,7 @@ fun MyApps( val installedApps = myAppsModel.installedApps val scrollBehavior = enterAlwaysScrollBehavior(rememberTopAppBarState()) var searchActive by rememberSaveable { mutableStateOf(false) } - val onSearchCleared = { myAppsInfo.search("") } + val onSearchCleared = { myAppsInfo.actions.search("") } // when search bar is shown, back button closes it again BackHandler(enabled = searchActive) { searchActive = false @@ -84,7 +84,10 @@ fun MyApps( Scaffold( topBar = { if (searchActive) { - TopSearchBar(onSearch = myAppsInfo::search, onSearchCleared = onSearchCleared) { + TopSearchBar( + onSearch = myAppsInfo.actions::search, + onSearchCleared = onSearchCleared, + ) { onBackPressedDispatcher?.onBackPressed() } } else TopAppBar( @@ -121,7 +124,7 @@ fun MyApps( ) }, onClick = { - myAppsInfo.changeSortOrder(AppListSortOrder.NAME) + myAppsInfo.actions.changeSortOrder(AppListSortOrder.NAME) sortByMenuExpanded = false }, ) @@ -137,7 +140,7 @@ fun MyApps( ) }, onClick = { - myAppsInfo.changeSortOrder(LAST_UPDATED) + myAppsInfo.actions.changeSortOrder(LAST_UPDATED) sortByMenuExpanded = false }, ) @@ -186,6 +189,7 @@ fun MyAppsLoadingPreview() { installingApps = emptyList(), appUpdates = null, installedApps = null, + showAppIssueHint = false, sortOrder = AppListSortOrder.NAME, networkState = NetworkState(isOnline = false, isMetered = false), ) @@ -210,3 +214,24 @@ fun MyAppsPreview() { ) } } + +@Preview +@Composable +@RestrictTo(RestrictTo.Scope.TESTS) +fun MyAppsEmptyPreview() { + FDroidContent { + val model = MyAppsModel( + installingApps = emptyList(), + appUpdates = emptyList(), + installedApps = emptyList(), + showAppIssueHint = false, + sortOrder = AppListSortOrder.NAME, + networkState = NetworkState(isOnline = false, isMetered = false), + ) + MyApps( + myAppsInfo = getMyAppsInfo(model), + currentPackageName = null, + onAppItemClick = {}, + ) + } +} diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsEntry.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsEntry.kt index 62b0c236a..7ea632594 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsEntry.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsEntry.kt @@ -6,8 +6,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey -import org.fdroid.database.AppListSortOrder -import org.fdroid.install.InstallConfirmationState import org.fdroid.ui.navigation.NavigationKey import org.fdroid.ui.navigation.Navigator @@ -22,19 +20,7 @@ fun EntryProviderScope.myAppsEntry( val myAppsViewModel = hiltViewModel() val myAppsInfo = object : MyAppsInfo { override val model = myAppsViewModel.myAppsModel.collectAsStateWithLifecycle().value - - override fun updateAll() = myAppsViewModel.updateAll() - override fun changeSortOrder(sort: AppListSortOrder) = - myAppsViewModel.changeSortOrder(sort) - - override fun search(query: String) = myAppsViewModel.search(query) - override fun confirmAppInstall( - packageName: String, - state: InstallConfirmationState, - ) = myAppsViewModel.confirmAppInstall(packageName, state) - - override fun ignoreAppIssue(item: AppWithIssueItem) = - myAppsViewModel.ignoreAppIssue(item) + override val actions: MyAppsActions = myAppsViewModel } MyApps( myAppsInfo = myAppsInfo, 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 f3b1d54fe..66c6c6ca6 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt @@ -6,11 +6,7 @@ import org.fdroid.install.InstallConfirmationState interface MyAppsInfo { val model: MyAppsModel - fun updateAll() - fun changeSortOrder(sort: AppListSortOrder) - fun search(query: String) - fun confirmAppInstall(packageName: String, state: InstallConfirmationState) - fun ignoreAppIssue(item: AppWithIssueItem) + val actions: MyAppsActions } data class MyAppsModel( @@ -19,7 +15,17 @@ data class MyAppsModel( val installingApps: List, val appsWithIssue: List? = null, val installedApps: List? = null, + val showAppIssueHint: Boolean, val sortOrder: AppListSortOrder = AppListSortOrder.NAME, val networkState: NetworkState, val appUpdatesBytes: Long? = null, ) + +interface MyAppsActions { + fun updateAll() + fun changeSortOrder(sort: AppListSortOrder) + fun search(query: String) + fun confirmAppInstall(packageName: String, state: InstallConfirmationState) + fun ignoreAppIssue(item: AppWithIssueItem) + fun onAppIssueHintSeen() +} 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 e3b077482..0001beced 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt @@ -3,8 +3,11 @@ package org.fdroid.ui.apps import androidx.annotation.RestrictTo import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +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.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -12,10 +15,14 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.material3.Button +import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -45,6 +52,16 @@ fun MyAppsList( val installingApps = myAppsInfo.model.installingApps val appsWithIssue = myAppsInfo.model.appsWithIssue val installedApps = myAppsInfo.model.installedApps + // scroll to top if new updatable apps were added + var previousNumUpdates by remember { mutableIntStateOf(0) } + LaunchedEffect(updatableApps) { + if (updatableApps != null && updatableApps.isNotEmpty()) { + if (updatableApps.size > previousNumUpdates) { + lazyListState.animateScrollToItem(0) + } + previousNumUpdates = updatableApps.size + } + } // allow us to hide "update all" button to avoid user pressing it twice var showUpdateAllButton by remember(updatableApps) { mutableStateOf(true) @@ -79,7 +96,7 @@ fun MyAppsList( if (showUpdateAllButton) Button( onClick = { val installLambda = { - myAppsInfo.updateAll() + myAppsInfo.actions.updateAll() showUpdateAllButton = false } if (myAppsInfo.model.networkState.isMetered) { @@ -160,6 +177,30 @@ fun MyAppsList( modifier = Modifier.padding(16.dp), ) } + if (myAppsInfo.model.showAppIssueHint) item(key = "C-hint", contentType = "hint") { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp, start = 16.dp, end = 16.dp), + ) { + Column( + modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp) + ) { + Text( + text = stringResource(R.string.app_issue_hint), + style = MaterialTheme.typography.bodyMedium, + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + TextButton(onClick = { myAppsInfo.actions.onAppIssueHintSeen() }) { + Text(stringResource(R.string.got_it)) + } + } + } + } + } // list of apps with issues items( items = appsWithIssue, @@ -190,13 +231,13 @@ fun MyAppsList( val modifier = Modifier .animateItem() .then(interactionModifier) - InstalledAppRow(app, isSelected, modifier, hasIssue = true) + InstalledAppRow(app, isSelected, modifier, app.issue) // Dialogs val appToIgnore = showIssueIgnoreDialog if (appToIgnore != null) IgnoreIssueDialog( appName = appToIgnore.name, onIgnore = { - myAppsInfo.ignoreAppIssue(appToIgnore) + myAppsInfo.actions.ignoreAppIssue(appToIgnore) showIssueIgnoreDialog = null }, onDismiss = { showIssueIgnoreDialog = null }, 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 b7e9f9d05..cb2ac6c12 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt @@ -22,6 +22,7 @@ fun MyAppsPresenter( appInstallStatesFlow: StateFlow>, appsWithIssuesFlow: StateFlow?>, installedAppsFlow: Flow>, + showAppIssueHintFlow: StateFlow, searchQueryFlow: StateFlow, sortOrderFlow: StateFlow, networkStateFlow: StateFlow, @@ -97,6 +98,7 @@ fun MyAppsPresenter( appUpdates = updates?.sort(sortOrder), appsWithIssue = withIssues?.sort(sortOrder), installedApps = installed?.sort(sortOrder), + showAppIssueHint = showAppIssueHintFlow.collectAsState().value, sortOrder = sortOrder, networkState = networkStateFlow.collectAsState().value, 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 7b453667f..8d240409c 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt @@ -28,6 +28,7 @@ import org.fdroid.install.AppInstallManager import org.fdroid.install.InstallConfirmationState import org.fdroid.install.InstallState import org.fdroid.install.InstalledAppsCache +import org.fdroid.settings.OnboardingManager import org.fdroid.settings.SettingsManager import org.fdroid.updates.UpdatesManager import org.fdroid.utils.IoDispatcher @@ -40,12 +41,13 @@ class MyAppsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val db: FDroidDatabase, private val settingsManager: SettingsManager, - private val installedAppsCache: InstalledAppsCache, + installedAppsCache: InstalledAppsCache, + private val onboardingManager: OnboardingManager, private val appInstallManager: AppInstallManager, private val networkMonitor: NetworkMonitor, private val updatesManager: UpdatesManager, private val repoManager: RepoManager, -) : AndroidViewModel(app) { +) : AndroidViewModel(app), MyAppsActions { private val log = KotlinLogging.logger { } private val localeList = LocaleListCompat.getDefault() @@ -83,6 +85,7 @@ class MyAppsViewModel @Inject constructor( appInstallStatesFlow = appInstallManager.appInstallStates, appsWithIssuesFlow = updatesManager.appsWithIssues, installedAppsFlow = installedAppItems, + showAppIssueHintFlow = onboardingManager.showAppIssueHint, searchQueryFlow = searchQuery, sortOrderFlow = sortOrder, networkStateFlow = networkMonitor.networkState, @@ -90,21 +93,21 @@ class MyAppsViewModel @Inject constructor( } } - fun updateAll() { + override fun updateAll() { scope.launch { updatesManager.updateAll(true) } } - fun search(query: String) { + override fun search(query: String) { searchQuery.value = query } - fun changeSortOrder(sort: AppListSortOrder) { + override fun changeSortOrder(sort: AppListSortOrder) { sortOrder.value = sort } - fun confirmAppInstall(packageName: String, state: InstallConfirmationState) { + override fun confirmAppInstall(packageName: String, state: InstallConfirmationState) { log.info { "Asking user to confirm install of $packageName..." } scope.launch(Dispatchers.Main) { when (state) { @@ -118,8 +121,10 @@ class MyAppsViewModel @Inject constructor( } } - fun ignoreAppIssue(item: AppWithIssueItem) { + override fun ignoreAppIssue(item: AppWithIssueItem) { settingsManager.ignoreAppIssue(item.packageName, item.installedVersionCode) updatesManager.loadUpdates() } + + override fun onAppIssueHintSeen() = onboardingManager.onAppIssueHintSeen() } diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/UpdatableAppRow.kt b/app/src/main/kotlin/org/fdroid/ui/apps/UpdatableAppRow.kt index 29ca70a8a..7fa794120 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/UpdatableAppRow.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/UpdatableAppRow.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.LiveRegionMode @@ -30,6 +31,7 @@ import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.liveRegion import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import org.fdroid.R import org.fdroid.ui.FDroidContent @@ -75,7 +77,12 @@ fun UpdatableAppRow( val size = app.update.size?.let { Formatter.formatFileSize(LocalContext.current, it) } - Text("${app.installedVersionName} → ${app.update.versionName} • $size") + val text = if (LocalLayoutDirection.current == LayoutDirection.Ltr) { + "${app.installedVersionName} → ${app.update.versionName} • $size" + } else { + "$size • ${app.update.versionName} ← ${app.installedVersionName}" + } + Text(text) }, trailingContent = { if (app.whatsNew != null) IconButton(onClick = { isExpanded = !isExpanded }) { @@ -135,3 +142,21 @@ fun UpdatableAppRowPreview() { } } } + +@Preview(locale = "fa") +@Composable +private fun UpdatableAppRowRtl() { + val app1 = AppUpdateItem( + repoId = 1, + packageName = "A", + name = "App Update 123", + installedVersionName = "1.0.1", + update = getPreviewVersion("1.1.0", 123456789), + whatsNew = "This is new, all is new, nothing old.", + ) + FDroidContent { + Column { + UpdatableAppRow(app1, false) + } + } +} diff --git a/app/src/main/kotlin/org/fdroid/ui/details/Screenshots.kt b/app/src/main/kotlin/org/fdroid/ui/details/Screenshots.kt index 3237605e6..174367c1a 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/Screenshots.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/Screenshots.kt @@ -97,7 +97,9 @@ private fun Screenshots(phoneScreenshots: List) { pageCount = { phoneScreenshots.size }, ) Surface { - HorizontalPager(state = pagerState) { page -> + // The overscrollEffect was bouncing screenshots with each swipe. + // Maybe this was a bug and overscroll effect can be enabled again once fixed. + HorizontalPager(state = pagerState, overscrollEffect = null) { page -> AsyncShimmerImage( model = phoneScreenshots[page], contentDescription = "", diff --git a/app/src/main/kotlin/org/fdroid/ui/discover/Discover.kt b/app/src/main/kotlin/org/fdroid/ui/discover/Discover.kt index 181785efb..4402a5108 100644 --- a/app/src/main/kotlin/org/fdroid/ui/discover/Discover.kt +++ b/app/src/main/kotlin/org/fdroid/ui/discover/Discover.kt @@ -3,6 +3,7 @@ package org.fdroid.ui.discover import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -61,9 +62,10 @@ fun Discover( BadgedBox(badge = { val hasRepoIssues = (discoverModel as? LoadedDiscoverModel)?.hasRepoIssues == true - if (dest.id == NavigationKey.Repos && hasRepoIssues) Badge { - Text("") - } + if (dest.id == NavigationKey.Repos && hasRepoIssues) Badge( + content = null, + modifier = Modifier.size(8.dp) + ) }) { IconButton(onClick = { onNav(dest.id) }) { Icon( diff --git a/app/src/main/kotlin/org/fdroid/ui/lists/AppListPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/lists/AppListPresenter.kt index 20eafc36b..cde6a045e 100644 --- a/app/src/main/kotlin/org/fdroid/ui/lists/AppListPresenter.kt +++ b/app/src/main/kotlin/org/fdroid/ui/lists/AppListPresenter.kt @@ -2,6 +2,7 @@ package org.fdroid.ui.lists +import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember @@ -15,6 +16,7 @@ import java.util.Locale @Composable fun AppListPresenter( + type: AppListType, appsFlow: StateFlow?>, sortByFlow: StateFlow, filterIncompatibleFlow: StateFlow, @@ -40,7 +42,12 @@ fun AppListPresenter( } val filteredCategories = remember(categories, apps) { categories?.filter { - it.id in availableCategoryIds + if (type is AppListType.Category) { + // don't show category for list we are currently seeing, because all apps are in it + it.id in availableCategoryIds && it.id != type.categoryId + } else { + it.id in availableCategoryIds + } } } val availableRepositories = remember(apps) { @@ -61,6 +68,8 @@ fun AppListPresenter( val matchesCompatibility = !filterIncompatible || it.isCompatible matchesCategories && matchesRepos && matchesQuery && matchesCompatibility } + + @SuppressLint("NonObservableLocale") // the alternative isn't available here val locale = Locale.getDefault() return AppListModel( apps = if (sortBy == AppListSortOrder.NAME) { diff --git a/app/src/main/kotlin/org/fdroid/ui/lists/AppListViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/lists/AppListViewModel.kt index 05d2e6e30..fb87b641c 100644 --- a/app/src/main/kotlin/org/fdroid/ui/lists/AppListViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/lists/AppListViewModel.kt @@ -86,6 +86,7 @@ class AppListViewModel @AssistedInject constructor( val appListModel: StateFlow by lazy(LazyThreadSafetyMode.NONE) { moleculeScope.launchMolecule(mode = ContextClock) { AppListPresenter( + type = type, appsFlow = apps, sortByFlow = sortBy, filterIncompatibleFlow = filterIncompatible, 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 e805efdaf..1458abd5d 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.ui.apps.AppUpdateItem import org.fdroid.ui.apps.AppWithIssueItem import org.fdroid.ui.apps.InstalledAppItem import org.fdroid.ui.apps.InstallingAppItem +import org.fdroid.ui.apps.MyAppsActions import org.fdroid.ui.apps.MyAppsInfo import org.fdroid.ui.apps.MyAppsModel import org.fdroid.ui.categories.CategoryItem @@ -268,11 +269,15 @@ fun getAppListInfo(model: AppListModel) = object : AppListInfo { fun getMyAppsInfo(model: MyAppsModel): MyAppsInfo = object : MyAppsInfo { override val model = model - override fun updateAll() {} - override fun changeSortOrder(sort: AppListSortOrder) {} - override fun search(query: String) {} - override fun confirmAppInstall(packageName: String, state: InstallConfirmationState) {} - override fun ignoreAppIssue(item: AppWithIssueItem) {} + override val actions: MyAppsActions + get() = object : MyAppsActions { + override fun updateAll() {} + override fun changeSortOrder(sort: AppListSortOrder) {} + override fun search(query: String) {} + override fun confirmAppInstall(packageName: String, state: InstallConfirmationState) {} + override fun ignoreAppIssue(item: AppWithIssueItem) {} + override fun onAppIssueHintSeen() {} + } } @RestrictTo(RestrictTo.Scope.TESTS) @@ -348,6 +353,7 @@ internal val myAppsModel = MyAppsModel( lastUpdated = System.currentTimeMillis() - DAYS.toMillis(3) ) ), + showAppIssueHint = true, sortOrder = AppListSortOrder.NAME, networkState = NetworkState(isOnline = false, isMetered = false), appUpdatesBytes = null, diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml index 53951dfa5..5d9a86611 100644 --- a/app/src/main/res/drawable/ic_notification.xml +++ b/app/src/main/res/drawable/ic_notification.xml @@ -1,16 +1,10 @@ - + android:height="24dp" + android:tint="#FFFFFFFF" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + android:fillColor="#FFFFFFFF" + android:pathData="m2.61,1.25c-0.45,0.01 -0.76,0.19 -0.98,0.45 0,0 0,0 0.01,0.01 -0,0 -0.01,0.01 -0.01,0.01 -0,-0 -0,-0 -0,-0.01 -0,0 -0.01,0.02 -0.02,0.03 -0.01,0.01 -0.01,0.02 -0.02,0.03 -0.15,0.2 -0.49,0.8 0.01,1.5L5.21,7.93C5.08,8.26 5,8.62 5,9v3,1 2.46L5,16c0,1.66 1.34,3 3,3h32c1.66,0 3,-1.34 3,-3L43,15.46 43,9.54 43,9C43,8.62 42.92,8.26 42.79,7.93l3.61,-4.66c0.5,-0.7 0.16,-1.3 0.01,-1.5 -0.01,-0.01 -0.01,-0.02 -0.02,-0.03 -0,-0.01 -0.01,-0.02 -0.02,-0.03 -0,0 -0,0 -0,0.01 -0,-0 -0.01,-0.01 -0.01,-0.01 0,-0 0,-0 0.01,-0.01 -0.22,-0.26 -0.53,-0.44 -0.98,-0.45 -0.38,0.01 -0.74,0.18 -0.97,0.49L40.99,6.18C40.86,6.13 40.74,6.09 40.61,6.06 40.41,6.02 40.21,6 40,6L8,6C7.65,6 7.32,6.07 7.01,6.18L3.58,1.74C3.35,1.43 2.99,1.26 2.61,1.25ZM14.38,9.75c1.86,0 3.38,1.51 3.38,3.38 0,1.86 -1.51,3.38 -3.38,3.38C12.51,16.5 11,14.99 11,13.13 11,11.26 12.51,9.75 14.38,9.75ZM33.88,9.75c1.86,0 3.38,1.51 3.38,3.38 0,1.86 -1.51,3.38 -3.38,3.38 -1.86,0 -3.38,-1.51 -3.38,-3.38 0,-1.86 1.51,-3.38 3.38,-3.38zM8,20.97c-1.66,0 -3,1.34 -3,3L5,30 5,36 5,42.23 5,43c0,1.66 1.34,3 3,3h32c1.66,0 3,-1.34 3,-3L43,42.23 43,24.73 43,23.97c0,-1.66 -1.34,-3 -3,-3z" /> diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml index 15e746762..bfa9b4212 100644 --- a/app/src/main/res/drawable/ic_refresh.xml +++ b/app/src/main/res/drawable/ic_refresh.xml @@ -1,10 +1,10 @@ diff --git a/app/src/main/res/values/new-strings.xml b/app/src/main/res/values/new-strings.xml index 364f27756..2474a3f9d 100644 --- a/app/src/main/res/values/new-strings.xml +++ b/app/src/main/res/values/new-strings.xml @@ -85,6 +85,7 @@ No longer available This app is not receiving updates, because it is no longer in any enabled repository.\n\nIt may have been in a repository you removed or disabled. Or it was simply removed. Show app info + The apps below need your attention. You can ignore an app issue by long pressing it. Added %1$s Size: %1$s Version code: %1$s diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4f6222c5e..2a195aa32 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] compileSdk = "36" -kotlin = "2.3.0" +kotlin = "2.3.10" androidGradlePlugin = "8.13.2" androidKspPlugin = "2.3.4" hilt = "2.57.2" @@ -10,11 +10,11 @@ dokka = "2.1.0" mavenPublish = "0.35.0" jlleitschuhKtlint = "14.0.1" -kotlinxSerializationCore = "1.9.0" -kotlinxSerializationJson = "1.9.0" +kotlinxSerializationCore = "1.10.0" +kotlinxSerializationJson = "1.10.0" kotlinxCoroutinesTest = "1.10.2" -ktor = "3.3.3" +ktor = "3.4.0" okhttp = "4.12.0" room = "2.8.4" glide = "5.0.5" @@ -28,7 +28,7 @@ androidxCoreKtx = "1.17.0" androidxAppcompat = "1.7.1" androidxPreferenceKtx = "1.2.1" androidxLifecycleLivedataKtx = "2.10.0" -androidxWork = "2.11.0" +androidxWork = "2.11.1" androidxDocumentfile = "1.1.0" androidxLocalbroadcastmanager = "1.1.0" androidxHiltCompiler = "1.3.0" @@ -37,8 +37,8 @@ androidxConstraintlayout = "2.2.1" androidxSwipeRefreshLayout = "1.2.0" androidxVectordrawable = "1.2.0" androidxGridlayout = "1.1.0" -androidxComposeBom = "2025.12.01" -androidxActivityCompose = "1.12.2" +androidxComposeBom = "2026.01.01" +androidxActivityCompose = "1.12.3" accompanistDrawablepainter = "0.37.3" # navigation3 @@ -67,12 +67,12 @@ rxandroid = "3.0.2" slf4jApi = "2.0.17" microutilsKotlinLogging = "3.0.5" -logbackClassic = "1.5.22" +logbackClassic = "1.5.27" logbackAndroid = "3.0.0" junit = "4.13.2" mockk = "1.14.6" -robolectric = "4.16" +robolectric = "4.16.1" androidxTestCore = "1.7.0" androidxTestRunner = "1.7.0" androidxTestExtJunit = "1.3.0" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 046a35c48..180635919 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -206,6 +206,11 @@ + + + + + @@ -256,6 +261,11 @@ + + + + + @@ -291,6 +301,11 @@ + + + + + @@ -736,6 +751,11 @@ + + + + + @@ -831,6 +851,11 @@ + + + + + @@ -941,6 +966,11 @@ + + + + + @@ -1036,6 +1066,11 @@ + + + + + @@ -1111,6 +1146,11 @@ + + + + + @@ -1321,6 +1361,11 @@ + + + + + @@ -1401,26 +1446,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1471,6 +1541,11 @@ + + + + + @@ -1521,6 +1596,11 @@ + + + + + @@ -1531,6 +1611,11 @@ + + + + + @@ -1581,6 +1666,11 @@ + + + + + @@ -1676,6 +1766,11 @@ + + + + + @@ -1771,6 +1866,11 @@ + + + + + @@ -1866,6 +1966,11 @@ + + + + + @@ -1911,16 +2016,31 @@ + + + + + + + + + + + + + + + @@ -1976,6 +2096,11 @@ + + + + + @@ -2026,6 +2151,11 @@ + + + + + @@ -2076,6 +2206,11 @@ + + + + + @@ -2171,6 +2306,11 @@ + + + + + @@ -2266,6 +2406,11 @@ + + + + + @@ -2351,6 +2496,11 @@ + + + + + @@ -4132,11 +4282,21 @@ + + + + + + + + + + @@ -5461,6 +5621,11 @@ + + + + + @@ -5491,6 +5656,11 @@ + + + + + @@ -5531,6 +5701,11 @@ + + + + + @@ -5726,6 +5901,11 @@ + + + + + @@ -5776,6 +5956,11 @@ + + + + + @@ -11336,6 +11521,11 @@ + + + + + @@ -11401,6 +11591,11 @@ + + + + + @@ -12115,6 +12310,11 @@ + + + + + @@ -12223,6 +12423,11 @@ + + + + + @@ -12299,6 +12504,11 @@ + + + + + @@ -12469,6 +12679,11 @@ + + + + + @@ -12535,6 +12750,11 @@ + + + + + @@ -12627,6 +12847,11 @@ + + + + + @@ -12694,6 +12919,11 @@ + + + + + @@ -12771,6 +13001,11 @@ + + + + + @@ -12837,6 +13072,11 @@ + + + + + @@ -12935,6 +13175,11 @@ + + + + + @@ -13017,6 +13262,11 @@ + + + + + @@ -13057,6 +13307,11 @@ + + + + + @@ -13154,6 +13409,11 @@ + + + + + @@ -13267,6 +13527,11 @@ + + + + + @@ -13348,6 +13613,11 @@ + + + + + @@ -13451,6 +13721,11 @@ + + + + + @@ -13491,6 +13766,11 @@ + + + + + @@ -13567,6 +13847,11 @@ + + + + + @@ -13659,6 +13944,11 @@ + + + + + @@ -13725,6 +14015,11 @@ + + + + + @@ -13755,6 +14050,11 @@ + + + + + @@ -13780,6 +14080,11 @@ + + + + + @@ -13873,6 +14178,11 @@ + + + + + @@ -13949,6 +14259,11 @@ + + + + + @@ -14047,6 +14362,11 @@ + + + + + @@ -14113,6 +14433,11 @@ + + + + + @@ -14195,6 +14520,11 @@ + + + + + @@ -14261,6 +14591,11 @@ + + + + + @@ -17003,6 +17338,11 @@ + + + + + @@ -17018,6 +17358,11 @@ + + + + + @@ -17043,11 +17388,21 @@ + + + + + + + + + + @@ -17068,6 +17423,11 @@ + + + + + @@ -17218,6 +17578,11 @@ + + + + + @@ -17248,11 +17613,21 @@ + + + + + + + + + + @@ -17283,6 +17658,11 @@ + + + + + @@ -17358,6 +17738,11 @@ + + + + + @@ -17423,6 +17808,11 @@ + + + + + @@ -17448,6 +17838,11 @@ + + + + + @@ -17513,6 +17908,11 @@ + + + + + @@ -17588,6 +17988,11 @@ + + + + + @@ -17665,6 +18070,11 @@ + + + + + @@ -17700,6 +18110,11 @@ + + + + + @@ -17818,6 +18233,11 @@ + + + + + @@ -17868,6 +18288,11 @@ + + + + + @@ -17913,6 +18338,11 @@ + + + + + @@ -17978,6 +18408,11 @@ + + + + + @@ -18043,6 +18478,11 @@ + + + + + @@ -18108,6 +18548,11 @@ + + + + + @@ -18128,6 +18573,11 @@ + + + + + @@ -18203,6 +18653,11 @@ + + + + + @@ -18403,6 +18858,11 @@ + + + + + @@ -18478,6 +18938,11 @@ + + + + + @@ -18543,6 +19008,11 @@ + + + + + @@ -18608,6 +19078,11 @@ + + + + + @@ -18673,6 +19148,11 @@ + + + + + @@ -18738,6 +19218,11 @@ + + + + + @@ -18809,6 +19294,11 @@ + + + + + @@ -18839,6 +19329,11 @@ + + + + + @@ -19059,6 +19554,14 @@ + + + + + + + + @@ -19484,6 +19987,14 @@ + + + + + + + + @@ -19629,6 +20140,11 @@ + + + + + @@ -19684,6 +20200,11 @@ + + + + + @@ -19774,6 +20295,11 @@ + + + + + @@ -19839,6 +20365,11 @@ + + + + + @@ -19859,6 +20390,11 @@ + + + + + @@ -20335,6 +20871,11 @@ + + + + + @@ -20345,6 +20886,11 @@ + + + + + @@ -20355,6 +20901,11 @@ + + + + + @@ -20365,6 +20916,11 @@ + + + + + @@ -20375,6 +20931,11 @@ + + + + + @@ -20433,6 +20994,11 @@ + + + + + @@ -20498,6 +21064,11 @@ + + + + + @@ -20551,6 +21122,11 @@ + + + + + @@ -20968,6 +21544,11 @@ + + + + + @@ -21112,6 +21693,11 @@ + + + + + @@ -21219,6 +21805,11 @@ + + + + + @@ -21272,6 +21863,11 @@ + + + + + @@ -21325,6 +21921,11 @@ + + + + + @@ -21395,6 +21996,11 @@ + + + + + @@ -21448,6 +22054,11 @@ + + + + + @@ -21501,6 +22112,11 @@ + + + + + @@ -21554,6 +22170,11 @@ + + + + + @@ -21607,6 +22228,11 @@ + + + + + @@ -21660,6 +22286,11 @@ + + + + + @@ -21713,6 +22344,11 @@ + + + + + @@ -21776,6 +22412,11 @@ + + + + + @@ -21829,6 +22470,11 @@ + + + + + diff --git a/libs/database/src/main/java/org/fdroid/database/DbAppChecker.kt b/libs/database/src/main/java/org/fdroid/database/DbAppChecker.kt index 5b4d20c53..663e9df3b 100644 --- a/libs/database/src/main/java/org/fdroid/database/DbAppChecker.kt +++ b/libs/database/src/main/java/org/fdroid/database/DbAppChecker.kt @@ -9,6 +9,7 @@ import org.fdroid.CompatibilityChecker import org.fdroid.CompatibilityCheckerImpl import org.fdroid.UpdateChecker import org.fdroid.index.IndexUtils.getPackageSigner +import java.util.concurrent.TimeUnit public class DbAppChecker( db: FDroidDatabase, @@ -115,12 +116,15 @@ public class DbAppChecker( ) } else if (hasCompatibleSigner) { // the signer is compatible, so the update must come from a non-preferred repo - AvailableAppWithIssue( + val now = System.currentTimeMillis() + // Only flag the compatible update in another repo, if older than a week. + // This is to prevent short delays in providing updates causing unneeded UX churn. + if (now - update.added > TimeUnit.DAYS.toMillis(7)) AvailableAppWithIssue( app = app, installVersionName = packageInfo.versionName ?: "???", installVersionCode = getLongVersionCode(packageInfo), issue = UpdateInOtherRepo(update.repoId), - ) + ) else null } else { // no update with compatible signer available getNoCompatibleSignerApp( diff --git a/src/basic/fastlane/metadata/android/en-US/changelogs/2000001.txt b/src/basic/fastlane/metadata/android/en-US/changelogs/2000002.txt similarity index 100% rename from src/basic/fastlane/metadata/android/en-US/changelogs/2000001.txt rename to src/basic/fastlane/metadata/android/en-US/changelogs/2000002.txt