react-native-document-processor 0.0.0-stage → 0.2.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 CHANGED
@@ -1,3 +1,208 @@
1
- # Temporary Holding Version
1
+ # react-native-document-processor
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Phiên bản **0.2.0** — thư viện React Native chụp tài liệu, tạo PDF từ ảnh và đọc
4
+ QR/barcode bằng native trên iOS và Android.
5
+
6
+ - [Repository](https://github.com/quocvu12345/react-native-document-processor)
7
+ - [Package npm](https://www.npmjs.com/package/react-native-document-processor)
8
+ - Source của phiên bản này: tag `v0.2.0`.
9
+
10
+ ## Cài đặt
11
+
12
+ ```sh
13
+ yarn add react-native-document-processor@0.2.0 --exact
14
+ # Hoặc: npm install react-native-document-processor@0.2.0 --save-exact
15
+ pod install --project-directory=ios
16
+ ```
17
+
18
+ Yêu cầu Android API 28+, iOS 15.1+ và React Native có autolinking. Đã kiểm tra
19
+ trong ứng dụng host dùng React Native 0.83. Camera Android cần Google Play services;
20
+ model ML Kit đọc barcode được đóng gói sẵn. iOS cần khai báo
21
+ `NSCameraUsageDescription` trong `Info.plist` của ứng dụng.
22
+
23
+ Rebuild native app sau khi cài hoặc nâng cấp thư viện. Reload JavaScript hoặc
24
+ cập nhật OTA không thêm được native module vào binary đã cài. Thư viện cần
25
+ native build riêng, chẳng hạn Expo development build.
26
+
27
+ Metadata giấy phép hiện là `UNLICENSED`.
28
+
29
+ ## Thay đổi trong 0.2.0
30
+
31
+ - Sửa lỗi không đọc được QR trên iOS Simulator khi Vision không tạo được
32
+ inference context: yêu cầu chỉ đọc QR sử dụng Vision revision 1 trên Simulator.
33
+ - Thiết bị iOS thật tiếp tục dùng revision mặc định. API JavaScript giữ nguyên.
34
+ - Bản sửa được đóng gói trực tiếp trong source native của npm.
35
+
36
+ ## Chụp tài liệu và tạo PDF
37
+
38
+ `scanDocument` mở camera native: VisionKit trên iOS và ML Kit Document Scanner
39
+ trên Android. Người dùng có thể phát hiện biên, crop và xác nhận các trang.
40
+
41
+ ```ts
42
+ import {
43
+ processDocumentImages,
44
+ scanDocument,
45
+ } from 'react-native-document-processor';
46
+ import RNFS from 'react-native-fs';
47
+
48
+ const capture = await scanDocument({ croppedImageQuality: 100 });
49
+ if (capture.status === 'cancel') return;
50
+
51
+ let output: Awaited<ReturnType<typeof processDocumentImages>> | undefined;
52
+ try {
53
+ output = await processDocumentImages(capture.scannedImages, {
54
+ onProgress: (completed, total) => updateProgress(completed, total),
55
+ isCancelled: () => hasLeftScreen,
56
+ });
57
+ // output.pdfUri: PDF để lưu hoặc upload
58
+ // output.pages: mã QR, trạng thái và preview của từng trang
59
+ // output.directoryUri: thư mục chứa PDF và preview, do caller sở hữu
60
+ await consumeDocument(output);
61
+ } finally {
62
+ // Chỉ dọn output sau khi mọi thao tác lưu/upload/hiển thị đã hoàn tất.
63
+ const temporaryPaths = [
64
+ ...capture.scannedImages,
65
+ ...(output ? [output.directoryUri] : []),
66
+ ];
67
+ await Promise.all(
68
+ temporaryPaths.map(path => RNFS.unlink(path).catch(() => undefined)),
69
+ );
70
+ }
71
+ ```
72
+
73
+ `updateProgress`, `hasLeftScreen` và `consumeDocument` là logic của ứng dụng host.
74
+ Ví dụ dùng `react-native-fs` để dọn file; thư viện này không phải dependency của
75
+ package và có thể được thay bằng API filesystem của ứng dụng.
76
+
77
+ `croppedImageQuality` nhận giá trị từ 0 đến 100; Android giữ JPEG gốc của scanner.
78
+ Mỗi lần chụp trả về `{ status: 'success' | 'cancel', scannedImages: string[] }`.
79
+ iOS trả tối đa 24 trang trong một lần chụp. Ứng dụng có thể gọi lại camera và gộp
80
+ các ảnh theo thứ tự trước khi tạo PDF.
81
+
82
+ `processDocumentImages` trả về các trang đánh số từ 1:
83
+
84
+ ```ts
85
+ {
86
+ pdfUri: 'file:///cache/session/document.pdf',
87
+ directoryUri: 'file:///cache/session',
88
+ pages: [
89
+ {
90
+ page: 1,
91
+ rawValues: ['doc|d|0030358'],
92
+ status: 'decoded', // 'decoded' | 'not-detected' | 'error'
93
+ previewUri: 'file:///cache/session/page-1.jpg', // tùy chọn
94
+ },
95
+ ],
96
+ }
97
+ ```
98
+
99
+ Dùng `{ scanQr: false }` khi chỉ cần PDF. Khi đó native bỏ qua đọc QR và tạo
100
+ preview. Native không xóa ảnh đầu vào. Sau thành công, caller chịu trách nhiệm
101
+ xóa output và ảnh chụp khi không còn sử dụng. Nếu hủy hoặc có lỗi trước khi
102
+ chuyển quyền sở hữu, wrapper tự giải phóng output của session.
103
+
104
+ Hủy được kiểm tra giữa các trang và trước/sau khi đóng PDF. Trang đang được
105
+ Vision/ML Kit xử lý sẽ hoàn tất trước khi dọn tài nguyên. Lỗi đọc QR nằm trong
106
+ trạng thái từng trang; lỗi decode ảnh hoặc ghi PDF hủy cả session để tránh trả
107
+ PDF thiếu trang. Lỗi tạo preview không làm mất mã đã đọc được.
108
+
109
+ ## Luồng xử lý native
110
+
111
+ ```text
112
+ Ảnh đã chụp
113
+ → decode và chuẩn hóa hướng EXIF
114
+ → đọc QR trên bitmap
115
+ → tạo preview và ghi trang PDF
116
+ → trả PDF URI, mã QR và trạng thái từng trang về JavaScript
117
+ ```
118
+
119
+ Các trang được xử lý tuần tự trên worker. Không truyền bitmap/base64 qua bridge
120
+ và không cần render lại PDF vừa tạo để đọc QR.
121
+
122
+ - iOS dùng ImageIO, Vision, Core Graphics và autorelease pool theo từng trang.
123
+ - Android dùng ImageDecoder, ML Kit barcode-scanning bundled 17.3.0 và PDF writer
124
+ ghi JPEG trực tiếp; camera dùng play-services-mlkit-document-scanner 16.0.0.
125
+ - Ảnh đầu vào được giới hạn cạnh dài 4096 px; PDF tối đa 3000 px, JPEG 92%;
126
+ preview tối đa 1000 px, JPEG 85%. Không phóng to ảnh nhỏ.
127
+ - Luồng xử lý tài liệu thử thêm bốn vùng chồng lấn 60% khi chưa thấy payload
128
+ `doc|...|<số>`. Kết quả vẫn chứa các chuỗi QR thô để ứng dụng tự parse, đối chiếu
129
+ và gộp mã theo nghiệp vụ.
130
+
131
+ ## Đọc QR/barcode từ ảnh
132
+
133
+ ```ts
134
+ import { scanBarcodes, scanQrImage } from 'react-native-document-processor';
135
+
136
+ const qr = await scanQrImage('/path/to/image.jpg');
137
+ const all = await scanBarcodes('file:///path/to/image.jpg');
138
+ const selected = await scanBarcodes('content://provider/image', {
139
+ formats: ['qr', 'ean-13'],
140
+ });
141
+ // { values: ['0030358'], codes: [{ value: '0030358', format: 'qr' }] }
142
+ ```
143
+
144
+ API chạy trên worker, giới hạn ảnh 4096 px và chuẩn hóa EXIF. Không mở camera,
145
+ tạo PDF hoặc xóa ảnh đầu vào. Hỗ trợ đường dẫn tuyệt đối, `file://` và thêm
146
+ `content://` trên Android; không nhận URL mạng hoặc base64.
147
+
148
+ Không phát hiện mã trả `{ values: [], codes: [] }`. Ảnh hỏng hoặc lỗi decoder
149
+ reject Promise. Kết quả hỗ trợ nhiều mã, loại mã trùng và giữ nguyên chuỗi,
150
+ kể cả số 0 đầu.
151
+
152
+ Các format: `qr`, `ean-13`, `ean-8`, `upc-a`, `upc-e`, `code-128`, `code-39`,
153
+ `code-93`, `codabar`, `itf`, `pdf-417`, `aztec`, `data-matrix`. iOS quy đổi UPC-A
154
+ từ EAN-13 có số 0 đầu khi yêu cầu UPC-A.
155
+
156
+ Package không render PDF có sẵn. Để đọc QR trong PDF local, ứng dụng cần render
157
+ lần lượt từng trang thành ảnh bằng một thư viện PDF, rồi gọi `scanQrImage` và
158
+ xóa ảnh tạm sau khi xử lý. Giữ tệp PDF gốc để lưu hoặc upload.
159
+
160
+ ## Phát triển và kiểm tra
161
+
162
+ Clone repository và dùng dependency `file:` trong ứng dụng host để thử source
163
+ local. Chạy autolinking/pod install rồi rebuild native khi thay đổi iOS/Android.
164
+
165
+ Repository chứa:
166
+
167
+ - `src/__tests__/index.test.ts`: kiểm tra API, format, camera, thứ tự trang,
168
+ hủy, lỗi native và quyền sở hữu file. Chạy với Jest preset React Native của
169
+ ứng dụng host; cấu hình Jest để bao gồm thư mục source của thư viện.
170
+ - `tests/check-pdf-writer.sh`: tạo PDF 30 trang bằng Java rồi đọc/render bằng
171
+ PDFKit để kiểm tra số trang, thứ tự, kích thước và hướng ảnh.
172
+ - Android instrumentation tests cho ML Kit, PDF, EXIF, nhiều QR, barcode,
173
+ cleanup và xử lý camera khi không có foreground activity.
174
+
175
+ ```sh
176
+ # Tại repository thư viện, cần macOS, Xcode và JDK
177
+ bash tests/check-pdf-writer.sh
178
+
179
+ # Tại thư mục android của ứng dụng host đã autolink, cần thiết bị ADB
180
+ ./gradlew :react-native-document-processor:connectedReleaseAndroidTest
181
+ ```
182
+
183
+ Nếu JDK không nằm trong PATH, đặt `DOCUMENT_PROCESSOR_JAVAC` và
184
+ `DOCUMENT_PROCESSOR_JAVA` thành đường dẫn executable tương ứng.
185
+
186
+ Bản sửa iOS Simulator đã được kiểm tra với PDF mẫu 30 trang, đọc đủ 10 QR trong
187
+ ứng dụng host. Camera trên thiết bị thật vẫn cần kiểm tra với giấy nhỏ/mờ/xoay,
188
+ chụp nhiều đợt và thao tác hủy. Không công bố tỷ lệ tăng tốc hoặc mức RAM chung
189
+ cho mọi thiết bị.
190
+
191
+ ## Log Android
192
+
193
+ Log native dùng tag `DocumentScan`, mặc định tắt. Bật trên thiết bị kiểm tra:
194
+
195
+ ```sh
196
+ adb shell setprop log.tag.DocumentScan DEBUG
197
+ adb logcat -v threadtime 'DocumentScan:D' 'AndroidRuntime:E' '*:S'
198
+ ```
199
+
200
+ Các sự kiện gồm `camera.*`, `session.*`, `page.*`, `pdf.complete` và `barcode.*`.
201
+ Ứng dụng có thể bổ sung sự kiện qua `logDocumentEvent(event, data)`. Log có thể
202
+ chứa mã tài liệu; chỉ bật khi cần chẩn đoán. Không ghi bitmap/base64.
203
+
204
+ Tắt khi hoàn tất hoặc khởi động lại thiết bị:
205
+
206
+ ```sh
207
+ adb shell setprop log.tag.DocumentScan INFO
208
+ ```
@@ -0,0 +1,24 @@
1
+ apply plugin: 'com.android.library'
2
+
3
+ android {
4
+ namespace 'vn.money24h.documentprocessor'
5
+ testBuildType 'release'
6
+ compileSdkVersion rootProject.ext.has('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
7
+ defaultConfig {
8
+ minSdkVersion 28
9
+ targetSdkVersion rootProject.ext.has('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35
10
+ testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
11
+ }
12
+ compileOptions {
13
+ sourceCompatibility JavaVersion.VERSION_1_8
14
+ targetCompatibility JavaVersion.VERSION_1_8
15
+ }
16
+ }
17
+
18
+ dependencies {
19
+ implementation 'com.facebook.react:react-android'
20
+ implementation 'com.google.mlkit:barcode-scanning:17.3.0'
21
+ implementation 'com.google.android.gms:play-services-mlkit-document-scanner:16.0.0'
22
+ androidTestImplementation 'androidx.test:runner:1.6.1'
23
+ androidTestImplementation 'com.google.zxing:core:3.5.2'
24
+ }
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,81 @@
1
+ package vn.money24h.documentprocessor;
2
+
3
+ import android.graphics.Bitmap;
4
+ import com.facebook.react.bridge.Arguments;
5
+ import com.facebook.react.bridge.ReadableArray;
6
+ import com.facebook.react.bridge.WritableArray;
7
+ import com.facebook.react.bridge.WritableMap;
8
+ import com.google.android.gms.tasks.Tasks;
9
+ import com.google.mlkit.vision.barcode.BarcodeScanner;
10
+ import com.google.mlkit.vision.barcode.BarcodeScannerOptions;
11
+ import com.google.mlkit.vision.barcode.BarcodeScanning;
12
+ import com.google.mlkit.vision.barcode.common.Barcode;
13
+ import com.google.mlkit.vision.common.InputImage;
14
+ import java.util.LinkedHashMap;
15
+ import java.util.LinkedHashSet;
16
+ import java.util.Map;
17
+ import java.util.Set;
18
+
19
+ /** Stateless image decoding on the processor worker; no document session/files. */
20
+ final class BarcodeImageScanner {
21
+ private static final Map<String, Integer> FORMATS = new LinkedHashMap<>();
22
+ static {
23
+ FORMATS.put("qr", Barcode.FORMAT_QR_CODE);
24
+ FORMATS.put("ean-13", Barcode.FORMAT_EAN_13);
25
+ FORMATS.put("ean-8", Barcode.FORMAT_EAN_8);
26
+ FORMATS.put("upc-a", Barcode.FORMAT_UPC_A);
27
+ FORMATS.put("upc-e", Barcode.FORMAT_UPC_E);
28
+ FORMATS.put("code-128", Barcode.FORMAT_CODE_128);
29
+ FORMATS.put("code-39", Barcode.FORMAT_CODE_39);
30
+ FORMATS.put("code-93", Barcode.FORMAT_CODE_93);
31
+ FORMATS.put("codabar", Barcode.FORMAT_CODABAR);
32
+ FORMATS.put("itf", Barcode.FORMAT_ITF);
33
+ FORMATS.put("pdf-417", Barcode.FORMAT_PDF417);
34
+ FORMATS.put("aztec", Barcode.FORMAT_AZTEC);
35
+ FORMATS.put("data-matrix", Barcode.FORMAT_DATA_MATRIX);
36
+ }
37
+
38
+ private BarcodeImageScanner() { }
39
+
40
+ static WritableMap scan(Bitmap bitmap, ReadableArray formats) throws Exception {
41
+ if (formats == null || formats.size() == 0) {
42
+ throw new IllegalArgumentException("At least one barcode format is required.");
43
+ }
44
+ int mask = 0;
45
+ for (int i = 0; i < formats.size(); i++) {
46
+ Integer format = FORMATS.get(formats.getString(i));
47
+ if (format == null) { throw new IllegalArgumentException("Unsupported barcode format."); }
48
+ mask |= format;
49
+ }
50
+ BarcodeScanner scanner = BarcodeScanning.getClient(new BarcodeScannerOptions.Builder()
51
+ .setBarcodeFormats(mask).build());
52
+ try {
53
+ WritableArray codes = Arguments.createArray();
54
+ Set<String> values = new LinkedHashSet<>();
55
+ Set<String> uniqueCodes = new LinkedHashSet<>();
56
+ for (Barcode barcode : Tasks.await(scanner.process(InputImage.fromBitmap(bitmap, 0)))) {
57
+ String value = barcode.getRawValue();
58
+ if (value == null || value.isEmpty()) { continue; }
59
+ for (Map.Entry<String, Integer> format : FORMATS.entrySet()) {
60
+ if (format.getValue() != barcode.getFormat() || (mask & format.getValue()) == 0) { continue; }
61
+ if (uniqueCodes.add(format.getKey() + "\n" + value)) {
62
+ WritableMap code = Arguments.createMap();
63
+ code.putString("value", value);
64
+ code.putString("format", format.getKey());
65
+ codes.pushMap(code);
66
+ values.add(value);
67
+ }
68
+ break;
69
+ }
70
+ }
71
+ WritableArray rawValues = Arguments.createArray();
72
+ for (String value : values) { rawValues.pushString(value); }
73
+ WritableMap result = Arguments.createMap();
74
+ result.putArray("values", rawValues);
75
+ result.putArray("codes", codes);
76
+ return result;
77
+ } finally {
78
+ scanner.close();
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,163 @@
1
+ package vn.money24h.documentprocessor;
2
+
3
+ import android.app.Activity;
4
+ import android.content.Intent;
5
+ import android.net.Uri;
6
+ import android.os.SystemClock;
7
+ import com.facebook.react.bridge.Arguments;
8
+ import com.facebook.react.bridge.BaseActivityEventListener;
9
+ import com.facebook.react.bridge.Promise;
10
+ import com.facebook.react.bridge.ReactApplicationContext;
11
+ import com.facebook.react.bridge.UiThreadUtil;
12
+ import com.facebook.react.bridge.WritableArray;
13
+ import com.facebook.react.bridge.WritableMap;
14
+ import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions;
15
+ import com.google.mlkit.vision.documentscanner.GmsDocumentScanning;
16
+ import com.google.mlkit.vision.documentscanner.GmsDocumentScanningResult;
17
+ import java.io.File;
18
+ import java.io.FileOutputStream;
19
+ import java.io.IOException;
20
+ import java.io.InputStream;
21
+ import java.util.ArrayList;
22
+ import java.util.List;
23
+ import java.util.concurrent.ExecutorService;
24
+ import java.util.concurrent.Executors;
25
+
26
+ /** Owns camera lifecycle and copies scanner results into files owned by our caller. */
27
+ final class DocumentCapture extends BaseActivityEventListener {
28
+ private static final int REQUEST_SCAN = 24861;
29
+ private final ReactApplicationContext context;
30
+ private final ExecutorService worker = Executors.newSingleThreadExecutor();
31
+ private Promise pending;
32
+ private boolean closed;
33
+ private long startedAt;
34
+
35
+ DocumentCapture(ReactApplicationContext context) {
36
+ this.context = context;
37
+ context.addActivityEventListener(this);
38
+ }
39
+
40
+ synchronized void scan(Promise promise) {
41
+ if (closed || pending != null) {
42
+ DocumentScanLog.event("camera.busy", "closed=" + closed);
43
+ promise.reject("DOCUMENT_CAPTURE_BUSY", "Document camera is busy or closed.");
44
+ return;
45
+ }
46
+ pending = promise;
47
+ startedAt = SystemClock.elapsedRealtime();
48
+ DocumentScanLog.event("camera.start", "format=JPEG mode=FULL");
49
+ UiThreadUtil.runOnUiThread(() -> {
50
+ if (!isActive(promise)) { return; }
51
+ Activity activity = context.getCurrentActivity();
52
+ if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
53
+ fail(promise, new IOException("No foreground activity for document camera."));
54
+ return;
55
+ }
56
+ try {
57
+ GmsDocumentScannerOptions options = new GmsDocumentScannerOptions.Builder()
58
+ .setResultFormats(GmsDocumentScannerOptions.RESULT_FORMAT_JPEG)
59
+ .setScannerMode(GmsDocumentScannerOptions.SCANNER_MODE_FULL)
60
+ .build();
61
+ GmsDocumentScanning.getClient(options).getStartScanIntent(activity)
62
+ .addOnSuccessListener(sender -> {
63
+ if (!isActive(promise)) { return; }
64
+ try {
65
+ DocumentScanLog.event("camera.ready", "elapsedMs=" + (SystemClock.elapsedRealtime() - startedAt));
66
+ activity.startIntentSenderForResult(sender, REQUEST_SCAN, null, 0, 0, 0);
67
+ } catch (Exception error) {
68
+ fail(promise, error);
69
+ }
70
+ })
71
+ .addOnFailureListener(error -> fail(promise, error));
72
+ } catch (Exception error) {
73
+ fail(promise, error);
74
+ }
75
+ });
76
+ }
77
+
78
+ private synchronized boolean isActive(Promise promise) {
79
+ return !closed && pending == promise;
80
+ }
81
+
82
+ private synchronized void fail(Promise promise, Exception error) {
83
+ if (pending == promise) {
84
+ pending = null;
85
+ DocumentScanLog.error("camera.error", error);
86
+ promise.reject("DOCUMENT_CAPTURE_ERROR", error.getMessage(), error);
87
+ }
88
+ }
89
+
90
+ private synchronized boolean complete(Promise promise, String status, List<File> files) {
91
+ if (!isActive(promise)) { return false; }
92
+ WritableArray images = Arguments.createArray();
93
+ for (File file : files) { images.pushString(Uri.fromFile(file).toString()); }
94
+ WritableMap result = Arguments.createMap();
95
+ result.putString("status", status);
96
+ result.putArray("scannedImages", images);
97
+ pending = null;
98
+ DocumentScanLog.event("camera.complete", "status=" + status + " pages=" + files.size()
99
+ + " elapsedMs=" + (SystemClock.elapsedRealtime() - startedAt));
100
+ promise.resolve(result);
101
+ return true;
102
+ }
103
+
104
+ @Override
105
+ public synchronized void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
106
+ if (requestCode != REQUEST_SCAN || pending == null || closed) { return; }
107
+ Promise promise = pending;
108
+ DocumentScanLog.event("camera.result", "resultCode=" + resultCode);
109
+ if (resultCode == Activity.RESULT_CANCELED) {
110
+ complete(promise, "cancel", new ArrayList<>());
111
+ return;
112
+ }
113
+ GmsDocumentScanningResult result = GmsDocumentScanningResult.fromActivityResultIntent(data);
114
+ if (resultCode != Activity.RESULT_OK || result == null || result.getPages() == null
115
+ || result.getPages().isEmpty()) {
116
+ fail(promise, new IOException("Document camera returned no pages."));
117
+ return;
118
+ }
119
+ List<GmsDocumentScanningResult.Page> pages = new ArrayList<>(result.getPages());
120
+ DocumentScanLog.event("camera.copy.start", "pages=" + pages.size());
121
+ worker.execute(() -> {
122
+ long copyStartedAt = SystemClock.elapsedRealtime();
123
+ List<File> files = new ArrayList<>();
124
+ boolean transferred = false;
125
+ try {
126
+ byte[] buffer = new byte[64 * 1024];
127
+ for (GmsDocumentScanningResult.Page page : pages) {
128
+ if (!isActive(promise)) { return; }
129
+ File file = File.createTempFile("document-capture-", ".jpg", context.getCacheDir());
130
+ files.add(file);
131
+ try (InputStream input = context.getContentResolver().openInputStream(page.getImageUri());
132
+ FileOutputStream output = new FileOutputStream(file)) {
133
+ if (input == null) { throw new IOException("Cannot read scanned page."); }
134
+ int count;
135
+ while ((count = input.read(buffer)) != -1) { output.write(buffer, 0, count); }
136
+ }
137
+ if (file.length() == 0) { throw new IOException("Scanned page is empty."); }
138
+ DocumentScanLog.event("camera.copy.page", "page=" + files.size() + " bytes=" + file.length());
139
+ }
140
+ DocumentScanLog.event("camera.copy.complete", "pages=" + files.size()
141
+ + " elapsedMs=" + (SystemClock.elapsedRealtime() - copyStartedAt));
142
+ transferred = complete(promise, "success", files);
143
+ } catch (Exception error) {
144
+ fail(promise, error);
145
+ } finally {
146
+ if (!transferred) {
147
+ for (File file : files) { file.delete(); }
148
+ }
149
+ }
150
+ });
151
+ }
152
+
153
+ synchronized void invalidate() {
154
+ if (closed) { return; }
155
+ closed = true;
156
+ if (pending != null) {
157
+ pending.reject("DOCUMENT_CAPTURE_CANCELLED", "Document camera was closed.");
158
+ pending = null;
159
+ }
160
+ context.removeActivityEventListener(this);
161
+ worker.shutdown();
162
+ }
163
+ }