mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-21 15:05:19 +00:00
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:
parent
70f217c030
commit
ab2e3744a9
@ -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() {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user