Merge pull request #249 from maxmoney21m/feature/zxing-qr-scanner

Replace mlkit with zxing QR scanner
This commit is contained in:
Vitor Pamplona
2023-03-13 11:27:45 -04:00
committed by GitHub
4 changed files with 86 additions and 204 deletions

View File

@@ -130,29 +130,19 @@ dependencies {
// For QR generation // For QR generation
implementation 'com.google.zxing:core:3.5.1' implementation 'com.google.zxing:core:3.5.1'
implementation "androidx.camera:camera-camera2:1.2.1" implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
implementation 'androidx.camera:camera-lifecycle:1.2.1'
implementation 'androidx.camera:camera-view:1.2.1'
// Markdown // Markdown
implementation "com.halilibo.compose-richtext:richtext-ui:0.16.0" implementation "com.halilibo.compose-richtext:richtext-ui:0.16.0"
implementation "com.halilibo.compose-richtext:richtext-ui-material:0.16.0" implementation "com.halilibo.compose-richtext:richtext-ui-material:0.16.0"
implementation "com.halilibo.compose-richtext:richtext-commonmark:0.16.0" implementation "com.halilibo.compose-richtext:richtext-commonmark:0.16.0"
// For QR Scanning
implementation 'com.google.mlkit:vision-common:17.3.0'
// Local Barcode Scanning model
// The idea is to make it work for degoogled phones
implementation 'com.google.mlkit:barcode-scanning:17.0.3'
// Local model for language identification // Local model for language identification
implementation 'com.google.mlkit:language-id:17.0.4' implementation 'com.google.mlkit:language-id:17.0.4'
// Google services model the translate text // Google services model the translate text
implementation 'com.google.mlkit:translate:17.0.1' implementation 'com.google.mlkit:translate:17.0.1'
// Automatic memory leak detection // Automatic memory leak detection
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'

View File

@@ -23,6 +23,7 @@
android:theme="@style/Theme.Amethyst" android:theme="@style/Theme.Amethyst"
android:largeHeap="true" android:largeHeap="true"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
android:hardwareAccelerated="true"
tools:targetApi="33"> tools:targetApi="33">
<activity <activity
android:name=".ui.MainActivity" android:name=".ui.MainActivity"
@@ -46,6 +47,11 @@
android:name="android.app.lib_name" android:name="android.app.lib_name"
android:value="" /> android:value="" />
</activity> </activity>
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor"
tools:replace="screenOrientation" />
</application> </application>
</manifest> </manifest>

View File

@@ -1,156 +1,59 @@
package com.vitorpamplona.amethyst.ui.qrcode package com.vitorpamplona.amethyst.ui.qrcode
import android.Manifest
import android.content.pm.PackageManager import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.runtime.Composable
import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.DisposableEffect
import androidx.camera.core.CameraSelector import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.camera.core.ImageAnalysis import androidx.compose.ui.res.stringResource
import androidx.camera.core.ImageProxy import com.google.zxing.client.android.Intents
import androidx.camera.core.Preview import com.journeyapps.barcodescanner.ScanContract
import androidx.camera.lifecycle.ProcessCameraProvider import com.journeyapps.barcodescanner.ScanOptions
import androidx.camera.view.PreviewView import com.vitorpamplona.amethyst.R
import androidx.compose.foundation.layout.Column import com.vitorpamplona.amethyst.service.nip19.Nip19
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier @Composable
import androidx.compose.ui.platform.LocalContext fun QrCodeScanner(onScan: (String?) -> Unit) {
import androidx.compose.ui.platform.LocalLifecycleOwner val lifecycleOwner = LocalLifecycleOwner.current
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat val parseQrResult = { it: String ->
import androidx.lifecycle.LifecycleOwner try {
import com.google.mlkit.vision.barcode.BarcodeScannerOptions val nip19 = Nip19.uriToRoute(it)
import com.google.mlkit.vision.barcode.BarcodeScanning val startingPage = when (nip19?.type) {
import com.google.mlkit.vision.barcode.common.Barcode Nip19.Type.USER -> "User/${nip19.hex}"
import com.google.mlkit.vision.common.InputImage Nip19.Type.NOTE -> "Note/${nip19.hex}"
import com.vitorpamplona.amethyst.service.nip19.Nip19 else -> null
import java.util.concurrent.ExecutorService }
import java.util.concurrent.Executors
if (startingPage != null) {
@Composable onScan(startingPage)
fun QrCodeScanner(onScan: (String) -> Unit) { } else {
val context = LocalContext.current onScan(null)
val lifecycleOwner = LocalLifecycleOwner.current }
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) } } catch (e: Throwable) {
// QR can be anything, do not throw errors.
val cameraExecutor = Executors.newSingleThreadExecutor() onScan(null)
}
var hasCameraPermission by remember { }
mutableStateOf(
ContextCompat.checkSelfPermission( val qrLauncher =
context, rememberLauncherForActivityResult(ScanContract()) {
Manifest.permission.CAMERA if (it.contents != null) {
) == PackageManager.PERMISSION_GRANTED parseQrResult(it.contents)
) } else {
} onScan(null)
}
val launcher = rememberLauncherForActivityResult( }
contract = ActivityResultContracts.RequestPermission(),
onResult = { granted -> val scanOptions = ScanOptions().apply {
hasCameraPermission = granted setDesiredBarcodeFormats(ScanOptions.QR_CODE)
} setPrompt(stringResource(id = R.string.point_to_the_qr_code))
) setBeepEnabled(false)
setOrientationLocked(false)
val analyzer = QRCodeAnalyzer { result -> addExtra(Intents.Scan.SCAN_TYPE, Intents.Scan.MIXED_SCAN)
result?.let { }
try {
val nip19 = Nip19.uriToRoute(it) DisposableEffect(lifecycleOwner) {
val startingPage = when (nip19?.type) { qrLauncher.launch(scanOptions)
Nip19.Type.USER -> "User/${nip19.hex}" onDispose { }
Nip19.Type.NOTE -> "Note/${nip19.hex}" }
else -> null }
}
if (startingPage != null) {
onScan(startingPage)
}
} catch (e: Throwable) {
// QR can be anythign. do not throw errors.
}
}
}
DisposableEffect(key1 = true) {
launcher.launch(Manifest.permission.CAMERA)
onDispose() {
cameraProviderFuture.get().unbindAll()
cameraExecutor.shutdown()
}
}
Column() {
if (hasCameraPermission) {
AndroidView(
factory = { context ->
val previewView = PreviewView(context)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
bindPreview(analyzer, previewView, cameraExecutor, cameraProvider, lifecycleOwner)
}, ContextCompat.getMainExecutor(context))
return@AndroidView previewView
},
modifier = Modifier.weight(1f)
)
}
}
}
fun bindPreview(
analyzer: ImageAnalysis.Analyzer,
previewView: PreviewView,
cameraExecutor: ExecutorService,
cameraProvider: ProcessCameraProvider,
lifecycleOwner: LifecycleOwner
) {
val preview = Preview.Builder().build()
val selector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
preview.setSurfaceProvider(previewView.surfaceProvider)
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(
cameraExecutor,
analyzer
)
cameraProvider.bindToLifecycle(
lifecycleOwner,
selector,
imageAnalysis,
preview
)
}
class QRCodeAnalyzer(
private val onQrCodeScanned: (result: String?) -> Unit
) : ImageAnalysis.Analyzer {
private val scanningOptions = BarcodeScannerOptions.Builder().setBarcodeFormats(Barcode.FORMAT_QR_CODE).build()
fun scanBarcodes(inputImage: InputImage) {
BarcodeScanning.getClient(scanningOptions).process(inputImage)
.addOnSuccessListener { barcodes ->
if (barcodes.isNotEmpty()) {
onQrCodeScanned(barcodes[0].displayValue)
}
}
.addOnFailureListener {
it.printStackTrace()
}
}
@androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class)
override fun analyze(imageProxy: ImageProxy) {
imageProxy.image?.let { image ->
val inputImage = InputImage.fromMediaImage(image, imageProxy.imageInfo.rotationDegrees)
scanBarcodes(inputImage)
}
imageProxy.close()
}
}

View File

@@ -59,7 +59,9 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
.fillMaxSize() .fillMaxSize()
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth().padding(10.dp), modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -67,13 +69,17 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
} }
Column( Column(
modifier = Modifier.fillMaxSize().padding(horizontal = 10.dp), modifier = Modifier
.fillMaxSize()
.padding(horizontal = 10.dp),
verticalArrangement = Arrangement.SpaceBetween verticalArrangement = Arrangement.SpaceBetween
) { ) {
if (presenting) { if (presenting) {
Row( Row(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(horizontal = 30.dp, vertical = 10.dp) modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 30.dp, vertical = 10.dp)
) { ) {
} }
@@ -107,7 +113,9 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
Row( Row(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(horizontal = 35.dp, vertical = 10.dp) modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 35.dp, vertical = 10.dp)
) { ) {
QrCodeDrawer("nostr:${user.pubkeyNpub()}") QrCodeDrawer("nostr:${user.pubkeyNpub()}")
} }
@@ -115,7 +123,9 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
Row( Row(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(horizontal = 30.dp, vertical = 10.dp) modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 30.dp, vertical = 10.dp)
) { ) {
Button( Button(
onClick = { presenting = false }, onClick = { presenting = false },
@@ -132,38 +142,11 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
} }
} }
} else { } else {
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { QrCodeScanner {
Text( if (it.isNullOrEmpty()) {
stringResource(R.string.point_to_the_qr_code), presenting = true
modifier = Modifier.padding(top = 7.dp), } else {
fontWeight = FontWeight.Bold, onScan(it)
fontSize = 25.sp
)
}
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(30.dp)
) {
QrCodeScanner(onScan)
}
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(horizontal = 30.dp, vertical = 10.dp)
) {
Button(
onClick = { presenting = true },
shape = RoundedCornerShape(35.dp),
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
)
) {
Text(text = stringResource(R.string.show_qr))
} }
} }
} }