From df5ef06109f47fe88018df326f921fff6e7922c4 Mon Sep 17 00:00:00 2001 From: Michelle Tang Date: Thu, 5 Dec 2024 18:33:12 -0500 Subject: [PATCH] Add link+sync error flows. --- .../linkdevice/LinkDeviceFragment.kt | 15 ++++------- .../linkdevice/LinkDeviceRepository.kt | 19 +++++++++++++ .../linkdevice/LinkDeviceSettingsState.kt | 2 +- .../linkdevice/LinkDeviceViewModel.kt | 27 ++++++++++++++----- .../main/java/org/signal/core/ui/Dialogs.kt | 11 ++++++-- .../signalservice/api/link/LinkDeviceApi.kt | 26 +++++++++++++++++- .../SetLinkedDeviceTransferArchiveRequest.kt | 15 +++++++---- .../api/link/TransferArchiveError.kt | 14 ++++++++++ 8 files changed, 104 insertions(+), 25 deletions(-) create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/link/TransferArchiveError.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceFragment.kt index 6876518e13..e4d857f941 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceFragment.kt @@ -179,7 +179,7 @@ class LinkDeviceFragment : ComposeFragment() { onLinkNewDeviceClicked = { navController.navigateToQrScannerIfAuthed() }, onDeviceSelectedForRemoval = { device -> viewModel.setDeviceToRemove(device) }, onDeviceRemovalConfirmed = { device -> viewModel.removeDevice(device) }, - onSyncFailureRetryRequested = { deviceId -> viewModel.onSyncErrorRetryRequested(deviceId) }, + onSyncFailureRetryRequested = { viewModel.onSyncErrorRetryRequested() }, onSyncFailureIgnored = { viewModel.onSyncErrorIgnored() }, onEditDevice = { device -> viewModel.setDeviceToEdit(device) @@ -228,7 +228,7 @@ fun DeviceListScreen( onLinkNewDeviceClicked: () -> Unit = {}, onDeviceSelectedForRemoval: (Device?) -> Unit = {}, onDeviceRemovalConfirmed: (Device) -> Unit = {}, - onSyncFailureRetryRequested: (Int?) -> Unit = {}, + onSyncFailureRetryRequested: () -> Unit = {}, onSyncFailureIgnored: () -> Unit = {}, onEditDevice: (Device) -> Unit = {} ) { @@ -253,15 +253,10 @@ fun DeviceListScreen( title = stringResource(R.string.LinkDeviceFragment__sync_failure_title), body = stringResource(R.string.LinkDeviceFragment__sync_failure_body), confirm = stringResource(R.string.LinkDeviceFragment__sync_failure_retry_button), - onConfirm = { - if (state.dialogState is DialogState.SyncingFailed) { - onSyncFailureRetryRequested(state.dialogState.deviceId) - } else { - onSyncFailureRetryRequested(null) - } - }, + onConfirm = onSyncFailureRetryRequested, dismiss = stringResource(R.string.LinkDeviceFragment__sync_failure_dismiss_button), - onDismiss = onSyncFailureIgnored + onDismissRequest = onSyncFailureIgnored, + onDeny = onSyncFailureIgnored ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceRepository.kt index ab381a3a7d..125f997e84 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceRepository.kt @@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.registration.secondary.DeviceNameCipher import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.backup.MessageBackupKey import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse +import org.whispersystems.signalservice.api.link.TransferArchiveError import org.whispersystems.signalservice.api.link.WaitForLinkedDeviceResponse import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo import org.whispersystems.signalservice.api.push.SignalServiceAddress @@ -351,6 +352,24 @@ object LinkDeviceRepository { return NetworkResult.NetworkError(IOException("Hit max retries!")) } + /** + * If [createAndUploadArchive] fails to upload an archive, alert the linked device of the failure and if the user will try again + */ + fun sendTransferArchiveError(deviceId: Int, deviceCreatedAt: Long, error: TransferArchiveError) { + val archiveErrorResult = SignalNetwork.linkDevice.setTransferArchiveError( + destinationDeviceId = deviceId, + destinationDeviceCreated = deviceCreatedAt, + error = error + ) + + when (archiveErrorResult) { + is NetworkResult.Success -> Log.i(TAG, "[sendTransferArchiveError] Successfully sent transfer archive error.") + is NetworkResult.ApplicationError -> throw archiveErrorResult.throwable + is NetworkResult.NetworkError -> Log.w(TAG, "[sendTransferArchiveError] Network error when sending transfer archive error.", archiveErrorResult.exception) + is NetworkResult.StatusCodeError -> Log.w(TAG, "[sendTransferArchiveError] Status code error when sending transfer archive error.", archiveErrorResult.exception) + } + } + /** * Changes the name of a linked device and sends a sync message if successful */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceSettingsState.kt index d0a79c6a9b..4ae377b8c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceSettingsState.kt @@ -27,7 +27,7 @@ data class LinkDeviceSettingsState( data object Unlinking : DialogState data object SyncingMessages : DialogState data object SyncingTimedOut : DialogState - data class SyncingFailed(val deviceId: Int) : DialogState + data class SyncingFailed(val deviceId: Int, val deviceCreatedAt: Long) : DialogState } sealed interface OneTimeEvent { diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceViewModel.kt index 1767e209c4..d3658c9fa1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceViewModel.kt @@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.linkdevice.LinkDeviceSettingsState.QrCodeState import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.Util import org.whispersystems.signalservice.api.backup.MessageBackupKey +import org.whispersystems.signalservice.api.link.TransferArchiveError import org.whispersystems.signalservice.api.link.WaitForLinkedDeviceResponse import kotlin.time.Duration.Companion.seconds @@ -260,7 +261,7 @@ class LinkDeviceViewModel : ViewModel() { Log.w(TAG, "[addDeviceWithSync] Failed to upload the archive! Result: $uploadResult") _state.update { it.copy( - dialogState = DialogState.SyncingFailed(waitResult.id) + dialogState = DialogState.SyncingFailed(waitResult.id, waitResult.created) ) } } @@ -309,16 +310,29 @@ class LinkDeviceViewModel : ViewModel() { return this.getQueryParameter("capabilities")?.split(",")?.contains("backup") == true } - fun onSyncErrorIgnored() { + fun onSyncErrorIgnored() = viewModelScope.launch(Dispatchers.IO) { + val dialogState = _state.value.dialogState + if (dialogState is DialogState.SyncingFailed) { + Log.i(TAG, "Alerting linked device of sync failure - will not retry") + LinkDeviceRepository.sendTransferArchiveError(dialogState.deviceId, dialogState.deviceCreatedAt, TransferArchiveError.CONTINUE_WITHOUT_UPLOAD) + } + _state.update { - it.copy(dialogState = DialogState.None) + it.copy( + linkDeviceResult = LinkDeviceResult.None, + dialogState = DialogState.None + ) } } - fun onSyncErrorRetryRequested(deviceId: Int?) = viewModelScope.launch(Dispatchers.IO) { - if (deviceId != null) { + fun onSyncErrorRetryRequested() = viewModelScope.launch(Dispatchers.IO) { + val dialogState = _state.value.dialogState + if (dialogState is DialogState.SyncingFailed) { + Log.i(TAG, "Alerting linked device of sync failure - will retry") + LinkDeviceRepository.sendTransferArchiveError(dialogState.deviceId, dialogState.deviceCreatedAt, TransferArchiveError.RELINK_REQUESTED) + Log.i(TAG, "Need to unlink device first...") - val success = LinkDeviceRepository.removeDevice(deviceId) + val success = LinkDeviceRepository.removeDevice(dialogState.deviceId) if (!success) { Log.w(TAG, "Failed to remove device! We did our best. Continuing.") } @@ -326,6 +340,7 @@ class LinkDeviceViewModel : ViewModel() { _state.update { it.copy( + linkDeviceResult = LinkDeviceResult.None, dialogState = DialogState.None, oneTimeEvent = OneTimeEvent.LaunchQrCodeScanner ) diff --git a/core-ui/src/main/java/org/signal/core/ui/Dialogs.kt b/core-ui/src/main/java/org/signal/core/ui/Dialogs.kt index d4ee8a4501..3bda3aee8b 100644 --- a/core-ui/src/main/java/org/signal/core/ui/Dialogs.kt +++ b/core-ui/src/main/java/org/signal/core/ui/Dialogs.kt @@ -76,8 +76,9 @@ object Dialogs { body: String, confirm: String, onConfirm: () -> Unit, - onDismiss: () -> Unit, + onDismiss: () -> Unit = {}, onDismissRequest: () -> Unit = onDismiss, + onDeny: () -> Unit = {}, modifier: Modifier = Modifier, dismiss: String = NoDismiss, confirmColor: Color = Color.Unspecified, @@ -104,7 +105,13 @@ object Dialogs { }, dismissButton = if (dismiss.isNotEmpty()) { { - TextButton(onClick = onDismiss) { + TextButton( + onClick = + { + onDismiss() + onDeny() + } + ) { Text(text = dismiss, color = dismissColor) } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/link/LinkDeviceApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/link/LinkDeviceApi.kt index ebb0e58278..ecc54b95da 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/link/LinkDeviceApi.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/link/LinkDeviceApi.kt @@ -123,7 +123,7 @@ class LinkDeviceApi(private val pushServiceSocket: PushServiceSocket) { SetLinkedDeviceTransferArchiveRequest( destinationDeviceId = destinationDeviceId, destinationDeviceCreated = destinationDeviceCreated, - transferArchive = SetLinkedDeviceTransferArchiveRequest.CdnInfo( + transferArchive = SetLinkedDeviceTransferArchiveRequest.TransferArchive.CdnInfo( cdn = cdn, key = cdnKey ) @@ -132,6 +132,30 @@ class LinkDeviceApi(private val pushServiceSocket: PushServiceSocket) { } } + /** + * If creating an archive has failed after linking a device, notify the linked + * device of the failure and if you are going to try relinking or skip syncing + * + * PUT /v1/devices/transfer_archive + * + * - 204: Success. + * - 422: Bad inputs. + * - 429: Rate-limited. + */ + fun setTransferArchiveError(destinationDeviceId: Int, destinationDeviceCreated: Long, error: TransferArchiveError): NetworkResult { + return NetworkResult.fromFetch { + pushServiceSocket.setLinkedDeviceTransferArchive( + SetLinkedDeviceTransferArchiveRequest( + destinationDeviceId = destinationDeviceId, + destinationDeviceCreated = destinationDeviceCreated, + transferArchive = SetLinkedDeviceTransferArchiveRequest.TransferArchive.Error( + error + ) + ) + ) + } + } + /** * Sets the name for a linked device * diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/link/SetLinkedDeviceTransferArchiveRequest.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/link/SetLinkedDeviceTransferArchiveRequest.kt index 5f080aef28..5d111b39d8 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/link/SetLinkedDeviceTransferArchiveRequest.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/link/SetLinkedDeviceTransferArchiveRequest.kt @@ -13,10 +13,15 @@ import com.fasterxml.jackson.annotation.JsonProperty data class SetLinkedDeviceTransferArchiveRequest( @JsonProperty val destinationDeviceId: Int, @JsonProperty val destinationDeviceCreated: Long, - @JsonProperty val transferArchive: CdnInfo + @JsonProperty val transferArchive: TransferArchive ) { - data class CdnInfo( - @JsonProperty val cdn: Int, - @JsonProperty val key: String - ) + sealed class TransferArchive { + data class CdnInfo( + @JsonProperty val cdn: Int, + @JsonProperty val key: String + ) : TransferArchive() + data class Error( + @JsonProperty val error: TransferArchiveError + ) : TransferArchive() + } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/link/TransferArchiveError.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/link/TransferArchiveError.kt new file mode 100644 index 0000000000..4923fedddf --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/link/TransferArchiveError.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.link + +/** + * Error response options chosen by a user. Response is sent to a linked device after its transfer archive has failed + */ +enum class TransferArchiveError { + RELINK_REQUESTED, + CONTINUE_WITHOUT_UPLOAD +}