Skip to content

Commit

Permalink
Re-add "Link without scanning" option for device linking
Browse files Browse the repository at this point in the history
Fixes #360
  • Loading branch information
valldrac committed Sep 12, 2024
1 parent 1f22ea8 commit 05805dd
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) },
Expand Down Expand Up @@ -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 = {},
Expand All @@ -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)
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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) },
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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)
)
Expand All @@ -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))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = {},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
Loading

0 comments on commit 05805dd

Please sign in to comment.