Rewrite engine threading (#697)

* Create orchestrator, replacing Step class

* Rewrite tests, improve test tools

* Fix integration tests
pull/708/head
Mattia Iavarone 5 years ago committed by GitHub
parent fd17a8339e
commit 3db6fd3fc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      cameraview/build.gradle
  2. 16
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java
  3. 4
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraLoggerTest.java
  4. 18
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraUtilsTest.java
  5. 4
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java
  6. 20
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  7. 1
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/Camera1IntegrationTest.java
  8. 89
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java
  9. 17
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java
  10. 6
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/GestureFinderTest.java
  11. 4
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/PinchGestureFinderTest.java
  12. 6
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/ScrollGestureFinderTest.java
  13. 8
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/TapGestureFinderTest.java
  14. 28
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/GridLinesLayoutTest.java
  15. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfilesTest.java
  16. 8
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/utils/ImageHelperTest.java
  17. 40
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/utils/WorkerHandlerTest.java
  18. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/MarkerLayoutTest.java
  19. 28
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/CameraPreviewTest.java
  20. 149
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/Op.java
  21. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkExclude.java
  22. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkExcludeFilter.java
  23. 17
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  24. 167
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  25. 238
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  26. 645
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  27. 178
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Step.java
  28. 3
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/BaseAction.java
  29. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/LogAction.java
  30. 183
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraOrchestrator.java
  31. 17
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraState.java
  32. 117
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraStateOrchestrator.java
  33. 13
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/GridLinesLayout.java
  34. 111
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/Op.java
  35. 21
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java
  36. 7
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java
  37. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/TextureCameraPreview.java

@ -17,7 +17,7 @@ android {
versionCode 1
versionName project.version
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArgument "filter", "com.otaliastudios.cameraview.runner.SdkExcludeFilter"
testInstrumentationRunnerArgument "filter", "com.otaliastudios.cameraview.tools.SdkExcludeFilter"
}
buildTypes {
@ -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.

@ -11,7 +11,7 @@ import android.os.PowerManager;
import androidx.annotation.NonNull;
import androidx.test.platform.app.InstrumentationRegistry;
import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.tools.Op;
import org.junit.After;
import org.junit.AfterClass;
@ -103,7 +103,7 @@ public class BaseTest {
}
@NonNull
protected static Stubber doCountDown(final CountDownLatch latch) {
protected static Stubber doCountDown(@NonNull final CountDownLatch latch) {
return doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) {
@ -118,7 +118,7 @@ public class BaseTest {
return doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) {
op.end(response);
op.controller().end(response);
return null;
}
});
@ -126,14 +126,6 @@ public class BaseTest {
@NonNull
protected static <T> Stubber doEndOp(final Op<T> op, final int withReturnArgument) {
return doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) {
//noinspection unchecked
T o = (T) invocation.getArguments()[withReturnArgument];
op.end(o);
return null;
}
});
return op.controller().from(withReturnArgument);
}
}

@ -1,7 +1,7 @@
package com.otaliastudios.cameraview;
import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.tools.Op;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@ -109,7 +109,7 @@ public class CameraLoggerTest extends BaseTest {
CameraLogger.Logger mock = mock(CameraLogger.Logger.class);
CameraLogger.registerLogger(mock);
final Op<Throwable> op = new Op<>();
final Op<Throwable> op = new Op<>(false);
doEndOp(op, 3)
.when(mock)
.log(anyInt(), anyString(), anyString(), any(Throwable.class));

@ -6,11 +6,10 @@ import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Color;
import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.tools.Op;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@ -20,7 +19,6 @@ import org.junit.runner.RunWith;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
@ -51,21 +49,21 @@ public class CameraUtilsTest extends BaseTest {
private Op<String> writeAndReadString(@NonNull String data) {
final File file = new File(getContext().getFilesDir(), "string.txt");
final byte[] bytes = data.getBytes(Charset.forName("UTF-8"));
final Op<String> result = new Op<>(true);
final Op<String> result = new Op<>();
final FileCallback callback = new FileCallback() {
@Override
public void onFileReady(@Nullable File file) {
if (file == null) {
result.end(null);
result.controller().end(null);
} else {
// Read back the file.
try {
FileInputStream stream = new FileInputStream(file);
byte[] bytes = new byte[stream.available()];
stream.read(bytes);
result.end(new String(bytes, Charset.forName("UTF-8")));
result.controller().end(new String(bytes, Charset.forName("UTF-8")));
} catch (IOException e) {
result.end(null);
result.controller().end(null);
}
}
}
@ -94,12 +92,12 @@ public class CameraUtilsTest extends BaseTest {
source.compress(Bitmap.CompressFormat.PNG, 100, os);
final byte[] data = os.toByteArray();
final Op<Bitmap> decode = new Op<>(true);
final Op<Bitmap> decode = new Op<>();
if (async) {
final BitmapCallback callback = new BitmapCallback() {
@Override
public void onBitmapReady(Bitmap bitmap) {
decode.end(bitmap);
decode.controller().end(bitmap);
}
};
@ -121,7 +119,7 @@ public class CameraUtilsTest extends BaseTest {
} else {
result = CameraUtils.decodeBitmap(data);
}
decode.end(result);
decode.controller().end(result);
}
return decode;
}

@ -17,7 +17,7 @@ import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameProcessor;
import com.otaliastudios.cameraview.gesture.Gesture;
import com.otaliastudios.cameraview.gesture.GestureAction;
import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.tools.Op;
import com.otaliastudios.cameraview.engine.MockCameraEngine;
import com.otaliastudios.cameraview.markers.AutoFocusMarker;
import com.otaliastudios.cameraview.markers.AutoFocusTrigger;
@ -90,7 +90,7 @@ public class CameraViewCallbacksTest extends BaseTest {
camera.doInstantiatePreview();
camera.addCameraListener(listener);
camera.addFrameProcessor(processor);
op = new Op<>(true);
op = new Op<>();
}
});
}

@ -23,9 +23,9 @@ import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.controls.Preview;
import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.filter.Filters;
import com.otaliastudios.cameraview.filter.NoFilter;
import com.otaliastudios.cameraview.filters.DuotoneFilter;
import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameProcessor;
@ -41,7 +41,7 @@ import com.otaliastudios.cameraview.gesture.PinchGestureFinder;
import com.otaliastudios.cameraview.gesture.ScrollGestureFinder;
import com.otaliastudios.cameraview.gesture.TapGestureFinder;
import com.otaliastudios.cameraview.engine.MockCameraEngine;
import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.tools.Op;
import com.otaliastudios.cameraview.markers.AutoFocusMarker;
import com.otaliastudios.cameraview.markers.DefaultAutoFocusMarker;
import com.otaliastudios.cameraview.markers.MarkerLayout;
@ -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
@ -239,7 +239,7 @@ public class CameraViewTest extends BaseTest {
public void testGestureAction_capture() {
CameraOptions o = mock(CameraOptions.class);
mockController.setMockCameraOptions(o);
mockController.setMockEngineState(true);
mockController.setMockState(CameraState.PREVIEW);
MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
uiSync(new Runnable() {
@Override
@ -261,7 +261,7 @@ public class CameraViewTest extends BaseTest {
public void testGestureAction_focus() {
CameraOptions o = mock(CameraOptions.class);
mockController.setMockCameraOptions(o);
mockController.setMockEngineState(true);
mockController.setMockState(CameraState.PREVIEW);
MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
uiSync(new Runnable() {
@Override
@ -286,7 +286,7 @@ public class CameraViewTest extends BaseTest {
public void testGestureAction_zoom() {
CameraOptions o = mock(CameraOptions.class);
mockController.setMockCameraOptions(o);
mockController.setMockEngineState(true);
mockController.setMockState(CameraState.PREVIEW);
mockController.mZoomChanged = false;
MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
final FactorHolder factor = new FactorHolder();
@ -327,7 +327,7 @@ public class CameraViewTest extends BaseTest {
o.exposureCorrectionMaxValue = 10F;
o.exposureCorrectionMinValue = -10F;
mockController.setMockCameraOptions(o);
mockController.setMockEngineState(true);
mockController.setMockState(CameraState.PREVIEW);
mockController.mExposureCorrectionChanged = false;
MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
final FactorHolder factor = new FactorHolder();
@ -363,7 +363,7 @@ public class CameraViewTest extends BaseTest {
@Test
public void testGestureAction_filterControl1() {
mockController.setMockEngineState(true);
mockController.setMockState(CameraState.PREVIEW);
mockController.setMockCameraOptions(mock(CameraOptions.class));
DuotoneFilter filter = new DuotoneFilter(); // supports two parameters
filter.setParameter1(0F);
@ -405,7 +405,7 @@ public class CameraViewTest extends BaseTest {
@Test
public void testGestureAction_filterControl2() {
mockController.setMockEngineState(true);
mockController.setMockState(CameraState.PREVIEW);
mockController.setMockCameraOptions(mock(CameraOptions.class));
DuotoneFilter filter = new DuotoneFilter(); // supports two parameters
filter.setParameter2(0F);
@ -891,7 +891,7 @@ public class CameraViewTest extends BaseTest {
cameraView.mMarkerLayout = markerLayout;
final PointF point = new PointF(0, 0);
final PointF[] points = new PointF[]{ point };
final Op<Boolean> op = new Op<>(true);
final Op<Boolean> op = new Op<>();
doEndOp(op, true).when(markerLayout).onEvent(MarkerLayout.TYPE_AUTOFOCUS, points);
cameraView.mCameraCallbacks.dispatchOnFocusStart(Gesture.TAP, point);
assertNotNull(op.await(100));

@ -2,6 +2,7 @@ package com.otaliastudios.cameraview.engine;
import com.otaliastudios.cameraview.controls.Engine;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.annotation.NonNull;

@ -25,9 +25,10 @@ import com.otaliastudios.cameraview.controls.Hdr;
import com.otaliastudios.cameraview.controls.Mode;
import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameProcessor;
import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.tools.Op;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.size.Size;
@ -110,12 +111,12 @@ public abstract class CameraIntegrationTest extends BaseTest {
// Ensure that controller exceptions are thrown on this thread (not on the UI thread).
// TODO this makes debugging for wrong tests very hard, as we don't get the exception
// unless waitForUiException() is called.
uiExceptionOp = new Op<>(true);
uiExceptionOp = new Op<>();
WorkerHandler crashThread = WorkerHandler.get("CrashThread");
crashThread.getThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
uiExceptionOp.end(e);
uiExceptionOp.controller().end(e);
}
});
controller.mCrashHandler = crashThread.getHandler();
@ -139,7 +140,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
private CameraOptions openSync(boolean expectSuccess) {
camera.open();
final Op<CameraOptions> open = new Op<>(true);
final Op<CameraOptions> open = new Op<>();
doEndOp(open, 0).when(listener).onCameraOpened(any(CameraOptions.class));
CameraOptions result = open.await(DELAY);
if (expectSuccess) {
@ -156,13 +157,12 @@ public abstract class CameraIntegrationTest extends BaseTest {
// Extra wait for the bind and preview state, so we run tests in a fully operational
// state. If we didn't do so, we could have null values, for example, in getPictureSize
// or in getSnapshotSize.
while (controller.getBindState() != CameraEngine.STATE_STARTED) {}
while (controller.getPreviewState() != CameraEngine.STATE_STARTED) {}
while (controller.getState() != CameraState.PREVIEW) {}
}
private void closeSync(boolean expectSuccess) {
camera.close();
final Op<Boolean> close = new Op<>(true);
final Op<Boolean> close = new Op<>();
doEndOp(close, true).when(listener).onCameraClosed();
Boolean result = close.await(DELAY);
if (expectSuccess) {
@ -180,7 +180,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
doCountDown(onVideoRecordingEnd).when(listener).onVideoRecordingEnd();
// Op for onVideoTaken.
final Op<VideoResult> video = new Op<>(true);
final Op<VideoResult> video = new Op<>();
doEndOp(video, 0).when(listener).onVideoTaken(any(VideoResult.class));
doEndOp(video, null).when(listener).onCameraError(argThat(new ArgumentMatcher<CameraException>() {
@Override
@ -218,7 +218,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Nullable
private PictureResult waitForPictureResult(boolean expectSuccess) {
final Op<PictureResult> pic = new Op<>(true);
final Op<PictureResult> pic = new Op<>();
doEndOp(pic, 0).when(listener).onPictureTaken(any(PictureResult.class));
doEndOp(pic, null).when(listener).onCameraError(argThat(new ArgumentMatcher<CameraException>() {
@Override
@ -240,7 +240,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
}
private void takeVideoSync(boolean expectSuccess, int duration) {
final Op<Boolean> op = new Op<>(true);
final Op<Boolean> op = new Op<>();
doEndOp(op, true).when(listener).onVideoRecordingStart();
doEndOp(op, false).when(listener).onCameraError(argThat(new ArgumentMatcher<CameraException>() {
@Override
@ -269,7 +269,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
}
private void takeVideoSnapshotSync(boolean expectSuccess, int duration) {
final Op<Boolean> op = new Op<>(true);
final Op<Boolean> op = new Op<>();
doEndOp(op, true).when(listener).onVideoRecordingStart();
doEndOp(op, false).when(listener).onCameraError(argThat(new ArgumentMatcher<CameraException>() {
@Override
@ -296,14 +296,11 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test
public void testOpenClose() {
// Starting and stopping are hard to get since they happen on another thread.
assertEquals(controller.getEngineState(), CameraEngine.STATE_STOPPED);
assertEquals(controller.getState(), CameraState.OFF);
openSync(true);
assertEquals(controller.getEngineState(), CameraEngine.STATE_STARTED);
assertTrue(controller.getState().isAtLeast(CameraState.ENGINE));
closeSync(true);
assertEquals(controller.getEngineState(), CameraEngine.STATE_STOPPED);
assertEquals(controller.getState(), CameraState.OFF);
}
@Test
@ -389,12 +386,11 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test
public void testSetZoom() {
CameraOptions options = openSync(true);
controller.mZoomOp.listen();
float oldValue = camera.getZoom();
float newValue = 0.65f;
camera.setZoom(newValue);
controller.mZoomOp.await(500);
Op<Void> op = new Op<>(controller.mZoomTask);
op.await(500);
if (options.isZoomSupported()) {
assertEquals(newValue, camera.getZoom(), 0f);
@ -406,12 +402,11 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test
public void testSetExposureCorrection() {
CameraOptions options = openSync(true);
controller.mExposureCorrectionOp.listen();
float oldValue = camera.getExposureCorrection();
float newValue = options.getExposureCorrectionMaxValue();
camera.setExposureCorrection(newValue);
controller.mExposureCorrectionOp.await(300);
Op<Void> op = new Op<>(controller.mExposureCorrectionTask);
op.await(300);
if (options.isExposureCorrectionSupported()) {
assertEquals(newValue, camera.getExposureCorrection(), 0f);
@ -426,9 +421,10 @@ public abstract class CameraIntegrationTest extends BaseTest {
Flash[] values = Flash.values();
Flash oldValue = camera.getFlash();
for (Flash value : values) {
controller.mFlashOp.listen();
camera.setFlash(value);
controller.mFlashOp.await(300);
Op<Void> op = new Op<>(controller.mFlashTask);
op.await(300);
if (options.supports(value)) {
assertEquals(camera.getFlash(), value);
oldValue = value;
@ -444,9 +440,9 @@ public abstract class CameraIntegrationTest extends BaseTest {
WhiteBalance[] values = WhiteBalance.values();
WhiteBalance oldValue = camera.getWhiteBalance();
for (WhiteBalance value : values) {
controller.mWhiteBalanceOp.listen();
camera.setWhiteBalance(value);
controller.mWhiteBalanceOp.await(300);
Op<Void> op = new Op<>(controller.mWhiteBalanceTask);
op.await(300);
if (options.supports(value)) {
assertEquals(camera.getWhiteBalance(), value);
oldValue = value;
@ -462,9 +458,9 @@ public abstract class CameraIntegrationTest extends BaseTest {
Hdr[] values = Hdr.values();
Hdr oldValue = camera.getHdr();
for (Hdr value : values) {
controller.mHdrOp.listen();
camera.setHdr(value);
controller.mHdrOp.await(300);
Op<Void> op = new Op<>(controller.mHdrTask);
op.await(300);
if (options.supports(value)) {
assertEquals(camera.getHdr(), value);
oldValue = value;
@ -487,9 +483,9 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test
public void testSetLocation() {
openSync(true);
controller.mLocationOp.listen();
camera.setLocation(10d, 2d);
controller.mLocationOp.await(300);
Op<Void> op = new Op<>(controller.mLocationTask);
op.await(300);
assertNotNull(camera.getLocation());
assertEquals(camera.getLocation().getLatitude(), 10d, 0d);
assertEquals(camera.getLocation().getLongitude(), 2d, 0d);
@ -499,19 +495,19 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test
public void testSetPreviewFrameRate() {
openSync(true);
controller.mPreviewFrameRateOp.listen();
camera.setPreviewFrameRate(30);
controller.mPreviewFrameRateOp.await(300);
Op<Void> op = new Op<>(controller.mPreviewFrameRateTask);
op.await(300);
assertEquals(camera.getPreviewFrameRate(), 30, 0);
}
@Test
public void testSetPlaySounds() {
controller.mPlaySoundsOp.listen();
boolean oldValue = camera.getPlaySounds();
boolean newValue = !oldValue;
camera.setPlaySounds(newValue);
controller.mPlaySoundsOp.await(300);
Op<Void> op = new Op<>(controller.mPlaySoundsTask);
op.await(300);
if (controller instanceof Camera1Engine) {
Camera1Engine camera1Engine = (Camera1Engine) controller;
@ -647,7 +643,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
public void testStartAutoFocus() {
CameraOptions o = openSync(true);
final Op<PointF> focus = new Op<>(true);
final Op<PointF> focus = new Op<>();
doEndOp(focus, 0).when(listener).onAutoFocusStart(any(PointF.class));
camera.startAutoFocus(1, 1);
@ -664,7 +660,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
public void testStopAutoFocus() {
CameraOptions o = openSync(true);
final Op<PointF> focus = new Op<>(true);
final Op<PointF> focus = new Op<>();
doEndOp(focus, 1).when(listener).onAutoFocusEnd(anyBoolean(), any(PointF.class));
camera.startAutoFocus(1, 1);
@ -809,25 +805,24 @@ public abstract class CameraIntegrationTest extends BaseTest {
//region Picture Formats
// TODO this fails because setPictureFormat triggers a restart() and takePicture can be called
// in the middle of the restart, failing because the engine is not properly set up. To fix this
// we would have to change the whole CameraEngine threading scheme.
// @Test
@SuppressWarnings("ConstantConditions")
@Test
public void testPictureFormat_DNG() {
openSync(true);
if (camera.getCameraOptions().supports(PictureFormat.DNG)) {
Op<Boolean> op = new Op<>();
doEndOp(op, true).when(listener).onCameraOpened(any(CameraOptions.class));
camera.setPictureFormat(PictureFormat.DNG);
assertNotNull(op.await(2000));
camera.takePicture();
PictureResult result = waitForPictureResult(true);
// assert that result.getData() is a DNG file:
// We can use the first 4 bytes assuming they are the same as a TIFF file
// https://en.wikipedia.org/wiki/List_of_file_signatures
// https://en.wikipedia.org/wiki/List_of_file_signatures 73, 73, 42, 0
byte[] b = result.getData();
boolean isII = b[0] == 'I' && b[1] == 'I' && b[2] == '*' && b[3] == '.';
boolean isMM = b[0] == 'M' && b[1] == 'M' && b[2] == '.' && b[3] == '*';
if (!isII && !isMM) {
throw new RuntimeException("Not a DNG file.");
}
boolean isII = b[0] == 'I' && b[1] == 'I' && b[2] == '*' && b[3] == 0;
boolean isMM = b[0] == 'M' && b[1] == 'M' && b[2] == 0 && b[3] == '*';
assertTrue(isII || isMM);
}
}

@ -12,6 +12,7 @@ import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
import com.otaliastudios.cameraview.frame.FrameManager;
import com.otaliastudios.cameraview.gesture.Gesture;
import com.otaliastudios.cameraview.controls.Hdr;
@ -24,6 +25,7 @@ import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
public class MockCameraEngine extends CameraEngine {
@ -80,8 +82,19 @@ public class MockCameraEngine extends CameraEngine {
mPreviewStreamSize = size;
}
public void setMockEngineState(boolean started) {
mEngineStep.setState(started ? STATE_STARTED : STATE_STOPPED);
public void setMockState(@NonNull CameraState state) {
Task<Void> change = mOrchestrator.scheduleStateChange(getState(),
state,
false,
new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
return Tasks.forResult(null);
}
});
try {
Tasks.await(change);
} catch (Exception ignore) {}
}
@Override

@ -15,7 +15,7 @@ import android.widget.FrameLayout;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.TestActivity;
import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.tools.Op;
import org.hamcrest.Matchers;
import org.junit.Before;
@ -49,12 +49,12 @@ public abstract class GestureFinderTest<T extends GestureFinder> extends BaseTes
finder.setActive(true);
a.inflate(layout);
touchOp = new Op<>();
touchOp = new Op<>(false);
layout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
boolean found = finder.onTouchEvent(motionEvent);
if (found) touchOp.end(finder.getGesture());
if (found) touchOp.controller().end(finder.getGesture());
return true;
}
});

@ -6,7 +6,7 @@ import androidx.test.espresso.ViewAction;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.runner.SdkExclude;
import com.otaliastudios.cameraview.tools.SdkExclude;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -52,7 +52,7 @@ public class PinchGestureFinderTest extends GestureFinderTest<PinchGestureFinder
private void testPinch(ViewAction action, boolean increasing) {
touchOp.listen();
touchOp.start();
touchOp.controller().start();
onLayout().perform(action);
Gesture found = touchOp.await(10000);
assertNotNull(found);

@ -6,7 +6,7 @@ import androidx.test.espresso.ViewAction;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.runner.SdkExclude;
import com.otaliastudios.cameraview.tools.SdkExclude;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -49,7 +49,7 @@ public class ScrollGestureFinderTest extends GestureFinderTest<ScrollGestureFind
public void testScrollDisabled() {
finder.setActive(false);
touchOp.listen();
touchOp.start();
touchOp.controller().start();
onLayout().perform(swipeUp());
Gesture found = touchOp.await(WAIT);
assertNull(found);
@ -57,7 +57,7 @@ public class ScrollGestureFinderTest extends GestureFinderTest<ScrollGestureFind
private void testScroll(ViewAction scroll, Gesture expected, boolean increasing) {
touchOp.listen();
touchOp.start();
touchOp.controller().start();
onLayout().perform(scroll);
Gesture found = touchOp.await(WAIT);
assertEquals(found, expected);

@ -11,7 +11,7 @@ import androidx.test.filters.SmallTest;
import android.view.InputDevice;
import android.view.MotionEvent;
import com.otaliastudios.cameraview.runner.SdkExclude;
import com.otaliastudios.cameraview.tools.SdkExclude;
import com.otaliastudios.cameraview.size.Size;
import org.junit.Test;
@ -45,7 +45,7 @@ public class TapGestureFinderTest extends GestureFinderTest<TapGestureFinder> {
@Test
public void testTap() {
touchOp.listen();
touchOp.start();
touchOp.controller().start();
GeneralClickAction a = new GeneralClickAction(
Tap.SINGLE, GeneralLocation.CENTER, Press.FINGER,
InputDevice.SOURCE_UNKNOWN, MotionEvent.BUTTON_PRIMARY);
@ -62,7 +62,7 @@ public class TapGestureFinderTest extends GestureFinderTest<TapGestureFinder> {
public void testTapWhileDisabled() {
finder.setActive(false);
touchOp.listen();
touchOp.start();
touchOp.controller().start();
onLayout().perform(click());
Gesture found = touchOp.await(500);
assertNull(found);
@ -71,7 +71,7 @@ public class TapGestureFinderTest extends GestureFinderTest<TapGestureFinder> {
@Test
public void testLongTap() {
touchOp.listen();
touchOp.start();
touchOp.controller().start();
GeneralClickAction a = new GeneralClickAction(
Tap.LONG, GeneralLocation.CENTER, Press.FINGER,
InputDevice.SOURCE_UNKNOWN, MotionEvent.BUTTON_PRIMARY);

@ -4,7 +4,9 @@ package com.otaliastudios.cameraview.internal;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.TestActivity;
import com.otaliastudios.cameraview.controls.Grid;
import com.otaliastudios.cameraview.tools.Op;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.rule.ActivityTestRule;
@ -15,6 +17,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
@ -25,6 +28,14 @@ public class GridLinesLayoutTest extends BaseTest {
private GridLinesLayout layout;
@NonNull
private Op<Integer> getDrawOp() {
final Op<Integer> op = new Op<>();
layout.callback = mock(GridLinesLayout.DrawCallback.class);
doEndOp(op, 0).when(layout.callback).onDraw(anyInt());
return op;
}
@Before
public void setUp() {
uiSync(new Runnable() {
@ -33,18 +44,17 @@ public class GridLinesLayoutTest extends BaseTest {
TestActivity a = rule.getActivity();
layout = new GridLinesLayout(a);
layout.setGridMode(Grid.OFF);
layout.drawOp.listen();
Op<Integer> op = getDrawOp();
a.getContentView().addView(layout);
op.await(1000);
}
});
// Wait for first draw.
layout.drawOp.await(1000);
}
private int setGridAndWait(Grid value) {
layout.drawOp.listen();
layout.setGridMode(value);
Integer result = layout.drawOp.await(1000);
Op<Integer> op = getDrawOp();
Integer result = op.await(1000);
assertNotNull(result);
return result;
}
@ -52,25 +62,25 @@ public class GridLinesLayoutTest extends BaseTest {
@Test
public void testOff() {
int linesDrawn = setGridAndWait(Grid.OFF);
assertEquals(linesDrawn, 0);
assertEquals(0, linesDrawn);
}
@Test
public void test3x3() {
int linesDrawn = setGridAndWait(Grid.DRAW_3X3);
assertEquals(linesDrawn, 2);
assertEquals(2, linesDrawn);
}
@Test
public void testPhi() {
int linesDrawn = setGridAndWait(Grid.DRAW_PHI);
assertEquals(linesDrawn, 2);
assertEquals(2, linesDrawn);
}
@Test
public void test4x4() {
int linesDrawn = setGridAndWait(Grid.DRAW_4X4);
assertEquals(linesDrawn, 3);
assertEquals(3, linesDrawn);
}
}

@ -8,7 +8,7 @@ import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.CameraUtils;
import com.otaliastudios.cameraview.runner.SdkExclude;
import com.otaliastudios.cameraview.tools.SdkExclude;
import com.otaliastudios.cameraview.size.Size;
import org.junit.Test;

@ -18,11 +18,11 @@ import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.runner.SdkExclude;
import com.otaliastudios.cameraview.tools.Op;
import com.otaliastudios.cameraview.tools.SdkExclude;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -45,12 +45,12 @@ public class ImageHelperTest extends BaseTest {
private Image getImage() {
ImageReader reader = ImageReader.newInstance(100, 100, ImageFormat.YUV_420_888, 1);
Surface readerSurface = reader.getSurface();
final Op<Image> imageOp = new Op<>(true);
final Op<Image> imageOp = new Op<>();
reader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireLatestImage();
if (image != null) imageOp.end(image);
if (image != null) imageOp.controller().end(image);
}
}, new Handler(Looper.getMainLooper()));

@ -4,6 +4,7 @@ package com.otaliastudios.cameraview.internal.utils;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.tools.Op;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -13,7 +14,6 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
@ -43,7 +43,7 @@ public class WorkerHandlerTest extends BaseTest {
return new Runnable() {
@Override
public void run() {
op.end(true);
op.controller().end(true);
}
};
}
@ -53,7 +53,7 @@ public class WorkerHandlerTest extends BaseTest {
return new Callable<Boolean>() {
@Override
public Boolean call() {
op.end(true);
op.controller().end(true);
return true;
}
};
@ -77,7 +77,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test
public void testFallbackExecute() {
final Op<Boolean> op = new Op<>(true);
final Op<Boolean> op = new Op<>();
WorkerHandler.execute(getRunnableForOp(op));
waitOp(op);
}
@ -85,7 +85,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test
public void testPostRunnable() {
WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true);
final Op<Boolean> op = new Op<>();
handler.post(getRunnableForOp(op));
waitOp(op);
}
@ -93,7 +93,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test
public void testPostCallable() {
WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true);
final Op<Boolean> op = new Op<>();
handler.post(getCallableForOp(op));
waitOp(op);
}
@ -110,7 +110,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test
public void testRunRunnable_background() {
WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true);
final Op<Boolean> op = new Op<>();
handler.run(getRunnableForOp(op));
waitOp(op);
}
@ -118,14 +118,14 @@ public class WorkerHandlerTest extends BaseTest {
@Test
public void testRunRunnable_sameThread() {
final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op1 = new Op<>(true);
final Op<Boolean> op2 = new Op<>(true);
final Op<Boolean> op1 = new Op<>();
final Op<Boolean> op2 = new Op<>();
handler.post(new Runnable() {
@Override
public void run() {
handler.run(getRunnableForOp(op2));
assertTrue(op2.await(0)); // Do not wait.
op1.end(true);
op1.controller().end(true);
}
});
waitOp(op1);
@ -134,7 +134,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test
public void testRunCallable_background() {
WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true);
final Op<Boolean> op = new Op<>();
handler.run(getCallableForOp(op));
waitOp(op);
}
@ -142,14 +142,14 @@ public class WorkerHandlerTest extends BaseTest {
@Test
public void testRunCallable_sameThread() {
final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op1 = new Op<>(true);
final Op<Boolean> op2 = new Op<>(true);
final Op<Boolean> op1 = new Op<>();
final Op<Boolean> op2 = new Op<>();
handler.post(new Runnable() {
@Override
public void run() {
handler.run(getCallableForOp(op2));
assertTrue(op2.await(0)); // Do not wait.
op1.end(true);
op1.controller().end(true);
}
});
waitOp(op1);
@ -158,14 +158,14 @@ public class WorkerHandlerTest extends BaseTest {
@Test
public void testRunCallable_sameThread_throws() {
final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true);
final Op<Boolean> op = new Op<>();
handler.post(new Runnable() {
@Override
public void run() {
Task<Void> task = handler.run(getThrowCallable());
assertTrue(task.isComplete()); // Already complete
assertFalse(task.isSuccessful());
op.end(true);
op.controller().end(true);
}
});
waitOp(op);
@ -174,7 +174,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test
public void testPostDelayed_tooEarly() {
final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true);
final Op<Boolean> op = new Op<>();
handler.post(1000, getRunnableForOp(op));
assertNull(op.await(500));
}
@ -182,7 +182,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test
public void testPostDelayed() {
final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true);
final Op<Boolean> op = new Op<>();
handler.post(1000, getRunnableForOp(op));
assertNotNull(op.await(2000));
}
@ -190,7 +190,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test
public void testRemove() {
final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true);
final Op<Boolean> op = new Op<>();
Runnable runnable = getRunnableForOp(op);
handler.post(1000, runnable);
handler.remove(runnable);
@ -210,7 +210,7 @@ public class WorkerHandlerTest extends BaseTest {
public void testExecutor() {
final WorkerHandler handler = WorkerHandler.get("handler");
Executor executor = handler.getExecutor();
final Op<Boolean> op = new Op<>(true);
final Op<Boolean> op = new Op<>();
executor.execute(getRunnableForOp(op));
waitOp(op);
}

@ -9,7 +9,7 @@ import android.view.ViewGroup;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.TestActivity;
import com.otaliastudios.cameraview.runner.SdkExclude;
import com.otaliastudios.cameraview.tools.SdkExclude;
import org.junit.Assert;
import org.junit.Before;

@ -7,7 +7,7 @@ import android.view.ViewGroup;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.TestActivity;
import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.tools.Op;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
@ -42,8 +42,8 @@ public abstract class CameraPreviewTest<T extends CameraPreview> extends BaseTes
@Before
public void setUp() {
available = new Op<>(true);
destroyed = new Op<>(true);
available = new Op<>();
destroyed = new Op<>();
uiSync(new Runnable() {
@Override
@ -55,7 +55,7 @@ public abstract class CameraPreviewTest<T extends CameraPreview> extends BaseTes
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) {
if (available != null) available.end(true);
if (available != null) available.controller().end(true);
return null;
}
}).when(callback).onSurfaceAvailable();
@ -63,7 +63,7 @@ public abstract class CameraPreviewTest<T extends CameraPreview> extends BaseTes
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) {
if (destroyed != null) destroyed.end(true);
if (destroyed != null) destroyed.controller().end(true);
return null;
}
}).when(callback).onSurfaceDestroyed();
@ -153,17 +153,23 @@ public abstract class CameraPreviewTest<T extends CameraPreview> extends BaseTes
// Since desired is 'desired', let's fake a new view size that is consistent with it.
// Ensure crop is not happening anymore.
preview.mCropOp.listen();
preview.dispatchOnSurfaceSizeChanged((int) (50f * desired), 50); // Wait...
preview.mCropOp.await();
preview.mCropCallback = mock(CameraPreview.CropCallback.class);
Op<Void> op = new Op<>();
doEndOp(op, null).when(preview.mCropCallback).onCrop();
preview.dispatchOnSurfaceSizeChanged((int) (50f * desired), 50);
op.await(); // Wait...
assertEquals(desired, getViewAspectRatioWithScale(), 0.01f);
assertFalse(preview.isCropping());
}
private void setDesiredAspectRatio(float desiredAspectRatio) {
preview.mCropOp.listen();
preview.setStreamSize((int) (10f * desiredAspectRatio), 10); // Wait...
preview.mCropOp.await();
preview.mCropCallback = mock(CameraPreview.CropCallback.class);
Op<Void> op = new Op<>();
doEndOp(op, null).when(preview.mCropCallback).onCrop();
preview.setStreamSize((int) (10f * desiredAspectRatio), 10);
op.await(); // Wait...
assertEquals(desiredAspectRatio, getViewAspectRatioWithScale(), 0.01f);
}

@ -0,0 +1,149 @@
package com.otaliastudios.cameraview.tools;
import androidx.annotation.NonNull;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;
import com.otaliastudios.cameraview.controls.Control;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.mockito.stubbing.Stubber;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* A naive implementation of {@link java.util.concurrent.CountDownLatch}
* to help in testing.
*/
public class Op<T> {
public class Controller {
private int mToBeIgnored;
private Controller() { }
/** Op owner method: notifies the action started. */
public void start() {
if (!isListening()) mToBeIgnored++;
}
/** Op owner method: notifies the action ended. */
public void end(T result) {
if (mToBeIgnored > 0) {
mToBeIgnored--;
return;
}
if (isListening()) { // Should be always true.
mResult = result;
mLatch.countDown();
}
}
public void from(@NonNull Task<T> task) {
start();
task.addOnSuccessListener(new OnSuccessListener<T>() {
@Override
public void onSuccess(T result) {
end(result);
}
});
}
@NonNull
public Stubber from(final int invocationArgument) {
return Mockito.doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) {
//noinspection unchecked
T o = (T) invocation.getArguments()[invocationArgument];
start();
end(o);
return null;
}
});
}
}
private CountDownLatch mLatch;
private Controller mController = new Controller();
private T mResult;
/**
* Listeners should:
* - call {@link #listen()} to notify they are interested in the next action
* - call {@link #await()} to know when the action is performed.
*
* Op owners should:
* - call {@link Controller#start()} when task started
* - call {@link Controller#end(Object)} when task ends
*/
public Op() {
this(true);
}
public Op(boolean startListening) {
if (startListening) listen();
}
public Op(@NonNull Task<T> task) {
listen();
controller().from(task);
}
private boolean isListening() {
return mLatch != null;
}
/**
* Listener method: notifies we are interested in the next action.
*/
public void listen() {
if (isListening()) throw new RuntimeException("Should not happen.");
mResult = null;
mLatch = new CountDownLatch(1);
}
/**
* Listener method: waits for next task action to end.
* @param millis milliseconds
* @return the action result
*/
public T await(long millis) {
return await(millis, TimeUnit.MILLISECONDS);
}
/**
* Listener method: waits 1 minute for next task action to end.
* @return the action result
*/
public T await() {
return await(1, TimeUnit.MINUTES);
}
/**
* Listener method: waits for next task action to end.
* @param time time
* @param unit the time unit
* @return the action result
*/
private T await(long time, @NonNull TimeUnit unit) {
try {
mLatch.await(time, unit);
} catch (Exception e) {
e.printStackTrace();
}
T result = mResult;
mResult = null;
mLatch = null;
return result;
}
@NonNull
public Controller controller() {
return mController;
}
}

@ -1,4 +1,4 @@
package com.otaliastudios.cameraview.runner;
package com.otaliastudios.cameraview.tools;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;

@ -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;
}
}

@ -39,6 +39,7 @@ public abstract class BaseAction implements Action {
@Override
public final void start(@NonNull ActionHolder holder) {
this.holder = holder;
holder.addAction(this);
if (holder.getLastResult(this) != null) {
onStart(holder);
@ -64,6 +65,8 @@ public abstract class BaseAction implements Action {
*/
@CallSuper
protected void onStart(@NonNull ActionHolder holder) {
// Repeating holder assignment here (already in start()) because we NEED it in start()
// but some special actions will not call start() at all for their children.
this.holder = holder;
// Overrideable
}

@ -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,183 @@
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;
import java.util.concurrent.CancellationException;
/**
* 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 if (task.isCanceled()) {
source.trySetException(new CancellationException());
} 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.forCanceled();
} 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();
}
}
});
}
}

@ -15,7 +15,6 @@ import android.util.TypedValue;
import android.view.View;
import com.otaliastudios.cameraview.controls.Grid;
import com.otaliastudios.cameraview.internal.utils.Op;
/**
* A layout overlay that draws grid lines based on the {@link Grid} parameter.
@ -32,8 +31,11 @@ public class GridLinesLayout extends View {
private ColorDrawable vert;
private final float width;
@VisibleForTesting
Op<Integer> drawOp = new Op<>();
interface DrawCallback {
void onDraw(int lines);
}
@VisibleForTesting DrawCallback callback;
public GridLinesLayout(@NonNull Context context) {
this(context, null);
@ -117,7 +119,6 @@ public class GridLinesLayout extends View {
@Override
protected void onDraw(@NonNull Canvas canvas) {
super.onDraw(canvas);
drawOp.start();
int count = getLineCount();
for (int n = 0; n < count; n++) {
float pos = getLinePosition(n);
@ -132,6 +133,8 @@ public class GridLinesLayout extends View {
vert.draw(canvas);
canvas.translate(- pos * getWidth(), 0);
}
drawOp.end(count);
if (callback != null) {
callback.onDraw(count);
}
}
}

@ -1,111 +0,0 @@
package com.otaliastudios.cameraview.internal.utils;
import androidx.annotation.NonNull;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* A naive implementation of {@link java.util.concurrent.CountDownLatch}
* to help in testing.
*/
public class Op<T> {
private CountDownLatch mLatch;
private T mResult;
private int mCount;
/**
* Creates an empty task.
*
* Listeners should:
* - call {@link #listen()} to notify they are interested in the next action
* - call {@link #await()} to know when the action is performed.
*
* Op owners should:
* - call {@link #start()} when task started
* - call {@link #end(Object)} when task ends
*/
public Op() { }
/**
* Creates an empty task and starts listening.
* @param startListening whether to call listen
*/
public Op(boolean startListening) {
if (startListening) listen();
}
private boolean isListening() {
return mLatch != null;
}
/**
* Op owner method: notifies the action started.
*/
public void start() {
if (!isListening()) mCount++;
}
/**
* Op owner method: notifies the action ended.
* @param result the action result
*/
public void end(T result) {
if (mCount > 0) {
mCount--;
return;
}
if (isListening()) { // Should be always true.
mResult = result;
mLatch.countDown();
}
}
/**
* Listener method: notifies we are interested in the next action.
*/
public void listen() {
if (isListening()) throw new RuntimeException("Should not happen.");
mResult = null;
mLatch = new CountDownLatch(1);
}
/**
* Listener method: waits for next task action to end.
* @param millis milliseconds
* @return the action result
*/
public T await(long millis) {
return await(millis, TimeUnit.MILLISECONDS);
}
/**
* Listener method: waits 1 minute for next task action to end.
* @return the action result
*/
public T await() {
return await(1, TimeUnit.MINUTES);
}
/**
* Listener method: waits for next task action to end.
* @param time time
* @param unit the time unit
* @return the action result
*/
private T await(long time, @NonNull TimeUnit unit) {
try {
mLatch.await(time, unit);
} catch (Exception e) {
e.printStackTrace();
}
T result = mResult;
mResult = null;
mLatch = null;
return result;
}
}

@ -18,7 +18,6 @@ import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.size.Size;
/**
@ -56,8 +55,11 @@ public abstract class CameraPreview<T extends View, Output> {
void onSurfaceDestroyed();
}
@VisibleForTesting
Op<Void> mCropOp = new Op<>();
protected interface CropCallback {
void onCrop();
}
@VisibleForTesting CropCallback mCropCallback;
private SurfaceCallback mSurfaceCallback;
private T mView;
boolean mCropping;
@ -152,7 +154,7 @@ public abstract class CameraPreview<T extends View, Output> {
mInputStreamWidth = width;
mInputStreamHeight = height;
if (mInputStreamWidth > 0 && mInputStreamHeight > 0) {
crop(mCropOp);
crop(mCropCallback);
}
}
@ -194,7 +196,7 @@ public abstract class CameraPreview<T extends View, Output> {
mOutputSurfaceWidth = width;
mOutputSurfaceHeight = height;
if (mOutputSurfaceWidth > 0 && mOutputSurfaceHeight > 0) {
crop(mCropOp);
crop(mCropCallback);
}
if (mSurfaceCallback != null) {
mSurfaceCallback.onSurfaceAvailable();
@ -213,7 +215,7 @@ public abstract class CameraPreview<T extends View, Output> {
mOutputSurfaceWidth = width;
mOutputSurfaceHeight = height;
if (width > 0 && height > 0) {
crop(mCropOp);
crop(mCropCallback);
}
if (mSurfaceCallback != null) {
mSurfaceCallback.onSurfaceChanged();
@ -291,12 +293,11 @@ public abstract class CameraPreview<T extends View, Output> {
* There might still be some absolute difference (e.g. same ratio but bigger / smaller).
* However that should be already managed by the framework.
*
* @param op the op
* @param callback the callback
*/
protected void crop(@NonNull Op<Void> op) {
protected void crop(@Nullable CropCallback callback) {
// The base implementation does not support cropping.
op.start();
op.end(null);
if (callback != null) callback.onCrop();
}
/**

@ -5,6 +5,7 @@ import android.graphics.SurfaceTexture;
import android.opengl.GLSurfaceView;
import android.opengl.Matrix;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import android.view.LayoutInflater;
@ -14,7 +15,6 @@ import android.view.ViewGroup;
import com.otaliastudios.cameraview.R;
import com.otaliastudios.cameraview.internal.egl.EglViewport;
import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.filter.NoFilter;
import com.otaliastudios.cameraview.size.AspectRatio;
@ -270,8 +270,7 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
* See {@link Renderer#onDrawFrame(GL10)}.
*/
@Override
protected void crop(@NonNull Op<Void> op) {
op.start();
protected void crop(@Nullable final CropCallback callback) {
if (mInputStreamWidth > 0 && mInputStreamHeight > 0 && mOutputSurfaceWidth > 0
&& mOutputSurfaceHeight > 0) {
float scaleX = 1f, scaleY = 1f;
@ -289,7 +288,7 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
mCropScaleY = 1F / scaleY;
getView().requestRender();
}
op.end(null);
if (callback != null) callback.onCrop();
}
/**

@ -17,7 +17,6 @@ import android.view.ViewGroup;
import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.R;
import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.size.AspectRatio;
import java.util.concurrent.ExecutionException;
@ -91,14 +90,13 @@ public class TextureCameraPreview extends CameraPreview<TextureView, SurfaceText
}
@Override
protected void crop(final @NonNull Op<Void> op) {
op.start();
protected void crop(@Nullable final CropCallback callback) {
getView().post(new Runnable() {
@Override
public void run() {
if (mInputStreamHeight == 0 || mInputStreamWidth == 0 ||
mOutputSurfaceHeight == 0 || mOutputSurfaceWidth == 0) {
op.end(null);
if (callback != null) callback.onCrop();
return;
}
float scaleX = 1f, scaleY = 1f;
@ -118,7 +116,7 @@ public class TextureCameraPreview extends CameraPreview<TextureView, SurfaceText
mCropping = scaleX > 1.02f || scaleY > 1.02f;
LOG.i("crop:", "applied scaleX=", scaleX);
LOG.i("crop:", "applied scaleY=", scaleY);
op.end(null);
if (callback != null) callback.onCrop();
}
});
}

Loading…
Cancel
Save