Handle QR errors

pr/1451-buttons
Andrew 11 months ago
parent 1445d56d08
commit c32a5b6bba

@ -31,6 +31,8 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
@ -78,6 +80,7 @@ class NewMessageFragment : Fragment() {
val uiState by viewModel.state.collectAsState(State()) val uiState by viewModel.state.collectAsState(State())
NewMessage( NewMessage(
uiState, uiState,
viewModel.qrErrors,
viewModel, viewModel,
onClose = { delegate.onDialogClosePressed() }, onClose = { delegate.onDialogClosePressed() },
onBack = { delegate.onDialogBackPressed() }, onBack = { delegate.onDialogBackPressed() },
@ -104,7 +107,7 @@ private fun PreviewNewMessage(
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
) { ) {
PreviewTheme(themeResId) { PreviewTheme(themeResId) {
NewMessage(State(), object: Callbacks {}) NewMessage(State())
} }
} }
@ -114,7 +117,8 @@ private val TITLES = listOf(R.string.enter_account_id, R.string.qrScan)
@Composable @Composable
private fun NewMessage( private fun NewMessage(
state: State, state: State,
callbacks: Callbacks, errors: Flow<String> = emptyFlow(),
callbacks: Callbacks = object: Callbacks {},
onClose: () -> Unit = {}, onClose: () -> Unit = {},
onBack: () -> Unit = {}, onBack: () -> Unit = {},
onHelp: () -> Unit = {}, onHelp: () -> Unit = {},
@ -127,7 +131,7 @@ private fun NewMessage(
HorizontalPager(pagerState) { HorizontalPager(pagerState) {
when (TITLES[it]) { when (TITLES[it]) {
R.string.enter_account_id -> EnterAccountId(state, callbacks, onHelp) R.string.enter_account_id -> EnterAccountId(state, callbacks, onHelp)
R.string.qrScan -> MaybeScanQrCode(onScan = callbacks::onScan) R.string.qrScan -> MaybeScanQrCode(errors, onScan = callbacks::onScan)
} }
} }
} }

@ -5,6 +5,7 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
@ -23,26 +24,33 @@ class NewMessageViewModel @Inject constructor(
private val application: Application private val application: Application
): AndroidViewModel(application), Callbacks { ): AndroidViewModel(application), Callbacks {
private val _state = MutableStateFlow(
State() private val _state = MutableStateFlow(State())
)
val state = _state.asStateFlow() val state = _state.asStateFlow()
private val _event = Channel<Event>() private val _event = Channel<Event>()
val event = _event.receiveAsFlow() val event = _event.receiveAsFlow()
private val _qrErrors = Channel<String>()
val qrErrors: Flow<String> = _qrErrors.receiveAsFlow()
override fun onChange(value: String) { override fun onChange(value: String) {
_state.update { it.copy( _state.update { it.copy(
newMessageIdOrOns = value, newMessageIdOrOns = value,
error = null error = null
) } ) }
} }
override fun onContinue() { override fun onContinue() {
createPrivateChatIfPossible(state.value.newMessageIdOrOns) createPrivateChatIfPossible(state.value.newMessageIdOrOns)
} }
override fun onScan(value: String) { override fun onScan(value: String) {
createPrivateChatIfPossible(value) if (PublicKeyValidation.isValid(value, isPrefixRequired = false) && PublicKeyValidation.hasValidPrefix(value)) {
onPublicKey(value)
} else {
_qrErrors.trySend(application.getString(R.string.this_qr_code_does_not_contain_an_account_id))
}
} }
private fun createPrivateChatIfPossible(onsNameOrPublicKey: String) { private fun createPrivateChatIfPossible(onsNameOrPublicKey: String) {

@ -137,8 +137,7 @@ fun RecoveryPassword(state: LinkDeviceState, onChange: (String) -> Unit = {}, on
text = state.recoveryPhrase, text = state.recoveryPhrase,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.contentDescription(R.string.AccessibilityId_recovery_phrase_input) .contentDescription(R.string.AccessibilityId_recovery_phrase_input),
.padding(horizontal = 64.dp),
placeholder = stringResource(R.string.recoveryPasswordEnter), placeholder = stringResource(R.string.recoveryPasswordEnter),
onChange = onChange, onChange = onChange,
onContinue = onContinue, onContinue = onContinue,
@ -165,24 +164,3 @@ fun RecoveryPassword(state: LinkDeviceState, onChange: (String) -> Unit = {}, on
} }
} }
class Analyzer(
private val scanner: BarcodeScanner,
private val onBarcodeScanned: (String) -> Unit
): Analyzer {
@SuppressLint("UnsafeOptInUsageError")
override fun analyze(image: ImageProxy) {
InputImage.fromMediaImage(
image.image!!,
image.imageInfo.rotationDegrees
).let(scanner::process).apply {
addOnSuccessListener { barcodes ->
barcodes.filter { it.valueType == Barcode.TYPE_TEXT }.forEach {
it.rawValue?.let(onBarcodeScanned)
}
}
addOnCompleteListener {
image.close()
}
}
}
}

@ -4,6 +4,8 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -38,9 +40,12 @@ 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>()
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
val qrErrorsFlow = qrErrors.receiveAsFlow() val qrErrorsFlow = qrErrors.receiveAsFlow()
.debounce(QR_ERROR_TIME) // .debounce(QR_ERROR_TIME)
.takeWhile { event.isEmpty } .takeWhile { event.isEmpty }
.mapNotNull { application.getString(R.string.qrNotRecoveryPassword) } .mapNotNull { application.getString(R.string.qrNotRecoveryPassword) }

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.preferences package org.thoughtcrime.securesms.preferences
import android.os.Bundle import android.os.Bundle
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -15,6 +14,9 @@ import androidx.compose.ui.Modifier
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.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
@ -34,33 +36,36 @@ private val TITLES = listOf(R.string.view, R.string.scan)
class QRCodeActivity : PassphraseRequiredActionBarActivity() { class QRCodeActivity : PassphraseRequiredActionBarActivity() {
private val errors = Channel<String>()
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
supportActionBar!!.title = resources.getString(R.string.activity_qr_code_title) supportActionBar!!.title = resources.getString(R.string.activity_qr_code_title)
setComposeContent { setComposeContent {
Tabs(TextSecurePreferences.getLocalNumber(this)!!, onScan = ::handleQRCodeScanned) Tabs(TextSecurePreferences.getLocalNumber(this)!!, errors.receiveAsFlow(), onScan = ::onScan)
} }
} }
fun handleQRCodeScanned(string: String) { fun onScan(string: String) {
if (!PublicKeyValidation.isValid(string)) { if (!PublicKeyValidation.isValid(string)) {
return Toast.makeText(this, R.string.invalid_session_id, Toast.LENGTH_SHORT).show() errors.trySend(getString(R.string.this_qr_code_does_not_contain_an_account_id))
} } else if (!isFinishing) {
val recipient = Recipient.from(this, Address.fromSerialized(string), false) val recipient = Recipient.from(this, Address.fromSerialized(string), false)
start<ConversationActivityV2> { start<ConversationActivityV2> {
putExtra(ConversationActivityV2.ADDRESS, recipient.address) putExtra(ConversationActivityV2.ADDRESS, recipient.address)
setDataAndType(intent.data, intent.type) setDataAndType(intent.data, intent.type)
val existingThread = threadDatabase().getThreadIdIfExistsFor(recipient) val existingThread = threadDatabase().getThreadIdIfExistsFor(recipient)
putExtra(ConversationActivityV2.THREAD_ID, existingThread) putExtra(ConversationActivityV2.THREAD_ID, existingThread)
}
finish()
} }
finish()
} }
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun Tabs(sessionId: String, onScan: (String) -> Unit) { private fun Tabs(sessionId: String, errors: Flow<String>, onScan: (String) -> Unit) {
val pagerState = rememberPagerState { TITLES.size } val pagerState = rememberPagerState { TITLES.size }
Column { Column {
@ -71,7 +76,7 @@ fun Tabs(sessionId: String, onScan: (String) -> Unit) {
) { page -> ) { page ->
when (TITLES[page]) { when (TITLES[page]) {
R.string.view -> QrPage(sessionId) R.string.view -> QrPage(sessionId)
R.string.scan -> MaybeScanQrCode(onScan = onScan) R.string.scan -> MaybeScanQrCode(errors, onScan = onScan)
} }
} }
} }
@ -79,9 +84,11 @@ fun Tabs(sessionId: String, onScan: (String) -> Unit) {
@Composable @Composable
fun QrPage(string: String) { fun QrPage(string: String) {
Column(modifier = Modifier Column(
.padding(horizontal = 32.dp) modifier = Modifier
.fillMaxSize()) { .padding(horizontal = 32.dp)
.fillMaxSize()
) {
QrImage( QrImage(
string = string, string = string,
contentDescription = "Your session id", contentDescription = "Your session id",

@ -7,6 +7,7 @@ import android.net.Uri
import android.provider.Settings import android.provider.Settings
import androidx.camera.core.CameraSelector import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView import androidx.camera.view.PreviewView
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -47,12 +48,18 @@ import com.google.mlkit.vision.barcode.BarcodeScanner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning 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 kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.onboarding.Analyzer
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.time.Duration.Companion.seconds
typealias CameraPreview = androidx.camera.core.Preview typealias CameraPreview = androidx.camera.core.Preview
@ -61,7 +68,7 @@ private const val TAG = "NewMessageFragment"
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
fun MaybeScanQrCode( fun MaybeScanQrCode(
errors: Flow<String> = emptyFlow(), errors: Flow<String>,
onClickSettings: () -> Unit = LocalContext.current.run { { onClickSettings: () -> Unit = LocalContext.current.run { {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", packageName, null) data = Uri.fromParts("package", packageName, null)
@ -75,7 +82,10 @@ fun MaybeScanQrCode(
val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA) val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
if (cameraPermissionState.status.isGranted) { if (cameraPermissionState.status.isGranted) {
ScanQrCode(errors, onScan) ScanQrCode(errors) {
Log.d("QR", "scan: $it")
onScan(it)
}
} else if (cameraPermissionState.status.shouldShowRationale) { } else if (cameraPermissionState.status.shouldShowRationale) {
Column( Column(
modifier = Modifier modifier = Modifier
@ -105,6 +115,7 @@ fun MaybeScanQrCode(
} }
} }
@OptIn(FlowPreview::class)
@Composable @Composable
fun ScanQrCode(errors: Flow<String>, onScan: (String) -> Unit) { fun ScanQrCode(errors: Flow<String>, onScan: (String) -> Unit) {
val localContext = LocalContext.current val localContext = LocalContext.current
@ -140,9 +151,11 @@ fun ScanQrCode(errors: Flow<String>, onScan: (String) -> Unit) {
val scaffoldState = rememberScaffoldState() val scaffoldState = rememberScaffoldState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
errors.collect { error -> errors.filter { scaffoldState.snackbarHostState.currentSnackbarData == null }
scaffoldState.snackbarHostState.showSnackbar(message = error) .buffer(0, BufferOverflow.DROP_OLDEST)
} .collect { error ->
scaffoldState.snackbarHostState.showSnackbar(message = error)
}
} }
Scaffold( Scaffold(
@ -185,4 +198,26 @@ private fun buildAnalysisUseCase(
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build().apply { .build().apply {
setAnalyzer(Executors.newSingleThreadExecutor(), Analyzer(scanner, onBarcodeScanned)) setAnalyzer(Executors.newSingleThreadExecutor(), Analyzer(scanner, onBarcodeScanned))
} }
class Analyzer(
private val scanner: BarcodeScanner,
private val onBarcodeScanned: (String) -> Unit
): ImageAnalysis.Analyzer {
@SuppressLint("UnsafeOptInUsageError")
override fun analyze(image: ImageProxy) {
InputImage.fromMediaImage(
image.image!!,
image.imageInfo.rotationDegrees
).let(scanner::process).apply {
addOnSuccessListener { barcodes ->
barcodes.forEach {
it.rawValue?.let(onBarcodeScanned)
}
}
addOnCompleteListener {
image.close()
}
}
}
}

@ -45,9 +45,12 @@ fun QrImage(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
LaunchedEffect(string) { LaunchedEffect(string) {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
bitmap = QRCodeUtilities.encode(string, 400).also { val c = 150
for (y in 150 until 250) { val w = c * 2
for (x in 150 until 250) { bitmap = QRCodeUtilities.encode(string, w).also {
val hw = 30
for (y in c - hw until c + hw) {
for (x in c - hw until c + hw) {
it.setPixel(x, y, 0x00000000) it.setPixel(x, y, 0x00000000)
} }
} }

@ -1132,4 +1132,5 @@
<string name="onsErrorNotRecognized">We couldn\'t recognize this ONS. Please check it and try again.</string> <string name="onsErrorNotRecognized">We couldn\'t recognize this ONS. Please check it and try again.</string>
<string name="this_is_your_account_id_other_users_can_scan_it_to_start_a_conversation_with_you">This is your Account ID. Other users can scan it to start a conversation with you.</string> <string name="this_is_your_account_id_other_users_can_scan_it_to_start_a_conversation_with_you">This is your Account ID. Other users can scan it to start a conversation with you.</string>
<string name="accountIdShare">Hey, I\'ve been using Session to chat with complete privacy and security. Come join me! My Account ID is \n\n%1$s\n\nDownload it at https://getsession.org/</string> <string name="accountIdShare">Hey, I\'ve been using Session to chat with complete privacy and security. Come join me! My Account ID is \n\n%1$s\n\nDownload it at https://getsession.org/</string>
<string name="this_qr_code_does_not_contain_an_account_id">This QR code does not contain an Account ID.</string>
</resources> </resources>

Loading…
Cancel
Save