From 87e56bf4bf9cce3674e4b88c4c5a8e33737cafde Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Fri, 21 Nov 2025 15:39:55 -0500 Subject: [PATCH] Route system camera quick restore qr scan into Signal camera. --- app/src/main/AndroidManifest.xml | 7 ++ .../thoughtcrime/securesms/MainActivity.kt | 70 +++++++---- .../mediasend/v2/MediaSelectionActivity.kt | 17 ++- .../mediasend/v2/QuickRestoreInfoDialog.kt | 88 ++++++++++++++ .../securesms/util/CommunicationActions.java | 21 ++++ .../main/res/drawable-night/quick_restore.xml | 115 ++++++++++++++++++ app/src/main/res/drawable/quick_restore.xml | 115 ++++++++++++++++++ app/src/main/res/values/strings.xml | 12 ++ 8 files changed, 417 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/QuickRestoreInfoDialog.kt create mode 100644 app/src/main/res/drawable-night/quick_restore.xml create mode 100644 app/src/main/res/drawable/quick_restore.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1d061b6057..54fa1d187a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -636,6 +636,13 @@ + + + + + + + + CommunicationActions.handlePotentialQuickRestoreUrl(this, data.toString()) { + onCameraClick(MainNavigationListLocation.CHATS, isForQuickRestore = true) + } + } + } + private fun updateNotificationProfileStatus(notificationProfiles: List) { val activeProfile = NotificationProfiles.getActiveProfile(notificationProfiles) if (activeProfile != null) { @@ -1028,6 +1037,39 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner } } + private fun onCameraClick(destination: MainNavigationListLocation, isForQuickRestore: Boolean) { + val onGranted = { + val intent = if (isForQuickRestore) { + MediaSelectionActivity.cameraForQuickRestore(context = this@MainActivity) + } else { + MediaSelectionActivity.camera( + context = this@MainActivity, + isStory = destination == MainNavigationListLocation.STORIES + ) + } + startActivity(intent) + } + + if (CameraXUtil.isSupported()) { + onGranted() + } else { + Permissions.with(this@MainActivity) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.symbol_camera_24) + .withPermanentDenialDialog( + getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), + null, + R.string.CameraXFragment_allow_access_camera, + R.string.CameraXFragment_to_capture_photos_videos, + supportFragmentManager + ) + .onAllGranted(onGranted) + .onAnyDenied { Toast.makeText(this@MainActivity, R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() } + .execute() + } + } + inner class ToolbarCallback : MainToolbarCallback { override fun onNewGroupClick() { @@ -1125,33 +1167,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner } override fun onCameraClick(destination: MainNavigationListLocation) { - val onGranted = { - startActivity( - MediaSelectionActivity.camera( - context = this@MainActivity, - isStory = destination == MainNavigationListLocation.STORIES - ) - ) - } - - if (CameraXUtil.isSupported()) { - onGranted() - } else { - Permissions.with(this@MainActivity) - .request(Manifest.permission.CAMERA) - .ifNecessary() - .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.symbol_camera_24) - .withPermanentDenialDialog( - getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), - null, - R.string.CameraXFragment_allow_access_camera, - R.string.CameraXFragment_to_capture_photos_videos, - supportFragmentManager - ) - .onAllGranted(onGranted) - .onAnyDenied { Toast.makeText(this@MainActivity, R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() } - .execute() - } + onCameraClick(destination, false) } override fun onMegaphoneVisible(megaphone: Megaphone) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt index 2dffaa6556..bff372546c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt @@ -183,6 +183,10 @@ class MediaSelectionActivity : .subscribe(this::handleError) onBackPressedDispatcher.addCallback(OnBackPressed()) + + if (savedInstanceState == null && intent.getBooleanExtra(IS_FOR_QUICK_RESTORE, false)) { + QuickRestoreInfoDialog.show(supportFragmentManager) + } } private fun handleError(error: MediaValidator.FilterError) { @@ -385,6 +389,7 @@ class MediaSelectionActivity : private const val IS_STORY = "is_story" private const val AS_TEXT_STORY = "as_text_story" private const val IS_ADD_TO_GROUP_STORY_FLOW = "is_add_to_group_story_flow" + private const val IS_FOR_QUICK_RESTORE = "is_for_quick_restore" @JvmStatic fun camera(context: Context): Intent { @@ -400,6 +405,14 @@ class MediaSelectionActivity : ) } + fun cameraForQuickRestore(context: Context): Intent { + return buildIntent( + context = context, + startAction = R.id.action_directly_to_mediaCaptureFragment, + isForQuickRestore = true + ) + } + fun addToGroupStory( context: Context, recipientId: RecipientId @@ -508,7 +521,8 @@ class MediaSelectionActivity : isReply: Boolean = false, isStory: Boolean = false, asTextStory: Boolean = false, - isAddToGroupStoryFlow: Boolean = false + isAddToGroupStoryFlow: Boolean = false, + isForQuickRestore: Boolean = false ): Intent { return Intent(context, MediaSelectionActivity::class.java).apply { putExtra(START_ACTION, startAction) @@ -520,6 +534,7 @@ class MediaSelectionActivity : putExtra(IS_STORY, isStory) putExtra(AS_TEXT_STORY, asTextStory) putExtra(IS_ADD_TO_GROUP_STORY_FLOW, isAddToGroupStoryFlow) + putExtra(IS_FOR_QUICK_RESTORE, isForQuickRestore) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/QuickRestoreInfoDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/QuickRestoreInfoDialog.kt new file mode 100644 index 0000000000..6dac96c044 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/QuickRestoreInfoDialog.kt @@ -0,0 +1,88 @@ +package org.thoughtcrime.securesms.mediasend.v2 + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.fragment.app.FragmentManager +import org.signal.core.ui.compose.BottomSheets +import org.signal.core.ui.compose.Buttons +import org.signal.core.ui.compose.DayNightPreviews +import org.signal.core.ui.compose.Previews +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment +import org.thoughtcrime.securesms.util.BottomSheetUtil + +/** + * Bottom sheet dialog displayed when users scan a quick restore with the system camera and then + * follow the prompt into the Signal camera to scan the qr code a second time from within Signal. + */ +class QuickRestoreInfoDialog : ComposeBottomSheetDialogFragment() { + + companion object { + fun show(fragmentManager: FragmentManager) { + QuickRestoreInfoDialog().show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) + } + } + + override val peekHeightPercentage: Float = 1f + + @Composable + override fun SheetContent() { + InfoSheet(this::dismissAllowingStateLoss) + } +} + +@Composable +private fun InfoSheet(onClick: () -> Unit) { + return Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + BottomSheets.Handle() + + Image( + imageVector = ImageVector.vectorResource(R.drawable.quick_restore), + contentDescription = null, + modifier = Modifier.padding(top = 14.dp, bottom = 24.dp) + ) + Text( + text = stringResource(R.string.QuickRestoreInfoDialog__scan_qr_code), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(bottom = 8.dp) + ) + Text( + text = stringResource(R.string.QuickRestoreInfoDialog__use_this_device_to_scan_qr_code), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = 28.dp) + ) + Buttons.LargeTonal( + onClick = onClick, + modifier = Modifier.defaultMinSize(minWidth = 220.dp) + ) { + Text(stringResource(id = R.string.QuickRestoreInfoDialog__okay)) + } + } +} + +@DayNightPreviews +@Composable +fun InfoSheetPreview() { + Previews.BottomSheetPreview { + InfoSheet(onClick = {}) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java index 1c9c643079..599b954c47 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java @@ -54,6 +54,7 @@ import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import org.whispersystems.signalservice.api.push.UsernameLinkComponents; import java.io.IOException; +import java.net.URI; import java.util.Objects; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -343,6 +344,26 @@ public class CommunicationActions { startVideoCall(new ActivityCallContext(activity), linkParseResult.getRootKey(), linkParseResult.getEpoch(), onUserAlreadyInAnotherCall); } + /** + * If the url is a quick restore link it will handle it. + * Otherwise returns false, indicating it was not a quick restore link. + */ + public static boolean handlePotentialQuickRestoreUrl(@NonNull FragmentActivity activity, @NonNull String potentialQuickRestoreUrl, @NonNull Runnable onContinue) { + URI uri = URI.create(potentialQuickRestoreUrl); + + if ("sgnl".equalsIgnoreCase(uri.getScheme()) && "rereg".equalsIgnoreCase(uri.getHost())) { + new MaterialAlertDialogBuilder(activity) + .setTitle(R.string.CommunicationActions__transfer_dialog_title) + .setMessage(R.string.CommunicationActions__transfer_dialog_message) + .setPositiveButton(R.string.DeviceProvisioningActivity_continue, (d, w) -> onContinue.run()) + .setNegativeButton(R.string.CommunicationActions__dont_transfer, null) + .show(); + return true; + } else { + return false; + } + } + /** * Attempts to start a video call for the given call link via root key. This will insert a call link into * the user's database if one does not already exist. diff --git a/app/src/main/res/drawable-night/quick_restore.xml b/app/src/main/res/drawable-night/quick_restore.xml new file mode 100644 index 0000000000..d3172ab5d6 --- /dev/null +++ b/app/src/main/res/drawable-night/quick_restore.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/quick_restore.xml b/app/src/main/res/drawable/quick_restore.xml new file mode 100644 index 0000000000..0dfdaed397 --- /dev/null +++ b/app/src/main/res/drawable/quick_restore.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 89c56e53d6..4a5cf650f1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -367,6 +367,11 @@ This is not a valid call link. Make sure the entire link is intact and correct before attempting to join. You are already in a call + + Transfer account to new device? + + To transfer this account to a new device, tap \"Continue\" and scan the QR code again with the Signal camera. Make sure you only scan QR codes that come directly from Signal. + Don\'t transfer @@ -8924,6 +8929,13 @@ Add a question + + Scan QR code + + Use this device to scan the QR code on the device you want to transfer to + + Okay + Audio issue