Handle QR errors

This commit is contained in:
Andrew 2024-05-10 01:08:53 +09:30
parent 1445d56d08
commit c32a5b6bba
8 changed files with 99 additions and 58 deletions

View File

@ -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)
}
}
}

View File

@ -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) {

View File

@ -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()
}
}
}
}

View File

@ -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) }

View File

@ -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()
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()
}
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()
}
}
@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",

View File

@ -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()
}
}
}
}

View File

@ -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)
}
}

View File

@ -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>