diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt index df38295787..6cf432a15f 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt @@ -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, padding: PaddingValues, - onBarcode: (String) -> Unit + onBarcode: suspend (String) -> Boolean ) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current - var preview by remember { mutableStateOf(null) } - var lastAnalyzedTimeStamp = 0L - var contactLink = "" + val preview = remember { mutableStateOf(null) } + val contactLink = remember { mutableStateOf("") } + val checkingLink = remember { mutableStateOf(false) } val cameraProviderFuture by produceState?>(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 = 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}") } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt index a12a75b747..428d4b1b8f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt @@ -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() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt index 69087ecd60..e670fae5ef 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt @@ -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) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt index f4f537aab7..788c07a9d2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt @@ -203,7 +203,7 @@ private fun MutableState.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.checkUserLink(link: String) { - if (strHasSimplexFileLink(link.trim())) { +private suspend fun MutableState.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.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 } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index e8190e0767..1b5b475b35 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -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 { + val completable = CompletableDeferred() + 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 { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt index 058c82c2fe..923c0256a8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -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, 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, 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?, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.kt index 1e497e0581..f368edea1b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.kt @@ -10,5 +10,5 @@ import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF expect fun QRCodeScanner( showQRCodeScanner: MutableState = remember { mutableStateOf(true) }, padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING * 2f, vertical = DEFAULT_PADDING_HALF), - onBarcode: (String) -> Unit + onBarcode: suspend (String) -> Boolean ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt index c3eed3118e..3b6e176ca3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt @@ -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) { 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) { 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, resp: String) { - connectDesktopAddress(sessionAddress, resp) -} - private fun findKnownDesktop(showConnectScreen: MutableState) { withBGApi { if (controller.findKnownRemoteCtrl()) { @@ -478,45 +476,48 @@ private fun findKnownDesktop(showConnectScreen: MutableState) { } } -private fun confirmKnownDesktop(sessionAddress: MutableState, rc: RemoteCtrlInfo) { - connectDesktop(sessionAddress) { - controller.confirmRemoteCtrl(rc.remoteCtrlId) +private suspend fun confirmKnownDesktop(sessionAddress: MutableState, rc: RemoteCtrlInfo): Boolean { + return withContext(Dispatchers.Default) { + connectDesktop(sessionAddress) { + controller.confirmRemoteCtrl(rc.remoteCtrlId) + } } } -private fun connectDesktopAddress(sessionAddress: MutableState, addr: String) { - connectDesktop(sessionAddress) { - controller.connectRemoteCtrl(addr) +private suspend fun connectDesktopAddress(sessionAddress: MutableState, addr: String): Boolean { + return withContext(Dispatchers.Default) { + connectDesktop(sessionAddress) { + controller.connectRemoteCtrl(addr) + } } } -private fun connectDesktop(sessionAddress: MutableState, connect: suspend () -> Pair) { - 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, connect: suspend () -> Pair): 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, sessCode: String) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.kt index 56f16d4eb1..d280773976 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.kt @@ -26,6 +26,7 @@ fun ScanProtocolServerLayout(rhId: Long?, onNext: (UserServer) -> Unit) { text = generalGetString(MR.strings.smp_servers_check_address) ) } + res != null } } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.desktop.kt index 0142afb4ac..6ae35e1a41 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.desktop.kt @@ -7,7 +7,7 @@ import androidx.compose.runtime.* actual fun QRCodeScanner( showQRCodeScanner: MutableState, padding: PaddingValues, - onBarcode: (String) -> Unit + onBarcode: suspend (String) -> Boolean ) { //LALAL }