From a63cd847261d24ce82ca6d869d9cef6dae80dc4c Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Wed, 16 Oct 2019 23:58:30 +0200 Subject: [PATCH] Bug fixes (#651) * Use CopyOnWriteArrayList for Camera2 action list. Fixes #649 * Listen to suspicious display rotation events. Fixes #623 * Correctly release GL objects on activity pause. Fixes #648 * Add synchronization in SnapshotVideoRecorder. Fixes #642 * Make Holder.getLastResult nullable. Fixes #621 * Fix tests --- .../internal/utils/OrientationHelperTest.java | 111 ++++++++++-------- .../otaliastudios/cameraview/CameraView.java | 21 +++- .../cameraview/engine/Camera2Engine.java | 28 ++--- .../cameraview/engine/LogAction.java | 2 +- .../engine/action/ActionHolder.java | 3 +- .../cameraview/engine/lock/ExposureLock.java | 14 ++- .../cameraview/engine/lock/FocusLock.java | 29 +++-- .../engine/lock/WhiteBalanceLock.java | 15 ++- .../engine/meter/ExposureMeter.java | 14 ++- .../engine/meter/ExposureReset.java | 5 +- .../cameraview/engine/meter/FocusMeter.java | 18 ++- .../cameraview/engine/meter/FocusReset.java | 4 +- .../engine/meter/WhiteBalanceMeter.java | 15 ++- .../internal/utils/OrientationHelper.java | 100 +++++++++++----- .../picture/Snapshot2PictureRecorder.java | 8 +- .../cameraview/preview/GlCameraPreview.java | 39 +++--- .../video/SnapshotVideoRecorder.java | 56 ++++++--- .../cameraview/video/VideoRecorder.java | 42 +++++-- .../cameraview/demo/CameraActivity.java | 4 +- 19 files changed, 338 insertions(+), 190 deletions(-) diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/utils/OrientationHelperTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/utils/OrientationHelperTest.java index 011176cb..b5fde579 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/utils/OrientationHelperTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/utils/OrientationHelperTest.java @@ -41,61 +41,68 @@ public class OrientationHelperTest extends BaseTest { @Test public void testEnable() { - assertNotNull(helper.mListener); - assertEquals(helper.getDisplayOffset(), -1); - assertEquals(helper.getDeviceOrientation(), -1); - - helper.enable(getContext()); - assertNotNull(helper.mListener); - assertNotEquals(helper.getDisplayOffset(), -1); // Don't know about device orientation. - - // Ensure nothing bad if called twice. - helper.enable(getContext()); - assertNotNull(helper.mListener); - assertNotEquals(helper.getDisplayOffset(), -1); - - helper.disable(); - assertNotNull(helper.mListener); - assertEquals(helper.getDisplayOffset(), -1); - assertEquals(helper.getDeviceOrientation(), -1); + // On some API levels, enable() needs to be run on the UI thread. + uiSync(new Runnable() { + @Override + public void run() { + assertEquals(helper.getLastDisplayOffset(), -1); + assertEquals(helper.getLastDeviceOrientation(), -1); + + helper.enable(); + assertNotEquals(helper.getLastDisplayOffset(), -1); // Don't know about device orientation. + + // Ensure nothing bad if called twice. + helper.enable(); + assertNotEquals(helper.getLastDisplayOffset(), -1); + + helper.disable(); + assertEquals(helper.getLastDisplayOffset(), -1); + assertEquals(helper.getLastDeviceOrientation(), -1); + } + }); } @Test public void testRotation() { - - // Sometimes (on some APIs) the helper will trigger an update to 0 - // right after enabling. But that's fine for us, times(1) will be OK either way. - helper.enable(getContext()); - helper.mListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN); - assertEquals(helper.getDeviceOrientation(), 0); - helper.mListener.onOrientationChanged(10); - assertEquals(helper.getDeviceOrientation(), 0); - helper.mListener.onOrientationChanged(-10); - assertEquals(helper.getDeviceOrientation(), 0); - helper.mListener.onOrientationChanged(44); - assertEquals(helper.getDeviceOrientation(), 0); - helper.mListener.onOrientationChanged(360); - assertEquals(helper.getDeviceOrientation(), 0); - - // Callback called just once. - verify(callback, times(1)).onDeviceOrientationChanged(0); - - helper.mListener.onOrientationChanged(90); - helper.mListener.onOrientationChanged(91); - assertEquals(helper.getDeviceOrientation(), 90); - verify(callback, times(1)).onDeviceOrientationChanged(90); - - helper.mListener.onOrientationChanged(180); - assertEquals(helper.getDeviceOrientation(), 180); - verify(callback, times(1)).onDeviceOrientationChanged(180); - - helper.mListener.onOrientationChanged(270); - assertEquals(helper.getDeviceOrientation(), 270); - verify(callback, times(1)).onDeviceOrientationChanged(270); - - // It is still 270 after ORIENTATION_UNKNOWN - helper.mListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN); - assertEquals(helper.getDeviceOrientation(), 270); - verify(callback, times(1)).onDeviceOrientationChanged(270); + // On some API levels, enable() needs to be run on the UI thread. + uiSync(new Runnable() { + @Override + public void run() { + // Sometimes (on some APIs) the helper will trigger an update to 0 + // right after enabling. But that's fine for us, times(1) will be OK either way. + helper.enable(); + helper.mDeviceOrientationListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN); + assertEquals(helper.getLastDeviceOrientation(), 0); + helper.mDeviceOrientationListener.onOrientationChanged(10); + assertEquals(helper.getLastDeviceOrientation(), 0); + helper.mDeviceOrientationListener.onOrientationChanged(-10); + assertEquals(helper.getLastDeviceOrientation(), 0); + helper.mDeviceOrientationListener.onOrientationChanged(44); + assertEquals(helper.getLastDeviceOrientation(), 0); + helper.mDeviceOrientationListener.onOrientationChanged(360); + assertEquals(helper.getLastDeviceOrientation(), 0); + + // Callback called just once. + verify(callback, times(1)).onDeviceOrientationChanged(0); + + helper.mDeviceOrientationListener.onOrientationChanged(90); + helper.mDeviceOrientationListener.onOrientationChanged(91); + assertEquals(helper.getLastDeviceOrientation(), 90); + verify(callback, times(1)).onDeviceOrientationChanged(90); + + helper.mDeviceOrientationListener.onOrientationChanged(180); + assertEquals(helper.getLastDeviceOrientation(), 180); + verify(callback, times(1)).onDeviceOrientationChanged(180); + + helper.mDeviceOrientationListener.onOrientationChanged(270); + assertEquals(helper.getLastDeviceOrientation(), 270); + verify(callback, times(1)).onDeviceOrientationChanged(270); + + // It is still 270 after ORIENTATION_UNKNOWN + helper.mDeviceOrientationListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN); + assertEquals(helper.getLastDeviceOrientation(), 270); + verify(callback, times(1)).onDeviceOrientationChanged(270); + } + }); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index ccbf7428..f55e9289 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -356,7 +356,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { // attached. That's why we instantiate the preview here. doInstantiatePreview(); } - mOrientationHelper.enable(getContext()); + mOrientationHelper.enable(); } @Override @@ -722,8 +722,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver { if (mCameraPreview != null) mCameraPreview.onResume(); if (checkPermissions(getAudio())) { // Update display orientation for current CameraEngine - mOrientationHelper.enable(getContext()); - mCameraEngine.getAngles().setDisplayOffset(mOrientationHelper.getDisplayOffset()); + mOrientationHelper.enable(); + mCameraEngine.getAngles().setDisplayOffset(mOrientationHelper.getLastDisplayOffset()); mCameraEngine.start(); } } @@ -2073,7 +2073,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { @Override public void onDeviceOrientationChanged(int deviceOrientation) { mLogger.i("onDeviceOrientationChanged", deviceOrientation); - int displayOffset = mOrientationHelper.getDisplayOffset(); + int displayOffset = mOrientationHelper.getLastDisplayOffset(); if (!mUseDeviceOrientation) { // To fool the engine to return outputs in the VIEW reference system, // The device orientation should be set to -displayOffset. @@ -2093,6 +2093,19 @@ public class CameraView extends FrameLayout implements LifecycleObserver { }); } + @Override + public void onDisplayOffsetChanged(int displayOffset, boolean willRecreate) { + mLogger.i("onDisplayOffsetChanged", displayOffset, "recreate:", willRecreate); + if (isOpened() && !willRecreate) { + // Display offset changes when the device rotation lock is off and the activity + // is free to rotate. However, some changes will NOT recreate the activity, namely + // 180 degrees flips. In this case, we must restart the camera manually. + mLogger.w("onDisplayOffsetChanged", "restarting the camera."); + close(); + open(); + } + } + @Override public void dispatchOnZoomChanged(final float newValue, @Nullable final PointF[] fingers) { mLogger.i("dispatchOnZoomChanged", newValue); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java index a35de766..ac657b46 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java @@ -70,6 +70,7 @@ import com.otaliastudios.cameraview.video.SnapshotVideoRecorder; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; @RequiresApi(Build.VERSION_CODES.LOLLIPOP) @@ -111,7 +112,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv private final boolean mPictureCaptureStopsPreview = false; // can be configurable at some point // Actions - private final List mActions = new ArrayList<>(); + // Use COW to properly synchronize the list. We'll iterate much more than mutate + private final List mActions = new CopyOnWriteArrayList<>(); private MeterAction mMeterAction; public Camera2Engine(Callback callback) { @@ -1428,28 +1430,14 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv @Override public void addAction(final @NonNull Action action) { - // This is likely to be called during a Capture callback while we iterate - // on the actions list, or worse, from other threads. We must use mHandler.post. - mHandler.post(new Runnable() { - @Override - public void run() { - if (!mActions.contains(action)) { - mActions.add(action); - } - } - }); + if (!mActions.contains(action)) { + mActions.add(action); + } } @Override public void removeAction(final @NonNull Action action) { - // This is likely to be called during a Capture callback while we iterate - // on the actions list, or worse, from other threads. We must use mHandler.post. - mHandler.post(new Runnable() { - @Override - public void run() { - mActions.remove(action); - } - }); + mActions.remove(action); } @NonNull @@ -1458,7 +1446,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv return mCameraCharacteristics; } - @NonNull + @Nullable @Override public TotalCaptureResult getLastResult(@NonNull Action action) { return mLastRepeatingResult; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/LogAction.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/LogAction.java index d51567a2..8c20a83f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/LogAction.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/LogAction.java @@ -36,7 +36,7 @@ class LogAction extends BaseAction { " afState: " + afState + " afTriggerState: " + afTriggerState; if (!log.equals(lastLog)) { lastLog = log; - LOG.i(log); + LOG.v(log); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/ActionHolder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/ActionHolder.java index 792aa590..7da89570 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/ActionHolder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/ActionHolder.java @@ -8,6 +8,7 @@ import android.hardware.camera2.TotalCaptureResult; import android.os.Build; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; /** @@ -51,7 +52,7 @@ public interface ActionHolder { * @param action action * @return last result */ - @NonNull + @Nullable TotalCaptureResult getLastResult(@NonNull Action action); /** diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/ExposureLock.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/ExposureLock.java index dd3f91ab..923a89dd 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/ExposureLock.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/ExposureLock.java @@ -40,10 +40,16 @@ public class ExposureLock extends BaseLock { @Override protected boolean checkShouldSkip(@NonNull ActionHolder holder) { - Integer aeState = holder.getLastResult(this).get(CaptureResult.CONTROL_AE_STATE); - boolean result = aeState != null && aeState == CaptureResult.CONTROL_AE_STATE_LOCKED; - LOG.i("checkShouldSkip:", result); - return result; + CaptureResult lastResult = holder.getLastResult(this); + if (lastResult != null) { + Integer aeState = lastResult.get(CaptureResult.CONTROL_AE_STATE); + boolean result = aeState != null && aeState == CaptureResult.CONTROL_AE_STATE_LOCKED; + LOG.i("checkShouldSkip:", result); + return result; + } else { + LOG.i("checkShouldSkip: false - lastResult is null."); + return false; + } } @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/FocusLock.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/FocusLock.java index fc0d69a5..408bea2b 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/FocusLock.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/FocusLock.java @@ -35,18 +35,23 @@ public class FocusLock extends BaseLock { @Override protected boolean checkShouldSkip(@NonNull ActionHolder holder) { CaptureResult lastResult = holder.getLastResult(this); - Integer afState = lastResult.get(CaptureResult.CONTROL_AF_STATE); - boolean afStateOk = afState != null && - (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED - || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED - || afState == CaptureResult.CONTROL_AF_STATE_INACTIVE - || afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED - || afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED); - Integer afMode = lastResult.get(CaptureResult.CONTROL_AF_MODE); - boolean afModeOk = afMode != null && afMode == CaptureResult.CONTROL_AF_MODE_AUTO; - boolean result = afStateOk && afModeOk; - LOG.i("checkShouldSkip:", result); - return result; + if (lastResult != null) { + Integer afState = lastResult.get(CaptureResult.CONTROL_AF_STATE); + boolean afStateOk = afState != null && + (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED + || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED + || afState == CaptureResult.CONTROL_AF_STATE_INACTIVE + || afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED + || afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED); + Integer afMode = lastResult.get(CaptureResult.CONTROL_AF_MODE); + boolean afModeOk = afMode != null && afMode == CaptureResult.CONTROL_AF_MODE_AUTO; + boolean result = afStateOk && afModeOk; + LOG.i("checkShouldSkip:", result); + return result; + } else { + LOG.i("checkShouldSkip: false - lastResult is null."); + return false; + } } @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/WhiteBalanceLock.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/WhiteBalanceLock.java index c56dcc64..ae547933 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/WhiteBalanceLock.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/WhiteBalanceLock.java @@ -33,10 +33,17 @@ public class WhiteBalanceLock extends BaseLock { @Override protected boolean checkShouldSkip(@NonNull ActionHolder holder) { - Integer awbState = holder.getLastResult(this).get(CaptureResult.CONTROL_AWB_STATE); - boolean result = awbState != null && awbState == CaptureRequest.CONTROL_AWB_STATE_LOCKED; - LOG.i("checkShouldSkip:", result); - return result; + CaptureResult lastResult = holder.getLastResult(this); + if (lastResult != null) { + Integer awbState = lastResult.get(CaptureResult.CONTROL_AWB_STATE); + boolean result = awbState != null + && awbState == CaptureRequest.CONTROL_AWB_STATE_LOCKED; + LOG.i("checkShouldSkip:", result); + return result; + } else { + LOG.i("checkShouldSkip: false - lastResult is null."); + return false; + } } @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureMeter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureMeter.java index 64c6d361..ed904932 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureMeter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureMeter.java @@ -50,10 +50,16 @@ public class ExposureMeter extends BaseMeter { @Override protected boolean checkShouldSkip(@NonNull ActionHolder holder) { - Integer aeState = holder.getLastResult(this).get(CaptureResult.CONTROL_AE_STATE); - boolean result = aeState != null && aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED; - LOG.i("checkShouldSkip:", result); - return result; + CaptureResult lastResult = holder.getLastResult(this); + if (lastResult != null) { + Integer aeState = lastResult.get(CaptureResult.CONTROL_AE_STATE); + boolean result = aeState != null && aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED; + LOG.i("checkShouldSkip:", result); + return result; + } else { + LOG.i("checkShouldSkip: false - lastResult is null."); + return false; + } } @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureReset.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureReset.java index c3755df7..fc676d3b 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureReset.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureReset.java @@ -37,8 +37,9 @@ public class ExposureReset extends BaseReset { } // NOTE: precapture might not be supported, in which case I think it will be ignored. - Integer trigger = holder.getLastResult(this) - .get(CaptureResult.CONTROL_AE_PRECAPTURE_TRIGGER); + CaptureResult lastResult = holder.getLastResult(this); + Integer trigger = lastResult == null ? null + : lastResult.get(CaptureResult.CONTROL_AE_PRECAPTURE_TRIGGER); LOG.i("onStarted:", "last precapture trigger is", trigger); if (trigger != null && trigger == CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) { LOG.i("onStarted:", "canceling precapture."); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusMeter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusMeter.java index 4d5c2f85..e7b59bfd 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusMeter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusMeter.java @@ -40,12 +40,18 @@ public class FocusMeter extends BaseMeter { @Override protected boolean checkShouldSkip(@NonNull ActionHolder holder) { - Integer afState = holder.getLastResult(this).get(CaptureResult.CONTROL_AF_STATE); - boolean result = afState != null && - (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || - afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED); - LOG.i("checkShouldSkip:", result); - return result; + CaptureResult lastResult = holder.getLastResult(this); + if (lastResult != null) { + Integer afState = lastResult.get(CaptureResult.CONTROL_AF_STATE); + boolean result = afState != null && + (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || + afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED); + LOG.i("checkShouldSkip:", result); + return result; + } else { + LOG.i("checkShouldSkip: false - lastResult is null."); + return false; + } } @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusReset.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusReset.java index c1d1883a..afe4e49d 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusReset.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusReset.java @@ -36,7 +36,9 @@ public class FocusReset extends BaseReset { } // NOTE: trigger might not be supported, in which case I think it will be ignored. - Integer trigger = holder.getLastResult(this).get(CaptureResult.CONTROL_AF_TRIGGER); + CaptureResult lastResult = holder.getLastResult(this); + Integer trigger = lastResult == null ? null + : lastResult.get(CaptureResult.CONTROL_AF_TRIGGER); LOG.w("onStarted:", "last focus trigger is", trigger); if (trigger != null && trigger == CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) { holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_TRIGGER, diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceMeter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceMeter.java index 0217b815..df13cbb2 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceMeter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceMeter.java @@ -40,10 +40,17 @@ public class WhiteBalanceMeter extends BaseMeter { @Override protected boolean checkShouldSkip(@NonNull ActionHolder holder) { - Integer awbState = holder.getLastResult(this).get(CaptureResult.CONTROL_AWB_STATE); - boolean result = awbState != null && awbState == CaptureRequest.CONTROL_AWB_STATE_CONVERGED; - LOG.i("checkShouldSkip:", result); - return result; + CaptureResult lastResult = holder.getLastResult(this); + if (lastResult != null) { + Integer awbState = lastResult.get(CaptureResult.CONTROL_AWB_STATE); + boolean result = awbState != null + && awbState == CaptureRequest.CONTROL_AWB_STATE_CONVERGED; + LOG.i("checkShouldSkip:", result); + return result; + } else { + LOG.i("checkShouldSkip: false - lastResult is null."); + return false; + } } @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/OrientationHelper.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/OrientationHelper.java index a72a626e..36c5f1d5 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/OrientationHelper.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/OrientationHelper.java @@ -5,6 +5,8 @@ import android.hardware.SensorManager; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import android.hardware.display.DisplayManager; +import android.os.Build; import android.view.Display; import android.view.OrientationEventListener; import android.view.Surface; @@ -12,21 +14,28 @@ import android.view.WindowManager; /** * Helps with keeping track of both device orientation (which changes when device is rotated) - * and the display offset (which depends on the activity orientation - * wrt the device default orientation). + * and the display offset (which depends on the activity orientation wrt the device default + * orientation). */ public class OrientationHelper { /** - * Receives callback about the device orientation changes. + * Receives callback about the orientation changes. */ public interface Callback { void onDeviceOrientationChanged(int deviceOrientation); + void onDisplayOffsetChanged(int displayOffset, boolean willRecreate); } - @VisibleForTesting final OrientationEventListener mListener; + private final Context mContext; private final Callback mCallback; + + @VisibleForTesting + final OrientationEventListener mDeviceOrientationListener; private int mDeviceOrientation = -1; + + @VisibleForTesting + final DisplayManager.DisplayListener mDisplayOffsetListener; private int mDisplayOffset = -1; /** @@ -35,57 +44,78 @@ public class OrientationHelper { * @param callback a {@link Callback} */ public OrientationHelper(@NonNull Context context, @NonNull Callback callback) { + mContext = context; mCallback = callback; - mListener = new OrientationEventListener(context.getApplicationContext(), + mDeviceOrientationListener = new OrientationEventListener(context.getApplicationContext(), SensorManager.SENSOR_DELAY_NORMAL) { @SuppressWarnings("ConstantConditions") @Override public void onOrientationChanged(int orientation) { - int or = 0; + int deviceOrientation = 0; if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) { - or = mDeviceOrientation != -1 ? mDeviceOrientation : 0; + deviceOrientation = mDeviceOrientation != -1 ? mDeviceOrientation : 0; } else if (orientation >= 315 || orientation < 45) { - or = 0; + deviceOrientation = 0; } else if (orientation >= 45 && orientation < 135) { - or = 90; + deviceOrientation = 90; } else if (orientation >= 135 && orientation < 225) { - or = 180; + deviceOrientation = 180; } else if (orientation >= 225 && orientation < 315) { - or = 270; + deviceOrientation = 270; } - if (or != mDeviceOrientation) { - mDeviceOrientation = or; + if (deviceOrientation != mDeviceOrientation) { + mDeviceOrientation = deviceOrientation; mCallback.onDeviceOrientationChanged(mDeviceOrientation); } } }; + if (Build.VERSION.SDK_INT >= 17) { + mDisplayOffsetListener = new DisplayManager.DisplayListener() { + public void onDisplayAdded(int displayId) { } + public void onDisplayRemoved(int displayId) { } + + @Override + public void onDisplayChanged(int displayId) { + int oldDisplayOffset = mDisplayOffset; + int newDisplayOffset = findDisplayOffset(); + if (newDisplayOffset != oldDisplayOffset) { + mDisplayOffset = newDisplayOffset; + // With 180 degrees flips, the activity is not recreated. + boolean willRecreate = Math.abs(newDisplayOffset - oldDisplayOffset) != 180; + mCallback.onDisplayOffsetChanged(newDisplayOffset, willRecreate); + } + } + }; + } else { + mDisplayOffsetListener = null; + } } /** * Enables this listener. - * @param context a context */ - public void enable(@NonNull Context context) { - Display display = ((WindowManager) context - .getSystemService(Context.WINDOW_SERVICE)) - .getDefaultDisplay(); - switch (display.getRotation()) { - case Surface.ROTATION_0: mDisplayOffset = 0; break; - case Surface.ROTATION_90: mDisplayOffset = 90; break; - case Surface.ROTATION_180: mDisplayOffset = 180; break; - case Surface.ROTATION_270: mDisplayOffset = 270; break; - default: mDisplayOffset = 0; break; + public void enable() { + mDisplayOffset = findDisplayOffset(); + if (Build.VERSION.SDK_INT >= 17) { + DisplayManager manager = (DisplayManager) + mContext.getSystemService(Context.DISPLAY_SERVICE); + manager.registerDisplayListener(mDisplayOffsetListener, null); } - mListener.enable(); + mDeviceOrientationListener.enable(); } /** * Disables this listener. */ public void disable() { - mListener.disable(); + mDeviceOrientationListener.disable(); + if (Build.VERSION.SDK_INT >= 17) { + DisplayManager manager = (DisplayManager) + mContext.getSystemService(Context.DISPLAY_SERVICE); + manager.unregisterDisplayListener(mDisplayOffsetListener); + } mDisplayOffset = -1; mDeviceOrientation = -1; } @@ -94,7 +124,8 @@ public class OrientationHelper { * Returns the current device orientation. * @return device orientation */ - public int getDeviceOrientation() { + @SuppressWarnings("WeakerAccess") + public int getLastDeviceOrientation() { return mDeviceOrientation; } @@ -102,7 +133,20 @@ public class OrientationHelper { * Returns the current display offset. * @return display offset */ - public int getDisplayOffset() { + public int getLastDisplayOffset() { return mDisplayOffset; } + + private int findDisplayOffset() { + Display display = ((WindowManager) mContext + .getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay(); + switch (display.getRotation()) { + case Surface.ROTATION_0: return 0; + case Surface.ROTATION_90: return 90; + case Surface.ROTATION_180: return 180; + case Surface.ROTATION_270: return 270; + default: return 0; + } + } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot2PictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot2PictureRecorder.java index c618b359..8e074ff3 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot2PictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot2PictureRecorder.java @@ -106,7 +106,13 @@ public class Snapshot2PictureRecorder extends SnapshotGlPictureRecorder { } }); - Integer aeState = mHolder.getLastResult(mAction).get(CaptureResult.CONTROL_AE_STATE); + CaptureResult lastResult = mHolder.getLastResult(mAction); + if (lastResult == null) { + LOG.w("Picture snapshot requested very early, before the first preview frame.", + "Metering might not work as intended."); + } + Integer aeState = lastResult == null ? null + : lastResult.get(CaptureResult.CONTROL_AE_STATE); mActionNeeded = engine.getPictureSnapshotMetering() && aeState != null && aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java index 4e32e1ef..ffabc8d4 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java @@ -2,7 +2,6 @@ package com.otaliastudios.cameraview.preview; import android.content.Context; import android.graphics.SurfaceTexture; -import android.hardware.camera2.CaptureResult; import android.opengl.GLSurfaceView; import android.opengl.Matrix; import androidx.annotation.NonNull; @@ -20,8 +19,6 @@ import com.otaliastudios.cameraview.filter.Filter; import com.otaliastudios.cameraview.filter.NoFilter; import com.otaliastudios.cameraview.size.AspectRatio; -import java.util.Collections; -import java.util.HashSet; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; @@ -87,9 +84,10 @@ public class GlCameraPreview extends FilterCameraPreview