mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-04-29 03:06:57 -04:00
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.
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,41 +14,39 @@ import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun MyAppsPresenter(
|
||||
appInstallStatesFlow: StateFlow<Map<String, InstallState>>,
|
||||
appUpdatesFlow: StateFlow<List<AppUpdateItem>?>,
|
||||
appInstallStatesFlow: StateFlow<Map<String, InstallState>>,
|
||||
installedAppsFlow: StateFlow<List<InstalledAppItem>?>,
|
||||
searchQueryFlow: StateFlow<String>,
|
||||
sortOrderFlow: StateFlow<AppListSortOrder>,
|
||||
): 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<String>()
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -68,8 +68,8 @@ class MyAppsViewModel @Inject constructor(
|
||||
private val sortOrder = savedStateHandle.getMutableStateFlow("sort", AppListSortOrder.NAME)
|
||||
val myAppsModel: StateFlow<MyAppsModel> = moleculeScope.launchMolecule(mode = ContextClock) {
|
||||
MyAppsPresenter(
|
||||
appInstallStatesFlow = appInstallManager.appInstallStates,
|
||||
appUpdatesFlow = updates,
|
||||
appInstallStatesFlow = appInstallManager.appInstallStates,
|
||||
installedAppsFlow = installedApps,
|
||||
searchQueryFlow = searchQuery,
|
||||
sortOrderFlow = sortOrder,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
<string name="architectures_colon">Architectures: %1$s</string>
|
||||
<string name="app_details_incompatible_version">Incompatible • Installation likely to fail</string>
|
||||
<string name="app_details_incompatible_signer">Signature mismatch, can\'t install</string>
|
||||
<string name="app_details_install_error_text">Sorry! There was an unexpected error installing this app.</string>
|
||||
|
||||
<string name="notification_title">Notifications</string>
|
||||
<string name="notification_summary">Opens system notification settings</string>
|
||||
|
||||
Reference in New Issue
Block a user