From 1b44b97368da01e86d436ef2ba46e78efa51f63a Mon Sep 17 00:00:00 2001 From: PhilippeFerreiraDeSousa Date: Fri, 10 Jul 2026 19:56:33 -0700 Subject: [PATCH 1/2] Improve camera device error reporting --- .../asg_client/camera/CameraNeoReadme.md | 8 +-- .../asg_client/camera/CameraNeoService.java | 34 ++++++--- .../camera/lifecycle/PhotoSession.java | 46 ++++++++---- .../camera/model/CameraOperationError.java | 70 +++++++++++++++++++ .../io/media/core/MediaCaptureService.java | 58 ++++++--------- .../io/media/managers/PhotoQueueManager.java | 7 +- .../camera/lifecycle/PhotoSessionTest.java | 42 ++++++++++- .../model/CameraOperationErrorTest.java | 70 +++++++++++++++++++ 8 files changed, 270 insertions(+), 65 deletions(-) create mode 100644 asg_client/app/src/main/java/com/mentra/asg_client/camera/model/CameraOperationError.java create mode 100644 asg_client/app/src/test/java/com/mentra/asg_client/camera/model/CameraOperationErrorTest.java diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/camera/CameraNeoReadme.md b/asg_client/app/src/main/java/com/mentra/asg_client/camera/CameraNeoReadme.md index e4f64ebd93..965922a2b6 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/camera/CameraNeoReadme.md +++ b/asg_client/app/src/main/java/com/mentra/asg_client/camera/CameraNeoReadme.md @@ -30,8 +30,8 @@ CameraNeoService.takePictureWithCallback( context, filePath, // if null/empty, a default path is generated new CameraNeoService.PhotoCaptureCallback() { - @Override public void onPhotoCaptured(String filePath) {} - @Override public void onPhotoError(String error) {} + @Override public void onPhotoCaptured(String filePath, JSONObject captureMetadata) {} + @Override public void onPhotoError(CameraOperationError error) {} } ); @@ -197,8 +197,8 @@ Notes: ```java public interface PhotoCaptureCallback { - void onPhotoCaptured(String filePath); - void onPhotoError(String errorMessage); + void onPhotoCaptured(String filePath, @Nullable JSONObject captureMetadata); + void onPhotoError(CameraOperationError error); } public interface VideoRecordingCallback { diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/camera/CameraNeoService.java b/asg_client/app/src/main/java/com/mentra/asg_client/camera/CameraNeoService.java index 3f22654e0e..db84655e0e 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/camera/CameraNeoService.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/camera/CameraNeoService.java @@ -31,6 +31,7 @@ import com.mentra.asg_client.camera.lifecycle.PhotoSession; import org.json.JSONObject; import com.mentra.asg_client.camera.lifecycle.VideoRecordingSession; +import com.mentra.asg_client.camera.model.CameraOperationError; import com.mentra.asg_client.camera.model.PhotoCaptureSettings; import com.mentra.asg_client.camera.model.QueuedPhotoRequest; import com.mentra.asg_client.camera.model.QueuedPhotoRequestQueue; @@ -168,7 +169,11 @@ default void onPhotoCaptured(String filePath) { void onPhotoCaptured(String filePath, @Nullable JSONObject captureMetadata); - void onPhotoError(String errorMessage); + void onPhotoError(CameraOperationError error); + + default void onPhotoError(String errorMessage) { + onPhotoError(CameraOperationError.captureFailed(errorMessage)); + } } /** Callback for {@code camera_warm_up} lifecycle (no capture). */ @@ -186,7 +191,11 @@ public interface CameraWarmUpCallback { void onCameraStopped(); /** Warm-up failed (open/configure failure). */ - void onCameraError(String errorMessage); + void onCameraError(CameraOperationError error); + + default void onCameraError(String errorMessage) { + onCameraError(CameraOperationError.warmUpFailed(errorMessage)); + } /** * The phone-side request this warm-up belongs to, or {@code null} if none. Used to keep at @@ -760,7 +769,7 @@ public static void warmUpCamera( // at the entry gate could race a capture that starts before setupWarmUp runs, letting the // phone see `warming` immediately followed by `error` (camera_busy). if (rejectBusy != null) { - rejectBusy.onCameraError("camera_busy"); + rejectBusy.onCameraError(CameraOperationError.cameraBusy()); } } @@ -953,14 +962,14 @@ private void fireWarmReady() { } /** Fail every in-flight warm-up (open/configure failure). */ - private void fireWarmError(String errorMessage) { + private void fireWarmError(CameraOperationError error) { final List failed; synchronized (SERVICE_LOCK) { failed = new ArrayList<>(warmCallbacks); warmCallbacks.clear(); } for (CameraWarmUpCallback callback : failed) { - callback.onCameraError(errorMessage); + callback.onCameraError(error); } } @@ -1261,15 +1270,22 @@ public void onDisconnected(@NonNull CameraDevice camera) { @Override public void onError(@NonNull CameraDevice camera, int error) { - Log.e(TAG, "Camera device error: " + error); + CameraOperationError cameraError = + CameraOperationError.fromCameraDeviceError(error); + Log.e( + TAG, + "Camera open failed: " + + cameraError.code() + + " (Android code " + + error + + ")"); cameraCoordinator.releaseOpenCloseLock(); camera.close(); cameraCoordinator.clearDevice(); if (forVideo) { - notifyVideoError( - videoSession.currentVideoId(), "Camera device error: " + error); + notifyVideoError(videoSession.currentVideoId(), cameraError.message()); } else { - photoSession.notifyHostPhotoError("Camera device error: " + error); + photoSession.notifyHostPhotoError(cameraError); } stopSelf(); } diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/camera/lifecycle/PhotoSession.java b/asg_client/app/src/main/java/com/mentra/asg_client/camera/lifecycle/PhotoSession.java index 99ac5052fa..bdaa7a09cd 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/camera/lifecycle/PhotoSession.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/camera/lifecycle/PhotoSession.java @@ -16,6 +16,7 @@ import com.mentra.asg_client.camera.CameraSettings; import com.mentra.asg_client.camera.diagnostics.CameraDiagnosticsLog; import com.mentra.asg_client.camera.model.ActivePhotoCapture; +import com.mentra.asg_client.camera.model.CameraOperationError; import com.mentra.asg_client.camera.model.PhotoCaptureSettings; import com.mentra.asg_client.camera.model.QueuedPhotoRequest; import com.mentra.asg_client.camera.model.QueuedPhotoRequestQueue; @@ -570,7 +571,7 @@ public void setupWarmUp( @Nullable PhotoCaptureSettings captureSettings, long durationMs, Runnable onReady, - java.util.function.Consumer onError) { + java.util.function.Consumer onError) { synchronized (hooks.serviceLock()) { // Serialize warm-ups: the camera is a single shared resource. If a capture is in flight, // or another warm-up is still opening, reject with a retryable busy error rather than @@ -586,7 +587,7 @@ public void setupWarmUp( + shotState + ")"); if (onError != null) { - onError.accept("camera_busy"); + onError.accept(CameraOperationError.cameraBusy()); } return; } @@ -657,20 +658,27 @@ private void finishWarmUpReady() { } } - private void failWarmUp(String errorMessage) { + private void failWarmUp(CameraOperationError error) { WarmUpRequest req = warmUpRequest; warmUpRequest = null; clearActiveCapture(); if (req != null && req.onError != null) { - hooks.executor().execute(() -> req.onError.accept(errorMessage)); + hooks.executor().execute(() -> req.onError.accept(error)); } } private void failWarmUpOrPhoto(String errorMessage) { + failWarmUpOrPhoto( + warmUpRequest != null + ? CameraOperationError.warmUpFailed(errorMessage) + : CameraOperationError.captureFailed(errorMessage)); + } + + private void failWarmUpOrPhoto(CameraOperationError error) { if (warmUpRequest != null) { - failWarmUp(errorMessage); + failWarmUp(error); } else { - notifyPhotoError(errorMessage); + notifyPhotoError(error); } } @@ -1007,11 +1015,15 @@ private void recordStillCaptureMetadata(long captureGeneration, JSONObject captu } private void notifyPhotoError(String errorMessage) { + notifyPhotoError(CameraOperationError.captureFailed(errorMessage)); + } + + private void notifyPhotoError(CameraOperationError error) { resetCaptureMetadataState(); CameraNeoService.PhotoCaptureCallback callback = activeCapture != null ? activeCapture.callback : null; if (callback != null) { - hooks.executor().execute(() -> callback.onPhotoError(errorMessage)); + hooks.executor().execute(() -> callback.onPhotoError(error)); } } @@ -1175,7 +1187,9 @@ public void startPreviewWithAeMonitoring() { } catch (CameraAccessException e) { Log.e(TAG, "Error starting preview with AE monitoring", e); if (warmUpRequest != null) { - failWarmUp("Error starting preview: " + e.getMessage()); + failWarmUp( + CameraOperationError.warmUpFailed( + "Error starting preview: " + e.getMessage())); } else { notifyPhotoError("Error starting preview: " + e.getMessage()); } @@ -1743,11 +1757,19 @@ public String photoRequestSizeTier() { /** Called from {@link CameraNeoService} when setup/open/session errors occur before capture. */ public void notifyHostPhotoError(String errorMessage) { + notifyHostPhotoError( + warmUpRequest != null + ? CameraOperationError.warmUpFailed(errorMessage) + : CameraOperationError.captureFailed(errorMessage)); + } + + /** Called when setup/open/session code has classified the failure. */ + public void notifyHostPhotoError(CameraOperationError error) { if (warmUpRequest != null) { - failWarmUp(errorMessage); + failWarmUp(error); return; } - notifyPhotoError(errorMessage); + notifyPhotoError(error); } public int previewJpegQuality() { @@ -1761,7 +1783,7 @@ private static final class WarmUpRequest { @Nullable final PhotoCaptureSettings captureSettings; final long durationMs; @Nullable final Runnable onReady; - @Nullable final java.util.function.Consumer onError; + @Nullable final java.util.function.Consumer onError; WarmUpRequest( @Nullable String size, @@ -1769,7 +1791,7 @@ private static final class WarmUpRequest { @Nullable PhotoCaptureSettings captureSettings, long durationMs, @Nullable Runnable onReady, - @Nullable java.util.function.Consumer onError) { + @Nullable java.util.function.Consumer onError) { this.size = size; this.exposureTimeNs = exposureTimeNs; this.captureSettings = captureSettings; diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/camera/model/CameraOperationError.java b/asg_client/app/src/main/java/com/mentra/asg_client/camera/model/CameraOperationError.java new file mode 100644 index 0000000000..dda05378db --- /dev/null +++ b/asg_client/app/src/main/java/com/mentra/asg_client/camera/model/CameraOperationError.java @@ -0,0 +1,70 @@ +package com.mentra.asg_client.camera.model; + +import android.hardware.camera2.CameraDevice; +import androidx.annotation.NonNull; + +/** Structured camera failure carried from Camera2 callbacks to SDK responses. */ +public final class CameraOperationError { + public static final String CAMERA_CAPTURE_FAILED = "CAMERA_CAPTURE_FAILED"; + public static final String CAMERA_WARM_UP_FAILED = "camera_warm_up_failed"; + + private final String code; + private final String message; + + public CameraOperationError(@NonNull String code, @NonNull String message) { + this.code = code; + this.message = message; + } + + @NonNull + public String code() { + return code; + } + + @NonNull + public String message() { + return message; + } + + public static CameraOperationError captureFailed(@NonNull String message) { + return new CameraOperationError(CAMERA_CAPTURE_FAILED, message); + } + + public static CameraOperationError warmUpFailed(@NonNull String message) { + return new CameraOperationError(CAMERA_WARM_UP_FAILED, message); + } + + public static CameraOperationError cameraBusy() { + return new CameraOperationError( + "camera_busy", "Camera is busy with another operation. Wait and try again."); + } + + public static CameraOperationError fromCameraDeviceError(int error) { + switch (error) { + case CameraDevice.StateCallback.ERROR_CAMERA_IN_USE: + return new CameraOperationError( + "CAMERA_IN_USE", "Camera is already in use. Wait and try again."); + case CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE: + return new CameraOperationError( + "MAX_CAMERAS_IN_USE", + "Another camera operation is active. Wait and try again."); + case CameraDevice.StateCallback.ERROR_CAMERA_DISABLED: + return new CameraOperationError( + "CAMERA_DISABLED", "Camera access is disabled by the system."); + case CameraDevice.StateCallback.ERROR_CAMERA_DEVICE: + return new CameraOperationError( + "CAMERA_DEVICE_ERROR", + "Camera hardware was not ready. Wait a few seconds and try again."); + case CameraDevice.StateCallback.ERROR_CAMERA_SERVICE: + return new CameraOperationError( + "CAMERA_SERVICE_ERROR", + "Camera service failed. Restart the glasses if the problem continues."); + default: + return new CameraOperationError( + "CAMERA_DEVICE_ERROR", + "Camera failed with an unknown device error (Android code " + + error + + "). Try again."); + } + } +} diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/io/media/core/MediaCaptureService.java b/asg_client/app/src/main/java/com/mentra/asg_client/io/media/core/MediaCaptureService.java index 2c78851823..3b6b61f29d 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/io/media/core/MediaCaptureService.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/io/media/core/MediaCaptureService.java @@ -9,6 +9,7 @@ import androidx.annotation.NonNull; import com.mentra.asg_client.audio.AudioAssets; import com.mentra.asg_client.camera.CameraNeoService; +import com.mentra.asg_client.camera.model.CameraOperationError; import com.mentra.asg_client.camera.model.PhotoCaptureSettings; import com.mentra.asg_client.settings.AsgSettings; import com.mentra.asg_client.camera.policy.PhotoSizeTier; @@ -1620,10 +1621,10 @@ public void onPhotoCaptured(String filePath, JSONObject captureMetadata) { } @Override - public void onPhotoError(String errorMessage) { - Log.e(TAG, "Failed to capture offline photo: " + errorMessage); + public void onPhotoError(CameraOperationError error) { + Log.e(TAG, "Failed to capture offline photo: " + error.message()); sendPhotoStatus( - requestId, "failed", null, "CAMERA_CAPTURE_FAILED", errorMessage); + requestId, "failed", null, error.code(), error.message()); // LED is now managed by CameraNeoService and will turn off when camera // closes @@ -1631,7 +1632,7 @@ public void onPhotoError(String errorMessage) { if (mMediaCaptureListener != null) { mMediaCaptureListener.onMediaError( requestId, - errorMessage, + error.message(), MediaUploadQueueManager.MEDIA_TYPE_PHOTO); } } @@ -1772,15 +1773,14 @@ public void onPhotoCaptured(String filePath, JSONObject captureMetadata) { } @Override - public void onPhotoError(String errorMessage) { - Log.e(TAG, "Failed to capture local-save photo: " + errorMessage); - sendPhotoErrorResponse( - requestId, "CAMERA_CAPTURE_FAILED", errorMessage); + public void onPhotoError(CameraOperationError error) { + Log.e(TAG, "Failed to capture local-save photo: " + error.message()); + sendPhotoErrorResponse(requestId, error.code(), error.message()); if (mMediaCaptureListener != null) { mMediaCaptureListener.onMediaError( requestId, - errorMessage, + error.message(), MediaUploadQueueManager.MEDIA_TYPE_PHOTO); } } @@ -2079,12 +2079,11 @@ public void onPhotoCaptured(String filePath, JSONObject captureMetadata) { } @Override - public void onPhotoError(String errorMessage) { + public void onPhotoError(CameraOperationError error) { releasePhotoJob(requestId); - Log.e(TAG, "Failed to capture photo: " + errorMessage); - sendPhotoErrorResponse( - requestId, "CAMERA_CAPTURE_FAILED", errorMessage); + Log.e(TAG, "Failed to capture photo: " + error.message()); + sendPhotoErrorResponse(requestId, error.code(), error.message()); // LED is now managed by CameraNeoService and will turn off when camera // closes @@ -2093,9 +2092,9 @@ public void onPhotoError(String errorMessage) { if (mMediaCaptureListener != null) { mMediaCaptureListener.onMediaError( - requestId, - errorMessage, - MediaUploadQueueManager.MEDIA_TYPE_PHOTO); + requestId, + error.message(), + MediaUploadQueueManager.MEDIA_TYPE_PHOTO); } } }); @@ -3861,23 +3860,22 @@ public void onPhotoCaptured(String filePath, JSONObject captureMetadata) { } @Override - public void onPhotoError(String errorMessage) { + public void onPhotoError(CameraOperationError error) { releasePhotoJob(requestId); - Log.e(TAG, "Failed to capture photo for BLE: " + errorMessage); + Log.e(TAG, "Failed to capture photo for BLE: " + error.message()); // LED is now managed by CameraNeoService and will turn off when camera // closes dumpTimings(requestId); - sendPhotoErrorResponse( - requestId, "CAMERA_CAPTURE_FAILED", errorMessage); + sendPhotoErrorResponse(requestId, error.code(), error.message()); if (mMediaCaptureListener != null) { mMediaCaptureListener.onMediaError( - requestId, - errorMessage, - MediaUploadQueueManager.MEDIA_TYPE_PHOTO); + requestId, + error.message(), + MediaUploadQueueManager.MEDIA_TYPE_PHOTO); } } }); @@ -4387,15 +4385,12 @@ public void onCameraStopped() { } @Override - public void onCameraError(String errorMessage) { + public void onCameraError(CameraOperationError error) { if (warmUpDispatching.get()) { rejectedSynchronously.set(true); } sendCameraStatus( - requestId, - "error", - cameraWarmUpErrorCode(errorMessage), - errorMessage); + requestId, "error", error.code(), error.message()); } @Override @@ -4457,13 +4452,6 @@ public void sendCameraStatus( } } - private static String cameraWarmUpErrorCode(String errorMessage) { - if ("camera_busy".equals(errorMessage)) { - return "camera_busy"; - } - return "camera_warm_up_failed"; - } - private void sendPhotoStatus(String requestId, String status) { sendPhotoStatus(requestId, status, null, null, null); } diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/io/media/managers/PhotoQueueManager.java b/asg_client/app/src/main/java/com/mentra/asg_client/io/media/managers/PhotoQueueManager.java index a34fb34038..daea6dfaa1 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/io/media/managers/PhotoQueueManager.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/io/media/managers/PhotoQueueManager.java @@ -21,6 +21,7 @@ import com.mentra.asg_client.io.media.upload.PhotoUploadService; import com.mentra.asg_client.camera.CameraNeoService; +import com.mentra.asg_client.camera.model.CameraOperationError; /** * Manages a queue of photos to be uploaded to AugmentOS Cloud. @@ -131,8 +132,8 @@ public void onPhotoCaptured(String filePath, JSONObject captureMetadata) { } @Override - public void onPhotoError(String errorMessage) { - callback.onPhotoError(errorMessage); + public void onPhotoError(CameraOperationError error) { + callback.onPhotoError(error.message()); } } ); @@ -872,4 +873,4 @@ public int clearQueue() { public File getQueueDirectory() { return mQueueDir; } -} \ No newline at end of file +} diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/camera/lifecycle/PhotoSessionTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/camera/lifecycle/PhotoSessionTest.java index fd85f4c09c..4712c52fb3 100644 --- a/asg_client/app/src/test/java/com/mentra/asg_client/camera/lifecycle/PhotoSessionTest.java +++ b/asg_client/app/src/test/java/com/mentra/asg_client/camera/lifecycle/PhotoSessionTest.java @@ -16,17 +16,20 @@ import android.hardware.camera2.CaptureRequest; import android.os.Handler; import com.mentra.asg_client.camera.CameraNeoService; +import com.mentra.asg_client.camera.model.CameraOperationError; +import com.mentra.asg_client.camera.model.PhotoCaptureSettings; import com.mentra.asg_client.camera.model.QueuedPhotoRequest; import com.mentra.asg_client.camera.model.QueuedPhotoRequestQueue; import com.mentra.asg_client.camera.policy.AeStateMachine; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; +import org.json.JSONObject; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.json.JSONObject; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @@ -169,6 +172,40 @@ public void finishFailedPhotoCapture_clearsActiveCaptureBeforeDispatch() throws verify(hooks).startPostCaptureKeepAlive(); } + @Test + public void notifyHostPhotoError_preservesStructuredPhotoError() throws Exception { + PhotoSession.Hooks hooks = mockConfiguredCameraHooks(); + CameraNeoService.PhotoCaptureCallback callback = + mock(CameraNeoService.PhotoCaptureCallback.class); + QueuedPhotoRequest request = + new QueuedPhotoRequest("/tmp/failed.jpg", "large", false, true, null, callback); + PhotoSession session = new PhotoSession(hooks); + activateQueuedRequest(session, request); + CameraOperationError error = + CameraOperationError.fromCameraDeviceError( + CameraDevice.StateCallback.ERROR_CAMERA_DEVICE); + + session.notifyHostPhotoError(error); + + verify(callback).onPhotoError(error); + } + + @Test + public void notifyHostPhotoError_stringDuringWarmUp_usesWarmUpFailureCode() { + PhotoSession.Hooks hooks = mockConfiguredCameraHooks(); + when(hooks.coordinator().isCameraKeptAlive()).thenReturn(false); + PhotoSession session = new PhotoSession(hooks); + AtomicReference receivedError = new AtomicReference<>(); + session.setupWarmUp( + "large", null, PhotoCaptureSettings.EMPTY, 30_000, () -> {}, receivedError::set); + + session.notifyHostPhotoError("Camera disconnected"); + + assertThat(receivedError.get().code()) + .isEqualTo(CameraOperationError.CAMERA_WARM_UP_FAILED); + assertThat(receivedError.get().message()).isEqualTo("Camera disconnected"); + } + @Test public void notifyPhotoCaptured_duplicateWhileMetadataPending_keepsTimeoutCompletingQueue() throws Exception { @@ -276,7 +313,8 @@ private static void activateQueuedRequest(PhotoSession session, QueuedPhotoReque load.invoke(session, request); } - private static void notifyPhotoCaptured(PhotoSession session, String filePath) throws Exception { + private static void notifyPhotoCaptured(PhotoSession session, String filePath) + throws Exception { Method notify = PhotoSession.class.getDeclaredMethod("notifyPhotoCaptured", String.class); notify.setAccessible(true); notify.invoke(session, filePath); diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/camera/model/CameraOperationErrorTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/camera/model/CameraOperationErrorTest.java new file mode 100644 index 0000000000..aa346ba022 --- /dev/null +++ b/asg_client/app/src/test/java/com/mentra/asg_client/camera/model/CameraOperationErrorTest.java @@ -0,0 +1,70 @@ +package com.mentra.asg_client.camera.model; + +import static org.assertj.core.api.Assertions.assertThat; + +import android.hardware.camera2.CameraDevice; +import org.junit.Test; + +public class CameraOperationErrorTest { + + @Test + public void fromCameraDeviceError_mapsCameraInUse() { + CameraOperationError error = + CameraOperationError.fromCameraDeviceError( + CameraDevice.StateCallback.ERROR_CAMERA_IN_USE); + + assertThat(error.code()).isEqualTo("CAMERA_IN_USE"); + assertThat(error.message()).isEqualTo("Camera is already in use. Wait and try again."); + } + + @Test + public void fromCameraDeviceError_mapsMaxCamerasInUse() { + CameraOperationError error = + CameraOperationError.fromCameraDeviceError( + CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE); + + assertThat(error.code()).isEqualTo("MAX_CAMERAS_IN_USE"); + assertThat(error.message()) + .isEqualTo("Another camera operation is active. Wait and try again."); + } + + @Test + public void fromCameraDeviceError_mapsCameraDisabled() { + CameraOperationError error = + CameraOperationError.fromCameraDeviceError( + CameraDevice.StateCallback.ERROR_CAMERA_DISABLED); + + assertThat(error.code()).isEqualTo("CAMERA_DISABLED"); + assertThat(error.message()).isEqualTo("Camera access is disabled by the system."); + } + + @Test + public void fromCameraDeviceError_mapsCameraDeviceFailure() { + CameraOperationError error = + CameraOperationError.fromCameraDeviceError( + CameraDevice.StateCallback.ERROR_CAMERA_DEVICE); + + assertThat(error.code()).isEqualTo("CAMERA_DEVICE_ERROR"); + assertThat(error.message()) + .isEqualTo("Camera hardware was not ready. Wait a few seconds and try again."); + } + + @Test + public void fromCameraDeviceError_mapsCameraServiceFailure() { + CameraOperationError error = + CameraOperationError.fromCameraDeviceError( + CameraDevice.StateCallback.ERROR_CAMERA_SERVICE); + + assertThat(error.code()).isEqualTo("CAMERA_SERVICE_ERROR"); + assertThat(error.message()) + .isEqualTo("Camera service failed. Restart the glasses if the problem continues."); + } + + @Test + public void fromCameraDeviceError_preservesUnknownAndroidCodeInMessage() { + CameraOperationError error = CameraOperationError.fromCameraDeviceError(99); + + assertThat(error.code()).isEqualTo("CAMERA_DEVICE_ERROR"); + assertThat(error.message()).contains("Android code 99"); + } +} From a15d5847b7d468da1645c1e318c86e64d5e94cdd Mon Sep 17 00:00:00 2001 From: PhilippeFerreiraDeSousa Date: Fri, 10 Jul 2026 20:56:25 -0700 Subject: [PATCH 2/2] Preserve legacy camera error callbacks --- .../asg_client/camera/CameraNeoReadme.md | 5 +- .../asg_client/camera/CameraNeoService.java | 12 ++-- .../io/media/core/MediaCaptureService.java | 25 +++++++++ .../io/media/managers/PhotoQueueManager.java | 5 +- .../model/CameraOperationErrorTest.java | 56 +++++++++++++++++++ 5 files changed, 92 insertions(+), 11 deletions(-) diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/camera/CameraNeoReadme.md b/asg_client/app/src/main/java/com/mentra/asg_client/camera/CameraNeoReadme.md index 965922a2b6..f41ad6e967 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/camera/CameraNeoReadme.md +++ b/asg_client/app/src/main/java/com/mentra/asg_client/camera/CameraNeoReadme.md @@ -31,7 +31,7 @@ CameraNeoService.takePictureWithCallback( filePath, // if null/empty, a default path is generated new CameraNeoService.PhotoCaptureCallback() { @Override public void onPhotoCaptured(String filePath, JSONObject captureMetadata) {} - @Override public void onPhotoError(CameraOperationError error) {} + @Override public void onPhotoError(String errorMessage) {} } ); @@ -198,7 +198,8 @@ Notes: ```java public interface PhotoCaptureCallback { void onPhotoCaptured(String filePath, @Nullable JSONObject captureMetadata); - void onPhotoError(CameraOperationError error); + void onPhotoError(String errorMessage); + default void onPhotoError(CameraOperationError error); } public interface VideoRecordingCallback { diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/camera/CameraNeoService.java b/asg_client/app/src/main/java/com/mentra/asg_client/camera/CameraNeoService.java index db84655e0e..f37ef2c0ee 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/camera/CameraNeoService.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/camera/CameraNeoService.java @@ -169,10 +169,10 @@ default void onPhotoCaptured(String filePath) { void onPhotoCaptured(String filePath, @Nullable JSONObject captureMetadata); - void onPhotoError(CameraOperationError error); + void onPhotoError(String errorMessage); - default void onPhotoError(String errorMessage) { - onPhotoError(CameraOperationError.captureFailed(errorMessage)); + default void onPhotoError(CameraOperationError error) { + onPhotoError(error.message()); } } @@ -191,10 +191,10 @@ public interface CameraWarmUpCallback { void onCameraStopped(); /** Warm-up failed (open/configure failure). */ - void onCameraError(CameraOperationError error); + void onCameraError(String errorMessage); - default void onCameraError(String errorMessage) { - onCameraError(CameraOperationError.warmUpFailed(errorMessage)); + default void onCameraError(CameraOperationError error) { + onCameraError(error.message()); } /** diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/io/media/core/MediaCaptureService.java b/asg_client/app/src/main/java/com/mentra/asg_client/io/media/core/MediaCaptureService.java index 3b6b61f29d..5df7bb168f 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/io/media/core/MediaCaptureService.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/io/media/core/MediaCaptureService.java @@ -1620,6 +1620,11 @@ public void onPhotoCaptured(String filePath, JSONObject captureMetadata) { sendGalleryStatusUpdate(); } + @Override + public void onPhotoError(String errorMessage) { + onPhotoError(CameraOperationError.captureFailed(errorMessage)); + } + @Override public void onPhotoError(CameraOperationError error) { Log.e(TAG, "Failed to capture offline photo: " + error.message()); @@ -1772,6 +1777,11 @@ public void onPhotoCaptured(String filePath, JSONObject captureMetadata) { sendGalleryStatusUpdate(); } + @Override + public void onPhotoError(String errorMessage) { + onPhotoError(CameraOperationError.captureFailed(errorMessage)); + } + @Override public void onPhotoError(CameraOperationError error) { Log.e(TAG, "Failed to capture local-save photo: " + error.message()); @@ -2078,6 +2088,11 @@ public void onPhotoCaptured(String filePath, JSONObject captureMetadata) { } } + @Override + public void onPhotoError(String errorMessage) { + onPhotoError(CameraOperationError.captureFailed(errorMessage)); + } + @Override public void onPhotoError(CameraOperationError error) { releasePhotoJob(requestId); @@ -3859,6 +3874,11 @@ public void onPhotoCaptured(String filePath, JSONObject captureMetadata) { compressAndSendViaBle(filePath, requestId, bleImgId); } + @Override + public void onPhotoError(String errorMessage) { + onPhotoError(CameraOperationError.captureFailed(errorMessage)); + } + @Override public void onPhotoError(CameraOperationError error) { releasePhotoJob(requestId); @@ -4384,6 +4404,11 @@ public void onCameraStopped() { sendCameraStatus(requestId, "stopped", null); } + @Override + public void onCameraError(String errorMessage) { + onCameraError(CameraOperationError.warmUpFailed(errorMessage)); + } + @Override public void onCameraError(CameraOperationError error) { if (warmUpDispatching.get()) { diff --git a/asg_client/app/src/main/java/com/mentra/asg_client/io/media/managers/PhotoQueueManager.java b/asg_client/app/src/main/java/com/mentra/asg_client/io/media/managers/PhotoQueueManager.java index daea6dfaa1..f6ea0df185 100644 --- a/asg_client/app/src/main/java/com/mentra/asg_client/io/media/managers/PhotoQueueManager.java +++ b/asg_client/app/src/main/java/com/mentra/asg_client/io/media/managers/PhotoQueueManager.java @@ -21,7 +21,6 @@ import com.mentra.asg_client.io.media.upload.PhotoUploadService; import com.mentra.asg_client.camera.CameraNeoService; -import com.mentra.asg_client.camera.model.CameraOperationError; /** * Manages a queue of photos to be uploaded to AugmentOS Cloud. @@ -132,8 +131,8 @@ public void onPhotoCaptured(String filePath, JSONObject captureMetadata) { } @Override - public void onPhotoError(CameraOperationError error) { - callback.onPhotoError(error.message()); + public void onPhotoError(String errorMessage) { + callback.onPhotoError(errorMessage); } } ); diff --git a/asg_client/app/src/test/java/com/mentra/asg_client/camera/model/CameraOperationErrorTest.java b/asg_client/app/src/test/java/com/mentra/asg_client/camera/model/CameraOperationErrorTest.java index aa346ba022..7d18afec53 100644 --- a/asg_client/app/src/test/java/com/mentra/asg_client/camera/model/CameraOperationErrorTest.java +++ b/asg_client/app/src/test/java/com/mentra/asg_client/camera/model/CameraOperationErrorTest.java @@ -3,7 +3,10 @@ import static org.assertj.core.api.Assertions.assertThat; import android.hardware.camera2.CameraDevice; +import com.mentra.asg_client.camera.CameraNeoService; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; +import org.json.JSONObject; public class CameraOperationErrorTest { @@ -67,4 +70,57 @@ public void fromCameraDeviceError_preservesUnknownAndroidCodeInMessage() { assertThat(error.code()).isEqualTo("CAMERA_DEVICE_ERROR"); assertThat(error.message()).contains("Android code 99"); } + + @Test + public void photoCallback_typedErrorDelegatesToLegacyStringContract() { + AtomicReference receivedMessage = new AtomicReference<>(); + CameraNeoService.PhotoCaptureCallback callback = + new CameraNeoService.PhotoCaptureCallback() { + @Override + public void onPhotoCaptured( + String filePath, JSONObject captureMetadata) {} + + @Override + public void onPhotoError(String errorMessage) { + receivedMessage.set(errorMessage); + } + }; + + callback.onPhotoError( + CameraOperationError.fromCameraDeviceError( + CameraDevice.StateCallback.ERROR_CAMERA_DEVICE)); + + assertThat(receivedMessage.get()) + .isEqualTo( + "Camera hardware was not ready. Wait a few seconds and try again."); + } + + @Test + public void warmUpCallback_typedErrorDelegatesToLegacyStringContract() { + AtomicReference receivedMessage = new AtomicReference<>(); + CameraNeoService.CameraWarmUpCallback callback = + new CameraNeoService.CameraWarmUpCallback() { + @Override + public void onWarming() {} + + @Override + public void onCameraReady() {} + + @Override + public void onCameraStopped() {} + + @Override + public void onCameraError(String errorMessage) { + receivedMessage.set(errorMessage); + } + }; + + callback.onCameraError( + CameraOperationError.fromCameraDeviceError( + CameraDevice.StateCallback.ERROR_CAMERA_SERVICE)); + + assertThat(receivedMessage.get()) + .isEqualTo( + "Camera service failed. Restart the glasses if the problem continues."); + } }