diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/AddLinkDeviceFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/AddLinkDeviceFragment.kt
index e0ef86cc8f..2753f89186 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/AddLinkDeviceFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/AddLinkDeviceFragment.kt
@@ -28,7 +28,6 @@ import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.permissions.Permissions
-import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* Fragment that allows users to scan a QR code from their camera to link a device
@@ -44,19 +43,15 @@ class AddLinkDeviceFragment : ComposeFragment() {
val navController: NavController by remember { mutableStateOf(findNavController()) }
val cameraPermissionState: PermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
- if (!state.seenIntroSheet) {
- navController.safeNavigate(R.id.action_addLinkDeviceFragment_to_linkDeviceIntroBottomSheet)
- viewModel.markIntroSheetSeen()
- }
-
- if ((state.qrCodeFound || state.qrCodeInvalid) && navController.currentDestination?.id == R.id.linkDeviceIntroBottomSheet) {
- navController.popBackStack()
- }
-
MainScreen(
state = state,
navController = navController,
hasPermissions = cameraPermissionState.status.isGranted,
+ linkWithoutQrCode = state.linkWithoutQrCode,
+ onLinkDeviceWithUrl = { url ->
+ viewModel.onQrCodeScanned(url)
+ viewModel.addDevice()
+ },
onRequestPermissions = { askPermissions() },
onShowFrontCamera = { viewModel.showFrontCamera() },
onQrCodeScanned = { data -> viewModel.onQrCodeScanned(data) },
@@ -91,6 +86,8 @@ private fun MainScreen(
state: LinkDeviceSettingsState,
navController: NavController? = null,
hasPermissions: Boolean = false,
+ linkWithoutQrCode: Boolean = false,
+ onLinkDeviceWithUrl: (String) -> Unit = {},
onRequestPermissions: () -> Unit = {},
onShowFrontCamera: () -> Unit = {},
onQrCodeScanned: (String) -> Unit = {},
@@ -101,31 +98,44 @@ private fun MainScreen(
onLinkDeviceFailure: () -> Unit = {}
) {
Scaffolds.Settings(
- title = "",
+ title = if (linkWithoutQrCode) stringResource(id = R.string.DeviceAddFragment__link_without_scanning) else "",
onNavigationClick = { navController?.popBackStack() },
navigationIconPainter = painterResource(id = R.drawable.ic_x),
navigationContentDescription = stringResource(id = R.string.Material3SearchToolbar__close),
actions = {
- IconButton(onClick = { onShowFrontCamera() }) {
- Icon(painterResource(id = R.drawable.symbol_switch_24), contentDescription = null)
+ if (!linkWithoutQrCode) {
+ IconButton(onClick = { onShowFrontCamera() }) {
+ Icon(painterResource(id = R.drawable.symbol_switch_24), contentDescription = null)
+ }
}
}
) { contentPadding: PaddingValues ->
- LinkDeviceQrScanScreen(
- hasPermission = hasPermissions,
- onRequestPermissions = onRequestPermissions,
- showFrontCamera = state.showFrontCamera,
- qrCodeFound = state.qrCodeFound,
- qrCodeInvalid = state.qrCodeInvalid,
- onQrCodeScanned = onQrCodeScanned,
- onQrCodeAccepted = onQrCodeApproved,
- onQrCodeDismissed = onQrCodeDismissed,
- onQrCodeRetry = onQrCodeRetry,
- linkDeviceResult = state.linkDeviceResult,
- onLinkDeviceSuccess = onLinkDeviceSuccess,
- onLinkDeviceFailure = onLinkDeviceFailure,
- modifier = Modifier.padding(contentPadding)
- )
+ if (!linkWithoutQrCode) {
+ LinkDeviceQrScanScreen(
+ hasPermission = hasPermissions,
+ onRequestPermissions = onRequestPermissions,
+ showFrontCamera = state.showFrontCamera,
+ qrCodeFound = state.qrCodeFound,
+ qrCodeInvalid = state.qrCodeInvalid,
+ onQrCodeScanned = onQrCodeScanned,
+ onQrCodeAccepted = onQrCodeApproved,
+ onQrCodeDismissed = onQrCodeDismissed,
+ onQrCodeRetry = onQrCodeRetry,
+ linkDeviceResult = state.linkDeviceResult,
+ onLinkDeviceSuccess = onLinkDeviceSuccess,
+ onLinkDeviceFailure = onLinkDeviceFailure,
+ modifier = Modifier.padding(contentPadding)
+ )
+ } else {
+ LinkDeviceManualEntryScreen(
+ onLinkDeviceWithUrl = onLinkDeviceWithUrl,
+ qrCodeFound = state.qrCodeFound,
+ linkDeviceResult = state.linkDeviceResult,
+ onLinkDeviceSuccess = onLinkDeviceSuccess,
+ onLinkDeviceFailure = onLinkDeviceFailure,
+ modifier = Modifier.padding(contentPadding)
+ )
+ }
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceFragment.kt
index ead6f0a9ca..b91f52eb35 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceFragment.kt
@@ -93,7 +93,7 @@ class LinkDeviceFragment : ComposeFragment() {
biometricDeviceLockLauncher = registerForActivityResult(BiometricDeviceLockContract()) { result: Int ->
if (result == BiometricDeviceAuthentication.AUTHENTICATED) {
- findNavController().safeNavigate(R.id.action_linkDeviceFragment_to_addLinkDeviceFragment)
+ findNavController().safeNavigate(R.id.action_linkDeviceFragment_to_linkDeviceIntroBottomSheet)
}
}
@@ -148,7 +148,7 @@ class LinkDeviceFragment : ComposeFragment() {
if (biometricAuth.canAuthenticate()) {
biometricAuth.authenticate(requireContext(), true) { biometricDeviceLockLauncher.launch(getString(R.string.LinkDeviceFragment__unlock_to_link)) }
} else {
- navController.safeNavigate(R.id.action_linkDeviceFragment_to_addLinkDeviceFragment)
+ navController.safeNavigate(R.id.action_linkDeviceFragment_to_linkDeviceIntroBottomSheet)
}
},
setDeviceToRemove = { device -> viewModel.setDeviceToRemove(device) },
@@ -171,7 +171,7 @@ class LinkDeviceFragment : ComposeFragment() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
Log.i(TAG, "Authentication succeeded")
- findNavController().safeNavigate(R.id.action_linkDeviceFragment_to_addLinkDeviceFragment)
+ findNavController().safeNavigate(R.id.action_linkDeviceFragment_to_linkDeviceIntroBottomSheet)
}
override fun onAuthenticationFailed() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceIntroBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceIntroBottomSheet.kt
index 16584118ce..a660264c2d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceIntroBottomSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceIntroBottomSheet.kt
@@ -10,11 +10,16 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.NavController
+import androidx.navigation.fragment.findNavController
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
@@ -25,22 +30,32 @@ import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
+import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* Bottom sheet dialog displayed when users click 'Link a device'
*/
class LinkDeviceIntroBottomSheet : ComposeBottomSheetDialogFragment() {
+ private val viewModel: LinkDeviceViewModel by activityViewModels()
+
override val peekHeightPercentage: Float = 0.8f
@Composable
override fun SheetContent() {
- EducationSheet(this::dismissAllowingStateLoss)
+ val navController: NavController by remember { mutableStateOf(findNavController()) }
+
+ EducationSheet(
+ onClick = { shouldScanQrCode ->
+ viewModel.requestLinkWithoutQrCode(!shouldScanQrCode)
+ navController.safeNavigate(R.id.action_linkDeviceIntroBottomSheet_to_addLinkDeviceFragment)
+ }
+ )
}
}
@Composable
-fun EducationSheet(onClick: () -> Unit) {
+fun EducationSheet(onClick: (Boolean) -> Unit) {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.linking_device))
return Column(
@@ -54,7 +69,7 @@ fun EducationSheet(onClick: () -> Unit) {
LottieAnimation(composition, iterations = LottieConstants.IterateForever, modifier = Modifier.matchParentSize())
}
Text(
- text = stringResource(R.string.AddLinkDeviceFragment__scan_qr_code),
+ text = stringResource(R.string.LinkDeviceFragment__link_a_new_device),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 12.dp)
)
@@ -65,10 +80,16 @@ fun EducationSheet(onClick: () -> Unit) {
modifier = Modifier.padding(bottom = 12.dp)
)
Buttons.LargeTonal(
- onClick = onClick,
+ onClick = { onClick(true) },
+ modifier = Modifier.defaultMinSize(minWidth = 220.dp)
+ ) {
+ Text(stringResource(id = R.string.AddLinkDeviceFragment__scan_qr_code))
+ }
+ Buttons.Small(
+ onClick = { onClick(false) },
modifier = Modifier.defaultMinSize(minWidth = 220.dp)
) {
- Text(stringResource(id = R.string.AddLinkDeviceFragment__okay))
+ Text(stringResource(id = R.string.DeviceAddFragment__link_without_scanning))
}
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceManualEntryScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceManualEntryScreen.kt
new file mode 100644
index 0000000000..7b290dd4c3
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceManualEntryScreen.kt
@@ -0,0 +1,161 @@
+package org.thoughtcrime.securesms.linkdevice
+
+import android.content.Context
+import android.net.Uri
+import android.widget.Toast
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import org.signal.core.ui.Buttons
+import org.signal.core.ui.Previews
+import org.signal.core.ui.SignalPreview
+import org.thoughtcrime.securesms.R
+
+@Composable
+fun LinkDeviceManualEntryScreen(
+ onLinkDeviceWithUrl: (String) -> Unit,
+ qrCodeFound: Boolean,
+ linkDeviceResult: LinkDeviceRepository.LinkDeviceResult,
+ onLinkDeviceSuccess: () -> Unit,
+ onLinkDeviceFailure: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val context = LocalContext.current
+
+ LaunchedEffect(linkDeviceResult) {
+ when (linkDeviceResult) {
+ LinkDeviceRepository.LinkDeviceResult.SUCCESS -> onLinkDeviceSuccess()
+ LinkDeviceRepository.LinkDeviceResult.NO_DEVICE -> makeToast(context, R.string.DeviceProvisioningActivity_content_progress_no_device, onLinkDeviceFailure)
+ LinkDeviceRepository.LinkDeviceResult.NETWORK_ERROR -> makeToast(context, R.string.DeviceProvisioningActivity_content_progress_network_error, onLinkDeviceFailure)
+ LinkDeviceRepository.LinkDeviceResult.KEY_ERROR -> makeToast(context, R.string.DeviceProvisioningActivity_content_progress_key_error, onLinkDeviceFailure)
+ LinkDeviceRepository.LinkDeviceResult.LIMIT_EXCEEDED -> makeToast(context, R.string.DeviceProvisioningActivity_sorry_you_have_too_many_devices_linked_already, onLinkDeviceFailure)
+ LinkDeviceRepository.LinkDeviceResult.BAD_CODE -> makeToast(context, R.string.DeviceActivity_sorry_this_is_not_a_valid_device_link_qr_code, onLinkDeviceFailure)
+ LinkDeviceRepository.LinkDeviceResult.UNKNOWN -> Unit
+ }
+ }
+
+ var qrLink by remember { mutableStateOf("") }
+ val isQrLinkValid = LinkDeviceRepository.isValidQr(Uri.parse(qrLink))
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = modifier
+ .fillMaxWidth()
+ .fillMaxHeight()
+ ) {
+ LazyColumn(
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 24.dp)
+ ) {
+ item {
+ Text(
+ text = stringResource(id = R.string.enter_device_link_dialog__if_your_phone_cant_scan_the_qr_code_you_can_manually_enter_the_link_encoded_in_the_qr_code),
+ style = MaterialTheme.typography.bodyMedium.copy(
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ ),
+ modifier = Modifier.padding(vertical = 12.dp)
+ )
+ }
+
+ item {
+ val textFieldStyle = MaterialTheme.typography.bodyLarge.copy(
+ fontFamily = FontFamily.Monospace,
+ fontSize = 12.sp
+ )
+ TextField(
+ value = qrLink,
+ onValueChange = { qrLink = it },
+ isError = !isQrLinkValid,
+ placeholder = {
+ Text(
+ text = stringResource(id = R.string.enter_device_link_dialog__url),
+ style = textFieldStyle
+ )
+ },
+ keyboardOptions = KeyboardOptions.Default.copy(
+ keyboardType = KeyboardType.Uri,
+ imeAction = ImeAction.Done
+ ),
+ textStyle = textFieldStyle,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 12.dp)
+ .defaultMinSize(minHeight = 40.dp)
+ )
+ }
+
+ item {
+ Text(
+ text = stringResource(id = R.string.AddLinkDeviceFragment__this_device_will_see_your_groups_contacts),
+ style = MaterialTheme.typography.bodyMedium.copy(
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ ),
+ modifier = Modifier.padding(vertical = 12.dp)
+ )
+ }
+ }
+
+ Row {
+ if (qrCodeFound) {
+ CircularProgressIndicator(
+ modifier = Modifier
+ .padding(vertical = 16.dp)
+ )
+ } else {
+ Buttons.LargeTonal(
+ enabled = isQrLinkValid,
+ onClick = { onLinkDeviceWithUrl(qrLink) },
+ modifier = Modifier
+ .defaultMinSize(minWidth = 220.dp)
+ .padding(vertical = 16.dp)
+ ) {
+ Text(text = stringResource(id = R.string.device_list_fragment__link_new_device))
+ }
+ }
+ }
+ }
+}
+
+private fun makeToast(context: Context, messageId: Int, onLinkDeviceFailure: () -> Unit) {
+ Toast.makeText(context, messageId, Toast.LENGTH_LONG).show()
+ onLinkDeviceFailure()
+}
+
+@SignalPreview
+@Composable
+private fun LinkDeviceManualEntryScreenPreview() {
+ Previews.Preview {
+ LinkDeviceManualEntryScreen(
+ onLinkDeviceWithUrl = {},
+ qrCodeFound = false,
+ linkDeviceResult = LinkDeviceRepository.LinkDeviceResult.SUCCESS,
+ onLinkDeviceSuccess = {},
+ onLinkDeviceFailure = {},
+ )
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceSettingsState.kt
index a6168fe67b..5ec7aba771 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceSettingsState.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceSettingsState.kt
@@ -16,6 +16,6 @@ data class LinkDeviceSettingsState(
val url: String = "",
val linkDeviceResult: LinkDeviceRepository.LinkDeviceResult = LinkDeviceRepository.LinkDeviceResult.UNKNOWN,
val showFinishedSheet: Boolean = false,
- val seenIntroSheet: Boolean = false,
+ val linkWithoutQrCode: Boolean = false,
val pendingNewDevice: Boolean = false
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceViewModel.kt
index 2797815860..08f159fe47 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceViewModel.kt
@@ -104,12 +104,9 @@ class LinkDeviceViewModel : ViewModel() {
}
}
- fun markIntroSheetSeen() {
+ fun requestLinkWithoutQrCode(value: Boolean) {
_state.update {
- it.copy(
- seenIntroSheet = true,
- showFrontCamera = null
- )
+ it.copy(linkWithoutQrCode = value)
}
}
diff --git a/app/src/main/res/navigation/app_settings_with_change_number.xml b/app/src/main/res/navigation/app_settings_with_change_number.xml
index a687b316ef..797d1d8ef5 100644
--- a/app/src/main/res/navigation/app_settings_with_change_number.xml
+++ b/app/src/main/res/navigation/app_settings_with_change_number.xml
@@ -244,12 +244,8 @@
android:name="org.thoughtcrime.securesms.linkdevice.LinkDeviceFragment"
android:label="link_device_fragment">
+ android:id="@+id/action_linkDeviceFragment_to_linkDeviceIntroBottomSheet"
+ app:destination="@id/linkDeviceIntroBottomSheet" />
@@ -268,13 +264,18 @@
android:id="@+id/addLinkDeviceFragment"
android:name="org.thoughtcrime.securesms.linkdevice.AddLinkDeviceFragment"
android:label="link_device_add_fragment">
-
+ android:name="org.thoughtcrime.securesms.linkdevice.LinkDeviceIntroBottomSheet">
+
+