Create orchestrator, replacing Step class

pull/697/head
Mattia Iavarone 6 years ago
parent fd17a8339e
commit 57a4cfba80
  1. 5
      cameraview/build.gradle
  2. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  3. 17
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  4. 167
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  5. 238
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  6. 645
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  7. 178
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Step.java
  8. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/LogAction.java
  9. 180
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraOrchestrator.java
  10. 17
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraState.java
  11. 117
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraStateOrchestrator.java

@ -42,8 +42,8 @@ dependencies {
androidTestImplementation 'org.mockito:mockito-android:2.28.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
api 'androidx.exifinterface:exifinterface:1.0.0'
api 'androidx.lifecycle:lifecycle-common:2.1.0-alpha01'
api 'androidx.exifinterface:exifinterface:1.1.0'
api 'androidx.lifecycle:lifecycle-common:2.1.0'
api 'com.google.android.gms:play-services-tasks:17.0.0'
implementation 'androidx.annotation:annotation:1.1.0'
}
@ -250,6 +250,7 @@ task mergeCoverageReports(type: JacocoReport) {
classFilter.add('**/com/otaliastudios/cameraview/picture/**.*')
classFilter.add('**/com/otaliastudios/cameraview/video/**.*')
// TODO these below could be easily testable ALSO outside of the integration tests
classFilter.add('**/com/otaliastudios/cameraview/orchestrator/**.*')
classFilter.add('**/com/otaliastudios/cameraview/video/encoding/**.*')
}
// We don't test OpenGL filters.

@ -128,7 +128,7 @@ public class CameraViewTest extends BaseTest {
public void testClose() {
cameraView.close();
verify(mockPreview, times(1)).onPause();
verify(mockController, times(1)).stop();
verify(mockController, times(1)).stop(false);
}
@Test

@ -48,6 +48,7 @@ import com.otaliastudios.cameraview.engine.Camera1Engine;
import com.otaliastudios.cameraview.engine.Camera2Engine;
import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.filter.FilterParser;
import com.otaliastudios.cameraview.filter.Filters;
@ -693,15 +694,17 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
//region Lifecycle APIs
/**
* Returns whether the camera has started showing its preview.
* Returns whether the camera engine has started.
* @return whether the camera has started
*/
public boolean isOpened() {
return mCameraEngine.getEngineState() >= CameraEngine.STATE_STARTED;
return mCameraEngine.getState().isAtLeast(CameraState.ENGINE)
&& mCameraEngine.getTargetState().isAtLeast(CameraState.ENGINE);
}
private boolean isClosed() {
return mCameraEngine.getEngineState() == CameraEngine.STATE_STOPPED;
return mCameraEngine.getState() == CameraState.OFF
&& !mCameraEngine.isChangingState();
}
/**
@ -792,7 +795,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void close() {
if (mInEditor) return;
mCameraEngine.stop();
mCameraEngine.stop(false);
if (mCameraPreview != null) mCameraPreview.onPause();
}
@ -2006,7 +2009,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
}
@Override
public void dispatchOnCameraOpened(final CameraOptions options) {
public void dispatchOnCameraOpened(@NonNull final CameraOptions options) {
mLogger.i("dispatchOnCameraOpened", options);
mUiHandler.post(new Runnable() {
@Override
@ -2054,7 +2057,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
}
@Override
public void dispatchOnPictureTaken(final PictureResult.Stub stub) {
public void dispatchOnPictureTaken(@NonNull final PictureResult.Stub stub) {
mLogger.i("dispatchOnPictureTaken", stub);
mUiHandler.post(new Runnable() {
@Override
@ -2068,7 +2071,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
}
@Override
public void dispatchOnVideoTaken(final VideoResult.Stub stub) {
public void dispatchOnVideoTaken(@NonNull final VideoResult.Stub stub) {
mLogger.i("dispatchOnVideoTaken", stub);
mUiHandler.post(new Runnable() {
@Override

@ -24,6 +24,7 @@ import com.otaliastudios.cameraview.engine.mappers.Camera1Mapper;
import com.otaliastudios.cameraview.engine.offset.Axis;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.engine.options.Camera1Options;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.VideoResult;
@ -49,7 +50,6 @@ import java.util.ArrayList;
import java.util.List;
@SuppressWarnings("deprecation")
public class Camera1Engine extends CameraEngine implements
Camera.PreviewCallback,
Camera.ErrorCallback,
@ -58,13 +58,15 @@ public class Camera1Engine extends CameraEngine implements
private static final String TAG = Camera1Engine.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG);
private static final String JOB_FOCUS_RESET = "focus reset";
private static final String JOB_FOCUS_END = "focus end";
private static final int PREVIEW_FORMAT = ImageFormat.NV21;
@VisibleForTesting static final int AUTOFOCUS_END_DELAY_MILLIS = 2500;
private final Camera1Mapper mMapper = Camera1Mapper.get();
private Camera mCamera;
@VisibleForTesting int mCameraId;
private Runnable mFocusEndRunnable;
public Camera1Engine(@NonNull Callback callback) {
super(callback);
@ -286,10 +288,8 @@ public class Camera1Engine extends CameraEngine implements
@Override
protected Task<Void> onStopEngine() {
LOG.i("onStopEngine:", "About to clean up.");
mHandler.remove(mFocusResetRunnable);
if (mFocusEndRunnable != null) {
mHandler.remove(mFocusEndRunnable);
}
mOrchestrator.remove(JOB_FOCUS_RESET);
mOrchestrator.remove(JOB_FOCUS_END);
if (mCamera != null) {
try {
LOG.i("onStopEngine:", "Clean up.", "Releasing camera.");
@ -457,14 +457,13 @@ public class Camera1Engine extends CameraEngine implements
public void setFlash(@NonNull Flash flash) {
final Flash old = mFlash;
mFlash = flash;
mHandler.run(new Runnable() {
mFlashTask = mOrchestrator.scheduleStateful("flash",
CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters();
if (applyFlash(params, old)) mCamera.setParameters(params);
}
mFlashOp.end(null);
Camera.Parameters params = mCamera.getParameters();
if (applyFlash(params, old)) mCamera.setParameters(params);
}
});
}
@ -482,14 +481,13 @@ public class Camera1Engine extends CameraEngine implements
public void setLocation(@Nullable Location location) {
final Location oldLocation = mLocation;
mLocation = location;
mHandler.run(new Runnable() {
mLocationTask = mOrchestrator.scheduleStateful("location",
CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters();
if (applyLocation(params, oldLocation)) mCamera.setParameters(params);
}
mLocationOp.end(null);
Camera.Parameters params = mCamera.getParameters();
if (applyLocation(params, oldLocation)) mCamera.setParameters(params);
}
});
}
@ -510,14 +508,13 @@ public class Camera1Engine extends CameraEngine implements
public void setWhiteBalance(@NonNull WhiteBalance whiteBalance) {
final WhiteBalance old = mWhiteBalance;
mWhiteBalance = whiteBalance;
mHandler.run(new Runnable() {
mWhiteBalanceTask = mOrchestrator.scheduleStateful("white balance",
CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters();
if (applyWhiteBalance(params, old)) mCamera.setParameters(params);
}
mWhiteBalanceOp.end(null);
Camera.Parameters params = mCamera.getParameters();
if (applyWhiteBalance(params, old)) mCamera.setParameters(params);
}
});
}
@ -536,14 +533,13 @@ public class Camera1Engine extends CameraEngine implements
public void setHdr(@NonNull Hdr hdr) {
final Hdr old = mHdr;
mHdr = hdr;
mHandler.run(new Runnable() {
mHdrTask = mOrchestrator.scheduleStateful("hdr",
CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters();
if (applyHdr(params, old)) mCamera.setParameters(params);
}
mHdrOp.end(null);
Camera.Parameters params = mCamera.getParameters();
if (applyHdr(params, old)) mCamera.setParameters(params);
}
});
}
@ -561,19 +557,18 @@ public class Camera1Engine extends CameraEngine implements
public void setZoom(final float zoom, @Nullable final PointF[] points, final boolean notify) {
final float old = mZoomValue;
mZoomValue = zoom;
mHandler.run(new Runnable() {
mZoomTask = mOrchestrator.scheduleStateful("zoom",
CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters();
if (applyZoom(params, old)) {
mCamera.setParameters(params);
if (notify) {
mCallback.dispatchOnZoomChanged(mZoomValue, points);
}
Camera.Parameters params = mCamera.getParameters();
if (applyZoom(params, old)) {
mCamera.setParameters(params);
if (notify) {
mCallback.dispatchOnZoomChanged(mZoomValue, points);
}
}
mZoomOp.end(null);
}
});
}
@ -594,20 +589,19 @@ public class Camera1Engine extends CameraEngine implements
@Nullable final PointF[] points, final boolean notify) {
final float old = mExposureCorrectionValue;
mExposureCorrectionValue = EVvalue;
mHandler.run(new Runnable() {
mExposureCorrectionTask = mOrchestrator.scheduleStateful("exposure correction",
CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters();
if (applyExposureCorrection(params, old)) {
mCamera.setParameters(params);
if (notify) {
mCallback.dispatchOnExposureCorrectionChanged(mExposureCorrectionValue,
bounds, points);
}
Camera.Parameters params = mCamera.getParameters();
if (applyExposureCorrection(params, old)) {
mCamera.setParameters(params);
if (notify) {
mCallback.dispatchOnExposureCorrectionChanged(mExposureCorrectionValue,
bounds, points);
}
}
mExposureCorrectionOp.end(null);
}
});
}
@ -635,13 +629,12 @@ public class Camera1Engine extends CameraEngine implements
public void setPlaySounds(boolean playSounds) {
final boolean old = mPlaySounds;
mPlaySounds = playSounds;
mHandler.run(new Runnable() {
mPlaySoundsTask = mOrchestrator.scheduleStateful("play sounds",
CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
if (getEngineState() == STATE_STARTED) {
applyPlaySounds(old);
}
mPlaySoundsOp.end(null);
applyPlaySounds(old);
}
});
}
@ -672,14 +665,13 @@ public class Camera1Engine extends CameraEngine implements
public void setPreviewFrameRate(float previewFrameRate) {
final float old = previewFrameRate;
mPreviewFrameRate = previewFrameRate;
mHandler.run(new Runnable() {
mPreviewFrameRateTask = mOrchestrator.scheduleStateful("preview fps",
CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters();
if (applyPreviewFrameRate(params, old)) mCamera.setParameters(params);
}
mPreviewFrameRateOp.end(null);
Camera.Parameters params = mCamera.getParameters();
if (applyPreviewFrameRate(params, old)) mCamera.setParameters(params);
}
});
}
@ -737,7 +729,8 @@ public class Camera1Engine extends CameraEngine implements
@Override
public void onBufferAvailable(@NonNull byte[] buffer) {
if (getEngineState() == STATE_STARTED) {
if (getState().isAtLeast(CameraState.ENGINE)
&& getTargetState().isAtLeast(CameraState.ENGINE)) {
mCamera.addCallbackBuffer(buffer);
}
}
@ -770,10 +763,9 @@ public class Camera1Engine extends CameraEngine implements
}
final int viewWidthF = viewWidth;
final int viewHeightF = viewHeight;
mHandler.run(new Runnable() {
mOrchestrator.scheduleStateful("auto focus", CameraState.ENGINE, new Runnable() {
@Override
public void run() {
if (getEngineState() < STATE_STARTED) return;
if (!mCameraOptions.isAutoFocusSupported()) return;
final PointF p = new PointF(point.x, point.y); // copy.
int offset = getAngles().offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE);
@ -794,14 +786,14 @@ public class Camera1Engine extends CameraEngine implements
// The auto focus callback is not guaranteed to be called, but we really want it
// to be. So we remove the old runnable if still present and post a new one.
if (mFocusEndRunnable != null) mHandler.remove(mFocusEndRunnable);
mFocusEndRunnable = new Runnable() {
mOrchestrator.remove(JOB_FOCUS_END);
mOrchestrator.scheduleDelayed(JOB_FOCUS_END, AUTOFOCUS_END_DELAY_MILLIS,
new Runnable() {
@Override
public void run() {
mCallback.dispatchOnFocusEnd(gesture, false, p);
}
};
mHandler.post(AUTOFOCUS_END_DELAY_MILLIS, mFocusEndRunnable);
});
// Wrapping autoFocus in a try catch to handle some device specific exceptions,
// see See https://github.com/natario1/CameraView/issues/181.
@ -809,14 +801,27 @@ public class Camera1Engine extends CameraEngine implements
mCamera.autoFocus(new Camera.AutoFocusCallback() {
@Override
public void onAutoFocus(boolean success, Camera camera) {
if (mFocusEndRunnable != null) {
mHandler.remove(mFocusEndRunnable);
mFocusEndRunnable = null;
}
mOrchestrator.remove(JOB_FOCUS_END);
mOrchestrator.remove(JOB_FOCUS_RESET);
mCallback.dispatchOnFocusEnd(gesture, success, p);
mHandler.remove(mFocusResetRunnable);
if (shouldResetAutoFocus()) {
mHandler.post(getAutoFocusResetDelay(), mFocusResetRunnable);
mOrchestrator.scheduleStatefulDelayed(
JOB_FOCUS_RESET,
CameraState.ENGINE,
getAutoFocusResetDelay(),
new Runnable() {
@Override
public void run() {
mCamera.cancelAutoFocus();
Camera.Parameters params = mCamera.getParameters();
int maxAF = params.getMaxNumFocusAreas();
int maxAE = params.getMaxNumMeteringAreas();
if (maxAF > 0) params.setFocusAreas(null);
if (maxAE > 0) params.setMeteringAreas(null);
applyDefaultFocus(params); // Revert to internal focus.
mCamera.setParameters(params);
}
});
}
}
});
@ -825,7 +830,6 @@ public class Camera1Engine extends CameraEngine implements
// Let the mFocusEndRunnable do its job. (could remove it and quickly dispatch
// onFocusEnd here, but let's make it simpler).
}
}
});
}
@ -876,21 +880,6 @@ public class Camera1Engine extends CameraEngine implements
return new Rect(left, top, right, bottom);
}
private final Runnable mFocusResetRunnable = new Runnable() {
@Override
public void run() {
if (getEngineState() < STATE_STARTED) return;
mCamera.cancelAutoFocus();
Camera.Parameters params = mCamera.getParameters();
int maxAF = params.getMaxNumFocusAreas();
int maxAE = params.getMaxNumMeteringAreas();
if (maxAF > 0) params.setFocusAreas(null);
if (maxAE > 0) params.setMeteringAreas(null);
applyDefaultFocus(params); // Revert to internal focus.
mCamera.setParameters(params);
}
};
//endregion
}

@ -48,12 +48,14 @@ import com.otaliastudios.cameraview.engine.action.ActionHolder;
import com.otaliastudios.cameraview.engine.action.Actions;
import com.otaliastudios.cameraview.engine.action.BaseAction;
import com.otaliastudios.cameraview.engine.action.CompletionCallback;
import com.otaliastudios.cameraview.engine.action.LogAction;
import com.otaliastudios.cameraview.engine.mappers.Camera2Mapper;
import com.otaliastudios.cameraview.engine.meter.MeterAction;
import com.otaliastudios.cameraview.engine.meter.MeterResetAction;
import com.otaliastudios.cameraview.engine.offset.Axis;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.engine.options.Camera2Options;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameManager;
import com.otaliastudios.cameraview.gesture.Gesture;
@ -243,7 +245,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
}
private void applyRepeatingRequestBuilder(boolean checkStarted, int errorReason) {
if (getPreviewState() == STATE_STARTED || !checkStarted) {
if ((getState() == CameraState.PREVIEW && !isChangingState()) || !checkStarted) {
try {
mSession.setRepeatingRequest(mRepeatingRequestBuilder.build(),
mRepeatingRequestCallback, null);
@ -256,9 +258,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
LOG.e("applyRepeatingRequestBuilder: session is invalid!", e,
"checkStarted:", checkStarted,
"currentThread:", Thread.currentThread().getName(),
"previewState:", getPreviewState(),
"bindState:", getBindState(),
"engineState:", getEngineState());
"state:", getState(),
"targetState:", getTargetState());
throw new CameraException(CameraException.REASON_DISCONNECTED);
}
}
@ -588,13 +589,12 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
if (mFullVideoPendingStub != null) {
// Do not call takeVideo/onTakeVideo. It will reset some stub parameters that
// the recorder sets. Also we are posting so that doTakeVideo sees a started preview.
LOG.i("onStartPreview", "Posting doTakeVideo call.");
final VideoResult.Stub stub = mFullVideoPendingStub;
mFullVideoPendingStub = null;
mHandler.post(new Runnable() {
mOrchestrator.scheduleStateful("do take video", CameraState.PREVIEW,
new Runnable() {
@Override
public void run() {
LOG.i("onStartPreview", "Executing doTakeVideo call.");
doTakeVideo(stub);
}
});
@ -895,10 +895,10 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
// SnapshotRecorder will invoke this on its own thread, so let's post in our own thread
// and check camera state before trying to restore the preview. Engine might have been
// torn down in the engine thread while this was still being called.
mHandler.run(new Runnable() {
mOrchestrator.scheduleStateful("restore preview template", CameraState.BIND,
new Runnable() {
@Override
public void run() {
if (getBindState() < STATE_STARTED) return;
maybeRestorePreviewTemplateAfterVideo();
}
});
@ -1028,33 +1028,32 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
public void setFlash(@NonNull final Flash flash) {
final Flash old = mFlash;
mFlash = flash;
mHandler.run(new Runnable() {
mFlashTask = mOrchestrator.scheduleStateful("flash (" + flash + ")",
CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
if (getEngineState() == STATE_STARTED) {
boolean shouldApply = applyFlash(mRepeatingRequestBuilder, old);
boolean needsWorkaround = getPreviewState() == STATE_STARTED;
if (needsWorkaround) {
// Runtime changes to the flash value are not correctly handled by the
// driver. See https://stackoverflow.com/q/53003383/4288782 for example.
// For this reason, we go back to OFF, capture once, then go to the new one.
mFlash = Flash.OFF;
applyFlash(mRepeatingRequestBuilder, old);
try {
mSession.capture(mRepeatingRequestBuilder.build(), null,
null);
} catch (CameraAccessException e) {
throw createCameraException(e);
}
mFlash = flash;
applyFlash(mRepeatingRequestBuilder, old);
applyRepeatingRequestBuilder();
} else if (shouldApply) {
applyRepeatingRequestBuilder();
boolean shouldApply = applyFlash(mRepeatingRequestBuilder, old);
boolean needsWorkaround = getState() == CameraState.PREVIEW;
if (needsWorkaround) {
// Runtime changes to the flash value are not correctly handled by the
// driver. See https://stackoverflow.com/q/53003383/4288782 for example.
// For this reason, we go back to OFF, capture once, then go to the new one.
mFlash = Flash.OFF;
applyFlash(mRepeatingRequestBuilder, old);
try {
mSession.capture(mRepeatingRequestBuilder.build(), null,
null);
} catch (CameraAccessException e) {
throw createCameraException(e);
}
mFlash = flash;
applyFlash(mRepeatingRequestBuilder, old);
applyRepeatingRequestBuilder();
} else if (shouldApply) {
applyRepeatingRequestBuilder();
}
mFlashOp.end(null);
}
});
}
@ -1104,15 +1103,14 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
public void setLocation(@Nullable Location location) {
final Location old = mLocation;
mLocation = location;
mHandler.run(new Runnable() {
mLocationTask = mOrchestrator.scheduleStateful("location",
CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
if (getEngineState() == STATE_STARTED) {
if (applyLocation(mRepeatingRequestBuilder, old)) {
applyRepeatingRequestBuilder();
}
if (applyLocation(mRepeatingRequestBuilder, old)) {
applyRepeatingRequestBuilder();
}
mLocationOp.end(null);
}
});
}
@ -1130,15 +1128,15 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
public void setWhiteBalance(@NonNull WhiteBalance whiteBalance) {
final WhiteBalance old = mWhiteBalance;
mWhiteBalance = whiteBalance;
mHandler.run(new Runnable() {
mWhiteBalanceTask = mOrchestrator.scheduleStateful(
"white balance (" + whiteBalance + ")",
CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
if (getEngineState() == STATE_STARTED) {
if (applyWhiteBalance(mRepeatingRequestBuilder, old)) {
applyRepeatingRequestBuilder();
}
if (applyWhiteBalance(mRepeatingRequestBuilder, old)) {
applyRepeatingRequestBuilder();
}
mWhiteBalanceOp.end(null);
}
});
}
@ -1159,15 +1157,14 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
public void setHdr(@NonNull Hdr hdr) {
final Hdr old = mHdr;
mHdr = hdr;
mHandler.run(new Runnable() {
mHdrTask = mOrchestrator.scheduleStateful("hdr (" + hdr + ")",
CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
if (getEngineState() == STATE_STARTED) {
if (applyHdr(mRepeatingRequestBuilder, old)) {
applyRepeatingRequestBuilder();
}
if (applyHdr(mRepeatingRequestBuilder, old)) {
applyRepeatingRequestBuilder();
}
mHdrOp.end(null);
}
});
}
@ -1187,18 +1184,18 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
public void setZoom(final float zoom, final @Nullable PointF[] points, final boolean notify) {
final float old = mZoomValue;
mZoomValue = zoom;
mHandler.run(new Runnable() {
mZoomTask = mOrchestrator.scheduleStateful(
"zoom (" + zoom + ")",
CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
if (getEngineState() == STATE_STARTED) {
if (applyZoom(mRepeatingRequestBuilder, old)) {
applyRepeatingRequestBuilder();
if (notify) {
mCallback.dispatchOnZoomChanged(zoom, points);
}
if (applyZoom(mRepeatingRequestBuilder, old)) {
applyRepeatingRequestBuilder();
if (notify) {
mCallback.dispatchOnZoomChanged(zoom, points);
}
}
mZoomOp.end(null);
}
});
}
@ -1243,18 +1240,18 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
final boolean notify) {
final float old = mExposureCorrectionValue;
mExposureCorrectionValue = EVvalue;
mHandler.run(new Runnable() {
mExposureCorrectionTask = mOrchestrator.scheduleStateful(
"exposure correction (" + EVvalue + ")",
CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
if (getEngineState() == STATE_STARTED) {
if (applyExposureCorrection(mRepeatingRequestBuilder, old)) {
applyRepeatingRequestBuilder();
if (notify) {
mCallback.dispatchOnExposureCorrectionChanged(EVvalue, bounds, points);
}
if (applyExposureCorrection(mRepeatingRequestBuilder, old)) {
applyRepeatingRequestBuilder();
if (notify) {
mCallback.dispatchOnExposureCorrectionChanged(EVvalue, bounds, points);
}
}
mExposureCorrectionOp.end(null);
}
});
}
@ -1278,21 +1275,22 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override
public void setPlaySounds(boolean playSounds) {
mPlaySounds = playSounds;
mPlaySoundsOp.end(null);
mPlaySoundsTask = Tasks.forResult(null);
}
@Override public void setPreviewFrameRate(float previewFrameRate) {
@Override
public void setPreviewFrameRate(float previewFrameRate) {
final float oldPreviewFrameRate = mPreviewFrameRate;
mPreviewFrameRate = previewFrameRate;
mHandler.run(new Runnable() {
mPreviewFrameRateTask = mOrchestrator.scheduleStateful(
"preview fps (" + previewFrameRate + ")",
CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
if (getEngineState() == STATE_STARTED) {
if (applyPreviewFrameRate(mRepeatingRequestBuilder, oldPreviewFrameRate)) {
applyRepeatingRequestBuilder();
}
if (applyPreviewFrameRate(mRepeatingRequestBuilder, oldPreviewFrameRate)) {
applyRepeatingRequestBuilder();
}
mPreviewFrameRateOp.end(null);
}
});
}
@ -1334,19 +1332,12 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
public void setPictureFormat(final @NonNull PictureFormat pictureFormat) {
if (pictureFormat != mPictureFormat) {
mPictureFormat = pictureFormat;
LOG.i("setPictureFormat", "changing to", pictureFormat, "posting.");
mHandler.run(new Runnable() {
mOrchestrator.scheduleStateful("picture format (" + pictureFormat + ")",
CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
LOG.i("setPictureFormat", "changing to", pictureFormat,
"executing. EngineState:", getEngineState(),
"BindState:", getBindState());
if (getEngineState() == STATE_STOPPED) {
LOG.i("setPictureFormat", "not started so won't restart.");
} else {
LOG.i("setPictureFormat", "started or starting. Calling restart()");
restart();
}
restart();
}
});
}
@ -1391,7 +1382,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
return;
}
image.close();
if (getPreviewState() == STATE_STARTED) {
if (getState() == CameraState.PREVIEW && !isChangingState()) {
// After preview, the frame manager is correctly set up
Frame frame = getFrameManager().getFrame(data,
System.currentTimeMillis(),
@ -1405,35 +1396,23 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override
public void setHasFrameProcessors(final boolean hasFrameProcessors) {
LOG.i("setHasFrameProcessors", "changing to", hasFrameProcessors, "posting.");
Camera2Engine.super.setHasFrameProcessors(hasFrameProcessors);
mHandler.run(new Runnable() {
// Frame processing is set up partially when binding and partially when starting
// the preview. If the value is changed between the two, the preview step can crash.
mOrchestrator.schedule("has frame processors (" + hasFrameProcessors + ")",
true, new Runnable() {
@Override
public void run() {
LOG.i("setHasFrameProcessors", "changing to", hasFrameProcessors,
"executing. BindState:", getBindState(),
"PreviewState:", getPreviewState());
// Frame processing is set up partially when binding and partially when starting
// the preview. We don't want to only check bind state or startPreview can fail.
if (getBindState() == STATE_STOPPED) {
LOG.i("setHasFrameProcessors", "not bound so won't restart.");
} else if (getPreviewState() == STATE_STARTED) {
// This needs a restartBind(). NOTE: if taking video, this stops it.
LOG.i("setHasFrameProcessors", "bound with preview.",
"Calling restartBind().");
if (getState().isAtLeast(CameraState.BIND) && isChangingState()) {
// Extremely rare case in which this was called in between startBind and
// startPreview. This can cause issues. Try later.
setHasFrameProcessors(hasFrameProcessors);
} else if (getState().isAtLeast(CameraState.BIND)) {
// Apply and restart.
Camera2Engine.super.setHasFrameProcessors(hasFrameProcessors);
restartBind();
} else {
// Bind+Preview is not completely started yet not completely stopped.
// This can happen if the user adds a frame processor in onCameraOpened().
// Supporting this would add lot of complexity to this class, and
// this should be discouraged anyway since changing the frame processor number
// at this time requires restarting the camera when it was just opened.
// For these reasons, let's throw.
throw new IllegalStateException("Added/removed a FrameProcessor at illegal " +
"time. These operations should be done before opening the camera, or " +
"before closing it - NOT when it just opened, for example during the " +
"onCameraOpened() callback.");
// Just apply.
Camera2Engine.super.setHasFrameProcessors(hasFrameProcessors);
}
}
});
@ -1445,16 +1424,14 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override
public void startAutoFocus(@Nullable final Gesture gesture, @NonNull final PointF point) {
LOG.i("startAutoFocus", "dispatching. Gesture:", gesture);
mHandler.run(new Runnable() {
// This will only work when we have a preview, since it launches the preview
// in the end. Even without this it would need the bind state at least,
// since we need the preview size.
mOrchestrator.scheduleStateful("autofocus (" + gesture + ")",
CameraState.PREVIEW,
new Runnable() {
@Override
public void run() {
LOG.i("startAutoFocus", "executing. Preview state:", getPreviewState());
// This will only work when we have a preview, since it launches the preview
// in the end. Even without this it would need the bind state at least,
// since we need the preview size.
if (getPreviewState() < STATE_STARTED) return;
// The camera options API still has the auto focus API but it really
// refers to "3A metering to a specific point". Since we have a point, check.
if (!mCameraOptions.isAutoFocusSupported()) return;
@ -1468,10 +1445,16 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override
protected void onActionCompleted(@NonNull Action a) {
mCallback.dispatchOnFocusEnd(gesture, action.isSuccessful(), point);
mHandler.remove(mUnlockAndResetMeteringRunnable);
mOrchestrator.remove("reset metering");
if (shouldResetAutoFocus()) {
mHandler.post(getAutoFocusResetDelay(),
mUnlockAndResetMeteringRunnable);
mOrchestrator.scheduleDelayed("reset metering",
getAutoFocusResetDelay(),
new Runnable() {
@Override
public void run() {
unlockAndResetMetering();
}
});
}
}
});
@ -1495,15 +1478,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
return mMeterAction;
}
private final Runnable mUnlockAndResetMeteringRunnable = new Runnable() {
@Override
public void run() {
unlockAndResetMetering();
}
};
private void unlockAndResetMetering() {
if (getEngineState() == STATE_STARTED) {
if (getState() == CameraState.PREVIEW && !isChangingState()) {
Actions.sequence(
new BaseAction() {
@Override
@ -1566,7 +1542,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override
public void applyBuilder(@NonNull Action source, @NonNull CaptureRequest.Builder builder)
throws CameraAccessException {
if (getPreviewState() == STATE_STARTED) {
if (getState() == CameraState.PREVIEW && !isChangingState()) {
mSession.capture(builder.build(), mRepeatingRequestCallback, null);
}
}

@ -8,24 +8,24 @@ import android.location.Location;
import android.os.Handler;
import android.os.Looper;
import com.google.android.gms.tasks.Continuation;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.SuccessContinuation;
import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.CameraException;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.engine.orchestrator.CameraOrchestrator;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
import com.otaliastudios.cameraview.engine.orchestrator.CameraStateOrchestrator;
import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.engine.offset.Angles;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameManager;
import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import com.otaliastudios.cameraview.picture.PictureRecorder;
import com.otaliastudios.cameraview.preview.CameraPreview;
@ -54,7 +54,6 @@ import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
@ -73,13 +72,8 @@ import java.util.concurrent.TimeUnit;
* So at the end of both step 1 and 2, the engine should check if both have
* been performed and trigger the steps 3 and 4.
*
* We use an abstraction for each step called {@link Step} that manages the state of
* each step and ensures that start and stop operations, for each step, are never called if the
* previous one has not ended.
*
*
* STATE
* We only expose generic {@link #start()} and {@link #stop()} calls to the outside.
* We only expose generic {@link #start()} and {@link #stop(boolean)} calls to the outside.
* The external users of this class are most likely interested in whether we have completed step 2
* or not, since that tells us if we can act on the camera or not, rather than knowing about
* steps 3 and 4.
@ -87,18 +81,11 @@ import java.util.concurrent.TimeUnit;
* So in the {@link CameraEngine} notation,
* - {@link #start()}: ASYNC - starts the engine (S2). When possible, at a later time,
* S3 and S4 are also performed.
* - {@link #stop()}: ASYNC - stops everything: undoes S4, then S3, then S2.
* - {@link #stop(boolean)}: ASYNC - stops everything: undoes S4, then S3, then S2.
* - {@link #restart()}: ASYNC - completes a stop then a start.
* - {@link #destroy()}: SYNC - performs a {@link #stop()} that will go on no matter the exceptions,
* - {@link #destroy()}: SYNC - performs a {@link #stop(boolean)} that will go on no matter what,
* without throwing. Makes the engine unusable and clears resources.
*
* For example, we expose the engine (S2) state through {@link #getEngineState()}. It will be:
* - {@link #STATE_STARTING} if we're into step 2
* - {@link #STATE_STARTED} if we've completed step 2. No clue about 3 or 4.
* - {@link #STATE_STOPPING} if we're undoing steps 4, 3 and 2.
* - {@link #STATE_STOPPED} if we have undone steps 4, 3 and 2 (or they never started at all).
*
*
* THREADING
* Subclasses should always execute code on the thread given by {@link #mHandler}.
* For convenience, all the setup and tear down methods are called on this engine thread:
@ -127,17 +114,16 @@ public abstract class CameraEngine implements
public interface Callback {
@NonNull Context getContext();
void dispatchOnCameraOpened(CameraOptions options);
void dispatchOnCameraOpened(@NonNull CameraOptions options);
void dispatchOnCameraClosed();
void onCameraPreviewStreamSizeChanged();
void onShutter(boolean shouldPlaySound);
void dispatchOnVideoTaken(VideoResult.Stub stub);
void dispatchOnPictureTaken(PictureResult.Stub stub);
void dispatchOnVideoTaken(@NonNull VideoResult.Stub stub);
void dispatchOnPictureTaken(@NonNull PictureResult.Stub stub);
void dispatchOnFocusStart(@Nullable Gesture trigger, @NonNull PointF where);
void dispatchOnFocusEnd(@Nullable Gesture trigger, boolean success, @NonNull PointF where);
void dispatchOnZoomChanged(final float newValue, @Nullable final PointF[] fingers);
void dispatchOnExposureCorrectionChanged(float newValue,
@NonNull float[] bounds,
void dispatchOnExposureCorrectionChanged(float newValue, @NonNull float[] bounds,
@Nullable PointF[] fingers);
void dispatchFrame(@NonNull Frame frame);
void dispatchError(CameraException exception);
@ -148,15 +134,7 @@ public abstract class CameraEngine implements
private static final String TAG = CameraEngine.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG);
@SuppressWarnings({"WeakerAccess", "unused"})
public static final int STATE_STOPPING = Step.STATE_STOPPING;
public static final int STATE_STOPPED = Step.STATE_STOPPED;
@SuppressWarnings({"WeakerAccess", "unused"})
public static final int STATE_STARTING = Step.STATE_STARTING;
public static final int STATE_STARTED = Step.STATE_STARTED;
// Need to be protected
@SuppressWarnings("WeakerAccess") protected WorkerHandler mHandler;
@SuppressWarnings("WeakerAccess") protected final Callback mCallback;
@SuppressWarnings("WeakerAccess") protected CameraPreview mPreview;
@SuppressWarnings("WeakerAccess") protected CameraOptions mCameraOptions;
@ -177,7 +155,7 @@ public abstract class CameraEngine implements
@SuppressWarnings("WeakerAccess") protected boolean mPictureSnapshotMetering;
@SuppressWarnings("WeakerAccess") protected float mPreviewFrameRate;
// Can be private
private WorkerHandler mHandler;
@VisibleForTesting Handler mCrashHandler;
private final FrameManager mFrameManager;
private final Angles mAngles;
@ -193,36 +171,42 @@ public abstract class CameraEngine implements
private int mAudioBitRate;
private boolean mHasFrameProcessors;
private long mAutoFocusResetDelayMillis;
// in REF_VIEW, for consistency with SizeSelectors
private int mSnapshotMaxWidth = Integer.MAX_VALUE;
// in REF_VIEW, for consistency with SizeSelectors
private int mSnapshotMaxHeight = Integer.MAX_VALUE;
private int mSnapshotMaxWidth = Integer.MAX_VALUE; // in REF_VIEW like SizeSelectors
private int mSnapshotMaxHeight = Integer.MAX_VALUE; // in REF_VIEW like SizeSelectors
private Overlay overlay;
// Steps
private final Step.Callback mStepCallback = new Step.Callback() {
@Override @NonNull public Executor getExecutor() { return mHandler.getExecutor(); }
@Override public void handleException(@NonNull Exception exception) {
CameraEngine.this.handleException(Thread.currentThread(), exception, false);
@SuppressWarnings("WeakerAccess")
protected final CameraStateOrchestrator mOrchestrator
= new CameraStateOrchestrator(new CameraOrchestrator.Callback() {
@Override
@NonNull
public WorkerHandler getJobWorker(@NonNull String job) {
return mHandler;
}
@Override
public void handleJobException(@NonNull String job, @NonNull Exception exception) {
handleException(Thread.currentThread(), exception, false);
}
};
@VisibleForTesting
Step mEngineStep = new Step("engine", mStepCallback);
private Step mBindStep = new Step("bind", mStepCallback);
private Step mPreviewStep = new Step("preview", mStepCallback);
private Step mAllStep = new Step("all", mStepCallback);
});
// Ops used for testing.
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mZoomOp = new Op<>();
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mExposureCorrectionOp
= new Op<>();
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mFlashOp = new Op<>();
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mWhiteBalanceOp
= new Op<>();
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mHdrOp = new Op<>();
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mLocationOp = new Op<>();
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mPlaySoundsOp = new Op<>();
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mPreviewFrameRateOp = new Op<>();
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mZoomTask
= Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mExposureCorrectionTask
= Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mFlashTask
= Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mWhiteBalanceTask
= Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mHdrTask
= Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mLocationTask
= Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mPlaySoundsTask
= Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mPreviewFrameRateTask
= Tasks.forResult(null);
protected CameraEngine(@NonNull Callback callback) {
mCallback = callback;
@ -252,7 +236,7 @@ public abstract class CameraEngine implements
*/
private class CrashExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(final Thread thread, final Throwable throwable) {
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
handleException(thread, throwable, true);
}
}
@ -263,7 +247,7 @@ public abstract class CameraEngine implements
*/
private static class NoOpExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
// No-op.
}
}
@ -305,7 +289,7 @@ public abstract class CameraEngine implements
final CameraException cameraException = (CameraException) throwable;
LOG.e("uncaughtException:", "Got CameraException:", cameraException,
"on engine state:", getEngineState());
"on state:", getState());
if (fromExceptionHandler) {
// Got to restart the handler.
thread.interrupt();
@ -322,49 +306,95 @@ public abstract class CameraEngine implements
//endregion
//region states and steps
//region State management
public final int getEngineState() {
return mEngineStep.getState();
@NonNull
public final CameraState getState() {
return mOrchestrator.getCurrentState();
}
@SuppressWarnings("WeakerAccess")
public final int getBindState() {
return mBindStep.getState();
@NonNull
public final CameraState getTargetState() {
return mOrchestrator.getTargetState();
}
@SuppressWarnings({"unused", "WeakerAccess"})
public final int getPreviewState() {
return mPreviewStep.getState();
public boolean isChangingState() {
return mOrchestrator.hasPendingStateChange();
}
private boolean canStartEngine() {
return mEngineStep.isStoppingOrStopped();
/**
* Calls {@link #stop(boolean)} and waits for it.
* Not final due to mockito requirements.
*
* NOTE: Should not be called on the orchestrator thread! This would cause deadlocks due to us
* awaiting for {@link #stop(boolean)} to return.
*/
public void destroy() {
LOG.i("DESTROY:", "state:", getState(), "thread:", Thread.currentThread());
// 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());
// Stop if needed, synchronously and silently.
// Cannot use Tasks.await() because we might be on the UI thread.
final CountDownLatch latch = new CountDownLatch(1);
stop(true).addOnCompleteListener(
WorkerHandler.get().getExecutor(),
new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
latch.countDown();
}
});
try {
boolean success = latch.await(3, TimeUnit.SECONDS);
if (!success) {
LOG.e("Probably some deadlock in destroy.",
"Current thread:", Thread.currentThread(),
"Handler thread: ", mHandler.getThread());
}
} catch (InterruptedException ignore) {}
}
private boolean needsStopEngine() {
return mEngineStep.isStartedOrStarting();
@SuppressWarnings("WeakerAccess")
public void restart() {
LOG.i("RESTART:", "scheduled. State:", getState());
stop(false);
start();
}
private boolean canStartBind() {
return mEngineStep.isStarted()
&& mPreview != null
&& mPreview.hasSurface()
&& mBindStep.isStoppingOrStopped();
@NonNull
public Task<Void> start() {
LOG.i("START:", "scheduled. State:", getState());
Task<Void> engine = startEngine();
startBind();
startPreview();
return engine;
}
private boolean needsStopBind() {
return mBindStep.isStartedOrStarting();
@NonNull
public Task<Void> stop(final boolean swallowExceptions) {
LOG.i("STOP:", "scheduled. State:", getState());
stopPreview(swallowExceptions);
stopBind(swallowExceptions);
return stopEngine(swallowExceptions);
}
private boolean canStartPreview() {
return mEngineStep.isStarted()
&& mBindStep.isStarted()
&& mPreviewStep.isStoppingOrStopped();
@SuppressWarnings("WeakerAccess")
@NonNull
protected Task<Void> restartBind() {
LOG.i("RESTART BIND:", "scheduled. State:", getState());
stopPreview(false);
stopBind(false);
startBind();
return startPreview();
}
private boolean needsStopPreview() {
return mPreviewStep.isStartedOrStarting();
@SuppressWarnings("WeakerAccess")
@NonNull
protected Task<Void> restartPreview() {
LOG.i("RESTART PREVIEW:", "scheduled. State:", getState());
stopPreview(false);
return startPreview();
}
//endregion
@ -374,43 +404,41 @@ public abstract class CameraEngine implements
@NonNull
@EngineThread
private Task<Void> startEngine() {
if (canStartEngine()) {
mEngineStep.doStart(false, new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
if (!collectCameraInfo(mFacing)) {
LOG.e("onStartEngine:", "No camera available for facing", mFacing);
throw new CameraException(CameraException.REASON_NO_CAMERA);
}
return onStartEngine();
}
}, new Runnable() {
@Override
public void run() {
mCallback.dispatchOnCameraOpened(mCameraOptions);
return mOrchestrator.scheduleStateChange(CameraState.OFF, CameraState.ENGINE,
true,
new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
if (!collectCameraInfo(mFacing)) {
LOG.e("onStartEngine:", "No camera available for facing", mFacing);
throw new CameraException(CameraException.REASON_NO_CAMERA);
}
});
}
return mEngineStep.getTask();
return onStartEngine();
}
}).addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
mCallback.dispatchOnCameraOpened(mCameraOptions);
}
});
}
@NonNull
@EngineThread
private Task<Void> stopEngine(boolean swallowExceptions) {
if (needsStopEngine()) {
mEngineStep.doStop(swallowExceptions, new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
return onStopEngine();
}
}, new Runnable() {
@Override
public void run() {
mCallback.dispatchOnCameraClosed();
}
});
}
return mEngineStep.getTask();
return mOrchestrator.scheduleStateChange(CameraState.ENGINE, CameraState.OFF,
!swallowExceptions,
new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
return onStopEngine();
}
}).addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
mCallback.dispatchOnCameraClosed();
}
});
}
/**
@ -438,29 +466,32 @@ public abstract class CameraEngine implements
@NonNull
@EngineThread
private Task<Void> startBind() {
if (canStartBind()) {
mBindStep.doStart(false, new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
return mOrchestrator.scheduleStateChange(CameraState.ENGINE, CameraState.BIND,
true,
new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
if (mPreview != null && mPreview.hasSurface()) {
return onStartBind();
} else {
return Tasks.forCanceled();
}
});
}
return mBindStep.getTask();
}
});
}
@SuppressWarnings("UnusedReturnValue")
@NonNull
@EngineThread
private Task<Void> stopBind(boolean swallowExceptions) {
if (needsStopBind()) {
mBindStep.doStop(swallowExceptions, new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
return onStopBind();
}
});
}
return mBindStep.getTask();
return mOrchestrator.scheduleStateChange(CameraState.BIND, CameraState.ENGINE,
!swallowExceptions,
new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
return onStopBind();
}
});
}
/**
@ -481,39 +512,6 @@ public abstract class CameraEngine implements
@EngineThread
protected abstract Task<Void> onStopBind();
@SuppressWarnings("WeakerAccess")
protected void restartBind() {
LOG.i("restartBind", "posting.");
mHandler.run(new Runnable() {
@Override
public void run() {
LOG.w("restartBind", "executing stopPreview.");
stopPreview(false).continueWithTask(mHandler.getExecutor(),
new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(@NonNull Task<Void> task) {
LOG.w("restartBind", "executing stopBind.");
return stopBind(false);
}
}).onSuccessTask(mHandler.getExecutor(), new SuccessContinuation<Void, Void>() {
@NonNull
@Override
public Task<Void> then(@Nullable Void aVoid) {
LOG.w("restartBind", "executing startBind.");
return startBind();
}
}).onSuccessTask(mHandler.getExecutor(), new SuccessContinuation<Void, Void>() {
@NonNull
@Override
public Task<Void> then(@Nullable Void aVoid) {
LOG.w("restartBind", "executing startPreview.");
return startPreview();
}
});
}
});
}
//endregion
//region Start & Stop preview
@ -521,44 +519,26 @@ public abstract class CameraEngine implements
@NonNull
@EngineThread
private Task<Void> startPreview() {
LOG.i("startPreview", "canStartPreview:", canStartPreview());
if (canStartPreview()) {
mPreviewStep.doStart(false, new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
return onStartPreview();
}
});
}
return mPreviewStep.getTask();
return mOrchestrator.scheduleStateChange(CameraState.BIND, CameraState.PREVIEW,
true,
new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
return onStartPreview();
}
});
}
@SuppressWarnings("UnusedReturnValue")
@NonNull
@EngineThread
private Task<Void> stopPreview(boolean swallowExceptions) {
LOG.i("stopPreview",
"needsStopPreview:", needsStopPreview(),
"swallowExceptions:", swallowExceptions);
if (needsStopPreview()) {
mPreviewStep.doStop(swallowExceptions, new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
return onStopPreview();
}
});
}
return mPreviewStep.getTask();
}
@SuppressWarnings("WeakerAccess")
protected void restartPreview() {
LOG.i("restartPreview", "posting.");
mHandler.run(new Runnable() {
return mOrchestrator.scheduleStateChange(CameraState.PREVIEW, CameraState.BIND,
!swallowExceptions,
new Callable<Task<Void>>() {
@Override
public void run() {
LOG.i("restartPreview", "executing.");
stopPreview(false);
startPreview();
public Task<Void> call() {
return onStopPreview();
}
});
}
@ -592,33 +572,24 @@ public abstract class CameraEngine implements
@Override
public final void onSurfaceAvailable() {
LOG.i("onSurfaceAvailable:", "Size is", getPreviewSurfaceSize(Reference.VIEW));
mHandler.run(new Runnable() {
@Override
public void run() {
startBind().onSuccessTask(mHandler.getExecutor(), new SuccessContinuation<Void, Void>() {
@NonNull
@Override
public Task<Void> then(@Nullable Void aVoid) {
return startPreview();
}
});
}
});
startBind();
startPreview();
}
@Override
public final void onSurfaceDestroyed() {
LOG.i("onSurfaceDestroyed");
stopPreview(false);
stopBind(false);
}
@Override
public final void onSurfaceChanged() {
LOG.i("onSurfaceChanged:", "Size is", getPreviewSurfaceSize(Reference.VIEW),
"Posting.");
mHandler.run(new Runnable() {
LOG.i("onSurfaceChanged:", "Size is", getPreviewSurfaceSize(Reference.VIEW));
mOrchestrator.scheduleStateful("surface changed", CameraState.BIND,
new Runnable() {
@Override
public void run() {
LOG.i("onSurfaceChanged:",
"Engine started?", mEngineStep.isStarted(),
"Bind started?", mBindStep.isStarted());
if (!mEngineStep.isStarted()) return; // Too early
if (!mBindStep.isStarted()) return; // Too early
// Compute a new camera preview size and apply.
Size newSize = computePreviewStreamSize();
if (newSize.equals(mPreviewStreamSize)) {
@ -643,176 +614,6 @@ public abstract class CameraEngine implements
@EngineThread
protected abstract void onPreviewStreamSizeChanged();
@Override
public final void onSurfaceDestroyed() {
LOG.i("onSurfaceDestroyed");
mHandler.run(new Runnable() {
@Override
public void run() {
stopPreview(false).onSuccessTask(mHandler.getExecutor(),
new SuccessContinuation<Void, Void>() {
@NonNull
@Override
public Task<Void> then(@Nullable Void aVoid) {
return stopBind(false);
}
});
}
});
}
//endregion
//region Start & Stop all
/**
* Not final due to mockito requirements, but this is basically
* it, nothing more to do.
*
* NOTE: Should not be called on the {@link #mHandler} thread! I think
* that would cause deadlocks due to us awaiting for {@link #stop()} to return.
*/
public void destroy() {
LOG.i("destroy:", "state:", getEngineState(), "thread:", Thread.currentThread());
// 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());
// Stop if needed, synchronously and silently.
// 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<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
latch.countDown();
}
});
try {
boolean success = latch.await(3, TimeUnit.SECONDS);
if (!success) {
// TODO seems like this is always the case?
LOG.e("Probably some deadlock in destroy.",
"Current thread:", Thread.currentThread(),
"Handler thread: ", mHandler.getThread());
}
} catch (InterruptedException ignore) {}
}
@SuppressWarnings("WeakerAccess")
protected final void restart() {
LOG.i("Restart:", "calling stop and start");
stop();
start();
}
@NonNull
public Task<Void> start() {
LOG.i("Start:", "posting runnable. State:", getEngineState());
final TaskCompletionSource<Void> outTask = new TaskCompletionSource<>();
mHandler.run(new Runnable() {
@Override
public void run() {
LOG.w("Start:", "executing runnable. AllState is", mAllStep.getState());
// It's better to schedule anyway. allStep might be STARTING and we might be
// tempted to early return here, but the truth is that there might be a stop
// already scheduled when the STARTING op ends.
// if (mAllStep.isStoppingOrStopped()) {
// LOG.i("Start:", "executing runnable. AllState is STOPPING or STOPPED,
// so we schedule a start.");
mAllStep.doStart(false, new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
return startEngine().addOnFailureListener(mHandler.getExecutor(),
new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
outTask.trySetException(e);
}
}).onSuccessTask(mHandler.getExecutor(), new SuccessContinuation<Void, Void>() {
@NonNull
@Override
public Task<Void> then(@Nullable Void aVoid) {
outTask.trySetResult(null);
return startBind();
}
}).onSuccessTask(mHandler.getExecutor(), new SuccessContinuation<Void, Void>() {
@NonNull
@Override
public Task<Void> then(@Nullable Void aVoid) {
return startPreview();
}
});
}
});
// } else {
// // NOTE: this returns early if we were STARTING.
// LOG.i("Start:",
// "executing runnable. AllState is STARTING or STARTED, so we return early.");
// outTask.trySetResult(null);
// }
}
});
return outTask.getTask();
}
@NonNull
public Task<Void> stop() {
return stop(false);
}
@NonNull
private Task<Void> stop(final boolean swallowExceptions) {
LOG.i("Stop:", "posting runnable. State:", getEngineState());
final TaskCompletionSource<Void> outTask = new TaskCompletionSource<>();
mHandler.run(new Runnable() {
@Override
public void run() {
LOG.w("Stop:", "executing runnable. AllState is", mAllStep.getState());
// It's better to schedule anyway. allStep might be STOPPING and we might be
// tempted to early return here, but the truth is that there might be a start
// already scheduled when the STOPPING op ends.
// if (mAllStep.isStartedOrStarting()) {
// LOG.i("Stop:", "executing runnable. AllState is STARTING or STARTED,
// so we schedule a stop.");
mAllStep.doStop(swallowExceptions, new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
return stopPreview(swallowExceptions).continueWithTask(
mHandler.getExecutor(), new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(@NonNull Task<Void> task) {
return stopBind(swallowExceptions);
}
}).continueWithTask(mHandler.getExecutor(), new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(@NonNull Task<Void> task) {
return stopEngine(swallowExceptions);
}
}).continueWithTask(mHandler.getExecutor(), new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(@NonNull Task<Void> task) {
if (task.isSuccessful()) {
outTask.trySetResult(null);
} else {
//noinspection ConstantConditions
outTask.trySetException(task.getException());
}
return task;
}
});
}
});
// } else {
// // NOTE: this returns early if we were STOPPING.
// LOG.i("Stop:", "executing runnable.
// AllState is STOPPING or STOPPED, so we return early.");
// outTask.trySetResult(null);
// }
}
});
return outTask.getTask();
}
//endregion
//region Final setters and getters
@ -922,7 +723,7 @@ public abstract class CameraEngine implements
}
/**
* Sets a new facing value. This will restart the session (if there's any)
* Sets a new facing value. This will restart the engine session (if there's any)
* so that we can open the new facing camera.
* @param facing facing
*/
@ -930,10 +731,10 @@ public abstract class CameraEngine implements
final Facing old = mFacing;
if (facing != old) {
mFacing = facing;
mHandler.run(new Runnable() {
mOrchestrator.scheduleStateful("facing", CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
if (getEngineState() < STATE_STARTED) return;
if (collectCameraInfo(facing)) {
restart();
} else {
@ -975,12 +776,11 @@ public abstract class CameraEngine implements
public final void setMode(@NonNull Mode mode) {
if (mode != mMode) {
mMode = mode;
mHandler.run(new Runnable() {
mOrchestrator.scheduleStateful("mode", CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
if (getEngineState() == STATE_STARTED) {
restart();
}
restart();
}
});
}
@ -1134,22 +934,22 @@ public abstract class CameraEngine implements
/* not final for tests */
public void takePicture(final @NonNull PictureResult.Stub stub) {
LOG.i("takePicture", "scheduling");
mHandler.run(new Runnable() {
// Save boolean before scheduling! See how Camera2Engine calls this with a temp value.
final boolean metering = mPictureMetering;
mOrchestrator.scheduleStateful("take picture", CameraState.BIND,
new Runnable() {
@Override
public void run() {
LOG.i("takePicture", "performing. BindState:", getBindState(),
"isTakingPicture:", isTakingPicture());
LOG.i("takePicture:", "running. isTakingPicture:", isTakingPicture());
if (isTakingPicture()) return;
if (mMode == Mode.VIDEO) {
throw new IllegalStateException("Can't take hq pictures while in VIDEO mode");
}
if (getBindState() < STATE_STARTED) return;
if (isTakingPicture()) return;
stub.isSnapshot = false;
stub.location = mLocation;
stub.facing = mFacing;
stub.format = mPictureFormat;
onTakePicture(stub, mPictureMetering);
onTakePicture(stub, metering);
}
});
}
@ -1160,13 +960,13 @@ public abstract class CameraEngine implements
* @param stub a picture stub
*/
public final void takePictureSnapshot(final @NonNull PictureResult.Stub stub) {
LOG.i("takePictureSnapshot", "scheduling");
mHandler.run(new Runnable() {
// Save boolean before scheduling! See how Camera2Engine calls this with a temp value.
final boolean metering = mPictureSnapshotMetering;
mOrchestrator.scheduleStateful("take picture snapshot", CameraState.BIND,
new Runnable() {
@Override
public void run() {
LOG.i("takePictureSnapshot", "performing. BindState:",
getBindState(), "isTakingPicture:", isTakingPicture());
if (getBindState() < STATE_STARTED) return;
LOG.i("takePictureSnapshot:", "running. isTakingPicture:", isTakingPicture());
if (isTakingPicture()) return;
stub.location = mLocation;
stub.isSnapshot = true;
@ -1175,7 +975,7 @@ public abstract class CameraEngine implements
// Leave the other parameters to subclasses.
//noinspection ConstantConditions
AspectRatio ratio = AspectRatio.of(getPreviewSurfaceSize(Reference.OUTPUT));
onTakePictureSnapshot(stub, ratio, mPictureSnapshotMetering);
onTakePictureSnapshot(stub, ratio, metering);
}
});
}
@ -1202,13 +1002,10 @@ public abstract class CameraEngine implements
}
public final void takeVideo(final @NonNull VideoResult.Stub stub, final @NonNull File file) {
LOG.i("takeVideo", "scheduling");
mHandler.run(new Runnable() {
mOrchestrator.scheduleStateful("take video", CameraState.BIND, new Runnable() {
@Override
public void run() {
LOG.i("takeVideo", "performing. BindState:", getBindState(),
"isTakingVideo:", isTakingVideo());
if (getBindState() < STATE_STARTED) return;
LOG.i("takeVideo:", "running. isTakingVideo:", isTakingVideo());
if (isTakingVideo()) return;
if (mMode == Mode.PICTURE) {
throw new IllegalStateException("Can't record video while in PICTURE mode");
@ -1234,14 +1031,11 @@ public abstract class CameraEngine implements
*/
public final void takeVideoSnapshot(@NonNull final VideoResult.Stub stub,
@NonNull final File file) {
LOG.i("takeVideoSnapshot", "scheduling");
mHandler.run(new Runnable() {
mOrchestrator.scheduleStateful("take video snapshot", CameraState.BIND,
new Runnable() {
@Override
public void run() {
LOG.i("takeVideoSnapshot", "performing. BindState:", getBindState(),
"isTakingVideo:", isTakingVideo());
if (getBindState() < STATE_STARTED) return;
if (isTakingVideo()) return;
LOG.i("takeVideoSnapshot:", "running. isTakingVideo:", isTakingVideo());
stub.file = file;
stub.isSnapshot = true;
stub.videoCodec = mVideoCodec;
@ -1260,11 +1054,10 @@ public abstract class CameraEngine implements
}
public final void stopVideo() {
LOG.i("stopVideo", "posting");
mHandler.run(new Runnable() {
mOrchestrator.schedule("stop video", true, new Runnable() {
@Override
public void run() {
LOG.i("stopVideo", "executing.", "isTakingVideo?", isTakingVideo());
LOG.i("stopVideo", "running. isTakingVideo?", isTakingVideo());
onStopVideo();
}
});

@ -1,178 +0,0 @@
package com.otaliastudios.cameraview.engine;
import com.google.android.gms.tasks.Continuation;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.SuccessContinuation;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.CameraLogger;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
/**
* Represents one of the steps in the {@link CameraEngine} setup: for example, the engine step,
* the bind-to-surface step, and the preview step.
*
* A step is something that can be setup (started) or torn down (stopped), and
* steps can of course depend onto each other.
*
* The purpose of this class is to manage the step state (stopping, stopped, starting or started)
* and, more importantly, to perform START and STOP operations in such a way that they do not
* overlap. For example, if we're stopping, we're wait for stop to finish before starting again.
*
* This is an important condition for simplifying the engine code.
* Since Camera1, the only requirement was basically to use a single thread.
* Since Camera2, which has an asynchronous API, further care must be used.
*
* For this reason, we use Google's {@link Task} abstraction and only start new operations
* once the previous one has ended.
*
* <strong>This class is NOT thread safe!</string>
*/
class Step {
private static final String TAG = Step.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG);
interface Callback {
@NonNull
Executor getExecutor();
void handleException(@NonNull Exception exception);
}
static final int STATE_STOPPING = -1;
static final int STATE_STOPPED = 0;
static final int STATE_STARTING = 1;
static final int STATE_STARTED = 2;
private int state = STATE_STOPPED;
// To avoid dirty scenarios (e.g. calling stopXXX while XXX is starting),
// and since every operation can be asynchronous, we use some tasks for each step.
private Task<Void> task = Tasks.forResult(null);
private final String name;
private final Callback callback;
Step(@NonNull String name, @NonNull Callback callback) {
this.name = name.toUpperCase();
this.callback = callback;
}
int getState() {
return state;
}
@VisibleForTesting void setState(int newState) {
state = newState;
}
@NonNull
String getStateName() {
switch (state) {
case STATE_STOPPING: return name + "_STATE_STOPPING";
case STATE_STOPPED: return name + "_STATE_STOPPED";
case STATE_STARTING: return name + "_STATE_STARTING";
case STATE_STARTED: return name + "_STATE_STARTED";
}
return "null";
}
boolean isStoppingOrStopped() {
return state == STATE_STOPPING || state == STATE_STOPPED;
}
boolean isStartedOrStarting() {
return state == STATE_STARTING || state == STATE_STARTED;
}
boolean isStarted() {
return state == STATE_STARTED;
}
@NonNull
Task<Void> getTask() {
return task;
}
@SuppressWarnings({"SameParameterValue", "UnusedReturnValue"})
Task<Void> doStart(final boolean swallowExceptions, final @NonNull Callable<Task<Void>> op) {
return doStart(swallowExceptions, op, null);
}
Task<Void> doStart(final boolean swallowExceptions,
final @NonNull Callable<Task<Void>> op,
final @Nullable Runnable onStarted) {
LOG.i(name, "doStart", "Called. Enqueuing.");
task = task.continueWithTask(callback.getExecutor(), new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(@NonNull Task<Void> task) throws Exception {
LOG.i(name, "doStart", "About to start. Setting state to STARTING");
setState(STATE_STARTING);
return op.call().addOnFailureListener(callback.getExecutor(),
new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
LOG.w(name, "doStart", "Failed with error", e,
"Setting state to STOPPED");
setState(STATE_STOPPED);
if (!swallowExceptions) callback.handleException(e);
}
});
}
}).onSuccessTask(callback.getExecutor(), new SuccessContinuation<Void, Void>() {
@NonNull
@Override
public Task<Void> then(@Nullable Void aVoid) {
LOG.i(name, "doStart", "Succeeded! Setting state to STARTED");
setState(STATE_STARTED);
if (onStarted != null) onStarted.run();
return Tasks.forResult(null);
}
});
return task;
}
@SuppressWarnings("UnusedReturnValue")
Task<Void> doStop(final boolean swallowExceptions, final @NonNull Callable<Task<Void>> op) {
return doStop(swallowExceptions, op, null);
}
Task<Void> doStop(final boolean swallowExceptions,
final @NonNull Callable<Task<Void>> op,
final @Nullable Runnable onStopped) {
LOG.i(name, "doStop", "Called. Enqueuing.");
task = task.continueWithTask(callback.getExecutor(), new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(@NonNull Task<Void> task) throws Exception {
LOG.i(name, "doStop", "About to stop. Setting state to STOPPING");
state = STATE_STOPPING;
return op.call().addOnFailureListener(callback.getExecutor(),
new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
LOG.w(name, "doStop", "Failed with error", e,
"Setting state to STOPPED");
state = STATE_STOPPED;
if (!swallowExceptions) callback.handleException(e);
}
});
}
}).onSuccessTask(callback.getExecutor(), new SuccessContinuation<Void, Void>() {
@NonNull
@Override
public Task<Void> then(@Nullable Void aVoid) {
LOG.i(name, "doStop", "Succeeded! Setting state to STOPPED");
state = STATE_STOPPED;
if (onStopped != null) onStopped.run();
return Tasks.forResult(null);
}
});
return task;
}
}

@ -1,4 +1,4 @@
package com.otaliastudios.cameraview.engine;
package com.otaliastudios.cameraview.engine.action;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
@ -9,11 +9,12 @@ import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.engine.Camera2Engine;
import com.otaliastudios.cameraview.engine.action.ActionHolder;
import com.otaliastudios.cameraview.engine.action.BaseAction;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class LogAction extends BaseAction {
public class LogAction extends BaseAction {
private final static CameraLogger LOG
= CameraLogger.create(Camera2Engine.class.getSimpleName());

@ -0,0 +1,180 @@
package com.otaliastudios.cameraview.engine.orchestrator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
/**
* Schedules {@link com.otaliastudios.cameraview.engine.CameraEngine} actions,
* so that they always run on the same thread.
*
* We need to be extra careful (not as easy as posting on a Handler) because the engine
* has different states, and some actions will modify the engine state - turn it on or
* tear it down. Other actions might need a specific state to be executed.
* And most importantly, some actions will finish asynchronously, so subsequent actions
* should wait for the previous to finish, but without blocking the thread.
*/
@SuppressWarnings("WeakerAccess")
public class CameraOrchestrator {
protected static final String TAG = CameraOrchestrator.class.getSimpleName();
protected static final CameraLogger LOG = CameraLogger.create(TAG);
public interface Callback {
@NonNull
WorkerHandler getJobWorker(@NonNull String job);
void handleJobException(@NonNull String job, @NonNull Exception exception);
}
protected static class Token {
public final String name;
public final Task<Void> task;
private Token(@NonNull String name, @NonNull Task<Void> task) {
this.name = name;
this.task = task;
}
@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof Token && ((Token) obj).name.equals(name);
}
}
protected final Callback mCallback;
protected final ArrayDeque<Token> mJobs = new ArrayDeque<>();
protected final Object mLock = new Object();
private final Map<String, Runnable> mDelayedJobs = new HashMap<>();
public CameraOrchestrator(@NonNull Callback callback) {
mCallback = callback;
ensureToken();
}
@NonNull
public Task<Void> schedule(@NonNull String name,
boolean dispatchExceptions,
@NonNull final Runnable job) {
return schedule(name, dispatchExceptions, new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
job.run();
return Tasks.forResult(null);
}
});
}
@NonNull
public Task<Void> schedule(@NonNull final String name,
final boolean dispatchExceptions,
@NonNull final Callable<Task<Void>> job) {
LOG.i(name.toUpperCase(), "- Scheduling.");
final TaskCompletionSource<Void> source = new TaskCompletionSource<>();
final WorkerHandler handler = mCallback.getJobWorker(name);
synchronized (mLock) {
applyCompletionListener(mJobs.getLast().task, handler,
new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
synchronized (mLock) {
mJobs.removeFirst();
ensureToken();
}
try {
LOG.i(name.toUpperCase(), "- Executing.");
Task<Void> inner = job.call();
applyCompletionListener(inner, handler, new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
Exception e = task.getException();
LOG.i(name.toUpperCase(), "- Finished.", e);
if (e != null) {
if (dispatchExceptions) {
mCallback.handleJobException(name, e);
}
source.trySetException(e);
} else {
source.trySetResult(null);
}
}
});
} catch (Exception e) {
LOG.i(name.toUpperCase(), "- Finished.", e);
if (dispatchExceptions) mCallback.handleJobException(name, e);
source.trySetException(e);
}
}
});
mJobs.addLast(new Token(name, source.getTask()));
}
return source.getTask();
}
public void scheduleDelayed(@NonNull final String name,
long minDelay,
@NonNull final Runnable runnable) {
Runnable wrapper = new Runnable() {
@Override
public void run() {
schedule(name, true, runnable);
synchronized (mLock) {
if (mDelayedJobs.containsValue(this)) {
mDelayedJobs.remove(name);
}
}
}
};
synchronized (mLock) {
mDelayedJobs.put(name, wrapper);
mCallback.getJobWorker(name).post(minDelay, wrapper);
}
}
public void remove(@NonNull String name) {
synchronized (mLock) {
if (mDelayedJobs.get(name) != null) {
//noinspection ConstantConditions
mCallback.getJobWorker(name).remove(mDelayedJobs.get(name));
mDelayedJobs.remove(name);
}
Token token = new Token(name, Tasks.<Void>forResult(null));
//noinspection StatementWithEmptyBody
while (mJobs.remove(token)) { /* do nothing */ }
ensureToken();
}
}
private void ensureToken() {
synchronized (mLock) {
if (mJobs.isEmpty()) {
mJobs.add(new Token("BASE", Tasks.<Void>forResult(null)));
}
}
}
private static void applyCompletionListener(@NonNull final Task<Void> task,
@NonNull WorkerHandler handler,
@NonNull final OnCompleteListener<Void> listener) {
if (task.isComplete()) {
handler.run(new Runnable() {
@Override
public void run() {
listener.onComplete(task);
}
});
} else {
task.addOnCompleteListener(handler.getExecutor(), listener);
}
}
}

@ -0,0 +1,17 @@
package com.otaliastudios.cameraview.engine.orchestrator;
import androidx.annotation.NonNull;
public enum CameraState {
OFF(0), ENGINE(1), BIND(2), PREVIEW(3);
private int mState;
CameraState(int state) {
mState = state;
}
public boolean isAtLeast(@NonNull CameraState reference) {
return mState >= reference.mState;
}
}

@ -0,0 +1,117 @@
package com.otaliastudios.cameraview.engine.orchestrator;
import androidx.annotation.NonNull;
import com.google.android.gms.tasks.Continuation;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
/**
* A special {@link CameraOrchestrator} with special methods that deal with the
* {@link CameraState}.
*/
public class CameraStateOrchestrator extends CameraOrchestrator {
private CameraState mCurrentState = CameraState.OFF;
private CameraState mTargetState = CameraState.OFF;
private int mStateChangeCount = 0;
public CameraStateOrchestrator(@NonNull Callback callback) {
super(callback);
}
@NonNull
public CameraState getCurrentState() {
return mCurrentState;
}
@NonNull
public CameraState getTargetState() {
return mTargetState;
}
public boolean hasPendingStateChange() {
synchronized (mLock) {
for (Token token : mJobs) {
if (token.name.contains(" > ") && !token.task.isComplete()) {
return true;
}
}
return false;
}
}
@NonNull
public Task<Void> scheduleStateChange(@NonNull final CameraState fromState,
@NonNull final CameraState toState,
boolean dispatchExceptions,
@NonNull final Callable<Task<Void>> stateChange) {
final int changeCount = ++mStateChangeCount;
mTargetState = toState;
final boolean isTearDown = !toState.isAtLeast(fromState);
final String changeName = fromState.name() + " > " + toState.name();
return schedule(changeName, dispatchExceptions, new Callable<Task<Void>>() {
@Override
public Task<Void> call() throws Exception {
if (getCurrentState() != fromState) {
LOG.w(changeName.toUpperCase(), "- State mismatch, aborting. current:",
getCurrentState(), "from:", fromState, "to:", toState);
return Tasks.forResult(null);
} else {
Executor executor = mCallback.getJobWorker(changeName).getExecutor();
return stateChange.call().continueWithTask(executor,
new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(@NonNull Task<Void> task) {
if (task.isSuccessful() || isTearDown) {
mCurrentState = toState;
}
return task;
}
});
}
}
}).addOnCompleteListener(new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
if (changeCount == mStateChangeCount) {
mTargetState = mCurrentState;
}
}
});
}
@SuppressWarnings("UnusedReturnValue")
@NonNull
public Task<Void> scheduleStateful(@NonNull String name,
@NonNull final CameraState atLeast,
@NonNull final Runnable job) {
return schedule(name, true, new Runnable() {
@Override
public void run() {
if (getCurrentState().isAtLeast(atLeast)) {
job.run();
}
}
});
}
public void scheduleStatefulDelayed(@NonNull String name,
@NonNull final CameraState atLeast,
long delay,
@NonNull final Runnable job) {
scheduleDelayed(name, delay, new Runnable() {
@Override
public void run() {
if (getCurrentState().isAtLeast(atLeast)) {
job.run();
}
}
});
}
}
Loading…
Cancel
Save