Handle QR errors

pr/1451-buttons
Andrew 1 month ago
parent 1445d56d08
commit c32a5b6bba

@ -31,6 +31,8 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
import network.loki.messenger.R
@ -78,6 +80,7 @@ class NewMessageFragment : Fragment() {
val uiState by viewModel.state.collectAsState(State())
NewMessage(
uiState,
viewModel.qrErrors,
viewModel,
onClose = { delegate.onDialogClosePressed() },
onBack = { delegate.onDialogBackPressed() },
@ -104,7 +107,7 @@ private fun PreviewNewMessage(
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
) {
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
private fun NewMessage(
state: State,
callbacks: Callbacks,
errors: Flow<String> = emptyFlow(),
callbacks: Callbacks = object: Callbacks {},
onClose: () -> Unit = {},
onBack: () -> Unit = {},
onHelp: () -> Unit = {},
@ -127,7 +131,7 @@ private fun NewMessage(
HorizontalPager(pagerState) {
when (TITLES[it]) {
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 dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
@ -23,26 +24,33 @@ class NewMessageViewModel @Inject constructor(
private val application: Application
): AndroidViewModel(application), Callbacks {
private val _state = MutableStateFlow(
State()
)
private val _state = MutableStateFlow(State())
val state = _state.asStateFlow()
private val _event = Channel<Event>()
val event = _event.receiveAsFlow()
private val _qrErrors = Channel<String>()
val qrErrors: Flow<String> = _qrErrors.receiveAsFlow()
override fun onChange(value: String) {
_state.update { it.copy(
newMessageIdOrOns = value,
error = null
) }
}
override fun onContinue() {
createPrivateChatIfPossible(state.value.newMessageIdOrOns)
}
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) {

@ -137,8 +137,7 @@ fun RecoveryPassword(state: LinkDeviceState, onChange: (String) -> Unit = {}, on
text = state.recoveryPhrase,
modifier = Modifier
.fillMaxWidth()
.contentDescription(R.string.AccessibilityId_recovery_phrase_input)
.padding(horizontal = 64.dp),
.contentDescription(R.string.AccessibilityId_recovery_phrase_input),
placeholder = stringResource(R.string.recoveryPasswordEnter),
onChange = onChange,
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.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -38,9 +40,12 @@ class LinkDeviceViewModel @Inject constructor(
private val event = Channel<LinkDeviceEvent>()
val eventFlow = event.receiveAsFlow().take(1)
private val qrErrors = Channel<Throwable>()
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
val qrErrorsFlow = qrErrors.receiveAsFlow()
.debounce(QR_ERROR_TIME)
// .debounce(QR_ERROR_TIME)
.takeWhile { event.isEmpty }
.mapNotNull { application.getString(R.string.qrNotRecoveryPassword) }

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

@ -7,6 +7,7 @@ import android.net.Uri
import android.provider.Settings
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
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.BarcodeScanning
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.buffer
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import network.loki.messenger.R
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.onboarding.Analyzer
import java.util.concurrent.Executors
import kotlin.time.Duration.Companion.seconds
typealias CameraPreview = androidx.camera.core.Preview
@ -61,7 +68,7 @@ private const val TAG = "NewMessageFragment"
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun MaybeScanQrCode(
errors: Flow<String> = emptyFlow(),
errors: Flow<String>,
onClickSettings: () -> Unit = LocalContext.current.run { {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", packageName, null)
@ -75,7 +82,10 @@ fun MaybeScanQrCode(
val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
if (cameraPermissionState.status.isGranted) {
ScanQrCode(errors, onScan)
ScanQrCode(errors) {
Log.d("QR", "scan: $it")
onScan(it)
}
} else if (cameraPermissionState.status.shouldShowRationale) {
Column(
modifier = Modifier
@ -105,6 +115,7 @@ fun MaybeScanQrCode(
}
}
@OptIn(FlowPreview::class)
@Composable
fun ScanQrCode(errors: Flow<String>, onScan: (String) -> Unit) {
val localContext = LocalContext.current
@ -140,9 +151,11 @@ fun ScanQrCode(errors: Flow<String>, onScan: (String) -> Unit) {
val scaffoldState = rememberScaffoldState()
LaunchedEffect(Unit) {
errors.collect { error ->
scaffoldState.snackbarHostState.showSnackbar(message = error)
}
errors.filter { scaffoldState.snackbarHostState.currentSnackbarData == null }
.buffer(0, BufferOverflow.DROP_OLDEST)
.collect { error ->
scaffoldState.snackbarHostState.showSnackbar(message = error)
}
}
Scaffold(
@ -185,4 +198,26 @@ private fun buildAnalysisUseCase(
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build().apply {
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()
LaunchedEffect(string) {
scope.launch(Dispatchers.IO) {
bitmap = QRCodeUtilities.encode(string, 400).also {
for (y in 150 until 250) {
for (x in 150 until 250) {
val c = 150
val w = c * 2
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)
}
}

@ -1132,4 +1132,5 @@
<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="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>

Loading…
Cancel
Save