Add basic error handling

pull/97/head
Mattia Iavarone 8 years ago
parent d6d3c1a437
commit 519a39bee7
  1. 13
      README.md
  2. 11
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java
  3. 127
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java
  4. 4
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java
  5. 150
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
  6. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java
  7. 179
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
  8. 12
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraException.java
  9. 20
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java
  10. 20
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  11. 16
      cameraview/src/test/java/com/otaliastudios/cameraview/CameraExceptionTest.java

@ -44,6 +44,7 @@ See below for a [list of what was done](#roadmap) and [licensing info](#contribu
- Automatically detected orientation tags - Automatically detected orientation tags
- Plug in location tags with `setLocation()` API - Plug in location tags with `setLocation()` API
- `CameraUtils` to help with Bitmaps and orientations - `CameraUtils` to help with Bitmaps and orientations
- Error handling
- **Lightweight**, no dependencies, just support `ExifInterface` - **Lightweight**, no dependencies, just support `ExifInterface`
- Works down to API level 15 - Works down to API level 15
@ -169,6 +170,16 @@ camera.addCameraListener(new CameraListener() {
@Override @Override
public void onCameraClosed() {} public void onCameraClosed() {}
/**
* Notifies about an error during the camera setup or configuration.
* At the moment, errors that are passed here are unrecoverable. When this is called,
* the camera has been released and is presumably showing a black preview.
*
* This is the right moment to show an error dialog to the user.
*/
@Override
public void onCameraError(CameraException error) {}
/** /**
* Notifies that a picture previously captured with capturePicture() * Notifies that a picture previously captured with capturePicture()
* or captureSnapshot() is ready to be shown or saved. * or captureSnapshot() is ready to be shown or saved.
@ -605,6 +616,7 @@ all the code was changed.
- *Better threading, start() in worker thread and callbacks in UI* - *Better threading, start() in worker thread and callbacks in UI*
- *Frame processor support* - *Frame processor support*
- *inject external loggers* - *inject external loggers*
- *error handling*
These are still things that need to be done, off the top of my head: These are still things that need to be done, off the top of my head:
@ -612,7 +624,6 @@ 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 - [ ] 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 - [ ] animate grid lines similar to stock camera app
- [ ] add onRequestPermissionResults for easy permission callback - [ ] 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
- [ ] decent code coverage - [ ] decent code coverage
# Contributing and licenses # Contributing and licenses

@ -34,7 +34,7 @@ import static org.mockito.Mockito.verify;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@MediumTest @MediumTest
public class CameraCallbacksTest extends BaseTest { public class CameraViewCallbacksTest extends BaseTest {
private CameraView camera; private CameraView camera;
private CameraListener listener; private CameraListener listener;
@ -226,6 +226,15 @@ public class CameraCallbacksTest extends BaseTest {
// TODO: test onShutter, here or elsewhere // TODO: test onShutter, here or elsewhere
@Test
public void testCameraError() {
CameraException error = new CameraException(new RuntimeException("Error"));
completeTask().when(listener).onCameraError(error);
camera.mCameraCallbacks.dispatchError(error);
assertNotNull(task.await(200));
verify(listener, times(1)).onCameraError(error);
}
@Test @Test
public void testProcessJpeg() { public void testProcessJpeg() {

@ -1,13 +1,12 @@
package com.otaliastudios.cameraview; package com.otaliastudios.cameraview;
import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.PointF; import android.graphics.PointF;
import android.media.MediaRecorder;
import android.support.test.filters.MediumTest; import android.support.test.filters.MediumTest;
import android.support.test.rule.ActivityTestRule; import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import android.view.ViewGroup;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
@ -42,6 +41,7 @@ public class IntegrationTest extends BaseTest {
private CameraView camera; private CameraView camera;
private Camera1 controller; private Camera1 controller;
private CameraListener listener; private CameraListener listener;
private Task<Throwable> uiExceptionTask;
@BeforeClass @BeforeClass
public static void grant() { public static void grant() {
@ -51,6 +51,7 @@ public class IntegrationTest extends BaseTest {
@Before @Before
public void setUp() { public void setUp() {
WorkerHandler.destroy(); WorkerHandler.destroy();
ui(new Runnable() { ui(new Runnable() {
@Override @Override
public void run() { public void run() {
@ -67,6 +68,17 @@ public class IntegrationTest extends BaseTest {
rule.getActivity().inflate(camera); rule.getActivity().inflate(camera);
} }
}); });
// Ensure that controller exceptions are thrown on this thread (not on the UI thread).
uiExceptionTask = new Task<>(true);
WorkerHandler crashThread = WorkerHandler.get("CrashThread");
crashThread.getThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
uiExceptionTask.end(e);
}
});
controller.mCrashHandler = crashThread.get();
} }
@After @After
@ -76,7 +88,13 @@ public class IntegrationTest extends BaseTest {
WorkerHandler.destroy(); WorkerHandler.destroy();
} }
private void waitForUiException() throws Throwable {
Throwable throwable = uiExceptionTask.await(2500);
if (throwable != null) throw throwable;
}
private CameraOptions waitForOpen(boolean expectSuccess) { private CameraOptions waitForOpen(boolean expectSuccess) {
camera.start();
final Task<CameraOptions> open = new Task<>(true); final Task<CameraOptions> open = new Task<>(true);
doEndTask(open, 0).when(listener).onCameraOpened(any(CameraOptions.class)); doEndTask(open, 0).when(listener).onCameraOpened(any(CameraOptions.class));
CameraOptions result = open.await(4000); CameraOptions result = open.await(4000);
@ -89,6 +107,7 @@ public class IntegrationTest extends BaseTest {
} }
private void waitForClose(boolean expectSuccess) { private void waitForClose(boolean expectSuccess) {
camera.stop();
final Task<Boolean> close = new Task<>(true); final Task<Boolean> close = new Task<>(true);
doEndTask(close, true).when(listener).onCameraClosed(); doEndTask(close, true).when(listener).onCameraClosed();
Boolean result = close.await(4000); Boolean result = close.await(4000);
@ -99,10 +118,10 @@ public class IntegrationTest extends BaseTest {
} }
} }
private void waitForVideo(boolean expectSuccess) { private void waitForVideoEnd(boolean expectSuccess) {
final Task<Boolean> video = new Task<>(true); final Task<Boolean> video = new Task<>(true);
doEndTask(video, true).when(listener).onVideoTaken(any(File.class)); doEndTask(video, true).when(listener).onVideoTaken(any(File.class));
Boolean result = video.await(2000); Boolean result = video.await(8000);
if (expectSuccess) { if (expectSuccess) {
assertNotNull("Can take video", result); assertNotNull("Can take video", result);
} else { } else {
@ -122,6 +141,18 @@ public class IntegrationTest extends BaseTest {
return result; return result;
} }
private void waitForVideoStart() {
controller.mStartVideoTask.listen();
camera.startCapturingVideo(null);
controller.mStartVideoTask.await(400);
}
private void waitForVideoQuality(VideoQuality quality) {
controller.mVideoQualityTask.listen();
camera.setVideoQuality(quality);
controller.mVideoQualityTask.await(400);
}
//region test open/close //region test open/close
@Test @Test
@ -129,26 +160,21 @@ public class IntegrationTest extends BaseTest {
// Starting and stopping are hard to get since they happen on another thread. // Starting and stopping are hard to get since they happen on another thread.
assertEquals(controller.getState(), CameraController.STATE_STOPPED); assertEquals(controller.getState(), CameraController.STATE_STOPPED);
camera.start();
waitForOpen(true); waitForOpen(true);
assertEquals(controller.getState(), CameraController.STATE_STARTED); assertEquals(controller.getState(), CameraController.STATE_STARTED);
camera.stop();
waitForClose(true); waitForClose(true);
assertEquals(controller.getState(), CameraController.STATE_STOPPED); assertEquals(controller.getState(), CameraController.STATE_STOPPED);
} }
@Test @Test
public void testOpenTwice() { public void testOpenTwice() {
camera.start();
waitForOpen(true); waitForOpen(true);
camera.start();
waitForOpen(false); waitForOpen(false);
} }
@Test @Test
public void testCloseTwice() { public void testCloseTwice() {
camera.stop();
waitForClose(false); waitForClose(false);
} }
@ -174,7 +200,6 @@ public class IntegrationTest extends BaseTest {
public void testStartInitializesOptions() { public void testStartInitializesOptions() {
assertNull(camera.getCameraOptions()); assertNull(camera.getCameraOptions());
assertNull(camera.getExtraProperties()); assertNull(camera.getExtraProperties());
camera.start();
waitForOpen(true); waitForOpen(true);
assertNotNull(camera.getCameraOptions()); assertNotNull(camera.getCameraOptions());
assertNotNull(camera.getExtraProperties()); assertNotNull(camera.getExtraProperties());
@ -187,7 +212,6 @@ public class IntegrationTest extends BaseTest {
@Test @Test
public void testSetFacing() throws Exception { public void testSetFacing() throws Exception {
camera.start();
CameraOptions o = waitForOpen(true); CameraOptions o = waitForOpen(true);
int size = o.getSupportedFacing().size(); int size = o.getSupportedFacing().size();
if (size > 1) { if (size > 1) {
@ -206,7 +230,6 @@ public class IntegrationTest extends BaseTest {
@Test @Test
public void testSetSessionType() throws Exception { public void testSetSessionType() throws Exception {
camera.setSessionType(SessionType.PICTURE); camera.setSessionType(SessionType.PICTURE);
camera.start();
waitForOpen(true); waitForOpen(true);
// set session type should call stop and start again. // set session type should call stop and start again.
@ -228,7 +251,6 @@ public class IntegrationTest extends BaseTest {
@Test @Test
public void testSetZoom() { public void testSetZoom() {
camera.start();
CameraOptions options = waitForOpen(true); CameraOptions options = waitForOpen(true);
controller.mZoomTask.listen(); controller.mZoomTask.listen();
@ -246,7 +268,6 @@ public class IntegrationTest extends BaseTest {
@Test @Test
public void testSetExposureCorrection() { public void testSetExposureCorrection() {
camera.start();
CameraOptions options = waitForOpen(true); CameraOptions options = waitForOpen(true);
controller.mExposureCorrectionTask.listen(); controller.mExposureCorrectionTask.listen();
@ -264,7 +285,6 @@ public class IntegrationTest extends BaseTest {
@Test @Test
public void testSetFlash() { public void testSetFlash() {
camera.start();
CameraOptions options = waitForOpen(true); CameraOptions options = waitForOpen(true);
Flash[] values = Flash.values(); Flash[] values = Flash.values();
Flash oldValue = camera.getFlash(); Flash oldValue = camera.getFlash();
@ -283,7 +303,6 @@ public class IntegrationTest extends BaseTest {
@Test @Test
public void testSetWhiteBalance() { public void testSetWhiteBalance() {
camera.start();
CameraOptions options = waitForOpen(true); CameraOptions options = waitForOpen(true);
WhiteBalance[] values = WhiteBalance.values(); WhiteBalance[] values = WhiteBalance.values();
WhiteBalance oldValue = camera.getWhiteBalance(); WhiteBalance oldValue = camera.getWhiteBalance();
@ -302,7 +321,6 @@ public class IntegrationTest extends BaseTest {
@Test @Test
public void testSetHdr() { public void testSetHdr() {
camera.start();
CameraOptions options = waitForOpen(true); CameraOptions options = waitForOpen(true);
Hdr[] values = Hdr.values(); Hdr[] values = Hdr.values();
Hdr oldValue = camera.getHdr(); Hdr oldValue = camera.getHdr();
@ -322,7 +340,6 @@ public class IntegrationTest extends BaseTest {
@Test @Test
public void testSetAudio() { public void testSetAudio() {
// TODO: when permissions are managed, check that Audio.ON triggers the audio permission // TODO: when permissions are managed, check that Audio.ON triggers the audio permission
camera.start();
waitForOpen(true); waitForOpen(true);
Audio[] values = Audio.values(); Audio[] values = Audio.values();
for (Audio value : values) { for (Audio value : values) {
@ -333,7 +350,6 @@ public class IntegrationTest extends BaseTest {
@Test @Test
public void testSetLocation() { public void testSetLocation() {
camera.start();
waitForOpen(true); waitForOpen(true);
controller.mLocationTask.listen(); controller.mLocationTask.listen();
camera.setLocation(10d, 2d); camera.setLocation(10d, 2d);
@ -349,42 +365,33 @@ public class IntegrationTest extends BaseTest {
//region testSetVideoQuality //region testSetVideoQuality
// This can be tricky because can trigger layout changes. // This can be tricky because can trigger layout changes.
// TODO: the exception is swallowed. @Test(expected = IllegalStateException.class) @Test(expected = RuntimeException.class)
public void testSetVideoQuality_whileRecording() { public void testSetVideoQuality_whileRecording() throws Throwable {
// Can't run on Travis, MediaRecorder not supported. // Can't run on Travis, MediaRecorder not supported.
// Error while starting MediaRecorder. java.lang.RuntimeException: start failed. // Error while starting MediaRecorder. java.lang.RuntimeException: start failed.
camera.setSessionType(SessionType.VIDEO); camera.setSessionType(SessionType.VIDEO);
camera.setVideoQuality(VideoQuality.HIGHEST); waitForVideoQuality(VideoQuality.HIGHEST);
camera.start();
waitForOpen(true); waitForOpen(true);
camera.startCapturingVideo(null); waitForVideoStart();
controller.mVideoQualityTask.listen(); waitForVideoQuality(VideoQuality.LOWEST);
camera.setVideoQuality(VideoQuality.LOWEST); waitForUiException();
controller.mVideoQualityTask.await(300);
} }
@Test @Test
public void testSetVideoQuality_whileInPictureSessionType() { public void testSetVideoQuality_whileInPictureSessionType() {
camera.setSessionType(SessionType.PICTURE); camera.setSessionType(SessionType.PICTURE);
camera.setVideoQuality(VideoQuality.HIGHEST); waitForVideoQuality(VideoQuality.HIGHEST);
camera.start();
waitForOpen(true); waitForOpen(true);
controller.mVideoQualityTask.listen(); waitForVideoQuality(VideoQuality.LOWEST);
camera.setVideoQuality(VideoQuality.LOWEST);
controller.mVideoQualityTask.await(300);
assertEquals(camera.getVideoQuality(), VideoQuality.LOWEST); assertEquals(camera.getVideoQuality(), VideoQuality.LOWEST);
} }
@Test @Test
public void testSetVideoQuality_whileNotStarted() { public void testSetVideoQuality_whileNotStarted() {
controller.mVideoQualityTask.listen(); waitForVideoQuality(VideoQuality.HIGHEST);
camera.setVideoQuality(VideoQuality.HIGHEST);
controller.mVideoQualityTask.await(300);
assertEquals(camera.getVideoQuality(), VideoQuality.HIGHEST); assertEquals(camera.getVideoQuality(), VideoQuality.HIGHEST);
controller.mVideoQualityTask.listen(); waitForVideoQuality(VideoQuality.LOWEST);
camera.setVideoQuality(VideoQuality.LOWEST);
controller.mVideoQualityTask.await(300);
assertEquals(camera.getVideoQuality(), VideoQuality.LOWEST); assertEquals(camera.getVideoQuality(), VideoQuality.LOWEST);
} }
@ -401,36 +408,34 @@ public class IntegrationTest extends BaseTest {
//region test startVideo //region test startVideo
// TODO: @Test(expected = IllegalStateException.class) @Test(expected = RuntimeException.class)
// Fails on Travis. Some emulators can't deal with MediaRecorder public void testStartVideo_whileInPictureMode() throws Throwable {
// Error while starting MediaRecorder. java.lang.RuntimeException: start failed. // Fails on Travis. Some emulators can't deal with MediaRecorder
// as documented. This works locally though. // Error while starting MediaRecorder. java.lang.RuntimeException: start failed.
public void testStartVideo_whileInPictureMode() { // as documented. This works locally though.
camera.setSessionType(SessionType.PICTURE); camera.setSessionType(SessionType.PICTURE);
camera.start();
waitForOpen(true); waitForOpen(true);
camera.startCapturingVideo(null); waitForVideoStart();
waitForUiException();
} }
// TODO: @Test @Test
// Fails on Travis. Some emulators can't deal with MediaRecorder,
// Error while starting MediaRecorder. java.lang.RuntimeException: start failed.
// as documented. This works locally though.
public void testStartEndVideo() { public void testStartEndVideo() {
// Fails on Travis. Some emulators can't deal with MediaRecorder,
// Error while starting MediaRecorder. java.lang.RuntimeException: start failed.
// as documented. This works locally though.
camera.setSessionType(SessionType.VIDEO); camera.setSessionType(SessionType.VIDEO);
camera.start();
waitForOpen(true); waitForOpen(true);
camera.startCapturingVideo(null, 1000); camera.startCapturingVideo(null, 4000);
waitForVideo(true); // waits 2000 waitForVideoEnd(true);
} }
@Test @Test
public void testEndVideo_withoutStarting() { public void testEndVideo_withoutStarting() {
camera.setSessionType(SessionType.VIDEO); camera.setSessionType(SessionType.VIDEO);
camera.start();
waitForOpen(true); waitForOpen(true);
camera.stopCapturingVideo(); camera.stopCapturingVideo();
waitForVideo(false); waitForVideoEnd(false);
} }
//endregion //endregion
@ -440,7 +445,6 @@ public class IntegrationTest extends BaseTest {
@Test @Test
public void testStartAutoFocus() { public void testStartAutoFocus() {
camera.start();
CameraOptions o = waitForOpen(true); CameraOptions o = waitForOpen(true);
final Task<PointF> focus = new Task<>(true); final Task<PointF> focus = new Task<>(true);
@ -469,7 +473,6 @@ public class IntegrationTest extends BaseTest {
@Test @Test
public void testCapturePicture_concurrentCalls() throws Exception { public void testCapturePicture_concurrentCalls() throws Exception {
// Second take should fail. // Second take should fail.
camera.start();
waitForOpen(true); waitForOpen(true);
CountDownLatch latch = new CountDownLatch(2); CountDownLatch latch = new CountDownLatch(2);
@ -485,7 +488,6 @@ public class IntegrationTest extends BaseTest {
@Test @Test
public void testCapturePicture_size() throws Exception { public void testCapturePicture_size() throws Exception {
camera.setCropOutput(false); camera.setCropOutput(false);
camera.start();
waitForOpen(true); waitForOpen(true);
Size size = camera.getCaptureSize(); Size size = camera.getCaptureSize();
@ -507,7 +509,6 @@ public class IntegrationTest extends BaseTest {
@Test @Test
public void testCaptureSnapshot_concurrentCalls() throws Exception { public void testCaptureSnapshot_concurrentCalls() throws Exception {
// Second take should fail. // Second take should fail.
camera.start();
waitForOpen(true); waitForOpen(true);
CountDownLatch latch = new CountDownLatch(2); CountDownLatch latch = new CountDownLatch(2);
@ -515,15 +516,14 @@ public class IntegrationTest extends BaseTest {
camera.captureSnapshot(); camera.captureSnapshot();
camera.captureSnapshot(); camera.captureSnapshot();
boolean did = latch.await(4, TimeUnit.SECONDS); boolean did = latch.await(6, TimeUnit.SECONDS);
assertFalse(did); assertFalse(did);
assertEquals(latch.getCount(), 1); assertEquals(1, latch.getCount());
} }
@Test @Test
public void testCaptureSnapshot_size() throws Exception { public void testCaptureSnapshot_size() throws Exception {
camera.setCropOutput(false); camera.setCropOutput(false);
camera.start();
waitForOpen(true); waitForOpen(true);
Size size = camera.getPreviewSize(); Size size = camera.getPreviewSize();
@ -552,7 +552,6 @@ public class IntegrationTest extends BaseTest {
public void testFrameProcessing_simple() throws Exception { public void testFrameProcessing_simple() throws Exception {
FrameProcessor processor = mock(FrameProcessor.class); FrameProcessor processor = mock(FrameProcessor.class);
camera.addFrameProcessor(processor); camera.addFrameProcessor(processor);
camera.start();
waitForOpen(true); waitForOpen(true);
assert30Frames(processor); assert30Frames(processor);
@ -562,7 +561,6 @@ public class IntegrationTest extends BaseTest {
public void testFrameProcessing_afterSnapshot() throws Exception { public void testFrameProcessing_afterSnapshot() throws Exception {
FrameProcessor processor = mock(FrameProcessor.class); FrameProcessor processor = mock(FrameProcessor.class);
camera.addFrameProcessor(processor); camera.addFrameProcessor(processor);
camera.start();
waitForOpen(true); waitForOpen(true);
// In Camera1, snapshots will clear the preview callback // In Camera1, snapshots will clear the preview callback
@ -577,11 +575,8 @@ public class IntegrationTest extends BaseTest {
public void testFrameProcessing_afterRestart() throws Exception { public void testFrameProcessing_afterRestart() throws Exception {
FrameProcessor processor = mock(FrameProcessor.class); FrameProcessor processor = mock(FrameProcessor.class);
camera.addFrameProcessor(processor); camera.addFrameProcessor(processor);
camera.start();
waitForOpen(true); waitForOpen(true);
camera.stop();
waitForClose(true); waitForClose(true);
camera.start();
waitForOpen(true); waitForOpen(true);
assert30Frames(processor); assert30Frames(processor);

@ -32,11 +32,11 @@ public class MockCameraController extends CameraController {
} }
@Override @Override
void onStart() throws Exception { void onStart() {
} }
@Override @Override
void onStop() throws Exception { void onStop() {
} }
@Override @Override

@ -16,13 +16,14 @@ import android.support.annotation.WorkerThread;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
class Camera1 extends CameraController implements Camera.PreviewCallback { class Camera1 extends CameraController implements Camera.PreviewCallback, Camera.ErrorCallback {
private static final String TAG = Camera1.class.getSimpleName(); private static final String TAG = Camera1.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG); private static final CameraLogger LOG = CameraLogger.create(TAG);
@ -48,7 +49,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
} }
}; };
private ScheduleRunnable mScheduleRunnable;
private Mapper mMapper; private Mapper mMapper;
private boolean mIsBound = false; private boolean mIsBound = false;
private boolean mIsCapturingImage = false; private boolean mIsCapturingImage = false;
@ -56,31 +56,21 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
Camera1(CameraView.CameraCallbacks callback) { Camera1(CameraView.CameraCallbacks callback) {
super(callback); super(callback);
mScheduleRunnable = new ScheduleRunnable();
mMapper = new Mapper.Mapper1(); mMapper = new Mapper.Mapper1();
} }
private class ScheduleRunnable implements Runnable { private void schedule(@Nullable final Task<Void> task, final boolean ensureAvailable, final Runnable action) {
private Task<Void> mTask; mHandler.post(new Runnable() {
private Runnable mAction; @Override
private boolean mEnsureAvailable; public void run() {
if (ensureAvailable && !isCameraAvailable()) {
@Override if (task != null) task.end(null);
public void run() { } else {
if (mEnsureAvailable && !isCameraAvailable()) { action.run();
if (mTask != null) mTask.end(null); if (task != null) task.end(null);
} else { }
mAction.run();
if (mTask != null) mTask.end(null);
} }
} });
}
private void schedule(@Nullable Task<Void> task, boolean ensureAvailable, Runnable action) {
mScheduleRunnable.mEnsureAvailable = ensureAvailable;
mScheduleRunnable.mAction = action;
mScheduleRunnable.mTask = task;
mHandler.post(mScheduleRunnable);
} }
// Preview surface is now available. If camera is open, set up. // Preview surface is now available. If camera is open, set up.
@ -96,7 +86,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
bindToSurface(); bindToSurface();
} catch (Exception e) { } catch (Exception e) {
LOG.e("onSurfaceAvailable:", "Exception while binding camera to preview.", e); LOG.e("onSurfaceAvailable:", "Exception while binding camera to preview.", e);
throw new RuntimeException(e); throw new CameraException(e);
} }
} }
} }
@ -133,13 +123,17 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
// The act of binding an "open" camera to a "ready" preview. // The act of binding an "open" camera to a "ready" preview.
// These can happen at different times but we want to end up here. // These can happen at different times but we want to end up here.
@WorkerThread @WorkerThread
private void bindToSurface() throws Exception { private void bindToSurface() {
LOG.i("bindToSurface:", "Started"); LOG.i("bindToSurface:", "Started");
Object output = mPreview.getOutput(); Object output = mPreview.getOutput();
if (mPreview.getOutputClass() == SurfaceHolder.class) { try {
mCamera.setPreviewDisplay((SurfaceHolder) output); if (mPreview.getOutputClass() == SurfaceHolder.class) {
} else { mCamera.setPreviewDisplay((SurfaceHolder) output);
mCamera.setPreviewTexture((SurfaceTexture) output); } else {
mCamera.setPreviewTexture((SurfaceTexture) output);
}
} catch (IOException e) {
throw new CameraException(e);
} }
mCaptureSize = computeCaptureSize(); mCaptureSize = computeCaptureSize();
@ -176,13 +170,14 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
@WorkerThread @WorkerThread
@Override @Override
void onStart() throws Exception { void onStart() {
if (isCameraAvailable()) { if (isCameraAvailable()) {
LOG.w("onStart:", "Camera not available. Should not happen."); LOG.w("onStart:", "Camera not available. Should not happen.");
onStop(); // Should not happen. onStop(); // Should not happen.
} }
if (collectCameraId()) { if (collectCameraId()) {
mCamera = Camera.open(mCameraId); mCamera = Camera.open(mCameraId);
mCamera.setErrorCallback(this);
// Set parameters that might have been set before the camera was opened. // Set parameters that might have been set before the camera was opened.
LOG.i("onStart:", "Applying default parameters."); LOG.i("onStart:", "Applying default parameters.");
@ -206,15 +201,15 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
@WorkerThread @WorkerThread
@Override @Override
void onStop() throws Exception { void onStop() {
Exception error = null; Exception error = null;
LOG.i("onStop:", "About to clean up."); LOG.i("onStop:", "About to clean up.");
mHandler.get().removeCallbacks(mPostFocusResetRunnable); mHandler.get().removeCallbacks(mPostFocusResetRunnable);
mFrameManager.release(); mFrameManager.release();
if (mCamera != null) { if (mCamera != null) {
LOG.i("onStop:", "Clean up.", "Ending video?", mIsCapturingVideo); LOG.i("onStop:", "Clean up.", "Ending video.");
if (mIsCapturingVideo) endVideo(); endVideoImmediately();
try { try {
LOG.i("onStop:", "Clean up.", "Stopping preview."); LOG.i("onStop:", "Clean up.", "Stopping preview.");
@ -222,7 +217,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
mCamera.setPreviewCallbackWithBuffer(null); mCamera.setPreviewCallbackWithBuffer(null);
LOG.i("onStop:", "Clean up.", "Stopped preview."); LOG.i("onStop:", "Clean up.", "Stopped preview.");
} catch (Exception e) { } catch (Exception e) {
LOG.w("onStop:", "Clean up.", "Exception while stopping preview."); LOG.w("onStop:", "Clean up.", "Exception while stopping preview.", e);
error = e; error = e;
} }
@ -231,7 +226,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
mCamera.release(); mCamera.release();
LOG.i("onStop:", "Clean up.", "Released camera."); LOG.i("onStop:", "Clean up.", "Released camera.");
} catch (Exception e) { } catch (Exception e) {
LOG.w("onStop:", "Clean up.", "Exception while releasing camera."); LOG.w("onStop:", "Clean up.", "Exception while releasing camera.", e);
error = e; error = e;
} }
} }
@ -242,7 +237,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
mCaptureSize = null; mCaptureSize = null;
mIsBound = false; mIsBound = false;
if (error != null) throw error; if (error != null) throw new CameraException(error);
} }
private boolean collectCameraId() { private boolean collectCameraId() {
@ -267,6 +262,20 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
} }
} }
@Override
public void onError(int error, Camera camera) {
if (error == Camera.CAMERA_ERROR_SERVER_DIED) {
// Looks like this is recoverable.
LOG.w("Recoverable error inside the onError callback.", "CAMERA_ERROR_SERVER_DIED");
stopImmediately();
start();
return;
}
LOG.e("Error inside the onError callback.", error);
throw new CameraException(new RuntimeException(CameraLogger.lastMessage));
}
@Override @Override
void setSessionType(SessionType sessionType) { void setSessionType(SessionType sessionType) {
if (sessionType != mSessionType) { if (sessionType != mSessionType) {
@ -512,9 +521,11 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
@Override @Override
void captureSnapshot() { void captureSnapshot() {
LOG.i("captureSnapshot: scheduling");
schedule(null, true, new Runnable() { schedule(null, true, new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("captureSnapshot: performing.", mIsCapturingImage);
if (mIsCapturingImage) return; if (mIsCapturingImage) return;
// This won't work while capturing a video. // This won't work while capturing a video.
// Switch to capturePicture. // Switch to capturePicture.
@ -523,9 +534,11 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
return; return;
} }
mIsCapturingImage = true; mIsCapturingImage = true;
LOG.i("captureSnapshot: add preview callback.");
mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() { mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() {
@Override @Override
public void onPreviewFrame(final byte[] data, Camera camera) { public void onPreviewFrame(final byte[] data, Camera camera) {
LOG.i("captureSnapshot: onShutter.");
mCameraCallbacks.onShutter(true); mCameraCallbacks.onShutter(true);
// Got to rotate the preview frame, since byte[] data here does not include // Got to rotate the preview frame, since byte[] data here does not include
@ -540,13 +553,17 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
final int postWidth = flip ? preHeight : preWidth; final int postWidth = flip ? preHeight : preWidth;
final int postHeight = flip ? preWidth : preHeight; final int postHeight = flip ? preWidth : preHeight;
final int format = mPreviewFormat; final int format = mPreviewFormat;
LOG.i("captureSnapshot: to worker handler.");
WorkerHandler.run(new Runnable() { WorkerHandler.run(new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("captureSnapshot: rotating.");
final boolean consistentWithView = (sensorToDevice + sensorToDisplay + 180) % 180 == 0; final boolean consistentWithView = (sensorToDevice + sensorToDisplay + 180) % 180 == 0;
byte[] rotatedData = RotationHelper.rotate(data, preWidth, preHeight, sensorToDevice); byte[] rotatedData = RotationHelper.rotate(data, preWidth, preHeight, sensorToDevice);
LOG.i("captureSnapshot: rotated.");
YuvImage yuv = new YuvImage(rotatedData, format, postWidth, postHeight, null); YuvImage yuv = new YuvImage(rotatedData, format, postWidth, postHeight, null);
LOG.i("captureSnapshot: dispatching to listeners.");
mCameraCallbacks.processSnapshot(yuv, consistentWithView, exifFlip); mCameraCallbacks.processSnapshot(yuv, consistentWithView, exifFlip);
mIsCapturingImage = false; mIsCapturingImage = false;
} }
@ -678,7 +695,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
@Override @Override
void startVideo(@NonNull final File videoFile) { void startVideo(@NonNull final File videoFile) {
schedule(null, true, new Runnable() { schedule(mStartVideoTask, true, new Runnable() {
@Override @Override
public void run() { public void run() {
if (mIsCapturingVideo) return; if (mIsCapturingVideo) return;
@ -693,7 +710,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
LOG.e("Error while starting MediaRecorder. Swallowing.", e); LOG.e("Error while starting MediaRecorder. Swallowing.", e);
mVideoFile = null; mVideoFile = null;
mCamera.lock(); mCamera.lock();
endVideoNow(); endVideoImmediately();
} }
} else { } else {
throw new IllegalStateException("Can't record video while session type is picture"); throw new IllegalStateException("Can't record video while session type is picture");
@ -707,30 +724,28 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
schedule(null, false, new Runnable() { schedule(null, false, new Runnable() {
@Override @Override
public void run() { public void run() {
endVideoNow(); endVideoImmediately();
} }
}); });
} }
@WorkerThread @WorkerThread
private void endVideoNow() { private void endVideoImmediately() {
if (mIsCapturingVideo) { LOG.i("endVideoImmediately:", "is capturing:", mIsCapturingVideo);
mIsCapturingVideo = false; mIsCapturingVideo = false;
if (mMediaRecorder != null) { if (mMediaRecorder != null) {
try { try {
mMediaRecorder.stop(); mMediaRecorder.stop();
mMediaRecorder.release(); } catch (Exception e) {
} catch (Exception e) { // This can happen if endVideo() is called right after startVideo(). We don't care.
// This can happen if endVideo() is called right after startVideo(). LOG.w("endVideoImmediately:", "Error while closing media recorder. Swallowing", e);
// We don't care.
LOG.w("Error while closing media recorder. Swallowing", e);
}
mMediaRecorder = null;
}
if (mVideoFile != null) {
mCameraCallbacks.dispatchOnVideoTaken(mVideoFile);
mVideoFile = null;
} }
mMediaRecorder.release();
mMediaRecorder = null;
}
if (mVideoFile != null) {
mCameraCallbacks.dispatchOnVideoTaken(mVideoFile);
mVideoFile = null;
} }
} }
@ -741,18 +756,21 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
mMediaRecorder.setCamera(mCamera); mMediaRecorder.setCamera(mCamera);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
if (mAudio == Audio.ON) {
// Must be called before setOutputFormat.
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
}
CamcorderProfile profile = getCamcorderProfile(mCameraId, mVideoQuality); CamcorderProfile profile = getCamcorderProfile(mCameraId, mVideoQuality);
mMediaRecorder.setOutputFormat(profile.fileFormat);
mMediaRecorder.setVideoFrameRate(profile.videoFrameRate);
mMediaRecorder.setVideoSize(profile.videoFrameWidth, profile.videoFrameHeight);
mMediaRecorder.setVideoEncoder(profile.videoCodec);
mMediaRecorder.setVideoEncodingBitRate(profile.videoBitRate);
if (mAudio == Audio.ON) { if (mAudio == Audio.ON) {
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); mMediaRecorder.setAudioChannels(profile.audioChannels);
mMediaRecorder.setProfile(profile); mMediaRecorder.setAudioSamplingRate(profile.audioSampleRate);
} else { mMediaRecorder.setAudioEncoder(profile.audioCodec);
// Set all values contained in profile except audio settings mMediaRecorder.setAudioEncodingBitRate(profile.audioBitRate);
mMediaRecorder.setOutputFormat(profile.fileFormat);
mMediaRecorder.setVideoEncoder(profile.videoCodec);
mMediaRecorder.setVideoEncodingBitRate(profile.videoBitRate);
mMediaRecorder.setVideoFrameRate(profile.videoFrameRate);
mMediaRecorder.setVideoSize(profile.videoFrameWidth, profile.videoFrameHeight);
} }
if (mLocation != null) { if (mLocation != null) {

@ -37,12 +37,12 @@ class Camera2 extends CameraController {
} }
@Override @Override
void onStart() throws Exception { void onStart() {
} }
@Override @Override
void onStop() throws Exception { void onStop() {
} }

@ -2,13 +2,18 @@ package com.otaliastudios.cameraview;
import android.graphics.PointF; import android.graphics.PointF;
import android.location.Location; import android.location.Location;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread; import android.support.annotation.WorkerThread;
import java.io.File; import java.io.File;
abstract class CameraController implements CameraPreview.SurfaceCallback, FrameManager.BufferCallback { abstract class CameraController implements
CameraPreview.SurfaceCallback,
FrameManager.BufferCallback,
Thread.UncaughtExceptionHandler {
private static final String TAG = CameraController.class.getSimpleName(); private static final String TAG = CameraController.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG); private static final CameraLogger LOG = CameraLogger.create(TAG);
@ -20,6 +25,8 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
protected final CameraView.CameraCallbacks mCameraCallbacks; protected final CameraView.CameraCallbacks mCameraCallbacks;
protected CameraPreview mPreview; protected CameraPreview mPreview;
protected WorkerHandler mHandler;
/* for tests */ Handler mCrashHandler;
protected Facing mFacing; protected Facing mFacing;
protected Flash mFlash; protected Flash mFlash;
@ -45,8 +52,6 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
protected int mDeviceOrientation; protected int mDeviceOrientation;
protected int mState = STATE_STOPPED; protected int mState = STATE_STOPPED;
protected WorkerHandler mHandler;
// Used for testing. // Used for testing.
Task<Void> mZoomTask = new Task<>(); Task<Void> mZoomTask = new Task<>();
Task<Void> mExposureCorrectionTask = new Task<>(); Task<Void> mExposureCorrectionTask = new Task<>();
@ -55,27 +60,13 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
Task<Void> mHdrTask = new Task<>(); Task<Void> mHdrTask = new Task<>();
Task<Void> mLocationTask = new Task<>(); Task<Void> mLocationTask = new Task<>();
Task<Void> mVideoQualityTask = new Task<>(); Task<Void> mVideoQualityTask = new Task<>();
Task<Void> mStartVideoTask = new Task<>();
CameraController(CameraView.CameraCallbacks callback) { CameraController(CameraView.CameraCallbacks callback) {
mCameraCallbacks = callback; mCameraCallbacks = callback;
mCrashHandler = new Handler(Looper.getMainLooper());
mHandler = WorkerHandler.get("CameraViewController"); mHandler = WorkerHandler.get("CameraViewController");
mHandler.getThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { mHandler.getThread().setUncaughtExceptionHandler(this);
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
// Something went wrong. Thread is terminated (about to?).
// Move to other thread and stop resources.
LOG.w("Interrupting thread, due to exception.", throwable);
thread.interrupt();
LOG.w("Interrupted thread. Posting a stopImmediately.", ss());
mHandler = WorkerHandler.get("CameraViewController");
mHandler.post(new Runnable() {
@Override
public void run() {
stopImmediately();
}
});
}
});
mFrameManager = new FrameManager(2, this); mFrameManager = new FrameManager(2, this);
} }
@ -84,6 +75,58 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
mPreview.setSurfaceCallback(this); mPreview.setSurfaceCallback(this);
} }
//region Error handling
@Override
public void uncaughtException(final Thread thread, final Throwable throwable) {
// Something went wrong. Thread is terminated (about to?).
// Move to other thread and release resources.
if (!(throwable instanceof CameraException)) {
// This is unexpected, either a bug or something the developer should know.
// Release and crash the UI thread so we get bug reports.
LOG.e("uncaughtException:", "Unexpected exception:", throwable);
destroy();
mCrashHandler.post(new Runnable() {
@Override
public void run() {
RuntimeException exception;
if (throwable instanceof RuntimeException) {
exception = (RuntimeException) throwable;
} else {
exception = new RuntimeException(throwable);
}
throw exception;
}
});
} else {
// At the moment all CameraExceptions are unrecoverable, there was something
// wrong when starting, stopping, or binding the camera to the preview.
final CameraException error = (CameraException) throwable;
LOG.e("uncaughtException:", "Interrupting thread with state:", ss(), "due to CameraException:", error);
thread.interrupt();
mHandler = WorkerHandler.get("CameraViewController");
mHandler.getThread().setUncaughtExceptionHandler(this);
LOG.i("uncaughtException:", "Calling stopImmediately and notifying.");
mHandler.post(new Runnable() {
@Override
public void run() {
stopImmediately();
mCameraCallbacks.dispatchError(error);
}
});
}
}
final void destroy() {
LOG.i("destroy:", "state:", ss());
// Prevent CameraController leaks.
mHandler.getThread().setUncaughtExceptionHandler(null);
// Stop if needed.
stopImmediately();
}
//endregion
//region Start&Stop //region Start&Stop
private String ss() { private String ss() {
@ -102,20 +145,14 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
mHandler.post(new Runnable() { mHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
try { LOG.i("Start:", "executing. State:", ss());
LOG.i("Start:", "executing. State:", ss()); if (mState >= STATE_STARTING) return;
if (mState >= STATE_STARTING) return; mState = STATE_STARTING;
mState = STATE_STARTING; LOG.i("Start:", "about to call onStart()", ss());
LOG.i("Start:", "about to call onStart()", ss()); onStart();
onStart(); LOG.i("Start:", "returned from onStart().", "Dispatching.", ss());
LOG.i("Start:", "returned from onStart().", "Dispatching.", ss()); mState = STATE_STARTED;
mState = STATE_STARTED; mCameraCallbacks.dispatchOnCameraOpened(mOptions);
mCameraCallbacks.dispatchOnCameraOpened(mOptions);
} catch (Exception e) {
LOG.e("Error while starting the camera engine.", e);
throw new RuntimeException(e);
}
} }
}); });
} }
@ -126,38 +163,31 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
mHandler.post(new Runnable() { mHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
try { LOG.i("Stop:", "executing. State:", ss());
LOG.i("Stop:", "executing. State:", ss()); if (mState <= STATE_STOPPED) return;
if (mState <= STATE_STOPPED) return; mState = STATE_STOPPING;
mState = STATE_STOPPING; LOG.i("Stop:", "about to call onStop()");
LOG.i("Stop:", "about to call onStop()"); onStop();
onStop(); LOG.i("Stop:", "returned from onStop().", "Dispatching.");
LOG.i("Stop:", "returned from onStop().", "Dispatching."); mState = STATE_STOPPED;
mState = STATE_STOPPED; mCameraCallbacks.dispatchOnCameraClosed();
mCameraCallbacks.dispatchOnCameraClosed();
} catch (Exception e) {
LOG.e("Error while stopping the camera engine.", e);
throw new RuntimeException(e);
}
} }
}); });
} }
// Stops the preview synchronously, ensuring no exceptions are thrown. // Stops the preview synchronously, ensuring no exceptions are thrown.
void stopImmediately() { final void stopImmediately() {
try { try {
// Don't check, try stop again. // Don't check, try stop again.
LOG.i("Stop immediately. State was:", ss()); LOG.i("stopImmediately:", "State was:", ss());
if (mState == STATE_STOPPED) return;
mState = STATE_STOPPING; mState = STATE_STOPPING;
// Prevent leaking CameraController.
mHandler.getThread().setUncaughtExceptionHandler(null);
onStop(); onStop();
mState = STATE_STOPPED; mState = STATE_STOPPED;
LOG.i("Stop immediately. Stopped. State is:", ss()); LOG.i("stopImmediately:", "Stopped. State is:", ss());
} catch (Exception e) { } catch (Exception e) {
// Do nothing. // Do nothing.
LOG.i("Stop immediately. Exception while stopping.", e); LOG.i("stopImmediately:", "Swallowing exception while stopping.", e);
mState = STATE_STOPPED; mState = STATE_STOPPED;
} }
} }
@ -168,29 +198,22 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
mHandler.post(new Runnable() { mHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
try { LOG.i("Restart:", "executing. Needs stopping:", mState > STATE_STOPPED, ss());
LOG.i("Restart:", "executing. Needs stopping:", mState > STATE_STOPPED, ss()); // Don't stop if stopped.
// Don't stop if stopped. if (mState > STATE_STOPPED) {
if (mState > STATE_STOPPED) { mState = STATE_STOPPING;
mState = STATE_STOPPING; onStop();
onStop(); mState = STATE_STOPPED;
mState = STATE_STOPPED; LOG.i("Restart:", "stopped. Dispatching.", ss());
LOG.i("Restart:", "stopped. Dispatching.", ss()); mCameraCallbacks.dispatchOnCameraClosed();
mCameraCallbacks.dispatchOnCameraClosed();
}
LOG.i("Restart: about to start. State:", ss());
mState = STATE_STARTING;
onStart();
mState = STATE_STARTED;
LOG.i("Restart: returned from start. Dispatching. State:", ss());
mCameraCallbacks.dispatchOnCameraOpened(mOptions);
} catch (Exception e) {
LOG.e("Error while restarting the camera engine.", e);
throw new RuntimeException(e);
} }
LOG.i("Restart: about to start. State:", ss());
mState = STATE_STARTING;
onStart();
mState = STATE_STARTED;
LOG.i("Restart: returned from start. Dispatching. State:", ss());
mCameraCallbacks.dispatchOnCameraOpened(mOptions);
} }
}); });
} }
@ -198,11 +221,11 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
// Starts the preview. // Starts the preview.
// At the end of this method camera must be available, e.g. for setting parameters. // At the end of this method camera must be available, e.g. for setting parameters.
@WorkerThread @WorkerThread
abstract void onStart() throws Exception; abstract void onStart();
// Stops the preview. // Stops the preview.
@WorkerThread @WorkerThread
abstract void onStop() throws Exception; abstract void onStop();
// Returns current state. // Returns current state.
final int getState() { final int getState() {

@ -0,0 +1,12 @@
package com.otaliastudios.cameraview;
/**
* Holds an error with the camera configuration.
*/
public class CameraException extends RuntimeException {
CameraException(Throwable cause) {
super(cause);
}
}

@ -1,6 +1,7 @@
package com.otaliastudios.cameraview; package com.otaliastudios.cameraview;
import android.graphics.PointF; import android.graphics.PointF;
import android.support.annotation.NonNull;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import java.io.File; import java.io.File;
@ -29,6 +30,25 @@ public abstract class CameraListener {
} }
/**
* Notifies about an error during the camera setup or configuration.
* At the moment, errors that are passed here are unrecoverable. When this is called,
* the camera has been released and is presumably showing a black preview.
*
* This is the right moment to show an error dialog to the user.
* You can try calling start() again, but that is not guaranteed to work - if it doesn't,
* this callback will be invoked again.
*
* In the future, more information will be passed through the {@link CameraException} instance.
*
* @param exception the error
*/
@UiThread
public void onCameraError(@NonNull CameraException exception) {
}
/** /**
* Notifies that a picture previously captured with {@link CameraView#capturePicture()} * Notifies that a picture previously captured with {@link CameraView#capturePicture()}
* or {@link CameraView#captureSnapshot()} is ready to be shown or saved. * or {@link CameraView#captureSnapshot()} is ready to be shown or saved.

@ -580,7 +580,7 @@ public class CameraView extends FrameLayout {
public void destroy() { public void destroy() {
clearCameraListeners(); clearCameraListeners();
clearFrameProcessors(); clearFrameProcessors();
mCameraController.stopImmediately(); mCameraController.destroy();
} }
//endregion //endregion
@ -1335,6 +1335,7 @@ public class CameraView extends FrameLayout {
void dispatchOnZoomChanged(final float newValue, final PointF[] fingers); void dispatchOnZoomChanged(final float newValue, final PointF[] fingers);
void dispatchOnExposureCorrectionChanged(float newValue, float[] bounds, PointF[] fingers); void dispatchOnExposureCorrectionChanged(float newValue, float[] bounds, PointF[] fingers);
void dispatchFrame(Frame frame); void dispatchFrame(Frame frame);
void dispatchError(CameraException exception);
} }
private class Callbacks implements CameraCallbacks { private class Callbacks implements CameraCallbacks {
@ -1608,11 +1609,20 @@ public class CameraView extends FrameLayout {
}); });
} }
} }
}
//endregion
//region Deprecated @Override
public void dispatchError(final CameraException exception) {
mLogger.i("dispatchError", exception);
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (CameraListener listener : mListeners) {
listener.onCameraError(exception);
}
}
});
}
}
//endregion //endregion
} }

@ -0,0 +1,16 @@
package com.otaliastudios.cameraview;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class CameraExceptionTest {
@Test
public void testConstructor() {
RuntimeException cause = new RuntimeException("Error");
CameraException camera = new CameraException(cause);
assertEquals(cause, camera.getCause());
}
}
Loading…
Cancel
Save