diff --git a/README.md b/README.md index 4b3a36ed..896105fc 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,74 @@ camera.postDelayed(new Runnable() { ``` +### Error Handling + +#### Default Handler +The default implementation will just throw all exceptions to prevent missing error handling. + +#### Basic Custom Handler +You can implement custom error handling by overriding the ``onError`` listener. +As soon as this method was overridden at least once (without calling the super method or re-throwing the exception), +the default behavior will be disabled. So pay attention to not swallowing any exceptions. + +```java +camera.addCameraListener(new CameraListener() { + @Override + public void onError(CameraException exception) { + throw exception; + } +}); +``` + +#### Advanced Custom Handler +Furthermore, you can distinguish different error types. E.g. some of them imply that the +CameraView should be disabled or restarted while others may be ignored in some use cases. + +```java +camera.addCameraListener(new CameraListener() { + @Override + public void onError(CameraException exception) { + if (exception instanceof CameraUnavailableException) { + // the exception prevents the camera from being used. The cause may be + // temporary or permanent. You should restart the camera or deactivate any + // user interaction with the camera. + } + else if (exception instanceof CameraConfigurationFailedException) { + // The previously started setting change failed, + // but the camera should be still available. + } + else if (exception instanceof CapturingFailedException) { + // The previously started capturing (of any kind) failed, but the camera + // should be still available. + + if (exception instanceof CapturingImageFailedException) { + // the previously started image capturing failed (snapshot or + // "real picture"), but the camera should be still available. + + if (exception instanceof CapturingPictureFailedException) { + // The previously started picture capturing failed, but the camera + // should be still available. This exception does not handle failed + // snapshots. + } + else if (exception instanceof CapturingSnapshotFailedException) { + // The previously started snapshot capturing failed, but the camera + // should be still available. This exception does not handle failed + // "real picture" capturing. + } + } + else if (exception instanceof CapturingVideoFailedException) { + // The previously started video capturing failed, but the camera + // should be still available. + } + } + else { + throw exception; + } + } +}); +``` + + ### Other camera events Make sure you can react to different camera events by setting up one or more `CameraListener` instances. All these are executed on the UI thread. @@ -220,8 +288,7 @@ camera.addCameraListener(new CameraListener() { * to be changed. This can be used, for example, to draw a seek bar. */ @Override - public void onExposureCorrectionChanged(float newValue, float[] bounds, PointF[] fingers) {} - + public void onExposureCorrectionChanged(float newValue, float[] bounds, PointF[] fingers) {} }); ``` @@ -542,6 +609,7 @@ This is what was done since the library was forked. I have kept the original str - *Tests!* - *`CameraLogger` APIs for logging and bug reports* - *Better threading, start() in worker thread and callbacks in UI* +- *Custom error handling: prevent app crashes (caused by inevitable errors) by simply overriding a listener* These are still things that need to be done, off the top of my head: @@ -550,7 +618,7 @@ These are still things that need to be done, off the top of my head: - [ ] add a `setPreferredAspectRatio` API to choose the capture size. Preview size will adapt, and then, if let free, the CameraView will adapt as well - [ ] animate grid lines similar to stock camera app - [ ] add onRequestPermissionResults for easy permission callback -- [ ] better error handling, maybe with a onError(e) method in the public listener, or have each public method return a boolean +- [ ] better error handling, maybe extending the current onError(e) method to handle more use cases (e.g. the ones only caught by a boolean return value) - [ ] decent code coverage ## Device-specific issues diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java index 0537cf9f..1f4dd0c1 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java @@ -19,6 +19,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static android.hardware.Camera.CAMERA_ERROR_SERVER_DIED; +import static android.hardware.Camera.CAMERA_ERROR_UNKNOWN; + @SuppressWarnings("deprecation") class Camera1 extends CameraController { @@ -37,14 +40,24 @@ class Camera1 extends CameraController { private Runnable mPostFocusResetRunnable = new Runnable() { @Override public void run() { - if (!isCameraAvailable()) return; - mCamera.cancelAutoFocus(); - synchronized (mLock) { - Camera.Parameters params = mCamera.getParameters(); - params.setFocusAreas(null); - params.setMeteringAreas(null); - applyDefaultFocus(params); // Revert to internal focus. - mCamera.setParameters(params); + try { + if (!isCameraAvailable()) return; + mCamera.cancelAutoFocus(); + synchronized (mLock) { + Camera.Parameters params = mCamera.getParameters(); + params.setFocusAreas(null); + params.setMeteringAreas(null); + applyDefaultFocus(params); // Revert to internal focus. + mCamera.setParameters(params); + } + } + catch (Exception e) { + // at least setParameters may fail. + // problem may be device-specific to the Samsung Galaxy J5 + // TODO why does it fail occasionally and is it possible to prevent such errors? + CameraException cameraException = new CameraConfigurationFailedException("Failed to " + + "reset auto focus.", e); + mCameraCallbacks.onError(cameraException); } } }; @@ -75,8 +88,9 @@ class Camera1 extends CameraController { try { setup(); } catch (Exception e) { - LOG.w("onSurfaceAvailable:", "Exception while binding camera to preview.", e); - throw new RuntimeException(e); + CameraException cameraException = new CameraUnavailableException( + "onSurfaceAvailable: Exception while binding camera to preview.", e); + mCameraCallbacks.onError(cameraException); } } }); @@ -164,6 +178,38 @@ class Camera1 extends CameraController { if (collectCameraId()) { mCamera = Camera.open(mCameraId); + /** + * attach Android native error listener for the Camera1 API + * TODO it's not yet sure how the caught errors interact with the exceptions caught + * outside of the following handler. Furthermore, for most errors it's not known whether + * they are crucial or not. Therefore, such errors are handled as low-priority + * CameraConfigurationFailedExceptions for now. + */ + mCamera.setErrorCallback(new Camera.ErrorCallback() { + @Override + public void onError(int errorCode, Camera camera) { + + // extend error information by known error codes + CameraException cameraException; + if (errorCode == CAMERA_ERROR_SERVER_DIED) { + cameraException = new CameraUnavailableException( + "Media server died. In this case, the application must release the" + + " Camera object and instantiate a new one."); + } + else if (errorCode == CAMERA_ERROR_UNKNOWN) { + cameraException = new CameraConfigurationFailedException( + "Unspecified camera error."); + } + else { + cameraException = new CameraConfigurationFailedException( + "Received camera error code: " + errorCode); + } + + // redirect error + mCameraCallbacks.onError(cameraException); + } + }); + // Set parameters that might have been set before the camera was opened. synchronized (mLock) { LOG.i("onStart:", "Applying default parameters."); @@ -250,14 +296,21 @@ class Camera1 extends CameraController { @Override void setLocation(Location location) { - Location oldLocation = mLocation; - mLocation = location; - if (isCameraAvailable()) { - synchronized (mLock) { - Camera.Parameters params = mCamera.getParameters(); - if (mergeLocation(params, oldLocation)) mCamera.setParameters(params); + try { + Location oldLocation = mLocation; + mLocation = location; + if (isCameraAvailable()) { + synchronized (mLock) { + Camera.Parameters params = mCamera.getParameters(); + if (mergeLocation(params, oldLocation)) mCamera.setParameters(params); + } } } + catch (Exception e) { + CameraException cameraException = + new CameraConfigurationFailedException("Failed to set the location.", e); + mCameraCallbacks.onError(cameraException); + } } private boolean mergeLocation(Camera.Parameters params, Location oldLocation) { @@ -288,14 +341,22 @@ class Camera1 extends CameraController { @Override void setWhiteBalance(WhiteBalance whiteBalance) { - WhiteBalance old = mWhiteBalance; - mWhiteBalance = whiteBalance; - if (isCameraAvailable()) { - synchronized (mLock) { - Camera.Parameters params = mCamera.getParameters(); - if (mergeWhiteBalance(params, old)) mCamera.setParameters(params); + try { + WhiteBalance old = mWhiteBalance; + mWhiteBalance = whiteBalance; + if (isCameraAvailable()) { + synchronized (mLock) { + Camera.Parameters params = mCamera.getParameters(); + if (mergeWhiteBalance(params, old)) mCamera.setParameters(params); + } } } + catch (Exception e) { + // TODO handle, !mergeWhiteBalance, too? + CameraException cameraException = + new CameraConfigurationFailedException("Failed to set the white balance.", e); + mCameraCallbacks.onError(cameraException); + } } private boolean mergeWhiteBalance(Camera.Parameters params, WhiteBalance oldWhiteBalance) { @@ -309,14 +370,22 @@ class Camera1 extends CameraController { @Override void setHdr(Hdr hdr) { - Hdr old = mHdr; - mHdr = hdr; - if (isCameraAvailable()) { - synchronized (mLock) { - Camera.Parameters params = mCamera.getParameters(); - if (mergeHdr(params, old)) mCamera.setParameters(params); + try { + Hdr old = mHdr; + mHdr = hdr; + if (isCameraAvailable()) { + synchronized (mLock) { + Camera.Parameters params = mCamera.getParameters(); + if (mergeHdr(params, old)) mCamera.setParameters(params); + } } } + catch (Exception e) { + // TODO handle, !mergeHdr, too? + CameraException cameraException = + new CameraConfigurationFailedException("Failed to set hdr.", e); + mCameraCallbacks.onError(cameraException); + } } private boolean mergeHdr(Camera.Parameters params, Hdr oldHdr) { @@ -341,14 +410,22 @@ class Camera1 extends CameraController { @Override void setFlash(Flash flash) { - Flash old = mFlash; - mFlash = flash; - if (isCameraAvailable()) { - synchronized (mLock) { - Camera.Parameters params = mCamera.getParameters(); - if (mergeFlash(params, old)) mCamera.setParameters(params); + try { + Flash old = mFlash; + mFlash = flash; + if (isCameraAvailable()) { + synchronized (mLock) { + Camera.Parameters params = mCamera.getParameters(); + if (mergeFlash(params, old)) mCamera.setParameters(params); + } } } + catch (Exception e) { + // TODO handle, !mergeFlash, too? + CameraException cameraException = + new CameraConfigurationFailedException("Failed to set flash.", e); + mCameraCallbacks.onError(cameraException); + } } @@ -391,29 +468,37 @@ class Camera1 extends CameraController { @Override void setVideoQuality(VideoQuality videoQuality) { - if (mIsCapturingVideo) { - // TODO: actually any call to getParameters() could fail while recording a video. - // See. https://stackoverflow.com/questions/14941625/correct-handling-of-exception-getparameters-failed-empty-parameters - throw new IllegalStateException("Can't change video quality while recording a video."); - } - - mVideoQuality = videoQuality; - if (isCameraAvailable() && mSessionType == SessionType.VIDEO) { - // Change capture size to a size that fits the video aspect ratio. - Size oldSize = mCaptureSize; - mCaptureSize = computeCaptureSize(); - if (!mCaptureSize.equals(oldSize)) { - // New video quality triggers a new aspect ratio. - // Go on and see if preview size should change also. - synchronized (mLock) { - Camera.Parameters params = mCamera.getParameters(); - params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); - mCamera.setParameters(params); + + try { + if (mIsCapturingVideo) { + // TODO: actually any call to getParameters() could fail while recording a video. + // See. https://stackoverflow.com/questions/14941625/correct-handling-of-exception-getparameters-failed-empty-parameters + throw new IllegalStateException("Can't change video quality while recording a video."); + } + + mVideoQuality = videoQuality; + if (isCameraAvailable() && mSessionType == SessionType.VIDEO) { + // Change capture size to a size that fits the video aspect ratio. + Size oldSize = mCaptureSize; + mCaptureSize = computeCaptureSize(); + if (!mCaptureSize.equals(oldSize)) { + // New video quality triggers a new aspect ratio. + // Go on and see if preview size should change also. + synchronized (mLock) { + Camera.Parameters params = mCamera.getParameters(); + params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); + mCamera.setParameters(params); + } + onSurfaceChanged(); } - onSurfaceChanged(); + LOG.i("setVideoQuality:", "captureSize:", mCaptureSize); + LOG.i("setVideoQuality:", "previewSize:", mPreviewSize); } - LOG.i("setVideoQuality:", "captureSize:", mCaptureSize); - LOG.i("setVideoQuality:", "previewSize:", mPreviewSize); + } + catch (Exception e) { + CameraException cameraException = + new CameraConfigurationFailedException("Failed to set video quality.", e); + mCameraCallbacks.onError(cameraException); } } @@ -425,36 +510,43 @@ class Camera1 extends CameraController { if (!mOptions.isVideoSnapshotSupported()) return false; } - // Set boolean to wait for image callback - mIsCapturingImage = true; - final int exifRotation = computeExifRotation(); - final boolean exifFlip = computeExifFlip(); - final int sensorToDisplay = computeSensorToDisplayOffset(); - synchronized (mLock) { - Camera.Parameters params = mCamera.getParameters(); - params.setRotation(exifRotation); - mCamera.setParameters(params); + try { + // Set boolean to wait for image callback + mIsCapturingImage = true; + final int exifRotation = computeExifRotation(); + final boolean exifFlip = computeExifFlip(); + final int sensorToDisplay = computeSensorToDisplayOffset(); + synchronized (mLock) { + Camera.Parameters params = mCamera.getParameters(); + params.setRotation(exifRotation); + mCamera.setParameters(params); + } + // Is the final picture (decoded respecting EXIF) consistent with CameraView orientation? + // We must consider exifOrientation to bring back the picture in the sensor world. + // Then use sensorToDisplay to move to the display world, where CameraView lives. + final boolean consistentWithView = (exifRotation + sensorToDisplay + 180) % 180 == 0; + mCamera.takePicture(null, null, null, + new Camera.PictureCallback() { + @Override + public void onPictureTaken(byte[] data, final Camera camera) { + mIsCapturingImage = false; + mHandler.post(new Runnable() { + @Override + public void run() { + // This is needed, read somewhere in the docs. + camera.startPreview(); + } + }); + mCameraCallbacks.processImage(data, consistentWithView, exifFlip); + } + }); + return true; + } + catch (Exception e) { + CameraException cameraException = new CapturingPictureFailedException("Capturing a picture failed.", e); + mCameraCallbacks.onError(cameraException); + return false; } - // Is the final picture (decoded respecting EXIF) consistent with CameraView orientation? - // We must consider exifOrientation to bring back the picture in the sensor world. - // Then use sensorToDisplay to move to the display world, where CameraView lives. - final boolean consistentWithView = (exifRotation + sensorToDisplay + 180) % 180 == 0; - mCamera.takePicture(null, null, null, - new Camera.PictureCallback() { - @Override - public void onPictureTaken(byte[] data, final Camera camera) { - mIsCapturingImage = false; - mHandler.post(new Runnable() { - @Override - public void run() { - // This is needed, read somewhere in the docs. - camera.startPreview(); - } - }); - mCameraCallbacks.processImage(data, consistentWithView, exifFlip); - } - }); - return true; } @@ -468,37 +560,45 @@ class Camera1 extends CameraController { capturePicture(); return false; } - mIsCapturingImage = true; - mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() { - @Override - public void onPreviewFrame(final byte[] data, Camera camera) { - // Got to rotate the preview frame, since byte[] data here does not include - // EXIF tags automatically set by camera. So either we add EXIF, or we rotate. - // Adding EXIF to a byte array, unfortunately, is hard. - Camera.Parameters params = mCamera.getParameters(); - final int sensorToDevice = computeExifRotation(); - final int sensorToDisplay = computeSensorToDisplayOffset(); - final boolean exifFlip = computeExifFlip(); - final boolean flip = sensorToDevice % 180 != 0; - final int preWidth = mPreviewSize.getWidth(); - final int preHeight = mPreviewSize.getHeight(); - final int postWidth = flip ? preHeight : preWidth; - final int postHeight = flip ? preWidth : preHeight; - final int format = params.getPreviewFormat(); - WorkerHandler.run(new Runnable() { - @Override - public void run() { - final boolean consistentWithView = (sensorToDevice + sensorToDisplay + 180) % 180 == 0; - byte[] rotatedData = RotationHelper.rotate(data, preWidth, preHeight, sensorToDevice); - YuvImage yuv = new YuvImage(rotatedData, format, postWidth, postHeight, null); - mCameraCallbacks.processSnapshot(yuv, consistentWithView, exifFlip); - mIsCapturingImage = false; - } - }); - } - }); - return true; + try { + mIsCapturingImage = true; + mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() { + @Override + public void onPreviewFrame(final byte[] data, Camera camera) { + // Got to rotate the preview frame, since byte[] data here does not include + // EXIF tags automatically set by camera. So either we add EXIF, or we rotate. + // Adding EXIF to a byte array, unfortunately, is hard. + Camera.Parameters params = mCamera.getParameters(); + final int sensorToDevice = computeExifRotation(); + final int sensorToDisplay = computeSensorToDisplayOffset(); + final boolean exifFlip = computeExifFlip(); + final boolean flip = sensorToDevice % 180 != 0; + final int preWidth = mPreviewSize.getWidth(); + final int preHeight = mPreviewSize.getHeight(); + final int postWidth = flip ? preHeight : preWidth; + final int postHeight = flip ? preWidth : preHeight; + final int format = params.getPreviewFormat(); + WorkerHandler.run(new Runnable() { + @Override + public void run() { + + final boolean consistentWithView = (sensorToDevice + sensorToDisplay + 180) % 180 == 0; + byte[] rotatedData = RotationHelper.rotate(data, preWidth, preHeight, sensorToDevice); + YuvImage yuv = new YuvImage(rotatedData, format, postWidth, postHeight, null); + mCameraCallbacks.processSnapshot(yuv, consistentWithView, exifFlip); + mIsCapturingImage = false; + } + }); + } + }); + return true; + } + catch (Exception e) { + CameraException cameraException = new CapturingSnapshotFailedException("Capturing a snapshot failed.", e); + mCameraCallbacks.onError(cameraException); + return false; + } } @Override @@ -619,14 +719,20 @@ class Camera1 extends CameraController { mMediaRecorder.start(); return true; } catch (Exception e) { - LOG.e("Error while starting MediaRecorder. Swallowing.", e); + CameraException cameraException = + new CapturingVideoFailedException("Error while starting MediaRecorder. " + + "Swallowing.", e); + mCameraCallbacks.onError(cameraException); mVideoFile = null; mCamera.lock(); endVideo(); return false; } } else { - throw new IllegalStateException("Can't record video while session type is picture"); + CameraException cameraException = + new CapturingVideoFailedException("Can't record video while session type is picture"); + mCameraCallbacks.onError(cameraException); + return false; } } @@ -736,32 +842,50 @@ class Camera1 extends CameraController { @Override boolean setZoom(float zoom) { - if (!isCameraAvailable()) return false; - if (!mOptions.isZoomSupported()) return false; - synchronized (mLock) { - Camera.Parameters params = mCamera.getParameters(); - float max = params.getMaxZoom(); - params.setZoom((int) (zoom * max)); - mCamera.setParameters(params); + try { + if (!isCameraAvailable()) return false; + if (!mOptions.isZoomSupported()) return false; + synchronized (mLock) { + Camera.Parameters params = mCamera.getParameters(); + float max = params.getMaxZoom(); + params.setZoom((int) (zoom * max)); + mCamera.setParameters(params); + } + return true; } - return true; + catch (Exception e) { + CameraException cameraException = + new CameraConfigurationFailedException("Failed to set zoom.", e); + mCameraCallbacks.onError(cameraException); + return false; + } + } @Override boolean setExposureCorrection(float EVvalue) { - if (!isCameraAvailable()) return false; - if (!mOptions.isExposureCorrectionSupported()) return false; - float max = mOptions.getExposureCorrectionMaxValue(); - float min = mOptions.getExposureCorrectionMinValue(); - EVvalue = EVvalue < min ? min : EVvalue > max ? max : EVvalue; // cap - synchronized (mLock) { - Camera.Parameters params = mCamera.getParameters(); - int indexValue = (int) (EVvalue / params.getExposureCompensationStep()); - params.setExposureCompensation(indexValue); - mCamera.setParameters(params); + try { + if (!isCameraAvailable()) return false; + if (!mOptions.isExposureCorrectionSupported()) return false; + float max = mOptions.getExposureCorrectionMaxValue(); + float min = mOptions.getExposureCorrectionMinValue(); + EVvalue = EVvalue < min ? min : EVvalue > max ? max : EVvalue; // cap + synchronized (mLock) { + Camera.Parameters params = mCamera.getParameters(); + int indexValue = (int) (EVvalue / params.getExposureCompensationStep()); + params.setExposureCompensation(indexValue); + mCamera.setParameters(params); + } + return true; } - return true; + catch (Exception e) { + CameraException cameraException = + new CameraConfigurationFailedException("Failed to set exposure correction.", e); + mCameraCallbacks.onError(cameraException); + return false; + } + } // ----------------- @@ -772,31 +896,42 @@ class Camera1 extends CameraController { boolean startAutoFocus(@Nullable final Gesture gesture, PointF point) { if (!isCameraAvailable()) return false; if (!mOptions.isAutoFocusSupported()) return false; - final PointF p = new PointF(point.x, point.y); // copy. - List meteringAreas2 = computeMeteringAreas(p.x, p.y); - List meteringAreas1 = meteringAreas2.subList(0, 1); - synchronized (mLock) { - // At this point we are sure that camera supports auto focus... right? Look at CameraView.onTouchEvent(). - Camera.Parameters params = mCamera.getParameters(); - int maxAF = params.getMaxNumFocusAreas(); - int maxAE = params.getMaxNumMeteringAreas(); - if (maxAF > 0) params.setFocusAreas(maxAF > 1 ? meteringAreas2 : meteringAreas1); - if (maxAE > 0) params.setMeteringAreas(maxAE > 1 ? meteringAreas2 : meteringAreas1); - params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); - mCamera.setParameters(params); - mCameraCallbacks.dispatchOnFocusStart(gesture, p); - // TODO this is not guaranteed to be called... Fix. - mCamera.autoFocus(new Camera.AutoFocusCallback() { - @Override - public void onAutoFocus(boolean success, Camera camera) { - // TODO lock auto exposure and white balance for a while - mCameraCallbacks.dispatchOnFocusEnd(gesture, success, p); - mHandler.get().removeCallbacks(mPostFocusResetRunnable); - mHandler.get().postDelayed(mPostFocusResetRunnable, mPostFocusResetDelay); - } - }); + + try { + final PointF p = new PointF(point.x, point.y); // copy. + List meteringAreas2 = computeMeteringAreas(p.x, p.y); + List meteringAreas1 = meteringAreas2.subList(0, 1); + synchronized (mLock) { + // At this point we are sure that camera supports auto focus... right? Look at CameraView.onTouchEvent(). + Camera.Parameters params = mCamera.getParameters(); + int maxAF = params.getMaxNumFocusAreas(); + int maxAE = params.getMaxNumMeteringAreas(); + if (maxAF > 0) params.setFocusAreas(maxAF > 1 ? meteringAreas2 : meteringAreas1); + if (maxAE > 0) params.setMeteringAreas(maxAE > 1 ? meteringAreas2 : meteringAreas1); + params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); + mCamera.setParameters(params); + mCameraCallbacks.dispatchOnFocusStart(gesture, p); + // TODO this is not guaranteed to be called... Fix. + mCamera.autoFocus(new Camera.AutoFocusCallback() { + @Override + public void onAutoFocus(boolean success, Camera camera) { + // TODO lock auto exposure and white balance for a while + mCameraCallbacks.dispatchOnFocusEnd(gesture, success, p); + mHandler.get().removeCallbacks(mPostFocusResetRunnable); + mHandler.get().postDelayed(mPostFocusResetRunnable, mPostFocusResetDelay); + } + }); + } + return true; + } + catch (Exception e) { + // at least getParameters and setParameters may fail. + // TODO why do they fail and is it possible to prevent such errors? + CameraException cameraException = new CameraConfigurationFailedException("Failed to " + + "start auto focus.", e); + mCameraCallbacks.onError(cameraException); + return false; } - return true; } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraConfigurationFailedException.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraConfigurationFailedException.java new file mode 100644 index 00000000..ef035ed8 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraConfigurationFailedException.java @@ -0,0 +1,16 @@ +package com.otaliastudios.cameraview; + +/** + * An object of this class describes an error that occurred during the normal runtime of the camera. + * The previously started setting change failed, but the camera should be still available. + */ +public class CameraConfigurationFailedException extends CameraException { + + CameraConfigurationFailedException(String message) { + super(message); + } + + CameraConfigurationFailedException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java index be221965..b882c9d4 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java @@ -104,8 +104,9 @@ abstract class CameraController implements CameraPreview.SurfaceCallback { mCameraCallbacks.dispatchOnCameraOpened(mOptions); } catch (Exception e) { - LOG.e("Error while starting the camera engine.", e); - throw new RuntimeException(e); + CameraException cameraException = + new CameraUnavailableException("Error while starting the camera engine.", e); + mCameraCallbacks.onError(cameraException); } } }); @@ -130,8 +131,9 @@ abstract class CameraController implements CameraPreview.SurfaceCallback { mCameraCallbacks.dispatchOnCameraClosed(); } catch (Exception e) { - LOG.e("Error while stopping the camera engine.", e); - throw new RuntimeException(e); + CameraException cameraException = + new CameraUnavailableException("Error while stopping the camera engine.", e); + mCameraCallbacks.onError(cameraException); } } }); @@ -180,9 +182,9 @@ abstract class CameraController implements CameraPreview.SurfaceCallback { mCameraCallbacks.dispatchOnCameraOpened(mOptions); } catch (Exception e) { - LOG.e("Error while restarting the camera engine.", e); - throw new RuntimeException(e); - + CameraException cameraException = + new CameraUnavailableException("Error while restarting the camera engine.", e); + mCameraCallbacks.onError(cameraException); } } }); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraException.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraException.java new file mode 100644 index 00000000..046e2dd8 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraException.java @@ -0,0 +1,15 @@ +package com.otaliastudios.cameraview; + +/** + * An object of this class describes an error that occurred during the normal runtime of the camera. + */ +public abstract class CameraException extends RuntimeException { + + CameraException(String message) { + super(message); + } + + CameraException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java index 1da71188..295f6165 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java @@ -130,4 +130,17 @@ public abstract class CameraListener { } + /** + * Notifies that an error occurred in any of the previously called methods. + * + * The default implementation will just throw the original exception again to prevent missing + * error handling. Override this method without calling the super method in order to implement + * custom error handling. + * + * @param exception the caught exception + */ + @UiThread + public void onError(CameraException exception) { + throw exception; + } } \ No newline at end of file diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraUnavailableException.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraUnavailableException.java new file mode 100644 index 00000000..e8637746 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraUnavailableException.java @@ -0,0 +1,17 @@ +package com.otaliastudios.cameraview; + +/** + * An object of this class describes an error that occurred during the normal runtime of the camera. + * It prevents the camera from being used. The cause may be temporary or permanent. You should + * restart the camera or deactivate any user interaction with the camera. + */ +public class CameraUnavailableException extends CameraException { + + CameraUnavailableException(String message) { + super(message); + } + + CameraUnavailableException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index 82772168..6a7a7cfb 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -559,9 +559,10 @@ public class CameraView extends FrameLayout { return; } } - LOG.e("Permission error:", "When the session type is set to video,", - "the RECORD_AUDIO permission should be added to the app manifest file."); - throw new IllegalStateException(CameraLogger.lastMessage); + CameraException cameraException = new CameraUnavailableException("Permission " + + "error: When the session type is set to video, the RECORD_AUDIO " + + "permission should be added to the app manifest file."); + mCameraCallbacks.onError(cameraException); } catch (PackageManager.NameNotFoundException e) { // Not possible. } @@ -1004,7 +1005,11 @@ public class CameraView extends FrameLayout { */ public void setJpegQuality(int jpegQuality) { if (jpegQuality <= 0 || jpegQuality > 100) { - throw new IllegalArgumentException("JPEG quality should be > 0 and <= 100"); + IllegalArgumentException illegalArgumentException = new + IllegalArgumentException("JPEG quality should be > 0 and <= 100"); + CameraException cameraException = new CameraConfigurationFailedException("Could not" + + " set JpegQuality", illegalArgumentException); + mCameraCallbacks.onError(cameraException); } mJpegQuality = jpegQuality; } @@ -1167,15 +1172,16 @@ public class CameraView extends FrameLayout { * so callers should ensure they have appropriate permissions to write to the file. * Recording will be automatically stopped after durationMillis, unless * {@link #stopCapturingVideo()} is not called meanwhile. + * Triggers error handler, if durationMillis is less than 500 milliseconds. * * @param file a file where the video will be saved * @param durationMillis video max duration - * - * @throws IllegalArgumentException if durationMillis is less than 500 milliseconds */ public void startCapturingVideo(File file, long durationMillis) { if (durationMillis < 500) { - throw new IllegalArgumentException("Video duration can't be < 500 milliseconds"); + CameraException cameraException = new CapturingVideoFailedException("Video duration" + + " can't be < 500 milliseconds"); + mCameraCallbacks.onError(cameraException); } startCapturingVideo(file); mUiHandler.postDelayed(new Runnable() { @@ -1310,6 +1316,7 @@ public class CameraView extends FrameLayout { void dispatchOnFocusEnd(@Nullable Gesture trigger, boolean success, PointF where); void dispatchOnZoomChanged(final float newValue, final PointF[] fingers); void dispatchOnExposureCorrectionChanged(float newValue, float[] bounds, PointF[] fingers); + void onError(CameraException exception); } private class Callbacks implements CameraCallbacks { @@ -1557,6 +1564,47 @@ public class CameraView extends FrameLayout { } }); } + + /** + * Log and redirect the given error information to the CameraListeners. + * + * @param exception the error cause + */ + @Override + public void onError(final CameraException exception) { + // log + LOG.e(exception.getMessage(), exception.getCause()); + + // redirect + mUiHandler.post(new Runnable() { + @Override + public void run() { + + // all error listeners will be called, but at most one of them should actually + // throw an exception + int count = 0; + for (CameraListener listener : mListeners) { + try { + listener.onError(exception); + } catch (CameraException ce) { + // if a custom error handler caused a new exception, we throw the new + // one instead of the original one + if (ce == exception) { + count++; + } + else { + throw ce; + } + } + } + + // the original exception is only thrown, if all existing listeners threw it + if (count == mListeners.size()) { + throw exception; + } + } + }); + } } //endregion diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CapturingFailedException.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CapturingFailedException.java new file mode 100644 index 00000000..f5e8044c --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CapturingFailedException.java @@ -0,0 +1,15 @@ +package com.otaliastudios.cameraview; + +/** + * An object of this class describes an error that occurred during the normal runtime of the camera. + * The previously started capturing failed, but the camera should be still available. + */ +public abstract class CapturingFailedException extends CameraException { + CapturingFailedException(String message) { + super(message); + } + + CapturingFailedException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CapturingImageFailedException.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CapturingImageFailedException.java new file mode 100644 index 00000000..58ee4b74 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CapturingImageFailedException.java @@ -0,0 +1,17 @@ +package com.otaliastudios.cameraview; + +/** + * An object of this class describes an error that occurred during the normal runtime of the camera. + * The previously started image capturing failed (snapshot or "real picture"), but the camera should + * be still available. + */ +public abstract class CapturingImageFailedException extends CapturingFailedException { + + CapturingImageFailedException(String message) { + super(message); + } + + CapturingImageFailedException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CapturingPictureFailedException.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CapturingPictureFailedException.java new file mode 100644 index 00000000..ea6aa6fa --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CapturingPictureFailedException.java @@ -0,0 +1,17 @@ +package com.otaliastudios.cameraview; + +/** + * An object of this class describes an error that occurred during the normal runtime of the camera. + * The previously started picture capturing failed, but the camera should be still available. + * This exception does not handle failed snapshots. + */ +public class CapturingPictureFailedException extends CapturingImageFailedException { + + CapturingPictureFailedException(String message) { + super(message); + } + + CapturingPictureFailedException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CapturingSnapshotFailedException.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CapturingSnapshotFailedException.java new file mode 100644 index 00000000..f19de20c --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CapturingSnapshotFailedException.java @@ -0,0 +1,17 @@ +package com.otaliastudios.cameraview; + +/** + * An object of this class describes an error that occurred during the normal runtime of the camera. + * The previously started snapshot capturing failed, but the camera should be still available. + * This exception does not handle failed "real picture" capturing. + */ +public class CapturingSnapshotFailedException extends CapturingImageFailedException { + + CapturingSnapshotFailedException(String message) { + super(message); + } + + CapturingSnapshotFailedException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CapturingVideoFailedException.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CapturingVideoFailedException.java new file mode 100644 index 00000000..d9707b8d --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CapturingVideoFailedException.java @@ -0,0 +1,16 @@ +package com.otaliastudios.cameraview; + +/** + * An object of this class describes an error that occurred during the normal runtime of the camera. + * The previously started video capturing failed, but the camera should be still available. + */ +public class CapturingVideoFailedException extends CapturingFailedException { + + CapturingVideoFailedException(String message) { + super(message); + } + + CapturingVideoFailedException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file