diff --git a/app/build.gradle b/app/build.gradle index b395bd09d..ade9ce843 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5a3417398..c22a472ac 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,6 +23,7 @@ android:theme="@style/Theme.Amethyst" android:largeHeap="true" android:usesCleartextTraffic="true" + android:hardwareAccelerated="true" tools:targetApi="33"> + + \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt index 1d3aa37d4..5be23dc40 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt @@ -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 { } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt index eb61f2ede..936d3f005 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt @@ -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) } } }