Fix ANR when backup deletion hangs.

This commit is contained in:
Alex Hart
2026-02-27 13:37:09 -04:00
committed by Greyson Parrelli
parent 20d16a8433
commit bd4ce1788c
4 changed files with 69 additions and 6 deletions

View File

@@ -13,8 +13,10 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.LifecycleResumeEffect
@@ -29,6 +31,7 @@ import androidx.navigation3.ui.NavDisplay
import androidx.navigationevent.compose.LocalNavigationEventDispatcherOwner
import kotlinx.coroutines.launch
import org.signal.core.ui.compose.ComposeFragment
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Launchers
import org.signal.core.ui.util.StorageUtil
import org.signal.core.util.logging.Log
@@ -39,6 +42,7 @@ import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyRec
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyRecordScreen
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyVerifyScreen
import org.thoughtcrime.securesms.keyvalue.SignalStore
import kotlin.time.Duration.Companion.milliseconds
private val TAG = Log.tag(LocalBackupsFragment::class)
@@ -127,6 +131,7 @@ class LocalBackupsFragment : ComposeFragment() {
val state: LocalBackupsKeyState by viewModel.backupState.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
val backupKeyUpdatedMessage = stringResource(R.string.OnDeviceBackupsFragment__backup_key_updated)
var upgradeInProgress by remember { mutableStateOf(false) }
MessageBackupsKeyVerifyScreen(
backupKey = state.accountEntropyPool.displayValue,
@@ -139,7 +144,9 @@ class LocalBackupsFragment : ComposeFragment() {
backstack.removeAll { it != LocalBackupsNavKey.SETTINGS }
scope.launch {
upgradeInProgress = true
viewModel.handleUpgrade(requireContext())
upgradeInProgress = false
snackbarHostState.showSnackbar(
message = backupKeyUpdatedMessage
@@ -147,6 +154,12 @@ class LocalBackupsFragment : ComposeFragment() {
}
}
)
Dialogs.IndeterminateProgressDialog(
visible = upgradeInProgress,
delayDuration = 100.milliseconds,
minimumDisplayDuration = 500.milliseconds
)
}
else -> error("Unknown key: $key")

View File

@@ -168,13 +168,13 @@ class LocalBackupsViewModel : ViewModel(), BackupKeyCredentialManagerHandler {
withContext(Dispatchers.IO) {
AppDependencies.jobManager.cancelAllInQueue(LocalBackupJob.QUEUE)
AppDependencies.jobManager.flush()
SignalStore.backup.newLocalBackupsDirectory = SignalStore.settings.signalBackupDirectory?.toString()
BackupPassphrase.set(context, null)
SignalStore.settings.isBackupEnabled = false
BackupUtil.deleteAllBackups()
}
SignalStore.backup.newLocalBackupsDirectory = SignalStore.settings.signalBackupDirectory?.toString()
BackupPassphrase.set(context, null)
SignalStore.settings.isBackupEnabled = false
BackupUtil.deleteAllBackups()
}
SignalStore.backup.newLocalBackupsEnabled = true

View File

@@ -11,6 +11,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.documentfile.provider.DocumentFile;
import org.signal.core.util.Util;
@@ -110,6 +111,7 @@ public class BackupUtil {
return backups.isEmpty() ? null : backups.get(0);
}
@WorkerThread
public static void deleteAllBackups() {
Log.i(TAG, "Deleting all backups");

View File

@@ -36,7 +36,9 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -56,12 +58,15 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import kotlinx.coroutines.delay
import org.signal.core.ui.compose.Dialogs.AdvancedAlertDialog
import org.signal.core.ui.compose.Dialogs.PermissionRationaleDialog
import org.signal.core.ui.compose.Dialogs.SimpleAlertDialog
import org.signal.core.ui.compose.Dialogs.SimpleMessageDialog
import org.signal.core.ui.compose.theme.SignalTheme
import kotlin.math.max
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
object Dialogs {
@@ -225,6 +230,49 @@ object Dialogs {
}
}
/**
* A dialog that shows a spinner with built-in delay and minimum display time.
*
* The dialog will not appear until [delayDuration] has elapsed after [visible] becomes true.
* If the operation completes before the delay, the dialog is never shown.
* Once visible, the dialog will remain for at least [minimumDisplayDuration] to
* avoid a jarring flash.
*
* This composable should always be in the composition (not wrapped in an `if`).
* Visibility is controlled by the [visible] parameter.
*/
@Composable
fun IndeterminateProgressDialog(
visible: Boolean,
delayDuration: Duration = Duration.ZERO,
minimumDisplayDuration: Duration = Duration.ZERO,
onDismissRequest: () -> Unit = {}
) {
var isVisible by remember { mutableStateOf(false) }
var isVisibleSince by remember { mutableLongStateOf(0L) }
LaunchedEffect(visible) {
if (visible) {
delay(delayDuration)
isVisible = true
isVisibleSince = System.currentTimeMillis()
} else {
if (isVisible && minimumDisplayDuration > Duration.ZERO) {
val elapsed = (System.currentTimeMillis() - isVisibleSince).milliseconds
val remaining = minimumDisplayDuration - elapsed
if (remaining > Duration.ZERO) {
delay(remaining)
}
}
isVisible = false
}
}
if (isVisible) {
IndeterminateProgressDialog(onDismissRequest = onDismissRequest)
}
}
/**
* Customizable progress spinner that shows [message] below the spinner to let users know
* an action is completing