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:
- tools
- platform-tools
- build-tools-27.0.3
- android-27
- doc-27
- build-tools-28.0.2
- android-28
- doc-28
install:
# Setup

@ -92,8 +92,21 @@ To use the CameraView engine, simply add a `CameraView` to your layout:
android:layout_height="wrap_content" />
```
`CameraView` has lots of XML attributes, so keep reading. Make sure you override `onResume`,
`onPause` and `onDestroy` in your activity or fragment, and call `CameraView.start()`, `stop()`
`CameraView` is a component bound to your activity or fragment lifecycle. This means that you must pass the
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()`.
```java

@ -7,11 +7,11 @@ buildscript {
}
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://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.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 {
compileSdkVersion = 27
buildToolsVersion = "27.0.3"
supportLibVersion = '27.1.1'
compileSdkVersion = 28
supportLibVersion = '28.0.0-rc02'
lifecycleVersion = '1.1.1'
minSdkVersion = 15
targetSdkVersion = 27
targetSdkVersion = 28
}
task clean(type: Delete) {

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

@ -14,6 +14,7 @@ import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import android.util.Log;
import android.view.SurfaceHolder;
import java.io.File;
@ -73,15 +74,8 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
schedule(null, false, new Runnable() {
@Override
public void run() {
if (shouldBindToSurface()) {
LOG.i("onSurfaceAvailable:", "Inside handler. About to bind.");
try {
bindToSurface();
} catch (Exception e) {
LOG.e("onSurfaceAvailable:", "Exception while binding camera to preview.", e);
throw new CameraException(e);
}
}
if (shouldBindToSurface()) bindToSurface();
}
});
}
@ -126,7 +120,8 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
mCamera.setPreviewTexture((SurfaceTexture) output);
}
} 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();
@ -157,7 +152,12 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
mFrameManager.allocate(ImageFormat.getBitsPerPixel(mPreviewFormat), mPreviewSize);
LOG.i(log, "Starting preview with startPreview().");
try {
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.");
}
@ -169,7 +169,12 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
onStop(); // Should not happen.
}
if (collectCameraId()) {
try {
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);
// 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
@Override
void onStop() {
Exception error = null;
LOG.i("onStop:", "About to clean up.");
mHandler.get().removeCallbacks(mPostFocusResetRunnable);
mFrameManager.release();
@ -212,7 +216,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
LOG.i("onStop:", "Clean up.", "Stopped preview.");
} catch (Exception e) {
LOG.w("onStop:", "Clean up.", "Exception while stopping preview.", e);
error = e;
}
try {
@ -221,7 +224,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
LOG.i("onStop:", "Clean up.", "Released camera.");
} catch (Exception e) {
LOG.w("onStop:", "Clean up.", "Exception while releasing camera.", e);
error = e;
}
}
mExtraProperties = null;
@ -233,7 +235,11 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
mIsCapturingImage = false;
mIsCapturingVideo = false;
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() {
@ -269,7 +275,14 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
}
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

@ -6,7 +6,42 @@ package com.otaliastudios.cameraview;
*/
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) {
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.TargetApi;
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.ContextWrapper;
import android.content.pm.PackageInfo;
@ -38,7 +43,7 @@ import static android.view.View.MeasureSpec.UNSPECIFIED;
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 static final CameraLogger LOG = CameraLogger.create(TAG);
@ -63,6 +68,7 @@ public class CameraView extends FrameLayout {
private MediaActionSound mSound;
/* for tests */ List<CameraListener> mListeners = new CopyOnWriteArrayList<>();
/* for tests */ List<FrameProcessor> mFrameProcessors = new CopyOnWriteArrayList<>();
private Lifecycle mLifecycle;
// Views
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_cameraPictureSizeBiggest, false)) constraints.add(SizeSelectors.biggest());
SizeSelector selector = !constraints.isEmpty() ?
SizeSelectors.and(constraints.toArray(new SizeSelector[constraints.size()])) :
SizeSelectors.and(constraints.toArray(new SizeSelector[0])) :
SizeSelectors.biggest();
// Gestures
@ -537,11 +543,24 @@ public class CameraView extends FrameLayout {
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.
* This should be called onResume(), or when you are ready with permissions.
*/
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
public void start() {
if (!isEnabled()) return;
@ -611,6 +630,7 @@ public class CameraView extends FrameLayout {
* Stops the current preview, if any was started.
* This should be called onPause().
*/
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void stop() {
mCameraController.stop();
}
@ -620,6 +640,7 @@ public class CameraView extends FrameLayout {
* Destroys this instance, releasing immediately
* the camera resource.
*/
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void destroy() {
clearCameraListeners();
clearFrameProcessors();

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

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

Loading…
Cancel
Save