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 versionCode 1
versionName project.version versionName project.version
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArgument "filter", "com.otaliastudios.cameraview.runner.SdkExcludeFilter" testInstrumentationRunnerArgument "filter", "com.otaliastudios.cameraview.tools.SdkExcludeFilter"
} }
buildTypes { buildTypes {
@ -42,8 +42,8 @@ dependencies {
androidTestImplementation 'org.mockito:mockito-android:2.28.2' androidTestImplementation 'org.mockito:mockito-android:2.28.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
api 'androidx.exifinterface:exifinterface:1.0.0' api 'androidx.exifinterface:exifinterface:1.1.0'
api 'androidx.lifecycle:lifecycle-common:2.1.0-alpha01' api 'androidx.lifecycle:lifecycle-common:2.1.0'
api 'com.google.android.gms:play-services-tasks:17.0.0' api 'com.google.android.gms:play-services-tasks:17.0.0'
implementation 'androidx.annotation:annotation:1.1.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/picture/**.*')
classFilter.add('**/com/otaliastudios/cameraview/video/**.*') classFilter.add('**/com/otaliastudios/cameraview/video/**.*')
// TODO these below could be easily testable ALSO outside of the integration tests // 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/**.*') classFilter.add('**/com/otaliastudios/cameraview/video/encoding/**.*')
} }
// We don't test OpenGL filters. // We don't test OpenGL filters.

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

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

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

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

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

@ -12,6 +12,7 @@ import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.Flash; import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.controls.PictureFormat; import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
import com.otaliastudios.cameraview.frame.FrameManager; import com.otaliastudios.cameraview.frame.FrameManager;
import com.otaliastudios.cameraview.gesture.Gesture; import com.otaliastudios.cameraview.gesture.Gesture;
import com.otaliastudios.cameraview.controls.Hdr; import com.otaliastudios.cameraview.controls.Hdr;
@ -24,6 +25,7 @@ import androidx.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.Callable;
public class MockCameraEngine extends CameraEngine { public class MockCameraEngine extends CameraEngine {
@ -80,8 +82,19 @@ public class MockCameraEngine extends CameraEngine {
mPreviewStreamSize = size; mPreviewStreamSize = size;
} }
public void setMockEngineState(boolean started) { public void setMockState(@NonNull CameraState state) {
mEngineStep.setState(started ? STATE_STARTED : STATE_STOPPED); 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 @Override

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

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

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

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

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

@ -18,11 +18,11 @@ import android.view.Surface;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest; 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.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -45,12 +45,12 @@ public class ImageHelperTest extends BaseTest {
private Image getImage() { private Image getImage() {
ImageReader reader = ImageReader.newInstance(100, 100, ImageFormat.YUV_420_888, 1); ImageReader reader = ImageReader.newInstance(100, 100, ImageFormat.YUV_420_888, 1);
Surface readerSurface = reader.getSurface(); Surface readerSurface = reader.getSurface();
final Op<Image> imageOp = new Op<>(true); final Op<Image> imageOp = new Op<>();
reader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() { reader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override @Override
public void onImageAvailable(ImageReader reader) { public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireLatestImage(); Image image = reader.acquireLatestImage();
if (image != null) imageOp.end(image); if (image != null) imageOp.controller().end(image);
} }
}, new Handler(Looper.getMainLooper())); }, 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.Task;
import com.google.android.gms.tasks.Tasks; import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.tools.Op;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -13,7 +14,6 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@ -43,7 +43,7 @@ public class WorkerHandlerTest extends BaseTest {
return new Runnable() { return new Runnable() {
@Override @Override
public void run() { public void run() {
op.end(true); op.controller().end(true);
} }
}; };
} }
@ -53,7 +53,7 @@ public class WorkerHandlerTest extends BaseTest {
return new Callable<Boolean>() { return new Callable<Boolean>() {
@Override @Override
public Boolean call() { public Boolean call() {
op.end(true); op.controller().end(true);
return true; return true;
} }
}; };
@ -77,7 +77,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testFallbackExecute() { public void testFallbackExecute() {
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
WorkerHandler.execute(getRunnableForOp(op)); WorkerHandler.execute(getRunnableForOp(op));
waitOp(op); waitOp(op);
} }
@ -85,7 +85,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testPostRunnable() { public void testPostRunnable() {
WorkerHandler handler = WorkerHandler.get("handler"); WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
handler.post(getRunnableForOp(op)); handler.post(getRunnableForOp(op));
waitOp(op); waitOp(op);
} }
@ -93,7 +93,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testPostCallable() { public void testPostCallable() {
WorkerHandler handler = WorkerHandler.get("handler"); WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
handler.post(getCallableForOp(op)); handler.post(getCallableForOp(op));
waitOp(op); waitOp(op);
} }
@ -110,7 +110,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testRunRunnable_background() { public void testRunRunnable_background() {
WorkerHandler handler = WorkerHandler.get("handler"); WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
handler.run(getRunnableForOp(op)); handler.run(getRunnableForOp(op));
waitOp(op); waitOp(op);
} }
@ -118,14 +118,14 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testRunRunnable_sameThread() { public void testRunRunnable_sameThread() {
final WorkerHandler handler = WorkerHandler.get("handler"); final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op1 = new Op<>(true); final Op<Boolean> op1 = new Op<>();
final Op<Boolean> op2 = new Op<>(true); final Op<Boolean> op2 = new Op<>();
handler.post(new Runnable() { handler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
handler.run(getRunnableForOp(op2)); handler.run(getRunnableForOp(op2));
assertTrue(op2.await(0)); // Do not wait. assertTrue(op2.await(0)); // Do not wait.
op1.end(true); op1.controller().end(true);
} }
}); });
waitOp(op1); waitOp(op1);
@ -134,7 +134,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testRunCallable_background() { public void testRunCallable_background() {
WorkerHandler handler = WorkerHandler.get("handler"); WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
handler.run(getCallableForOp(op)); handler.run(getCallableForOp(op));
waitOp(op); waitOp(op);
} }
@ -142,14 +142,14 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testRunCallable_sameThread() { public void testRunCallable_sameThread() {
final WorkerHandler handler = WorkerHandler.get("handler"); final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op1 = new Op<>(true); final Op<Boolean> op1 = new Op<>();
final Op<Boolean> op2 = new Op<>(true); final Op<Boolean> op2 = new Op<>();
handler.post(new Runnable() { handler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
handler.run(getCallableForOp(op2)); handler.run(getCallableForOp(op2));
assertTrue(op2.await(0)); // Do not wait. assertTrue(op2.await(0)); // Do not wait.
op1.end(true); op1.controller().end(true);
} }
}); });
waitOp(op1); waitOp(op1);
@ -158,14 +158,14 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testRunCallable_sameThread_throws() { public void testRunCallable_sameThread_throws() {
final WorkerHandler handler = WorkerHandler.get("handler"); final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
handler.post(new Runnable() { handler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
Task<Void> task = handler.run(getThrowCallable()); Task<Void> task = handler.run(getThrowCallable());
assertTrue(task.isComplete()); // Already complete assertTrue(task.isComplete()); // Already complete
assertFalse(task.isSuccessful()); assertFalse(task.isSuccessful());
op.end(true); op.controller().end(true);
} }
}); });
waitOp(op); waitOp(op);
@ -174,7 +174,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testPostDelayed_tooEarly() { public void testPostDelayed_tooEarly() {
final WorkerHandler handler = WorkerHandler.get("handler"); final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
handler.post(1000, getRunnableForOp(op)); handler.post(1000, getRunnableForOp(op));
assertNull(op.await(500)); assertNull(op.await(500));
} }
@ -182,7 +182,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testPostDelayed() { public void testPostDelayed() {
final WorkerHandler handler = WorkerHandler.get("handler"); final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
handler.post(1000, getRunnableForOp(op)); handler.post(1000, getRunnableForOp(op));
assertNotNull(op.await(2000)); assertNotNull(op.await(2000));
} }
@ -190,7 +190,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testRemove() { public void testRemove() {
final WorkerHandler handler = WorkerHandler.get("handler"); final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
Runnable runnable = getRunnableForOp(op); Runnable runnable = getRunnableForOp(op);
handler.post(1000, runnable); handler.post(1000, runnable);
handler.remove(runnable); handler.remove(runnable);
@ -210,7 +210,7 @@ public class WorkerHandlerTest extends BaseTest {
public void testExecutor() { public void testExecutor() {
final WorkerHandler handler = WorkerHandler.get("handler"); final WorkerHandler handler = WorkerHandler.get("handler");
Executor executor = handler.getExecutor(); Executor executor = handler.getExecutor();
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
executor.execute(getRunnableForOp(op)); executor.execute(getRunnableForOp(op));
waitOp(op); waitOp(op);
} }

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

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

@ -1,4 +1,4 @@
package com.otaliastudios.cameraview.runner; package com.otaliastudios.cameraview.tools;
import android.os.Build; import android.os.Build;

@ -48,6 +48,7 @@ import com.otaliastudios.cameraview.engine.Camera1Engine;
import com.otaliastudios.cameraview.engine.Camera2Engine; import com.otaliastudios.cameraview.engine.Camera2Engine;
import com.otaliastudios.cameraview.engine.CameraEngine; import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.engine.offset.Reference; 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.Filter;
import com.otaliastudios.cameraview.filter.FilterParser; import com.otaliastudios.cameraview.filter.FilterParser;
import com.otaliastudios.cameraview.filter.Filters; import com.otaliastudios.cameraview.filter.Filters;
@ -693,15 +694,17 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
//region Lifecycle APIs //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 * @return whether the camera has started
*/ */
public boolean isOpened() { public boolean isOpened() {
return mCameraEngine.getEngineState() >= CameraEngine.STATE_STARTED; return mCameraEngine.getState().isAtLeast(CameraState.ENGINE)
&& mCameraEngine.getTargetState().isAtLeast(CameraState.ENGINE);
} }
private boolean isClosed() { 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) @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void close() { public void close() {
if (mInEditor) return; if (mInEditor) return;
mCameraEngine.stop(); mCameraEngine.stop(false);
if (mCameraPreview != null) mCameraPreview.onPause(); if (mCameraPreview != null) mCameraPreview.onPause();
} }
@ -2006,7 +2009,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
} }
@Override @Override
public void dispatchOnCameraOpened(final CameraOptions options) { public void dispatchOnCameraOpened(@NonNull final CameraOptions options) {
mLogger.i("dispatchOnCameraOpened", options); mLogger.i("dispatchOnCameraOpened", options);
mUiHandler.post(new Runnable() { mUiHandler.post(new Runnable() {
@Override @Override
@ -2054,7 +2057,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
} }
@Override @Override
public void dispatchOnPictureTaken(final PictureResult.Stub stub) { public void dispatchOnPictureTaken(@NonNull final PictureResult.Stub stub) {
mLogger.i("dispatchOnPictureTaken", stub); mLogger.i("dispatchOnPictureTaken", stub);
mUiHandler.post(new Runnable() { mUiHandler.post(new Runnable() {
@Override @Override
@ -2068,7 +2071,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
} }
@Override @Override
public void dispatchOnVideoTaken(final VideoResult.Stub stub) { public void dispatchOnVideoTaken(@NonNull final VideoResult.Stub stub) {
mLogger.i("dispatchOnVideoTaken", stub); mLogger.i("dispatchOnVideoTaken", stub);
mUiHandler.post(new Runnable() { mUiHandler.post(new Runnable() {
@Override @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.Axis;
import com.otaliastudios.cameraview.engine.offset.Reference; import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.engine.options.Camera1Options; import com.otaliastudios.cameraview.engine.options.Camera1Options;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
@ -49,7 +50,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
@SuppressWarnings("deprecation")
public class Camera1Engine extends CameraEngine implements public class Camera1Engine extends CameraEngine implements
Camera.PreviewCallback, Camera.PreviewCallback,
Camera.ErrorCallback, Camera.ErrorCallback,
@ -58,13 +58,15 @@ public class Camera1Engine extends CameraEngine implements
private static final String TAG = Camera1Engine.class.getSimpleName(); private static final String TAG = Camera1Engine.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG); 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; private static final int PREVIEW_FORMAT = ImageFormat.NV21;
@VisibleForTesting static final int AUTOFOCUS_END_DELAY_MILLIS = 2500; @VisibleForTesting static final int AUTOFOCUS_END_DELAY_MILLIS = 2500;
private final Camera1Mapper mMapper = Camera1Mapper.get(); private final Camera1Mapper mMapper = Camera1Mapper.get();
private Camera mCamera; private Camera mCamera;
@VisibleForTesting int mCameraId; @VisibleForTesting int mCameraId;
private Runnable mFocusEndRunnable;
public Camera1Engine(@NonNull Callback callback) { public Camera1Engine(@NonNull Callback callback) {
super(callback); super(callback);
@ -286,10 +288,8 @@ public class Camera1Engine extends CameraEngine implements
@Override @Override
protected Task<Void> onStopEngine() { protected Task<Void> onStopEngine() {
LOG.i("onStopEngine:", "About to clean up."); LOG.i("onStopEngine:", "About to clean up.");
mHandler.remove(mFocusResetRunnable); mOrchestrator.remove(JOB_FOCUS_RESET);
if (mFocusEndRunnable != null) { mOrchestrator.remove(JOB_FOCUS_END);
mHandler.remove(mFocusEndRunnable);
}
if (mCamera != null) { if (mCamera != null) {
try { try {
LOG.i("onStopEngine:", "Clean up.", "Releasing camera."); LOG.i("onStopEngine:", "Clean up.", "Releasing camera.");
@ -457,14 +457,13 @@ public class Camera1Engine extends CameraEngine implements
public void setFlash(@NonNull Flash flash) { public void setFlash(@NonNull Flash flash) {
final Flash old = mFlash; final Flash old = mFlash;
mFlash = flash; mFlash = flash;
mHandler.run(new Runnable() { mFlashTask = mOrchestrator.scheduleStateful("flash",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) { Camera.Parameters params = mCamera.getParameters();
Camera.Parameters params = mCamera.getParameters(); if (applyFlash(params, old)) mCamera.setParameters(params);
if (applyFlash(params, old)) mCamera.setParameters(params);
}
mFlashOp.end(null);
} }
}); });
} }
@ -482,14 +481,13 @@ public class Camera1Engine extends CameraEngine implements
public void setLocation(@Nullable Location location) { public void setLocation(@Nullable Location location) {
final Location oldLocation = mLocation; final Location oldLocation = mLocation;
mLocation = location; mLocation = location;
mHandler.run(new Runnable() { mLocationTask = mOrchestrator.scheduleStateful("location",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) { Camera.Parameters params = mCamera.getParameters();
Camera.Parameters params = mCamera.getParameters(); if (applyLocation(params, oldLocation)) mCamera.setParameters(params);
if (applyLocation(params, oldLocation)) mCamera.setParameters(params);
}
mLocationOp.end(null);
} }
}); });
} }
@ -510,14 +508,13 @@ public class Camera1Engine extends CameraEngine implements
public void setWhiteBalance(@NonNull WhiteBalance whiteBalance) { public void setWhiteBalance(@NonNull WhiteBalance whiteBalance) {
final WhiteBalance old = mWhiteBalance; final WhiteBalance old = mWhiteBalance;
mWhiteBalance = whiteBalance; mWhiteBalance = whiteBalance;
mHandler.run(new Runnable() { mWhiteBalanceTask = mOrchestrator.scheduleStateful("white balance",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) { Camera.Parameters params = mCamera.getParameters();
Camera.Parameters params = mCamera.getParameters(); if (applyWhiteBalance(params, old)) mCamera.setParameters(params);
if (applyWhiteBalance(params, old)) mCamera.setParameters(params);
}
mWhiteBalanceOp.end(null);
} }
}); });
} }
@ -536,14 +533,13 @@ public class Camera1Engine extends CameraEngine implements
public void setHdr(@NonNull Hdr hdr) { public void setHdr(@NonNull Hdr hdr) {
final Hdr old = mHdr; final Hdr old = mHdr;
mHdr = hdr; mHdr = hdr;
mHandler.run(new Runnable() { mHdrTask = mOrchestrator.scheduleStateful("hdr",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) { Camera.Parameters params = mCamera.getParameters();
Camera.Parameters params = mCamera.getParameters(); if (applyHdr(params, old)) mCamera.setParameters(params);
if (applyHdr(params, old)) mCamera.setParameters(params);
}
mHdrOp.end(null);
} }
}); });
} }
@ -561,19 +557,18 @@ public class Camera1Engine extends CameraEngine implements
public void setZoom(final float zoom, @Nullable final PointF[] points, final boolean notify) { public void setZoom(final float zoom, @Nullable final PointF[] points, final boolean notify) {
final float old = mZoomValue; final float old = mZoomValue;
mZoomValue = zoom; mZoomValue = zoom;
mHandler.run(new Runnable() { mZoomTask = mOrchestrator.scheduleStateful("zoom",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) { Camera.Parameters params = mCamera.getParameters();
Camera.Parameters params = mCamera.getParameters(); if (applyZoom(params, old)) {
if (applyZoom(params, old)) { mCamera.setParameters(params);
mCamera.setParameters(params); if (notify) {
if (notify) { mCallback.dispatchOnZoomChanged(mZoomValue, points);
mCallback.dispatchOnZoomChanged(mZoomValue, points);
}
} }
} }
mZoomOp.end(null);
} }
}); });
} }
@ -594,20 +589,19 @@ public class Camera1Engine extends CameraEngine implements
@Nullable final PointF[] points, final boolean notify) { @Nullable final PointF[] points, final boolean notify) {
final float old = mExposureCorrectionValue; final float old = mExposureCorrectionValue;
mExposureCorrectionValue = EVvalue; mExposureCorrectionValue = EVvalue;
mHandler.run(new Runnable() { mExposureCorrectionTask = mOrchestrator.scheduleStateful("exposure correction",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) { Camera.Parameters params = mCamera.getParameters();
Camera.Parameters params = mCamera.getParameters(); if (applyExposureCorrection(params, old)) {
if (applyExposureCorrection(params, old)) { mCamera.setParameters(params);
mCamera.setParameters(params); if (notify) {
if (notify) { mCallback.dispatchOnExposureCorrectionChanged(mExposureCorrectionValue,
mCallback.dispatchOnExposureCorrectionChanged(mExposureCorrectionValue, bounds, points);
bounds, points);
}
} }
} }
mExposureCorrectionOp.end(null);
} }
}); });
} }
@ -635,13 +629,12 @@ public class Camera1Engine extends CameraEngine implements
public void setPlaySounds(boolean playSounds) { public void setPlaySounds(boolean playSounds) {
final boolean old = mPlaySounds; final boolean old = mPlaySounds;
mPlaySounds = playSounds; mPlaySounds = playSounds;
mHandler.run(new Runnable() { mPlaySoundsTask = mOrchestrator.scheduleStateful("play sounds",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) { applyPlaySounds(old);
applyPlaySounds(old);
}
mPlaySoundsOp.end(null);
} }
}); });
} }
@ -672,14 +665,13 @@ public class Camera1Engine extends CameraEngine implements
public void setPreviewFrameRate(float previewFrameRate) { public void setPreviewFrameRate(float previewFrameRate) {
final float old = previewFrameRate; final float old = previewFrameRate;
mPreviewFrameRate = previewFrameRate; mPreviewFrameRate = previewFrameRate;
mHandler.run(new Runnable() { mPreviewFrameRateTask = mOrchestrator.scheduleStateful("preview fps",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) { Camera.Parameters params = mCamera.getParameters();
Camera.Parameters params = mCamera.getParameters(); if (applyPreviewFrameRate(params, old)) mCamera.setParameters(params);
if (applyPreviewFrameRate(params, old)) mCamera.setParameters(params);
}
mPreviewFrameRateOp.end(null);
} }
}); });
} }
@ -737,7 +729,8 @@ public class Camera1Engine extends CameraEngine implements
@Override @Override
public void onBufferAvailable(@NonNull byte[] buffer) { public void onBufferAvailable(@NonNull byte[] buffer) {
if (getEngineState() == STATE_STARTED) { if (getState().isAtLeast(CameraState.ENGINE)
&& getTargetState().isAtLeast(CameraState.ENGINE)) {
mCamera.addCallbackBuffer(buffer); mCamera.addCallbackBuffer(buffer);
} }
} }
@ -770,10 +763,9 @@ public class Camera1Engine extends CameraEngine implements
} }
final int viewWidthF = viewWidth; final int viewWidthF = viewWidth;
final int viewHeightF = viewHeight; final int viewHeightF = viewHeight;
mHandler.run(new Runnable() { mOrchestrator.scheduleStateful("auto focus", CameraState.ENGINE, new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() < STATE_STARTED) return;
if (!mCameraOptions.isAutoFocusSupported()) return; if (!mCameraOptions.isAutoFocusSupported()) return;
final PointF p = new PointF(point.x, point.y); // copy. final PointF p = new PointF(point.x, point.y); // copy.
int offset = getAngles().offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE); 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 // 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. // to be. So we remove the old runnable if still present and post a new one.
if (mFocusEndRunnable != null) mHandler.remove(mFocusEndRunnable); mOrchestrator.remove(JOB_FOCUS_END);
mFocusEndRunnable = new Runnable() { mOrchestrator.scheduleDelayed(JOB_FOCUS_END, AUTOFOCUS_END_DELAY_MILLIS,
new Runnable() {
@Override @Override
public void run() { public void run() {
mCallback.dispatchOnFocusEnd(gesture, false, p); mCallback.dispatchOnFocusEnd(gesture, false, p);
} }
}; });
mHandler.post(AUTOFOCUS_END_DELAY_MILLIS, mFocusEndRunnable);
// Wrapping autoFocus in a try catch to handle some device specific exceptions, // Wrapping autoFocus in a try catch to handle some device specific exceptions,
// see See https://github.com/natario1/CameraView/issues/181. // see See https://github.com/natario1/CameraView/issues/181.
@ -809,14 +801,27 @@ public class Camera1Engine extends CameraEngine implements
mCamera.autoFocus(new Camera.AutoFocusCallback() { mCamera.autoFocus(new Camera.AutoFocusCallback() {
@Override @Override
public void onAutoFocus(boolean success, Camera camera) { public void onAutoFocus(boolean success, Camera camera) {
if (mFocusEndRunnable != null) { mOrchestrator.remove(JOB_FOCUS_END);
mHandler.remove(mFocusEndRunnable); mOrchestrator.remove(JOB_FOCUS_RESET);
mFocusEndRunnable = null;
}
mCallback.dispatchOnFocusEnd(gesture, success, p); mCallback.dispatchOnFocusEnd(gesture, success, p);
mHandler.remove(mFocusResetRunnable);
if (shouldResetAutoFocus()) { 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 // Let the mFocusEndRunnable do its job. (could remove it and quickly dispatch
// onFocusEnd here, but let's make it simpler). // 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); 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 //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.Actions;
import com.otaliastudios.cameraview.engine.action.BaseAction; import com.otaliastudios.cameraview.engine.action.BaseAction;
import com.otaliastudios.cameraview.engine.action.CompletionCallback; 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.mappers.Camera2Mapper;
import com.otaliastudios.cameraview.engine.meter.MeterAction; import com.otaliastudios.cameraview.engine.meter.MeterAction;
import com.otaliastudios.cameraview.engine.meter.MeterResetAction; import com.otaliastudios.cameraview.engine.meter.MeterResetAction;
import com.otaliastudios.cameraview.engine.offset.Axis; import com.otaliastudios.cameraview.engine.offset.Axis;
import com.otaliastudios.cameraview.engine.offset.Reference; import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.engine.options.Camera2Options; 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.Frame;
import com.otaliastudios.cameraview.frame.FrameManager; import com.otaliastudios.cameraview.frame.FrameManager;
import com.otaliastudios.cameraview.gesture.Gesture; 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) { private void applyRepeatingRequestBuilder(boolean checkStarted, int errorReason) {
if (getPreviewState() == STATE_STARTED || !checkStarted) { if ((getState() == CameraState.PREVIEW && !isChangingState()) || !checkStarted) {
try { try {
mSession.setRepeatingRequest(mRepeatingRequestBuilder.build(), mSession.setRepeatingRequest(mRepeatingRequestBuilder.build(),
mRepeatingRequestCallback, null); mRepeatingRequestCallback, null);
@ -256,9 +258,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
LOG.e("applyRepeatingRequestBuilder: session is invalid!", e, LOG.e("applyRepeatingRequestBuilder: session is invalid!", e,
"checkStarted:", checkStarted, "checkStarted:", checkStarted,
"currentThread:", Thread.currentThread().getName(), "currentThread:", Thread.currentThread().getName(),
"previewState:", getPreviewState(), "state:", getState(),
"bindState:", getBindState(), "targetState:", getTargetState());
"engineState:", getEngineState());
throw new CameraException(CameraException.REASON_DISCONNECTED); throw new CameraException(CameraException.REASON_DISCONNECTED);
} }
} }
@ -588,13 +589,12 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
if (mFullVideoPendingStub != null) { if (mFullVideoPendingStub != null) {
// Do not call takeVideo/onTakeVideo. It will reset some stub parameters that // 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. // 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; final VideoResult.Stub stub = mFullVideoPendingStub;
mFullVideoPendingStub = null; mFullVideoPendingStub = null;
mHandler.post(new Runnable() { mOrchestrator.scheduleStateful("do take video", CameraState.PREVIEW,
new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("onStartPreview", "Executing doTakeVideo call.");
doTakeVideo(stub); 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 // 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 // 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. // 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 @Override
public void run() { public void run() {
if (getBindState() < STATE_STARTED) return;
maybeRestorePreviewTemplateAfterVideo(); maybeRestorePreviewTemplateAfterVideo();
} }
}); });
@ -1028,33 +1028,32 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
public void setFlash(@NonNull final Flash flash) { public void setFlash(@NonNull final Flash flash) {
final Flash old = mFlash; final Flash old = mFlash;
mFlash = flash; mFlash = flash;
mHandler.run(new Runnable() { mFlashTask = mOrchestrator.scheduleStateful("flash (" + flash + ")",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) { boolean shouldApply = applyFlash(mRepeatingRequestBuilder, old);
boolean shouldApply = applyFlash(mRepeatingRequestBuilder, old); boolean needsWorkaround = getState() == CameraState.PREVIEW;
boolean needsWorkaround = getPreviewState() == STATE_STARTED; if (needsWorkaround) {
if (needsWorkaround) { // Runtime changes to the flash value are not correctly handled by the
// Runtime changes to the flash value are not correctly handled by the // driver. See https://stackoverflow.com/q/53003383/4288782 for example.
// 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.
// For this reason, we go back to OFF, capture once, then go to the new one. mFlash = Flash.OFF;
mFlash = Flash.OFF; applyFlash(mRepeatingRequestBuilder, old);
applyFlash(mRepeatingRequestBuilder, old); try {
try { mSession.capture(mRepeatingRequestBuilder.build(), null,
mSession.capture(mRepeatingRequestBuilder.build(), null, null);
null); } catch (CameraAccessException e) {
} catch (CameraAccessException e) { throw createCameraException(e);
throw createCameraException(e);
}
mFlash = flash;
applyFlash(mRepeatingRequestBuilder, old);
applyRepeatingRequestBuilder();
} else if (shouldApply) {
applyRepeatingRequestBuilder();
} }
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) { public void setLocation(@Nullable Location location) {
final Location old = mLocation; final Location old = mLocation;
mLocation = location; mLocation = location;
mHandler.run(new Runnable() { mLocationTask = mOrchestrator.scheduleStateful("location",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) { if (applyLocation(mRepeatingRequestBuilder, old)) {
if (applyLocation(mRepeatingRequestBuilder, old)) { applyRepeatingRequestBuilder();
applyRepeatingRequestBuilder();
}
} }
mLocationOp.end(null);
} }
}); });
} }
@ -1130,15 +1128,15 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
public void setWhiteBalance(@NonNull WhiteBalance whiteBalance) { public void setWhiteBalance(@NonNull WhiteBalance whiteBalance) {
final WhiteBalance old = mWhiteBalance; final WhiteBalance old = mWhiteBalance;
mWhiteBalance = whiteBalance; mWhiteBalance = whiteBalance;
mHandler.run(new Runnable() { mWhiteBalanceTask = mOrchestrator.scheduleStateful(
"white balance (" + whiteBalance + ")",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) { if (applyWhiteBalance(mRepeatingRequestBuilder, old)) {
if (applyWhiteBalance(mRepeatingRequestBuilder, old)) { applyRepeatingRequestBuilder();
applyRepeatingRequestBuilder();
}
} }
mWhiteBalanceOp.end(null);
} }
}); });
} }
@ -1159,15 +1157,14 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
public void setHdr(@NonNull Hdr hdr) { public void setHdr(@NonNull Hdr hdr) {
final Hdr old = mHdr; final Hdr old = mHdr;
mHdr = hdr; mHdr = hdr;
mHandler.run(new Runnable() { mHdrTask = mOrchestrator.scheduleStateful("hdr (" + hdr + ")",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) { if (applyHdr(mRepeatingRequestBuilder, old)) {
if (applyHdr(mRepeatingRequestBuilder, old)) { applyRepeatingRequestBuilder();
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) { public void setZoom(final float zoom, final @Nullable PointF[] points, final boolean notify) {
final float old = mZoomValue; final float old = mZoomValue;
mZoomValue = zoom; mZoomValue = zoom;
mHandler.run(new Runnable() { mZoomTask = mOrchestrator.scheduleStateful(
"zoom (" + zoom + ")",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) { if (applyZoom(mRepeatingRequestBuilder, old)) {
if (applyZoom(mRepeatingRequestBuilder, old)) { applyRepeatingRequestBuilder();
applyRepeatingRequestBuilder(); if (notify) {
if (notify) { mCallback.dispatchOnZoomChanged(zoom, points);
mCallback.dispatchOnZoomChanged(zoom, points);
}
} }
} }
mZoomOp.end(null);
} }
}); });
} }
@ -1243,18 +1240,18 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
final boolean notify) { final boolean notify) {
final float old = mExposureCorrectionValue; final float old = mExposureCorrectionValue;
mExposureCorrectionValue = EVvalue; mExposureCorrectionValue = EVvalue;
mHandler.run(new Runnable() { mExposureCorrectionTask = mOrchestrator.scheduleStateful(
"exposure correction (" + EVvalue + ")",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) { if (applyExposureCorrection(mRepeatingRequestBuilder, old)) {
if (applyExposureCorrection(mRepeatingRequestBuilder, old)) { applyRepeatingRequestBuilder();
applyRepeatingRequestBuilder(); if (notify) {
if (notify) { mCallback.dispatchOnExposureCorrectionChanged(EVvalue, bounds, points);
mCallback.dispatchOnExposureCorrectionChanged(EVvalue, bounds, points);
}
} }
} }
mExposureCorrectionOp.end(null);
} }
}); });
} }
@ -1278,21 +1275,22 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override @Override
public void setPlaySounds(boolean playSounds) { public void setPlaySounds(boolean playSounds) {
mPlaySounds = 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; final float oldPreviewFrameRate = mPreviewFrameRate;
mPreviewFrameRate = previewFrameRate; mPreviewFrameRate = previewFrameRate;
mHandler.run(new Runnable() { mPreviewFrameRateTask = mOrchestrator.scheduleStateful(
"preview fps (" + previewFrameRate + ")",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) { if (applyPreviewFrameRate(mRepeatingRequestBuilder, oldPreviewFrameRate)) {
if (applyPreviewFrameRate(mRepeatingRequestBuilder, oldPreviewFrameRate)) { applyRepeatingRequestBuilder();
applyRepeatingRequestBuilder();
}
} }
mPreviewFrameRateOp.end(null);
} }
}); });
} }
@ -1334,19 +1332,12 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
public void setPictureFormat(final @NonNull PictureFormat pictureFormat) { public void setPictureFormat(final @NonNull PictureFormat pictureFormat) {
if (pictureFormat != mPictureFormat) { if (pictureFormat != mPictureFormat) {
mPictureFormat = pictureFormat; mPictureFormat = pictureFormat;
LOG.i("setPictureFormat", "changing to", pictureFormat, "posting."); mOrchestrator.scheduleStateful("picture format (" + pictureFormat + ")",
mHandler.run(new Runnable() { CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("setPictureFormat", "changing to", pictureFormat, restart();
"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();
}
} }
}); });
} }
@ -1391,7 +1382,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
return; return;
} }
image.close(); image.close();
if (getPreviewState() == STATE_STARTED) { if (getState() == CameraState.PREVIEW && !isChangingState()) {
// After preview, the frame manager is correctly set up // After preview, the frame manager is correctly set up
Frame frame = getFrameManager().getFrame(data, Frame frame = getFrameManager().getFrame(data,
System.currentTimeMillis(), System.currentTimeMillis(),
@ -1405,35 +1396,23 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override @Override
public void setHasFrameProcessors(final boolean hasFrameProcessors) { public void setHasFrameProcessors(final boolean hasFrameProcessors) {
LOG.i("setHasFrameProcessors", "changing to", hasFrameProcessors, "posting."); // Frame processing is set up partially when binding and partially when starting
Camera2Engine.super.setHasFrameProcessors(hasFrameProcessors); // the preview. If the value is changed between the two, the preview step can crash.
mHandler.run(new Runnable() { mOrchestrator.schedule("has frame processors (" + hasFrameProcessors + ")",
true, new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("setHasFrameProcessors", "changing to", hasFrameProcessors, if (getState().isAtLeast(CameraState.BIND) && isChangingState()) {
"executing. BindState:", getBindState(), // Extremely rare case in which this was called in between startBind and
"PreviewState:", getPreviewState()); // startPreview. This can cause issues. Try later.
setHasFrameProcessors(hasFrameProcessors);
// Frame processing is set up partially when binding and partially when starting } else if (getState().isAtLeast(CameraState.BIND)) {
// the preview. We don't want to only check bind state or startPreview can fail. // Apply and restart.
if (getBindState() == STATE_STOPPED) { Camera2Engine.super.setHasFrameProcessors(hasFrameProcessors);
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().");
restartBind(); restartBind();
} else { } else {
// Bind+Preview is not completely started yet not completely stopped. // Just apply.
// This can happen if the user adds a frame processor in onCameraOpened(). Camera2Engine.super.setHasFrameProcessors(hasFrameProcessors);
// 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.");
} }
} }
}); });
@ -1445,16 +1424,14 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override @Override
public void startAutoFocus(@Nullable final Gesture gesture, @NonNull final PointF point) { public void startAutoFocus(@Nullable final Gesture gesture, @NonNull final PointF point) {
LOG.i("startAutoFocus", "dispatching. Gesture:", gesture); // This will only work when we have a preview, since it launches the preview
mHandler.run(new Runnable() { // 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 @Override
public void run() { 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 // 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. // refers to "3A metering to a specific point". Since we have a point, check.
if (!mCameraOptions.isAutoFocusSupported()) return; if (!mCameraOptions.isAutoFocusSupported()) return;
@ -1468,10 +1445,16 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override @Override
protected void onActionCompleted(@NonNull Action a) { protected void onActionCompleted(@NonNull Action a) {
mCallback.dispatchOnFocusEnd(gesture, action.isSuccessful(), point); mCallback.dispatchOnFocusEnd(gesture, action.isSuccessful(), point);
mHandler.remove(mUnlockAndResetMeteringRunnable); mOrchestrator.remove("reset metering");
if (shouldResetAutoFocus()) { if (shouldResetAutoFocus()) {
mHandler.post(getAutoFocusResetDelay(), mOrchestrator.scheduleDelayed("reset metering",
mUnlockAndResetMeteringRunnable); getAutoFocusResetDelay(),
new Runnable() {
@Override
public void run() {
unlockAndResetMetering();
}
});
} }
} }
}); });
@ -1495,15 +1478,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
return mMeterAction; return mMeterAction;
} }
private final Runnable mUnlockAndResetMeteringRunnable = new Runnable() {
@Override
public void run() {
unlockAndResetMetering();
}
};
private void unlockAndResetMetering() { private void unlockAndResetMetering() {
if (getEngineState() == STATE_STARTED) { if (getState() == CameraState.PREVIEW && !isChangingState()) {
Actions.sequence( Actions.sequence(
new BaseAction() { new BaseAction() {
@Override @Override
@ -1566,7 +1542,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override @Override
public void applyBuilder(@NonNull Action source, @NonNull CaptureRequest.Builder builder) public void applyBuilder(@NonNull Action source, @NonNull CaptureRequest.Builder builder)
throws CameraAccessException { throws CameraAccessException {
if (getPreviewState() == STATE_STARTED) { if (getState() == CameraState.PREVIEW && !isChangingState()) {
mSession.capture(builder.build(), mRepeatingRequestCallback, null); mSession.capture(builder.build(), mRepeatingRequestCallback, null);
} }
} }

@ -8,24 +8,24 @@ import android.location.Location;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import com.google.android.gms.tasks.Continuation;
import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.OnFailureListener; import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.SuccessContinuation;
import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.CameraException; import com.otaliastudios.cameraview.CameraException;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.controls.PictureFormat; 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.overlay.Overlay;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.engine.offset.Angles; import com.otaliastudios.cameraview.engine.offset.Angles;
import com.otaliastudios.cameraview.engine.offset.Reference; import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameManager; import com.otaliastudios.cameraview.frame.FrameManager;
import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler; import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import com.otaliastudios.cameraview.picture.PictureRecorder; import com.otaliastudios.cameraview.picture.PictureRecorder;
import com.otaliastudios.cameraview.preview.CameraPreview; import com.otaliastudios.cameraview.preview.CameraPreview;
@ -54,7 +54,6 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit; 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 * 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. * 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 * 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 * 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 * or not, since that tells us if we can act on the camera or not, rather than knowing about
* steps 3 and 4. * steps 3 and 4.
@ -87,18 +81,11 @@ import java.util.concurrent.TimeUnit;
* So in the {@link CameraEngine} notation, * So in the {@link CameraEngine} notation,
* - {@link #start()}: ASYNC - starts the engine (S2). When possible, at a later time, * - {@link #start()}: ASYNC - starts the engine (S2). When possible, at a later time,
* S3 and S4 are also performed. * 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 #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. * 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 * THREADING
* Subclasses should always execute code on the thread given by {@link #mHandler}. * 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: * 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 { public interface Callback {
@NonNull Context getContext(); @NonNull Context getContext();
void dispatchOnCameraOpened(CameraOptions options); void dispatchOnCameraOpened(@NonNull CameraOptions options);
void dispatchOnCameraClosed(); void dispatchOnCameraClosed();
void onCameraPreviewStreamSizeChanged(); void onCameraPreviewStreamSizeChanged();
void onShutter(boolean shouldPlaySound); void onShutter(boolean shouldPlaySound);
void dispatchOnVideoTaken(VideoResult.Stub stub); void dispatchOnVideoTaken(@NonNull VideoResult.Stub stub);
void dispatchOnPictureTaken(PictureResult.Stub stub); void dispatchOnPictureTaken(@NonNull PictureResult.Stub stub);
void dispatchOnFocusStart(@Nullable Gesture trigger, @NonNull PointF where); void dispatchOnFocusStart(@Nullable Gesture trigger, @NonNull PointF where);
void dispatchOnFocusEnd(@Nullable Gesture trigger, boolean success, @NonNull PointF where); void dispatchOnFocusEnd(@Nullable Gesture trigger, boolean success, @NonNull PointF where);
void dispatchOnZoomChanged(final float newValue, @Nullable final PointF[] fingers); void dispatchOnZoomChanged(final float newValue, @Nullable final PointF[] fingers);
void dispatchOnExposureCorrectionChanged(float newValue, void dispatchOnExposureCorrectionChanged(float newValue, @NonNull float[] bounds,
@NonNull float[] bounds,
@Nullable PointF[] fingers); @Nullable PointF[] fingers);
void dispatchFrame(@NonNull Frame frame); void dispatchFrame(@NonNull Frame frame);
void dispatchError(CameraException exception); void dispatchError(CameraException exception);
@ -148,15 +134,7 @@ public abstract class CameraEngine implements
private static final String TAG = CameraEngine.class.getSimpleName(); private static final String TAG = CameraEngine.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG); 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 // Need to be protected
@SuppressWarnings("WeakerAccess") protected WorkerHandler mHandler;
@SuppressWarnings("WeakerAccess") protected final Callback mCallback; @SuppressWarnings("WeakerAccess") protected final Callback mCallback;
@SuppressWarnings("WeakerAccess") protected CameraPreview mPreview; @SuppressWarnings("WeakerAccess") protected CameraPreview mPreview;
@SuppressWarnings("WeakerAccess") protected CameraOptions mCameraOptions; @SuppressWarnings("WeakerAccess") protected CameraOptions mCameraOptions;
@ -177,7 +155,7 @@ public abstract class CameraEngine implements
@SuppressWarnings("WeakerAccess") protected boolean mPictureSnapshotMetering; @SuppressWarnings("WeakerAccess") protected boolean mPictureSnapshotMetering;
@SuppressWarnings("WeakerAccess") protected float mPreviewFrameRate; @SuppressWarnings("WeakerAccess") protected float mPreviewFrameRate;
// Can be private private WorkerHandler mHandler;
@VisibleForTesting Handler mCrashHandler; @VisibleForTesting Handler mCrashHandler;
private final FrameManager mFrameManager; private final FrameManager mFrameManager;
private final Angles mAngles; private final Angles mAngles;
@ -193,36 +171,42 @@ public abstract class CameraEngine implements
private int mAudioBitRate; private int mAudioBitRate;
private boolean mHasFrameProcessors; private boolean mHasFrameProcessors;
private long mAutoFocusResetDelayMillis; private long mAutoFocusResetDelayMillis;
// in REF_VIEW, for consistency with SizeSelectors private int mSnapshotMaxWidth = Integer.MAX_VALUE; // in REF_VIEW like SizeSelectors
private int mSnapshotMaxWidth = Integer.MAX_VALUE; private int mSnapshotMaxHeight = Integer.MAX_VALUE; // in REF_VIEW like SizeSelectors
// in REF_VIEW, for consistency with SizeSelectors
private int mSnapshotMaxHeight = Integer.MAX_VALUE;
private Overlay overlay; private Overlay overlay;
// Steps @SuppressWarnings("WeakerAccess")
private final Step.Callback mStepCallback = new Step.Callback() { protected final CameraStateOrchestrator mOrchestrator
@Override @NonNull public Executor getExecutor() { return mHandler.getExecutor(); } = new CameraStateOrchestrator(new CameraOrchestrator.Callback() {
@Override public void handleException(@NonNull Exception exception) { @Override
CameraEngine.this.handleException(Thread.currentThread(), exception, false); @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. // Ops used for testing.
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mZoomOp = new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mZoomTask
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mExposureCorrectionOp = Tasks.forResult(null);
= new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mExposureCorrectionTask
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mFlashOp = new Op<>(); = Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mWhiteBalanceOp @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mFlashTask
= new Op<>(); = Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mHdrOp = new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mWhiteBalanceTask
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mLocationOp = new Op<>(); = Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mPlaySoundsOp = new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mHdrTask
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mPreviewFrameRateOp = new Op<>(); = 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) { protected CameraEngine(@NonNull Callback callback) {
mCallback = callback; mCallback = callback;
@ -252,7 +236,7 @@ public abstract class CameraEngine implements
*/ */
private class CrashExceptionHandler implements Thread.UncaughtExceptionHandler { private class CrashExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override @Override
public void uncaughtException(final Thread thread, final Throwable throwable) { public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
handleException(thread, throwable, true); handleException(thread, throwable, true);
} }
} }
@ -263,7 +247,7 @@ public abstract class CameraEngine implements
*/ */
private static class NoOpExceptionHandler implements Thread.UncaughtExceptionHandler { private static class NoOpExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override @Override
public void uncaughtException(Thread t, Throwable e) { public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
// No-op. // No-op.
} }
} }
@ -305,7 +289,7 @@ public abstract class CameraEngine implements
final CameraException cameraException = (CameraException) throwable; final CameraException cameraException = (CameraException) throwable;
LOG.e("uncaughtException:", "Got CameraException:", cameraException, LOG.e("uncaughtException:", "Got CameraException:", cameraException,
"on engine state:", getEngineState()); "on state:", getState());
if (fromExceptionHandler) { if (fromExceptionHandler) {
// Got to restart the handler. // Got to restart the handler.
thread.interrupt(); thread.interrupt();
@ -322,49 +306,95 @@ public abstract class CameraEngine implements
//endregion //endregion
//region states and steps //region State management
public final int getEngineState() { @NonNull
return mEngineStep.getState(); public final CameraState getState() {
return mOrchestrator.getCurrentState();
} }
@SuppressWarnings("WeakerAccess") @NonNull
public final int getBindState() { public final CameraState getTargetState() {
return mBindStep.getState(); return mOrchestrator.getTargetState();
} }
@SuppressWarnings({"unused", "WeakerAccess"}) public boolean isChangingState() {
public final int getPreviewState() { return mOrchestrator.hasPendingStateChange();
return mPreviewStep.getState();
} }
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() { @SuppressWarnings("WeakerAccess")
return mEngineStep.isStartedOrStarting(); public void restart() {
LOG.i("RESTART:", "scheduled. State:", getState());
stop(false);
start();
} }
private boolean canStartBind() { @NonNull
return mEngineStep.isStarted() public Task<Void> start() {
&& mPreview != null LOG.i("START:", "scheduled. State:", getState());
&& mPreview.hasSurface() Task<Void> engine = startEngine();
&& mBindStep.isStoppingOrStopped(); startBind();
startPreview();
return engine;
} }
private boolean needsStopBind() { @NonNull
return mBindStep.isStartedOrStarting(); public Task<Void> stop(final boolean swallowExceptions) {
LOG.i("STOP:", "scheduled. State:", getState());
stopPreview(swallowExceptions);
stopBind(swallowExceptions);
return stopEngine(swallowExceptions);
} }
private boolean canStartPreview() { @SuppressWarnings("WeakerAccess")
return mEngineStep.isStarted() @NonNull
&& mBindStep.isStarted() protected Task<Void> restartBind() {
&& mPreviewStep.isStoppingOrStopped(); LOG.i("RESTART BIND:", "scheduled. State:", getState());
stopPreview(false);
stopBind(false);
startBind();
return startPreview();
} }
private boolean needsStopPreview() { @SuppressWarnings("WeakerAccess")
return mPreviewStep.isStartedOrStarting(); @NonNull
protected Task<Void> restartPreview() {
LOG.i("RESTART PREVIEW:", "scheduled. State:", getState());
stopPreview(false);
return startPreview();
} }
//endregion //endregion
@ -374,43 +404,41 @@ public abstract class CameraEngine implements
@NonNull @NonNull
@EngineThread @EngineThread
private Task<Void> startEngine() { private Task<Void> startEngine() {
if (canStartEngine()) { return mOrchestrator.scheduleStateChange(CameraState.OFF, CameraState.ENGINE,
mEngineStep.doStart(false, new Callable<Task<Void>>() { true,
@Override new Callable<Task<Void>>() {
public Task<Void> call() { @Override
if (!collectCameraInfo(mFacing)) { public Task<Void> call() {
LOG.e("onStartEngine:", "No camera available for facing", mFacing); if (!collectCameraInfo(mFacing)) {
throw new CameraException(CameraException.REASON_NO_CAMERA); 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 onStartEngine();
} }
return mEngineStep.getTask(); }).addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
mCallback.dispatchOnCameraOpened(mCameraOptions);
}
});
} }
@NonNull @NonNull
@EngineThread @EngineThread
private Task<Void> stopEngine(boolean swallowExceptions) { private Task<Void> stopEngine(boolean swallowExceptions) {
if (needsStopEngine()) { return mOrchestrator.scheduleStateChange(CameraState.ENGINE, CameraState.OFF,
mEngineStep.doStop(swallowExceptions, new Callable<Task<Void>>() { !swallowExceptions,
@Override new Callable<Task<Void>>() {
public Task<Void> call() { @Override
return onStopEngine(); public Task<Void> call() {
} return onStopEngine();
}, new Runnable() { }
@Override }).addOnSuccessListener(new OnSuccessListener<Void>() {
public void run() { @Override
mCallback.dispatchOnCameraClosed(); public void onSuccess(Void aVoid) {
} mCallback.dispatchOnCameraClosed();
}); }
} });
return mEngineStep.getTask();
} }
/** /**
@ -438,29 +466,32 @@ public abstract class CameraEngine implements
@NonNull @NonNull
@EngineThread @EngineThread
private Task<Void> startBind() { private Task<Void> startBind() {
if (canStartBind()) { return mOrchestrator.scheduleStateChange(CameraState.ENGINE, CameraState.BIND,
mBindStep.doStart(false, new Callable<Task<Void>>() { true,
@Override new Callable<Task<Void>>() {
public Task<Void> call() { @Override
public Task<Void> call() {
if (mPreview != null && mPreview.hasSurface()) {
return onStartBind(); return onStartBind();
} else {
return Tasks.forCanceled();
} }
}); }
} });
return mBindStep.getTask();
} }
@SuppressWarnings("UnusedReturnValue")
@NonNull @NonNull
@EngineThread @EngineThread
private Task<Void> stopBind(boolean swallowExceptions) { private Task<Void> stopBind(boolean swallowExceptions) {
if (needsStopBind()) { return mOrchestrator.scheduleStateChange(CameraState.BIND, CameraState.ENGINE,
mBindStep.doStop(swallowExceptions, new Callable<Task<Void>>() { !swallowExceptions,
@Override new Callable<Task<Void>>() {
public Task<Void> call() { @Override
return onStopBind(); public Task<Void> call() {
} return onStopBind();
}); }
} });
return mBindStep.getTask();
} }
/** /**
@ -481,39 +512,6 @@ public abstract class CameraEngine implements
@EngineThread @EngineThread
protected abstract Task<Void> onStopBind(); 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 //endregion
//region Start & Stop preview //region Start & Stop preview
@ -521,44 +519,26 @@ public abstract class CameraEngine implements
@NonNull @NonNull
@EngineThread @EngineThread
private Task<Void> startPreview() { private Task<Void> startPreview() {
LOG.i("startPreview", "canStartPreview:", canStartPreview()); return mOrchestrator.scheduleStateChange(CameraState.BIND, CameraState.PREVIEW,
if (canStartPreview()) { true,
mPreviewStep.doStart(false, new Callable<Task<Void>>() { new Callable<Task<Void>>() {
@Override @Override
public Task<Void> call() { public Task<Void> call() {
return onStartPreview(); return onStartPreview();
} }
}); });
}
return mPreviewStep.getTask();
} }
@SuppressWarnings("UnusedReturnValue")
@NonNull @NonNull
@EngineThread @EngineThread
private Task<Void> stopPreview(boolean swallowExceptions) { private Task<Void> stopPreview(boolean swallowExceptions) {
LOG.i("stopPreview", return mOrchestrator.scheduleStateChange(CameraState.PREVIEW, CameraState.BIND,
"needsStopPreview:", needsStopPreview(), !swallowExceptions,
"swallowExceptions:", swallowExceptions); new Callable<Task<Void>>() {
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() {
@Override @Override
public void run() { public Task<Void> call() {
LOG.i("restartPreview", "executing."); return onStopPreview();
stopPreview(false);
startPreview();
} }
}); });
} }
@ -592,33 +572,24 @@ public abstract class CameraEngine implements
@Override @Override
public final void onSurfaceAvailable() { public final void onSurfaceAvailable() {
LOG.i("onSurfaceAvailable:", "Size is", getPreviewSurfaceSize(Reference.VIEW)); LOG.i("onSurfaceAvailable:", "Size is", getPreviewSurfaceSize(Reference.VIEW));
mHandler.run(new Runnable() { startBind();
@Override startPreview();
public void run() { }
startBind().onSuccessTask(mHandler.getExecutor(), new SuccessContinuation<Void, Void>() {
@NonNull @Override
@Override public final void onSurfaceDestroyed() {
public Task<Void> then(@Nullable Void aVoid) { LOG.i("onSurfaceDestroyed");
return startPreview(); stopPreview(false);
} stopBind(false);
});
}
});
} }
@Override @Override
public final void onSurfaceChanged() { public final void onSurfaceChanged() {
LOG.i("onSurfaceChanged:", "Size is", getPreviewSurfaceSize(Reference.VIEW), LOG.i("onSurfaceChanged:", "Size is", getPreviewSurfaceSize(Reference.VIEW));
"Posting."); mOrchestrator.scheduleStateful("surface changed", CameraState.BIND,
mHandler.run(new Runnable() { new Runnable() {
@Override @Override
public void run() { 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. // Compute a new camera preview size and apply.
Size newSize = computePreviewStreamSize(); Size newSize = computePreviewStreamSize();
if (newSize.equals(mPreviewStreamSize)) { if (newSize.equals(mPreviewStreamSize)) {
@ -643,176 +614,6 @@ public abstract class CameraEngine implements
@EngineThread @EngineThread
protected abstract void onPreviewStreamSizeChanged(); 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 //endregion
//region Final setters and getters //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. * so that we can open the new facing camera.
* @param facing facing * @param facing facing
*/ */
@ -930,10 +731,10 @@ public abstract class CameraEngine implements
final Facing old = mFacing; final Facing old = mFacing;
if (facing != old) { if (facing != old) {
mFacing = facing; mFacing = facing;
mHandler.run(new Runnable() { mOrchestrator.scheduleStateful("facing", CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() < STATE_STARTED) return;
if (collectCameraInfo(facing)) { if (collectCameraInfo(facing)) {
restart(); restart();
} else { } else {
@ -975,12 +776,11 @@ public abstract class CameraEngine implements
public final void setMode(@NonNull Mode mode) { public final void setMode(@NonNull Mode mode) {
if (mode != mMode) { if (mode != mMode) {
mMode = mode; mMode = mode;
mHandler.run(new Runnable() { mOrchestrator.scheduleStateful("mode", CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) { restart();
restart();
}
} }
}); });
} }
@ -1134,22 +934,22 @@ public abstract class CameraEngine implements
/* not final for tests */ /* not final for tests */
public void takePicture(final @NonNull PictureResult.Stub stub) { public void takePicture(final @NonNull PictureResult.Stub stub) {
LOG.i("takePicture", "scheduling"); // Save boolean before scheduling! See how Camera2Engine calls this with a temp value.
mHandler.run(new Runnable() { final boolean metering = mPictureMetering;
mOrchestrator.scheduleStateful("take picture", CameraState.BIND,
new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("takePicture", "performing. BindState:", getBindState(), LOG.i("takePicture:", "running. isTakingPicture:", isTakingPicture());
"isTakingPicture:", isTakingPicture()); if (isTakingPicture()) return;
if (mMode == Mode.VIDEO) { if (mMode == Mode.VIDEO) {
throw new IllegalStateException("Can't take hq pictures while in VIDEO mode"); throw new IllegalStateException("Can't take hq pictures while in VIDEO mode");
} }
if (getBindState() < STATE_STARTED) return;
if (isTakingPicture()) return;
stub.isSnapshot = false; stub.isSnapshot = false;
stub.location = mLocation; stub.location = mLocation;
stub.facing = mFacing; stub.facing = mFacing;
stub.format = mPictureFormat; stub.format = mPictureFormat;
onTakePicture(stub, mPictureMetering); onTakePicture(stub, metering);
} }
}); });
} }
@ -1160,13 +960,13 @@ public abstract class CameraEngine implements
* @param stub a picture stub * @param stub a picture stub
*/ */
public final void takePictureSnapshot(final @NonNull PictureResult.Stub stub) { public final void takePictureSnapshot(final @NonNull PictureResult.Stub stub) {
LOG.i("takePictureSnapshot", "scheduling"); // Save boolean before scheduling! See how Camera2Engine calls this with a temp value.
mHandler.run(new Runnable() { final boolean metering = mPictureSnapshotMetering;
mOrchestrator.scheduleStateful("take picture snapshot", CameraState.BIND,
new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("takePictureSnapshot", "performing. BindState:", LOG.i("takePictureSnapshot:", "running. isTakingPicture:", isTakingPicture());
getBindState(), "isTakingPicture:", isTakingPicture());
if (getBindState() < STATE_STARTED) return;
if (isTakingPicture()) return; if (isTakingPicture()) return;
stub.location = mLocation; stub.location = mLocation;
stub.isSnapshot = true; stub.isSnapshot = true;
@ -1175,7 +975,7 @@ public abstract class CameraEngine implements
// Leave the other parameters to subclasses. // Leave the other parameters to subclasses.
//noinspection ConstantConditions //noinspection ConstantConditions
AspectRatio ratio = AspectRatio.of(getPreviewSurfaceSize(Reference.OUTPUT)); 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) { public final void takeVideo(final @NonNull VideoResult.Stub stub, final @NonNull File file) {
LOG.i("takeVideo", "scheduling"); mOrchestrator.scheduleStateful("take video", CameraState.BIND, new Runnable() {
mHandler.run(new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("takeVideo", "performing. BindState:", getBindState(), LOG.i("takeVideo:", "running. isTakingVideo:", isTakingVideo());
"isTakingVideo:", isTakingVideo());
if (getBindState() < STATE_STARTED) return;
if (isTakingVideo()) return; if (isTakingVideo()) return;
if (mMode == Mode.PICTURE) { if (mMode == Mode.PICTURE) {
throw new IllegalStateException("Can't record video while in PICTURE mode"); 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, public final void takeVideoSnapshot(@NonNull final VideoResult.Stub stub,
@NonNull final File file) { @NonNull final File file) {
LOG.i("takeVideoSnapshot", "scheduling"); mOrchestrator.scheduleStateful("take video snapshot", CameraState.BIND,
mHandler.run(new Runnable() { new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("takeVideoSnapshot", "performing. BindState:", getBindState(), LOG.i("takeVideoSnapshot:", "running. isTakingVideo:", isTakingVideo());
"isTakingVideo:", isTakingVideo());
if (getBindState() < STATE_STARTED) return;
if (isTakingVideo()) return;
stub.file = file; stub.file = file;
stub.isSnapshot = true; stub.isSnapshot = true;
stub.videoCodec = mVideoCodec; stub.videoCodec = mVideoCodec;
@ -1260,11 +1054,10 @@ public abstract class CameraEngine implements
} }
public final void stopVideo() { public final void stopVideo() {
LOG.i("stopVideo", "posting"); mOrchestrator.schedule("stop video", true, new Runnable() {
mHandler.run(new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("stopVideo", "executing.", "isTakingVideo?", isTakingVideo()); LOG.i("stopVideo", "running. isTakingVideo?", isTakingVideo());
onStopVideo(); 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 @Override
public final void start(@NonNull ActionHolder holder) { public final void start(@NonNull ActionHolder holder) {
this.holder = holder;
holder.addAction(this); holder.addAction(this);
if (holder.getLastResult(this) != null) { if (holder.getLastResult(this) != null) {
onStart(holder); onStart(holder);
@ -64,6 +65,8 @@ public abstract class BaseAction implements Action {
*/ */
@CallSuper @CallSuper
protected void onStart(@NonNull ActionHolder holder) { 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; this.holder = holder;
// Overrideable // 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.CaptureRequest;
import android.hardware.camera2.CaptureResult; import android.hardware.camera2.CaptureResult;
@ -9,11 +9,12 @@ import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.engine.Camera2Engine;
import com.otaliastudios.cameraview.engine.action.ActionHolder; import com.otaliastudios.cameraview.engine.action.ActionHolder;
import com.otaliastudios.cameraview.engine.action.BaseAction; import com.otaliastudios.cameraview.engine.action.BaseAction;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class LogAction extends BaseAction { public class LogAction extends BaseAction {
private final static CameraLogger LOG private final static CameraLogger LOG
= CameraLogger.create(Camera2Engine.class.getSimpleName()); = 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 android.view.View;
import com.otaliastudios.cameraview.controls.Grid; 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. * 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 ColorDrawable vert;
private final float width; private final float width;
@VisibleForTesting interface DrawCallback {
Op<Integer> drawOp = new Op<>(); void onDraw(int lines);
}
@VisibleForTesting DrawCallback callback;
public GridLinesLayout(@NonNull Context context) { public GridLinesLayout(@NonNull Context context) {
this(context, null); this(context, null);
@ -117,7 +119,6 @@ public class GridLinesLayout extends View {
@Override @Override
protected void onDraw(@NonNull Canvas canvas) { protected void onDraw(@NonNull Canvas canvas) {
super.onDraw(canvas); super.onDraw(canvas);
drawOp.start();
int count = getLineCount(); int count = getLineCount();
for (int n = 0; n < count; n++) { for (int n = 0; n < count; n++) {
float pos = getLinePosition(n); float pos = getLinePosition(n);
@ -132,6 +133,8 @@ public class GridLinesLayout extends View {
vert.draw(canvas); vert.draw(canvas);
canvas.translate(- pos * getWidth(), 0); 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.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.engine.CameraEngine; import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
/** /**
@ -56,8 +55,11 @@ public abstract class CameraPreview<T extends View, Output> {
void onSurfaceDestroyed(); void onSurfaceDestroyed();
} }
@VisibleForTesting protected interface CropCallback {
Op<Void> mCropOp = new Op<>(); void onCrop();
}
@VisibleForTesting CropCallback mCropCallback;
private SurfaceCallback mSurfaceCallback; private SurfaceCallback mSurfaceCallback;
private T mView; private T mView;
boolean mCropping; boolean mCropping;
@ -152,7 +154,7 @@ public abstract class CameraPreview<T extends View, Output> {
mInputStreamWidth = width; mInputStreamWidth = width;
mInputStreamHeight = height; mInputStreamHeight = height;
if (mInputStreamWidth > 0 && mInputStreamHeight > 0) { if (mInputStreamWidth > 0 && mInputStreamHeight > 0) {
crop(mCropOp); crop(mCropCallback);
} }
} }
@ -194,7 +196,7 @@ public abstract class CameraPreview<T extends View, Output> {
mOutputSurfaceWidth = width; mOutputSurfaceWidth = width;
mOutputSurfaceHeight = height; mOutputSurfaceHeight = height;
if (mOutputSurfaceWidth > 0 && mOutputSurfaceHeight > 0) { if (mOutputSurfaceWidth > 0 && mOutputSurfaceHeight > 0) {
crop(mCropOp); crop(mCropCallback);
} }
if (mSurfaceCallback != null) { if (mSurfaceCallback != null) {
mSurfaceCallback.onSurfaceAvailable(); mSurfaceCallback.onSurfaceAvailable();
@ -213,7 +215,7 @@ public abstract class CameraPreview<T extends View, Output> {
mOutputSurfaceWidth = width; mOutputSurfaceWidth = width;
mOutputSurfaceHeight = height; mOutputSurfaceHeight = height;
if (width > 0 && height > 0) { if (width > 0 && height > 0) {
crop(mCropOp); crop(mCropCallback);
} }
if (mSurfaceCallback != null) { if (mSurfaceCallback != null) {
mSurfaceCallback.onSurfaceChanged(); 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). * There might still be some absolute difference (e.g. same ratio but bigger / smaller).
* However that should be already managed by the framework. * 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. // The base implementation does not support cropping.
op.start(); if (callback != null) callback.onCrop();
op.end(null);
} }
/** /**

@ -5,6 +5,7 @@ import android.graphics.SurfaceTexture;
import android.opengl.GLSurfaceView; import android.opengl.GLSurfaceView;
import android.opengl.Matrix; import android.opengl.Matrix;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -14,7 +15,6 @@ import android.view.ViewGroup;
import com.otaliastudios.cameraview.R; import com.otaliastudios.cameraview.R;
import com.otaliastudios.cameraview.internal.egl.EglViewport; 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.Filter;
import com.otaliastudios.cameraview.filter.NoFilter; import com.otaliastudios.cameraview.filter.NoFilter;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
@ -270,8 +270,7 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
* See {@link Renderer#onDrawFrame(GL10)}. * See {@link Renderer#onDrawFrame(GL10)}.
*/ */
@Override @Override
protected void crop(@NonNull Op<Void> op) { protected void crop(@Nullable final CropCallback callback) {
op.start();
if (mInputStreamWidth > 0 && mInputStreamHeight > 0 && mOutputSurfaceWidth > 0 if (mInputStreamWidth > 0 && mInputStreamHeight > 0 && mOutputSurfaceWidth > 0
&& mOutputSurfaceHeight > 0) { && mOutputSurfaceHeight > 0) {
float scaleX = 1f, scaleY = 1f; float scaleX = 1f, scaleY = 1f;
@ -289,7 +288,7 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
mCropScaleY = 1F / scaleY; mCropScaleY = 1F / scaleY;
getView().requestRender(); 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.TaskCompletionSource;
import com.google.android.gms.tasks.Tasks; import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.R; import com.otaliastudios.cameraview.R;
import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -91,14 +90,13 @@ public class TextureCameraPreview extends CameraPreview<TextureView, SurfaceText
} }
@Override @Override
protected void crop(final @NonNull Op<Void> op) { protected void crop(@Nullable final CropCallback callback) {
op.start();
getView().post(new Runnable() { getView().post(new Runnable() {
@Override @Override
public void run() { public void run() {
if (mInputStreamHeight == 0 || mInputStreamWidth == 0 || if (mInputStreamHeight == 0 || mInputStreamWidth == 0 ||
mOutputSurfaceHeight == 0 || mOutputSurfaceWidth == 0) { mOutputSurfaceHeight == 0 || mOutputSurfaceWidth == 0) {
op.end(null); if (callback != null) callback.onCrop();
return; return;
} }
float scaleX = 1f, scaleY = 1f; float scaleX = 1f, scaleY = 1f;
@ -118,7 +116,7 @@ public class TextureCameraPreview extends CameraPreview<TextureView, SurfaceText
mCropping = scaleX > 1.02f || scaleY > 1.02f; mCropping = scaleX > 1.02f || scaleY > 1.02f;
LOG.i("crop:", "applied scaleX=", scaleX); LOG.i("crop:", "applied scaleX=", scaleX);
LOG.i("crop:", "applied scaleY=", scaleY); LOG.i("crop:", "applied scaleY=", scaleY);
op.end(null); if (callback != null) callback.onCrop();
} }
}); });
} }

Loading…
Cancel
Save