From 3d8bf1618c38c0117eb2b6f705054ae2409e387e Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Fri, 14 Sep 2018 14:06:17 -0300 Subject: [PATCH] Error handling and lifecycle boilerplate (#265) * Update dependencies * Improve error handling * Add lifecycle support * Use lifecycle in demo app * Add README info --- .travis.yml | 6 +-- README.md | 17 ++++++- build.gradle | 12 ++--- cameraview/build.gradle | 9 ++-- .../com/otaliastudios/cameraview/Camera1.java | 47 ++++++++++++------- .../cameraview/CameraException.java | 35 ++++++++++++++ .../otaliastudios/cameraview/CameraView.java | 25 +++++++++- demo/build.gradle | 3 +- .../cameraview/demo/CameraActivity.java | 21 +-------- 9 files changed, 121 insertions(+), 54 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0957e925..828a1704 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/README.md b/README.md index a78546d7..4ed5ae16 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/build.gradle b/build.gradle index 129a0ad0..d26434d3 100644 --- a/build.gradle +++ b/build.gradle @@ -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) { diff --git a/cameraview/build.gradle b/cameraview/build.gradle index 4882e0ac..ab9c9440 100644 --- a/cameraview/build.gradle +++ b/cameraview/build.gradle @@ -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" } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java index 6721572b..9fda44b4 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java @@ -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); - } - } + LOG.i("onSurfaceAvailable:", "Inside handler. About to bind."); + 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()."); - mCamera.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()) { - mCamera = Camera.open(mCameraId); + 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 diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraException.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraException.java index bdb11b0d..9b768d8e 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraException.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraException.java @@ -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; + } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index da30b4f5..3f44b18e 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -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 mListeners = new CopyOnWriteArrayList<>(); /* for tests */ List 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(); diff --git a/demo/build.gradle b/demo/build.gradle index 4098460b..e522710b 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -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 diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java index 4337a629..32b21017 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java @@ -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) {