Replaced MLKit with ZXing for QR code scanning (#1630)

* Replaced MLKit with ZXing for QR code scanning

* Adjusted some comment spacing

* Adjusted some comment phrasing

* Renamed MaybeScanQrCode to QRScannerScreen & removed double-import of ZXing core + removed ZXing android-integration

---------

Co-authored-by: alansley <aclansley@gmail.com>
This commit is contained in:
AL-Session 2024-08-21 12:10:55 +10:00 committed by GitHub
parent 70f217c030
commit ab2e3744a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 65 additions and 44 deletions

View File

@ -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'
}
@ -352,7 +350,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 +368,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

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

View File

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

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

@ -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
@ -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) {
@ -83,7 +83,7 @@ private fun Tabs(accountId: String, errors: Flow<String>, 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)
}
}
}

View File

@ -47,25 +47,27 @@ 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"
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun MaybeScanQrCode(
fun QRScannerScreen(
errors: Flow<String>,
onClickSettings: () -> Unit = LocalContext.current.run { {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
@ -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 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 */ }
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