package com.peachpasswords.ui.pairing 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.Preview import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.PreviewView import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat import androidx.lifecycle.compose.LocalLifecycleOwner import com.peachpasswords.ui.theme.PeachType import com.peachpasswords.ui.theme.peachColors import java.util.concurrent.Executors @Composable internal fun PairingQrCamera( onInvitation: (String) -> Unit, modifier: Modifier = Modifier, ) { val context = LocalContext.current var granted by remember { mutableStateOf( ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED, ) } val permission = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission(), ) { granted = it } LaunchedEffect(Unit) { if (!granted) permission.launch(Manifest.permission.CAMERA) } if (!granted) { Box( modifier .fillMaxWidth() .height(250.dp) .clip(RoundedCornerShape(16.dp)) .background(peachColors.SurfaceElevated), contentAlignment = Alignment.Center, ) { Text( "Camera access is needed to scan.\nUse the manual invitation below instead.", color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 11.sp, ) } return } val lifecycleOwner = LocalLifecycleOwner.current val executor = remember { Executors.newSingleThreadExecutor() } DisposableEffect(Unit) { onDispose { executor.shutdown() } } AndroidView( modifier = modifier .fillMaxWidth() .height(250.dp) .clip(RoundedCornerShape(16.dp)), factory = { viewContext -> PreviewView(viewContext).apply { scaleType = PreviewView.ScaleType.FILL_CENTER val previewView = this val provider = ProcessCameraProvider.getInstance(viewContext) provider.addListener({ val cameraProvider = provider.get() val preview = Preview.Builder().build().also { it.setSurfaceProvider(previewView.surfaceProvider) } val analysis = ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() analysis.setAnalyzer(executor) { proxy -> try { // This runs on a bare executor thread, so anything // thrown here is an uncaught exception that kills // the process. A frame that will not decode is // normal; dropping it and waiting for the next one // is always the right answer. runCatching { PairingQrFrame.decode(proxy) } .getOrNull() ?.let(onInvitation) } finally { proxy.close() } } runCatching { cameraProvider.unbindAll() cameraProvider.bindToLifecycle( lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis, ) } }, ContextCompat.getMainExecutor(viewContext)) } }, ) }