mirror of
https://github.com/oxen-io/session-android.git
synced 2025-01-11 16:33:39 +00:00
Handle QR errors
This commit is contained in:
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()
|
||||
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",
|
||||
|
@ -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…
x
Reference in New Issue
Block a user