updates: fix stale UI states & cancel-all coverage

- AppUpdateItem now shows a disabled "Installing" button while the
  download is COMPLETED and awaiting install, instead of briefly
  reverting to "Update" between download-done and InstallerEvent.Installed.
- Section header replaces all-or-nothing "Update all" toggle with an
  any-active check, so "Cancel all" appears as soon as a single update
  is in flight. Same treatment for the ownership-approval section.
- Empty placeholder no longer hides the ignored-updates list when there
  are no live updates.
- DownloadHelper.cancelAll also flips COMPLETED -> CANCELLED so the
  Cancel-all button gives immediate feedback for pending-install rows.
- DownloadListItem: surface the failed icon for FAILED downloads too.
This commit is contained in:
Rahul Patel
2026-05-20 01:21:19 +05:30
parent 91fc4d714a
commit b84f3a3fa9
4 changed files with 47 additions and 13 deletions

View File

@@ -67,7 +67,10 @@ fun DownloadListItem(modifier: Modifier = Modifier, download: Download, onClick:
tint = Color(0xFF4CAF50)
)
}
} else if (download.status == DownloadStatus.CANCELLED) {
} else if (
download.status == DownloadStatus.CANCELLED ||
download.status == DownloadStatus.FAILED
) {
{
Icon(
painter = painterResource(R.drawable.ic_cancel),

View File

@@ -43,7 +43,8 @@ fun AppUpdateItem(
onCancel: () -> Unit = {},
onUnignore: (() -> Unit)? = null
) {
val inProgress = download != null && download.status !in DownloadStatus.finished
val inProgress = download != null && !download.isFinished
val pendingInstall = download?.status == DownloadStatus.COMPLETED
val progress = if (download?.status == DownloadStatus.DOWNLOADING) {
download.progress.toFloat()
} else {
@@ -64,7 +65,7 @@ fun AppUpdateItem(
AnimatedAppIcon(
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium)),
iconUrl = update.iconURL,
inProgress = inProgress,
inProgress = inProgress || pendingInstall,
progress = progress
)
}
@@ -99,6 +100,12 @@ fun AppUpdateItem(
}
}
pendingInstall -> {
OutlinedButton(onClick = {}, enabled = false) {
Text(stringResource(R.string.action_installing))
}
}
inProgress -> {
OutlinedButton(onClick = onCancel) {
Text(stringResource(R.string.action_cancel))

View File

@@ -29,6 +29,7 @@ import com.aurora.store.compose.composable.SectionHeader
import com.aurora.store.compose.composable.ShimmerUpdateItem
import com.aurora.store.compose.composable.app.AppUpdateItem
import com.aurora.store.compose.navigation.Destination
import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.room.download.Download
import com.aurora.store.data.room.update.Update
import com.aurora.store.viewmodel.all.UpdatesViewModel
@@ -72,8 +73,8 @@ fun UpdatesScreen(
}
val (mainEntries, approvalEntries, incompatibleEntries) = groupedUpdates
val mainAllEnqueued = mainEntries.isNotEmpty() &&
mainEntries.all { it.value?.isRunning == true }
val mainAnyActive = mainEntries.any { it.value.isActive() }
val approvalAnyActive = approvalEntries.any { it.value.isActive() }
PullToRefreshBox(
modifier = Modifier.fillMaxSize(),
@@ -92,7 +93,7 @@ fun UpdatesScreen(
}
}
updateMap.isEmpty() -> {
updateMap.isEmpty() && ignoredUpdates.isEmpty() -> {
Placeholder(
modifier = Modifier.fillMaxSize(),
painter = painterResource(R.drawable.ic_updates),
@@ -119,8 +120,8 @@ fun UpdatesScreen(
}
)
val actionLabel = stringResource(
if (mainAllEnqueued) {
R.string.action_cancel
if (mainAnyActive) {
R.string.download_cancel_all
} else {
R.string.action_update_all
}
@@ -130,7 +131,7 @@ fun UpdatesScreen(
trailing = {
TextButton(
onClick = {
if (mainAllEnqueued) {
if (mainAnyActive) {
onCancelAll()
} else {
onRequestUpdateAll(mainEntries.map { it.key })
@@ -151,15 +152,28 @@ fun UpdatesScreen(
if (approvalEntries.isNotEmpty()) {
item(key = "header_approval") {
val actionLabel = stringResource(
if (approvalAnyActive) {
R.string.download_cancel_all
} else {
R.string.action_update_all
}
)
SectionHeader(
title = stringResource(R.string.updates_approval_header),
subtitle = stringResource(R.string.updates_approval_desc),
trailing = {
TextButton(
onClick = {
onRequestUpdateAll(approvalEntries.map { it.key })
if (approvalAnyActive) {
onCancelAll()
} else {
onRequestUpdateAll(
approvalEntries.map { it.key }
)
}
) { Text(stringResource(R.string.action_update_all)) }
}
) { Text(actionLabel) }
}
)
}
@@ -212,6 +226,11 @@ fun UpdatesScreen(
}
}
private fun Download?.isActive(): Boolean {
if (this == null) return false
return !isFinished || status == DownloadStatus.COMPLETED
}
private fun LazyListScope.updateItems(
entries: List<Map.Entry<Update, Download?>>,
keyPrefix: String,

View File

@@ -165,9 +165,14 @@ class DownloadHelper @Inject constructor(
* @param updatesOnly Whether to cancel only updates, defaults to false
*/
suspend fun cancelAll(updatesOnly: Boolean = false) {
// Cancel all enqueued downloads first to avoid triggering re-download
// Cancel queued/completed downloads first to avoid triggering re-download and to give
// the user immediate feedback for items that already finished downloading. The actual
// OS-level install for COMPLETED entries is wrapped in NonCancellable inside the
// worker and cannot be aborted — if it succeeds, the update row is removed via
// InstallerEvent.Installed; if it fails, the user can re-trigger an update.
val cancellableStatuses = setOf(DownloadStatus.QUEUED, DownloadStatus.COMPLETED)
downloadDao.downloads().firstOrNull()
?.filter { it.status == DownloadStatus.QUEUED }
?.filter { it.status in cancellableStatuses }
?.filter { if (updatesOnly) it.isInstalled else true }
?.forEach {
downloadDao.updateStatus(it.packageName, DownloadStatus.CANCELLED)