From 38d11da00b872705fabb26c094b03c19b6b125e0 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Mon, 21 Sep 2026 11:53:45 -0400 Subject: [PATCH] Add capability support on quick restore QR codes. --- .../mediasend/v3/MediaSendV3QrRepository.kt | 7 +++- .../data/QuickRegistrationRepository.kt | 32 ++++++++++++++++++- .../transferaccount/TransferScreen.kt | 8 +++++ app/src/main/res/values/strings.xml | 2 ++ .../mediasend/MediaSendFlowViewModel.kt | 2 ++ .../signal/mediasend/MediaSendQrRepository.kt | 3 ++ .../org/signal/mediasend/MediaSendScreen.kt | 8 +++++ .../src/main/res/values/strings.xml | 5 +++ 8 files changed, 65 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3QrRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3QrRepository.kt index 2ce1fe58a7..ad659a88f0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3QrRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3QrRepository.kt @@ -50,6 +50,11 @@ object MediaSendV3QrRepository : MediaSendQrRepository { } private fun handleReReg(qrData: String): MediaSendQrRepository.QrCheckResult { - return MediaSendQrRepository.QrCheckResult.ReRegistration(qrData) + return if (QuickRegistrationRepository.isNewDeviceCompatible(qrData)) { + MediaSendQrRepository.QrCheckResult.ReRegistration(qrData) + } else { + Log.w(TAG, "New device cannot accept a phone-numberless account") + MediaSendQrRepository.QrCheckResult.ReRegistrationOutdatedDevice + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/data/QuickRegistrationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/QuickRegistrationRepository.kt index 97930daeab..b99a48c285 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/data/QuickRegistrationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/QuickRegistrationRepository.kt @@ -48,6 +48,26 @@ object QuickRegistrationRepository { return uri.host == REREG_URI_HOST && ephemeralId.isNotNullOrBlank() && publicKeyEncoded.isNotNullOrBlank() } + /** + * Whether the new device that generated [data] can handle this account. A phone-numberless account can only be + * transferred to a new device that advertises [Capability.Numberless]. + */ + fun isNewDeviceCompatible(data: String): Boolean { + if (!SignalStore.account.isPhoneNumberless) { + return true + } + + return Capability.Numberless in Uri.parse(data).getCapabilities() + } + + private fun Uri.getCapabilities(): Set { + return this.getQueryParameter("capabilities") + ?.split(",") + ?.mapNotNull { value -> Capability.entries.firstOrNull { it.value == value } } + ?.toSet() + ?: emptySet() + } + /** * Send registration provisioning message to new device. */ @@ -57,6 +77,11 @@ object QuickRegistrationRepository { return TransferAccountResult.FAILED } + if (!isNewDeviceCompatible(reRegisterUri)) { + Log.w(TAG, "New device cannot accept a phone-numberless account") + return TransferAccountResult.NEW_DEVICE_OUTDATED + } + val uri = Uri.parse(reRegisterUri) try { @@ -165,6 +190,11 @@ object QuickRegistrationRepository { enum class TransferAccountResult { SUCCESS, - FAILED + FAILED, + NEW_DEVICE_OUTDATED + } + + private enum class Capability(val value: String) { + Numberless("nopni") } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/olddevice/transferaccount/TransferScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/olddevice/transferaccount/TransferScreen.kt index 87005ae4b1..2a8cbfc53b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/olddevice/transferaccount/TransferScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/olddevice/transferaccount/TransferScreen.kt @@ -131,6 +131,14 @@ fun TransferAccountScreen( ) } + QuickRegistrationRepository.TransferAccountResult.NEW_DEVICE_OUTDATED -> { + Dialogs.SimpleMessageDialog( + message = stringResource(R.string.TransferAccount_the_device_you_are_transferring_to_is_out_of_date), + dismiss = stringResource(android.R.string.ok), + onDismiss = { emitter(TransferScreenEvents.ErrorDialogDismissed) } + ) + } + null -> Unit } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a802b7f29a..123ba108f3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9021,6 +9021,8 @@ Continue on your other device Continue transferring your account on your other device. + + The device you\'re transferring to is out of date. Update Signal on that device and try again. Getting your device ready diff --git a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendFlowViewModel.kt b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendFlowViewModel.kt index b0f4a1801e..c4ca94c610 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendFlowViewModel.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendFlowViewModel.kt @@ -125,6 +125,7 @@ class MediaSendFlowViewModel( internal val usernameScannedDialog = DialogController() internal val linkedDeviceScannedDialog = DialogController() + internal val reRegistrationOutdatedDeviceDialog = DialogController() internal val discardMediaDialog = DialogController() internal val addToGroupStoryDialog = DialogController() @@ -189,6 +190,7 @@ class MediaSendFlowViewModel( } MediaSendQrRepository.QrCheckResult.None -> Unit is MediaSendQrRepository.QrCheckResult.ReRegistration -> sendHudCommand(MediaSendFlowHudCommand.GoToQuickTransfer(qrData)) + MediaSendQrRepository.QrCheckResult.ReRegistrationOutdatedDevice -> reRegistrationOutdatedDeviceDialog.show(Unit) is MediaSendQrRepository.QrCheckResult.Username -> { when (usernameScannedDialog.show(result.username)) { DialogResult.POSITIVE -> sendHudCommand(MediaSendFlowHudCommand.GoToConversation(result.recipientId)) diff --git a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendQrRepository.kt b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendQrRepository.kt index 4415ed502f..eb3d8c6e37 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendQrRepository.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendQrRepository.kt @@ -13,5 +13,8 @@ interface MediaSendQrRepository { data class Username(val recipientId: MediaRecipientId, val username: String) : QrCheckResult data object LinkDevice : QrCheckResult data class ReRegistration(val qrData: String) : QrCheckResult + + /** Scanned a valid re-registration QR code from a new device that can't accept this account. */ + data object ReRegistrationOutdatedDevice : QrCheckResult } } diff --git a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendScreen.kt b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendScreen.kt index e34d0ce8a2..21b6043837 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendScreen.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendScreen.kt @@ -60,6 +60,14 @@ fun MediaSendScreen( onDismissRequest = onDismissRequest ) } + viewModel.reRegistrationOutdatedDeviceDialog.Content { _, onDismissRequest, _, _, _ -> + Dialogs.SimpleMessageDialog( + title = stringResource(R.string.ReRegistrationScannedDialog__transfer_failed_title), + message = stringResource(R.string.ReRegistrationScannedDialog__the_device_you_are_transferring_to_is_out_of_date), + dismiss = stringResource(android.R.string.ok), + onDismiss = onDismissRequest + ) + } viewModel.addToGroupStoryDialog.Content { recipientId, onDismissRequest, onConfirm, _, onDeny -> val groupName: String by LocalDisplayNameProvider.current(recipientId.id) diff --git a/feature/media-send/src/main/res/values/strings.xml b/feature/media-send/src/main/res/values/strings.xml index cd7463bb6c..03ea53f199 100644 --- a/feature/media-send/src/main/res/values/strings.xml +++ b/feature/media-send/src/main/res/values/strings.xml @@ -231,6 +231,11 @@ Continue + + Can\'t transfer account + + The device you\'re transferring to is out of date. Update Signal on that device and try again. + %1$d Gif