|
| 1 | +package com.sameerasw.airsync.presentation.ui.activities |
| 2 | + |
| 3 | +import android.Manifest |
| 4 | +import android.annotation.SuppressLint |
| 5 | +import android.content.Intent |
| 6 | +import android.content.pm.PackageManager |
| 7 | +import android.os.Bundle |
| 8 | +import android.util.Log |
| 9 | +import androidx.activity.ComponentActivity |
| 10 | +import androidx.activity.compose.setContent |
| 11 | +import androidx.activity.result.contract.ActivityResultContracts |
| 12 | +import androidx.camera.core.CameraSelector |
| 13 | +import androidx.camera.core.ImageAnalysis |
| 14 | +import androidx.camera.core.ImageProxy |
| 15 | +import androidx.camera.lifecycle.ProcessCameraProvider |
| 16 | +import androidx.camera.view.PreviewView |
| 17 | +import androidx.compose.foundation.background |
| 18 | +import androidx.compose.foundation.layout.* |
| 19 | +import androidx.compose.material.icons.Icons |
| 20 | +import androidx.compose.material.icons.automirrored.filled.ArrowBack |
| 21 | +import androidx.compose.material3.* |
| 22 | +import androidx.compose.runtime.* |
| 23 | +import androidx.compose.ui.Alignment |
| 24 | +import androidx.compose.ui.Modifier |
| 25 | +import androidx.compose.ui.graphics.Color |
| 26 | +import androidx.compose.ui.text.style.TextAlign |
| 27 | +import androidx.compose.ui.unit.dp |
| 28 | +import androidx.compose.ui.viewinterop.AndroidView |
| 29 | +import androidx.compose.foundation.Canvas |
| 30 | +import androidx.core.content.ContextCompat |
| 31 | +import com.google.mlkit.vision.barcode.BarcodeScannerOptions |
| 32 | +import com.google.mlkit.vision.barcode.BarcodeScanning |
| 33 | +import com.google.mlkit.vision.common.InputImage |
| 34 | +import com.sameerasw.airsync.ui.theme.AirSyncTheme |
| 35 | +import java.util.concurrent.Executors |
| 36 | + |
| 37 | +class QRScannerActivity : ComponentActivity() { |
| 38 | + |
| 39 | + private val cameraPermissionLauncher = registerForActivityResult( |
| 40 | + ActivityResultContracts.RequestPermission() |
| 41 | + ) { isGranted -> |
| 42 | + if (!isGranted) { |
| 43 | + // Permission denied, finish the activity |
| 44 | + setResult(RESULT_CANCELED) |
| 45 | + finish() |
| 46 | + } |
| 47 | + } |
| 48 | + |
| 49 | + override fun onCreate(savedInstanceState: Bundle?) { |
| 50 | + super.onCreate(savedInstanceState) |
| 51 | + |
| 52 | + // Check camera permission |
| 53 | + if (ContextCompat.checkSelfPermission( |
| 54 | + this, |
| 55 | + Manifest.permission.CAMERA |
| 56 | + ) != PackageManager.PERMISSION_GRANTED |
| 57 | + ) { |
| 58 | + cameraPermissionLauncher.launch(Manifest.permission.CAMERA) |
| 59 | + return |
| 60 | + } |
| 61 | + |
| 62 | + setContent { |
| 63 | + AirSyncTheme { |
| 64 | + QRScannerScreen( |
| 65 | + onQrScanned = { qrData -> |
| 66 | + // Return the scanned QR code data to the caller |
| 67 | + val intent = Intent().apply { |
| 68 | + putExtra("QR_CODE", qrData) |
| 69 | + } |
| 70 | + setResult(RESULT_OK, intent) |
| 71 | + finish() |
| 72 | + }, |
| 73 | + onClosed = { |
| 74 | + setResult(RESULT_CANCELED) |
| 75 | + finish() |
| 76 | + } |
| 77 | + ) |
| 78 | + } |
| 79 | + } |
| 80 | + } |
| 81 | +} |
| 82 | + |
| 83 | +@SuppressLint("UnsafeOptInUsageError") |
| 84 | +@Composable |
| 85 | +private fun QRScannerScreen( |
| 86 | + onQrScanned: (String) -> Unit, |
| 87 | + onClosed: () -> Unit |
| 88 | +) { |
| 89 | + var scanned by remember { mutableStateOf(false) } |
| 90 | + var lastScannedCode by remember { mutableStateOf("") } |
| 91 | + var scanMessage by remember { mutableStateOf("") } |
| 92 | + |
| 93 | + Scaffold( |
| 94 | + modifier = Modifier.fillMaxSize(), |
| 95 | + topBar = { |
| 96 | + @OptIn(ExperimentalMaterial3Api::class) |
| 97 | + TopAppBar( |
| 98 | + title = { Text("Scan QR Code") }, |
| 99 | + navigationIcon = { |
| 100 | + IconButton(onClick = onClosed) { |
| 101 | + Icon( |
| 102 | + Icons.AutoMirrored.Filled.ArrowBack, |
| 103 | + contentDescription = "Back" |
| 104 | + ) |
| 105 | + } |
| 106 | + } |
| 107 | + ) |
| 108 | + } |
| 109 | + ) { innerPadding -> |
| 110 | + Box( |
| 111 | + modifier = Modifier |
| 112 | + .fillMaxSize() |
| 113 | + .padding(innerPadding) |
| 114 | + .background(Color.Black) |
| 115 | + ) { |
| 116 | + QrCodeScannerView( |
| 117 | + modifier = Modifier.fillMaxSize(), |
| 118 | + onQrScanned = { qrData -> |
| 119 | + if (!scanned && qrData != lastScannedCode) { |
| 120 | + lastScannedCode = qrData |
| 121 | + scanned = true |
| 122 | + scanMessage = "QR Code detected!" |
| 123 | + Log.d("QrScanner", "QR detected in screen: $qrData") |
| 124 | + // Small delay to ensure UI update before finishing |
| 125 | + onQrScanned(qrData) |
| 126 | + } |
| 127 | + } |
| 128 | + ) |
| 129 | + |
| 130 | + // Scanning frame overlay |
| 131 | + Canvas( |
| 132 | + modifier = Modifier |
| 133 | + .size(300.dp) |
| 134 | + .align(Alignment.Center) |
| 135 | + ) { |
| 136 | + drawRect( |
| 137 | + color = Color(0x99FFFFFF), |
| 138 | + topLeft = androidx.compose.ui.geometry.Offset( |
| 139 | + (size.width - 200.dp.toPx()) / 2, |
| 140 | + (size.height - 200.dp.toPx()) / 2 |
| 141 | + ), |
| 142 | + size = androidx.compose.ui.geometry.Size(200.dp.toPx(), 200.dp.toPx()), |
| 143 | + style = androidx.compose.ui.graphics.drawscope.Stroke(width = 3f) |
| 144 | + ) |
| 145 | + } |
| 146 | + |
| 147 | + // Status messages |
| 148 | + Column( |
| 149 | + modifier = Modifier |
| 150 | + .align(Alignment.BottomCenter) |
| 151 | + .padding(16.dp) |
| 152 | + .fillMaxWidth(), |
| 153 | + horizontalAlignment = Alignment.CenterHorizontally, |
| 154 | + verticalArrangement = Arrangement.spacedBy(8.dp) |
| 155 | + ) { |
| 156 | + if (scanMessage.isNotEmpty()) { |
| 157 | + Text( |
| 158 | + scanMessage, |
| 159 | + color = Color.Green, |
| 160 | + textAlign = TextAlign.Center, |
| 161 | + modifier = Modifier.padding(bottom = 8.dp), |
| 162 | + style = MaterialTheme.typography.titleMedium |
| 163 | + ) |
| 164 | + } |
| 165 | + Text( |
| 166 | + "Position QR code in frame\nKeep phone steady", |
| 167 | + color = Color.White, |
| 168 | + textAlign = TextAlign.Center, |
| 169 | + style = MaterialTheme.typography.bodyMedium |
| 170 | + ) |
| 171 | + } |
| 172 | + } |
| 173 | + } |
| 174 | +} |
| 175 | + |
| 176 | +@SuppressLint("UnsafeOptInUsageError") |
| 177 | +@Composable |
| 178 | +fun QrCodeScannerView( |
| 179 | + modifier: Modifier = Modifier, |
| 180 | + onQrScanned: (String) -> Unit |
| 181 | +) { |
| 182 | + var lastScanned by remember { mutableStateOf("") } |
| 183 | + |
| 184 | + AndroidView( |
| 185 | + modifier = modifier, |
| 186 | + factory = { ctx -> |
| 187 | + val previewView = PreviewView(ctx) |
| 188 | + val cameraProviderFuture = ProcessCameraProvider.getInstance(ctx) |
| 189 | + |
| 190 | + cameraProviderFuture.addListener({ |
| 191 | + try { |
| 192 | + val cameraProvider = cameraProviderFuture.get() |
| 193 | + Log.d("QrScanner", "Camera provider obtained") |
| 194 | + |
| 195 | + @Suppress("DEPRECATION") |
| 196 | + val preview = androidx.camera.core.Preview.Builder() |
| 197 | + .setTargetResolution(android.util.Size(1280, 720)) |
| 198 | + .build() |
| 199 | + preview.setSurfaceProvider(previewView.surfaceProvider) |
| 200 | + |
| 201 | + // Configure barcode scanner options for QR codes only |
| 202 | + val options = BarcodeScannerOptions.Builder() |
| 203 | + .setBarcodeFormats(256) // Barcode.FORMAT_QR_CODE |
| 204 | + .build() |
| 205 | + |
| 206 | + val scanner = BarcodeScanning.getClient(options) |
| 207 | + Log.d("QrScanner", "Barcode scanner initialized") |
| 208 | + |
| 209 | + // Configure image analysis for continuous scanning |
| 210 | + @Suppress("DEPRECATION") |
| 211 | + val analysis = ImageAnalysis.Builder() |
| 212 | + .setTargetResolution(android.util.Size(1280, 720)) |
| 213 | + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) |
| 214 | + .build() |
| 215 | + |
| 216 | + val cameraExecutor = Executors.newSingleThreadExecutor() |
| 217 | + |
| 218 | + analysis.setAnalyzer(cameraExecutor) { imageProxy -> |
| 219 | + processQrImage(scanner, imageProxy) { result -> |
| 220 | + if (result != lastScanned && result.isNotEmpty()) { |
| 221 | + lastScanned = result |
| 222 | + Log.d("QrScanner", "QR scanned, calling callback with: $result") |
| 223 | + onQrScanned(result) |
| 224 | + } |
| 225 | + } |
| 226 | + } |
| 227 | + |
| 228 | + try { |
| 229 | + cameraProvider.unbindAll() |
| 230 | + val cameraSelector = CameraSelector.Builder() |
| 231 | + .requireLensFacing(CameraSelector.LENS_FACING_BACK) |
| 232 | + .build() |
| 233 | + |
| 234 | + cameraProvider.bindToLifecycle( |
| 235 | + ctx as androidx.lifecycle.LifecycleOwner, |
| 236 | + cameraSelector, |
| 237 | + preview, |
| 238 | + analysis |
| 239 | + ) |
| 240 | + Log.d("QrScanner", "Camera bound successfully") |
| 241 | + } catch (e: Exception) { |
| 242 | + Log.e("QrScanner", "Camera binding failed: ${e.message}", e) |
| 243 | + } |
| 244 | + } catch (e: Exception) { |
| 245 | + Log.e("QrScanner", "Failed to initialize camera: ${e.message}", e) |
| 246 | + } |
| 247 | + }, ContextCompat.getMainExecutor(ctx)) |
| 248 | + |
| 249 | + previewView |
| 250 | + } |
| 251 | + ) |
| 252 | +} |
| 253 | + |
| 254 | +@SuppressLint("UnsafeOptInUsageError") |
| 255 | +private fun processQrImage( |
| 256 | + scanner: com.google.mlkit.vision.barcode.BarcodeScanner, |
| 257 | + imageProxy: ImageProxy, |
| 258 | + onResult: (String) -> Unit |
| 259 | +) { |
| 260 | + try { |
| 261 | + val mediaImage = imageProxy.image |
| 262 | + if (mediaImage != null) { |
| 263 | + val image = InputImage.fromMediaImage( |
| 264 | + mediaImage, |
| 265 | + imageProxy.imageInfo.rotationDegrees |
| 266 | + ) |
| 267 | + scanner.process(image) |
| 268 | + .addOnSuccessListener { barcodes -> |
| 269 | + try { |
| 270 | + for (barcode in barcodes) { |
| 271 | + val rawValue = barcode.rawValue |
| 272 | + if (rawValue != null && rawValue.isNotEmpty()) { |
| 273 | + Log.d("QrScanner", "QR Code detected: $rawValue") |
| 274 | + onResult(rawValue) |
| 275 | + return@addOnSuccessListener |
| 276 | + } |
| 277 | + } |
| 278 | + } catch (e: Exception) { |
| 279 | + Log.e("QrScanner", "Error processing barcode: ${e.message}", e) |
| 280 | + } |
| 281 | + } |
| 282 | + .addOnFailureListener { e -> |
| 283 | + Log.e("QrScanner", "Scanner failed: ${e.message}", e) |
| 284 | + } |
| 285 | + .addOnCompleteListener { |
| 286 | + try { |
| 287 | + imageProxy.close() |
| 288 | + } catch (e: Exception) { |
| 289 | + Log.e("QrScanner", "Error closing imageProxy: ${e.message}") |
| 290 | + } |
| 291 | + } |
| 292 | + } else { |
| 293 | + imageProxy.close() |
| 294 | + } |
| 295 | + } catch (e: Exception) { |
| 296 | + Log.e("QrScanner", "Exception in processQrImage: ${e.message}", e) |
| 297 | + try { |
| 298 | + imageProxy.close() |
| 299 | + } catch (ex: Exception) { |
| 300 | + Log.e("QrScanner", "Error closing imageProxy in exception handler: ${ex.message}") |
| 301 | + } |
| 302 | + } |
| 303 | +} |
| 304 | + |
0 commit comments