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

Replace mlkit with zxing QR scanner
pull/268/head
Vitor Pamplona 2023-03-13 11:27:45 -04:00 zatwierdzone przez GitHub
commit 27b2cda14f
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
4 zmienionych plików z 86 dodań i 204 usunięć

Wyświetl plik

@ -130,29 +130,19 @@ dependencies {
// For QR generation
implementation 'com.google.zxing:core:3.5.1'
implementation "androidx.camera:camera-camera2:1.2.1"
implementation 'androidx.camera:camera-lifecycle:1.2.1'
implementation 'androidx.camera:camera-view:1.2.1'
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
// Markdown
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-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
implementation 'com.google.mlkit:language-id:17.0.4'
// Google services model the translate text
implementation 'com.google.mlkit:translate:17.0.1'
// Automatic memory leak detection
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'

Wyświetl plik

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

Wyświetl plik

@ -1,156 +1,59 @@
package com.vitorpamplona.amethyst.ui.qrcode
import android.Manifest
import android.content.pm.PackageManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.camera.core.CameraSelector
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.layout.Column
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
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.vitorpamplona.amethyst.service.nip19.Nip19
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@Composable
fun QrCodeScanner(onScan: (String) -> Unit) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
val cameraExecutor = Executors.newSingleThreadExecutor()
var hasCameraPermission by remember {
mutableStateOf(
ContextCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
)
}
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
onResult = { granted ->
hasCameraPermission = granted
}
)
val analyzer = QRCodeAnalyzer { result ->
result?.let {
try {
val nip19 = Nip19.uriToRoute(it)
val startingPage = when (nip19?.type) {
Nip19.Type.USER -> "User/${nip19.hex}"
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()
}
}
package com.vitorpamplona.amethyst.ui.qrcode
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import com.google.zxing.client.android.Intents
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.nip19.Nip19
@Composable
fun QrCodeScanner(onScan: (String?) -> Unit) {
val lifecycleOwner = LocalLifecycleOwner.current
val parseQrResult = { it: String ->
try {
val nip19 = Nip19.uriToRoute(it)
val startingPage = when (nip19?.type) {
Nip19.Type.USER -> "User/${nip19.hex}"
Nip19.Type.NOTE -> "Note/${nip19.hex}"
else -> null
}
if (startingPage != null) {
onScan(startingPage)
} else {
onScan(null)
}
} catch (e: Throwable) {
// QR can be anything, do not throw errors.
onScan(null)
}
}
val qrLauncher =
rememberLauncherForActivityResult(ScanContract()) {
if (it.contents != null) {
parseQrResult(it.contents)
} else {
onScan(null)
}
}
val scanOptions = ScanOptions().apply {
setDesiredBarcodeFormats(ScanOptions.QR_CODE)
setPrompt(stringResource(id = R.string.point_to_the_qr_code))
setBeepEnabled(false)
setOrientationLocked(false)
addExtra(Intents.Scan.SCAN_TYPE, Intents.Scan.MIXED_SCAN)
}
DisposableEffect(lifecycleOwner) {
qrLauncher.launch(scanOptions)
onDispose { }
}
}

Wyświetl plik

@ -59,7 +59,9 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
.fillMaxSize()
) {
Row(
modifier = Modifier.fillMaxWidth().padding(10.dp),
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@ -67,13 +69,17 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
}
Column(
modifier = Modifier.fillMaxSize().padding(horizontal = 10.dp),
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 10.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
if (presenting) {
Row(
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(
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()}")
}
@ -115,7 +123,9 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(horizontal = 30.dp, vertical = 10.dp)
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 30.dp, vertical = 10.dp)
) {
Button(
onClick = { presenting = false },
@ -132,38 +142,11 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
}
}
} else {
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
Text(
stringResource(R.string.point_to_the_qr_code),
modifier = Modifier.padding(top = 7.dp),
fontWeight = FontWeight.Bold,
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))
QrCodeScanner {
if (it.isNullOrEmpty()) {
presenting = true
} else {
onScan(it)
}
}
}