PairingQrCamera.kt

4.9 KB · raw

1package com.peachpasswords.ui.pairing23import android.Manifest4import android.content.pm.PackageManager5import androidx.activity.compose.rememberLauncherForActivityResult6import androidx.activity.result.contract.ActivityResultContracts7import androidx.camera.core.CameraSelector8import androidx.camera.core.ImageAnalysis9import androidx.camera.core.Preview10import androidx.camera.lifecycle.ProcessCameraProvider11import androidx.camera.view.PreviewView12import androidx.compose.foundation.background13import androidx.compose.foundation.layout.Box14import androidx.compose.foundation.layout.fillMaxSize15import androidx.compose.foundation.layout.fillMaxWidth16import androidx.compose.foundation.layout.height17import androidx.compose.foundation.shape.RoundedCornerShape18import androidx.compose.material3.Text19import androidx.compose.runtime.Composable20import androidx.compose.runtime.DisposableEffect21import androidx.compose.runtime.LaunchedEffect22import androidx.compose.runtime.getValue23import androidx.compose.runtime.mutableStateOf24import androidx.compose.runtime.remember25import androidx.compose.runtime.setValue26import androidx.compose.ui.Alignment27import androidx.compose.ui.Modifier28import androidx.compose.ui.draw.clip29import androidx.compose.ui.platform.LocalContext30import androidx.compose.ui.unit.dp31import androidx.compose.ui.unit.sp32import androidx.compose.ui.viewinterop.AndroidView33import androidx.core.content.ContextCompat34import androidx.lifecycle.compose.LocalLifecycleOwner35import com.peachpasswords.ui.theme.PeachType36import com.peachpasswords.ui.theme.peachColors37import java.util.concurrent.Executors3839@Composable40internal fun PairingQrCamera(41    onInvitation: (String) -> Unit,42    modifier: Modifier = Modifier,43) {44    val context = LocalContext.current45    var granted by remember {46        mutableStateOf(47            ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==48                PackageManager.PERMISSION_GRANTED,49        )50    }51    val permission = rememberLauncherForActivityResult(52        ActivityResultContracts.RequestPermission(),53    ) { granted = it }54    LaunchedEffect(Unit) {55        if (!granted) permission.launch(Manifest.permission.CAMERA)56    }5758    if (!granted) {59        Box(60            modifier61                .fillMaxWidth()62                .height(250.dp)63                .clip(RoundedCornerShape(16.dp))64                .background(peachColors.SurfaceElevated),65            contentAlignment = Alignment.Center,66        ) {67            Text(68                "Camera access is needed to scan.\nUse the manual invitation below instead.",69                color = peachColors.T2,70                fontFamily = PeachType.Mono,71                fontSize = 11.sp,72            )73        }74        return75    }7677    val lifecycleOwner = LocalLifecycleOwner.current78    val executor = remember { Executors.newSingleThreadExecutor() }79    DisposableEffect(Unit) {80        onDispose { executor.shutdown() }81    }82    AndroidView(83        modifier = modifier84            .fillMaxWidth()85            .height(250.dp)86            .clip(RoundedCornerShape(16.dp)),87        factory = { viewContext ->88            PreviewView(viewContext).apply {89                scaleType = PreviewView.ScaleType.FILL_CENTER90                val previewView = this91                val provider = ProcessCameraProvider.getInstance(viewContext)92                provider.addListener({93                    val cameraProvider = provider.get()94                    val preview = Preview.Builder().build().also {95                        it.setSurfaceProvider(previewView.surfaceProvider)96                    }97                    val analysis = ImageAnalysis.Builder()98                        .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)99                        .build()100                    analysis.setAnalyzer(executor) { proxy ->101                        try {102                            // This runs on a bare executor thread, so anything103                            // thrown here is an uncaught exception that kills104                            // the process. A frame that will not decode is105                            // normal; dropping it and waiting for the next one106                            // is always the right answer.107                            runCatching { PairingQrFrame.decode(proxy) }108                                .getOrNull()109                                ?.let(onInvitation)110                        } finally {111                            proxy.close()112                        }113                    }114                    runCatching {115                        cameraProvider.unbindAll()116                        cameraProvider.bindToLifecycle(117                            lifecycleOwner,118                            CameraSelector.DEFAULT_BACK_CAMERA,119                            preview,120                            analysis,121                        )122                    }123                }, ContextCompat.getMainExecutor(viewContext))124            }125        },126    )127}