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