From ee89b59bb01e0724a386bbdcebae3c863b957a1c Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 30 Oct 2025 12:09:49 -0300 Subject: [PATCH] Improve presentation of install errors we now show a dialog with collapsed technical error message instead of a snackbar and the "Installing" section of My apps also shows installation errors. --- .../org/fdroid/install/AppInstallManager.kt | 19 ++++++++- .../kotlin/org/fdroid/install/InstallState.kt | 19 ++++++++- .../fdroid/install/SessionInstallManager.kt | 9 ++-- .../org/fdroid/ui/apps/InstallingAppRow.kt | 23 ++++++++++ .../org/fdroid/ui/apps/MyAppsPresenter.kt | 34 +++++++-------- .../org/fdroid/ui/apps/MyAppsViewModel.kt | 2 +- .../org/fdroid/ui/details/AppDetails.kt | 42 +++++++++++++++---- next/src/main/res/values/strings-next.xml | 1 + 8 files changed, 115 insertions(+), 34 deletions(-) diff --git a/next/src/main/kotlin/org/fdroid/install/AppInstallManager.kt b/next/src/main/kotlin/org/fdroid/install/AppInstallManager.kt index f19c14bd1..376438a82 100644 --- a/next/src/main/kotlin/org/fdroid/install/AppInstallManager.kt +++ b/next/src/main/kotlin/org/fdroid/install/AppInstallManager.kt @@ -162,7 +162,14 @@ class AppInstallManager @Inject constructor( ) // continue depending on result, abort early if no approval was given return when (preApprovalResult) { - is PreApprovalResult.Error -> InstallState.Error(preApprovalResult.errorMsg) + is PreApprovalResult.Error -> InstallState.Error( + msg = preApprovalResult.errorMsg, + name = appMetadata.name.getBestLocale(LocaleListCompat.getDefault()) ?: "Unknown", + versionName = version.versionName, + currentVersionName = currentVersionName, + lastUpdated = version.added, + iconDownloadRequest = iconDownloadRequest, + ) is PreApprovalResult.UserAborted -> InstallState.UserAborted is PreApprovalResult.Success, PreApprovalResult.NotSupported -> { apps.checkAndUpdateApp(appMetadata.packageName) { @@ -205,7 +212,15 @@ class AppInstallManager @Inject constructor( if (e is CancellationException) throw e log.error(e) { "Error downloading ${version.file}" } val msg = "Download failed: ${e::class.java.simpleName} ${e.message}" - return InstallState.Error(msg) + return InstallState.Error( + msg = msg, + name = appMetadata.name.getBestLocale(LocaleListCompat.getDefault()) + ?: "Unknown", + versionName = version.versionName, + currentVersionName = currentVersionName, + lastUpdated = version.added, + iconDownloadRequest = iconDownloadRequest, + ) } coroutineContext.ensureActive() val newState = apps.checkAndUpdateApp(appMetadata.packageName) { diff --git a/next/src/main/kotlin/org/fdroid/install/InstallState.kt b/next/src/main/kotlin/org/fdroid/install/InstallState.kt index 09790b905..da5470794 100644 --- a/next/src/main/kotlin/org/fdroid/install/InstallState.kt +++ b/next/src/main/kotlin/org/fdroid/install/InstallState.kt @@ -79,7 +79,24 @@ sealed class InstallState(val showProgress: Boolean) { ) : InstallStateWithInfo(false) data object UserAborted : InstallState(false) - data class Error(val msg: String?) : InstallState(false) + + data class Error( + val msg: String?, + override val name: String, + override val versionName: String, + override val currentVersionName: String?, + override val lastUpdated: Long, + override val iconDownloadRequest: DownloadRequest?, + ) : InstallStateWithInfo(false) { + constructor(msg: String?, s: InstallStateWithInfo) : this( + msg = msg, + name = s.name, + versionName = s.versionName, + currentVersionName = s.currentVersionName, + lastUpdated = s.lastUpdated, + iconDownloadRequest = s.iconDownloadRequest, + ) + } data object Uninstalled : InstallState(false) } diff --git a/next/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt b/next/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt index a8d346755..2e1332971 100644 --- a/next/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt +++ b/next/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt @@ -195,7 +195,8 @@ class SessionInstallManager @Inject constructor( } } catch (e: Exception) { log.error(e) { "Error when creating session: " } - cont.resume(InstallState.Error("${e::class.java.simpleName} ${e.message}")) + val s = InstallState.Error("${e::class.java.simpleName} ${e.message}", state) + cont.resume(s) return@suspendCancellableCoroutine } // set-up receiver for install result @@ -248,7 +249,7 @@ class SessionInstallManager @Inject constructor( ) cont.resume(newState) } else { - cont.resume(InstallState.Error(msg)) + cont.resume(InstallState.Error(msg, state)) } } } @@ -284,7 +285,7 @@ class SessionInstallManager @Inject constructor( } } catch (e: Exception) { log.error(e) { "Error during install session: " } - cont.resume(InstallState.Error("${e::class.java.simpleName} ${e.message}")) + cont.resume(InstallState.Error("${e::class.java.simpleName} ${e.message}", state)) } } @@ -311,7 +312,7 @@ class SessionInstallManager @Inject constructor( if (status == PackageInstaller.STATUS_FAILURE_ABORTED) { cont.resume(InstallState.UserAborted) } else { - cont.resume(InstallState.Error(msg)) + cont.resume(InstallState.Error(msg, installState)) } } } diff --git a/next/src/main/kotlin/org/fdroid/ui/apps/InstallingAppRow.kt b/next/src/main/kotlin/org/fdroid/ui/apps/InstallingAppRow.kt index 305dc5ad2..7324beb7b 100644 --- a/next/src/main/kotlin/org/fdroid/ui/apps/InstallingAppRow.kt +++ b/next/src/main/kotlin/org/fdroid/ui/apps/InstallingAppRow.kt @@ -1,9 +1,11 @@ package org.fdroid.ui.apps import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.ErrorOutline import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.ListItem @@ -56,6 +58,15 @@ fun InstallingAppRow( imageVector = Icons.Default.CheckCircle, contentDescription = stringResource(R.string.app_installed), tint = MaterialTheme.colorScheme.secondary, + modifier = Modifier.padding(8.dp) + ) + } else if (app.installState is InstallState.Error) { + val desc = stringResource(R.string.notification_title_summary_install_error) + Icon( + imageVector = Icons.Default.ErrorOutline, + contentDescription = desc, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(8.dp) ) } else { if (app.installState is InstallState.Downloading) { @@ -103,10 +114,22 @@ private fun Preview() { iconDownloadRequest = null, ) ) + val installingApp3 = InstallingAppItem( + packageName = "A3", + installState = InstallState.Error( + msg = "error msg", + name = "Installing App 2", + versionName = "0.0.4", + currentVersionName = null, + lastUpdated = 13, + iconDownloadRequest = null, + ) + ) FDroidContent { Column { InstallingAppRow(installingApp1, false) InstallingAppRow(installingApp2, true) + InstallingAppRow(installingApp3, false) } } } diff --git a/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt b/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt index 38d6ba881..368d3f633 100644 --- a/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt +++ b/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt @@ -14,41 +14,39 @@ import java.util.Locale @Composable fun MyAppsPresenter( - appInstallStatesFlow: StateFlow>, appUpdatesFlow: StateFlow?>, + appInstallStatesFlow: StateFlow>, installedAppsFlow: StateFlow?>, searchQueryFlow: StateFlow, sortOrderFlow: StateFlow, ): MyAppsModel { - val appInstallStates = appInstallStatesFlow.collectAsState().value val appUpdates = appUpdatesFlow.collectAsState().value + val appInstallStates = appInstallStatesFlow.collectAsState().value val installedApps = installedAppsFlow.collectAsState().value val searchQuery = searchQueryFlow.collectAsState().value.normalize() val sortOrder = sortOrderFlow.collectAsState().value val processedPackageNames = mutableSetOf() - // TODO process updates first + val updates = appUpdates?.filter { + val keep = searchQuery.isBlank() || + it.name.normalize().contains(searchQuery, ignoreCase = true) + if (keep) processedPackageNames.add(it.packageName) + keep + } val installingApps = appInstallStates.mapNotNull { (packageName, state) -> if (state is InstallStateWithInfo) { + val keep = if (searchQuery.isBlank()) { + packageName !in processedPackageNames + } else { + packageName !in processedPackageNames && + state.name.normalize().contains(searchQuery, ignoreCase = true) + } processedPackageNames.add(packageName) - InstallingAppItem(packageName, state) + if (keep) InstallingAppItem(packageName, state) else null } else { null } } - val installing = if (searchQuery.isBlank()) installingApps else installingApps.filter { - it.name.normalize().contains(searchQuery, ignoreCase = true) - } - 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) - } - processedPackageNames.add(it.packageName) - keep - } val installed = installedApps?.filter { if (searchQuery.isBlank()) { it.packageName !in processedPackageNames @@ -58,7 +56,7 @@ fun MyAppsPresenter( } } return MyAppsModel( - installingApps = installing.sort(sortOrder), + installingApps = installingApps.sort(sortOrder), appUpdates = updates?.sort(sortOrder), installedApps = installed?.sort(sortOrder), sortOrder = sortOrder, diff --git a/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt b/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt index fe3febac7..9b2a774b5 100644 --- a/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt +++ b/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt @@ -68,8 +68,8 @@ class MyAppsViewModel @Inject constructor( private val sortOrder = savedStateHandle.getMutableStateFlow("sort", AppListSortOrder.NAME) val myAppsModel: StateFlow = moleculeScope.launchMolecule(mode = ContextClock) { MyAppsPresenter( - appInstallStatesFlow = appInstallManager.appInstallStates, appUpdatesFlow = updates, + appInstallStatesFlow = appInstallManager.appInstallStates, installedAppsFlow = installedApps, searchQueryFlow = searchQuery, sortOrderFlow = sortOrder, diff --git a/next/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt b/next/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt index f4dd75735..bf4cf3fbb 100644 --- a/next/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt +++ b/next/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt @@ -22,6 +22,7 @@ import androidx.compose.material.icons.filled.Mail import androidx.compose.material.icons.filled.OndemandVideo import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Translate +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard @@ -29,8 +30,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults @@ -45,11 +44,11 @@ import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.fromHtml import androidx.compose.ui.text.style.TextAlign.Companion.Center import androidx.compose.ui.text.style.TextOverflow @@ -79,9 +78,8 @@ fun AppDetails( onBackNav: (() -> Unit)?, modifier: Modifier = Modifier, ) { - val res = LocalResources.current val topAppBarState = rememberTopAppBarState() - val snackbarHostState = remember { SnackbarHostState() } + var showInstallError by remember { mutableStateOf(false) } val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarState) if (item == null) BigLoadingIndicator() else Scaffold( @@ -89,7 +87,6 @@ fun AppDetails( topBar = { AppDetailsTopAppBar(item, topAppBarState, scrollBehavior, onBackNav) }, - snackbarHost = { SnackbarHost(snackbarHostState) }, ) { innerPadding -> // react to install state changes LaunchedEffect(item.installState) { @@ -98,8 +95,7 @@ fun AppDetails( Log.i("AppDetails", "Requesting user confirmation... $state") item.actions.requestUserConfirmation(item.app.packageName, state) } else if (state is InstallState.Error) { - val msg = res.getString(R.string.install_error_notify_title, state.msg ?: "") - snackbarHostState.showSnackbar(msg) + showInstallError = true } } val scrollState = rememberScrollState() @@ -376,6 +372,36 @@ fun AppDetails( } } } + if (showInstallError && item != null && item.installState is InstallState.Error) AlertDialog( + onDismissRequest = { showInstallError = false }, + containerColor = MaterialTheme.colorScheme.errorContainer, + title = { + Text(stringResource(R.string.install_error_notify_title, item.name)) + }, + text = { + if (item.installState.msg == null) { + Text(stringResource(R.string.app_details_install_error_text)) + } else { + ExpandableSection( + icon = null, + title = stringResource(R.string.app_details_install_error_text) + ) { + SelectionContainer { + Text( + text = item.installState.msg, + fontFamily = FontFamily.Monospace, + modifier = Modifier.padding(top = 8.dp), + ) + } + } + } + }, + confirmButton = { + TextButton(onClick = { showInstallError = false }) { + Text(stringResource(R.string.ok)) + } + }, + ) } @Preview diff --git a/next/src/main/res/values/strings-next.xml b/next/src/main/res/values/strings-next.xml index 5e98f9d6a..f227e4508 100644 --- a/next/src/main/res/values/strings-next.xml +++ b/next/src/main/res/values/strings-next.xml @@ -65,6 +65,7 @@ Architectures: %1$s Incompatible • Installation likely to fail Signature mismatch, can\'t install + Sorry! There was an unexpected error installing this app. Notifications Opens system notification settings