From c5e3e4439c89ed36be9dc17e94e5a66017cc705b Mon Sep 17 00:00:00 2001 From: alansley Date: Wed, 21 Aug 2024 09:35:41 +1000 Subject: [PATCH] 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 bfb2c0cdd0..2b771d53a2 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 3c7a6f6a56..8669db87e4 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 f98c725dea..bdb6716145 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 9778a1c8b8..84f316db7a 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 c58f7dc97f..fe1bf0357f 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 80eccae41a..ae4fd9a3f4 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 d0e7a7e371..91d8222de7 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