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 '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() {

View File

@ -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() {

View File

@ -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)

View File

@ -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) {

View File

@ -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<String>, 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<String>, 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()
}
}

View File

@ -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
}
}

View File

@ -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