diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt index 3cff0a26be..32932e30a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt @@ -12,7 +12,6 @@ import androidx.camera.core.ExperimentalGetImage import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageAnalysis.Analyzer import androidx.camera.core.ImageProxy -import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.PreviewView import androidx.compose.foundation.ExperimentalFoundationApi @@ -36,9 +35,14 @@ import androidx.compose.material.LocalContentAlpha import androidx.compose.material.LocalContentColor import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Scaffold +import androidx.compose.material.Snackbar +import androidx.compose.material.SnackbarHost import androidx.compose.material.Text import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -53,6 +57,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.lifecycleScope @@ -66,7 +71,7 @@ import com.google.mlkit.vision.barcode.BarcodeScanning import com.google.mlkit.vision.barcode.common.Barcode import com.google.mlkit.vision.common.InputImage import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import network.loki.messenger.R import org.session.libsession.utilities.TextSecurePreferences @@ -79,7 +84,6 @@ import org.thoughtcrime.securesms.ui.colorDestructive import org.thoughtcrime.securesms.ui.components.SessionTabRow import java.util.concurrent.Executors import javax.inject.Inject -import kotlin.coroutines.CoroutineContext private const val TAG = "LinkDeviceActivity" @@ -94,7 +98,7 @@ class LinkDeviceActivity : BaseActionBarActivity() { val viewModel: LinkDeviceViewModel by viewModels() - val preview = Preview.Builder().build() + val preview = androidx.camera.core.Preview.Builder().build() val selector = CameraSelector.Builder() .requireLensFacing(CameraSelector.LENS_FACING_BACK) .build() @@ -147,15 +151,15 @@ class LinkDeviceActivity : BaseActionBarActivity() { val scanner = BarcodeScanning.getClient(options) runCatching { - when (title) { - R.string.activity_link_device_scan_qr_code -> { - LocalSoftwareKeyboardController.current?.hide() - cameraProvider.get().apply { - unbindAll() - bindToLifecycle(LocalLifecycleOwner.current, selector, preview, buildAnalysisUseCase(scanner, viewModel::scan)) - } - } - else -> cameraProvider.get().unbindAll() + cameraProvider.get().unbindAll() + if (title == R.string.activity_link_device_scan_qr_code) { + LocalSoftwareKeyboardController.current?.hide() + cameraProvider.get().bindToLifecycle( + LocalLifecycleOwner.current, + selector, + preview, + buildAnalysisUseCase(scanner, viewModel::scan) + ) } }.onFailure { Log.e(TAG, "error binding camera", it) } when (title) { @@ -166,6 +170,7 @@ class LinkDeviceActivity : BaseActionBarActivity() { } } + @OptIn(ExperimentalPermissionsApi::class) @Composable fun MaybeScanQrCode() { @@ -173,7 +178,7 @@ class LinkDeviceActivity : BaseActionBarActivity() { val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA) if (cameraPermissionState.status.isGranted) { - ScanQrCode(preview) + ScanQrCode(preview, viewModel.qrErrorsFlow) } else if (cameraPermissionState.status.shouldShowRationale) { Column( modifier = Modifier @@ -204,27 +209,59 @@ class LinkDeviceActivity : BaseActionBarActivity() { } } } -} -@Composable -fun ScanQrCode(preview: Preview) { - Box { - AndroidView( - modifier = Modifier.fillMaxSize(), - factory = { PreviewView(it).apply { preview.setSurfaceProvider(surfaceProvider) } } - ) + @Composable + fun ScanQrCode(preview: androidx.camera.core.Preview, errors: Flow) { + val scaffoldState = rememberScaffoldState() - Box( - Modifier - .aspectRatio(1f) - .padding(20.dp) - .clip(shape = RoundedCornerShape(20.dp)) - .background(Color(0x33ffffff)) - .align(Alignment.Center) - ) + LaunchedEffect(Unit) { + errors.collect { error -> + lifecycleScope.launch { + scaffoldState.snackbarHostState.showSnackbar( + message = error, + actionLabel = "Dismiss" + ) + } + } + } + + Scaffold( + scaffoldState = scaffoldState, + snackbarHost = { + SnackbarHost( + hostState = scaffoldState.snackbarHostState, + modifier = Modifier.padding(16.dp) + ) { data -> + Snackbar( + snackbarData = data, + modifier = Modifier.padding(16.dp) + ) + } + } + ) { padding -> + Box(modifier = Modifier.padding(padding)) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { PreviewView(it).apply { preview.setSurfaceProvider(surfaceProvider) } } + ) + + Box( + Modifier + .aspectRatio(1f) + .padding(20.dp) + .clip(shape = RoundedCornerShape(20.dp)) + .background(Color(0x33ffffff)) + .align(Alignment.Center) + ) + } + } } } +@Preview +@Composable +fun PreviewRecoveryPassword() = RecoveryPassword(state = LinkDeviceState()) + @Composable fun RecoveryPassword(state: LinkDeviceState, onChange: (String) -> Unit = {}, onContinue: () -> Unit = {}) { Column( @@ -271,9 +308,9 @@ fun RecoveryPassword(state: LinkDeviceState, onChange: (String) -> Unit = {}, on OutlineButton( text = stringResource(id = R.string.continue_2), modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(horizontal = 64.dp, vertical = 20.dp) - .width(200.dp) + .align(Alignment.CenterHorizontally) + .padding(horizontal = 64.dp, vertical = 20.dp) + .width(200.dp) ) { onContinue() } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceViewModel.kt index c35111b230..c91939a386 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceViewModel.kt @@ -11,12 +11,15 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.session.libsignal.crypto.MnemonicCodec +import org.session.libsignal.crypto.MnemonicCodec.DecodingError.InputTooShort +import org.session.libsignal.crypto.MnemonicCodec.DecodingError.InvalidWord import org.session.libsignal.utilities.Hex import org.thoughtcrime.securesms.crypto.MnemonicUtilities import javax.inject.Inject @@ -35,7 +38,9 @@ class LinkDeviceViewModel @Inject constructor( private val event = Channel() val eventFlow = event.receiveAsFlow().take(1) private val qrErrors = Channel() - private val qrErrorsFlow = qrErrors.receiveAsFlow().debounce(QR_ERROR_TIME).takeWhile { event.isEmpty } + val qrErrorsFlow = qrErrors.receiveAsFlow().debounce(QR_ERROR_TIME).takeWhile { event.isEmpty }.mapNotNull { + "This QR code does not contain a Recovery Password." + } private val codec by lazy { MnemonicCodec { MnemonicUtilities.loadFileContents(getApplication(), it) } } @@ -64,11 +69,19 @@ class LinkDeviceViewModel @Inject constructor( } private fun onFailure(error: Throwable) { - state.update { it.copy(error = error.message) } + state.update { + it.copy( + error = when (error) { + is InputTooShort -> "The Recovery Password you entered is not long enough. Please check and try again." + is InvalidWord -> "Some of the words in your Recovery Password are incorrect. Please check and try again." + else -> "Please check your Recovery Password and try again." + } + ) + } } private fun onScanFailure(error: Throwable) { - state.update { it.copy(error = error.message) } + viewModelScope.launch { qrErrors.send(error) } } private fun runDecodeCatching(mnemonic: String) = runCatching {