android, desktop: allow to scan QR multiple times after fail (#5323)

This commit is contained in:
Stanislav Dmitrenko
2024-12-05 15:52:45 +00:00
committed by GitHub
parent c1a0943448
commit 4f8a70a6c1
10 changed files with 123 additions and 99 deletions
@@ -33,6 +33,7 @@ import com.google.accompanist.permissions.rememberPermissionState
import com.google.common.util.concurrent.ListenableFuture
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
import java.util.concurrent.*
// Adapted from learntodroid - https://gist.github.com/learntodroid/8f839be0b29d0378f843af70607bd7f5
@@ -41,13 +42,13 @@ import java.util.concurrent.*
actual fun QRCodeScanner(
showQRCodeScanner: MutableState<Boolean>,
padding: PaddingValues,
onBarcode: (String) -> Unit
onBarcode: suspend (String) -> Boolean
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
var preview by remember { mutableStateOf<Preview?>(null) }
var lastAnalyzedTimeStamp = 0L
var contactLink = ""
val preview = remember { mutableStateOf<Preview?>(null) }
val contactLink = remember { mutableStateOf("") }
val checkingLink = remember { mutableStateOf(false) }
val cameraProviderFuture by produceState<ListenableFuture<ProcessCameraProvider>?>(initialValue = null) {
value = ProcessCameraProvider.getInstance(context)
@@ -86,28 +87,33 @@ actual fun QRCodeScanner(
.build()
val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
cameraProviderFuture?.addListener({
preview = Preview.Builder().build().also {
preview.value = Preview.Builder().build().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val detector: QrCodeDetector<GrayU8> = FactoryFiducial.qrcode(null, GrayU8::class.java)
fun getQR(imageProxy: ImageProxy) {
val currentTimeStamp = System.currentTimeMillis()
if (currentTimeStamp - lastAnalyzedTimeStamp >= TimeUnit.SECONDS.toMillis(1)) {
detector.process(imageProxyToGrayU8(imageProxy))
val found = detector.detections
val qr = found.firstOrNull()
if (qr != null) {
if (qr.message != contactLink) {
// Make sure link is new and not a repeat
contactLink = qr.message
onBarcode(contactLink)
suspend fun getQR(imageProxy: ImageProxy) {
if (checkingLink.value) return
checkingLink.value = true
detector.process(imageProxyToGrayU8(imageProxy))
val found = detector.detections
val qr = found.firstOrNull()
if (qr != null) {
if (qr.message != contactLink.value) {
// Make sure link is new and not a repeat if that link was handled successfully
if (onBarcode(qr.message)) {
contactLink.value = qr.message
}
// just some delay to not spam endlessly with alert in case the user scan something wrong, and it fails fast
// (for example, scan user's address while verifying contact code - it prevents alert spam)
delay(1000)
}
}
checkingLink.value = false
imageProxy.close()
}
val imageAnalyzer = ImageAnalysis.Analyzer { proxy -> getQR(proxy) }
val imageAnalyzer = ImageAnalysis.Analyzer { proxy -> withApi { getQR(proxy) } }
val imageAnalysis: ImageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setImageQueueDepth(1)
@@ -115,7 +121,7 @@ actual fun QRCodeScanner(
.also { it.setAnalyzer(cameraExecutor, imageAnalyzer) }
try {
cameraProviderFuture?.get()?.unbindAll()
cameraProviderFuture?.get()?.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis)
cameraProviderFuture?.get()?.bindToLifecycle(lifecycleOwner, cameraSelector, preview.value, imageAnalysis)
} catch (e: Exception) {
Log.d(TAG, "CameraPreview: ${e.localizedMessage}")
}
@@ -13,19 +13,19 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
@Composable
fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
fun ScanCodeView(verifyCode: suspend (String?) -> Boolean, close: () -> Unit) {
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.scan_code))
QRCodeScanner { text ->
verifyCode(text) {
if (it) {
close()
} else {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.incorrect_code)
)
}
val success = verifyCode(text)
if (success) {
close()
} else {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.incorrect_code)
)
}
success
}
Text(stringResource(MR.strings.scan_code_from_contacts_app), Modifier.padding(horizontal = DEFAULT_PADDING))
SectionBottomSpacer()
@@ -35,14 +35,14 @@ fun VerifyCodeView(
displayName,
connectionCode,
connectionVerified,
verifyCode = { newCode, cb ->
withBGApi {
val res = verify(newCode)
if (res != null) {
val (verified) = res
cb(verified)
if (verified) close()
}
verifyCode = { newCode ->
val res = verify(newCode)
if (res != null) {
val (verified) = res
if (verified) close()
verified
} else {
false
}
}
)
@@ -54,7 +54,7 @@ private fun VerifyCodeLayout(
displayName: String,
connectionCode: String,
connectionVerified: Boolean,
verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit,
verifyCode: suspend (String?) -> Boolean,
) {
ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) {
AppBarTitle(stringResource(MR.strings.security_code), withPadding = false)
@@ -100,7 +100,7 @@ private fun VerifyCodeLayout(
) {
if (connectionVerified) {
SimpleButton(generalGetString(MR.strings.clear_verification), painterResource(MR.images.ic_shield)) {
verifyCode(null) {}
withApi { verifyCode(null) }
}
} else {
if (appPlatform.isAndroid) {
@@ -111,7 +111,8 @@ private fun VerifyCodeLayout(
}
}
SimpleButton(generalGetString(MR.strings.mark_code_verified), painterResource(MR.images.ic_verified_user)) {
verifyCode(connectionCode) { verified ->
withApi {
val verified = verifyCode(connectionCode)
if (!verified) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.incorrect_code)
@@ -203,7 +203,7 @@ private fun MutableState<MigrationToState?>.PasteOrScanLinkView(close: () -> Uni
if (appPlatform.isAndroid) {
SectionView(stringResource(MR.strings.scan_QR_code).replace('\n', ' ').uppercase()) {
QRCodeScanner(showQRCodeScanner = remember { mutableStateOf(true) }) { text ->
withBGApi { checkUserLink(text) }
checkUserLink(text)
}
}
SectionSpacer()
@@ -518,8 +518,8 @@ private fun ProgressView() {
DefaultProgressView(null)
}
private suspend fun MutableState<MigrationToState?>.checkUserLink(link: String) {
if (strHasSimplexFileLink(link.trim())) {
private suspend fun MutableState<MigrationToState?>.checkUserLink(link: String): Boolean {
return if (strHasSimplexFileLink(link.trim())) {
val data = MigrationFileLinkData.readFromLink(link)
val hasProxyConfigured = data?.networkConfig?.hasProxyConfigured() ?: false
val networkConfig = data?.networkConfig?.transformToPlatformSupported()
@@ -537,11 +537,13 @@ private suspend fun MutableState<MigrationToState?>.checkUserLink(link: String)
networkProxy = null
)
}
true
} else {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.invalid_file_link),
text = generalGetString(MR.strings.the_text_you_pasted_is_not_a_link)
)
false
}
}
@@ -12,6 +12,7 @@ import chat.simplex.common.platform.*
import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.*
import java.net.URI
enum class ConnectionLinkType {
@@ -26,8 +27,18 @@ suspend fun planAndConnect(
cleanup: (() -> Unit)? = null,
filterKnownContact: ((Contact) -> Unit)? = null,
filterKnownGroup: ((GroupInfo) -> Unit)? = null,
) {
val connectionPlan = chatModel.controller.apiConnectPlan(rhId, uri.toString())
): CompletableDeferred<Boolean> {
val completable = CompletableDeferred<Boolean>()
val close: (() -> Unit)? = {
close?.invoke()
// if close was called, it means the connection was created
completable.complete(true)
}
val cleanup: (() -> Unit)? = {
cleanup?.invoke()
completable.complete(!completable.isActive)
}
val connectionPlan = chatModel.controller.apiConnectPlan(rhId, uri)
if (connectionPlan != null) {
val link = strHasSingleSimplexLink(uri.trim())
val linkText = if (link?.format is Format.SimplexLink)
@@ -333,6 +344,7 @@ suspend fun planAndConnect(
)
}
}
return completable
}
suspend fun connectViaUri(
@@ -343,7 +355,7 @@ suspend fun connectViaUri(
connectionPlan: ConnectionPlan?,
close: (() -> Unit)?,
cleanup: (() -> Unit)?,
) {
): Boolean {
val pcc = chatModel.controller.apiConnect(rhId, incognito, uri)
val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION
if (pcc != null) {
@@ -363,6 +375,7 @@ suspend fun connectViaUri(
)
}
cleanup?.invoke()
return pcc != null
}
fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType {
@@ -38,8 +38,7 @@ import chat.simplex.common.views.chat.topPaddingToContent
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import java.net.URI
enum class NewChatOption {
@@ -559,15 +558,14 @@ private fun ConnectView(rhId: Long?, showQRCodeScanner: MutableState<Boolean>, p
SectionView(stringResource(MR.strings.or_scan_qr_code).uppercase(), headerBottomPadding = 5.dp) {
QRCodeScanner(showQRCodeScanner) { text ->
withBGApi {
val res = verify(rhId, text, close)
if (!res) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.invalid_qr_code),
text = generalGetString(MR.strings.code_you_scanned_is_not_simplex_link_qr_code)
)
}
val linkVerified = verifyOnly(text)
if (!linkVerified) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.invalid_qr_code),
text = generalGetString(MR.strings.code_you_scanned_is_not_simplex_link_qr_code)
)
}
verifyAndConnect(rhId, text, close)
}
}
}
@@ -656,23 +654,25 @@ private fun filteredProfiles(users: List<User>, searchTextOrPassword: String): L
}
}
private suspend fun verify(rhId: Long?, text: String?, close: () -> Unit): Boolean {
private fun verifyOnly(text: String?): Boolean = text != null && strIsSimplexLink(text)
private suspend fun verifyAndConnect(rhId: Long?, text: String?, close: () -> Unit): Boolean {
if (text != null && strIsSimplexLink(text)) {
connect(rhId, text, close)
return true
return withContext(Dispatchers.Default) {
connect(rhId, text, close)
}
}
return false
}
private suspend fun connect(rhId: Long?, link: String, close: () -> Unit, cleanup: (() -> Unit)? = null) {
private suspend fun connect(rhId: Long?, link: String, close: () -> Unit, cleanup: (() -> Unit)? = null): Boolean =
planAndConnect(
rhId,
link,
close = close,
cleanup = cleanup,
incognito = null
)
}
).await()
private fun createInvitation(
rhId: Long?,
@@ -10,5 +10,5 @@ import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
expect fun QRCodeScanner(
showQRCodeScanner: MutableState<Boolean> = remember { mutableStateOf(true) },
padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING * 2f, vertical = DEFAULT_PADDING_HALF),
onBarcode: (String) -> Unit
onBarcode: suspend (String) -> Boolean
)
@@ -40,6 +40,8 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun ConnectDesktopView(close: () -> Unit) {
@@ -233,7 +235,7 @@ private fun FoundDesktop(
SectionSpacer()
if (compatible) {
SectionItemView({ confirmKnownDesktop(sessionAddress, rc) }) {
SectionItemView({ withBGApi { confirmKnownDesktop(sessionAddress, rc) } }) {
Icon(painterResource(MR.images.ic_check), generalGetString(MR.strings.connect_button), tint = MaterialTheme.colors.secondary)
TextIconSpaced(false)
Text(generalGetString(MR.strings.connect_button))
@@ -356,7 +358,7 @@ private fun ScanDesktopAddressView(sessionAddress: MutableState<String>) {
SectionView(stringResource(MR.strings.scan_qr_code_from_desktop).uppercase()) {
QRCodeScanner { text ->
sessionAddress.value = text
processDesktopQRCode(sessionAddress, text)
connectDesktopAddress(sessionAddress, text)
}
}
}
@@ -398,7 +400,7 @@ private fun DesktopAddressView(sessionAddress: MutableState<String>) {
stringResource(MR.strings.connect_to_desktop),
disabled = sessionAddress.value.isEmpty(),
click = {
connectDesktopAddress(sessionAddress, sessionAddress.value)
withBGApi { connectDesktopAddress(sessionAddress, sessionAddress.value) }
},
)
}
@@ -461,10 +463,6 @@ private suspend fun updateRemoteCtrls(remoteCtrls: SnapshotStateList<RemoteCtrlI
}
}
private fun processDesktopQRCode(sessionAddress: MutableState<String>, resp: String) {
connectDesktopAddress(sessionAddress, resp)
}
private fun findKnownDesktop(showConnectScreen: MutableState<Boolean>) {
withBGApi {
if (controller.findKnownRemoteCtrl()) {
@@ -478,45 +476,48 @@ private fun findKnownDesktop(showConnectScreen: MutableState<Boolean>) {
}
}
private fun confirmKnownDesktop(sessionAddress: MutableState<String>, rc: RemoteCtrlInfo) {
connectDesktop(sessionAddress) {
controller.confirmRemoteCtrl(rc.remoteCtrlId)
private suspend fun confirmKnownDesktop(sessionAddress: MutableState<String>, rc: RemoteCtrlInfo): Boolean {
return withContext(Dispatchers.Default) {
connectDesktop(sessionAddress) {
controller.confirmRemoteCtrl(rc.remoteCtrlId)
}
}
}
private fun connectDesktopAddress(sessionAddress: MutableState<String>, addr: String) {
connectDesktop(sessionAddress) {
controller.connectRemoteCtrl(addr)
private suspend fun connectDesktopAddress(sessionAddress: MutableState<String>, addr: String): Boolean {
return withContext(Dispatchers.Default) {
connectDesktop(sessionAddress) {
controller.connectRemoteCtrl(addr)
}
}
}
private fun connectDesktop(sessionAddress: MutableState<String>, connect: suspend () -> Pair<SomeRemoteCtrl?, CR.ChatCmdError?>) {
withBGApi {
val res = connect()
if (res.first != null) {
val (rc_, ctrlAppInfo, v) = res.first!!
sessionAddress.value = ""
chatModel.remoteCtrlSession.value = RemoteCtrlSession(
ctrlAppInfo = ctrlAppInfo,
appVersion = v,
sessionState = UIRemoteCtrlSessionState.Connecting(remoteCtrl_ = rc_)
)
} else {
val e = res.second ?: return@withBGApi
when {
e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadInvitation -> showBadInvitationErrorAlert()
e.chatError is ChatError.ChatErrorChat && e.chatError.errorType is ChatErrorType.CommandError -> showBadInvitationErrorAlert()
e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadVersion -> showBadVersionAlert(v = e.chatError.remoteCtrlError.appVersion)
e.chatError is ChatError.ChatErrorAgent && e.chatError.agentError is AgentErrorType.RCP && e.chatError.agentError.rcpErr is RCErrorType.VERSION -> showBadVersionAlert(v = null)
e.chatError is ChatError.ChatErrorAgent && e.chatError.agentError is AgentErrorType.RCP && e.chatError.agentError.rcpErr is RCErrorType.CTRL_AUTH -> showDesktopDisconnectedErrorAlert()
else -> {
val errMsg = "${e.responseType}: ${e.details}"
Log.e(TAG, "bad response: $errMsg")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), errMsg)
}
private suspend fun connectDesktop(sessionAddress: MutableState<String>, connect: suspend () -> Pair<SomeRemoteCtrl?, CR.ChatCmdError?>): Boolean {
val res = connect()
if (res.first != null) {
val (rc_, ctrlAppInfo, v) = res.first!!
sessionAddress.value = ""
chatModel.remoteCtrlSession.value = RemoteCtrlSession(
ctrlAppInfo = ctrlAppInfo,
appVersion = v,
sessionState = UIRemoteCtrlSessionState.Connecting(remoteCtrl_ = rc_)
)
} else {
val e = res.second ?: return false
when {
e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadInvitation -> showBadInvitationErrorAlert()
e.chatError is ChatError.ChatErrorChat && e.chatError.errorType is ChatErrorType.CommandError -> showBadInvitationErrorAlert()
e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadVersion -> showBadVersionAlert(v = e.chatError.remoteCtrlError.appVersion)
e.chatError is ChatError.ChatErrorAgent && e.chatError.agentError is AgentErrorType.RCP && e.chatError.agentError.rcpErr is RCErrorType.VERSION -> showBadVersionAlert(v = null)
e.chatError is ChatError.ChatErrorAgent && e.chatError.agentError is AgentErrorType.RCP && e.chatError.agentError.rcpErr is RCErrorType.CTRL_AUTH -> showDesktopDisconnectedErrorAlert()
else -> {
val errMsg = "${e.responseType}: ${e.details}"
Log.e(TAG, "bad response: $errMsg")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), errMsg)
}
}
}
return res.first != null
}
private fun verifyDesktopSessionCode(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>, sessCode: String) {
@@ -26,6 +26,7 @@ fun ScanProtocolServerLayout(rhId: Long?, onNext: (UserServer) -> Unit) {
text = generalGetString(MR.strings.smp_servers_check_address)
)
}
res != null
}
}
}
@@ -7,7 +7,7 @@ import androidx.compose.runtime.*
actual fun QRCodeScanner(
showQRCodeScanner: MutableState<Boolean>,
padding: PaddingValues,
onBarcode: (String) -> Unit
onBarcode: suspend (String) -> Boolean
) {
//LALAL
}