Merge branch '2.0' into 'master'

Fixes for 2.0-alpha2

Closes #3188, #3186, #3185, #3184, #3182, and #3178

See merge request fdroid/fdroidclient!1618
This commit is contained in:
Michael Pöhn
2026-02-10 09:06:36 +00:00
24 changed files with 865 additions and 84 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<NavKey>.myAppsEntry(
val myAppsViewModel = hiltViewModel<MyAppsViewModel>()
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,

View File

@@ -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<InstallingAppItem>,
val appsWithIssue: List<AppWithIssueItem>? = null,
val installedApps: List<InstalledAppItem>? = 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()
}

View File

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

View File

@@ -22,6 +22,7 @@ fun MyAppsPresenter(
appInstallStatesFlow: StateFlow<Map<String, InstallState>>,
appsWithIssuesFlow: StateFlow<List<AppWithIssueItem>?>,
installedAppsFlow: Flow<List<InstalledAppItem>>,
showAppIssueHintFlow: StateFlow<Boolean>,
searchQueryFlow: StateFlow<String>,
sortOrderFlow: StateFlow<AppListSortOrder>,
networkStateFlow: StateFlow<NetworkState>,
@@ -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,

View File

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

View File

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

View File

@@ -97,7 +97,9 @@ private fun Screenshots(phoneScreenshots: List<Any>) {
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 = "",

View File

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

View File

@@ -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<List<AppListItem>?>,
sortByFlow: StateFlow<AppListSortOrder>,
filterIncompatibleFlow: StateFlow<Boolean>,
@@ -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) {

View File

@@ -86,6 +86,7 @@ class AppListViewModel @AssistedInject constructor(
val appListModel: StateFlow<AppListModel> by lazy(LazyThreadSafetyMode.NONE) {
moleculeScope.launchMolecule(mode = ContextClock) {
AppListPresenter(
type = type,
appsFlow = apps,
sortByFlow = sortBy,
filterIncompatibleFlow = filterIncompatible,

View File

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

View File

@@ -1,16 +1,10 @@
<vector android:height="24dp"
android:viewportHeight="48.0"
android:tint="?android:attr/colorControlNormal"
android:viewportWidth="48.0"
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
android:height="24dp"
android:tint="#FFFFFFFF"
android:viewportWidth="48.0"
android:viewportHeight="48.0">
<path
android:fillAlpha="1"
android:fillColor="#ffffff"
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"
android:strokeAlpha="1"
android:strokeColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="miter"
android:strokeWidth="2.5" />
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" />
</vector>

View File

@@ -1,10 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/colorControlNormal"
android:tint="#FFFFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:fillColor="#FFFFFFFF"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z" />
</vector>

View File

@@ -85,6 +85,7 @@
<string name="app_issue_not_available_title">No longer available</string>
<string name="app_issue_not_available_text">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.</string>
<string name="app_issue_not_available_button">Show app info</string>
<string name="app_issue_hint">The apps below need your attention. You can ignore an app issue by long pressing it.</string>
<string name="added_x_ago">Added %1$s</string>
<string name="size_colon">Size: %1$s</string>
<string name="version_code_colon">Version code: %1$s</string>

View File

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

View File

File diff suppressed because it is too large Load Diff

View File

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