Replaced MLKit with ZXing for QR code scanning

This commit is contained in:
alansley 2024-08-21 09:35:41 +10:00
parent 9919f716a7
commit c5e3e4439c
7 changed files with 58 additions and 35 deletions

View File

@ -352,7 +352,6 @@ dependencies {
testImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.5.2' // For Robolectric testImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.5.2' // For Robolectric
testImplementation 'app.cash.turbine:turbine:1.1.0' testImplementation 'app.cash.turbine:turbine:1.1.0'
implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.5' implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.5'
implementation "androidx.compose.ui:ui:$composeVersion" implementation "androidx.compose.ui:ui:$composeVersion"
implementation "androidx.compose.animation:animation:$composeVersion" implementation "androidx.compose.animation:animation:$composeVersion"
@ -371,7 +370,8 @@ dependencies {
implementation "androidx.camera:camera-lifecycle:1.3.2" implementation "androidx.camera:camera-lifecycle:1.3.2"
implementation "androidx.camera:camera-view: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() { static def getLastCommitTimestamp() {

View File

@ -6,6 +6,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences 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.onboarding.messagenotifications.MessageNotificationsActivity
import org.thoughtcrime.securesms.ui.setComposeContent import org.thoughtcrime.securesms.ui.setComposeContent
import org.thoughtcrime.securesms.util.start import org.thoughtcrime.securesms.util.start
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class LoadAccountActivity : BaseActionBarActivity() { class LoadAccountActivity : BaseActionBarActivity() {

View File

@ -4,6 +4,7 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow 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.InputTooShort
import org.session.libsignal.crypto.MnemonicCodec.DecodingError.InvalidWord import org.session.libsignal.crypto.MnemonicCodec.DecodingError.InvalidWord
import org.thoughtcrime.securesms.crypto.MnemonicUtilities import org.thoughtcrime.securesms.crypto.MnemonicUtilities
import javax.inject.Inject
class LoadAccountEvent(val mnemonic: ByteArray) class LoadAccountEvent(val mnemonic: ByteArray)
@ -54,6 +54,7 @@ internal class LoadAccountViewModel @Inject constructor(
} }
fun onScanQrCode(string: String) { fun onScanQrCode(string: String) {
viewModelScope.launch { viewModelScope.launch {
try { try {
codec.decodeMnemonicOrHexAsByteArray(string).let(::onSuccess) codec.decodeMnemonicOrHexAsByteArray(string).let(::onSuccess)

View File

@ -54,7 +54,7 @@ class QRCodeActivity : PassphraseRequiredActionBarActivity() {
} }
} }
fun onScan(string: String) { private fun onScan(string: String) {
if (!PublicKeyValidation.isValid(string)) { if (!PublicKeyValidation.isValid(string)) {
errors.tryEmit(getString(R.string.this_qr_code_does_not_contain_an_account_id)) errors.tryEmit(getString(R.string.this_qr_code_does_not_contain_an_account_id))
} else if (!isFinishing) { } else if (!isFinishing) {

View File

@ -47,19 +47,21 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.permissions.shouldShowRationale import com.google.accompanist.permissions.shouldShowRationale
import com.google.mlkit.vision.barcode.BarcodeScanner import com.google.zxing.BinaryBitmap
import com.google.mlkit.vision.barcode.BarcodeScannerOptions import com.google.zxing.ChecksumException
import com.google.mlkit.vision.barcode.BarcodeScanning import com.google.zxing.FormatException
import com.google.mlkit.vision.barcode.common.Barcode import com.google.zxing.NotFoundException
import com.google.mlkit.vision.common.InputImage 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.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsignal.utilities.Log 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.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.LocalType
import java.util.concurrent.Executors
private const val TAG = "NewMessageFragment" private const val TAG = "NewMessageFragment"
@ -137,17 +139,13 @@ fun ScanQrCode(errors: Flow<String>, onScan: (String) -> Unit) {
runCatching { runCatching {
cameraProvider.get().unbindAll() cameraProvider.get().unbindAll()
val options = BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()
val scanner = BarcodeScanning.getClient(options)
cameraProvider.get().bindToLifecycle( cameraProvider.get().bindToLifecycle(
LocalLifecycleOwner.current, LocalLifecycleOwner.current,
selector, selector,
preview, preview,
buildAnalysisUseCase(scanner, onScan) buildAnalysisUseCase(QRCodeReader(), onScan)
) )
}.onFailure { Log.e(TAG, "error binding camera", it) } }.onFailure { Log.e(TAG, "error binding camera", it) }
DisposableEffect(cameraProvider) { DisposableEffect(cameraProvider) {
@ -211,32 +209,51 @@ fun ScanQrCode(errors: Flow<String>, onScan: (String) -> Unit) {
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")
private fun buildAnalysisUseCase( private fun buildAnalysisUseCase(
scanner: BarcodeScanner, scanner: QRCodeReader,
onBarcodeScanned: (String) -> Unit onBarcodeScanned: (String) -> Unit
): ImageAnalysis = ImageAnalysis.Builder() ): ImageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build().apply { .build().apply {
setAnalyzer(Executors.newSingleThreadExecutor(), Analyzer(scanner, onBarcodeScanned)) setAnalyzer(Executors.newSingleThreadExecutor(), QRCodeAnalyzer(scanner, onBarcodeScanned))
} }
class Analyzer( class QRCodeAnalyzer(
private val scanner: BarcodeScanner, private val qrCodeReader: QRCodeReader,
private val onBarcodeScanned: (String) -> Unit private val onBarcodeScanned: (String) -> Unit
): ImageAnalysis.Analyzer { ): ImageAnalysis.Analyzer {
// Note: This analyze method is called once per frame of the camera feed.
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")
override fun analyze(image: ImageProxy) { override fun analyze(image: ImageProxy) {
InputImage.fromMediaImage( // Grab the image data as a byte array so we can generate a PlanarYUVLuminanceSource from it
image.image!!, val buffer = image.planes[0].buffer
image.imageInfo.rotationDegrees buffer.rewind()
).let(scanner::process).apply { val imageBytes = ByteArray(buffer.capacity())
addOnSuccessListener { barcodes -> 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!
barcodes.forEach {
it.rawValue?.let(onBarcodeScanned) // 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)
} }
addOnCompleteListener {
// 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() image.close()
} }
}
}
} }

View File

@ -6,6 +6,7 @@ import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.QRCodeWriter import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import org.session.libsignal.utilities.Log
object QRCodeUtilities { object QRCodeUtilities {
@ -34,5 +35,8 @@ object QRCodeUtilities {
} }
} }
} }
}.getOrNull() }.getOrElse {
Log.e("QRCodeUtilities", "Failed to generate QR Code", it)
null
}
} }

View File

@ -40,6 +40,7 @@ phraseVersion=1.2.0
preferenceVersion=1.2.0 preferenceVersion=1.2.0
protobufVersion=2.5.0 protobufVersion=2.5.0
testCoreVersion=1.5.0 testCoreVersion=1.5.0
zxingVersion=3.5.3
android.defaults.buildfeatures.buildconfig=true android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false android.nonTransitiveRClass=false
android.nonFinalResIds=false android.nonFinalResIds=false