diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index 0f4d82c5..15e214b7 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -347,9 +347,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { @NonNull protected CameraEngine instantiateCameraEngine(@NonNull Engine engine, @NonNull CameraEngine.Callback callback) { - if (mExperimental - && engine == Engine.CAMERA2 - && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (mExperimental && engine == Engine.CAMERA2) { return new Camera2Engine(callback); } else { mEngine = Engine.CAMERA1; @@ -2160,7 +2158,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { @NonNull @Override public Context getContext() { - return CameraView.this.getContext(); + return CameraView.this.getContext().getApplicationContext(); } @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java index 37f75628..faeeec81 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java @@ -222,7 +222,11 @@ public class Camera2Engine extends CameraBaseEngine implements * Removes default surfaces from the repeating request builder. */ private void removeRepeatingRequestBuilderSurfaces() { - mRepeatingRequestBuilder.removeTarget(mPreviewStreamSurface); + if(mRepeatingRequestBuilder == null) return; + + if(mPreviewStreamSurface != null){ + mRepeatingRequestBuilder.removeTarget(mPreviewStreamSurface); + } if (mFrameProcessingSurface != null) { mRepeatingRequestBuilder.removeTarget(mFrameProcessingSurface); } @@ -486,7 +490,7 @@ public class Camera2Engine extends CameraBaseEngine implements // Create a preview surface with the correct size. final Class outputClass = mPreview.getOutputClass(); final Object output = mPreview.getOutput(); - if (outputClass == SurfaceHolder.class) { + if (output != null && outputClass == SurfaceHolder.class) { try { // This must be called from the UI thread... Tasks.await(Tasks.call(new Callable() { @@ -502,12 +506,12 @@ public class Camera2Engine extends CameraBaseEngine implements throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT); } mPreviewStreamSurface = ((SurfaceHolder) output).getSurface(); - } else if (outputClass == SurfaceTexture.class) { + } else if (output != null && outputClass == SurfaceTexture.class) { ((SurfaceTexture) output).setDefaultBufferSize( mPreviewStreamSize.getWidth(), mPreviewStreamSize.getHeight()); mPreviewStreamSurface = new Surface((SurfaceTexture) output); - } else { + } else if(output != null) { throw new RuntimeException("Unknown CameraPreview output class."); } outputSurfaces.add(mPreviewStreamSurface); @@ -581,7 +585,6 @@ public class Camera2Engine extends CameraBaseEngine implements public void onConfigureFailed(@NonNull CameraCaptureSession session) { // This SHOULD be a library error so we throw a RuntimeException. String message = LOG.e("onConfigureFailed! Session", session); - throw new RuntimeException(message); } @Override @@ -704,22 +707,48 @@ public class Camera2Engine extends CameraBaseEngine implements mPreviewStreamSize = null; mCaptureSize = null; mFrameProcessingSize = null; - if (mFrameProcessingReader != null) { - // WARNING: This call synchronously releases all Images and their underlying - // properties. This can cause issues if the Image is being used. - mFrameProcessingReader.close(); - mFrameProcessingReader = null; - } - if (mPictureReader != null) { - mPictureReader.close(); - mPictureReader = null; - } - mSession.close(); - mSession = null; + closeFrameProcessor(); + closePictureRender(); + closeSession(); LOG.i("onStopBind:", "Returning."); return Tasks.forResult(null); } + private void closeFrameProcessor() { + try{ + if (mFrameProcessingReader != null) { + // WARNING: This call synchronously releases all Images and their underlying + // properties. This can cause issues if the Image is being used. + mFrameProcessingReader.close(); + mFrameProcessingReader = null; + } + }catch (Exception ex){ + LOG.e("cameraEngine", "Error while closing FrameProcessor", ex); + } + } + + private void closePictureRender() { + try{ + if (mPictureReader != null) { + mPictureReader.close(); + mPictureReader = null; + } + }catch (Exception ex){ + LOG.e("cameraEngine", "Error while closing PictureRender", ex); + } + } + + private void closeSession() { + try{ + if(mSession != null){ + mSession.close(); + mSession = null; + } + }catch (Exception ex){ + LOG.e("cameraEngine", "Error while closing session", ex); + } + } + @EngineThread @NonNull @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java index e695b61c..6d59c31b 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java @@ -51,8 +51,6 @@ import androidx.annotation.VisibleForTesting; import java.io.File; import java.io.FileDescriptor; import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; /** @@ -132,14 +130,11 @@ public abstract class CameraEngine implements protected static final String TAG = CameraEngine.class.getSimpleName(); protected static final CameraLogger LOG = CameraLogger.create(TAG); - // If this is 2, this means we'll try to run destroy() twice. - private static final int DESTROY_RETRIES = 2; private WorkerHandler mHandler; @VisibleForTesting Handler mCrashHandler; private final Callback mCallback; - private final CameraStateOrchestrator mOrchestrator - = new CameraStateOrchestrator(new CameraOrchestrator.Callback() { + private final CameraStateOrchestrator mOrchestrator = new CameraStateOrchestrator(new CameraOrchestrator.Callback() { @Override @NonNull public WorkerHandler getJobWorker(@NonNull String job) { @@ -286,48 +281,22 @@ public abstract class CameraEngine implements * awaiting for {@link #stop(boolean)} to return. */ public void destroy(boolean unrecoverably) { - destroy(unrecoverably, 0); - } - - private void destroy(boolean unrecoverably, int depth) { LOG.i("DESTROY:", "state:", getState(), - "thread:", Thread.currentThread(), - "depth:", depth, - "unrecoverably:", unrecoverably); + "thread:", Thread.currentThread(), + "unrecoverably:", unrecoverably); + if (unrecoverably) { // Prevent CameraEngine leaks. Don't set to null, or exceptions // inside the standard stop() method might crash the main thread. mHandler.getThread().setUncaughtExceptionHandler(new NoOpExceptionHandler()); } - // Cannot use Tasks.await() because we might be on the UI thread. - final CountDownLatch latch = new CountDownLatch(1); - stop(true).addOnCompleteListener( - mHandler.getExecutor(), - new OnCompleteListener() { - @Override - public void onComplete(@NonNull Task task) { - latch.countDown(); - } - }); - try { - boolean success = latch.await(6, TimeUnit.SECONDS); - if (!success) { - // This thread is likely stuck. The reason might be deadlock issues in the internal - // camera implementation, at least in emulators: see Camera1Engine and Camera2Engine - // onStopEngine() implementation and comments. - LOG.e("DESTROY: Could not destroy synchronously after 6 seconds.", - "Current thread:", Thread.currentThread(), - "Handler thread:", mHandler.getThread()); - depth++; - if (depth < DESTROY_RETRIES) { - recreateHandler(true); - LOG.e("DESTROY: Trying again on thread:", mHandler.getThread()); - destroy(unrecoverably, depth); - } else { - LOG.w("DESTROY: Giving up because DESTROY_RETRIES was reached."); - } - } - } catch (InterruptedException ignore) {} + + // Release orchestrator, to cancel all current pending process. + // It will make future tasks that will open the camera not be executed. + mOrchestrator.release(); + + // Close camera device. + forceStop(); } @SuppressWarnings("WeakerAccess") @@ -340,12 +309,47 @@ public abstract class CameraEngine implements @NonNull public Task start() { LOG.i("START:", "scheduled. State:", getState()); - Task engine = startEngine(); - startBind(); - startPreview(); + // The user may have left screen when the camera is properly started. + // So, if that happen, the orchestrator will be released and we close the camera in order to avoid memory leak. + Task engine = startEngine().addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if(mOrchestrator.isReleased()){ + onStopEngine(); + } + } + }); + startBind().addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if(mOrchestrator.isReleased()){ + onStopBind(); + } + } + }); + startPreview().addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if(mOrchestrator.isReleased()){ + onStopPreview(); + } + } + }); return engine; } + public void forceStop(){ + mHandler.post(new Runnable() { + @Override + public void run() { + LOG.i("STOP:", "scheduled. State:", getState()); + onStopPreview(); + onStopBind(); + onStopEngine(); + } + }); + } + @NonNull public Task stop(final boolean swallowExceptions) { LOG.i("STOP:", "scheduled. State:", getState()); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraOrchestrator.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraOrchestrator.java index d703eabf..a9afaaa3 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraOrchestrator.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraOrchestrator.java @@ -59,6 +59,7 @@ public class CameraOrchestrator { protected final ArrayDeque mJobs = new ArrayDeque<>(); protected final Object mLock = new Object(); private final Map mDelayedJobs = new HashMap<>(); + private boolean released = false; public CameraOrchestrator(@NonNull Callback callback) { mCallback = callback; @@ -78,6 +79,17 @@ public class CameraOrchestrator { }); } + public void release(){ + mJobs.clear(); + mDelayedJobs.clear(); + reset(); + released = true; + } + + public boolean isReleased(){ + return released; + } + @SuppressWarnings("unchecked") @NonNull public Task schedule(@NonNull final String name, @@ -87,11 +99,17 @@ public class CameraOrchestrator { final TaskCompletionSource source = new TaskCompletionSource<>(); final WorkerHandler handler = mCallback.getJobWorker(name); synchronized (mLock) { - applyCompletionListener(mJobs.getLast().task, handler, - new OnCompleteListener() { + if(released){ + return source.getTask(); + } + + applyCompletionListener(mJobs.getLast().task, handler, new OnCompleteListener() { @Override public void onComplete(@NonNull Task task) { synchronized (mLock) { + if (mJobs.isEmpty()) { + return; + } mJobs.removeFirst(); ensureToken(); } @@ -119,7 +137,9 @@ public class CameraOrchestrator { }); } catch (Exception e) { LOG.i(name.toUpperCase(), "- Finished.", e); - if (dispatchExceptions) mCallback.handleJobException(name, e); + if (dispatchExceptions) { + mCallback.handleJobException(name, e); + } source.trySetException(e); } } @@ -132,6 +152,8 @@ public class CameraOrchestrator { public void scheduleDelayed(@NonNull final String name, long minDelay, @NonNull final Runnable runnable) { + if(released) return; + Runnable wrapper = new Runnable() { @Override public void run() { @@ -165,6 +187,7 @@ public class CameraOrchestrator { public void reset() { synchronized (mLock) { + released = false; List all = new ArrayList<>(); //noinspection CollectionAddAllCanBeReplacedWithConstructor all.addAll(mDelayedJobs.keySet()); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java index 068c84e2..4cfdad0a 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java @@ -139,7 +139,6 @@ public abstract class CameraPreview { * or a SurfaceTexture). * @return the surface object */ - @NonNull public abstract Output getOutput(); /**