@xbone-3/cordova-plugin-mlkit-barcodescanner 4.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/CHANGELOG.md +51 -0
- package/LICENSE.md +21 -0
- package/README.md +260 -0
- package/package.json +67 -0
- package/plugin.xml +133 -0
- package/src/android/build-extras.gradle +14 -0
- package/src/android/res/assets/beep.ogg +0 -0
- package/src/android/res/drawable/flashlight.png +0 -0
- package/src/android/res/drawable/torch_active.xml +12 -0
- package/src/android/res/drawable/torch_inactive.xml +12 -0
- package/src/android/res/layout/capture_activity.xml +97 -0
- package/src/android/res/values/strings-de.xml +20 -0
- package/src/android/res/values/strings-en.xml +20 -0
- package/src/android/src/CaptureActivity.java +762 -0
- package/src/android/src/MLKitBarcodeScanner.java +221 -0
- package/src/android/src/utils/BitmapUtils.java +286 -0
- package/src/android/src/utils/FrameMetadata.java +69 -0
- package/src/ios/CDViOSScanner.h +21 -0
- package/src/ios/CDViOSScanner.m +183 -0
- package/src/ios/CameraViewController.h +36 -0
- package/src/ios/CameraViewController.m +503 -0
- package/src/ios/Sounds/beep.caf +0 -0
- package/src/ios/de.lproj/Localizable.strings +8 -0
- package/src/ios/en.lproj/Localizable.strings +8 -0
- package/www/BarcodeScanner.contract.d.ts +10 -0
- package/www/BarcodeScanner.contract.js +10 -0
- package/www/BarcodeScanner.plugin.d.ts +8 -0
- package/www/BarcodeScanner.plugin.js +145 -0
- package/www/Detector.d.ts +29 -0
- package/www/Interface.d.ts +65 -0
- package/www/Options.d.ts +2 -0
- package/www/util/Object.d.ts +3 -0
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
package com.mobisys.cordova.plugins.mlkit.barcode.scanner;
|
|
2
|
+
|
|
3
|
+
import android.Manifest;
|
|
4
|
+
import android.annotation.SuppressLint;
|
|
5
|
+
import android.content.DialogInterface;
|
|
6
|
+
import android.content.Intent;
|
|
7
|
+
import android.content.pm.PackageManager;
|
|
8
|
+
import android.graphics.Bitmap;
|
|
9
|
+
import android.graphics.Canvas;
|
|
10
|
+
import android.graphics.Color;
|
|
11
|
+
import android.graphics.Paint;
|
|
12
|
+
import android.graphics.PixelFormat;
|
|
13
|
+
import android.graphics.Path;
|
|
14
|
+
import android.graphics.Point;
|
|
15
|
+
import android.graphics.PorterDuff;
|
|
16
|
+
import android.graphics.Rect;
|
|
17
|
+
import android.graphics.RectF;
|
|
18
|
+
import android.os.Bundle;
|
|
19
|
+
|
|
20
|
+
import android.view.GestureDetector;
|
|
21
|
+
import android.view.MotionEvent;
|
|
22
|
+
import android.view.ScaleGestureDetector;
|
|
23
|
+
import android.view.SurfaceHolder;
|
|
24
|
+
import android.view.SurfaceView;
|
|
25
|
+
import android.view.View;
|
|
26
|
+
import android.widget.Button;
|
|
27
|
+
import android.widget.ImageButton;
|
|
28
|
+
import android.widget.ImageView;
|
|
29
|
+
import android.widget.TextView;
|
|
30
|
+
|
|
31
|
+
import androidx.annotation.NonNull;
|
|
32
|
+
import androidx.appcompat.app.AlertDialog;
|
|
33
|
+
import androidx.appcompat.app.AppCompatActivity;
|
|
34
|
+
import androidx.camera.core.AspectRatio;
|
|
35
|
+
import androidx.camera.core.Camera;
|
|
36
|
+
import androidx.camera.core.CameraSelector;
|
|
37
|
+
import androidx.camera.core.ImageAnalysis;
|
|
38
|
+
import androidx.camera.core.ImageProxy;
|
|
39
|
+
import androidx.camera.core.Preview;
|
|
40
|
+
import androidx.camera.lifecycle.ProcessCameraProvider;
|
|
41
|
+
import androidx.camera.view.PreviewView;
|
|
42
|
+
import androidx.core.app.ActivityCompat;
|
|
43
|
+
import androidx.core.content.ContextCompat;
|
|
44
|
+
import androidx.lifecycle.LifecycleOwner;
|
|
45
|
+
import androidx.lifecycle.LiveData;
|
|
46
|
+
|
|
47
|
+
import com.google.android.gms.common.api.CommonStatusCodes;
|
|
48
|
+
import com.google.android.gms.tasks.OnCompleteListener;
|
|
49
|
+
import com.google.android.gms.tasks.OnFailureListener;
|
|
50
|
+
import com.google.android.gms.tasks.OnSuccessListener;
|
|
51
|
+
import com.google.android.gms.tasks.Task;
|
|
52
|
+
import com.google.android.material.snackbar.Snackbar;
|
|
53
|
+
import com.google.common.util.concurrent.ListenableFuture;
|
|
54
|
+
import com.google.mlkit.vision.barcode.Barcode;
|
|
55
|
+
import com.google.mlkit.vision.barcode.BarcodeScanner;
|
|
56
|
+
import com.google.mlkit.vision.barcode.BarcodeScannerOptions;
|
|
57
|
+
import com.google.mlkit.vision.barcode.BarcodeScanning;
|
|
58
|
+
import com.google.mlkit.vision.common.InputImage;
|
|
59
|
+
import com.mobisys.cordova.plugins.mlkit.barcode.scanner.utils.BitmapUtils;
|
|
60
|
+
|
|
61
|
+
import org.json.JSONArray;
|
|
62
|
+
import org.json.JSONException;
|
|
63
|
+
|
|
64
|
+
import java.nio.charset.StandardCharsets;
|
|
65
|
+
import java.util.HashSet;
|
|
66
|
+
import java.util.List;
|
|
67
|
+
import java.util.Set;
|
|
68
|
+
import java.util.concurrent.ExecutionException;
|
|
69
|
+
import java.util.concurrent.ExecutorService;
|
|
70
|
+
import java.util.concurrent.Executors;
|
|
71
|
+
|
|
72
|
+
public class CaptureActivity extends AppCompatActivity implements SurfaceHolder.Callback {
|
|
73
|
+
|
|
74
|
+
public Integer BarcodeFormats;
|
|
75
|
+
public double DetectorSize = .5;
|
|
76
|
+
|
|
77
|
+
// JSON string extra holding an array of [text, format, type] triples.
|
|
78
|
+
public static final String BarcodePayload = "MLKitBarcodes";
|
|
79
|
+
|
|
80
|
+
private boolean continuous = false;
|
|
81
|
+
private boolean multiple = false;
|
|
82
|
+
private boolean drawDetectionBorder = false;
|
|
83
|
+
private boolean confirmation = false;
|
|
84
|
+
private boolean rotateCamera = false;
|
|
85
|
+
|
|
86
|
+
// Barcodes currently drawn on the overlay, so a surface change can redraw them.
|
|
87
|
+
private volatile List<Barcode> overlayBarcodes;
|
|
88
|
+
|
|
89
|
+
// Raw values already streamed back in continuous mode, so each barcode is
|
|
90
|
+
// reported only once per session.
|
|
91
|
+
private final Set<String> emittedValues = new HashSet<>();
|
|
92
|
+
|
|
93
|
+
// Overlay/view dimensions, captured from the surface so the analyzer thread
|
|
94
|
+
// can compute the image<->view transform.
|
|
95
|
+
private volatile int viewWidth = 0;
|
|
96
|
+
private volatile int viewHeight = 0;
|
|
97
|
+
|
|
98
|
+
// Mapping from the cropped frame handed to ML Kit back to view coordinates:
|
|
99
|
+
// viewPoint = (cropOrigin + barcodePoint) * fillScale + fillOffset. Matches
|
|
100
|
+
// PreviewView's default FILL_CENTER scaling.
|
|
101
|
+
private volatile int cropLeft = 0;
|
|
102
|
+
private volatile int cropTop = 0;
|
|
103
|
+
private volatile float fillScale = 1;
|
|
104
|
+
private volatile float fillOffsetX = 0;
|
|
105
|
+
private volatile float fillOffsetY = 0;
|
|
106
|
+
private volatile boolean haveMapping = false;
|
|
107
|
+
|
|
108
|
+
// Serialises overlay canvas access between the analyzer thread and the UI
|
|
109
|
+
// thread (surface callbacks, confirm/retry).
|
|
110
|
+
private final Object overlayLock = new Object();
|
|
111
|
+
|
|
112
|
+
// While a detected barcode is awaiting Confirm/Retry the analyzer stops
|
|
113
|
+
// processing frames and the preview is frozen.
|
|
114
|
+
private volatile boolean awaitingConfirmation = false;
|
|
115
|
+
private JSONArray pendingPayload;
|
|
116
|
+
|
|
117
|
+
private ImageView freezeFrame;
|
|
118
|
+
private View confirmPanel;
|
|
119
|
+
private TextView confirmText;
|
|
120
|
+
private Button confirmButton;
|
|
121
|
+
private Button retryButton;
|
|
122
|
+
|
|
123
|
+
private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
|
|
124
|
+
private ExecutorService executor = Executors.newSingleThreadExecutor();
|
|
125
|
+
private PreviewView mCameraView;
|
|
126
|
+
private SurfaceHolder holder;
|
|
127
|
+
private SurfaceView surfaceView;
|
|
128
|
+
|
|
129
|
+
private static final int RC_HANDLE_CAMERA_PERM = 2;
|
|
130
|
+
private ImageButton _TorchButton;
|
|
131
|
+
private Camera camera;
|
|
132
|
+
|
|
133
|
+
private ScaleGestureDetector _ScaleGestureDetector;
|
|
134
|
+
private GestureDetector _GestureDetector;
|
|
135
|
+
|
|
136
|
+
@Override
|
|
137
|
+
protected void onCreate(Bundle savedInstanceState) {
|
|
138
|
+
super.onCreate(savedInstanceState);
|
|
139
|
+
setContentView(getResources().getIdentifier("capture_activity", "layout", getPackageName()));
|
|
140
|
+
|
|
141
|
+
// Create the bounding box
|
|
142
|
+
surfaceView = findViewById(getResources().getIdentifier("overlay", "id", getPackageName()));
|
|
143
|
+
surfaceView.setZOrderOnTop(true);
|
|
144
|
+
|
|
145
|
+
holder = surfaceView.getHolder();
|
|
146
|
+
holder.setFormat(PixelFormat.TRANSPARENT);
|
|
147
|
+
holder.addCallback(this);
|
|
148
|
+
|
|
149
|
+
// read parameters from the intent used to launch the activity.
|
|
150
|
+
BarcodeFormats = getIntent().getIntExtra("BarcodeFormats", 1234);
|
|
151
|
+
DetectorSize = getIntent().getDoubleExtra("DetectorSize", .5);
|
|
152
|
+
continuous = getIntent().getBooleanExtra("Continuous", false);
|
|
153
|
+
multiple = getIntent().getBooleanExtra("Multiple", false);
|
|
154
|
+
drawDetectionBorder = getIntent().getBooleanExtra("DrawDetectionBorder", false);
|
|
155
|
+
rotateCamera = getIntent().getBooleanExtra("RotateCamera", false);
|
|
156
|
+
// Confirmation is meaningless while continuously streaming results.
|
|
157
|
+
confirmation = getIntent().getBooleanExtra("Confirmation", false) && !continuous;
|
|
158
|
+
|
|
159
|
+
// DetectorSize is the fraction of the screen that is scanned (0..1]; 1
|
|
160
|
+
// means the whole screen. Out-of-range values fall back to the default.
|
|
161
|
+
if (DetectorSize <= 0 || DetectorSize > 1) {
|
|
162
|
+
DetectorSize = 0.6;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
setupConfirmationUi();
|
|
166
|
+
|
|
167
|
+
int rc = ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA);
|
|
168
|
+
|
|
169
|
+
if (rc == PackageManager.PERMISSION_GRANTED) {
|
|
170
|
+
// Start Camera
|
|
171
|
+
startCamera();
|
|
172
|
+
} else {
|
|
173
|
+
requestCameraPermission();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
_GestureDetector = new GestureDetector(this, new CaptureGestureListener());
|
|
177
|
+
_ScaleGestureDetector = new ScaleGestureDetector(this, new ScaleListener());
|
|
178
|
+
|
|
179
|
+
_TorchButton = findViewById(getResources().getIdentifier("torch_button", "id", this.getPackageName()));
|
|
180
|
+
|
|
181
|
+
_TorchButton.setOnClickListener(new View.OnClickListener() {
|
|
182
|
+
@Override
|
|
183
|
+
public void onClick(View v) {
|
|
184
|
+
|
|
185
|
+
LiveData<Integer> flashState = camera.getCameraInfo().getTorchState();
|
|
186
|
+
if (flashState.getValue() != null) {
|
|
187
|
+
boolean state = flashState.getValue() == 1;
|
|
188
|
+
_TorchButton.setBackgroundResource(getResources().getIdentifier(!state ? "torch_active" : "torch_inactive",
|
|
189
|
+
"drawable", CaptureActivity.this.getPackageName()));
|
|
190
|
+
camera.getCameraControl().enableTorch(!state);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ----------------------------------------------------------------------------
|
|
199
|
+
// | Helper classes
|
|
200
|
+
// ----------------------------------------------------------------------------
|
|
201
|
+
private class CaptureGestureListener extends GestureDetector.SimpleOnGestureListener {
|
|
202
|
+
@Override
|
|
203
|
+
public boolean onSingleTapConfirmed(MotionEvent e) {
|
|
204
|
+
return super.onSingleTapConfirmed(e);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private class ScaleListener implements ScaleGestureDetector.OnScaleGestureListener {
|
|
209
|
+
@Override
|
|
210
|
+
public boolean onScale(ScaleGestureDetector detector) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@Override
|
|
215
|
+
public boolean onScaleBegin(ScaleGestureDetector detector) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@Override
|
|
220
|
+
public void onScaleEnd(ScaleGestureDetector detector) {
|
|
221
|
+
|
|
222
|
+
if (camera != null) {
|
|
223
|
+
float scale = camera.getCameraInfo().getZoomState().getValue().getZoomRatio() * detector.getScaleFactor();
|
|
224
|
+
camera.getCameraControl().setZoomRatio(scale);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private void requestCameraPermission() {
|
|
230
|
+
|
|
231
|
+
final String[] permissions = new String[] { Manifest.permission.CAMERA,
|
|
232
|
+
Manifest.permission.WRITE_EXTERNAL_STORAGE };
|
|
233
|
+
|
|
234
|
+
boolean shouldShowPermission = !ActivityCompat.shouldShowRequestPermissionRationale(this,
|
|
235
|
+
Manifest.permission.CAMERA);
|
|
236
|
+
shouldShowPermission = shouldShowPermission
|
|
237
|
+
&& !ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
|
238
|
+
|
|
239
|
+
if (shouldShowPermission) {
|
|
240
|
+
ActivityCompat.requestPermissions(this, permissions, RC_HANDLE_CAMERA_PERM);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
View.OnClickListener listener = new View.OnClickListener() {
|
|
245
|
+
@Override
|
|
246
|
+
public void onClick(View view) {
|
|
247
|
+
ActivityCompat.requestPermissions(CaptureActivity.this, permissions, RC_HANDLE_CAMERA_PERM);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
findViewById(getResources().getIdentifier("topLayout", "id", getPackageName())).setOnClickListener(listener);
|
|
252
|
+
Snackbar
|
|
253
|
+
.make(surfaceView, getResources().getIdentifier("permission_camera_rationale", "string", getPackageName()),
|
|
254
|
+
Snackbar.LENGTH_INDEFINITE)
|
|
255
|
+
.setAction(getResources().getIdentifier("ok", "string", getPackageName()), listener).show();
|
|
256
|
+
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@Override
|
|
260
|
+
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
|
261
|
+
if (requestCode != RC_HANDLE_CAMERA_PERM) {
|
|
262
|
+
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (grantResults.length != 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
267
|
+
startCamera();
|
|
268
|
+
redrawOverlay(overlayBarcodes);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
|
|
273
|
+
public void onClick(DialogInterface dialog, int id) {
|
|
274
|
+
finish();
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
|
279
|
+
builder.setTitle("Camera permission required")
|
|
280
|
+
.setMessage(getResources().getIdentifier("no_camera_permission", "string", getPackageName()))
|
|
281
|
+
.setPositiveButton(getResources().getIdentifier("ok", "string", getPackageName()), listener).show();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@Override
|
|
285
|
+
public void surfaceCreated(SurfaceHolder surfaceHolder) {
|
|
286
|
+
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
@Override
|
|
290
|
+
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int width, int height) {
|
|
291
|
+
viewWidth = width;
|
|
292
|
+
viewHeight = height;
|
|
293
|
+
redrawOverlay(overlayBarcodes);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@Override
|
|
297
|
+
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
|
|
298
|
+
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@Override
|
|
302
|
+
public boolean onTouchEvent(MotionEvent e) {
|
|
303
|
+
boolean b = _ScaleGestureDetector.onTouchEvent(e);
|
|
304
|
+
boolean c = _GestureDetector.onTouchEvent(e);
|
|
305
|
+
|
|
306
|
+
return b || c || super.onTouchEvent(e);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
@Override
|
|
310
|
+
protected void onPause() {
|
|
311
|
+
super.onPause();
|
|
312
|
+
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
@Override
|
|
316
|
+
protected void onResume() {
|
|
317
|
+
super.onResume();
|
|
318
|
+
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
void startCamera() {
|
|
322
|
+
mCameraView = findViewById(getResources().getIdentifier("previewView", "id", getPackageName()));
|
|
323
|
+
mCameraView.setPreferredImplementationMode(PreviewView.ImplementationMode.TEXTURE_VIEW);
|
|
324
|
+
|
|
325
|
+
if (rotateCamera) {
|
|
326
|
+
mCameraView.setScaleX(-1F);
|
|
327
|
+
mCameraView.setScaleY(-1F);
|
|
328
|
+
} else {
|
|
329
|
+
mCameraView.setScaleX(1F);
|
|
330
|
+
mCameraView.setScaleY(1F);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// mCameraView.setScaleType(PreviewView.ScaleType.FIT_CENTER);
|
|
334
|
+
|
|
335
|
+
cameraProviderFuture = ProcessCameraProvider.getInstance(this);
|
|
336
|
+
cameraProviderFuture.addListener(new Runnable() {
|
|
337
|
+
@Override
|
|
338
|
+
public void run() {
|
|
339
|
+
try {
|
|
340
|
+
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
|
|
341
|
+
CaptureActivity.this.bindPreview(cameraProvider);
|
|
342
|
+
|
|
343
|
+
} catch (ExecutionException | InterruptedException e) {
|
|
344
|
+
// No errors need to be handled for this Future.
|
|
345
|
+
// This should never be reached.
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}, ContextCompat.getMainExecutor(this));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Binding to camera
|
|
353
|
+
*/
|
|
354
|
+
private void bindPreview(ProcessCameraProvider cameraProvider) {
|
|
355
|
+
|
|
356
|
+
int barcodeFormat;
|
|
357
|
+
if (BarcodeFormats == 0 || BarcodeFormats == 1234) {
|
|
358
|
+
barcodeFormat = (Barcode.FORMAT_CODE_39 | Barcode.FORMAT_DATA_MATRIX);
|
|
359
|
+
} else {
|
|
360
|
+
barcodeFormat = BarcodeFormats;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
Preview preview = new Preview.Builder().build();
|
|
364
|
+
|
|
365
|
+
CameraSelector cameraSelector = new CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK)
|
|
366
|
+
.build();
|
|
367
|
+
|
|
368
|
+
preview.setSurfaceProvider(mCameraView.createSurfaceProvider());
|
|
369
|
+
|
|
370
|
+
ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
|
|
371
|
+
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).setTargetAspectRatio(AspectRatio.RATIO_16_9)
|
|
372
|
+
.build();
|
|
373
|
+
|
|
374
|
+
BarcodeScanner scanner = BarcodeScanning
|
|
375
|
+
.getClient(new BarcodeScannerOptions.Builder().setBarcodeFormats(barcodeFormat).build());
|
|
376
|
+
|
|
377
|
+
imageAnalysis.setAnalyzer(executor, new ImageAnalysis.Analyzer() {
|
|
378
|
+
@SuppressLint("UnsafeExperimentalUsageError")
|
|
379
|
+
@Override
|
|
380
|
+
public void analyze(@NonNull ImageProxy image) {
|
|
381
|
+
|
|
382
|
+
if (image == null || image.getImage() == null) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// While the user is confirming a detection the preview is frozen, so
|
|
387
|
+
// there is nothing to analyze.
|
|
388
|
+
if (awaitingConfirmation) {
|
|
389
|
+
image.close();
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
Bitmap bmp = BitmapUtils.getBitmap(image);
|
|
394
|
+
|
|
395
|
+
int width = bmp.getWidth();
|
|
396
|
+
int height = bmp.getHeight();
|
|
397
|
+
|
|
398
|
+
// The scan area is DetectorSize (0..1) of the screen, centred. Map that
|
|
399
|
+
// view rectangle back into image pixels so ML Kit only sees what the
|
|
400
|
+
// user sees inside the focus box (the whole screen at DetectorSize 1).
|
|
401
|
+
int vw = viewWidth > 0 ? viewWidth : width;
|
|
402
|
+
int vh = viewHeight > 0 ? viewHeight : height;
|
|
403
|
+
|
|
404
|
+
// PreviewView FILL_CENTER: scale the image up to cover the view.
|
|
405
|
+
float scale = Math.max((float) vw / width, (float) vh / height);
|
|
406
|
+
float offX = (vw - width * scale) / 2f;
|
|
407
|
+
float offY = (vh - height * scale) / 2f;
|
|
408
|
+
|
|
409
|
+
double ds = DetectorSize;
|
|
410
|
+
float fLeftV = (float) (vw * (1 - ds) / 2);
|
|
411
|
+
float fTopV = (float) (vh * (1 - ds) / 2);
|
|
412
|
+
float fRightV = (float) (vw * (1 + ds) / 2);
|
|
413
|
+
float fBotV = (float) (vh * (1 + ds) / 2);
|
|
414
|
+
|
|
415
|
+
int left = clampInt((fLeftV - offX) / scale, 0, width);
|
|
416
|
+
int top = clampInt((fTopV - offY) / scale, 0, height);
|
|
417
|
+
int right = clampInt((fRightV - offX) / scale, 0, width);
|
|
418
|
+
int bottom = clampInt((fBotV - offY) / scale, 0, height);
|
|
419
|
+
|
|
420
|
+
int boxWidth = right - left;
|
|
421
|
+
int boxHeight = bottom - top;
|
|
422
|
+
if (boxWidth <= 0 || boxHeight <= 0) {
|
|
423
|
+
left = 0;
|
|
424
|
+
top = 0;
|
|
425
|
+
boxWidth = width;
|
|
426
|
+
boxHeight = height;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
Bitmap bitmap = Bitmap.createBitmap(bmp, left, top, boxWidth, boxHeight);
|
|
430
|
+
|
|
431
|
+
// BitmapUtils.getBitmap() has already rotated the frame upright, so the
|
|
432
|
+
// crop is upright too; pass rotation 0 so ML Kit reports corner points
|
|
433
|
+
// directly in crop-pixel space. Record the mapping for the overlay.
|
|
434
|
+
cropLeft = left;
|
|
435
|
+
cropTop = top;
|
|
436
|
+
fillScale = scale;
|
|
437
|
+
fillOffsetX = offX;
|
|
438
|
+
fillOffsetY = offY;
|
|
439
|
+
haveMapping = true;
|
|
440
|
+
scanner.process(InputImage.fromBitmap(bitmap, 0))
|
|
441
|
+
.addOnSuccessListener(new OnSuccessListener<List<Barcode>>() {
|
|
442
|
+
@Override
|
|
443
|
+
public void onSuccess(List<Barcode> barCodes) {
|
|
444
|
+
// Another frame may have entered confirmation while this one
|
|
445
|
+
// was in flight; drop it.
|
|
446
|
+
if (awaitingConfirmation) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Keep the live detection border in sync with what is on screen.
|
|
451
|
+
if (drawDetectionBorder) {
|
|
452
|
+
redrawOverlay(barCodes);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (barCodes.size() == 0) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
handleBarcodes(barCodes);
|
|
461
|
+
} catch (JSONException e) {
|
|
462
|
+
e.printStackTrace();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}).addOnFailureListener(new OnFailureListener() {
|
|
466
|
+
@Override
|
|
467
|
+
public void onFailure(@NonNull Exception e) {
|
|
468
|
+
|
|
469
|
+
}
|
|
470
|
+
}).addOnCompleteListener(new OnCompleteListener<List<Barcode>>() {
|
|
471
|
+
@Override
|
|
472
|
+
public void onComplete(@NonNull Task<List<Barcode>> task) {
|
|
473
|
+
image.close();
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Release any use cases still bound from a previous scan before rebinding,
|
|
481
|
+
// otherwise reopening the scanner can leave the preview/analyzer detached
|
|
482
|
+
// (the camera appears frozen and every subsequent scan gets cancelled).
|
|
483
|
+
cameraProvider.unbindAll();
|
|
484
|
+
|
|
485
|
+
camera = cameraProvider.bindToLifecycle((LifecycleOwner) this, cameraSelector, imageAnalysis, preview);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Turns the detected barcodes into a result payload. In continuous mode the
|
|
490
|
+
* camera stays open and only newly seen codes are streamed back; otherwise
|
|
491
|
+
* the (first or all) codes are returned and the activity finishes.
|
|
492
|
+
*/
|
|
493
|
+
private void handleBarcodes(List<Barcode> barCodes) throws JSONException {
|
|
494
|
+
JSONArray payload = new JSONArray();
|
|
495
|
+
|
|
496
|
+
for (Barcode barcode : barCodes) {
|
|
497
|
+
String value = rawValueOf(barcode);
|
|
498
|
+
if (value == null) {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (continuous && !emittedValues.add(value)) {
|
|
503
|
+
// Already streamed this code in a previous frame.
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
payload.put(toTriple(barcode, value));
|
|
508
|
+
|
|
509
|
+
if (!multiple) {
|
|
510
|
+
// Single-result mode: one code is enough.
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (payload.length() == 0) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (continuous) {
|
|
520
|
+
MLKitBarcodeScanner.sendContinuousResult(payload);
|
|
521
|
+
} else if (confirmation) {
|
|
522
|
+
enterConfirmation(payload, barCodes);
|
|
523
|
+
} else {
|
|
524
|
+
finishWithResult(payload);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Returns the payload to the plugin and closes the scanner. */
|
|
529
|
+
private void finishWithResult(JSONArray payload) {
|
|
530
|
+
Intent data = new Intent();
|
|
531
|
+
data.putExtra(BarcodePayload, payload.toString());
|
|
532
|
+
setResult(CommonStatusCodes.SUCCESS, data);
|
|
533
|
+
finish();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Returns the barcode's raw value, falling back to ASCII decoding when it is
|
|
538
|
+
* not UTF-8 encoded (the most common case for 1D barcodes).
|
|
539
|
+
* e.g. https://www.barcodefaq.com/1d/code-128/
|
|
540
|
+
*/
|
|
541
|
+
private String rawValueOf(Barcode barcode) {
|
|
542
|
+
String value = barcode.getRawValue();
|
|
543
|
+
if (value == null && barcode.getRawBytes() != null) {
|
|
544
|
+
value = new String(barcode.getRawBytes(), StandardCharsets.US_ASCII);
|
|
545
|
+
}
|
|
546
|
+
return value;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Encodes a barcode as a [text, format, type] triple. */
|
|
550
|
+
private JSONArray toTriple(Barcode barcode, String value) throws JSONException {
|
|
551
|
+
JSONArray triple = new JSONArray();
|
|
552
|
+
triple.put(value);
|
|
553
|
+
triple.put(barcode.getFormat());
|
|
554
|
+
triple.put(barcode.getValueType());
|
|
555
|
+
return triple;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
@Override
|
|
559
|
+
protected void onDestroy() {
|
|
560
|
+
super.onDestroy();
|
|
561
|
+
if (executor != null) {
|
|
562
|
+
executor.shutdown();
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// --------------------------------------------------------------------------
|
|
567
|
+
// | Overlay drawing
|
|
568
|
+
// --------------------------------------------------------------------------
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Redraws the overlay: the focus rectangle plus a corner-point border around
|
|
572
|
+
* each supplied barcode. Pass {@code null} to draw only the focus rectangle.
|
|
573
|
+
*/
|
|
574
|
+
private void redrawOverlay(List<Barcode> barcodes) {
|
|
575
|
+
overlayBarcodes = barcodes;
|
|
576
|
+
|
|
577
|
+
if (mCameraView == null || holder == null) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
synchronized (overlayLock) {
|
|
582
|
+
Canvas c = holder.lockCanvas();
|
|
583
|
+
if (c == null) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
c.drawColor(0, PorterDuff.Mode.CLEAR);
|
|
588
|
+
|
|
589
|
+
int width = mCameraView.getWidth();
|
|
590
|
+
int height = mCameraView.getHeight();
|
|
591
|
+
|
|
592
|
+
// The focus box is DetectorSize of the screen in both dimensions,
|
|
593
|
+
// centred. At full screen (DetectorSize >= 1) there is no box to draw.
|
|
594
|
+
if (DetectorSize < 1) {
|
|
595
|
+
float left = (float) (width * (1 - DetectorSize) / 2);
|
|
596
|
+
float top = (float) (height * (1 - DetectorSize) / 2);
|
|
597
|
+
float right = (float) (width * (1 + DetectorSize) / 2);
|
|
598
|
+
float bottom = (float) (height * (1 + DetectorSize) / 2);
|
|
599
|
+
|
|
600
|
+
Paint focusPaint = new Paint();
|
|
601
|
+
focusPaint.setStyle(Paint.Style.STROKE);
|
|
602
|
+
focusPaint.setColor(Color.WHITE);
|
|
603
|
+
focusPaint.setStrokeWidth(5);
|
|
604
|
+
if (DetectorSize <= 0.3) {
|
|
605
|
+
c.drawRect(new RectF(left, top, right, bottom), focusPaint);
|
|
606
|
+
} else {
|
|
607
|
+
c.drawRoundRect(new RectF(left, top, right, bottom), 100, 100, focusPaint);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (barcodes != null && haveMapping) {
|
|
612
|
+
Paint borderPaint = new Paint();
|
|
613
|
+
borderPaint.setStyle(Paint.Style.STROKE);
|
|
614
|
+
borderPaint.setColor(Color.parseColor("#00E676"));
|
|
615
|
+
borderPaint.setStrokeWidth(6);
|
|
616
|
+
borderPaint.setAntiAlias(true);
|
|
617
|
+
for (Barcode barcode : barcodes) {
|
|
618
|
+
Path path = barcodeBorderPath(barcode);
|
|
619
|
+
if (path != null) {
|
|
620
|
+
c.drawPath(path, borderPaint);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
} finally {
|
|
625
|
+
holder.unlockCanvasAndPost(c);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Maps a barcode's corner points (relative to the crop handed to ML Kit) into
|
|
632
|
+
* a closed Path in overlay/view space using the recorded FILL_CENTER mapping.
|
|
633
|
+
*/
|
|
634
|
+
private Path barcodeBorderPath(Barcode barcode) {
|
|
635
|
+
float[] xs = new float[4];
|
|
636
|
+
float[] ys = new float[4];
|
|
637
|
+
|
|
638
|
+
Point[] corners = barcode.getCornerPoints();
|
|
639
|
+
if (corners != null && corners.length == 4) {
|
|
640
|
+
for (int i = 0; i < 4; i++) {
|
|
641
|
+
xs[i] = corners[i].x;
|
|
642
|
+
ys[i] = corners[i].y;
|
|
643
|
+
}
|
|
644
|
+
} else {
|
|
645
|
+
Rect box = barcode.getBoundingBox();
|
|
646
|
+
if (box == null) {
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
xs[0] = box.left; ys[0] = box.top;
|
|
650
|
+
xs[1] = box.right; ys[1] = box.top;
|
|
651
|
+
xs[2] = box.right; ys[2] = box.bottom;
|
|
652
|
+
xs[3] = box.left; ys[3] = box.bottom;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
Path path = new Path();
|
|
656
|
+
for (int i = 0; i < 4; i++) {
|
|
657
|
+
// Crop-relative -> full image -> view.
|
|
658
|
+
float vx = (cropLeft + xs[i]) * fillScale + fillOffsetX;
|
|
659
|
+
float vy = (cropTop + ys[i]) * fillScale + fillOffsetY;
|
|
660
|
+
// The preview is rotated 180 degrees (scaleX/Y = -1) when rotateCamera is
|
|
661
|
+
// set, so mirror the points to keep the border aligned.
|
|
662
|
+
if (rotateCamera) {
|
|
663
|
+
vx = viewWidth - vx;
|
|
664
|
+
vy = viewHeight - vy;
|
|
665
|
+
}
|
|
666
|
+
if (i == 0) {
|
|
667
|
+
path.moveTo(vx, vy);
|
|
668
|
+
} else {
|
|
669
|
+
path.lineTo(vx, vy);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
path.close();
|
|
673
|
+
return path;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** Clamps {@code value} into [min, max] and rounds to the nearest int. */
|
|
677
|
+
private static int clampInt(float value, int min, int max) {
|
|
678
|
+
return Math.max(min, Math.min(max, Math.round(value)));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// --------------------------------------------------------------------------
|
|
682
|
+
// | Scan confirmation
|
|
683
|
+
// --------------------------------------------------------------------------
|
|
684
|
+
|
|
685
|
+
private void setupConfirmationUi() {
|
|
686
|
+
freezeFrame = findViewById(getResources().getIdentifier("freeze_frame", "id", getPackageName()));
|
|
687
|
+
confirmPanel = findViewById(getResources().getIdentifier("confirm_panel", "id", getPackageName()));
|
|
688
|
+
confirmText = findViewById(getResources().getIdentifier("confirm_text", "id", getPackageName()));
|
|
689
|
+
confirmButton = findViewById(getResources().getIdentifier("confirm_button", "id", getPackageName()));
|
|
690
|
+
retryButton = findViewById(getResources().getIdentifier("retry_button", "id", getPackageName()));
|
|
691
|
+
|
|
692
|
+
if (confirmButton != null) {
|
|
693
|
+
confirmButton.setText(getResources().getIdentifier("scan_confirm", "string", getPackageName()));
|
|
694
|
+
confirmButton.setOnClickListener(new View.OnClickListener() {
|
|
695
|
+
@Override
|
|
696
|
+
public void onClick(View v) {
|
|
697
|
+
if (pendingPayload != null) {
|
|
698
|
+
finishWithResult(pendingPayload);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
if (retryButton != null) {
|
|
704
|
+
retryButton.setText(getResources().getIdentifier("scan_retry", "string", getPackageName()));
|
|
705
|
+
retryButton.setOnClickListener(new View.OnClickListener() {
|
|
706
|
+
@Override
|
|
707
|
+
public void onClick(View v) {
|
|
708
|
+
exitConfirmation();
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** Freezes the preview on the detected frame and shows the Confirm/Retry prompt. */
|
|
715
|
+
private void enterConfirmation(JSONArray payload, List<Barcode> barCodes) {
|
|
716
|
+
awaitingConfirmation = true;
|
|
717
|
+
pendingPayload = payload;
|
|
718
|
+
|
|
719
|
+
final Bitmap frozen = (mCameraView != null) ? mCameraView.getBitmap() : null;
|
|
720
|
+
final int count = payload.length();
|
|
721
|
+
final String firstValue = (barCodes != null && !barCodes.isEmpty()) ? rawValueOf(barCodes.get(0)) : null;
|
|
722
|
+
|
|
723
|
+
// Draw the detected border(s) and keep them on screen while confirming.
|
|
724
|
+
redrawOverlay(barCodes);
|
|
725
|
+
|
|
726
|
+
runOnUiThread(new Runnable() {
|
|
727
|
+
@Override
|
|
728
|
+
public void run() {
|
|
729
|
+
if (frozen != null && freezeFrame != null) {
|
|
730
|
+
freezeFrame.setImageBitmap(frozen);
|
|
731
|
+
freezeFrame.setVisibility(View.VISIBLE);
|
|
732
|
+
}
|
|
733
|
+
if (confirmText != null) {
|
|
734
|
+
if (multiple) {
|
|
735
|
+
confirmText.setText(getString(
|
|
736
|
+
getResources().getIdentifier("scan_confirm_count", "string", getPackageName()), count));
|
|
737
|
+
} else {
|
|
738
|
+
confirmText.setText(firstValue);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (confirmPanel != null) {
|
|
742
|
+
confirmPanel.setVisibility(View.VISIBLE);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/** Dismisses the confirmation prompt and resumes scanning. */
|
|
749
|
+
private void exitConfirmation() {
|
|
750
|
+
if (freezeFrame != null) {
|
|
751
|
+
freezeFrame.setVisibility(View.GONE);
|
|
752
|
+
freezeFrame.setImageBitmap(null);
|
|
753
|
+
}
|
|
754
|
+
if (confirmPanel != null) {
|
|
755
|
+
confirmPanel.setVisibility(View.GONE);
|
|
756
|
+
}
|
|
757
|
+
pendingPayload = null;
|
|
758
|
+
redrawOverlay(null);
|
|
759
|
+
// Resume the analyzer last, once the UI has been reset.
|
|
760
|
+
awaitingConfirmation = false;
|
|
761
|
+
}
|
|
762
|
+
}
|