mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-26 14:09:58 +00:00
Add ability to cancel a link+sync.
This commit is contained in:
committed by
Greyson Parrelli
parent
d473ff6e86
commit
fe5de65273
@@ -75,6 +75,7 @@ class EditDeviceNameFragment : ComposeFragment() {
|
||||
is LinkDeviceSettingsState.OneTimeEvent.ToastLinked -> Unit
|
||||
LinkDeviceSettingsState.OneTimeEvent.ToastNetworkFailed -> Unit
|
||||
is LinkDeviceSettingsState.OneTimeEvent.ToastUnlinked -> Unit
|
||||
LinkDeviceSettingsState.OneTimeEvent.SnackbarLinkCancelled -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Dialogs
|
||||
import org.signal.core.ui.Dividers
|
||||
@@ -132,7 +133,7 @@ class LinkDeviceFragment : ComposeFragment() {
|
||||
Log.i(TAG, "Releasing wake lock for linked device")
|
||||
linkDeviceWakeLock.release()
|
||||
}
|
||||
DialogState.SyncingMessages, DialogState.Linking -> {
|
||||
is DialogState.SyncingMessages, DialogState.Linking -> {
|
||||
Log.i(TAG, "Acquiring wake lock for linked device")
|
||||
linkDeviceWakeLock.acquire()
|
||||
}
|
||||
@@ -151,6 +152,9 @@ class LinkDeviceFragment : ComposeFragment() {
|
||||
is LinkDeviceSettingsState.OneTimeEvent.ToastUnlinked -> {
|
||||
Toast.makeText(context, context.getString(R.string.LinkDeviceFragment__s_unlinked, event.name), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
LinkDeviceSettingsState.OneTimeEvent.SnackbarLinkCancelled -> {
|
||||
Snackbar.make(requireView(), context.getString(R.string.LinkDeviceFragment__linking_cancelled), Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
LinkDeviceSettingsState.OneTimeEvent.ToastNetworkFailed -> {
|
||||
Toast.makeText(context, context.getString(R.string.DeviceListActivity_network_failed), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
@@ -198,6 +202,7 @@ class LinkDeviceFragment : ComposeFragment() {
|
||||
onDeviceRemovalConfirmed = { device -> viewModel.removeDevice(device) },
|
||||
onSyncFailureRetryRequested = { viewModel.onSyncErrorRetryRequested() },
|
||||
onSyncFailureIgnored = { viewModel.onSyncErrorIgnored() },
|
||||
onSyncCancelled = { viewModel.onSyncCancelled() },
|
||||
onEditDevice = { device ->
|
||||
viewModel.setDeviceToEdit(device)
|
||||
navController.safeNavigate(R.id.action_linkDeviceFragment_to_editDeviceNameFragment)
|
||||
@@ -251,6 +256,7 @@ fun DeviceListScreen(
|
||||
onDeviceRemovalConfirmed: (Device) -> Unit = {},
|
||||
onSyncFailureRetryRequested: () -> Unit = {},
|
||||
onSyncFailureIgnored: () -> Unit = {},
|
||||
onSyncCancelled: () -> Unit = {},
|
||||
onEditDevice: (Device) -> Unit = {}
|
||||
) {
|
||||
// If a bottom sheet is showing, we don't want the spinner underneath
|
||||
@@ -265,8 +271,13 @@ fun DeviceListScreen(
|
||||
DialogState.Unlinking -> {
|
||||
Dialogs.IndeterminateProgressDialog(stringResource(id = R.string.DeviceListActivity_unlinking_device))
|
||||
}
|
||||
DialogState.SyncingMessages -> {
|
||||
Dialogs.IndeterminateProgressDialog(stringResource(id = R.string.LinkDeviceFragment__syncing_messages))
|
||||
is DialogState.SyncingMessages -> {
|
||||
Dialogs.IndeterminateProgressDialog(
|
||||
message = stringResource(id = R.string.LinkDeviceFragment__syncing_messages),
|
||||
caption = stringResource(id = R.string.LinkDeviceFragment__do_not_close),
|
||||
dismiss = stringResource(id = android.R.string.cancel),
|
||||
onDismiss = onSyncCancelled
|
||||
)
|
||||
}
|
||||
is DialogState.SyncingFailed,
|
||||
DialogState.SyncingTimedOut -> {
|
||||
@@ -507,7 +518,9 @@ private fun DeviceListScreenPreview() {
|
||||
devices = listOf(
|
||||
Device(1, "Sam's Macbook Pro", 1715793982000, 1716053182000),
|
||||
Device(1, "Sam's iPad", 1715793182000, 1716053122000)
|
||||
)
|
||||
),
|
||||
seenQrEducationSheet = true,
|
||||
seenBioAuthEducationSheet = true
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -519,7 +532,9 @@ private fun DeviceListScreenLoadingPreview() {
|
||||
Previews.Preview {
|
||||
DeviceListScreen(
|
||||
state = LinkDeviceSettingsState(
|
||||
deviceListLoading = true
|
||||
deviceListLoading = true,
|
||||
seenQrEducationSheet = true,
|
||||
seenBioAuthEducationSheet = true
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -531,7 +546,9 @@ private fun DeviceListScreenLinkingPreview() {
|
||||
Previews.Preview {
|
||||
DeviceListScreen(
|
||||
state = LinkDeviceSettingsState(
|
||||
dialogState = DialogState.Linking
|
||||
dialogState = DialogState.Linking,
|
||||
seenQrEducationSheet = true,
|
||||
seenBioAuthEducationSheet = true
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -543,7 +560,9 @@ private fun DeviceListScreenUnlinkingPreview() {
|
||||
Previews.Preview {
|
||||
DeviceListScreen(
|
||||
state = LinkDeviceSettingsState(
|
||||
dialogState = DialogState.Unlinking
|
||||
dialogState = DialogState.Unlinking,
|
||||
seenQrEducationSheet = true,
|
||||
seenBioAuthEducationSheet = true
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -555,7 +574,9 @@ private fun DeviceListScreenSyncingMessagesPreview() {
|
||||
Previews.Preview {
|
||||
DeviceListScreen(
|
||||
state = LinkDeviceSettingsState(
|
||||
dialogState = DialogState.SyncingMessages
|
||||
dialogState = DialogState.SyncingMessages(1, 1),
|
||||
seenQrEducationSheet = true,
|
||||
seenBioAuthEducationSheet = true
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -567,7 +588,9 @@ private fun DeviceListScreenSyncingFailedPreview() {
|
||||
Previews.Preview {
|
||||
DeviceListScreen(
|
||||
state = LinkDeviceSettingsState(
|
||||
dialogState = DialogState.SyncingTimedOut
|
||||
dialogState = DialogState.SyncingTimedOut,
|
||||
seenQrEducationSheet = true,
|
||||
seenBioAuthEducationSheet = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -241,7 +241,7 @@ object LinkDeviceRepository {
|
||||
/**
|
||||
* Performs the entire process of creating and uploading an archive for a newly-linked device.
|
||||
*/
|
||||
fun createAndUploadArchive(ephemeralMessageBackupKey: MessageBackupKey, deviceId: Int, deviceCreatedAt: Long): LinkUploadArchiveResult {
|
||||
fun createAndUploadArchive(ephemeralMessageBackupKey: MessageBackupKey, deviceId: Int, deviceCreatedAt: Long, cancellationSignal: () -> Boolean): LinkUploadArchiveResult {
|
||||
Log.d(TAG, "[createAndUploadArchive] Beginning process.")
|
||||
val stopwatch = Stopwatch("link-archive")
|
||||
val tempBackupFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
|
||||
@@ -249,7 +249,13 @@ object LinkDeviceRepository {
|
||||
|
||||
try {
|
||||
Log.d(TAG, "[createAndUploadArchive] Starting the export.")
|
||||
BackupRepository.export(outputStream = outputStream, append = { tempBackupFile.appendBytes(it) }, messageBackupKey = ephemeralMessageBackupKey, mediaBackupEnabled = false)
|
||||
BackupRepository.export(
|
||||
outputStream = outputStream,
|
||||
append = { tempBackupFile.appendBytes(it) },
|
||||
messageBackupKey = ephemeralMessageBackupKey,
|
||||
mediaBackupEnabled = false,
|
||||
cancellationSignal = cancellationSignal
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "[createAndUploadArchive] Failed to export a backup!", e)
|
||||
return LinkUploadArchiveResult.BackupCreationFailure(e)
|
||||
@@ -257,6 +263,11 @@ object LinkDeviceRepository {
|
||||
Log.d(TAG, "[createAndUploadArchive] Successfully created backup.")
|
||||
stopwatch.split("create-backup")
|
||||
|
||||
if (cancellationSignal()) {
|
||||
Log.i(TAG, "[createAndUploadArchive] Backup was cancelled.")
|
||||
return LinkUploadArchiveResult.BackupCreationCancelled
|
||||
}
|
||||
|
||||
when (val result = ArchiveValidator.validate(tempBackupFile, ephemeralMessageBackupKey)) {
|
||||
ArchiveValidator.ValidationResult.Success -> {
|
||||
Log.d(TAG, "[createAndUploadArchive] Successfully passed validation.")
|
||||
@@ -272,6 +283,11 @@ object LinkDeviceRepository {
|
||||
}
|
||||
stopwatch.split("validate-backup")
|
||||
|
||||
if (cancellationSignal()) {
|
||||
Log.i(TAG, "[createAndUploadArchive] Backup was cancelled.")
|
||||
return LinkUploadArchiveResult.BackupCreationCancelled
|
||||
}
|
||||
|
||||
Log.d(TAG, "[createAndUploadArchive] Fetching an upload form...")
|
||||
val uploadForm = when (val result = NetworkResult.withRetry { SignalNetwork.attachments.getAttachmentV4UploadForm() }) {
|
||||
is NetworkResult.Success -> result.result.logD(TAG, "[createAndUploadArchive] Successfully retrieved upload form.")
|
||||
@@ -280,6 +296,11 @@ object LinkDeviceRepository {
|
||||
is NetworkResult.StatusCodeError -> return LinkUploadArchiveResult.NetworkError(result.exception).logW(TAG, "[createAndUploadArchive] Status code error when fetching form.", result.exception)
|
||||
}
|
||||
|
||||
if (cancellationSignal()) {
|
||||
Log.i(TAG, "[createAndUploadArchive] Backup was cancelled.")
|
||||
return LinkUploadArchiveResult.BackupCreationCancelled
|
||||
}
|
||||
|
||||
when (val result = uploadArchive(tempBackupFile, uploadForm)) {
|
||||
is NetworkResult.Success -> Log.i(TAG, "[createAndUploadArchive] Successfully uploaded backup.")
|
||||
is NetworkResult.NetworkError -> return LinkUploadArchiveResult.NetworkError(result.exception).logW(TAG, "[createAndUploadArchive] Network error when uploading archive.", result.exception)
|
||||
@@ -288,6 +309,11 @@ object LinkDeviceRepository {
|
||||
}
|
||||
stopwatch.split("upload-backup")
|
||||
|
||||
if (cancellationSignal()) {
|
||||
Log.i(TAG, "[createAndUploadArchive] Backup was cancelled.")
|
||||
return LinkUploadArchiveResult.BackupCreationCancelled
|
||||
}
|
||||
|
||||
Log.d(TAG, "[createAndUploadArchive] Setting the transfer archive...")
|
||||
val transferSetResult = NetworkResult.withRetry {
|
||||
SignalNetwork.linkDevice.setTransferArchive(
|
||||
@@ -399,6 +425,7 @@ object LinkDeviceRepository {
|
||||
|
||||
sealed interface LinkUploadArchiveResult {
|
||||
data object Success : LinkUploadArchiveResult
|
||||
data object BackupCreationCancelled : LinkUploadArchiveResult
|
||||
data class BackupCreationFailure(val exception: Exception) : LinkUploadArchiveResult
|
||||
data class BadRequest(val exception: IOException) : LinkUploadArchiveResult
|
||||
data class NetworkError(val exception: IOException) : LinkUploadArchiveResult
|
||||
|
||||
@@ -21,13 +21,14 @@ data class LinkDeviceSettingsState(
|
||||
val seenBioAuthEducationSheet: Boolean = false,
|
||||
val needsBioAuthEducationSheet: Boolean = !seenBioAuthEducationSheet && !SignalStore.uiHints.hasSeenLinkDeviceAuthSheet() && !SignalStore.account.hasLinkedDevices,
|
||||
val bottomSheetVisible: Boolean = false,
|
||||
val deviceToEdit: Device? = null
|
||||
val deviceToEdit: Device? = null,
|
||||
val shouldCancelArchiveUpload: Boolean = false
|
||||
) {
|
||||
sealed interface DialogState {
|
||||
data object None : DialogState
|
||||
data object Linking : DialogState
|
||||
data object Unlinking : DialogState
|
||||
data object SyncingMessages : DialogState
|
||||
data class SyncingMessages(val deviceId: Int, val deviceCreatedAt: Long) : DialogState
|
||||
data object SyncingTimedOut : DialogState
|
||||
data class SyncingFailed(val deviceId: Int, val deviceCreatedAt: Long) : DialogState
|
||||
}
|
||||
@@ -37,6 +38,7 @@ data class LinkDeviceSettingsState(
|
||||
data object ToastNetworkFailed : OneTimeEvent
|
||||
data class ToastUnlinked(val name: String) : OneTimeEvent
|
||||
data class ToastLinked(val name: String) : OneTimeEvent
|
||||
data object SnackbarLinkCancelled : OneTimeEvent
|
||||
data object SnackbarNameChangeSuccess : OneTimeEvent
|
||||
data object SnackbarNameChangeFailure : OneTimeEvent
|
||||
data object ShowFinishedSheet : OneTimeEvent
|
||||
|
||||
@@ -150,7 +150,8 @@ class LinkDeviceViewModel : ViewModel() {
|
||||
it.copy(
|
||||
qrCodeState = QrCodeState.NONE,
|
||||
linkUri = null,
|
||||
dialogState = DialogState.Linking
|
||||
dialogState = DialogState.Linking,
|
||||
shouldCancelArchiveUpload = false
|
||||
)
|
||||
}
|
||||
|
||||
@@ -247,12 +248,17 @@ class LinkDeviceViewModel : ViewModel() {
|
||||
_state.update {
|
||||
it.copy(
|
||||
linkDeviceResult = result,
|
||||
dialogState = DialogState.SyncingMessages
|
||||
dialogState = DialogState.SyncingMessages(waitResult.id, waitResult.created)
|
||||
)
|
||||
}
|
||||
|
||||
Log.d(TAG, "[addDeviceWithSync] Beginning the archive generation process...")
|
||||
val uploadResult = LinkDeviceRepository.createAndUploadArchive(ephemeralMessageBackupKey, waitResult.id, waitResult.created)
|
||||
val uploadResult = LinkDeviceRepository.createAndUploadArchive(
|
||||
ephemeralMessageBackupKey = ephemeralMessageBackupKey,
|
||||
deviceId = waitResult.id,
|
||||
deviceCreatedAt = waitResult.created,
|
||||
cancellationSignal = { _state.value.shouldCancelArchiveUpload }
|
||||
)
|
||||
|
||||
Log.d(TAG, "[addDeviceWithSync] Archive finished with result: $uploadResult")
|
||||
when (uploadResult) {
|
||||
@@ -276,6 +282,14 @@ class LinkDeviceViewModel : ViewModel() {
|
||||
)
|
||||
}
|
||||
}
|
||||
LinkDeviceRepository.LinkUploadArchiveResult.BackupCreationCancelled -> {
|
||||
Log.i(TAG, "[addDeviceWithoutSync] Cancelling archive upload")
|
||||
_state.update {
|
||||
it.copy(
|
||||
dialogState = DialogState.None
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,6 +377,26 @@ class LinkDeviceViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun onSyncCancelled() = viewModelScope.launch(Dispatchers.IO) {
|
||||
Log.i(TAG, "Cancelling sync and removing linked device")
|
||||
val dialogState = _state.value.dialogState
|
||||
if (dialogState is DialogState.SyncingMessages) {
|
||||
val success = LinkDeviceRepository.removeDevice(dialogState.deviceId)
|
||||
if (success) {
|
||||
Log.i(TAG, "Removing device after cancelling sync")
|
||||
_state.update {
|
||||
it.copy(
|
||||
oneTimeEvent = OneTimeEvent.SnackbarLinkCancelled,
|
||||
dialogState = DialogState.None,
|
||||
shouldCancelArchiveUpload = true
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Unable to remove device after cancelling sync")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setDeviceToEdit(device: Device) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
|
||||
Reference in New Issue
Block a user