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) tint = Color(0xFF4CAF50)
) )
} }
} else if (download.status == DownloadStatus.CANCELLED) { } else if (
download.status == DownloadStatus.CANCELLED ||
download.status == DownloadStatus.FAILED
) {
{ {
Icon( Icon(
painter = painterResource(R.drawable.ic_cancel), painter = painterResource(R.drawable.ic_cancel),

View File

@@ -43,7 +43,8 @@ fun AppUpdateItem(
onCancel: () -> Unit = {}, onCancel: () -> Unit = {},
onUnignore: (() -> Unit)? = null 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) { val progress = if (download?.status == DownloadStatus.DOWNLOADING) {
download.progress.toFloat() download.progress.toFloat()
} else { } else {
@@ -64,7 +65,7 @@ fun AppUpdateItem(
AnimatedAppIcon( AnimatedAppIcon(
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium)), modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium)),
iconUrl = update.iconURL, iconUrl = update.iconURL,
inProgress = inProgress, inProgress = inProgress || pendingInstall,
progress = progress progress = progress
) )
} }
@@ -99,6 +100,12 @@ fun AppUpdateItem(
} }
} }
pendingInstall -> {
OutlinedButton(onClick = {}, enabled = false) {
Text(stringResource(R.string.action_installing))
}
}
inProgress -> { inProgress -> {
OutlinedButton(onClick = onCancel) { OutlinedButton(onClick = onCancel) {
Text(stringResource(R.string.action_cancel)) 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.ShimmerUpdateItem
import com.aurora.store.compose.composable.app.AppUpdateItem import com.aurora.store.compose.composable.app.AppUpdateItem
import com.aurora.store.compose.navigation.Destination 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.download.Download
import com.aurora.store.data.room.update.Update import com.aurora.store.data.room.update.Update
import com.aurora.store.viewmodel.all.UpdatesViewModel import com.aurora.store.viewmodel.all.UpdatesViewModel
@@ -72,8 +73,8 @@ fun UpdatesScreen(
} }
val (mainEntries, approvalEntries, incompatibleEntries) = groupedUpdates val (mainEntries, approvalEntries, incompatibleEntries) = groupedUpdates
val mainAllEnqueued = mainEntries.isNotEmpty() && val mainAnyActive = mainEntries.any { it.value.isActive() }
mainEntries.all { it.value?.isRunning == true } val approvalAnyActive = approvalEntries.any { it.value.isActive() }
PullToRefreshBox( PullToRefreshBox(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@@ -92,7 +93,7 @@ fun UpdatesScreen(
} }
} }
updateMap.isEmpty() -> { updateMap.isEmpty() && ignoredUpdates.isEmpty() -> {
Placeholder( Placeholder(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
painter = painterResource(R.drawable.ic_updates), painter = painterResource(R.drawable.ic_updates),
@@ -119,8 +120,8 @@ fun UpdatesScreen(
} }
) )
val actionLabel = stringResource( val actionLabel = stringResource(
if (mainAllEnqueued) { if (mainAnyActive) {
R.string.action_cancel R.string.download_cancel_all
} else { } else {
R.string.action_update_all R.string.action_update_all
} }
@@ -130,7 +131,7 @@ fun UpdatesScreen(
trailing = { trailing = {
TextButton( TextButton(
onClick = { onClick = {
if (mainAllEnqueued) { if (mainAnyActive) {
onCancelAll() onCancelAll()
} else { } else {
onRequestUpdateAll(mainEntries.map { it.key }) onRequestUpdateAll(mainEntries.map { it.key })
@@ -151,15 +152,28 @@ fun UpdatesScreen(
if (approvalEntries.isNotEmpty()) { if (approvalEntries.isNotEmpty()) {
item(key = "header_approval") { item(key = "header_approval") {
val actionLabel = stringResource(
if (approvalAnyActive) {
R.string.download_cancel_all
} else {
R.string.action_update_all
}
)
SectionHeader( SectionHeader(
title = stringResource(R.string.updates_approval_header), title = stringResource(R.string.updates_approval_header),
subtitle = stringResource(R.string.updates_approval_desc), subtitle = stringResource(R.string.updates_approval_desc),
trailing = { trailing = {
TextButton( TextButton(
onClick = { 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( private fun LazyListScope.updateItems(
entries: List<Map.Entry<Update, Download?>>, entries: List<Map.Entry<Update, Download?>>,
keyPrefix: String, keyPrefix: String,

View File

@@ -165,9 +165,14 @@ class DownloadHelper @Inject constructor(
* @param updatesOnly Whether to cancel only updates, defaults to false * @param updatesOnly Whether to cancel only updates, defaults to false
*/ */
suspend fun cancelAll(updatesOnly: Boolean = 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() downloadDao.downloads().firstOrNull()
?.filter { it.status == DownloadStatus.QUEUED } ?.filter { it.status in cancellableStatuses }
?.filter { if (updatesOnly) it.isInstalled else true } ?.filter { if (updatesOnly) it.isInstalled else true }
?.forEach { ?.forEach {
downloadDao.updateStatus(it.packageName, DownloadStatus.CANCELLED) downloadDao.updateStatus(it.packageName, DownloadStatus.CANCELLED)