@@ -89,14 +89,9 @@ fun DiagnosticsScreen(onBack: () -> Unit) {
8989 val snackbar = remember { SnackbarHostState() }
9090 val undoLabel = stringResource(R.string.undo)
9191 val reportDeletedLabel = stringResource(R.string.report_deleted)
92− // Track report names whose deletion has been deferred for an undo window, so a
93− // swipe-triggered delete can be cancelled before the file is actually removed.
94− // rememberSaveable (not remember) so the list survives rotation: otherwise a
95− // rotation mid-undo-window resets it to empty, the deferred-delete coroutine is
96− // cancelled by dispose, and a report the user swiped to delete silently survives
97− // (or in edge cases is deleted without an undo path). Uses a SnapshotStateList
98− // (saveable) instead of a plain Set so it round-trips through the state registry.
99− val deferredDeletes = rememberSaveable { androidx.compose.runtime.mutableStateListOf<String>() }
92+ // Deferred report deletions are owned by the CrashLogger (its process-lived scope),
93+ // so they survive both rotation and navigating away from this screen. The screen only
94+ // shows the Undo snackbar and asks the logger to cancel a pending delete on undo.
10095 val shareLabel = stringResource(R.string.share)
10196 val shareSubject = stringResource(R.string.crash_report_share_subject)
10297 val timeTick = rememberRelativeTimeTick()
@@ -309,27 +304,27 @@ fun DiagnosticsScreen(onBack: () -> Unit) {
309304 pendingReportDelete = null
310305 viewing = null
311306 // Defer the actual delete so the Undo snackbar can cancel it. The
312− // report stays in the list until the window expires; the undo
313− // action removes the name from the deferred list.
314− deferredDeletes.add(name)
315− // The deferred delete is scheduled by a LaunchedEffect keyed on
316− // deferredDeletes (below), which survives rotation because
317− // deferredDeletes is rememberSaveable. Only the snackbar runs in
318− // shareScope here — if a rotation cancels it, the undo window is
319− // lost but the report is still deleted after the delay (the safer
320− // failure mode) rather than silently surviving.
307+ // deletion is owned by the CrashLogger's own scope, so it commits even
308+ // if the user navigates away during the undo window (the screen's
309+ // shareScope would otherwise cancel it, silently dropping the delete).
310+ logger.scheduleDelete(name, NetworkConfig.undoReportDeleteDelayMs)
321311 shareScope.launch {
312+ // Indefinite + a matching timed dismiss keeps the Undo button
313+ // visible for exactly the undo window and no longer: a fixed
314+ // SnackbarDuration.Long (~10s) outlasted the 5s window, leaving
315+ // the button on screen but dead for its second half.
316+ val dismisser = launch {
317+ delay(NetworkConfig.undoReportDeleteDelayMs)
318+ snackbar.currentSnackbarData?.dismiss()
319+ }
322320 val result = snackbar.showSnackbar(
323321 message = reportDeletedLabel,
324322 actionLabel = undoLabel,
325− // Long (~10s) outlasts the undoReportDeleteDelayMs window
326− // (5s) so the Undo button remains available for the whole
327− // undo period; Short (~1.5s) vanished before the delete
328− // fired, leaving no way to cancel during the last 3.5s.
329− duration = SnackbarDuration.Long,
323+ duration = SnackbarDuration.Indefinite,
330324 )
325+ dismisser.cancel()
331326 if (result == SnackbarResult.ActionPerformed) {
332− deferredDeletes.remove(name)
327+ logger.cancelScheduledDelete(name)
333328 }
334329 }
335330 }) { Text(stringResource(R.string.delete), color = MaterialTheme.colorScheme.error) }
@@ -340,26 +335,6 @@ fun DiagnosticsScreen(onBack: () -> Unit) {
340335 )
341336 }
342337
343− // Schedule the deferred delete for every entry in the deferred list. Keyed on the
344− // list so a rotation (which restores the list via rememberSaveable) re-schedules
345− // any pending deletes that were cancelled when shareScope was disposed. Each
346− // entry gets its own delay; undo removes the name from the list, which re-keys
347− // this effect and cancels the in-flight delays for remaining entries (they're
348− // re-launched, but the removed name is no longer present so its delete is
349− // skipped). collectLatest would cancel sibling deletes, so we launch per-name.
350− LaunchedEffect(deferredDeletes.toList()) {
351− val pending = deferredDeletes.toList()
352− pending.forEach { name ->
353− shareScope.launch {
354− delay(NetworkConfig.undoReportDeleteDelayMs)
355− if (name in deferredDeletes) {
356− deferredDeletes.remove(name)
357− logger.deleteReport(name)
358− }
359− }
360− }
361− }
362−
363338 if (showClearAll) {
364339 AlertDialog(
365340 onDismissRequest = { showClearAll = false },