@@ -22,16 +22,19 @@ import androidx.compose.material3.*
2222import androidx.compose.runtime.*
2323import androidx.compose.ui.Alignment
2424import androidx.compose.ui.Modifier
25+ import androidx.compose.ui.draw.scale
2526import androidx.compose.ui.graphics.Color
27+ import androidx.compose.ui.platform.LocalHapticFeedback
2628import androidx.compose.ui.text.style.TextAlign
2729import androidx.compose.ui.unit.dp
2830import androidx.compose.ui.viewinterop.AndroidView
29- import androidx.compose.foundation.Canvas
3031import androidx.core.content.ContextCompat
32+ import androidx.core.view.WindowCompat
3133import com.google.mlkit.vision.barcode.BarcodeScannerOptions
3234import com.google.mlkit.vision.barcode.BarcodeScanning
3335import com.google.mlkit.vision.common.InputImage
3436import com.sameerasw.airsync.ui.theme.AirSyncTheme
37+ import com.sameerasw.airsync.utils.HapticUtil
3538import java.util.concurrent.Executors
3639
3740class QRScannerActivity : ComponentActivity () {
@@ -73,102 +76,116 @@ class QRScannerActivity : ComponentActivity() {
7376 onClosed = {
7477 setResult(RESULT_CANCELED )
7578 finish()
76- }
79+ },
80+ activity = this
7781 )
7882 }
7983 }
8084 }
8185}
8286
87+ @OptIn(ExperimentalMaterial3ExpressiveApi ::class )
8388@SuppressLint(" UnsafeOptInUsageError" )
8489@Composable
8590private fun QRScannerScreen (
8691 onQrScanned : (String ) -> Unit ,
87- onClosed : () -> Unit
92+ onClosed : () -> Unit ,
93+ activity : ComponentActivity ? = null
8894) {
8995 var scanned by remember { mutableStateOf(false ) }
9096 var lastScannedCode by remember { mutableStateOf(" " ) }
9197 var scanMessage by remember { mutableStateOf(" " ) }
98+ var isActivelyScanning by remember { mutableStateOf(false ) }
99+ var loadingHapticsJob by remember { mutableStateOf< kotlinx.coroutines.Job ? > (null ) }
100+ val haptics = LocalHapticFeedback .current
92101
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- )
102+ // Make camera draw behind system bars
103+ LaunchedEffect (Unit ) {
104+ if (activity != null ) {
105+ WindowCompat .setDecorFitsSystemWindows(activity.window, false )
108106 }
109- ) { innerPadding ->
107+ }
108+
109+ // Continuous subtle haptic feedback while actively scanning
110+ LaunchedEffect (isActivelyScanning) {
111+ if (isActivelyScanning) {
112+ loadingHapticsJob = HapticUtil .startLoadingHaptics(haptics)
113+ } else {
114+ loadingHapticsJob?.cancel()
115+ loadingHapticsJob = null
116+ }
117+ }
118+
119+ Box (
120+ modifier = Modifier
121+ .fillMaxSize()
122+ .background(Color .Black )
123+ ) {
124+ QrCodeScannerView (
125+ modifier = Modifier .fillMaxSize(),
126+ onQrScanned = { qrData ->
127+ if (! scanned && qrData != lastScannedCode) {
128+ lastScannedCode = qrData
129+ scanned = true
130+ scanMessage = " QR Code detected!"
131+ Log .d(" QrScanner" , " QR detected in screen: $qrData " )
132+
133+ // Trigger 3-step haptic feedback on successful scan
134+ HapticUtil .performSuccess(haptics)
135+
136+ isActivelyScanning = false
137+ loadingHapticsJob?.cancel()
138+ // Small delay to ensure UI update before finishing
139+ onQrScanned(qrData)
140+ }
141+ },
142+ onActiveScanningChanged = { isScanning ->
143+ isActivelyScanning = isScanning
144+ }
145+ )
146+
147+ // Scanning indicator
110148 Box (
111149 modifier = Modifier
112- .fillMaxSize()
113- .padding(innerPadding)
114- .background(Color .Black )
150+ .align(Alignment .Center ),
151+ contentAlignment = Alignment .Center
115152 ) {
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- )
153+ LoadingIndicator (modifier = Modifier .scale(2f ))
154+ }
129155
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- }
156+ // Back button overlay on top-left
157+ IconButton (
158+ onClick = onClosed,
159+ modifier = Modifier
160+ .align(Alignment .TopStart )
161+ .padding(16 .dp)
162+ .systemBarsPadding()
163+ .background(MaterialTheme .colorScheme.surface, shape = MaterialTheme .shapes.large)
164+ ) {
165+ Icon (
166+ Icons .AutoMirrored .Filled .ArrowBack ,
167+ contentDescription = " Back" ,
168+ tint = MaterialTheme .colorScheme.onSurface,
169+ )
170+ }
146171
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\n Keep phone steady" ,
167- color = Color .White ,
168- textAlign = TextAlign .Center ,
169- style = MaterialTheme .typography.bodyMedium
170- )
171- }
172+ // Status messages with background
173+ Column (
174+ modifier = Modifier
175+ .align(Alignment .BottomCenter )
176+ .padding(16 .dp)
177+ .systemBarsPadding()
178+ .background(MaterialTheme .colorScheme.surface, shape = MaterialTheme .shapes.large)
179+ .padding(16 .dp),
180+ horizontalAlignment = Alignment .CenterHorizontally ,
181+ verticalArrangement = Arrangement .spacedBy(8 .dp)
182+ ) {
183+ Text (
184+ " Scan the QR code to connect" ,
185+ color = MaterialTheme .colorScheme.onSurface,
186+ textAlign = TextAlign .Center ,
187+ style = MaterialTheme .typography.bodyMedium
188+ )
172189 }
173190 }
174191}
@@ -177,7 +194,8 @@ private fun QRScannerScreen(
177194@Composable
178195fun QrCodeScannerView (
179196 modifier : Modifier = Modifier ,
180- onQrScanned : (String ) -> Unit
197+ onQrScanned : (String ) -> Unit ,
198+ onActiveScanningChanged : (Boolean ) -> Unit = {}
181199) {
182200 var lastScanned by remember { mutableStateOf(" " ) }
183201
@@ -219,6 +237,7 @@ fun QrCodeScannerView(
219237 processQrImage(scanner, imageProxy) { result ->
220238 if (result != lastScanned && result.isNotEmpty()) {
221239 lastScanned = result
240+ onActiveScanningChanged(true )
222241 Log .d(" QrScanner" , " QR scanned, calling callback with: $result " )
223242 onQrScanned(result)
224243 }
0 commit comments