diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsFragment.kt index fe2d224b62..613bf7fb71 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsFragment.kt @@ -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") diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsViewModel.kt index ab397dba69..f2ce414bca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsViewModel.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.java index 5627b6c9eb..79514c818d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.java @@ -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"); diff --git a/core/ui/src/main/java/org/signal/core/ui/compose/Dialogs.kt b/core/ui/src/main/java/org/signal/core/ui/compose/Dialogs.kt index d653be1feb..9233a70a75 100644 --- a/core/ui/src/main/java/org/signal/core/ui/compose/Dialogs.kt +++ b/core/ui/src/main/java/org/signal/core/ui/compose/Dialogs.kt @@ -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