@cleanuidev/react-native-scanner 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +609 -0
  3. package/Scanner.podspec +20 -0
  4. package/android/build.gradle +90 -0
  5. package/android/gradle.properties +5 -0
  6. package/android/src/main/AndroidManifest.xml +8 -0
  7. package/android/src/main/java/com/scanner/CameraInfoModule.kt +253 -0
  8. package/android/src/main/java/com/scanner/ScannerPackage.kt +21 -0
  9. package/android/src/main/java/com/scanner/ScannerView.kt +783 -0
  10. package/android/src/main/java/com/scanner/ScannerViewManager.kt +181 -0
  11. package/android/src/main/java/com/scanner/utils/BarcodeFrameManager.kt +170 -0
  12. package/android/src/main/java/com/scanner/views/BarcodeFrameOverlayView.kt +43 -0
  13. package/android/src/main/java/com/scanner/views/FocusAreaView.kt +124 -0
  14. package/ios/BarcodeDetectionManager.swift +229 -0
  15. package/ios/BarcodeFrameManager.swift +175 -0
  16. package/ios/BarcodeFrameOverlayView.swift +102 -0
  17. package/ios/CameraManager.swift +396 -0
  18. package/ios/CoordinateTransformer.swift +140 -0
  19. package/ios/FocusAreaOverlayView.swift +161 -0
  20. package/ios/Models.swift +341 -0
  21. package/ios/Protocols.swift +194 -0
  22. package/ios/ScannerView.h +14 -0
  23. package/ios/ScannerView.mm +358 -0
  24. package/ios/ScannerViewImpl.swift +580 -0
  25. package/ios/react-native-scanner-Bridging-Header.h +26 -0
  26. package/lib/module/CameraInfoModule.js +8 -0
  27. package/lib/module/CameraInfoModule.js.map +1 -0
  28. package/lib/module/ScannerViewNativeComponent.ts +121 -0
  29. package/lib/module/hooks/useCameraInfo.js +106 -0
  30. package/lib/module/hooks/useCameraInfo.js.map +1 -0
  31. package/lib/module/index.js +13 -0
  32. package/lib/module/index.js.map +1 -0
  33. package/lib/module/package.json +1 -0
  34. package/lib/module/types.js +47 -0
  35. package/lib/module/types.js.map +1 -0
  36. package/lib/typescript/package.json +1 -0
  37. package/lib/typescript/src/CameraInfoModule.d.ts +8 -0
  38. package/lib/typescript/src/CameraInfoModule.d.ts.map +1 -0
  39. package/lib/typescript/src/ScannerViewNativeComponent.d.ts +91 -0
  40. package/lib/typescript/src/ScannerViewNativeComponent.d.ts.map +1 -0
  41. package/lib/typescript/src/hooks/useCameraInfo.d.ts +25 -0
  42. package/lib/typescript/src/hooks/useCameraInfo.d.ts.map +1 -0
  43. package/lib/typescript/src/index.d.ts +8 -0
  44. package/lib/typescript/src/index.d.ts.map +1 -0
  45. package/lib/typescript/src/types.d.ts +145 -0
  46. package/lib/typescript/src/types.d.ts.map +1 -0
  47. package/package.json +178 -0
  48. package/src/CameraInfoModule.ts +11 -0
  49. package/src/ScannerViewNativeComponent.ts +121 -0
  50. package/src/hooks/useCameraInfo.ts +190 -0
  51. package/src/index.tsx +30 -0
  52. package/src/types.ts +177 -0
@@ -0,0 +1,783 @@
1
+ package com.scanner
2
+
3
+ import android.content.Context
4
+ import android.graphics.*
5
+ import android.os.PowerManager
6
+ import android.util.AttributeSet
7
+ import android.util.Log
8
+ import android.util.Rational
9
+ import android.util.Size
10
+ import android.view.Surface
11
+ import android.view.View
12
+ import android.view.ViewGroup
13
+ import android.view.WindowManager
14
+ import android.widget.FrameLayout
15
+ import androidx.annotation.OptIn
16
+ import androidx.camera.core.*
17
+ import androidx.camera.lifecycle.ProcessCameraProvider
18
+ import androidx.camera.view.PreviewView
19
+ import androidx.camera.camera2.interop.Camera2CameraControl
20
+ import androidx.camera.camera2.interop.CaptureRequestOptions
21
+ import android.hardware.camera2.CaptureRequest
22
+ import kotlin.math.max
23
+ import androidx.core.content.ContextCompat
24
+ import androidx.lifecycle.LifecycleOwner
25
+ import com.facebook.react.bridge.*
26
+ import com.facebook.react.uimanager.ThemedReactContext
27
+ import com.google.mlkit.vision.barcode.BarcodeScanning
28
+ import com.google.mlkit.vision.barcode.common.Barcode
29
+ import com.google.mlkit.vision.common.InputImage
30
+ import java.util.concurrent.ExecutorService
31
+ import java.util.concurrent.Executors
32
+ import com.scanner.utils.BarcodeFrameManager
33
+ import com.scanner.views.FocusAreaView
34
+ import com.scanner.views.BarcodeFrameOverlayView
35
+ import androidx.core.graphics.toColorInt
36
+ import com.facebook.react.uimanager.events.RCTEventEmitter
37
+
38
+ class ScannerView : FrameLayout {
39
+ companion object {
40
+ const val TAG = "ScannerView"
41
+ }
42
+
43
+ private var cameraProvider: ProcessCameraProvider? = null
44
+ private var camera: androidx.camera.core.Camera? = null
45
+ private var imageAnalyzer: ImageAnalysis? = null
46
+ private var preview: Preview? = null
47
+ private var cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
48
+ private var barcodeScanner = BarcodeScanning.getClient()
49
+ private var previewView: PreviewView? = null
50
+ private var overlayView: FocusAreaView? = null
51
+ private var barcodeFrameOverlayView: BarcodeFrameOverlayView? = null
52
+
53
+ // Frame configuration
54
+ private var enableFrame: Boolean = false
55
+ private var borderColor: Int = Color.TRANSPARENT
56
+ private var tintColor: Int = Color.BLACK
57
+ private var frameSize: FrameSize = FrameSize.Square(300)
58
+ private var positionX: Float = 50f // Default center (50%)
59
+ private var positionY: Float = 50f // Default center (50%)
60
+ private var showBarcodeFramesOnlyInFrame: Boolean = false
61
+
62
+ // Focus area and barcode frames configuration
63
+ private var focusAreaEnabled: Boolean = false
64
+ private var barcodeFramesEnabled: Boolean = false
65
+ private var barcodeFramesColor: Int = Color.RED
66
+
67
+ // React context for event emission
68
+ private var reactContext: ThemedReactContext? = null
69
+
70
+ private var zoom: Float = 1.0f
71
+ private var torchEnabled: Boolean = false
72
+ private var isScanningPaused: Boolean = false
73
+
74
+ // Barcode scan strategy
75
+ private var barcodeScanStrategy: String = "ALL"
76
+
77
+ // Barcode frame management
78
+ private val barcodeFrameManager = BarcodeFrameManager()
79
+
80
+ // Wake lock and screen keep-awake
81
+ private var wakeLock: PowerManager.WakeLock? = null
82
+ private var keepScreenOn: Boolean = true
83
+
84
+ // Debounce mechanism to prevent multiple rapid barcode detections
85
+ private var lastBarcodeEmissionTime: Long = 0
86
+ private var barcodeEmissionDebounceInterval: Long = 500 // Default 500ms debounce
87
+
88
+ constructor(context: Context) : super(context) {
89
+ initScannerView(context)
90
+ }
91
+
92
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
93
+ initScannerView(context)
94
+ }
95
+
96
+ constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
97
+ context,
98
+ attrs,
99
+ defStyleAttr
100
+ ) {
101
+ initScannerView(context)
102
+ }
103
+
104
+ private fun initScannerView(context: Context) {
105
+ if (context is ThemedReactContext) {
106
+ reactContext = context
107
+ }
108
+
109
+ // Initialize wake lock
110
+ initializeWakeLock()
111
+
112
+ previewView = PreviewView(context).apply {
113
+ layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
114
+ scaleType = PreviewView.ScaleType.FILL_CENTER
115
+ implementationMode = PreviewView.ImplementationMode.PERFORMANCE
116
+ // setBackgroundColor(Color.RED) // Optional: for debugging layout
117
+ }
118
+ installHierarchyFitter(previewView!!)
119
+ addView(previewView)
120
+
121
+ // Create overlay view for frame drawing
122
+ overlayView = FocusAreaView(context).apply {
123
+ layoutParams = LayoutParams(
124
+ LayoutParams.MATCH_PARENT,
125
+ LayoutParams.MATCH_PARENT
126
+ )
127
+ }
128
+ addView(overlayView)
129
+
130
+ // Create overlay view for barcode frame drawing
131
+ barcodeFrameOverlayView = BarcodeFrameOverlayView(context).apply {
132
+ layoutParams = LayoutParams(
133
+ LayoutParams.MATCH_PARENT,
134
+ LayoutParams.MATCH_PARENT
135
+ )
136
+ }
137
+ addView(barcodeFrameOverlayView)
138
+
139
+ // Set up barcode frame manager
140
+ setupBarcodeFrameManager()
141
+ }
142
+
143
+ private fun setupBarcodeFrameManager() {
144
+ barcodeFrameManager.setOnFramesChangedListener {
145
+ // Update the overlay view with current frames
146
+ val currentFrames = barcodeFrameManager.getActiveFrames()
147
+ barcodeFrameOverlayView?.setBarcodeBoxes(currentFrames)
148
+ }
149
+ }
150
+
151
+ private fun initializeWakeLock() {
152
+ val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
153
+ wakeLock = powerManager.newWakeLock(
154
+ PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
155
+ "ScannerView::WakeLock"
156
+ )
157
+ wakeLock?.setReferenceCounted(false)
158
+ }
159
+
160
+ private fun acquireWakeLock() {
161
+ if (keepScreenOn && wakeLock?.isHeld == false) {
162
+ wakeLock?.acquire()
163
+ Log.d(TAG, "Wake lock acquired")
164
+ }
165
+ }
166
+
167
+ private fun releaseWakeLock() {
168
+ if (wakeLock?.isHeld == true) {
169
+ wakeLock?.release()
170
+ Log.d(TAG, "Wake lock released")
171
+ }
172
+ }
173
+
174
+ private fun updateKeepScreenOn(keepOn: Boolean) {
175
+ keepScreenOn = keepOn
176
+ if (keepOn) {
177
+ // Set FLAG_KEEP_SCREEN_ON on the activity window
178
+ val activity = reactContext?.currentActivity
179
+ activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
180
+ acquireWakeLock()
181
+ } else {
182
+ // Remove FLAG_KEEP_SCREEN_ON from the activity window
183
+ val activity = reactContext?.currentActivity
184
+ activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
185
+ releaseWakeLock()
186
+ }
187
+ Log.d(TAG, "Keep screen on: $keepOn")
188
+ }
189
+
190
+ private fun installHierarchyFitter(view: ViewGroup) {
191
+ if (context is ThemedReactContext) { // only react-native setup
192
+ view.setOnHierarchyChangeListener(object : OnHierarchyChangeListener {
193
+ override fun onChildViewRemoved(parent: View?, child: View?) = Unit
194
+ override fun onChildViewAdded(parent: View?, child: View?) {
195
+ parent?.measure(
196
+ MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY),
197
+ MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)
198
+ )
199
+ parent?.layout(0, 0, parent.measuredWidth, parent.measuredHeight)
200
+ }
201
+ })
202
+ }
203
+ }
204
+
205
+ override fun onAttachedToWindow() {
206
+ super.onAttachedToWindow()
207
+ Log.d(TAG, "View attached to window, starting camera directly")
208
+ startCamera()
209
+ }
210
+
211
+ override fun onDetachedFromWindow() {
212
+ super.onDetachedFromWindow()
213
+ stopCamera()
214
+ barcodeFrameManager.shutdown()
215
+
216
+ // Ensure wake lock is released when view is destroyed
217
+ releaseWakeLock()
218
+ }
219
+
220
+ private fun startCamera() {
221
+ Log.d(TAG, "Starting camera...")
222
+
223
+ if (!hasCameraPermission()) {
224
+ Log.e(TAG, "Camera permission not granted")
225
+ return
226
+ }
227
+
228
+ // Acquire wake lock when camera starts
229
+ updateKeepScreenOn(true)
230
+
231
+ val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
232
+ cameraProviderFuture.addListener({
233
+ try {
234
+ cameraProvider = cameraProviderFuture.get()
235
+ Log.d(TAG, "CameraProvider obtained")
236
+
237
+ previewView?.post {
238
+ Log.d(TAG, "Binding camera use cases on UI thread")
239
+ bindCameraUseCases()
240
+ }
241
+ } catch (e: Exception) {
242
+ Log.e(TAG, "Failed to initialize camera", e)
243
+ }
244
+ // }, ContextCompat.getMainExecutor(context))
245
+ }, cameraExecutor)
246
+ }
247
+
248
+ private fun stopCamera() {
249
+ camera?.cameraControl?.cancelFocusAndMetering()
250
+ cameraProvider?.unbindAll()
251
+ cameraExecutor.shutdown()
252
+ barcodeScanner.close()
253
+
254
+ // Release wake lock when camera stops
255
+ updateKeepScreenOn(false)
256
+
257
+ Log.e(TAG, "Camera stopped.")
258
+ }
259
+
260
+ private fun bindCameraUseCases() {
261
+ val cameraProvider = cameraProvider ?: run {
262
+ Log.e(TAG, "Camera provider not available for binding.")
263
+ return
264
+ }
265
+ val lifecycleOwner = reactContext?.currentActivity as? LifecycleOwner ?: run {
266
+ Log.e(TAG, "No LifecycleOwner available for binding.")
267
+ return
268
+ }
269
+
270
+ // Unbind any previous use cases first
271
+ cameraProvider.unbindAll()
272
+
273
+ try {
274
+ val surfaceProvider = previewView?.surfaceProvider
275
+ if (surfaceProvider == null) {
276
+ Log.e(TAG, "SurfaceProvider is null! Cannot bind.")
277
+ return
278
+ }
279
+
280
+ // Preview
281
+ preview = Preview.Builder()
282
+ .setTargetRotation(previewView?.display?.rotation ?: Surface.ROTATION_0)
283
+ .build()
284
+ .also {
285
+ it.setSurfaceProvider(surfaceProvider)
286
+ }
287
+
288
+ // Image Analysis
289
+ imageAnalyzer = ImageAnalysis.Builder()
290
+ // .setTargetResolution(Size(1280, 720)) // review this later if needed
291
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
292
+ .build()
293
+ .also {
294
+ it.setAnalyzer(cameraExecutor) { imageProxy ->
295
+ processImage(imageProxy)
296
+ }
297
+ }
298
+
299
+ // Select back camera as default
300
+ val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
301
+
302
+ // Create a ViewPort that matches the PreviewView
303
+ val viewPort = ViewPort.Builder(
304
+ Rational(previewView!!.width, previewView!!.height),
305
+ previewView!!.display.rotation
306
+ )
307
+ .setScaleType(ViewPort.FILL_CENTER) // or FIT_CENTER as needed
308
+ .build()
309
+
310
+ // Create a UseCaseGroup with the ViewPort
311
+ val useCaseGroup = UseCaseGroup.Builder()
312
+ .addUseCase(preview!!)
313
+ .addUseCase(imageAnalyzer!!)
314
+ .setViewPort(viewPort)
315
+ .build()
316
+
317
+ // Bind use cases to camera
318
+ camera = cameraProvider.bindToLifecycle(
319
+ lifecycleOwner,
320
+ cameraSelector,
321
+ useCaseGroup
322
+ )
323
+
324
+ // Set up torch and zoom
325
+ camera?.cameraControl?.enableTorch(torchEnabled)
326
+ setZoom(zoom)
327
+
328
+ // Set up continuous autofocus for better barcode scanning
329
+ setupContinuousAutoFocus()
330
+
331
+ // Set up auto-focus on frame center (if focus area is enabled)
332
+ setupAutoFocusOnFrame()
333
+
334
+ } catch (exc: Exception) {
335
+ Log.e(TAG, "Use case binding failed", exc)
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Set up continuous autofocus mode for better barcode scanning.
341
+ * This enables AF_MODE_CONTINUOUS_PICTURE which continuously adjusts focus,
342
+ * making it much easier to scan barcodes at different distances.
343
+ */
344
+ private fun setupContinuousAutoFocus() {
345
+ try {
346
+ val camera2Control = camera?.cameraControl?.let {
347
+ Camera2CameraControl.from(it)
348
+ } ?: run {
349
+ Log.w(TAG, "Camera2CameraControl not available")
350
+ return
351
+ }
352
+
353
+ // Enable continuous autofocus mode
354
+ camera2Control.captureRequestOptions = CaptureRequestOptions.Builder()
355
+ .setCaptureRequestOption<Int>(
356
+ CaptureRequest.CONTROL_AF_MODE,
357
+ CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
358
+ )
359
+ .build()
360
+
361
+ Log.d(TAG, "✅ Continuous autofocus enabled (AF_MODE_CONTINUOUS_PICTURE)")
362
+ } catch (e: Exception) {
363
+ Log.e(TAG, "Failed to setup continuous autofocus", e)
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Set up autofocus on the focus area frame center.
369
+ * This provides additional focus assistance when a focus area is defined.
370
+ */
371
+ private fun setupAutoFocusOnFrame() {
372
+ if (!enableFrame) {
373
+ // Even without focus area overlay, trigger autofocus on screen center
374
+ triggerAutoFocusOnCenter()
375
+ return
376
+ }
377
+
378
+ val frame = overlayView?.frameRect ?: run {
379
+ triggerAutoFocusOnCenter()
380
+ return
381
+ }
382
+
383
+ val centerX = frame.centerX()
384
+ val centerY = frame.centerY()
385
+
386
+ val viewWidth = previewView?.width ?: return
387
+ val viewHeight = previewView?.height ?: return
388
+
389
+ val normalizedX = centerX / viewWidth
390
+ val normalizedY = centerY / viewHeight
391
+
392
+ triggerAutoFocusAt(normalizedX, normalizedY)
393
+ }
394
+
395
+ /**
396
+ * Trigger autofocus at screen center (useful when no focus area is defined)
397
+ */
398
+ private fun triggerAutoFocusOnCenter() {
399
+ triggerAutoFocusAt(0.5f, 0.5f)
400
+ }
401
+
402
+ /**
403
+ * Trigger autofocus at a specific normalized point (0.0-1.0)
404
+ */
405
+ private fun triggerAutoFocusAt(normalizedX: Float, normalizedY: Float) {
406
+ val meteringPointFactory = previewView?.meteringPointFactory ?: return
407
+ val afPoint = meteringPointFactory.createPoint(normalizedX, normalizedY)
408
+
409
+ val focusAction = FocusMeteringAction.Builder(afPoint, FocusMeteringAction.FLAG_AF)
410
+ .setAutoCancelDuration(2, java.util.concurrent.TimeUnit.SECONDS) // Auto-cancel after 2 seconds
411
+ .build()
412
+
413
+ camera?.cameraControl?.startFocusAndMetering(focusAction)
414
+ ?.addListener({
415
+ Log.d(TAG, "Auto-focus triggered at ($normalizedX, $normalizedY)")
416
+ }, ContextCompat.getMainExecutor(context))
417
+ }
418
+
419
+
420
+ @OptIn(ExperimentalGetImage::class)
421
+ private fun processImage(imageProxy: ImageProxy) {
422
+ try {
423
+ // Skip processing if scanning is paused
424
+ if (isScanningPaused) {
425
+ imageProxy.close()
426
+ return
427
+ }
428
+
429
+ val mediaImage = imageProxy.image
430
+ if (mediaImage != null && previewView != null) {
431
+ val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
432
+ val transformationInfo = ImageAnalysisTransformationInfo(
433
+ resolution = Size(imageProxy.width, imageProxy.height),
434
+ rotationDegrees = imageProxy.imageInfo.rotationDegrees
435
+ )
436
+
437
+ barcodeScanner.process(image)
438
+ .addOnSuccessListener { barcodes ->
439
+ // Process barcode detection for the main scanning logic
440
+ if (barcodes.isNotEmpty() && !isScanningPaused) {
441
+ val processedBarcodes = processBarcodesAccordingToStrategy(barcodes, transformationInfo)
442
+
443
+ // Update barcode frames for processed barcodes (filtered)
444
+ updateBarcodeFrames(processedBarcodes, transformationInfo)
445
+
446
+ if (processedBarcodes.isNotEmpty()) {
447
+ val currentTime = System.currentTimeMillis()
448
+ val timeSinceLastEmission = currentTime - lastBarcodeEmissionTime
449
+
450
+ // Debounce: Prevent rapid duplicate emissions
451
+ // If we've emitted recently (within debounce interval), skip this emission
452
+ // This prevents multiple alerts when pauseScanning is set but detection callbacks are still in flight
453
+ if (timeSinceLastEmission < barcodeEmissionDebounceInterval) {
454
+ Log.d(TAG, "⏭️ Skipping barcode emission (debounced, last emission was ${timeSinceLastEmission}ms ago)")
455
+ return@addOnSuccessListener
456
+ }
457
+
458
+ lastBarcodeEmissionTime = currentTime
459
+
460
+ // Create array of barcode events
461
+ val barcodeEvents = Arguments.createArray()
462
+ processedBarcodes.forEach { barcode ->
463
+ val eventData = Arguments.createMap().apply {
464
+ putString("data", barcode.rawValue)
465
+ putString("format", barcode.format.toString())
466
+ putDouble("timestamp", System.currentTimeMillis().toDouble())
467
+
468
+ // Add bounding box if available
469
+ barcode.boundingBox?.let { box ->
470
+ val boundingBoxData = Arguments.createMap().apply {
471
+ putDouble("left", box.left.toDouble())
472
+ putDouble("top", box.top.toDouble())
473
+ putDouble("right", box.right.toDouble())
474
+ putDouble("bottom", box.bottom.toDouble())
475
+ }
476
+ putMap("boundingBox", boundingBoxData)
477
+
478
+ // Calculate area
479
+ val area = (box.right - box.left) * (box.bottom - box.top)
480
+ putDouble("area", area.toDouble())
481
+ }
482
+ }
483
+ barcodeEvents.pushMap(eventData)
484
+ }
485
+
486
+ val ctx = reactContext
487
+ if (ctx is ThemedReactContext) {
488
+ ctx.runOnUiQueueThread {
489
+ val eventData = Arguments.createMap().apply {
490
+ putArray("barcodes", barcodeEvents)
491
+ }
492
+ ctx.getJSModule(RCTEventEmitter::class.java)
493
+ .receiveEvent(this@ScannerView.id, "onBarcodeScanned", eventData)
494
+ Log.d(TAG, "✅ Barcode emitted (debounced)")
495
+ }
496
+ }
497
+ }
498
+ } else {
499
+ // TODO: Review this part later
500
+ // Clear barcode frames when no barcodes are detected or scanning is paused
501
+ // updateBarcodeFrames(emptyList(), transformationInfo)
502
+ }
503
+ }
504
+ .addOnFailureListener { e ->
505
+ Log.e(TAG, "Barcode scanning failed", e)
506
+ }
507
+ .addOnCompleteListener {
508
+ imageProxy.close()
509
+ }
510
+ } else {
511
+ imageProxy.close()
512
+ }
513
+ } catch (e: Exception) {
514
+ Log.e(TAG, "Error processing image", e)
515
+ imageProxy.close()
516
+ }
517
+ }
518
+
519
+ // Frame setter methods
520
+ fun setEnableFrame(enable: Boolean) {
521
+ enableFrame = enable
522
+ overlayView?.setEnableFrame(enable)
523
+
524
+ // Update focus when frame is toggled
525
+ if (camera != null) {
526
+ setupAutoFocusOnFrame()
527
+ }
528
+ }
529
+
530
+ fun setBorderColor(color: String) {
531
+ try {
532
+ borderColor = color.toColorInt()
533
+ overlayView?.setBorderColor(borderColor)
534
+ } catch (e: Exception) {
535
+ Log.e(TAG, "Invalid color format: $color")
536
+ }
537
+ }
538
+
539
+ fun setTintColor(color: String) {
540
+ try {
541
+ tintColor = color.toColorInt()
542
+ overlayView?.setTintColor(tintColor)
543
+ } catch (e: Exception) {
544
+ Log.e(TAG, "Invalid color format: $color")
545
+ }
546
+ }
547
+
548
+ fun setFrameSize(size: FrameSize) {
549
+ frameSize = size
550
+ overlayView?.setFrameSize(size)
551
+ }
552
+
553
+ fun setShowBarcodeFramesOnlyInFrame(showOnlyInFrame: Boolean) {
554
+ showBarcodeFramesOnlyInFrame = showOnlyInFrame
555
+ Log.d(TAG, "Show barcode frames only in frame: $showOnlyInFrame")
556
+ }
557
+
558
+ // Focus area configuration methods
559
+ fun setFocusAreaEnabled(enabled: Boolean) {
560
+ focusAreaEnabled = enabled
561
+ Log.d(TAG, "Focus area enabled: $enabled")
562
+ }
563
+
564
+ // Barcode frames configuration methods
565
+ fun setBarcodeFramesEnabled(enabled: Boolean) {
566
+ barcodeFramesEnabled = enabled
567
+ Log.d(TAG, "Barcode frames enabled: $enabled")
568
+ }
569
+
570
+ fun setBarcodeFramesColor(color: String) {
571
+ try {
572
+ barcodeFramesColor = color.toColorInt()
573
+ Log.d(TAG, "Barcode frames color set: $color")
574
+ } catch (e: Exception) {
575
+ Log.e(TAG, "Invalid barcode frames color format: $color")
576
+ }
577
+ }
578
+
579
+ fun setZoom(zoom: Float) {
580
+ this.zoom = zoom
581
+ // Apply zoom, respecting the device's limits
582
+ val zoomState = camera?.cameraInfo?.zoomState?.value
583
+ if (zoomState != null) {
584
+ val newZoom = zoom.coerceIn(zoomState.minZoomRatio, zoomState.maxZoomRatio)
585
+ camera?.cameraControl?.setZoomRatio(newZoom)
586
+ Log.d(TAG, "Setting zoom to $newZoom (requested $zoom)")
587
+ } else {
588
+ camera?.cameraControl?.setZoomRatio(zoom)
589
+ }
590
+ }
591
+
592
+ fun setTorch(enabled: Boolean) {
593
+ torchEnabled = enabled
594
+ camera?.cameraControl?.enableTorch(enabled)
595
+ Log.d(TAG, "Torch ${if (enabled) "enabled" else "disabled"}")
596
+ }
597
+
598
+ fun resumeScanning() {
599
+ isScanningPaused = false
600
+ // Reset debounce timer when resuming to allow immediate detection
601
+ lastBarcodeEmissionTime = 0
602
+ Log.d(TAG, "Scanning resumed")
603
+ }
604
+
605
+ fun pauseScanning() {
606
+ isScanningPaused = true
607
+ // Clear all barcode frames when pausing
608
+ barcodeFrameManager.clearAllFrames()
609
+ // Reset debounce timer when pausing to prevent stale emissions
610
+ lastBarcodeEmissionTime = 0
611
+ Log.d(TAG, "Scanning paused")
612
+ }
613
+
614
+ fun setBarcodeEmissionInterval(intervalSeconds: Double) {
615
+ // Convert seconds to milliseconds, ensure non-negative
616
+ barcodeEmissionDebounceInterval = max(0, (intervalSeconds * 1000).toLong())
617
+ Log.d(TAG, "Barcode emission interval set to: ${barcodeEmissionDebounceInterval}ms")
618
+ }
619
+
620
+ fun setBarcodeScanStrategy(strategy: String) {
621
+ barcodeScanStrategy = strategy
622
+ Log.d(TAG, "Barcode scan strategy set to: $strategy")
623
+ }
624
+
625
+ fun setPosition(x: Float, y: Float) {
626
+ positionX = x.coerceIn(0f, 100f) // Clamp to 0-100 range
627
+ positionY = y.coerceIn(0f, 100f) // Clamp to 0-100 range
628
+ overlayView?.setPosition(positionX, positionY)
629
+ Log.d(TAG, "Focus area position set to: x=$positionX%, y=$positionY%")
630
+ }
631
+
632
+ fun setKeepScreenOnEnabled(keepOn: Boolean) {
633
+ updateKeepScreenOn(keepOn)
634
+ }
635
+
636
+ private fun hasCameraPermission(): Boolean {
637
+ return context.checkSelfPermission(android.Manifest.permission.CAMERA) == android.content.pm.PackageManager.PERMISSION_GRANTED
638
+ }
639
+
640
+ private fun updateBarcodeFrames(
641
+ barcodes: List<Barcode>,
642
+ transformationInfo: ImageAnalysisTransformationInfo
643
+ ) {
644
+ // Only update barcode frames if they are enabled
645
+ if (!barcodeFramesEnabled) {
646
+ return
647
+ }
648
+
649
+ val barcodeFrames = mutableMapOf<String, RectF>()
650
+
651
+ barcodes.forEach { barcode ->
652
+ val barcodeValue = barcode.rawValue ?: return@forEach
653
+ val boundingBox = barcode.boundingBox ?: return@forEach
654
+
655
+ val transformedRect = transformBarcodeBoundingBox(
656
+ boundingBox,
657
+ transformationInfo,
658
+ previewView!!
659
+ )
660
+
661
+ // If showBarcodeFramesOnlyInFrame is true, only include frames within the overlay frame
662
+ if (showBarcodeFramesOnlyInFrame && enableFrame) {
663
+ val frame = overlayView?.frameRect
664
+ if (frame != null && frame.contains(transformedRect)) {
665
+ barcodeFrames[barcodeValue] = transformedRect
666
+ }
667
+ } else {
668
+ // Show all barcode frames
669
+ barcodeFrames[barcodeValue] = transformedRect
670
+ }
671
+ }
672
+
673
+ // Update frames using the manager
674
+ barcodeFrameManager.updateBarcodeFrames(barcodeFrames)
675
+ }
676
+
677
+ private fun processBarcodesAccordingToStrategy(
678
+ barcodes: List<Barcode>,
679
+ transformationInfo: ImageAnalysisTransformationInfo
680
+ ): List<Barcode> {
681
+ if (barcodes.isEmpty()) return emptyList()
682
+
683
+ // Filter barcodes based on focus area if enabled
684
+ val filteredBarcodes = if (focusAreaEnabled) {
685
+ val frame = overlayView?.frameRect
686
+ if (frame != null) {
687
+ barcodes.filter { barcode ->
688
+ barcode.boundingBox?.let { box ->
689
+ val transformedBox = transformBarcodeBoundingBox(box, transformationInfo, previewView!!)
690
+ frame.contains(transformedBox)
691
+ } ?: false
692
+ }
693
+ } else {
694
+ barcodes
695
+ }
696
+ } else {
697
+ barcodes
698
+ }
699
+
700
+ if (filteredBarcodes.isEmpty()) return emptyList()
701
+
702
+ // Apply strategy
703
+ return when (barcodeScanStrategy) {
704
+ "ONE" -> {
705
+ listOf(filteredBarcodes[0])
706
+ }
707
+ "BIGGEST" -> {
708
+ val barcodeWithArea = filteredBarcodes.mapNotNull { barcode ->
709
+ barcode.boundingBox?.let { box ->
710
+ val area = (box.right - box.left) * (box.bottom - box.top)
711
+ barcode to area
712
+ }
713
+ }
714
+ if (barcodeWithArea.isNotEmpty()) {
715
+ val biggest = barcodeWithArea.maxByOrNull { it.second }
716
+ listOf(biggest!!.first)
717
+ } else {
718
+ listOf(filteredBarcodes[0])
719
+ }
720
+ }
721
+ "SORT_BY_BIGGEST" -> {
722
+ val barcodeWithArea = filteredBarcodes.mapNotNull { barcode ->
723
+ barcode.boundingBox?.let { box ->
724
+ val area = (box.right - box.left) * (box.bottom - box.top)
725
+ barcode to area
726
+ }
727
+ }
728
+ if (barcodeWithArea.isNotEmpty()) {
729
+ barcodeWithArea.sortedByDescending { it.second }.map { it.first }
730
+ } else {
731
+ filteredBarcodes
732
+ }
733
+ }
734
+ "ALL" -> filteredBarcodes
735
+ else -> filteredBarcodes // Default to ALL
736
+ }
737
+ }
738
+ }
739
+
740
+ private data class ImageAnalysisTransformationInfo(
741
+ val resolution: Size,
742
+ val rotationDegrees: Int
743
+ )
744
+
745
+ private fun transformBarcodeBoundingBox(
746
+ barcodeBoundingBox: Rect,
747
+ imageAnalysisInfo: ImageAnalysisTransformationInfo,
748
+ previewView: View
749
+ ): RectF {
750
+ // Get image analysis resolution and rotation
751
+ val imageWidth = imageAnalysisInfo.resolution.width
752
+ val imageHeight = imageAnalysisInfo.resolution.height
753
+ val imageRotation = imageAnalysisInfo.rotationDegrees
754
+
755
+ // Adjust for rotation
756
+ val (rotatedWidth, rotatedHeight) = if (imageRotation == 90 || imageRotation == 270) {
757
+ imageHeight to imageWidth
758
+ } else {
759
+ imageWidth to imageHeight
760
+ }
761
+
762
+ // Get preview view dimensions
763
+ val viewWidth = previewView.width
764
+ val viewHeight = previewView.height
765
+
766
+ // Calculate scale factors
767
+ val scaleX = viewWidth.toFloat() / rotatedWidth
768
+ val scaleY = viewHeight.toFloat() / rotatedHeight
769
+ val scale = maxOf(scaleX, scaleY) // For FILL_CENTER
770
+
771
+ // Calculate offsets
772
+ val offsetX = (viewWidth - rotatedWidth * scale) / 2
773
+ val offsetY = (viewHeight - rotatedHeight * scale) / 2
774
+
775
+ // Transform the bounding box
776
+ val transformedRect = RectF(barcodeBoundingBox)
777
+ transformedRect.left = transformedRect.left * scale + offsetX
778
+ transformedRect.top = transformedRect.top * scale + offsetY
779
+ transformedRect.right = transformedRect.right * scale + offsetX
780
+ transformedRect.bottom = transformedRect.bottom * scale + offsetY
781
+
782
+ return transformedRect
783
+ }