Add QR errors

This commit is contained in:
Andrew 2024-03-13 13:14:14 +10:30
parent 8fc83213ba
commit ca0206409c
2 changed files with 86 additions and 36 deletions

View File

@ -12,7 +12,6 @@ import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageAnalysis.Analyzer import androidx.camera.core.ImageAnalysis.Analyzer
import androidx.camera.core.ImageProxy import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView import androidx.camera.view.PreviewView
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
@ -36,9 +35,14 @@ import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.LocalContentColor import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField 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.Text
import androidx.compose.material.TextFieldDefaults import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember 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.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.lifecycleScope 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.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage import com.google.mlkit.vision.common.InputImage
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
@ -79,7 +84,6 @@ import org.thoughtcrime.securesms.ui.colorDestructive
import org.thoughtcrime.securesms.ui.components.SessionTabRow import org.thoughtcrime.securesms.ui.components.SessionTabRow
import java.util.concurrent.Executors import java.util.concurrent.Executors
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
private const val TAG = "LinkDeviceActivity" private const val TAG = "LinkDeviceActivity"
@ -94,7 +98,7 @@ class LinkDeviceActivity : BaseActionBarActivity() {
val viewModel: LinkDeviceViewModel by viewModels() val viewModel: LinkDeviceViewModel by viewModels()
val preview = Preview.Builder().build() val preview = androidx.camera.core.Preview.Builder().build()
val selector = CameraSelector.Builder() val selector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK) .requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build() .build()
@ -147,15 +151,15 @@ class LinkDeviceActivity : BaseActionBarActivity() {
val scanner = BarcodeScanning.getClient(options) val scanner = BarcodeScanning.getClient(options)
runCatching { runCatching {
when (title) { cameraProvider.get().unbindAll()
R.string.activity_link_device_scan_qr_code -> { if (title == R.string.activity_link_device_scan_qr_code) {
LocalSoftwareKeyboardController.current?.hide() LocalSoftwareKeyboardController.current?.hide()
cameraProvider.get().apply { cameraProvider.get().bindToLifecycle(
unbindAll() LocalLifecycleOwner.current,
bindToLifecycle(LocalLifecycleOwner.current, selector, preview, buildAnalysisUseCase(scanner, viewModel::scan)) selector,
} preview,
} buildAnalysisUseCase(scanner, viewModel::scan)
else -> cameraProvider.get().unbindAll() )
} }
}.onFailure { Log.e(TAG, "error binding camera", it) } }.onFailure { Log.e(TAG, "error binding camera", it) }
when (title) { when (title) {
@ -166,6 +170,7 @@ class LinkDeviceActivity : BaseActionBarActivity() {
} }
} }
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
fun MaybeScanQrCode() { fun MaybeScanQrCode() {
@ -173,7 +178,7 @@ class LinkDeviceActivity : BaseActionBarActivity() {
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA) val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
if (cameraPermissionState.status.isGranted) { if (cameraPermissionState.status.isGranted) {
ScanQrCode(preview) ScanQrCode(preview, viewModel.qrErrorsFlow)
} else if (cameraPermissionState.status.shouldShowRationale) { } else if (cameraPermissionState.status.shouldShowRationale) {
Column( Column(
modifier = Modifier modifier = Modifier
@ -204,27 +209,59 @@ class LinkDeviceActivity : BaseActionBarActivity() {
} }
} }
} }
}
@Composable @Composable
fun ScanQrCode(preview: Preview) { fun ScanQrCode(preview: androidx.camera.core.Preview, errors: Flow<String>) {
Box { val scaffoldState = rememberScaffoldState()
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { PreviewView(it).apply { preview.setSurfaceProvider(surfaceProvider) } }
)
Box( LaunchedEffect(Unit) {
Modifier errors.collect { error ->
.aspectRatio(1f) lifecycleScope.launch {
.padding(20.dp) scaffoldState.snackbarHostState.showSnackbar(
.clip(shape = RoundedCornerShape(20.dp)) message = error,
.background(Color(0x33ffffff)) actionLabel = "Dismiss"
.align(Alignment.Center) )
) }
}
}
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 @Composable
fun RecoveryPassword(state: LinkDeviceState, onChange: (String) -> Unit = {}, onContinue: () -> Unit = {}) { fun RecoveryPassword(state: LinkDeviceState, onChange: (String) -> Unit = {}, onContinue: () -> Unit = {}) {
Column( Column(
@ -271,9 +308,9 @@ fun RecoveryPassword(state: LinkDeviceState, onChange: (String) -> Unit = {}, on
OutlineButton( OutlineButton(
text = stringResource(id = R.string.continue_2), text = stringResource(id = R.string.continue_2),
modifier = Modifier modifier = Modifier
.align(Alignment.CenterHorizontally) .align(Alignment.CenterHorizontally)
.padding(horizontal = 64.dp, vertical = 20.dp) .padding(horizontal = 64.dp, vertical = 20.dp)
.width(200.dp) .width(200.dp)
) { onContinue() } ) { onContinue() }
} }
} }

View File

@ -11,12 +11,15 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.session.libsignal.crypto.MnemonicCodec 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.session.libsignal.utilities.Hex
import org.thoughtcrime.securesms.crypto.MnemonicUtilities import org.thoughtcrime.securesms.crypto.MnemonicUtilities
import javax.inject.Inject import javax.inject.Inject
@ -35,7 +38,9 @@ class LinkDeviceViewModel @Inject constructor(
private val event = Channel<LinkDeviceEvent>() private val event = Channel<LinkDeviceEvent>()
val eventFlow = event.receiveAsFlow().take(1) val eventFlow = event.receiveAsFlow().take(1)
private val qrErrors = Channel<Throwable>() private val qrErrors = Channel<Throwable>()
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) } } private val codec by lazy { MnemonicCodec { MnemonicUtilities.loadFileContents(getApplication(), it) } }
@ -64,11 +69,19 @@ class LinkDeviceViewModel @Inject constructor(
} }
private fun onFailure(error: Throwable) { 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) { private fun onScanFailure(error: Throwable) {
state.update { it.copy(error = error.message) } viewModelScope.launch { qrErrors.send(error) }
} }
private fun runDecodeCatching(mnemonic: String) = runCatching { private fun runDecodeCatching(mnemonic: String) = runCatching {