mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-26 00:25:26 +00:00
Refactor(Verification): Replace zxing with MLKit for QR scanning
This commit is contained in:
@@ -1,6 +1,16 @@
|
|||||||
package com.bitchat.android.ui
|
package com.bitchat.android.ui
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.camera.core.CameraSelector
|
||||||
|
import androidx.camera.core.ExperimentalGetImage
|
||||||
|
import androidx.camera.core.ImageAnalysis
|
||||||
|
import androidx.camera.core.ImageProxy
|
||||||
|
import androidx.camera.core.Preview
|
||||||
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
|
import androidx.camera.view.PreviewView
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
@@ -11,7 +21,6 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
@@ -32,7 +41,6 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -40,6 +48,7 @@ import androidx.compose.runtime.rememberUpdatedState
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -53,19 +62,22 @@ import androidx.compose.ui.unit.sp
|
|||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.core.graphics.createBitmap
|
import androidx.core.graphics.createBitmap
|
||||||
import androidx.core.graphics.set
|
import androidx.core.graphics.set
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.bitchat.android.R
|
import com.bitchat.android.R
|
||||||
import com.bitchat.android.services.VerificationService
|
import com.bitchat.android.services.VerificationService
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
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.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.BarcodeFormat
|
import com.google.zxing.BarcodeFormat
|
||||||
import com.google.zxing.common.BitMatrix
|
import com.google.zxing.common.BitMatrix
|
||||||
import com.google.zxing.qrcode.QRCodeWriter
|
import com.google.zxing.qrcode.QRCodeWriter
|
||||||
import com.journeyapps.barcodescanner.BarcodeCallback
|
import java.util.concurrent.ExecutorService
|
||||||
import com.journeyapps.barcodescanner.BarcodeResult
|
import java.util.concurrent.Executors
|
||||||
import com.journeyapps.barcodescanner.DecoratedBarcodeView
|
|
||||||
import com.journeyapps.barcodescanner.DefaultDecoderFactory
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -123,8 +135,9 @@ fun VerificationSheet(
|
|||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
if (showingScanner) {
|
if (showingScanner) {
|
||||||
VerificationScanner(
|
QRScannerPanel(
|
||||||
accent = accent,
|
accent = accent,
|
||||||
|
boxColor = boxColor,
|
||||||
onScan = { code ->
|
onScan = { code ->
|
||||||
val qr = VerificationService.verifyScannedQR(code)
|
val qr = VerificationService.verifyScannedQR(code)
|
||||||
if (qr != null && viewModel.beginQRVerification(qr)) {
|
if (qr != null && viewModel.beginQRVerification(qr)) {
|
||||||
@@ -215,22 +228,6 @@ private fun MyQrPanel(
|
|||||||
QRCodeCard(qrString = qrString, boxColor = boxColor)
|
QRCodeCard(qrString = qrString, boxColor = boxColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun VerificationScanner(
|
|
||||||
accent: Color,
|
|
||||||
onScan: (String) -> Unit
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.verify_scan_prompt_friend),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
color = accent,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
QRScannerPanel(onCode = onScan)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ToggleVerificationModeButton(
|
private fun ToggleVerificationModeButton(
|
||||||
showingScanner: Boolean,
|
showingScanner: Boolean,
|
||||||
@@ -350,56 +347,87 @@ private fun bitmapFromMatrix(matrix: BitMatrix): Bitmap {
|
|||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun QRScannerPanel(
|
private fun QRScannerPanel(
|
||||||
onCode: (String) -> Unit,
|
onScan: (String) -> Unit,
|
||||||
|
accent: Color,
|
||||||
|
boxColor: Color,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val permissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
|
val permissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
var lastValid by remember { mutableStateOf<String?>(null) }
|
var lastValid by remember { mutableStateOf<String?>(null) }
|
||||||
|
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
|
||||||
|
val cameraExecutor: ExecutorService = remember { Executors.newSingleThreadExecutor() }
|
||||||
|
val barcodeView = remember { PreviewView(context) }
|
||||||
|
val mainHandler = remember { Handler(Looper.getMainLooper()) }
|
||||||
|
|
||||||
val onCodeState = rememberUpdatedState(onCode)
|
val onCodeState = rememberUpdatedState(onScan)
|
||||||
val barcodeCallback = remember {
|
val analyzer = remember {
|
||||||
object : BarcodeCallback {
|
QRCodeAnalyzer { text ->
|
||||||
override fun barcodeResult(result: BarcodeResult?) {
|
mainHandler.post {
|
||||||
val text = result?.text ?: return
|
if (text == lastValid) return@post
|
||||||
if (text == lastValid) return
|
|
||||||
lastValid = text
|
lastValid = text
|
||||||
onCodeState.value(text)
|
onCodeState.value(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val barcodeView = remember {
|
|
||||||
DecoratedBarcodeView(context).apply {
|
|
||||||
barcodeView.decoderFactory = DefaultDecoderFactory(listOf(BarcodeFormat.QR_CODE))
|
|
||||||
setStatusText("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
barcodeView.decodeContinuous(barcodeCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(permissionState.status.isGranted) {
|
DisposableEffect(permissionState.status.isGranted) {
|
||||||
if (permissionState.status.isGranted) {
|
if (permissionState.status.isGranted) {
|
||||||
barcodeView.resume()
|
val cameraProvider = cameraProviderFuture.get()
|
||||||
} else {
|
val preview = Preview.Builder()
|
||||||
barcodeView.pause()
|
.build()
|
||||||
|
.also { it.surfaceProvider = barcodeView.surfaceProvider }
|
||||||
|
val analysis = ImageAnalysis.Builder()
|
||||||
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||||
|
.build()
|
||||||
|
.also { it.setAnalyzer(cameraExecutor, analyzer) }
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
cameraProvider.unbindAll()
|
||||||
|
cameraProvider.bindToLifecycle(
|
||||||
|
lifecycleOwner,
|
||||||
|
CameraSelector.DEFAULT_BACK_CAMERA,
|
||||||
|
preview,
|
||||||
|
analysis
|
||||||
|
)
|
||||||
|
}.onFailure {
|
||||||
|
Log.w("VerificationSheet", "Failed to bind camera: ${it.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onDispose { barcodeView.pause() }
|
|
||||||
|
onDispose {
|
||||||
|
runCatching { cameraProviderFuture.get().unbindAll() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose { cameraExecutor.shutdown() }
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
.fillMaxWidth()
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
.background(boxColor, RoundedCornerShape(12.dp))
|
||||||
|
.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.verify_scan_prompt_friend),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
color = accent,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
if (permissionState.status.isGranted) {
|
if (permissionState.status.isGranted) {
|
||||||
AndroidView(
|
AndroidView(
|
||||||
factory = { barcodeView },
|
factory = { barcodeView },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.size(220.dp)
|
||||||
.height(260.dp)
|
.clipToBounds()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
@@ -424,3 +452,28 @@ private fun QRScannerPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class QRCodeAnalyzer(
|
||||||
|
private val onCode: (String) -> Unit
|
||||||
|
) : ImageAnalysis.Analyzer {
|
||||||
|
private val scanner = BarcodeScanning.getClient(
|
||||||
|
BarcodeScannerOptions.Builder()
|
||||||
|
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
@ExperimentalGetImage
|
||||||
|
override fun analyze(imageProxy: ImageProxy) {
|
||||||
|
val mediaImage = imageProxy.image ?: run {
|
||||||
|
imageProxy.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val input = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
|
||||||
|
scanner.process(input)
|
||||||
|
.addOnSuccessListener { barcodes ->
|
||||||
|
val text = barcodes.firstOrNull()?.rawValue
|
||||||
|
if (!text.isNullOrBlank()) onCode(text)
|
||||||
|
}
|
||||||
|
.addOnCompleteListener { imageProxy.close() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user