@scr2em/capacitor-scanner 6.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.
@@ -0,0 +1,489 @@
1
+ package com.leadliaion.capacitorscanner;
2
+
3
+ import android.Manifest;
4
+ import android.content.Intent;
5
+ import android.graphics.Color;
6
+ import android.net.Uri;
7
+ import android.provider.Settings;
8
+ import android.util.Base64;
9
+ import android.util.DisplayMetrics;
10
+ import android.util.Log;
11
+ import android.util.Size;
12
+ import android.view.Surface;
13
+ import android.view.ViewGroup;
14
+ import android.webkit.WebView;
15
+ import android.view.View;
16
+
17
+ import com.getcapacitor.JSArray;
18
+
19
+ import androidx.annotation.NonNull;
20
+ import androidx.camera.core.CameraSelector;
21
+
22
+ import androidx.camera.core.ExperimentalGetImage;
23
+ import androidx.camera.core.ImageAnalysis;
24
+ import androidx.camera.core.ImageCapture;
25
+ import androidx.camera.core.ImageProxy;
26
+ import androidx.camera.core.ImageCaptureException;
27
+ import androidx.camera.core.Preview;
28
+ import androidx.camera.lifecycle.ProcessCameraProvider;
29
+ import androidx.camera.view.PreviewView;
30
+ import androidx.core.content.ContextCompat;
31
+
32
+ import com.getcapacitor.JSObject;
33
+ import com.getcapacitor.PermissionState;
34
+ import com.getcapacitor.Plugin;
35
+ import com.getcapacitor.PluginCall;
36
+ import com.getcapacitor.PluginMethod;
37
+ import com.getcapacitor.annotation.CapacitorPlugin;
38
+
39
+ import com.getcapacitor.annotation.Permission;
40
+ import com.getcapacitor.annotation.PermissionCallback;
41
+ import android.widget.FrameLayout;
42
+
43
+ import com.google.common.util.concurrent.ListenableFuture;
44
+ import com.google.mlkit.vision.barcode.common.Barcode;
45
+ import com.google.mlkit.vision.barcode.BarcodeScanner;
46
+ import com.google.mlkit.vision.barcode.BarcodeScannerOptions;
47
+ import com.google.mlkit.vision.barcode.BarcodeScanning;
48
+ import com.google.mlkit.vision.common.InputImage;
49
+
50
+ import java.io.ByteArrayOutputStream;
51
+ import java.io.IOException;
52
+ import java.util.concurrent.ExecutionException;
53
+ import java.util.concurrent.Executor;
54
+ import java.util.concurrent.Executors;
55
+ import java.util.*;
56
+ import java.util.concurrent.atomic.AtomicBoolean;
57
+
58
+ import android.view.OrientationEventListener;
59
+
60
+ @ExperimentalGetImage
61
+ @CapacitorPlugin(
62
+ name = "CapacitorScanner",
63
+ permissions = {
64
+ @Permission(strings = { Manifest.permission.CAMERA }, alias = "camera")
65
+ })
66
+ public class CapacitorScannerPlugin extends Plugin {
67
+
68
+ private PreviewView previewView;
69
+ private ProcessCameraProvider cameraProvider;
70
+ private BarcodeScanner scanner;
71
+ private final Map<String, VoteStatus> scannedCodesVotes = new HashMap<>();
72
+ private final int voteThreshold = 2;
73
+ private final Executor executor = Executors.newSingleThreadExecutor();
74
+ private final AtomicBoolean isScanning = new AtomicBoolean(false);
75
+ private FrameLayout containerView;
76
+
77
+ private OrientationEventListener orientationEventListener;
78
+ private Preview preview;
79
+ private ImageAnalysis imageAnalysis;
80
+ private ImageCapture imageCapture;
81
+
82
+
83
+ @Override
84
+ public void load() {
85
+ super.load();
86
+ scanner = BarcodeScanning.getClient(new BarcodeScannerOptions.Builder()
87
+ .setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
88
+ .build());
89
+ }
90
+
91
+ @PluginMethod
92
+ public void startScanning(PluginCall call) {
93
+ echo("startScanning");
94
+ if (isScanning.get()) {
95
+ call.resolve();
96
+ return;
97
+ }
98
+
99
+ if (getPermissionState("camera") != PermissionState.GRANTED) {
100
+ echo("requestPermissionForAlias");
101
+ requestPermissionForAlias("camera", call, "cameraPermsCallback");
102
+ call.reject("Camera permission is required to start scanning");
103
+ return;
104
+ }
105
+
106
+ getActivity().runOnUiThread(() -> {
107
+ isScanning.set(true);
108
+
109
+ try {
110
+ String cameraDirectionStr = call.getString("cameraDirection", "BACK");
111
+ int lensFacing;
112
+ if (cameraDirectionStr != null) {
113
+ lensFacing = cameraDirectionStr.equals("FRONT") ? CameraSelector.LENS_FACING_FRONT : CameraSelector.LENS_FACING_BACK;
114
+ } else {
115
+ lensFacing = CameraSelector.LENS_FACING_BACK;
116
+ }
117
+
118
+ JSArray formatsArray = call.getArray("formats");
119
+ BarcodeScannerOptions.Builder optionsBuilder = new BarcodeScannerOptions.Builder();
120
+
121
+ if (formatsArray != null && formatsArray.length() > 0) {
122
+ List<Integer> formatList = new ArrayList<>();
123
+ for (int i = 0; i < formatsArray.length(); i++) {
124
+ String formatStr = formatsArray.getString(i);
125
+ int formatInt = BarcodeFormatHelper.stringToBarcodeFormat(formatStr);
126
+ if (formatInt != -1) {
127
+ formatList.add(formatInt);
128
+ }
129
+ }
130
+ if (!formatList.isEmpty()) {
131
+ int firstFormat = formatList.get(0);
132
+ int[] additionalFormats = new int[formatList.size() - 1];
133
+ for (int i = 1; i < formatList.size(); i++) {
134
+ additionalFormats[i - 1] = formatList.get(i);
135
+ }
136
+ optionsBuilder.setBarcodeFormats(firstFormat, additionalFormats);
137
+ } else {
138
+ optionsBuilder.setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS);
139
+ }
140
+ } else {
141
+ optionsBuilder.setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS);
142
+ }
143
+
144
+ scanner = BarcodeScanning.getClient(optionsBuilder.build());
145
+
146
+ previewView = new PreviewView(getContext());
147
+ previewView.setLayoutParams(new FrameLayout.LayoutParams(
148
+ FrameLayout.LayoutParams.MATCH_PARENT,
149
+ FrameLayout.LayoutParams.MATCH_PARENT));
150
+ previewView.setScaleType(PreviewView.ScaleType.FILL_CENTER);
151
+
152
+ containerView = new FrameLayout(getContext());
153
+ containerView.setLayoutParams(new FrameLayout.LayoutParams(
154
+ FrameLayout.LayoutParams.MATCH_PARENT,
155
+ FrameLayout.LayoutParams.MATCH_PARENT));
156
+
157
+ containerView.addView(previewView);
158
+
159
+ WebView webView = getBridge().getWebView();
160
+ ViewGroup webViewParent = (ViewGroup) webView.getParent();
161
+
162
+ int webViewIndex = webViewParent.indexOfChild(webView);
163
+ webViewParent.addView(containerView, webViewIndex);
164
+
165
+ hideWebViewBackground();
166
+
167
+ ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(getContext());
168
+
169
+ cameraProviderFuture.addListener(() -> {
170
+ try {
171
+ cameraProvider = cameraProviderFuture.get();
172
+ bindCamera(cameraProvider, previewView, lensFacing);
173
+
174
+ orientationEventListener = new OrientationEventListener(getContext()) {
175
+ @Override
176
+ public void onOrientationChanged(int orientation) {
177
+ if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) {
178
+ return;
179
+ }
180
+ int rotation = getDisplaySurfaceRotation();
181
+
182
+ if (imageAnalysis != null) {
183
+ imageAnalysis.setTargetRotation(rotation);
184
+ }
185
+ if (preview != null) {
186
+ preview.setTargetRotation(rotation);
187
+ }
188
+ if (imageCapture != null) {
189
+ imageCapture.setTargetRotation(rotation);
190
+ }
191
+ }
192
+ };
193
+
194
+ if (orientationEventListener.canDetectOrientation()) {
195
+ orientationEventListener.enable();
196
+ }
197
+
198
+ call.resolve();
199
+ } catch (ExecutionException | InterruptedException e) {
200
+
201
+ echo("Failed to initialize camera: " + e.getMessage());
202
+ call.reject("Failed to initialize camera: " + e.getMessage());
203
+ }
204
+ }, ContextCompat.getMainExecutor(getContext()));
205
+ } catch (Exception e) {
206
+ echo("Error setting up camera preview: " + e.getMessage());
207
+ call.reject("Error setting up camera preview: " + e.getMessage());
208
+ }
209
+ });
210
+ }
211
+
212
+ private void bindCamera(@NonNull ProcessCameraProvider cameraProvider, PreviewView previewView, int lensFacing) {
213
+ cameraProvider.unbindAll();
214
+
215
+ DisplayMetrics metrics = new DisplayMetrics();
216
+ getActivity().getWindowManager().getDefaultDisplay().getRealMetrics(metrics);
217
+ int screenWidth = metrics.widthPixels;
218
+ int screenHeight = metrics.heightPixels;
219
+
220
+ int rotation = getDisplaySurfaceRotation();
221
+ boolean isPortrait = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180;
222
+
223
+ int targetWidth = isPortrait ? screenWidth : screenHeight;
224
+ int targetHeight = isPortrait ? screenHeight : screenWidth;
225
+
226
+ Size targetResolution = new Size(targetWidth, targetHeight);
227
+
228
+ preview = new Preview.Builder()
229
+ .setTargetResolution(targetResolution)
230
+ .setTargetRotation(rotation)
231
+ .build();
232
+
233
+ CameraSelector cameraSelector = new CameraSelector.Builder()
234
+ .requireLensFacing(lensFacing)
235
+ .build();
236
+
237
+ imageAnalysis = new ImageAnalysis.Builder()
238
+ .setTargetResolution(targetResolution)
239
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
240
+ .setTargetRotation(rotation)
241
+ .build();
242
+
243
+ imageAnalysis.setAnalyzer(executor, new BarcodeAnalyzer());
244
+
245
+ imageCapture = new ImageCapture.Builder()
246
+ .setTargetResolution(targetResolution)
247
+ .setTargetRotation(rotation)
248
+ .build();
249
+
250
+ try {
251
+ cameraProvider.bindToLifecycle(getActivity(), cameraSelector, preview, imageAnalysis, imageCapture);
252
+ preview.setSurfaceProvider(previewView.getSurfaceProvider());
253
+ } catch (Exception e) {
254
+ echo("Failed to bind camera to lifecycle: " + e.getMessage());
255
+ }
256
+ }
257
+
258
+ private int getDisplaySurfaceRotation() {
259
+ return getActivity().getWindowManager().getDefaultDisplay().getRotation();
260
+ }
261
+
262
+ @ExperimentalGetImage private class BarcodeAnalyzer implements ImageAnalysis.Analyzer {
263
+ @Override
264
+ public void analyze(@NonNull ImageProxy imageProxy) {
265
+ @androidx.camera.core.ExperimentalGetImage
266
+ android.media.Image mediaImage = imageProxy.getImage();
267
+ if (mediaImage != null) {
268
+ InputImage image = InputImage.fromMediaImage(mediaImage, imageProxy.getImageInfo().getRotationDegrees());
269
+ scanner.process(image)
270
+ .addOnSuccessListener(executor,CapacitorScannerPlugin.this::processBarcodes)
271
+ .addOnFailureListener(executor,e -> {
272
+ echo("Failed to process image: " + e.getMessage());
273
+ })
274
+ .addOnCompleteListener(task -> imageProxy.close());
275
+ } else {
276
+ imageProxy.close();
277
+ }
278
+ }
279
+ }
280
+ private void processBarcodes(List<Barcode> barcodes) {
281
+ for (Barcode barcode : barcodes) {
282
+ String rawValue = barcode.getRawValue();
283
+ if (rawValue == null || rawValue.isEmpty()) {
284
+ byte[] rawBytes = barcode.getRawBytes();
285
+ if (rawBytes != null && rawBytes.length > 0) {
286
+ rawValue = bytesToString(rawBytes);
287
+ } else {
288
+ echo("Barcode has no rawValue or rawBytes, skipping");
289
+ }
290
+ }
291
+ echo("Processing barcode with rawValue: " + rawValue);
292
+ int format = barcode.getFormat();
293
+
294
+ VoteStatus voteStatus = scannedCodesVotes.get(rawValue);
295
+ if (voteStatus == null) {
296
+ voteStatus = new VoteStatus(0, false);
297
+ scannedCodesVotes.put(rawValue, voteStatus);
298
+ }
299
+
300
+ if (!voteStatus.done) {
301
+ voteStatus.votes += 1;
302
+
303
+ echo("Barcode " + rawValue + " votes: " + voteStatus.votes + " done: ");
304
+
305
+ if (voteStatus.votes >= voteThreshold) {
306
+ voteStatus.done = true;
307
+
308
+ JSObject data = new JSObject();
309
+ data.put("scannedCode", rawValue);
310
+ data.put("format", BarcodeFormatHelper.barcodeFormatToString(format));
311
+ notifyListeners("barcodeScanned", data, true);
312
+ echo( "Barcode NO_MORE" + rawValue + " scanned with format: " + BarcodeFormatHelper.barcodeFormatToString(format));
313
+ }
314
+ }
315
+ }
316
+ }
317
+
318
+ private String bytesToString(byte[] bytes) {
319
+ char[] chars = new char[bytes.length];
320
+ for (int i = 0; i < bytes.length; i++) {
321
+ chars[i] = (char) (bytes[i] & 0xFF);
322
+ }
323
+ return new String(chars);
324
+ }
325
+
326
+
327
+ @PluginMethod
328
+ public void stopScanning(PluginCall call) {
329
+ echo("CUSTOM_LOG_IDENTIFIER stopScanning");
330
+ getActivity().runOnUiThread(() -> {
331
+
332
+ if (scanner != null) {
333
+ scanner.close();
334
+ scanner = null;
335
+ }
336
+
337
+ if (cameraProvider != null) {
338
+ cameraProvider.unbindAll();
339
+ cameraProvider = null;
340
+ }
341
+
342
+ if (previewView != null) {
343
+ ViewGroup parentView = (ViewGroup) previewView.getParent();
344
+ if (parentView != null) {
345
+ parentView.removeView(previewView);
346
+ }
347
+ previewView = null;
348
+ }
349
+ scannedCodesVotes.clear();
350
+ isScanning.set(false);
351
+
352
+ showWebViewBackground();
353
+
354
+
355
+ if (orientationEventListener != null) {
356
+ orientationEventListener.disable();
357
+ orientationEventListener = null;
358
+ }
359
+
360
+ call.resolve();
361
+ });
362
+ }
363
+
364
+ @PluginMethod
365
+ public void capturePhoto(PluginCall call) {
366
+ if (imageCapture == null) {
367
+ call.reject("Camera is not set up or running");
368
+ return;
369
+ }
370
+
371
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
372
+
373
+ ImageCapture.OutputFileOptions outputOptions =
374
+ new ImageCapture.OutputFileOptions.Builder(outputStream).build();
375
+
376
+ imageCapture.takePicture(outputOptions, ContextCompat.getMainExecutor(getContext()),
377
+ new ImageCapture.OnImageSavedCallback() {
378
+ @Override
379
+ public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
380
+ try {
381
+ byte[] bytes = outputStream.toByteArray();
382
+
383
+ String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
384
+
385
+ JSObject ret = new JSObject();
386
+ ret.put("imageBase64", "data:image/jpeg;base64," + base64);
387
+
388
+ call.resolve(ret);
389
+ } catch (Exception e) {
390
+ call.reject("Failed to process image: " + e.getMessage());
391
+ } finally {
392
+ try {
393
+ outputStream.close();
394
+ } catch (IOException e) {
395
+ echo("Failed to close output stream: " + e.getMessage());
396
+ }
397
+ }
398
+ }
399
+
400
+ @Override
401
+ public void onError(@NonNull ImageCaptureException exception) {
402
+ call.reject("Photo capture failed: " + exception.getMessage());
403
+ echo("Photo capture failed: " + exception.getMessage());
404
+ try {
405
+ outputStream.close();
406
+ } catch (IOException e) {
407
+ echo("Failed to close output stream: " + e.getMessage());
408
+ }
409
+ }
410
+ });
411
+ }
412
+
413
+
414
+ @PluginMethod
415
+ public void checkPermissions(PluginCall call) {
416
+ PermissionState cameraPermState = getPermissionState("camera");
417
+ String status = "prompt";
418
+ if (cameraPermState == PermissionState.DENIED) {
419
+ status = "denied";
420
+ } else if (cameraPermState == PermissionState.GRANTED) {
421
+ status = "granted";
422
+ }
423
+
424
+ JSObject ret = new JSObject();
425
+ ret.put("camera", status);
426
+ // If permission is denied, camera will not open
427
+ // and frontend must handle this case and request permission.
428
+ call.resolve(ret);
429
+ }
430
+
431
+ @PluginMethod
432
+ public void requestPermissions(PluginCall call) {
433
+ requestPermissionForAlias("camera", call, "cameraPermsCallback");
434
+ }
435
+
436
+ @PermissionCallback
437
+ private void cameraPermsCallback(PluginCall call) {
438
+ checkPermissions(call);
439
+ }
440
+
441
+ @PluginMethod
442
+ public void openSettings(PluginCall call) {
443
+ try {
444
+ Intent intent = new Intent();
445
+ intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
446
+ Uri uri = Uri.fromParts("package", getContext().getPackageName(), null);
447
+ intent.setData(uri);
448
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
449
+ getContext().startActivity(intent);
450
+ call.resolve();
451
+ } catch (Exception ex) {
452
+ echo("Failed to open settings: " + ex.getMessage());
453
+ call.reject("Failed to open settings: " + ex.getMessage());
454
+ }
455
+ }
456
+
457
+ private void logLongMessage(String message) {
458
+ if (message.length() > 4000) {
459
+ int chunkCount = message.length() / 4000;
460
+ for (int i = 0; i <= chunkCount; i++) {
461
+ int max = 4000 * (i + 1);
462
+ if (max >= message.length()) {
463
+ Log.d("SCANNER_LOG_IDENTIFIER", message.substring(4000 * i));
464
+ } else {
465
+ Log.d("SCANNER_LOG_IDENTIFIER", message.substring(4000 * i, max));
466
+ }
467
+ }
468
+ } else {
469
+ Log.d("SCANNER_LOG_IDENTIFIER", message);
470
+ }
471
+ }
472
+
473
+ private void echo(String value) {
474
+ logLongMessage(value);
475
+ }
476
+
477
+ private void hideWebViewBackground() {
478
+ WebView webView = getBridge().getWebView();
479
+ webView.setBackgroundColor(Color.TRANSPARENT);
480
+ webView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
481
+ }
482
+
483
+ private void showWebViewBackground() {
484
+ WebView webView = getBridge().getWebView();
485
+ webView.setBackgroundColor(Color.WHITE);
486
+ webView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
487
+ }
488
+
489
+ }