Add link+sync error flows.

This commit is contained in:
Michelle Tang 2024-12-05 18:33:12 -05:00 committed by Greyson Parrelli
parent e1d4566dfd
commit df5ef06109
8 changed files with 104 additions and 25 deletions

View file

@ -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
)
}
}

View file

@ -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
*/

View file

@ -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 {

View file

@ -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
)

View file

@ -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)
}
}

View file

@ -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<Unit> {
return NetworkResult.fromFetch {
pushServiceSocket.setLinkedDeviceTransferArchive(
SetLinkedDeviceTransferArchiveRequest(
destinationDeviceId = destinationDeviceId,
destinationDeviceCreated = destinationDeviceCreated,
transferArchive = SetLinkedDeviceTransferArchiveRequest.TransferArchive.Error(
error
)
)
)
}
}
/**
* Sets the name for a linked device
*

View file

@ -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()
}
}

View file

@ -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
}