From c5e3e4439c89ed36be9dc17e94e5a66017cc705b Mon Sep 17 00:00:00 2001 From: alansley Date: Wed, 21 Aug 2024 09:35:41 +1000 Subject: [PATCH 1/4] Replaced MLKit with ZXing for QR code scanning --- app/build.gradle | 4 +- .../loadaccount/LoadAccountActivity.kt | 2 +- .../loadaccount/LoadAccountViewModel.kt | 3 +- .../securesms/preferences/QRCodeActivity.kt | 2 +- .../securesms/ui/components/QR.kt | 75 ++++++++++++------- .../securesms/util/QRCodeUtilities.kt | 6 +- gradle.properties | 1 + 7 files changed, 58 insertions(+), 35 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bfb2c0cdd03..2b771d53a2b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -352,7 +352,6 @@ dependencies { testImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.5.2' // For Robolectric testImplementation 'app.cash.turbine:turbine:1.1.0' - implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.5' implementation "androidx.compose.ui:ui:$composeVersion" implementation "androidx.compose.animation:animation:$composeVersion" @@ -371,7 +370,8 @@ dependencies { implementation "androidx.camera:camera-lifecycle:1.3.2" implementation "androidx.camera:camera-view:1.3.2" - implementation "com.google.mlkit:barcode-scanning:17.2.0" + // Note: ZXing 3.5.3 is the latest stable release as of 2024/08/21 + implementation "com.google.zxing:core:$zxingVersion" } static def getLastCommitTimestamp() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccountActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccountActivity.kt index 3c7a6f6a56c..8669db87e4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccountActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccountActivity.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.launch import network.loki.messenger.R import org.session.libsession.utilities.TextSecurePreferences @@ -14,7 +15,6 @@ import org.thoughtcrime.securesms.onboarding.manager.LoadAccountManager import org.thoughtcrime.securesms.onboarding.messagenotifications.MessageNotificationsActivity import org.thoughtcrime.securesms.ui.setComposeContent import org.thoughtcrime.securesms.util.start -import javax.inject.Inject @AndroidEntryPoint class LoadAccountActivity : BaseActionBarActivity() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccountViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccountViewModel.kt index f98c725deac..bdb67161458 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccountViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccountViewModel.kt @@ -4,6 +4,7 @@ import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -17,7 +18,6 @@ import org.session.libsignal.crypto.MnemonicCodec import org.session.libsignal.crypto.MnemonicCodec.DecodingError.InputTooShort import org.session.libsignal.crypto.MnemonicCodec.DecodingError.InvalidWord import org.thoughtcrime.securesms.crypto.MnemonicUtilities -import javax.inject.Inject class LoadAccountEvent(val mnemonic: ByteArray) @@ -54,6 +54,7 @@ internal class LoadAccountViewModel @Inject constructor( } fun onScanQrCode(string: String) { + viewModelScope.launch { try { codec.decodeMnemonicOrHexAsByteArray(string).let(::onSuccess) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt index 9778a1c8b8c..84f316db7ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt @@ -54,7 +54,7 @@ class QRCodeActivity : PassphraseRequiredActionBarActivity() { } } - fun onScan(string: String) { + private fun onScan(string: String) { if (!PublicKeyValidation.isValid(string)) { errors.tryEmit(getString(R.string.this_qr_code_does_not_contain_an_account_id)) } else if (!isFinishing) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt index c58f7dc97ff..fe1bf0357f4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt @@ -47,19 +47,21 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.shouldShowRationale -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 com.google.zxing.BinaryBitmap +import com.google.zxing.ChecksumException +import com.google.zxing.FormatException +import com.google.zxing.NotFoundException +import com.google.zxing.PlanarYUVLuminanceSource +import com.google.zxing.Result +import com.google.zxing.common.HybridBinarizer +import com.google.zxing.qrcode.QRCodeReader +import java.util.concurrent.Executors import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import network.loki.messenger.R import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType -import java.util.concurrent.Executors private const val TAG = "NewMessageFragment" @@ -137,17 +139,13 @@ fun ScanQrCode(errors: Flow, onScan: (String) -> Unit) { runCatching { cameraProvider.get().unbindAll() - val options = BarcodeScannerOptions.Builder() - .setBarcodeFormats(Barcode.FORMAT_QR_CODE) - .build() - val scanner = BarcodeScanning.getClient(options) - cameraProvider.get().bindToLifecycle( LocalLifecycleOwner.current, selector, preview, - buildAnalysisUseCase(scanner, onScan) + buildAnalysisUseCase(QRCodeReader(), onScan) ) + }.onFailure { Log.e(TAG, "error binding camera", it) } DisposableEffect(cameraProvider) { @@ -211,32 +209,51 @@ fun ScanQrCode(errors: Flow, onScan: (String) -> Unit) { @SuppressLint("UnsafeOptInUsageError") private fun buildAnalysisUseCase( - scanner: BarcodeScanner, + scanner: QRCodeReader, onBarcodeScanned: (String) -> Unit ): ImageAnalysis = ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build().apply { - setAnalyzer(Executors.newSingleThreadExecutor(), Analyzer(scanner, onBarcodeScanned)) + setAnalyzer(Executors.newSingleThreadExecutor(), QRCodeAnalyzer(scanner, onBarcodeScanned)) } -class Analyzer( - private val scanner: BarcodeScanner, +class QRCodeAnalyzer( + private val qrCodeReader: QRCodeReader, private val onBarcodeScanned: (String) -> Unit ): ImageAnalysis.Analyzer { + + // Note: This analyze method is called once per frame of the camera feed. @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() - } + // Grab the image data as a byte array so we can generate a PlanarYUVLuminanceSource from it + val buffer = image.planes[0].buffer + buffer.rewind() + val imageBytes = ByteArray(buffer.capacity()) + buffer.get(imageBytes) // IMPORTANT: This transfers data from the buffer INTO the imageBytes array, although it looks like it would go the other way around! + + // ZXing requires data as a BinaryBitmap to scan for QR codes, and to generate that we need to feed it a PlanarYUVLuminanceSource + val luminanceSource = PlanarYUVLuminanceSource(imageBytes, image.width, image.height, 0, 0, image.width, image.height, false) + val binaryBitmap = BinaryBitmap(HybridBinarizer(luminanceSource)) + + // Attempt to extract a QR code from the binary bitmap, and pass it through to our `onBarcodeScanned` method if we find one + try { + val result: Result = qrCodeReader.decode(binaryBitmap) + val resultTxt = result.text + // No need to close the image here - it'll always make it to the end, and calling + // `onBarcodeScanned` with a valid recovery code will stop calling this `analyze` method. + onBarcodeScanned(resultTxt) + } + catch (nfe: NotFoundException) { /* Hits if there is no QR code in the image */ } + catch (fe: FormatException) { /* Hits if we found a QR code but failed to decode it */ } + catch (ce: ChecksumException) { /* Hits if we found a QR code which is corrupted */ } + catch (e: Exception) { + // Hits if there's a genuine problem + Log.e("QR", "error", e) } + + // Remember to close the image when we're done with it! + // IMPORTANT: It is CLOSING the image that allows this method to run again! If we don't + // close the image this method runs precisely ONCE and that's it, which is essentially useless. + image.close() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/QRCodeUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/QRCodeUtilities.kt index 80eccae41aa..ae4fd9a3f45 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/QRCodeUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/QRCodeUtilities.kt @@ -6,6 +6,7 @@ import com.google.zxing.BarcodeFormat import com.google.zxing.EncodeHintType import com.google.zxing.qrcode.QRCodeWriter import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import org.session.libsignal.utilities.Log object QRCodeUtilities { @@ -34,5 +35,8 @@ object QRCodeUtilities { } } } - }.getOrNull() + }.getOrElse { + Log.e("QRCodeUtilities", "Failed to generate QR Code", it) + null + } } diff --git a/gradle.properties b/gradle.properties index d0e7a7e3719..91d8222de72 100644 --- a/gradle.properties +++ b/gradle.properties @@ -40,6 +40,7 @@ phraseVersion=1.2.0 preferenceVersion=1.2.0 protobufVersion=2.5.0 testCoreVersion=1.5.0 +zxingVersion=3.5.3 android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false android.nonFinalResIds=false From fb5673161c854b3c0f6d286265f81ad7d453a48f Mon Sep 17 00:00:00 2001 From: alansley Date: Wed, 21 Aug 2024 10:59:09 +1000 Subject: [PATCH 2/4] Adjusted some comment spacing --- .../java/org/thoughtcrime/securesms/ui/components/QR.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt index fe1bf0357f4..92378c3b31c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt @@ -243,9 +243,9 @@ class QRCodeAnalyzer( // `onBarcodeScanned` with a valid recovery code will stop calling this `analyze` method. onBarcodeScanned(resultTxt) } - catch (nfe: NotFoundException) { /* Hits if there is no QR code in the image */ } - catch (fe: FormatException) { /* Hits if we found a QR code but failed to decode it */ } - catch (ce: ChecksumException) { /* Hits if we found a QR code which is corrupted */ } + catch (nfe: NotFoundException) { /* Hits if there is no QR code in the image */ } + catch (fe: FormatException) { /* Hits if we found a QR code but failed to decode it */ } + catch (ce: ChecksumException) { /* Hits if we found a QR code which is corrupted */ } catch (e: Exception) { // Hits if there's a genuine problem Log.e("QR", "error", e) From a0028614f18cf265a5a244d81f7d89bf6efc74b6 Mon Sep 17 00:00:00 2001 From: alansley Date: Wed, 21 Aug 2024 11:14:32 +1000 Subject: [PATCH 3/4] Adjusted some comment phrasing --- .../main/java/org/thoughtcrime/securesms/ui/components/QR.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt index 92378c3b31c..d0fd4ac3e9f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt @@ -239,8 +239,8 @@ class QRCodeAnalyzer( try { val result: Result = qrCodeReader.decode(binaryBitmap) val resultTxt = result.text - // No need to close the image here - it'll always make it to the end, and calling - // `onBarcodeScanned` with a valid recovery code will stop calling this `analyze` method. + // No need to close the image here - it'll always make it to the end, and calling `onBarcodeScanned` + // with a valid contact / recovery phrase / community code will stop calling this `analyze` method. onBarcodeScanned(resultTxt) } catch (nfe: NotFoundException) { /* Hits if there is no QR code in the image */ } From 5c39404db5b2256f38e79b839cb90d078f9457ef Mon Sep 17 00:00:00 2001 From: alansley Date: Wed, 21 Aug 2024 12:06:54 +1000 Subject: [PATCH 4/4] Renamed MaybeScanQrCode to QRScannerScreen & removed double-import of ZXing core + removed ZXing android-integration --- app/build.gradle | 2 -- .../securesms/conversation/start/newmessage/NewMessage.kt | 4 ++-- .../securesms/onboarding/loadaccount/LoadAccount.kt | 4 ++-- .../org/thoughtcrime/securesms/preferences/QRCodeActivity.kt | 4 ++-- .../main/java/org/thoughtcrime/securesms/ui/components/QR.kt | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2b771d53a2b..36cdee2a3ff 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -274,8 +274,6 @@ dependencies { implementation 'pl.tajchert:waitingdots:0.1.0' implementation 'com.vanniktech:android-image-cropper:4.5.0' implementation 'com.melnykov:floatingactionbutton:1.3.0' - implementation 'com.google.zxing:android-integration:3.1.0' - implementation 'com.google.zxing:core:3.2.1' implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') { exclude group: 'com.android.support', module: 'support-annotations' } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt index a2cb95a6b46..df54f9cae86 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt @@ -48,7 +48,7 @@ import org.thoughtcrime.securesms.ui.LoadingArcOr import org.thoughtcrime.securesms.ui.components.AppBarCloseIcon import org.thoughtcrime.securesms.ui.components.BackAppBar import org.thoughtcrime.securesms.ui.components.BorderlessButtonWithIcon -import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode +import org.thoughtcrime.securesms.ui.components.QRScannerScreen import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField import org.thoughtcrime.securesms.ui.components.SessionTabRow @@ -89,7 +89,7 @@ internal fun NewMessage( HorizontalPager(pagerState) { when (TITLES[it]) { R.string.enter_account_id -> EnterAccountId(state, callbacks, onHelp) - R.string.qrScan -> MaybeScanQrCode(qrErrors, onScan = callbacks::onScanQrCode) + R.string.qrScan -> QRScannerScreen(qrErrors, onScan = callbacks::onScanQrCode) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccount.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccount.kt index cc1033ee964..56d1c54ea47 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccount.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccount.kt @@ -26,7 +26,7 @@ import network.loki.messenger.R import org.thoughtcrime.securesms.onboarding.ui.ContinuePrimaryOutlineButton import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.PreviewTheme -import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode +import org.thoughtcrime.securesms.ui.components.QRScannerScreen import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField import org.thoughtcrime.securesms.ui.components.SessionTabRow import org.thoughtcrime.securesms.ui.theme.LocalType @@ -52,7 +52,7 @@ internal fun LoadAccountScreen( ) { page -> when (TITLES[page]) { R.string.sessionRecoveryPassword -> RecoveryPassword(state, onChange, onContinue) - R.string.qrScan -> MaybeScanQrCode(qrErrors, onScan = onScan) + R.string.qrScan -> QRScannerScreen(qrErrors, onScan = onScan) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt index 84f316db7ad..52cb345fab0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt @@ -27,7 +27,7 @@ import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.database.threadDatabase import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalColors -import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode +import org.thoughtcrime.securesms.ui.components.QRScannerScreen import org.thoughtcrime.securesms.ui.components.QrImage import org.thoughtcrime.securesms.ui.components.SessionTabRow import org.thoughtcrime.securesms.ui.contentDescription @@ -83,7 +83,7 @@ private fun Tabs(accountId: String, errors: Flow, onScan: (String) -> Un ) { page -> when (TITLES[page]) { R.string.view -> QrPage(accountId) - R.string.scan -> MaybeScanQrCode(errors, onScan = onScan) + R.string.scan -> QRScannerScreen(errors, onScan = onScan) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt index d0fd4ac3e9f..9661b3bc06a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt @@ -67,7 +67,7 @@ private const val TAG = "NewMessageFragment" @OptIn(ExperimentalPermissionsApi::class) @Composable -fun MaybeScanQrCode( +fun QRScannerScreen( errors: Flow, onClickSettings: () -> Unit = LocalContext.current.run { { Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {