@capgo/camera-preview 7.21.8 → 7.21.10

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.
@@ -239,11 +239,11 @@ public class CameraPreview
239
239
  // Prevent starting while an existing view is still active or stopping
240
240
  if (cameraXView != null) {
241
241
  try {
242
- if (cameraXView.isRunning()) {
242
+ if (cameraXView.isRunning() && !cameraXView.isStopping()) {
243
243
  call.reject("Camera is already running");
244
244
  return;
245
245
  }
246
- if (cameraXView.isBusy()) {
246
+ if (cameraXView.isStopping() || cameraXView.isBusy()) {
247
247
  if (enqueuePendingStart(call)) {
248
248
  Log.d(
249
249
  TAG,
@@ -445,15 +445,11 @@ public class CameraPreview
445
445
  }
446
446
 
447
447
  if (cameraXView != null) {
448
- boolean willDefer = false;
449
- try {
450
- willDefer = cameraXView.isCapturing();
451
- } catch (Exception ignored) {}
452
448
  if (cameraXView.isRunning()) {
453
449
  cameraXView.stopSession();
454
450
  }
455
451
  // Only drop the reference if no deferred stop is pending
456
- if (!willDefer) {
452
+ if (!cameraXView.isStopDeferred()) {
457
453
  cameraXView = null;
458
454
  }
459
455
  }
@@ -203,6 +203,18 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
203
203
  }
204
204
  }
205
205
 
206
+ public boolean isStopDeferred() {
207
+ synchronized (operationLock) {
208
+ return stopPending && activeOperations > 0;
209
+ }
210
+ }
211
+
212
+ public boolean isStopping() {
213
+ synchronized (operationLock) {
214
+ return stopPending;
215
+ }
216
+ }
217
+
206
218
  public CameraXView(Context context, WebView webView) {
207
219
  this.context = context;
208
220
  this.webView = webView;
@@ -1174,11 +1186,19 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1174
1186
  final boolean embedTimestamp,
1175
1187
  final boolean embedLocation
1176
1188
  ) {
1189
+ if (imageCapture == null) {
1190
+ if (listener != null) {
1191
+ listener.onPictureTakenError("Camera not ready");
1192
+ }
1193
+ return;
1194
+ }
1195
+
1177
1196
  // Prevent capture if a stop is pending
1178
1197
  if (IsOperationRunning("capturePhoto")) {
1179
1198
  Log.d(TAG, "capturePhoto: Ignored because stop is pending");
1180
1199
  return;
1181
1200
  }
1201
+
1182
1202
  Log.d(
1183
1203
  TAG,
1184
1204
  "capturePhoto: Starting photo capture with: " +
@@ -1195,224 +1215,239 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1195
1215
  embedLocation
1196
1216
  );
1197
1217
 
1198
- if (imageCapture == null) {
1199
- if (listener != null) {
1200
- listener.onPictureTakenError("Camera not ready");
1218
+ boolean dispatched = false;
1219
+ try {
1220
+ synchronized (captureLock) {
1221
+ isCapturingPhoto = true;
1201
1222
  }
1202
- return;
1203
- }
1204
1223
 
1205
- synchronized (captureLock) {
1206
- isCapturingPhoto = true;
1207
- }
1208
-
1209
- ByteArrayOutputStream imageStream = new ByteArrayOutputStream();
1210
- ImageCapture.Metadata metadata = new ImageCapture.Metadata();
1211
- if (location != null) {
1212
- metadata.setLocation(location);
1213
- }
1214
- ImageCapture.OutputFileOptions outputFileOptions =
1215
- new ImageCapture.OutputFileOptions.Builder(imageStream)
1216
- .setMetadata(metadata)
1217
- .build();
1224
+ final ByteArrayOutputStream imageStream = new ByteArrayOutputStream();
1225
+ ImageCapture.Metadata metadata = new ImageCapture.Metadata();
1226
+ if (location != null) {
1227
+ metadata.setLocation(location);
1228
+ }
1229
+ ImageCapture.OutputFileOptions outputFileOptions =
1230
+ new ImageCapture.OutputFileOptions.Builder(imageStream)
1231
+ .setMetadata(metadata)
1232
+ .build();
1218
1233
 
1219
- imageCapture.takePicture(
1220
- outputFileOptions,
1221
- cameraExecutor,
1222
- new ImageCapture.OnImageSavedCallback() {
1223
- @Override
1224
- public void onError(@NonNull ImageCaptureException exception) {
1225
- Log.e(TAG, "capturePhoto: Photo capture failed", exception);
1226
- if (listener != null) {
1227
- listener.onPictureTakenError(
1228
- "Photo capture failed: " + exception.getMessage()
1229
- );
1230
- }
1231
- // End of capture lifecycle
1232
- synchronized (captureLock) {
1233
- isCapturingPhoto = false;
1234
- if (stopRequested) {
1235
- performImmediateStop();
1234
+ imageCapture.takePicture(
1235
+ outputFileOptions,
1236
+ cameraExecutor,
1237
+ new ImageCapture.OnImageSavedCallback() {
1238
+ @Override
1239
+ public void onError(@NonNull ImageCaptureException exception) {
1240
+ Log.e(TAG, "capturePhoto: Photo capture failed", exception);
1241
+ if (listener != null) {
1242
+ listener.onPictureTakenError(
1243
+ "Photo capture failed: " + exception.getMessage()
1244
+ );
1236
1245
  }
1246
+ // End of capture lifecycle
1247
+ synchronized (captureLock) {
1248
+ isCapturingPhoto = false;
1249
+ if (stopRequested) {
1250
+ performImmediateStop();
1251
+ }
1252
+ }
1253
+ endOperation("capturePhoto");
1237
1254
  }
1238
- endOperation("capturePhoto");
1239
- }
1240
1255
 
1241
- @Override
1242
- public void onImageSaved(
1243
- @NonNull ImageCapture.OutputFileResults output
1244
- ) {
1245
- try {
1246
- byte[] originalCaptureBytes = imageStream.toByteArray();
1247
- byte[] bytes = originalCaptureBytes; // will be replaced if we transform
1248
- int finalWidthOut = -1;
1249
- int finalHeightOut = -1;
1250
- boolean transformedPixels = false;
1251
-
1252
- ExifInterface exifInterface = new ExifInterface(
1253
- new ByteArrayInputStream(originalCaptureBytes)
1254
- );
1255
- // Build EXIF JSON from captured bytes (location applied by metadata if provided)
1256
- JSONObject exifData = getExifData(exifInterface);
1257
-
1258
- if (width != null || height != null) {
1259
- Bitmap bitmap = BitmapFactory.decodeByteArray(
1260
- originalCaptureBytes,
1261
- 0,
1262
- originalCaptureBytes.length
1263
- );
1264
- bitmap = applyExifOrientation(bitmap, exifInterface);
1265
- Bitmap resizedBitmap = resizeBitmapToMaxDimensions(
1266
- bitmap,
1267
- width,
1268
- height
1256
+ @Override
1257
+ public void onImageSaved(
1258
+ @NonNull ImageCapture.OutputFileResults output
1259
+ ) {
1260
+ try {
1261
+ byte[] originalCaptureBytes = imageStream.toByteArray();
1262
+ byte[] bytes = originalCaptureBytes; // will be replaced if we transform
1263
+ int finalWidthOut = -1;
1264
+ int finalHeightOut = -1;
1265
+ boolean transformedPixels = false;
1266
+
1267
+ ExifInterface exifInterface = new ExifInterface(
1268
+ new ByteArrayInputStream(originalCaptureBytes)
1269
1269
  );
1270
- if (embedTimestamp || embedLocation) {
1271
- resizedBitmap = drawTimestampAndLocationOntoBitmap(
1272
- resizedBitmap,
1273
- exifInterface,
1274
- embedTimestamp,
1275
- embedLocation
1270
+ // Build EXIF JSON from captured bytes (location applied by metadata if provided)
1271
+ JSONObject exifData = getExifData(exifInterface);
1272
+
1273
+ if (width != null || height != null) {
1274
+ Bitmap bitmap = BitmapFactory.decodeByteArray(
1275
+ originalCaptureBytes,
1276
+ 0,
1277
+ originalCaptureBytes.length
1276
1278
  );
1277
- }
1278
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
1279
- resizedBitmap.compress(
1280
- Bitmap.CompressFormat.JPEG,
1281
- quality,
1282
- stream
1283
- );
1284
- bytes = stream.toByteArray();
1285
- transformedPixels = true;
1286
-
1287
- // Update EXIF JSON to reflect new dimensions; no in-place EXIF write to bytes
1288
- try {
1289
- exifData.put("PixelXDimension", resizedBitmap.getWidth());
1290
- exifData.put("PixelYDimension", resizedBitmap.getHeight());
1291
- exifData.put("ImageWidth", resizedBitmap.getWidth());
1292
- exifData.put("ImageLength", resizedBitmap.getHeight());
1293
- exifData.put(
1294
- "Orientation",
1295
- Integer.toString(ExifInterface.ORIENTATION_NORMAL)
1279
+ bitmap = applyExifOrientation(bitmap, exifInterface);
1280
+ Bitmap resizedBitmap = resizeBitmapToMaxDimensions(
1281
+ bitmap,
1282
+ width,
1283
+ height
1296
1284
  );
1297
- } catch (Exception ignore) {}
1298
- finalWidthOut = resizedBitmap.getWidth();
1299
- finalHeightOut = resizedBitmap.getHeight();
1300
- } else {
1301
- // No explicit size/ratio: crop to match current preview content
1302
- Bitmap originalBitmap = BitmapFactory.decodeByteArray(
1303
- originalCaptureBytes,
1304
- 0,
1305
- originalCaptureBytes.length
1306
- );
1307
- originalBitmap = applyExifOrientation(
1308
- originalBitmap,
1309
- exifInterface
1310
- );
1311
- Bitmap previewCropped = cropBitmapToMatchPreview(originalBitmap);
1312
- if (embedTimestamp || embedLocation) {
1313
- previewCropped = drawTimestampAndLocationOntoBitmap(
1314
- previewCropped,
1315
- exifInterface,
1316
- embedTimestamp,
1317
- embedLocation
1285
+ if (embedTimestamp || embedLocation) {
1286
+ resizedBitmap = drawTimestampAndLocationOntoBitmap(
1287
+ resizedBitmap,
1288
+ exifInterface,
1289
+ embedTimestamp,
1290
+ embedLocation
1291
+ );
1292
+ }
1293
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
1294
+ resizedBitmap.compress(
1295
+ Bitmap.CompressFormat.JPEG,
1296
+ quality,
1297
+ stream
1318
1298
  );
1319
- }
1320
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
1321
- previewCropped.compress(
1322
- Bitmap.CompressFormat.JPEG,
1323
- quality,
1324
- stream
1325
- );
1326
- bytes = stream.toByteArray();
1327
- transformedPixels = true;
1328
- // Update EXIF JSON to reflect cropped dimensions; no in-place EXIF write to bytes
1329
- try {
1330
- exifData.put("PixelXDimension", previewCropped.getWidth());
1331
- exifData.put("PixelYDimension", previewCropped.getHeight());
1332
- exifData.put("ImageWidth", previewCropped.getWidth());
1333
- exifData.put("ImageLength", previewCropped.getHeight());
1334
- exifData.put(
1335
- "Orientation",
1336
- Integer.toString(ExifInterface.ORIENTATION_NORMAL)
1299
+ bytes = stream.toByteArray();
1300
+ transformedPixels = true;
1301
+
1302
+ // Update EXIF JSON to reflect new dimensions; no in-place EXIF write to bytes
1303
+ try {
1304
+ exifData.put("PixelXDimension", resizedBitmap.getWidth());
1305
+ exifData.put("PixelYDimension", resizedBitmap.getHeight());
1306
+ exifData.put("ImageWidth", resizedBitmap.getWidth());
1307
+ exifData.put("ImageLength", resizedBitmap.getHeight());
1308
+ exifData.put(
1309
+ "Orientation",
1310
+ Integer.toString(ExifInterface.ORIENTATION_NORMAL)
1311
+ );
1312
+ } catch (Exception ignore) {}
1313
+ finalWidthOut = resizedBitmap.getWidth();
1314
+ finalHeightOut = resizedBitmap.getHeight();
1315
+ } else {
1316
+ // No explicit size/ratio: crop to match current preview content
1317
+ Bitmap originalBitmap = BitmapFactory.decodeByteArray(
1318
+ originalCaptureBytes,
1319
+ 0,
1320
+ originalCaptureBytes.length
1337
1321
  );
1338
- } catch (Exception ignore) {}
1339
- finalWidthOut = previewCropped.getWidth();
1340
- finalHeightOut = previewCropped.getHeight();
1341
- }
1322
+ originalBitmap = applyExifOrientation(
1323
+ originalBitmap,
1324
+ exifInterface
1325
+ );
1326
+ Bitmap previewCropped = cropBitmapToMatchPreview(
1327
+ originalBitmap
1328
+ );
1329
+ if (embedTimestamp || embedLocation) {
1330
+ previewCropped = drawTimestampAndLocationOntoBitmap(
1331
+ previewCropped,
1332
+ exifInterface,
1333
+ embedTimestamp,
1334
+ embedLocation
1335
+ );
1336
+ }
1337
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
1338
+ previewCropped.compress(
1339
+ Bitmap.CompressFormat.JPEG,
1340
+ quality,
1341
+ stream
1342
+ );
1343
+ bytes = stream.toByteArray();
1344
+ transformedPixels = true;
1345
+ // Update EXIF JSON to reflect cropped dimensions; no in-place EXIF write to bytes
1346
+ try {
1347
+ exifData.put("PixelXDimension", previewCropped.getWidth());
1348
+ exifData.put("PixelYDimension", previewCropped.getHeight());
1349
+ exifData.put("ImageWidth", previewCropped.getWidth());
1350
+ exifData.put("ImageLength", previewCropped.getHeight());
1351
+ exifData.put(
1352
+ "Orientation",
1353
+ Integer.toString(ExifInterface.ORIENTATION_NORMAL)
1354
+ );
1355
+ } catch (Exception ignore) {}
1356
+ finalWidthOut = previewCropped.getWidth();
1357
+ finalHeightOut = previewCropped.getHeight();
1358
+ }
1342
1359
 
1343
- // After any transform, inject EXIF back into the in-memory JPEG bytes (no temp file)
1344
- if (transformedPixels) {
1345
- Integer fW = (finalWidthOut > 0) ? finalWidthOut : null;
1346
- Integer fH = (finalHeightOut > 0) ? finalHeightOut : null;
1347
- bytes = injectExifInMemory(bytes, originalCaptureBytes, fW, fH);
1348
- }
1360
+ // After any transform, inject EXIF back into the in-memory JPEG bytes (no temp file)
1361
+ if (transformedPixels) {
1362
+ Integer fW = (finalWidthOut > 0) ? finalWidthOut : null;
1363
+ Integer fH = (finalHeightOut > 0) ? finalHeightOut : null;
1364
+ bytes = injectExifInMemory(bytes, originalCaptureBytes, fW, fH);
1365
+ }
1349
1366
 
1350
- // Save to gallery asynchronously if requested, copy EXIF to file
1351
- if (saveToGallery) {
1352
- final byte[] finalBytes = bytes;
1353
- final ExifInterface exifForFile = exifInterface;
1354
- final Integer fW = (finalWidthOut > 0) ? finalWidthOut : null;
1355
- final Integer fH = (finalHeightOut > 0) ? finalHeightOut : null;
1356
- new Thread(() ->
1357
- saveImageToGallery(finalBytes, exifForFile, fW, fH)
1358
- ).start();
1359
- }
1367
+ // Save to gallery asynchronously if requested, copy EXIF to file
1368
+ if (saveToGallery) {
1369
+ final byte[] finalBytes = bytes;
1370
+ final ExifInterface exifForFile = exifInterface;
1371
+ final Integer fW = (finalWidthOut > 0) ? finalWidthOut : null;
1372
+ final Integer fH = (finalHeightOut > 0) ? finalHeightOut : null;
1373
+ new Thread(() ->
1374
+ saveImageToGallery(finalBytes, exifForFile, fW, fH)
1375
+ ).start();
1376
+ }
1360
1377
 
1361
- String resultValue;
1362
- boolean returnFileUri =
1363
- sessionConfig != null && sessionConfig.isStoreToFile();
1364
- if (returnFileUri) {
1365
- // Persist processed image to a file and return its URI to avoid heavy base64 bridging
1366
- try {
1367
- String fileName =
1368
- "cpcp_" +
1369
- new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(
1370
- new java.util.Date()
1371
- ) +
1372
- ".jpg";
1373
- File outDir = context.getCacheDir();
1374
- File outFile = new File(outDir, fileName);
1375
- FileOutputStream outFos = new FileOutputStream(outFile);
1376
- outFos.write(bytes);
1377
- outFos.close();
1378
-
1379
- // No EXIF rewrite here; bytes already contain EXIF when needed
1380
-
1381
- // Return a file path; apps can convert via Capacitor.convertFileSrc on JS side
1382
- resultValue = outFile.getAbsolutePath();
1383
- } catch (IOException ioEx) {
1384
- Log.e(TAG, "capturePhoto: Failed to write image file", ioEx);
1385
- // Fallback to base64 if file write fails
1378
+ String resultValue;
1379
+ boolean returnFileUri =
1380
+ sessionConfig != null && sessionConfig.isStoreToFile();
1381
+ if (returnFileUri) {
1382
+ // Persist processed image to a file and return its URI to avoid heavy base64 bridging
1383
+ try {
1384
+ String fileName =
1385
+ "cpcp_" +
1386
+ new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(
1387
+ new java.util.Date()
1388
+ ) +
1389
+ ".jpg";
1390
+ File outDir = context.getCacheDir();
1391
+ File outFile = new File(outDir, fileName);
1392
+ FileOutputStream outFos = new FileOutputStream(outFile);
1393
+ outFos.write(bytes);
1394
+ outFos.close();
1395
+
1396
+ // No EXIF rewrite here; bytes already contain EXIF when needed
1397
+
1398
+ // Return a file path; apps can convert via Capacitor.convertFileSrc on JS side
1399
+ resultValue = outFile.getAbsolutePath();
1400
+ } catch (IOException ioEx) {
1401
+ Log.e(TAG, "capturePhoto: Failed to write image file", ioEx);
1402
+ // Fallback to base64 if file write fails
1403
+ resultValue = Base64.encodeToString(bytes, Base64.NO_WRAP);
1404
+ }
1405
+ } else {
1406
+ // Backward-compatible behavior
1386
1407
  resultValue = Base64.encodeToString(bytes, Base64.NO_WRAP);
1387
1408
  }
1388
- } else {
1389
- // Backward-compatible behavior
1390
- resultValue = Base64.encodeToString(bytes, Base64.NO_WRAP);
1391
- }
1392
1409
 
1393
- if (listener != null) {
1394
- listener.onPictureTaken(resultValue, exifData);
1395
- }
1396
- } catch (Exception e) {
1397
- Log.e(TAG, "capturePhoto: Error processing image", e);
1398
- if (listener != null) {
1399
- listener.onPictureTakenError(
1400
- "Error processing image: " + e.getMessage()
1401
- );
1402
- }
1403
- } finally {
1404
- // End of capture lifecycle
1405
- synchronized (captureLock) {
1406
- isCapturingPhoto = false;
1407
- if (stopRequested) {
1408
- performImmediateStop();
1410
+ if (listener != null) {
1411
+ listener.onPictureTaken(resultValue, exifData);
1412
+ }
1413
+ } catch (Exception e) {
1414
+ Log.e(TAG, "capturePhoto: Error processing image", e);
1415
+ if (listener != null) {
1416
+ listener.onPictureTakenError(
1417
+ "Error processing image: " + e.getMessage()
1418
+ );
1419
+ }
1420
+ } finally {
1421
+ // End of capture lifecycle
1422
+ synchronized (captureLock) {
1423
+ isCapturingPhoto = false;
1424
+ if (stopRequested) {
1425
+ performImmediateStop();
1426
+ }
1409
1427
  }
1428
+ endOperation("capturePhoto");
1410
1429
  }
1411
- endOperation("capturePhoto");
1412
1430
  }
1413
1431
  }
1432
+ );
1433
+
1434
+ dispatched = true;
1435
+ } catch (Exception e) {
1436
+ Log.e(TAG, "capturePhoto: Failed to start photo capture", e);
1437
+ if (listener != null) {
1438
+ listener.onPictureTakenError("Photo capture failed: " + e.getMessage());
1414
1439
  }
1415
- );
1440
+ } finally {
1441
+ if (!dispatched) {
1442
+ synchronized (captureLock) {
1443
+ isCapturingPhoto = false;
1444
+ if (stopRequested) {
1445
+ performImmediateStop();
1446
+ }
1447
+ }
1448
+ endOperation("capturePhoto");
1449
+ }
1450
+ }
1416
1451
  }
1417
1452
 
1418
1453
  private Bitmap drawTimestampAndLocationOntoBitmap(
@@ -1970,6 +2005,13 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1970
2005
  // we recompress JPEG in-memory and update EXIF info only in the returned JSON, not in the bytes.
1971
2006
 
1972
2007
  public void captureSample(int quality) {
2008
+ if (sampleImageCapture == null) {
2009
+ if (listener != null) {
2010
+ listener.onSampleTakenError("Camera not ready");
2011
+ }
2012
+ return;
2013
+ }
2014
+
1973
2015
  if (IsOperationRunning("captureSample")) {
1974
2016
  Log.d(TAG, "captureSample: Ignored because stop is pending");
1975
2017
  return;
@@ -1979,52 +2021,59 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1979
2021
  "captureSample: Starting sample capture with quality: " + quality
1980
2022
  );
1981
2023
 
1982
- if (sampleImageCapture == null) {
1983
- if (listener != null) {
1984
- listener.onSampleTakenError("Camera not ready");
1985
- }
1986
- return;
1987
- }
1988
-
1989
- sampleImageCapture.takePicture(
1990
- cameraExecutor,
1991
- new ImageCapture.OnImageCapturedCallback() {
1992
- @Override
1993
- public void onError(@NonNull ImageCaptureException exception) {
1994
- Log.e(TAG, "captureSample: Sample capture failed", exception);
1995
- if (listener != null) {
1996
- listener.onSampleTakenError(
1997
- "Sample capture failed: " + exception.getMessage()
1998
- );
1999
- }
2000
- endOperation("captureSample");
2001
- }
2002
-
2003
- @Override
2004
- public void onCaptureSuccess(@NonNull ImageProxy image) {
2005
- //noinspection TryFinallyCanBeTryWithResources
2006
- try {
2007
- // Convert ImageProxy to byte array
2008
- byte[] bytes = imageProxyToByteArray(image);
2009
- String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
2010
-
2011
- if (listener != null) {
2012
- listener.onSampleTaken(base64);
2013
- }
2014
- } catch (Exception e) {
2015
- Log.e(TAG, "captureSample: Error processing sample", e);
2024
+ boolean dispatched = false;
2025
+ try {
2026
+ sampleImageCapture.takePicture(
2027
+ cameraExecutor,
2028
+ new ImageCapture.OnImageCapturedCallback() {
2029
+ @Override
2030
+ public void onError(@NonNull ImageCaptureException exception) {
2031
+ Log.e(TAG, "captureSample: Sample capture failed", exception);
2016
2032
  if (listener != null) {
2017
2033
  listener.onSampleTakenError(
2018
- "Error processing sample: " + e.getMessage()
2034
+ "Sample capture failed: " + exception.getMessage()
2019
2035
  );
2020
2036
  }
2021
- } finally {
2022
- image.close();
2023
2037
  endOperation("captureSample");
2024
2038
  }
2039
+
2040
+ @Override
2041
+ public void onCaptureSuccess(@NonNull ImageProxy image) {
2042
+ //noinspection TryFinallyCanBeTryWithResources
2043
+ try {
2044
+ // Convert ImageProxy to byte array
2045
+ byte[] bytes = imageProxyToByteArray(image);
2046
+ String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
2047
+
2048
+ if (listener != null) {
2049
+ listener.onSampleTaken(base64);
2050
+ }
2051
+ } catch (Exception e) {
2052
+ Log.e(TAG, "captureSample: Error processing sample", e);
2053
+ if (listener != null) {
2054
+ listener.onSampleTakenError(
2055
+ "Error processing sample: " + e.getMessage()
2056
+ );
2057
+ }
2058
+ } finally {
2059
+ image.close();
2060
+ endOperation("captureSample");
2061
+ }
2062
+ }
2025
2063
  }
2064
+ );
2065
+
2066
+ dispatched = true;
2067
+ } catch (Exception e) {
2068
+ Log.e(TAG, "captureSample: Failed to start sample capture", e);
2069
+ if (listener != null) {
2070
+ listener.onSampleTakenError("Sample capture failed: " + e.getMessage());
2026
2071
  }
2027
- );
2072
+ } finally {
2073
+ if (!dispatched) {
2074
+ endOperation("captureSample");
2075
+ }
2076
+ }
2028
2077
  }
2029
2078
 
2030
2079
  private byte[] imageProxyToByteArray(ImageProxy image) {
@@ -2377,18 +2426,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2377
2426
  );
2378
2427
  return;
2379
2428
  }
2380
- if (IsOperationRunning("setFocus")) {
2381
- Log.d(TAG, "setFocus: Ignored because stop is pending");
2382
- return;
2383
- }
2384
- if (camera == null) {
2385
- throw new Exception("Camera not initialized");
2386
- }
2387
-
2388
- if (previewView == null) {
2389
- throw new Exception("Preview view not initialized");
2390
- }
2391
-
2392
2429
  // Validate that coordinates are within bounds (0-1 range)
2393
2430
  if (x < 0f || x > 1f || y < 0f || y > 1f) {
2394
2431
  Log.w(TAG, "setFocus: Coordinates out of bounds - x: " + x + ", y: " + y);
@@ -2426,21 +2463,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2426
2463
  );
2427
2464
  }
2428
2465
 
2429
- // Only show focus indicator after validation passes
2430
- float indicatorX = x * viewWidth;
2431
- float indicatorY = y * viewHeight;
2432
- final long indicatorToken;
2433
- long indicatorToken1;
2434
- try {
2435
- indicatorToken1 = showFocusIndicator(indicatorX, indicatorY);
2436
- } catch (Exception ignore) {
2437
- // If we can't show the indicator (e.g., view is gone), still proceed with metering
2438
- // Use current token so hide is a no-op later
2439
- indicatorToken1 = focusIndicatorAnimationId;
2440
- }
2441
-
2442
- // Create MeteringPoint using the preview view
2443
- indicatorToken = indicatorToken1;
2444
2466
  MeteringPointFactory factory = previewView.getMeteringPointFactory();
2445
2467
  MeteringPoint point = factory.createPoint(x * viewWidth, y * viewHeight);
2446
2468
 
@@ -2452,16 +2474,34 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2452
2474
  .disableAutoCancel()
2453
2475
  .build();
2454
2476
 
2477
+ if (IsOperationRunning("setFocus")) {
2478
+ Log.d(TAG, "setFocus: Ignored because stop is pending");
2479
+ return;
2480
+ }
2481
+
2482
+ // Only show focus indicator after validation passes and operation is accepted
2483
+ float indicatorX = x * viewWidth;
2484
+ float indicatorY = y * viewHeight;
2485
+ long indicatorToken = focusIndicatorAnimationId;
2455
2486
  try {
2456
- final ListenableFuture<FocusMeteringResult> future = camera
2457
- .getCameraControl()
2458
- .startFocusAndMetering(action);
2487
+ indicatorToken = showFocusIndicator(indicatorX, indicatorY);
2488
+ } catch (Exception ignore) {
2489
+ // If we can't show the indicator (e.g., view is gone), still proceed with metering
2490
+ }
2491
+
2492
+ ListenableFuture<FocusMeteringResult> future = null;
2493
+ boolean dispatched = false;
2494
+ try {
2495
+ future = camera.getCameraControl().startFocusAndMetering(action);
2459
2496
  currentFocusFuture = future;
2497
+ dispatched = true;
2460
2498
 
2499
+ final ListenableFuture<FocusMeteringResult> capturedFuture = future;
2500
+ final long tokenForListener = indicatorToken;
2461
2501
  future.addListener(
2462
2502
  () -> {
2463
2503
  try {
2464
- FocusMeteringResult result = future.get();
2504
+ FocusMeteringResult result = capturedFuture.get();
2465
2505
  } catch (Exception e) {
2466
2506
  // Handle cancellation gracefully - this is expected when rapid taps occur
2467
2507
  if (
@@ -2483,10 +2523,13 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2483
2523
  Log.e(TAG, "Error during focus: " + e.getMessage());
2484
2524
  }
2485
2525
  } finally {
2486
- if (currentFocusFuture == future && currentFocusFuture.isDone()) {
2526
+ if (
2527
+ currentFocusFuture == capturedFuture &&
2528
+ currentFocusFuture.isDone()
2529
+ ) {
2487
2530
  currentFocusFuture = null;
2488
2531
  }
2489
- hideFocusIndicator(indicatorToken);
2532
+ hideFocusIndicator(tokenForListener);
2490
2533
  endOperation("setFocus");
2491
2534
  }
2492
2535
  },
@@ -2495,9 +2538,15 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2495
2538
  } catch (Exception e) {
2496
2539
  currentFocusFuture = null;
2497
2540
  Log.e(TAG, "Failed to set focus: " + e.getMessage());
2498
- hideFocusIndicator(indicatorToken);
2499
- endOperation("setFocus");
2500
2541
  throw e;
2542
+ } finally {
2543
+ if (!dispatched) {
2544
+ if (currentFocusFuture == future) {
2545
+ currentFocusFuture = null;
2546
+ }
2547
+ hideFocusIndicator(indicatorToken);
2548
+ endOperation("setFocus");
2549
+ }
2501
2550
  }
2502
2551
  }
2503
2552
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "7.21.8",
3
+ "version": "7.21.10",
4
4
  "description": "Camera preview",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -8,7 +8,7 @@
8
8
  "url": "https://github.com/Cap-go/capacitor-camera-preview"
9
9
  },
10
10
  "bugs": {
11
- "url": "https://github.com/Cap-go/camera-preview/issues"
11
+ "url": "https://github.com/Cap-go/capacitor-camera-preview/issues"
12
12
  },
13
13
  "author": "Martin Donadieu <martin@capgo.app>",
14
14
  "keywords": [