@unif/react-native-hms-scan 0.6.2 → 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/README.md +3 -2
- package/android/src/main/AndroidManifest.xml +0 -9
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanImage.kt +135 -0
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanModule.kt +163 -147
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanResultMapper.kt +17 -9
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanView.kt +57 -19
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanViewManager.kt +1 -1
- package/ios/HmsScanModule.mm +13 -37
- package/ios/HmsScanResultMapper.h +4 -3
- package/ios/HmsScanResultMapper.mm +31 -19
- package/ios/HmsScanView.h +1 -1
- package/ios/HmsScanView.mm +140 -25
- package/lib/module/HmsScanView/HmsScanView.js +66 -0
- package/lib/module/HmsScanView/HmsScanView.js.map +1 -0
- package/lib/module/HmsScanView/HmsScanView.web.js +26 -0
- package/lib/module/HmsScanView/HmsScanView.web.js.map +1 -0
- package/lib/module/HmsScanView/types.js +4 -0
- package/lib/module/HmsScanView/types.js.map +1 -0
- package/lib/module/HmsScanViewNativeComponent.ts +7 -5
- package/lib/module/NativeHmsScan.js +1 -1
- package/lib/module/NativeHmsScan.js.map +1 -1
- package/lib/module/ScanError.js +12 -0
- package/lib/module/ScanError.js.map +1 -0
- package/lib/module/Scanner/DeniedOverlay.js +1 -1
- package/lib/module/Scanner/DeniedOverlay.js.map +1 -1
- package/lib/module/Scanner/ResultFail.js +3 -2
- package/lib/module/Scanner/ResultFail.js.map +1 -1
- package/lib/module/Scanner/ResultFocus/ResultFocus.js +56 -0
- package/lib/module/Scanner/ResultFocus/ResultFocus.js.map +1 -0
- package/lib/module/Scanner/ResultFocus/styles.js +39 -0
- package/lib/module/Scanner/ResultFocus/styles.js.map +1 -0
- package/lib/module/Scanner/ResultFocus/types.js +4 -0
- package/lib/module/Scanner/ResultFocus/types.js.map +1 -0
- package/lib/module/Scanner/Scanner.js +11 -310
- package/lib/module/Scanner/Scanner.js.map +1 -1
- package/lib/module/Scanner/Scanner.web.js +29 -0
- package/lib/module/Scanner/Scanner.web.js.map +1 -0
- package/lib/module/Scanner/ScannerInner.js +310 -0
- package/lib/module/Scanner/ScannerInner.js.map +1 -0
- package/lib/module/Scanner/styles.js +11 -0
- package/lib/module/Scanner/styles.js.map +1 -0
- package/lib/module/Scanner/types.js +4 -0
- package/lib/module/Scanner/types.js.map +1 -0
- package/lib/module/constants.js +20 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/decodeImage.js +29 -24
- package/lib/module/decodeImage.js.map +1 -1
- package/lib/module/decodeImage.web.js +7 -0
- package/lib/module/decodeImage.web.js.map +1 -0
- package/lib/module/errors.js +24 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/format.js +53 -47
- package/lib/module/format.js.map +1 -1
- package/lib/module/index.js +5 -19
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +8 -0
- package/lib/module/index.web.js.map +1 -0
- package/lib/module/mock.js +2 -23
- package/lib/module/mock.js.map +1 -1
- package/lib/module/permissions.js +24 -20
- package/lib/module/permissions.js.map +1 -1
- package/lib/module/permissions.web.js +10 -0
- package/lib/module/permissions.web.js.map +1 -0
- package/lib/module/types.js +1 -45
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/src/HmsScanView/HmsScanView.d.ts +3 -0
- package/lib/typescript/src/HmsScanView/HmsScanView.d.ts.map +1 -0
- package/lib/typescript/src/HmsScanView/HmsScanView.web.d.ts +3 -0
- package/lib/typescript/src/HmsScanView/HmsScanView.web.d.ts.map +1 -0
- package/lib/typescript/src/HmsScanView/types.d.ts +12 -0
- package/lib/typescript/src/HmsScanView/types.d.ts.map +1 -0
- package/lib/typescript/src/HmsScanViewNativeComponent.d.ts +6 -4
- package/lib/typescript/src/HmsScanViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/NativeHmsScan.d.ts +3 -5
- package/lib/typescript/src/NativeHmsScan.d.ts.map +1 -1
- package/lib/typescript/src/ScanError.d.ts +7 -0
- package/lib/typescript/src/ScanError.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/DeniedOverlay.d.ts.map +1 -1
- package/lib/typescript/src/Scanner/ResultFail.d.ts +2 -1
- package/lib/typescript/src/Scanner/ResultFail.d.ts.map +1 -1
- package/lib/typescript/src/Scanner/ResultFocus/ResultFocus.d.ts +4 -0
- package/lib/typescript/src/Scanner/ResultFocus/ResultFocus.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ResultFocus/styles.d.ts +36 -0
- package/lib/typescript/src/Scanner/ResultFocus/styles.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ResultFocus/types.d.ts +8 -0
- package/lib/typescript/src/Scanner/ResultFocus/types.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/Scanner.d.ts +2 -45
- package/lib/typescript/src/Scanner/Scanner.d.ts.map +1 -1
- package/lib/typescript/src/Scanner/Scanner.web.d.ts +3 -0
- package/lib/typescript/src/Scanner/Scanner.web.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ScannerInner.d.ts +3 -0
- package/lib/typescript/src/Scanner/ScannerInner.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/styles.d.ts +7 -0
- package/lib/typescript/src/Scanner/styles.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/types.d.ts +20 -0
- package/lib/typescript/src/Scanner/types.d.ts.map +1 -0
- package/lib/typescript/src/constants.d.ts +8 -0
- package/lib/typescript/src/constants.d.ts.map +1 -0
- package/lib/typescript/src/decodeImage.d.ts +2 -12
- package/lib/typescript/src/decodeImage.d.ts.map +1 -1
- package/lib/typescript/src/decodeImage.web.d.ts +3 -0
- package/lib/typescript/src/decodeImage.web.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +6 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/format.d.ts +15 -12
- package/lib/typescript/src/format.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +6 -7
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/index.web.d.ts +9 -0
- package/lib/typescript/src/index.web.d.ts.map +1 -0
- package/lib/typescript/src/mock.d.ts +9 -8
- package/lib/typescript/src/mock.d.ts.map +1 -1
- package/lib/typescript/src/permissions.d.ts +4 -8
- package/lib/typescript/src/permissions.d.ts.map +1 -1
- package/lib/typescript/src/permissions.web.d.ts +4 -0
- package/lib/typescript/src/permissions.web.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +31 -56
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +8 -1
- package/src/HmsScanView/HmsScanView.tsx +67 -0
- package/src/HmsScanView/HmsScanView.web.tsx +22 -0
- package/src/HmsScanView/types.ts +17 -0
- package/src/HmsScanViewNativeComponent.ts +7 -5
- package/src/NativeHmsScan.ts +3 -5
- package/src/ScanError.ts +13 -0
- package/src/Scanner/DeniedOverlay.tsx +4 -2
- package/src/Scanner/ResultFail.tsx +23 -5
- package/src/Scanner/ResultFocus/ResultFocus.tsx +43 -0
- package/src/Scanner/ResultFocus/styles.ts +25 -0
- package/src/Scanner/ResultFocus/types.ts +8 -0
- package/src/Scanner/Scanner.tsx +14 -432
- package/src/Scanner/Scanner.web.tsx +26 -0
- package/src/Scanner/ScannerInner.tsx +391 -0
- package/src/Scanner/styles.ts +6 -0
- package/src/Scanner/types.ts +36 -0
- package/src/constants.ts +55 -0
- package/src/decodeImage.ts +35 -29
- package/src/decodeImage.web.ts +8 -0
- package/src/errors.ts +26 -0
- package/src/format.ts +77 -76
- package/src/index.tsx +17 -32
- package/src/index.web.tsx +22 -0
- package/src/mock.ts +21 -29
- package/src/permissions.ts +33 -20
- package/src/permissions.web.ts +9 -0
- package/src/types.ts +38 -126
- package/lib/module/HmsScanView.js +0 -43
- package/lib/module/HmsScanView.js.map +0 -1
- package/lib/module/Scanner/ResultFocus.js +0 -171
- package/lib/module/Scanner/ResultFocus.js.map +0 -1
- package/lib/typescript/src/HmsScanView.d.ts +0 -33
- package/lib/typescript/src/HmsScanView.d.ts.map +0 -1
- package/lib/typescript/src/Scanner/ResultFocus.d.ts +0 -14
- package/lib/typescript/src/Scanner/ResultFocus.d.ts.map +0 -1
- package/src/HmsScanView.tsx +0 -87
- package/src/Scanner/ResultFocus.tsx +0 -122
package/README.md
CHANGED
|
@@ -27,12 +27,13 @@ Android 内置识别引擎,不要求华为手机或 HMS Core。两端均不需
|
|
|
27
27
|
```ts
|
|
28
28
|
import { decodeImage } from '@unif/react-native-hms-scan';
|
|
29
29
|
|
|
30
|
-
const results = await decodeImage(
|
|
30
|
+
const results = await decodeImage({
|
|
31
|
+
uri: 'file:///path/to/photo.jpg',
|
|
31
32
|
formats: ['QR_CODE'],
|
|
32
33
|
});
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
正常识别但没有发现码时返回 `[]`;读取或解码失败抛出 `
|
|
36
|
+
正常识别但没有发现码时返回 `[]`;读取或解码失败抛出 `ScanError`。函数不下载网络图片,应用需先准备本地文件。
|
|
36
37
|
|
|
37
38
|
## 文档与开发
|
|
38
39
|
|
|
@@ -3,15 +3,6 @@
|
|
|
3
3
|
<!-- Camera preview / scanning (RemoteView). -->
|
|
4
4
|
<uses-permission android:name="android.permission.CAMERA" />
|
|
5
5
|
|
|
6
|
-
<!-- Reading a local image for decodeImage().
|
|
7
|
-
Android 13+ (API 33) uses the scoped media permission; older devices fall
|
|
8
|
-
back to READ_EXTERNAL_STORAGE (capped at API 32 so the merger doesn't
|
|
9
|
-
force it on newer OS versions). -->
|
|
10
|
-
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
|
11
|
-
<uses-permission
|
|
12
|
-
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
|
13
|
-
android:maxSdkVersion="32" />
|
|
14
|
-
|
|
15
6
|
<!-- Camera is used but not strictly required (gallery decode works without it). -->
|
|
16
7
|
<uses-feature
|
|
17
8
|
android:name="android.hardware.camera"
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
package com.unif.reactnativehmsscan
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.BitmapFactory
|
|
5
|
+
import android.graphics.Matrix
|
|
6
|
+
import android.media.ExifInterface
|
|
7
|
+
import android.net.Uri
|
|
8
|
+
import java.io.Closeable
|
|
9
|
+
import java.io.File
|
|
10
|
+
import java.io.FileInputStream
|
|
11
|
+
import java.io.IOException
|
|
12
|
+
|
|
13
|
+
internal class ScanImageFailure(
|
|
14
|
+
val code: String,
|
|
15
|
+
message: String,
|
|
16
|
+
) : IOException(message)
|
|
17
|
+
|
|
18
|
+
/** Owns one upright bitmap; points are returned in the upright original image. */
|
|
19
|
+
internal class HmsScanImage private constructor(
|
|
20
|
+
val bitmap: Bitmap,
|
|
21
|
+
val scaleX: Double,
|
|
22
|
+
val scaleY: Double,
|
|
23
|
+
) : Closeable {
|
|
24
|
+
override fun close() {
|
|
25
|
+
if (!bitmap.isRecycled) bitmap.recycle()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
companion object {
|
|
29
|
+
// At most 32 MiB per ARGB bitmap, temporarily 64 MiB during EXIF rotation.
|
|
30
|
+
// The vendor has additional working memory. Only one image job runs at once.
|
|
31
|
+
private const val MAX_PIXELS = 8_388_608L
|
|
32
|
+
private const val MAX_SOURCE_PIXELS = 268_435_456L
|
|
33
|
+
private const val MAX_SOURCE_EDGE = 32_768
|
|
34
|
+
private const val MAX_FILE_BYTES = 128L * 1024 * 1024
|
|
35
|
+
|
|
36
|
+
fun read(uri: String): HmsScanImage {
|
|
37
|
+
val parsed = Uri.parse(uri)
|
|
38
|
+
val path = parsed.path
|
|
39
|
+
if (!parsed.scheme.equals("file", ignoreCase = true) || path.isNullOrEmpty()) {
|
|
40
|
+
throw ScanImageFailure("E_INVALID_INPUT", "需要可读的 file URI")
|
|
41
|
+
}
|
|
42
|
+
val file = File(path)
|
|
43
|
+
if (!file.isFile || !file.canRead()) {
|
|
44
|
+
throw ScanImageFailure("E_IMAGE_LOAD_FAILED", "无法读取本地图片")
|
|
45
|
+
}
|
|
46
|
+
if (file.length() > MAX_FILE_BYTES) {
|
|
47
|
+
throw ScanImageFailure("E_INVALID_INPUT", "图片文件超过 128 MiB")
|
|
48
|
+
}
|
|
49
|
+
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
|
50
|
+
BitmapFactory.decodeFile(path, bounds)
|
|
51
|
+
val width = bounds.outWidth
|
|
52
|
+
val height = bounds.outHeight
|
|
53
|
+
if (width <= 0 || height <= 0) {
|
|
54
|
+
throw ScanImageFailure("E_IMAGE_LOAD_FAILED", "无法读取图片尺寸")
|
|
55
|
+
}
|
|
56
|
+
if (width > MAX_SOURCE_EDGE || height > MAX_SOURCE_EDGE || width.toLong() * height > MAX_SOURCE_PIXELS) {
|
|
57
|
+
throw ScanImageFailure("E_INVALID_INPUT", "图片尺寸超过安全解码范围")
|
|
58
|
+
}
|
|
59
|
+
var sample = 1
|
|
60
|
+
while (((width + sample - 1) / sample).toLong() * ((height + sample - 1) / sample) > MAX_PIXELS) {
|
|
61
|
+
sample *= 2
|
|
62
|
+
}
|
|
63
|
+
val options =
|
|
64
|
+
BitmapFactory.Options().apply {
|
|
65
|
+
inSampleSize = sample
|
|
66
|
+
inPreferredConfig = Bitmap.Config.ARGB_8888
|
|
67
|
+
}
|
|
68
|
+
var bitmap =
|
|
69
|
+
BitmapFactory.decodeFile(path, options)
|
|
70
|
+
?: throw ScanImageFailure("E_IMAGE_LOAD_FAILED", "无法解码本地图片")
|
|
71
|
+
try {
|
|
72
|
+
if (bitmap.width.toLong() * bitmap.height > MAX_PIXELS || bitmap.allocationByteCount.toLong() > MAX_PIXELS * 4) {
|
|
73
|
+
throw ScanImageFailure("E_INVALID_INPUT", "图片解码超过内存预算")
|
|
74
|
+
}
|
|
75
|
+
// Unsupported/missing optional EXIF does not discard an otherwise valid image.
|
|
76
|
+
val orientation =
|
|
77
|
+
try {
|
|
78
|
+
FileInputStream(file).use {
|
|
79
|
+
ExifInterface(it).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
|
|
80
|
+
}
|
|
81
|
+
} catch (_: IOException) {
|
|
82
|
+
ExifInterface.ORIENTATION_NORMAL
|
|
83
|
+
}
|
|
84
|
+
val matrix = Matrix()
|
|
85
|
+
when (orientation) {
|
|
86
|
+
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> {
|
|
87
|
+
matrix.setScale(-1f, 1f)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
ExifInterface.ORIENTATION_ROTATE_180 -> {
|
|
91
|
+
matrix.setRotate(180f)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
ExifInterface.ORIENTATION_FLIP_VERTICAL -> {
|
|
95
|
+
matrix.setScale(1f, -1f)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
ExifInterface.ORIENTATION_TRANSPOSE -> {
|
|
99
|
+
matrix.setRotate(90f)
|
|
100
|
+
matrix.postScale(-1f, 1f)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
ExifInterface.ORIENTATION_ROTATE_90 -> {
|
|
104
|
+
matrix.setRotate(90f)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
ExifInterface.ORIENTATION_TRANSVERSE -> {
|
|
108
|
+
matrix.setRotate(270f)
|
|
109
|
+
matrix.postScale(-1f, 1f)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
ExifInterface.ORIENTATION_ROTATE_270 -> {
|
|
113
|
+
matrix.setRotate(270f)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (orientation in ExifInterface.ORIENTATION_FLIP_HORIZONTAL..ExifInterface.ORIENTATION_ROTATE_270) {
|
|
117
|
+
val upright = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
|
118
|
+
if (upright !== bitmap) {
|
|
119
|
+
bitmap.recycle()
|
|
120
|
+
bitmap = upright
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
val swapsAxes = orientation in ExifInterface.ORIENTATION_TRANSPOSE..ExifInterface.ORIENTATION_ROTATE_270
|
|
124
|
+
return HmsScanImage(
|
|
125
|
+
bitmap,
|
|
126
|
+
(if (swapsAxes) height else width).toDouble() / bitmap.width,
|
|
127
|
+
(if (swapsAxes) width else height).toDouble() / bitmap.height,
|
|
128
|
+
)
|
|
129
|
+
} catch (error: Throwable) {
|
|
130
|
+
if (!bitmap.isRecycled) bitmap.recycle()
|
|
131
|
+
throw error
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -3,200 +3,216 @@ package com.unif.reactnativehmsscan
|
|
|
3
3
|
import android.Manifest
|
|
4
4
|
import android.app.Activity
|
|
5
5
|
import android.content.pm.PackageManager
|
|
6
|
-
import android.graphics.Bitmap
|
|
7
|
-
import android.graphics.BitmapFactory
|
|
8
|
-
import android.net.Uri
|
|
9
6
|
import androidx.core.app.ActivityCompat
|
|
10
7
|
import androidx.core.content.ContextCompat
|
|
8
|
+
import com.facebook.react.bridge.LifecycleEventListener
|
|
11
9
|
import com.facebook.react.bridge.Promise
|
|
12
10
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
11
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
13
12
|
import com.facebook.react.modules.core.PermissionAwareActivity
|
|
14
13
|
import com.facebook.react.modules.core.PermissionListener
|
|
15
14
|
import com.huawei.hms.hmsscankit.ScanUtil
|
|
16
15
|
import com.huawei.hms.ml.scan.HmsScan
|
|
17
16
|
import com.huawei.hms.ml.scan.HmsScanAnalyzerOptions
|
|
18
|
-
import java.
|
|
19
|
-
import java.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* - getCameraPermissionStatus() / requestCameraPermission(): camera permission.
|
|
25
|
-
*/
|
|
17
|
+
import java.util.concurrent.ArrayBlockingQueue
|
|
18
|
+
import java.util.concurrent.RejectedExecutionException
|
|
19
|
+
import java.util.concurrent.ThreadPoolExecutor
|
|
20
|
+
import java.util.concurrent.TimeUnit
|
|
21
|
+
|
|
22
|
+
/** Independent image decoding and camera permission boundaries. */
|
|
26
23
|
class HmsScanModule(
|
|
27
24
|
reactContext: ReactApplicationContext,
|
|
28
|
-
) : NativeHmsScanSpec(reactContext)
|
|
29
|
-
|
|
25
|
+
) : NativeHmsScanSpec(reactContext),
|
|
26
|
+
LifecycleEventListener {
|
|
27
|
+
// One allocated image at a time, with at most four waiting file borrowers.
|
|
28
|
+
private val imageQueue =
|
|
29
|
+
ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS, ArrayBlockingQueue(4)) { work ->
|
|
30
|
+
Thread(work, "HmsScan-image").apply { isDaemon = true }
|
|
31
|
+
}
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
@Volatile private var invalidated = false
|
|
34
|
+
|
|
35
|
+
// Permission requests and host callbacks are serialized on the UI thread.
|
|
36
|
+
private class PermissionBatch(
|
|
37
|
+
val activity: Activity,
|
|
38
|
+
val promises: MutableList<Promise>,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
private var permissionBatch: PermissionBatch? = null
|
|
42
|
+
|
|
43
|
+
override fun getName(): String = NAME
|
|
32
44
|
|
|
33
|
-
/**
|
|
34
|
-
* Decode barcodes/QR codes from a local image. Loads the bitmap from a
|
|
35
|
-
* file:// / content:// URI or an absolute path, runs ScanUtil.decodeWithBitmap
|
|
36
|
-
* (photo mode), and resolves the ScanResult[] JSON. Never rejects on "no code
|
|
37
|
-
* found" — it resolves an empty array, matching the JS contract.
|
|
38
|
-
*/
|
|
39
45
|
override fun decodeImage(
|
|
40
46
|
uri: String,
|
|
41
47
|
formatsCsv: String,
|
|
42
48
|
promise: Promise,
|
|
43
49
|
) {
|
|
44
|
-
val
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
val work = ImageWork(uri, formatsCsv, promise)
|
|
51
|
+
synchronized(imageQueue) {
|
|
52
|
+
if (invalidated) {
|
|
53
|
+
work.rejectUnavailable()
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
imageQueue.execute(work)
|
|
58
|
+
} catch (_: RejectedExecutionException) {
|
|
59
|
+
work.rejectUnavailable()
|
|
60
|
+
}
|
|
48
61
|
}
|
|
62
|
+
}
|
|
49
63
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
private inner class ImageWork(
|
|
65
|
+
val uri: String,
|
|
66
|
+
val formatsCsv: String,
|
|
67
|
+
val promise: Promise,
|
|
68
|
+
) : Runnable {
|
|
69
|
+
fun rejectUnavailable() {
|
|
70
|
+
promise.reject("E_UNAVAILABLE", "图片识别不可用或等待队列已满")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
override fun run() {
|
|
74
|
+
if (invalidated) {
|
|
75
|
+
rejectUnavailable()
|
|
76
|
+
return
|
|
59
77
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
try {
|
|
79
|
+
// use closes the bitmap before the promise settles and the caller releases its file.
|
|
80
|
+
val json =
|
|
81
|
+
HmsScanImage.read(uri).use { image ->
|
|
82
|
+
val types = HmsScanResultMapper.parseFormatsCsv(formatsCsv)
|
|
83
|
+
val creator = HmsScanAnalyzerOptions.Creator().setPhotoMode(true)
|
|
84
|
+
if (types != null && types.isNotEmpty()) {
|
|
85
|
+
creator.setHmsScanTypes(types.first(), *types.drop(1).toIntArray())
|
|
86
|
+
} else {
|
|
87
|
+
creator.setHmsScanTypes(HmsScan.ALL_SCAN_TYPE)
|
|
88
|
+
}
|
|
89
|
+
val scans = ScanUtil.decodeWithBitmap(reactApplicationContext, image.bitmap, creator.create())
|
|
90
|
+
HmsScanResultMapper.toJson(scans, image.scaleX, image.scaleY)
|
|
91
|
+
}
|
|
92
|
+
promise.resolve(json)
|
|
93
|
+
} catch (error: ScanImageFailure) {
|
|
94
|
+
promise.reject(error.code, error.message, error)
|
|
95
|
+
} catch (error: InvalidScanResponse) {
|
|
96
|
+
promise.reject("E_INVALID_RESPONSE", error.message, error)
|
|
97
|
+
} catch (error: Exception) {
|
|
98
|
+
promise.reject("E_DECODE_FAILED", error.message ?: "图片识别失败", error)
|
|
99
|
+
} catch (error: OutOfMemoryError) {
|
|
100
|
+
promise.reject("E_DECODE_FAILED", "图片识别内存不足", error)
|
|
69
101
|
}
|
|
70
102
|
}
|
|
71
103
|
}
|
|
72
104
|
|
|
73
|
-
/**
|
|
74
|
-
|
|
105
|
+
/** Android does not distinguish never asked from blocked in a read-only query. */
|
|
106
|
+
override fun getCameraPermissionStatus(promise: Promise) {
|
|
75
107
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
var stream: InputStream? = null
|
|
82
|
-
try {
|
|
83
|
-
stream = resolver.openInputStream(parsed)
|
|
84
|
-
if (stream == null) null else BitmapFactory.decodeStream(stream)
|
|
85
|
-
} finally {
|
|
86
|
-
stream?.close()
|
|
87
|
-
}
|
|
88
|
-
}
|
|
108
|
+
promise.resolve(if (hasCameraPermission()) GRANTED else DENIED)
|
|
109
|
+
} catch (error: Exception) {
|
|
110
|
+
promise.reject("E_UNAVAILABLE", "读取相机权限失败", error)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
89
113
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
114
|
+
override fun requestCameraPermission(promise: Promise) {
|
|
115
|
+
UiThreadUtil.runOnUiThread {
|
|
116
|
+
if (invalidated) {
|
|
117
|
+
promise.reject("E_UNAVAILABLE", "扫码模块已释放")
|
|
118
|
+
return@runOnUiThread
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
if (hasCameraPermission()) {
|
|
122
|
+
promise.resolve(GRANTED)
|
|
123
|
+
return@runOnUiThread
|
|
94
124
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
125
|
+
val activity = reactApplicationContext.currentActivity
|
|
126
|
+
if (activity !is PermissionAwareActivity) {
|
|
127
|
+
promise.reject("E_NO_ACTIVITY", "没有可申请相机权限的界面")
|
|
128
|
+
return@runOnUiThread
|
|
129
|
+
}
|
|
130
|
+
permissionBatch?.let { pending ->
|
|
131
|
+
if (pending.activity === activity) {
|
|
132
|
+
pending.promises.add(promise)
|
|
133
|
+
return@runOnUiThread
|
|
134
|
+
}
|
|
135
|
+
rejectPermissions(pending, "E_NO_ACTIVITY", "相机权限所属界面已结束")
|
|
136
|
+
}
|
|
137
|
+
val batch = PermissionBatch(activity, mutableListOf(promise))
|
|
138
|
+
permissionBatch = batch
|
|
139
|
+
reactApplicationContext.addLifecycleEventListener(this)
|
|
140
|
+
val listener =
|
|
141
|
+
PermissionListener { requestCode, _, grantResults ->
|
|
142
|
+
if (requestCode != CAMERA_PERMISSION_REQUEST_CODE) {
|
|
143
|
+
return@PermissionListener false
|
|
144
|
+
}
|
|
145
|
+
if (permissionBatch !== batch) return@PermissionListener true
|
|
146
|
+
if (grantResults.isEmpty()) {
|
|
147
|
+
rejectPermissions(batch, "E_UNAVAILABLE", "相机权限申请未完成")
|
|
148
|
+
} else {
|
|
149
|
+
val status =
|
|
150
|
+
when {
|
|
151
|
+
grantResults[0] == PackageManager.PERMISSION_GRANTED -> GRANTED
|
|
152
|
+
ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.CAMERA) -> DENIED
|
|
153
|
+
else -> BLOCKED
|
|
154
|
+
}
|
|
155
|
+
finishPermissions(batch).forEach { it.resolve(status) }
|
|
156
|
+
}
|
|
157
|
+
true
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
activity.requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE, listener)
|
|
161
|
+
} catch (error: Exception) {
|
|
162
|
+
rejectPermissions(batch, "E_UNAVAILABLE", error.message ?: "申请相机权限失败")
|
|
99
163
|
}
|
|
164
|
+
} catch (error: Exception) {
|
|
165
|
+
promise.reject("E_UNAVAILABLE", "申请相机权限失败", error)
|
|
100
166
|
}
|
|
101
|
-
} catch (e: Throwable) {
|
|
102
|
-
null
|
|
103
167
|
}
|
|
168
|
+
}
|
|
104
169
|
|
|
105
|
-
private fun
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
170
|
+
private fun finishPermissions(batch: PermissionBatch): List<Promise> {
|
|
171
|
+
if (permissionBatch !== batch) return emptyList()
|
|
172
|
+
permissionBatch = null
|
|
173
|
+
reactApplicationContext.removeLifecycleEventListener(this)
|
|
174
|
+
return batch.promises.toList().also { batch.promises.clear() }
|
|
109
175
|
}
|
|
110
176
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
* "blocked" / "undetermined" from a plain denial (shouldShowRequestPermissionRationale
|
|
118
|
-
* is false for both the never-asked and permanently-denied states), so a
|
|
119
|
-
* not-granted result is reported as "denied". The blocked/undetermined nuance is
|
|
120
|
-
* resolved by requestCameraPermission() via the post-request rationale check.
|
|
121
|
-
*/
|
|
122
|
-
override fun getCameraPermissionStatus(promise: Promise) {
|
|
123
|
-
promise.resolve(if (hasCameraPermission()) GRANTED else DENIED)
|
|
177
|
+
private fun rejectPermissions(
|
|
178
|
+
batch: PermissionBatch,
|
|
179
|
+
code: String,
|
|
180
|
+
message: String,
|
|
181
|
+
) {
|
|
182
|
+
finishPermissions(batch).forEach { it.reject(code, message) }
|
|
124
183
|
}
|
|
125
184
|
|
|
126
|
-
|
|
127
|
-
* Request the camera permission (system dialog if needed) and resolve the
|
|
128
|
-
* resulting status:
|
|
129
|
-
* - granted: permission held.
|
|
130
|
-
* - denied: rejected but the app may ask again (rationale should be shown).
|
|
131
|
-
* - blocked: rejected with "don't ask again" (rationale will not be shown).
|
|
132
|
-
*/
|
|
133
|
-
override fun requestCameraPermission(promise: Promise) {
|
|
134
|
-
if (hasCameraPermission()) {
|
|
135
|
-
promise.resolve(GRANTED)
|
|
136
|
-
return
|
|
137
|
-
}
|
|
185
|
+
override fun onHostResume() = Unit
|
|
138
186
|
|
|
139
|
-
|
|
140
|
-
if (activity == null || activity !is PermissionAwareActivity) {
|
|
141
|
-
// Can't surface a system dialog without a PermissionAwareActivity; report the
|
|
142
|
-
// current (not-granted) state rather than hanging the promise.
|
|
143
|
-
promise.reject(
|
|
144
|
-
E_NO_ACTIVITY,
|
|
145
|
-
"Cannot request camera permission: no current PermissionAwareActivity",
|
|
146
|
-
)
|
|
147
|
-
return
|
|
148
|
-
}
|
|
187
|
+
override fun onHostPause() = Unit
|
|
149
188
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (requestCode != CAMERA_PERMISSION_REQUEST_CODE) {
|
|
154
|
-
return@PermissionListener false
|
|
155
|
-
}
|
|
156
|
-
val granted =
|
|
157
|
-
grantResults.isNotEmpty() &&
|
|
158
|
-
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
|
159
|
-
val status =
|
|
160
|
-
when {
|
|
161
|
-
granted -> GRANTED
|
|
162
|
-
|
|
163
|
-
// After a denial: rationale==true means the user can be asked again;
|
|
164
|
-
// rationale==false means "don't ask again" (blocked).
|
|
165
|
-
ActivityCompat.shouldShowRequestPermissionRationale(
|
|
166
|
-
activity,
|
|
167
|
-
Manifest.permission.CAMERA,
|
|
168
|
-
) -> DENIED
|
|
169
|
-
|
|
170
|
-
else -> BLOCKED
|
|
171
|
-
}
|
|
172
|
-
promise.resolve(status)
|
|
173
|
-
true
|
|
174
|
-
}
|
|
189
|
+
override fun onHostDestroy() {
|
|
190
|
+
permissionBatch?.let { rejectPermissions(it, "E_NO_ACTIVITY", "相机权限所属界面已结束") }
|
|
191
|
+
}
|
|
175
192
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
193
|
+
override fun invalidate() {
|
|
194
|
+
val waiting = mutableListOf<Runnable>()
|
|
195
|
+
synchronized(imageQueue) {
|
|
196
|
+
invalidated = true
|
|
197
|
+
imageQueue.queue.drainTo(waiting)
|
|
198
|
+
// Do not interrupt an active vendor read or return its file before it finishes.
|
|
199
|
+
imageQueue.shutdown()
|
|
200
|
+
}
|
|
201
|
+
waiting.forEach { (it as ImageWork).rejectUnavailable() }
|
|
202
|
+
UiThreadUtil.runOnUiThread {
|
|
203
|
+
permissionBatch?.let { rejectPermissions(it, "E_UNAVAILABLE", "扫码模块已释放") }
|
|
204
|
+
}
|
|
205
|
+
super.invalidate()
|
|
181
206
|
}
|
|
182
207
|
|
|
183
208
|
private fun hasCameraPermission(): Boolean =
|
|
184
|
-
ContextCompat.checkSelfPermission(
|
|
185
|
-
reactApplicationContext,
|
|
186
|
-
Manifest.permission.CAMERA,
|
|
187
|
-
) == PackageManager.PERMISSION_GRANTED
|
|
209
|
+
ContextCompat.checkSelfPermission(reactApplicationContext, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
|
188
210
|
|
|
189
211
|
companion object {
|
|
190
212
|
const val NAME = NativeHmsScanSpec.NAME
|
|
191
|
-
|
|
192
213
|
private const val GRANTED = "granted"
|
|
193
214
|
private const val DENIED = "denied"
|
|
194
215
|
private const val BLOCKED = "blocked"
|
|
195
|
-
|
|
196
|
-
private const val E_IMAGE_LOAD_FAILED = "E_IMAGE_LOAD_FAILED"
|
|
197
|
-
private const val E_DECODE_FAILED = "E_DECODE_FAILED"
|
|
198
|
-
private const val E_NO_ACTIVITY = "E_NO_ACTIVITY"
|
|
199
|
-
|
|
200
|
-
private const val CAMERA_PERMISSION_REQUEST_CODE = 0x48_4D // "HM"
|
|
216
|
+
private const val CAMERA_PERMISSION_REQUEST_CODE = 0x48_4D
|
|
201
217
|
}
|
|
202
218
|
}
|
|
@@ -15,22 +15,28 @@ import org.json.JSONObject
|
|
|
15
15
|
* "cornerPoints":[{"x":10,"y":20}]}]
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
|
+
internal class InvalidScanResponse(
|
|
19
|
+
message: String,
|
|
20
|
+
) : IllegalArgumentException(message)
|
|
21
|
+
|
|
18
22
|
internal object HmsScanResultMapper {
|
|
19
23
|
/**
|
|
20
24
|
* Serialize an array of [HmsScan] into the ScanResult[] JSON string. The element
|
|
21
25
|
* type is nullable because the HMS APIs (decodeWithBitmap / OnResultCallback)
|
|
22
26
|
* hand back a Java array that can contain nulls.
|
|
23
27
|
*/
|
|
24
|
-
fun toJson(
|
|
28
|
+
fun toJson(
|
|
29
|
+
scans: Array<out HmsScan?>?,
|
|
30
|
+
scaleX: Double = 1.0,
|
|
31
|
+
scaleY: Double = 1.0,
|
|
32
|
+
): String {
|
|
25
33
|
val array = JSONArray()
|
|
26
34
|
if (scans != null) {
|
|
27
35
|
for (scan in scans) {
|
|
28
36
|
if (scan == null) continue
|
|
29
37
|
val value = scan.getOriginalValue()
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (value.isNullOrEmpty()) continue
|
|
33
|
-
array.put(toJsonObject(scan, value))
|
|
38
|
+
if (value == null) throw InvalidScanResponse("ScanKit returned a result without text")
|
|
39
|
+
array.put(toJsonObject(scan, value, scaleX, scaleY))
|
|
34
40
|
}
|
|
35
41
|
}
|
|
36
42
|
return array.toString()
|
|
@@ -39,6 +45,8 @@ internal object HmsScanResultMapper {
|
|
|
39
45
|
private fun toJsonObject(
|
|
40
46
|
scan: HmsScan,
|
|
41
47
|
value: String,
|
|
48
|
+
scaleX: Double,
|
|
49
|
+
scaleY: Double,
|
|
42
50
|
): JSONObject {
|
|
43
51
|
val obj = JSONObject()
|
|
44
52
|
obj.put("value", value)
|
|
@@ -53,8 +61,8 @@ internal object HmsScanResultMapper {
|
|
|
53
61
|
if (point == null) continue
|
|
54
62
|
points.put(
|
|
55
63
|
JSONObject().apply {
|
|
56
|
-
put("x", point.x)
|
|
57
|
-
put("y", point.y)
|
|
64
|
+
put("x", point.x * scaleX)
|
|
65
|
+
put("y", point.y * scaleY)
|
|
58
66
|
},
|
|
59
67
|
)
|
|
60
68
|
}
|
|
@@ -66,7 +74,7 @@ internal object HmsScanResultMapper {
|
|
|
66
74
|
return obj
|
|
67
75
|
}
|
|
68
76
|
|
|
69
|
-
/** HmsScan.getScanType() (barcode symbology) -> unified
|
|
77
|
+
/** HmsScan.getScanType() (barcode symbology) -> unified ScanFormat string. */
|
|
70
78
|
fun mapScanType(scanType: Int): String =
|
|
71
79
|
when (scanType) {
|
|
72
80
|
HmsScan.QRCODE_SCAN_TYPE -> "QR_CODE"
|
|
@@ -105,7 +113,7 @@ internal object HmsScanResultMapper {
|
|
|
105
113
|
return if (types.isEmpty()) null else types.toIntArray()
|
|
106
114
|
}
|
|
107
115
|
|
|
108
|
-
/** Unified
|
|
116
|
+
/** Unified ScanFormat string -> HmsScan.*_SCAN_TYPE (null when unknown). */
|
|
109
117
|
private fun mapFormatString(format: String): Int? =
|
|
110
118
|
when (format) {
|
|
111
119
|
"QR_CODE" -> HmsScan.QRCODE_SCAN_TYPE
|