react-native-fabric-barcode-scanner 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +203 -0
- package/android/build.gradle +68 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/com/rncustomscanner/BarcodeAnalyzer.kt +56 -0
- package/android/src/main/java/com/rncustomscanner/BarcodeFormatMapper.kt +59 -0
- package/android/src/main/java/com/rncustomscanner/CameraPermissionHelper.kt +140 -0
- package/android/src/main/java/com/rncustomscanner/RNCustomScannerPackage.kt +44 -0
- package/android/src/main/java/com/rncustomscanner/ScannerView.kt +245 -0
- package/android/src/main/java/com/rncustomscanner/events/BarcodeScannedEvent.kt +29 -0
- package/android/src/newarch/java/com/rncustomscanner/ScannerModule.kt +103 -0
- package/android/src/newarch/java/com/rncustomscanner/ScannerViewManager.kt +61 -0
- package/android/src/oldarch/java/com/rncustomscanner/ScannerModule.kt +109 -0
- package/android/src/oldarch/java/com/rncustomscanner/ScannerViewManager.kt +54 -0
- package/ios/RNCustomScanner-Bridging-Header.h +3 -0
- package/ios/RNScannerView.swift +132 -0
- package/ios/ScannerModule.mm +65 -0
- package/ios/ScannerModule.swift +105 -0
- package/ios/ScannerViewManager.mm +67 -0
- package/package.json +71 -0
- package/react-native-fabric-barcode-scanner.podspec +28 -0
- package/react-native.config.js +8 -0
- package/src/NativeScannerModule.ts +21 -0
- package/src/ScannerViewNativeComponent.ts +28 -0
- package/src/index.tsx +66 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
package com.rncustomscanner
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.annotation.SuppressLint
|
|
5
|
+
import android.content.pm.PackageManager
|
|
6
|
+
import android.graphics.Color
|
|
7
|
+
import android.util.Log
|
|
8
|
+
import android.view.View
|
|
9
|
+
import android.view.ViewGroup
|
|
10
|
+
import android.widget.FrameLayout
|
|
11
|
+
import androidx.appcompat.app.AppCompatActivity
|
|
12
|
+
import androidx.camera.core.Camera
|
|
13
|
+
import androidx.camera.core.CameraSelector
|
|
14
|
+
import androidx.camera.core.ImageAnalysis
|
|
15
|
+
import androidx.camera.core.Preview
|
|
16
|
+
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
17
|
+
import androidx.camera.view.PreviewView
|
|
18
|
+
import androidx.core.content.ContextCompat
|
|
19
|
+
import com.facebook.react.bridge.LifecycleEventListener
|
|
20
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
21
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
22
|
+
import com.rncustomscanner.events.BarcodeScannedEvent
|
|
23
|
+
import java.util.concurrent.ExecutorService
|
|
24
|
+
import java.util.concurrent.Executors
|
|
25
|
+
|
|
26
|
+
@SuppressLint("ViewConstructor")
|
|
27
|
+
class ScannerView(context: ThemedReactContext) : FrameLayout(context) {
|
|
28
|
+
|
|
29
|
+
private val reactContext: ThemedReactContext = context
|
|
30
|
+
private val previewView = PreviewView(context)
|
|
31
|
+
private val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
|
32
|
+
|
|
33
|
+
private var cameraProvider: ProcessCameraProvider? = null
|
|
34
|
+
private var camera: Camera? = null
|
|
35
|
+
private var preview: Preview? = null
|
|
36
|
+
private var imageAnalysis: ImageAnalysis? = null
|
|
37
|
+
private var barcodeAnalyzer: BarcodeAnalyzer? = null
|
|
38
|
+
|
|
39
|
+
private var isActive: Boolean = true
|
|
40
|
+
private var torchOn: Boolean = false
|
|
41
|
+
private var scanFormats: List<String> = BarcodeFormatMapper.supportedFormats
|
|
42
|
+
private var isCameraBound = false
|
|
43
|
+
|
|
44
|
+
private val lifecycleListener = object : LifecycleEventListener {
|
|
45
|
+
override fun onHostResume() {
|
|
46
|
+
if (isActive && hasCameraPermission()) {
|
|
47
|
+
isCameraBound = false
|
|
48
|
+
scheduleCameraStart()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override fun onHostPause() {
|
|
53
|
+
stopCamera()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
override fun onHostDestroy() {
|
|
57
|
+
stopCamera()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
init {
|
|
62
|
+
setBackgroundColor(Color.BLACK)
|
|
63
|
+
previewView.layoutParams = LayoutParams(
|
|
64
|
+
LayoutParams.MATCH_PARENT,
|
|
65
|
+
LayoutParams.MATCH_PARENT,
|
|
66
|
+
)
|
|
67
|
+
previewView.scaleType = PreviewView.ScaleType.FILL_CENTER
|
|
68
|
+
installHierarchyFitter(previewView)
|
|
69
|
+
addView(previewView)
|
|
70
|
+
|
|
71
|
+
reactContext.addLifecycleEventListener(lifecycleListener)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
override fun onAttachedToWindow() {
|
|
75
|
+
super.onAttachedToWindow()
|
|
76
|
+
scheduleCameraStart()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
override fun onDetachedFromWindow() {
|
|
80
|
+
reactContext.removeLifecycleEventListener(lifecycleListener)
|
|
81
|
+
stopCamera()
|
|
82
|
+
barcodeAnalyzer?.close()
|
|
83
|
+
barcodeAnalyzer = null
|
|
84
|
+
cameraExecutor.shutdown()
|
|
85
|
+
super.onDetachedFromWindow()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
|
|
89
|
+
super.onWindowFocusChanged(hasWindowFocus)
|
|
90
|
+
if (hasWindowFocus && isActive && hasCameraPermission() && !isCameraBound) {
|
|
91
|
+
previewView.post { scheduleCameraStart() }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
96
|
+
super.onSizeChanged(w, h, oldw, oldh)
|
|
97
|
+
if (w > 0 && h > 0 && !isCameraBound) {
|
|
98
|
+
scheduleCameraStart()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fun setIsActive(active: Boolean) {
|
|
103
|
+
isActive = active
|
|
104
|
+
if (active) {
|
|
105
|
+
scheduleCameraStart()
|
|
106
|
+
} else {
|
|
107
|
+
stopCamera()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fun setTorchOn(enabled: Boolean) {
|
|
112
|
+
torchOn = enabled
|
|
113
|
+
camera?.cameraControl?.enableTorch(enabled)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fun setScanFormats(formats: List<String>?) {
|
|
117
|
+
scanFormats = formats?.takeIf { it.isNotEmpty() } ?: BarcodeFormatMapper.supportedFormats
|
|
118
|
+
barcodeAnalyzer?.updateFormats(BarcodeFormatMapper.toMlKitFormats(scanFormats))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private fun scheduleCameraStart() {
|
|
122
|
+
if (!isActive || !hasCameraPermission() || width <= 0 || height <= 0) {
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
previewView.post { configureCameraIfNeeded() }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private fun hasCameraPermission(): Boolean {
|
|
129
|
+
return ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==
|
|
130
|
+
PackageManager.PERMISSION_GRANTED
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private fun configureCameraIfNeeded() {
|
|
134
|
+
if (!isActive || !hasCameraPermission() || width <= 0 || height <= 0 || isCameraBound) {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
val activity = reactContext.currentActivity
|
|
139
|
+
if (activity == null) {
|
|
140
|
+
Log.w(TAG, "Activity not available yet, retrying camera start")
|
|
141
|
+
previewView.postDelayed({ configureCameraIfNeeded() }, 100)
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
|
|
146
|
+
cameraProviderFuture.addListener({
|
|
147
|
+
try {
|
|
148
|
+
cameraProvider = cameraProviderFuture.get()
|
|
149
|
+
bindCameraUseCases()
|
|
150
|
+
} catch (exc: Exception) {
|
|
151
|
+
Log.e(TAG, "Failed to initialize camera", exc)
|
|
152
|
+
isCameraBound = false
|
|
153
|
+
}
|
|
154
|
+
}, ContextCompat.getMainExecutor(activity))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private fun bindCameraUseCases() {
|
|
158
|
+
val provider = cameraProvider ?: return
|
|
159
|
+
val activity = reactContext.currentActivity as? AppCompatActivity ?: return
|
|
160
|
+
|
|
161
|
+
provider.unbindAll()
|
|
162
|
+
|
|
163
|
+
preview = Preview.Builder().build()
|
|
164
|
+
|
|
165
|
+
val formats = BarcodeFormatMapper.toMlKitFormats(scanFormats)
|
|
166
|
+
barcodeAnalyzer = BarcodeAnalyzer { barcodes ->
|
|
167
|
+
val barcode = barcodes.firstOrNull() ?: return@BarcodeAnalyzer
|
|
168
|
+
val value = barcode.rawValue ?: return@BarcodeAnalyzer
|
|
169
|
+
emitBarcodeScanned(
|
|
170
|
+
value = value,
|
|
171
|
+
type = BarcodeFormatMapper.fromMlKitFormat(barcode.format),
|
|
172
|
+
timestamp = System.currentTimeMillis().toInt(),
|
|
173
|
+
)
|
|
174
|
+
}.also {
|
|
175
|
+
it.updateFormats(formats)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
imageAnalysis = ImageAnalysis.Builder()
|
|
179
|
+
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
180
|
+
.build()
|
|
181
|
+
.also { analysis ->
|
|
182
|
+
analysis.setAnalyzer(cameraExecutor, barcodeAnalyzer!!)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
val useCases = listOfNotNull(preview, imageAnalysis)
|
|
187
|
+
camera = provider.bindToLifecycle(
|
|
188
|
+
activity,
|
|
189
|
+
CameraSelector.DEFAULT_BACK_CAMERA,
|
|
190
|
+
*useCases.toTypedArray(),
|
|
191
|
+
)
|
|
192
|
+
camera?.cameraControl?.enableTorch(torchOn)
|
|
193
|
+
// Attach surface AFTER bind — required for PreviewView inside React Native.
|
|
194
|
+
preview?.setSurfaceProvider(previewView.surfaceProvider)
|
|
195
|
+
isCameraBound = true
|
|
196
|
+
Log.d(TAG, "Camera bound successfully (${width}x${height})")
|
|
197
|
+
} catch (exc: Exception) {
|
|
198
|
+
Log.e(TAG, "Use case binding failed", exc)
|
|
199
|
+
isCameraBound = false
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private fun stopCamera() {
|
|
204
|
+
cameraProvider?.unbindAll()
|
|
205
|
+
camera = null
|
|
206
|
+
preview = null
|
|
207
|
+
imageAnalysis = null
|
|
208
|
+
isCameraBound = false
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// React Native does not always layout TextureView children correctly — without this
|
|
212
|
+
// the PreviewView stays black. See: https://github.com/facebook/react-native/issues/17968
|
|
213
|
+
private fun installHierarchyFitter(view: ViewGroup) {
|
|
214
|
+
view.setOnHierarchyChangeListener(object : OnHierarchyChangeListener {
|
|
215
|
+
override fun onChildViewRemoved(parent: View?, child: View?) = Unit
|
|
216
|
+
|
|
217
|
+
override fun onChildViewAdded(parent: View?, child: View?) {
|
|
218
|
+
parent?.measure(
|
|
219
|
+
MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY),
|
|
220
|
+
MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY),
|
|
221
|
+
)
|
|
222
|
+
parent?.layout(0, 0, parent.measuredWidth, parent.measuredHeight)
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private fun emitBarcodeScanned(value: String, type: String, timestamp: Int) {
|
|
228
|
+
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
|
|
229
|
+
UIManagerHelper
|
|
230
|
+
.getEventDispatcherForReactTag(reactContext, id)
|
|
231
|
+
?.dispatchEvent(
|
|
232
|
+
BarcodeScannedEvent(
|
|
233
|
+
surfaceId,
|
|
234
|
+
id,
|
|
235
|
+
value,
|
|
236
|
+
type,
|
|
237
|
+
timestamp,
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
companion object {
|
|
243
|
+
private const val TAG = "ScannerView"
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package com.rncustomscanner.events
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.WritableMap
|
|
5
|
+
import com.facebook.react.uimanager.events.Event
|
|
6
|
+
|
|
7
|
+
class BarcodeScannedEvent(
|
|
8
|
+
surfaceId: Int,
|
|
9
|
+
viewId: Int,
|
|
10
|
+
private val value: String,
|
|
11
|
+
private val type: String,
|
|
12
|
+
private val timestamp: Int,
|
|
13
|
+
) : Event<BarcodeScannedEvent>(surfaceId, viewId) {
|
|
14
|
+
|
|
15
|
+
override fun getEventName(): String = EVENT_NAME
|
|
16
|
+
|
|
17
|
+
override fun getEventData(): WritableMap =
|
|
18
|
+
Arguments.createMap().apply {
|
|
19
|
+
putString("value", value)
|
|
20
|
+
putString("type", type)
|
|
21
|
+
putInt("timestamp", timestamp)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override fun canCoalesce(): Boolean = false
|
|
25
|
+
|
|
26
|
+
companion object {
|
|
27
|
+
const val EVENT_NAME = "topBarcodeScanned"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
package com.rncustomscanner
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.content.pm.PackageManager
|
|
5
|
+
import android.hardware.camera2.CameraManager
|
|
6
|
+
import androidx.core.app.ActivityCompat
|
|
7
|
+
import androidx.core.content.ContextCompat
|
|
8
|
+
import com.facebook.react.bridge.Promise
|
|
9
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
10
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
11
|
+
import com.facebook.react.modules.core.PermissionAwareActivity
|
|
12
|
+
import com.facebook.react.modules.core.PermissionListener
|
|
13
|
+
|
|
14
|
+
@ReactModule(name = ScannerModule.NAME)
|
|
15
|
+
class ScannerModule(
|
|
16
|
+
private val reactContext: ReactApplicationContext,
|
|
17
|
+
) : NativeScannerModuleSpec(reactContext) {
|
|
18
|
+
|
|
19
|
+
override fun getName(): String = NAME
|
|
20
|
+
|
|
21
|
+
override fun getTypedExportedConstants(): Map<String, Any> {
|
|
22
|
+
return mapOf("SUPPORTED_FORMATS" to BarcodeFormatMapper.supportedFormats)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
override fun checkCameraPermission(promise: Promise) {
|
|
26
|
+
promise.resolve(CameraPermissionHelper.checkPermission(reactContext))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override fun requestCameraPermission(promise: Promise) {
|
|
30
|
+
val current = CameraPermissionHelper.checkPermission(reactContext)
|
|
31
|
+
|
|
32
|
+
if (current == CameraPermissionHelper.STATUS_GRANTED) {
|
|
33
|
+
promise.resolve(current)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!CameraPermissionHelper.canRequestPermission(current)) {
|
|
38
|
+
promise.resolve(current)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
val activity = reactContext.currentActivity
|
|
43
|
+
if (activity == null) {
|
|
44
|
+
promise.resolve(CameraPermissionHelper.STATUS_DENIED)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) ==
|
|
50
|
+
PackageManager.PERMISSION_GRANTED
|
|
51
|
+
) {
|
|
52
|
+
promise.resolve(CameraPermissionHelper.STATUS_GRANTED)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
val listener = PermissionListener { requestCode, _, grantResults ->
|
|
57
|
+
if (requestCode != CAMERA_PERMISSION_REQUEST_CODE) {
|
|
58
|
+
return@PermissionListener false
|
|
59
|
+
}
|
|
60
|
+
promise.resolve(
|
|
61
|
+
CameraPermissionHelper.resolveRequestResult(reactContext, grantResults),
|
|
62
|
+
)
|
|
63
|
+
true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
CameraPermissionHelper.markPermissionRequested(reactContext)
|
|
67
|
+
|
|
68
|
+
if (activity is PermissionAwareActivity) {
|
|
69
|
+
activity.requestPermissions(
|
|
70
|
+
arrayOf(Manifest.permission.CAMERA),
|
|
71
|
+
CAMERA_PERMISSION_REQUEST_CODE,
|
|
72
|
+
listener,
|
|
73
|
+
)
|
|
74
|
+
} else {
|
|
75
|
+
ActivityCompat.requestPermissions(
|
|
76
|
+
activity,
|
|
77
|
+
arrayOf(Manifest.permission.CAMERA),
|
|
78
|
+
CAMERA_PERMISSION_REQUEST_CODE,
|
|
79
|
+
)
|
|
80
|
+
promise.resolve(CameraPermissionHelper.checkPermission(reactContext))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
override fun openCameraSettings() {
|
|
85
|
+
CameraPermissionHelper.openAppSettings(reactContext)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
override fun setTorchEnabled(enabled: Boolean) {
|
|
89
|
+
val cameraManager =
|
|
90
|
+
reactContext.getSystemService(CameraManager::class.java) ?: return
|
|
91
|
+
try {
|
|
92
|
+
val cameraId = cameraManager.cameraIdList.firstOrNull() ?: return
|
|
93
|
+
cameraManager.setTorchMode(cameraId, enabled)
|
|
94
|
+
} catch (_: Exception) {
|
|
95
|
+
// Best-effort: torch failures are non-fatal for scanning
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
companion object {
|
|
100
|
+
const val NAME = "ScannerModule"
|
|
101
|
+
const val CAMERA_PERMISSION_REQUEST_CODE = 0x5343414E
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
package com.rncustomscanner
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
4
|
+
import com.facebook.react.bridge.ReadableArray
|
|
5
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
6
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
7
|
+
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
8
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
9
|
+
import com.facebook.react.viewmanagers.ScannerViewManagerDelegate
|
|
10
|
+
import com.facebook.react.viewmanagers.ScannerViewManagerInterface
|
|
11
|
+
import com.rncustomscanner.events.BarcodeScannedEvent
|
|
12
|
+
|
|
13
|
+
class ScannerViewManager(
|
|
14
|
+
@Suppress("UNUSED_PARAMETER") context: ReactApplicationContext,
|
|
15
|
+
) : SimpleViewManager<ScannerView>(), ScannerViewManagerInterface<ScannerView> {
|
|
16
|
+
|
|
17
|
+
private val delegate: ViewManagerDelegate<ScannerView> = ScannerViewManagerDelegate(this)
|
|
18
|
+
|
|
19
|
+
override fun getDelegate(): ViewManagerDelegate<ScannerView> = delegate
|
|
20
|
+
|
|
21
|
+
override fun getName(): String = REACT_CLASS
|
|
22
|
+
|
|
23
|
+
override fun createViewInstance(context: ThemedReactContext): ScannerView {
|
|
24
|
+
return ScannerView(context)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
|
|
28
|
+
return mapOf(
|
|
29
|
+
BarcodeScannedEvent.EVENT_NAME to mapOf(
|
|
30
|
+
"registrationName" to "onBarcodeScanned",
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@ReactProp(name = "isActive", defaultBoolean = true)
|
|
36
|
+
override fun setIsActive(view: ScannerView, value: Boolean) {
|
|
37
|
+
view.setIsActive(value)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@ReactProp(name = "torchOn", defaultBoolean = false)
|
|
41
|
+
override fun setTorchOn(view: ScannerView, value: Boolean) {
|
|
42
|
+
view.setTorchOn(value)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@ReactProp(name = "scanFormats")
|
|
46
|
+
override fun setScanFormats(view: ScannerView, value: ReadableArray?) {
|
|
47
|
+
if (value == null) {
|
|
48
|
+
view.setScanFormats(null)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
val formats = ArrayList<String>(value.size())
|
|
52
|
+
for (i in 0 until value.size()) {
|
|
53
|
+
value.getString(i)?.let { formats.add(it) }
|
|
54
|
+
}
|
|
55
|
+
view.setScanFormats(formats)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
companion object {
|
|
59
|
+
const val REACT_CLASS = "ScannerView"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
package com.rncustomscanner
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.content.pm.PackageManager
|
|
5
|
+
import android.hardware.camera2.CameraManager
|
|
6
|
+
import androidx.core.app.ActivityCompat
|
|
7
|
+
import androidx.core.content.ContextCompat
|
|
8
|
+
import com.facebook.react.bridge.Promise
|
|
9
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
10
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
11
|
+
import com.facebook.react.bridge.ReactMethod
|
|
12
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
13
|
+
import com.facebook.react.modules.core.PermissionAwareActivity
|
|
14
|
+
import com.facebook.react.modules.core.PermissionListener
|
|
15
|
+
|
|
16
|
+
@ReactModule(name = ScannerModule.NAME)
|
|
17
|
+
class ScannerModule(
|
|
18
|
+
private val reactContext: ReactApplicationContext,
|
|
19
|
+
) : ReactContextBaseJavaModule(reactContext) {
|
|
20
|
+
|
|
21
|
+
override fun getName(): String = NAME
|
|
22
|
+
|
|
23
|
+
override fun getConstants(): Map<String, Any> {
|
|
24
|
+
return mapOf("SUPPORTED_FORMATS" to BarcodeFormatMapper.supportedFormats)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@ReactMethod
|
|
28
|
+
fun checkCameraPermission(promise: Promise) {
|
|
29
|
+
promise.resolve(CameraPermissionHelper.checkPermission(reactContext))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@ReactMethod
|
|
33
|
+
fun requestCameraPermission(promise: Promise) {
|
|
34
|
+
val current = CameraPermissionHelper.checkPermission(reactContext)
|
|
35
|
+
|
|
36
|
+
if (current == CameraPermissionHelper.STATUS_GRANTED) {
|
|
37
|
+
promise.resolve(current)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!CameraPermissionHelper.canRequestPermission(current)) {
|
|
42
|
+
promise.resolve(current)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
val activity = reactContext.currentActivity
|
|
47
|
+
if (activity == null) {
|
|
48
|
+
promise.resolve(CameraPermissionHelper.STATUS_DENIED)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (
|
|
53
|
+
ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) ==
|
|
54
|
+
PackageManager.PERMISSION_GRANTED
|
|
55
|
+
) {
|
|
56
|
+
promise.resolve(CameraPermissionHelper.STATUS_GRANTED)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
val listener = PermissionListener { requestCode, _, grantResults ->
|
|
61
|
+
if (requestCode != CAMERA_PERMISSION_REQUEST_CODE) {
|
|
62
|
+
return@PermissionListener false
|
|
63
|
+
}
|
|
64
|
+
promise.resolve(
|
|
65
|
+
CameraPermissionHelper.resolveRequestResult(reactContext, grantResults),
|
|
66
|
+
)
|
|
67
|
+
true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
CameraPermissionHelper.markPermissionRequested(reactContext)
|
|
71
|
+
|
|
72
|
+
if (activity is PermissionAwareActivity) {
|
|
73
|
+
activity.requestPermissions(
|
|
74
|
+
arrayOf(Manifest.permission.CAMERA),
|
|
75
|
+
CAMERA_PERMISSION_REQUEST_CODE,
|
|
76
|
+
listener,
|
|
77
|
+
)
|
|
78
|
+
} else {
|
|
79
|
+
ActivityCompat.requestPermissions(
|
|
80
|
+
activity,
|
|
81
|
+
arrayOf(Manifest.permission.CAMERA),
|
|
82
|
+
CAMERA_PERMISSION_REQUEST_CODE,
|
|
83
|
+
)
|
|
84
|
+
promise.resolve(CameraPermissionHelper.checkPermission(reactContext))
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@ReactMethod
|
|
89
|
+
fun openCameraSettings() {
|
|
90
|
+
CameraPermissionHelper.openAppSettings(reactContext)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@ReactMethod
|
|
94
|
+
fun setTorchEnabled(enabled: Boolean) {
|
|
95
|
+
val cameraManager =
|
|
96
|
+
reactContext.getSystemService(CameraManager::class.java) ?: return
|
|
97
|
+
try {
|
|
98
|
+
val cameraId = cameraManager.cameraIdList.firstOrNull() ?: return
|
|
99
|
+
cameraManager.setTorchMode(cameraId, enabled)
|
|
100
|
+
} catch (_: Exception) {
|
|
101
|
+
// Best-effort
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
companion object {
|
|
106
|
+
const val NAME = "ScannerModule"
|
|
107
|
+
const val CAMERA_PERMISSION_REQUEST_CODE = 0x5343414E
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
package com.rncustomscanner
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
4
|
+
import com.facebook.react.bridge.ReadableArray
|
|
5
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
6
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
7
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
8
|
+
import com.rncustomscanner.events.BarcodeScannedEvent
|
|
9
|
+
|
|
10
|
+
class ScannerViewManager(
|
|
11
|
+
@Suppress("UNUSED_PARAMETER") context: ReactApplicationContext,
|
|
12
|
+
) : SimpleViewManager<ScannerView>() {
|
|
13
|
+
|
|
14
|
+
override fun getName(): String = REACT_CLASS
|
|
15
|
+
|
|
16
|
+
override fun createViewInstance(context: ThemedReactContext): ScannerView {
|
|
17
|
+
return ScannerView(context)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
|
|
21
|
+
return mapOf(
|
|
22
|
+
BarcodeScannedEvent.EVENT_NAME to mapOf(
|
|
23
|
+
"registrationName" to "onBarcodeScanned",
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@ReactProp(name = "isActive", defaultBoolean = true)
|
|
29
|
+
fun setIsActive(view: ScannerView, value: Boolean) {
|
|
30
|
+
view.setIsActive(value)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@ReactProp(name = "torchOn", defaultBoolean = false)
|
|
34
|
+
fun setTorchOn(view: ScannerView, value: Boolean) {
|
|
35
|
+
view.setTorchOn(value)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@ReactProp(name = "scanFormats")
|
|
39
|
+
fun setScanFormats(view: ScannerView, value: ReadableArray?) {
|
|
40
|
+
if (value == null) {
|
|
41
|
+
view.setScanFormats(null)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
val formats = ArrayList<String>(value.size())
|
|
45
|
+
for (i in 0 until value.size()) {
|
|
46
|
+
value.getString(i)?.let { formats.add(it) }
|
|
47
|
+
}
|
|
48
|
+
view.setScanFormats(formats)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
companion object {
|
|
52
|
+
const val REACT_CLASS = "ScannerView"
|
|
53
|
+
}
|
|
54
|
+
}
|