Route system camera quick restore qr scan into Signal camera.

This commit is contained in:
Cody Henthorne
2025-11-21 15:39:55 -05:00
committed by jeffrey-signal
parent 8783d69406
commit 87e56bf4bf
8 changed files with 417 additions and 28 deletions

View File

@@ -934,6 +934,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
handleSignalMeIntent(intent)
handleCallLinkInIntent(intent)
handleDonateReturnIntent(intent)
handleQuickRestoreIntent(intent)
}
@SuppressLint("NewApi")
@@ -990,6 +991,14 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
}
private fun handleQuickRestoreIntent(intent: Intent) {
intent.data?.let { data ->
CommunicationActions.handlePotentialQuickRestoreUrl(this, data.toString()) {
onCameraClick(MainNavigationListLocation.CHATS, isForQuickRestore = true)
}
}
}
private fun updateNotificationProfileStatus(notificationProfiles: List<NotificationProfile>) {
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) {

View File

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

View File

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

View File

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