Error handling and lifecycle boilerplate (#265)

* Update dependencies

* Improve error handling

* Add lifecycle support

* Use lifecycle in demo app

* Add README info
pull/266/head
Mattia Iavarone 6 years ago committed by GitHub
parent 7f43a4a10b
commit 3d8bf1618c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      .travis.yml
  2. 17
      README.md
  3. 12
      build.gradle
  4. 9
      cameraview/build.gradle
  5. 41
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
  6. 35
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraException.java
  7. 25
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  8. 3
      demo/build.gradle
  9. 21
      demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java

@ -24,9 +24,9 @@ android:
components: components:
- tools - tools
- platform-tools - platform-tools
- build-tools-27.0.3 - build-tools-28.0.2
- android-27 - android-28
- doc-27 - doc-28
install: install:
# Setup # Setup

@ -92,8 +92,21 @@ To use the CameraView engine, simply add a `CameraView` to your layout:
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
``` ```
`CameraView` has lots of XML attributes, so keep reading. Make sure you override `onResume`, `CameraView` is a component bound to your activity or fragment lifecycle. This means that you must pass the
`onPause` and `onDestroy` in your activity or fragment, and call `CameraView.start()`, `stop()` lifecycle owner using `setLifecycleOwner`:
```java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
CameraView camera = findViewById(R.id.camera);
camera.setLifecycleOwner(this);
// From fragments, use fragment.viewLifecycleOwner instead of this!
}
```
For those who are not using the support libraries and the lifecycle implementation, make sure you override `onResume`,
`onPause` and `onDestroy` in your component, and call `CameraView.start()`, `stop()`
and `destroy()`. and `destroy()`.
```java ```java

@ -7,11 +7,11 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.2.0-alpha15' classpath 'com.android.tools.build:gradle:3.2.0-rc03'
// https://inthecheesefactory.com/blog/how-to-upload-library-to-jcenter-maven-central-as-dependency/en // https://inthecheesefactory.com/blog/how-to-upload-library-to-jcenter-maven-central-as-dependency/en
// https://www.theguardian.com/technology/developer-blog/2016/dec/06/how-to-publish-an-android-library-a-mysterious-conversation // https://www.theguardian.com/technology/developer-blog/2016/dec/06/how-to-publish-an-android-library-a-mysterious-conversation
classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5'
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.0'
} }
} }
@ -23,11 +23,11 @@ allprojects {
} }
ext { ext {
compileSdkVersion = 27 compileSdkVersion = 28
buildToolsVersion = "27.0.3" supportLibVersion = '28.0.0-rc02'
supportLibVersion = '27.1.1' lifecycleVersion = '1.1.1'
minSdkVersion = 15 minSdkVersion = 15
targetSdkVersion = 27 targetSdkVersion = 28
} }
task clean(type: Delete) { task clean(type: Delete) {

@ -10,7 +10,7 @@ group = 'com.otaliastudios'
android { android {
compileSdkVersion rootProject.ext.compileSdkVersion compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion // buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig { defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
@ -42,13 +42,14 @@ dependencies {
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:1.10.19' testImplementation 'org.mockito:mockito-core:1.10.19'
androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.1' androidTestImplementation 'com.android.support.test:rules:1.0.2'
androidTestImplementation 'com.google.dexmaker:dexmaker:1.2' androidTestImplementation 'com.google.dexmaker:dexmaker:1.2'
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:1.2' androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:1.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
api "com.android.support:exifinterface:$supportLibVersion" api "com.android.support:exifinterface:$supportLibVersion"
api "android.arch.lifecycle:common:$lifecycleVersion"
implementation "com.android.support:support-annotations:$supportLibVersion" implementation "com.android.support:support-annotations:$supportLibVersion"
} }

@ -14,6 +14,7 @@ import android.os.Build;
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 android.util.Log;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
import java.io.File; import java.io.File;
@ -73,15 +74,8 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
schedule(null, false, new Runnable() { schedule(null, false, new Runnable() {
@Override @Override
public void run() { public void run() {
if (shouldBindToSurface()) {
LOG.i("onSurfaceAvailable:", "Inside handler. About to bind."); LOG.i("onSurfaceAvailable:", "Inside handler. About to bind.");
try { if (shouldBindToSurface()) bindToSurface();
bindToSurface();
} catch (Exception e) {
LOG.e("onSurfaceAvailable:", "Exception while binding camera to preview.", e);
throw new CameraException(e);
}
}
} }
}); });
} }
@ -126,7 +120,8 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
mCamera.setPreviewTexture((SurfaceTexture) output); mCamera.setPreviewTexture((SurfaceTexture) output);
} }
} catch (IOException e) { } catch (IOException e) {
throw new CameraException(e); Log.e("bindToSurface:", "Failed to bind.", e);
throw new CameraException(e, CameraException.REASON_FAILED_TO_START_PREVIEW);
} }
mPictureSize = computePictureSize(); mPictureSize = computePictureSize();
@ -157,7 +152,12 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
mFrameManager.allocate(ImageFormat.getBitsPerPixel(mPreviewFormat), mPreviewSize); mFrameManager.allocate(ImageFormat.getBitsPerPixel(mPreviewFormat), mPreviewSize);
LOG.i(log, "Starting preview with startPreview()."); LOG.i(log, "Starting preview with startPreview().");
try {
mCamera.startPreview(); mCamera.startPreview();
} catch (Exception e) {
LOG.e(log, "Failed to start preview.", e);
throw new CameraException(e, CameraException.REASON_FAILED_TO_START_PREVIEW);
}
LOG.i(log, "Started preview."); LOG.i(log, "Started preview.");
} }
@ -169,7 +169,12 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
onStop(); // Should not happen. onStop(); // Should not happen.
} }
if (collectCameraId()) { if (collectCameraId()) {
try {
mCamera = Camera.open(mCameraId); mCamera = Camera.open(mCameraId);
} catch (Exception e) {
LOG.e("onStart:", "Failed to connect. Maybe in use by another app?");
throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT);
}
mCamera.setErrorCallback(this); 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.
@ -196,7 +201,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
@WorkerThread @WorkerThread
@Override @Override
void onStop() { void onStop() {
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();
@ -212,7 +216,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
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.", e); LOG.w("onStop:", "Clean up.", "Exception while stopping preview.", e);
error = e;
} }
try { try {
@ -221,7 +224,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
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.", e); LOG.w("onStop:", "Clean up.", "Exception while releasing camera.", e);
error = e;
} }
} }
mExtraProperties = null; mExtraProperties = null;
@ -233,7 +235,11 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
mIsCapturingImage = false; mIsCapturingImage = false;
mIsCapturingVideo = false; mIsCapturingVideo = false;
LOG.w("onStop:", "Clean up.", "Returning."); LOG.w("onStop:", "Clean up.", "Returning.");
if (error != null) throw new CameraException(error);
// We were saving a reference to the exception here and throwing to the user.
// I don't think it's correct. We are closing and have already done our best
// to clean up resources. No need to throw.
// if (error != null) throw new CameraException(error);
} }
private boolean collectCameraId() { private boolean collectCameraId() {
@ -269,7 +275,14 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
} }
LOG.e("Error inside the onError callback.", error); LOG.e("Error inside the onError callback.", error);
throw new CameraException(new RuntimeException(CameraLogger.lastMessage)); Exception runtime = new RuntimeException(CameraLogger.lastMessage);
int reason;
switch (error) {
case Camera.CAMERA_ERROR_EVICTED: reason = CameraException.REASON_DISCONNECTED; break;
case Camera.CAMERA_ERROR_UNKNOWN: reason = CameraException.REASON_UNKNOWN; break;
default: reason = CameraException.REASON_UNKNOWN;
}
throw new CameraException(runtime, reason);
} }
@Override @Override

@ -6,7 +6,42 @@ package com.otaliastudios.cameraview;
*/ */
public class CameraException extends RuntimeException { public class CameraException extends RuntimeException {
/**
* Unknown error. No further info available.
*/
public static final int REASON_UNKNOWN = 0;
/**
* We failed to connect to the camera service.
* The camera might be in use by another app.
*/
public static final int REASON_FAILED_TO_CONNECT = 1;
/**
* Failed to start the camera preview.
* Again, the camera might be in use by another app.
*/
public static final int REASON_FAILED_TO_START_PREVIEW = 2;
/**
* Camera was forced to disconnect.
* In Camera1, this is thrown when {@link android.hardware.Camera.CAMERA_ERROR_EVICTED}
* is caught.
*/
public static final int REASON_DISCONNECTED = 3;
private int reason = REASON_UNKNOWN;
CameraException(Throwable cause) { CameraException(Throwable cause) {
super(cause); super(cause);
} }
CameraException(Throwable cause, int reason) {
super(cause);
this.reason = reason;
}
public int getReason() {
return reason;
}
} }

@ -4,6 +4,11 @@ import android.Manifest;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.app.Activity; import android.app.Activity;
import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.LifecycleObserver;
import android.arch.lifecycle.LifecycleOwner;
import android.arch.lifecycle.Lifecycling;
import android.arch.lifecycle.OnLifecycleEvent;
import android.content.Context; import android.content.Context;
import android.content.ContextWrapper; import android.content.ContextWrapper;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
@ -38,7 +43,7 @@ import static android.view.View.MeasureSpec.UNSPECIFIED;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
public class CameraView extends FrameLayout { public class CameraView extends FrameLayout implements LifecycleObserver {
private final static String TAG = CameraView.class.getSimpleName(); private final static String TAG = CameraView.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG); private static final CameraLogger LOG = CameraLogger.create(TAG);
@ -63,6 +68,7 @@ public class CameraView extends FrameLayout {
private MediaActionSound mSound; private MediaActionSound mSound;
/* for tests */ List<CameraListener> mListeners = new CopyOnWriteArrayList<>(); /* for tests */ List<CameraListener> mListeners = new CopyOnWriteArrayList<>();
/* for tests */ List<FrameProcessor> mFrameProcessors = new CopyOnWriteArrayList<>(); /* for tests */ List<FrameProcessor> mFrameProcessors = new CopyOnWriteArrayList<>();
private Lifecycle mLifecycle;
// Views // Views
GridLinesLayout mGridLinesLayout; GridLinesLayout mGridLinesLayout;
@ -138,7 +144,7 @@ public class CameraView extends FrameLayout {
if (a.getBoolean(R.styleable.CameraView_cameraPictureSizeSmallest, false)) constraints.add(SizeSelectors.smallest()); if (a.getBoolean(R.styleable.CameraView_cameraPictureSizeSmallest, false)) constraints.add(SizeSelectors.smallest());
if (a.getBoolean(R.styleable.CameraView_cameraPictureSizeBiggest, false)) constraints.add(SizeSelectors.biggest()); if (a.getBoolean(R.styleable.CameraView_cameraPictureSizeBiggest, false)) constraints.add(SizeSelectors.biggest());
SizeSelector selector = !constraints.isEmpty() ? SizeSelector selector = !constraints.isEmpty() ?
SizeSelectors.and(constraints.toArray(new SizeSelector[constraints.size()])) : SizeSelectors.and(constraints.toArray(new SizeSelector[0])) :
SizeSelectors.biggest(); SizeSelectors.biggest();
// Gestures // Gestures
@ -537,11 +543,24 @@ public class CameraView extends FrameLayout {
return mCameraController.getState() == CameraController.STATE_STOPPED; return mCameraController.getState() == CameraController.STATE_STOPPED;
} }
/**
* Sets the lifecycle owner for this view. This means you don't need
* to call {@link #start()}, {@link #stop()} or {@link #destroy()} at all.
*
* @param owner the owner activity or fragment
*/
public void setLifecycleOwner(LifecycleOwner owner) {
if (mLifecycle != null) mLifecycle.removeObserver(this);
mLifecycle = owner.getLifecycle();
mLifecycle.addObserver(this);
}
/** /**
* Starts the camera preview, if not started already. * Starts the camera preview, if not started already.
* This should be called onResume(), or when you are ready with permissions. * This should be called onResume(), or when you are ready with permissions.
*/ */
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
public void start() { public void start() {
if (!isEnabled()) return; if (!isEnabled()) return;
@ -611,6 +630,7 @@ public class CameraView extends FrameLayout {
* Stops the current preview, if any was started. * Stops the current preview, if any was started.
* This should be called onPause(). * This should be called onPause().
*/ */
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void stop() { public void stop() {
mCameraController.stop(); mCameraController.stop();
} }
@ -620,6 +640,7 @@ public class CameraView extends FrameLayout {
* Destroys this instance, releasing immediately * Destroys this instance, releasing immediately
* the camera resource. * the camera resource.
*/ */
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void destroy() { public void destroy() {
clearCameraListeners(); clearCameraListeners();
clearFrameProcessors(); clearFrameProcessors();

@ -2,7 +2,8 @@ apply plugin: 'com.android.application'
android { android {
compileSdkVersion rootProject.ext.compileSdkVersion compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion // buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig { defaultConfig {
applicationId "com.otaliastudios.cameraview.demo" applicationId "com.otaliastudios.cameraview.demo"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion

@ -44,6 +44,7 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis
CameraLogger.setLogLevel(CameraLogger.LEVEL_VERBOSE); CameraLogger.setLogLevel(CameraLogger.LEVEL_VERBOSE);
camera = findViewById(R.id.camera); camera = findViewById(R.id.camera);
camera.setLifecycleOwner(this);
camera.addCameraListener(new CameraListener() { camera.addCameraListener(new CameraListener() {
public void onCameraOpened(CameraOptions options) { onOpened(); } public void onCameraOpened(CameraOptions options) { onOpened(); }
public void onPictureTaken(byte[] jpeg) { onPicture(jpeg); } public void onPictureTaken(byte[] jpeg) { onPicture(jpeg); }
@ -196,25 +197,7 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis
return true; return true;
} }
//region Boilerplate //region Permissions
@Override
protected void onResume() {
super.onResume();
camera.start();
}
@Override
protected void onPause() {
super.onPause();
camera.stop();
}
@Override
protected void onDestroy() {
super.onDestroy();
camera.destroy();
}
@Override @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

Loading…
Cancel
Save