From 30ffce08f511fbed4cf18daa743778b541583351 Mon Sep 17 00:00:00 2001 From: Andy Huibers Date: Thu, 31 Jul 2014 16:31:43 -0700 Subject: [PATCH] [OC] Add autofocus (tap-to-focus, UI and pre-shot focus) to OneCamera. Change-Id: Ic88359ad27b8e9ee0e74c8d1bd6ecb71fe3c24f0 --- src/com/android/camera/CaptureModule.java | 156 ++++++++- src/com/android/camera/CaptureModuleUI.java | 9 +- src/com/android/camera/one/OneCamera.java | 61 +++- src/com/android/camera/one/v2/OneCameraImpl.java | 395 ++++++++++++++++++++++- 4 files changed, 582 insertions(+), 39 deletions(-) diff --git a/src/com/android/camera/CaptureModule.java b/src/com/android/camera/CaptureModule.java index 0ade6b164..71eba6859 100644 --- a/src/com/android/camera/CaptureModule.java +++ b/src/com/android/camera/CaptureModule.java @@ -47,6 +47,8 @@ import com.android.camera.debug.Log.Tag; import com.android.camera.hardware.HardwareSpec; import com.android.camera.module.ModuleController; import com.android.camera.one.OneCamera; +import com.android.camera.one.OneCamera.AutoFocusMode; +import com.android.camera.one.OneCamera.AutoFocusState; import com.android.camera.one.OneCamera.CaptureReadyCallback; import com.android.camera.one.OneCamera.Facing; import com.android.camera.one.OneCamera.OpenCallback; @@ -62,6 +64,8 @@ import com.android.camera.ui.PreviewStatusListener; import com.android.camera.ui.TouchCoordinate; import com.android.camera.util.CameraUtil; import com.android.camera.util.Size; +import com.android.camera.util.SystemProperties; +import com.android.camera.util.UsageStatistics; import com.android.camera2.R; import com.android.ex.camera2.portability.CameraAgent.CameraProxy; @@ -87,6 +91,7 @@ public class CaptureModule extends CameraModule implements MediaSaver.QueueListener, ModuleController, OneCamera.PictureCallback, + OneCamera.FocusStateListener, PreviewStatusListener.PreviewAreaChangedListener, RemoteCameraModule, SensorEventListener, @@ -140,6 +145,29 @@ public class CaptureModule extends CameraModule } }; + /** + * Show AF target in center of preview and start animation. + */ + Runnable mShowAutoFocusTargetInCenterRunnable = new Runnable() { + @Override + public void run() { + mUI.setAutoFocusTarget(((int) (mPreviewArea.left + mPreviewArea.right)) / 2, + ((int) (mPreviewArea.top + mPreviewArea.bottom)) / 2); + mUI.showAutoFocusInProgress(); + } + }; + + /** + * Hide AF target UI element. + */ + Runnable mHideAutoFocusTargetRunnable = new Runnable() { + @Override + public void run() { + // showAutoFocusSuccess() just hides the AF UI. + mUI.showAutoFocusSuccess(); + } + }; + private static final Tag TAG = new Tag("CaptureModule"); private static final String PHOTO_MODULE_STRING_ID = "PhotoModule"; /** Enable additional debug output. */ @@ -152,6 +180,12 @@ public class CaptureModule extends CameraModule */ private static final int ON_RESUME_TASKS_DELAY_MSEC = 20; + /** System Properties switch to enable debugging focus UI. */ + private static final String PROP_FOCUS_DEBUG_UI_KEY = "persist.camera.focus_debug_ui"; + private static final String PROP_FOCUS_DEBUG_UI_OFF = "0"; + private static final boolean FOCUS_DEBUG_UI = !PROP_FOCUS_DEBUG_UI_OFF + .equals(SystemProperties.get(PROP_FOCUS_DEBUG_UI_KEY, PROP_FOCUS_DEBUG_UI_OFF)); + private final Object mDimensionLock = new Object(); /** * Lock for race conditions in the SurfaceTextureListener callbacks. @@ -186,6 +220,16 @@ public class CaptureModule extends CameraModule private ModuleState mState = ModuleState.IDLE; /** Current orientation of the device. */ private int mOrientation = OrientationEventListener.ORIENTATION_UNKNOWN; + /** Current zoom value. */ + private float mZoomValue = 1f; + + /** True if in AF tap-to-focus sequence. */ + private boolean mTapToFocusInProgress = false; + + /** Persistence of Tap to Focus target UI after scan complete. */ + private static final int FOCUS_HOLD_UI_MILLIS = 500; + /** Persistence of Tap to Focus target UI timeout. */ + private static final int FOCUS_HOLD_UI_TIMEOUT_MILLIS = 1500; /** Accelerometer data. */ private final float[] mGData = new float[3]; @@ -214,19 +258,16 @@ public class CaptureModule extends CameraModule /** Current display rotation in degrees. */ private int mDisplayRotation; - /** Current width of the screen, in pixels. */ + /** Current screen width in pixels. */ private int mScreenWidth; - /** Current height of the screen, in pixels. */ + /** Current screen height in pixels. */ private int mScreenHeight; - /** Current preview width, in pixels. */ + /** Current width of preview frames from camera. */ private int mPreviewBufferWidth; - /** Current preview height, in pixels. */ + /** Current height of preview frames from camera.. */ private int mPreviewBufferHeight; - - // /** Current preview area width. */ - // private float mFullPreviewWidth; - // /** Current preview area height. */ - // private float mFullPreviewHeight; + /** Area used by preview. */ + RectF mPreviewArea; /** The current preview transformation matrix. */ private Matrix mPreviewTranformationMatrix = new Matrix(); @@ -325,6 +366,7 @@ public class CaptureModule extends CameraModule @Override public void onPreviewAreaChanged(RectF previewArea) { + mPreviewArea = previewArea; // mUI.updatePreviewAreaRect(previewArea); // mUI.positionProgressOverlay(previewArea); } @@ -421,6 +463,7 @@ public class CaptureModule extends CameraModule public void onReadyForCapture() { Log.d(TAG, "Ready for capture."); onPreviewStarted(); + mCamera.setFocusStateListener(CaptureModule.this); } }); } @@ -578,8 +621,97 @@ public class CaptureModule extends CameraModule return false; } + /** + * Focus sequence starts for zone around tap location for single tap. + */ @Override public void onSingleTapUp(View view, int x, int y) { + Log.v(TAG, "onSingleTapUp x=" + x + " y=" + y); + // TODO: This should query actual capability. + if (mCameraFacing == Facing.FRONT) { + return; + } + triggerFocusAtScreenCoord(x, y); + } + + // TODO: Consider refactoring FocusOverlayManager. + // Currently AF state transitions are controlled in OneCameraImpl. + // PhotoModule uses FocusOverlayManager which uses API1/portability + // logic and coordinates. + + private void triggerFocusAtScreenCoord(int x, int y) { + mTapToFocusInProgress = true; + // Show UI immediately even though scan has not started yet. + mUI.setAutoFocusTarget(x, y); + mUI.showAutoFocusInProgress(); + mMainHandler.removeCallbacks(mHideAutoFocusTargetRunnable); + mMainHandler.postDelayed(mHideAutoFocusTargetRunnable, FOCUS_HOLD_UI_TIMEOUT_MILLIS); + + // Normalize coordinates to [0,1] per CameraOne API. + float points[] = new float[2]; + points[0] = (x - mPreviewArea.left) / mPreviewArea.width(); + points[1] = (y - mPreviewArea.top) / mPreviewArea.height(); + + // Rotate coordinates to portrait orientation per CameraOne API. + Matrix rotationMatrix = new Matrix(); + rotationMatrix.setRotate(mDisplayRotation, 0.5f, 0.5f); + rotationMatrix.mapPoints(points); + mCamera.triggerFocusAndMeterAtPoint(points[0], points[1]); + + // Log touch (screen coordinates). + if (mZoomValue == 1f) { + TouchCoordinate touchCoordinate = new TouchCoordinate(x - mPreviewArea.left, + y - mPreviewArea.top, mPreviewArea.width(), mPreviewArea.height()); + // TODO: Add to logging: duration, rotation. + UsageStatistics.instance().tapToFocus(touchCoordinate, null); + } + } + + /** + * This AF status listener does two things: + *
    + *
  1. Ends tap-to-focus period when mode goes from AUTO to CONTINUOUS_PICTURE.
  2. + *
  3. Updates AF UI if tap-to-focus is not in progress.
  4. + *
+ */ + public void onFocusStatusUpdate(final AutoFocusMode mode, final AutoFocusState state) { + Log.v(TAG, "AF status is mode:" + mode + " state:" + state); + + if (FOCUS_DEBUG_UI) { + // TODO: Add debug circle radius+color UI to FocusOverlay. + // mMainHandler.post(...) + } + + // After tap to focus SCAN completes, clear UI after FOCUS_HOLD_UI_MILLIS. + if (mTapToFocusInProgress && mode == AutoFocusMode.AUTO && + (state == AutoFocusState.STOPPED_FOCUSED || + state == AutoFocusState.STOPPED_UNFOCUSED)) { + mMainHandler.postDelayed(new Runnable() { + @Override + public void run() { + mTapToFocusInProgress = false; + mMainHandler.removeCallbacks(mHideAutoFocusTargetRunnable); + mMainHandler.post(mHideAutoFocusTargetRunnable); + } + }, FOCUS_HOLD_UI_MILLIS); + } + + // Use the OneCamera auto focus callbacks to show the UI, except for + // tap to focus where we show UI right away at touch, and then turn + // it off early at 0.5 sec, before the focus lock expires at 3 sec. + if (!mTapToFocusInProgress) { + switch (state) { + case SCANNING: + mMainHandler.removeCallbacks(mHideAutoFocusTargetRunnable); + mMainHandler.post(mShowAutoFocusTargetInCenterRunnable); + break; + case STOPPED_FOCUSED: + case STOPPED_UNFOCUSED: + mMainHandler.removeCallbacks(mHideAutoFocusTargetRunnable); + mMainHandler.post(mHideAutoFocusTargetRunnable); + break; + } + } } @Override @@ -654,6 +786,7 @@ public class CaptureModule extends CameraModule /*** * Update the preview transform based on the new dimensions. + * TODO: Make work with all: aspect ratios/resolutions x screens/cameras. */ private void updatePreviewTransform(int incomingWidth, int incomingHeight, boolean forceUpdate) { @@ -759,11 +892,9 @@ public class CaptureModule extends CameraModule } mPreviewTranformationMatrix.postScale(scale, scale, centerX, centerY); + // TODO: Take these quantities from mPreviewArea. float previewWidth = effectiveWidth * scale; float previewHeight = effectiveHeight * scale; - // mFullPreviewWidth = previewWidth; - // mFullPreviewHeight = previewHeight; - float previewCenterX = previewWidth / 2; float previewCenterY = previewHeight / 2; mPreviewTranformationMatrix.postTranslate(previewCenterX - centerX, previewCenterY @@ -833,6 +964,7 @@ public class CaptureModule extends CameraModule private void closeCamera() { if (mCamera != null) { + mCamera.setFocusStateListener(null); mCamera.close(null); mCamera = null; } diff --git a/src/com/android/camera/CaptureModuleUI.java b/src/com/android/camera/CaptureModuleUI.java index e926a4ad5..b3ea99c08 100644 --- a/src/com/android/camera/CaptureModuleUI.java +++ b/src/com/android/camera/CaptureModuleUI.java @@ -174,13 +174,10 @@ public class CaptureModuleUI implements mFocusUI.onFocusFailed(); } - public void setAutoFocusTarget(int x, int y, boolean isAutoFocus) { + public void setAutoFocusTarget(int x, int y) { + // TODO: refactor. + boolean isAutoFocus = false; mFocusUI.setFocusPosition(x, y, isAutoFocus); - // Log manual tap to focus. - // TODO: Log coordinates. Note: 16x9 mode will be tricky. - if (isAutoFocus == false) { - UsageStatistics.instance().tapToFocus(null, null); - } } public void clearAutoFocusIndicator() { diff --git a/src/com/android/camera/one/OneCamera.java b/src/com/android/camera/one/OneCamera.java index a1d4c1146..d179c7283 100644 --- a/src/com/android/camera/one/OneCamera.java +++ b/src/com/android/camera/one/OneCamera.java @@ -36,6 +36,40 @@ public interface OneCamera { } /** + * Auto focus system status. + * + */ + public static enum AutoFocusState { + /** Indicates AF system is inactive for some reason (could be an error). */ + INACTIVE, + /** Indicates scan in progress. */ + SCANNING, + /** Indicates scan success (camera in focus). */ + STOPPED_FOCUSED, + /** Indicates scan or other failure. */ + STOPPED_UNFOCUSED + } + + /** + * Auto focus system mode. + * + */ + public static enum AutoFocusMode { + /** System is continuously focusing. */ + CONTINUOUS_PICTURE, + /** System is running a triggered scan. */ + AUTO + } + + /** * Classes implementing this interface will be called when the camera was * opened or failed to open. */ @@ -129,16 +163,17 @@ public interface OneCamera { /** * Classes implementing this interface will be called when the state of the - * focus changes. + * focus changes. Guaranteed not to stay stuck in scanning state past some + * reasonable timeout even if Camera API is stuck. */ public static interface FocusStateListener { /** - * Called when an auto-focus run ended + * Called when mode or state of auto focus system changes. * - * @param success whether auto-focus succeeded. If true, it means that - * the image should not be sharp. + * @param mode Is manual AF trigger cycle active. + * @param state Current state: scanning, focused, not focused, inactive. */ - public void onAutoFocusDone(boolean success); + public void onFocusStatusUpdate(AutoFocusMode mode, AutoFocusState state); } /** @@ -191,6 +226,22 @@ public interface OneCamera { } /** + * Triggers auto focus scan for default ROI. + */ + public void triggerAutoFocus(); + + /** + * Meters and triggers auto focus scan with ROI around tap point. + *

+ * Normalized coordinates are referenced to portrait preview window + * with 0,0 top left and 1,1 bottom right. Rotation has no effect. + * + * @param nx normalized x coordinate. + * @param nx normalized y coordinate. + */ + public void triggerFocusAndMeterAtPoint(float nx, float ny); + + /** * Call this to take a picture. * * @param params parameters for taking pictures. diff --git a/src/com/android/camera/one/v2/OneCameraImpl.java b/src/com/android/camera/one/v2/OneCameraImpl.java index 56e2a0d65..fb14c9f15 100644 --- a/src/com/android/camera/one/v2/OneCameraImpl.java +++ b/src/com/android/camera/one/v2/OneCameraImpl.java @@ -17,6 +17,7 @@ package com.android.camera.one.v2; import android.graphics.ImageFormat; +import android.graphics.Matrix; import android.graphics.Rect; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; @@ -24,12 +25,16 @@ import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraMetadata; import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; +import android.hardware.camera2.params.MeteringRectangle; import android.hardware.camera2.params.StreamConfigurationMap; import android.media.Image; import android.media.ImageReader; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; +import android.os.SystemClock; import android.view.OrientationEventListener; import android.view.Surface; @@ -45,6 +50,8 @@ import com.android.camera.one.OneCamera; import com.android.camera.one.OneCamera.PhotoCaptureParameters.Flash; import com.android.camera.session.CaptureSession; import com.android.camera.util.Size; +import com.android.camera.util.CameraUtil; +import com.android.camera.util.SystemProperties; import java.io.IOException; import java.nio.ByteBuffer; @@ -70,9 +77,96 @@ public class OneCameraImpl extends AbstractOneCamera { } private static final Tag TAG = new Tag("OneCameraImpl2"); + + /** System Properties switch to enable additional focus logging. */ + private static final String PROP_FOCUS_DEBUG_KEY = "persist.camera.focus_debug_log"; + private static final String PROP_FOCUS_DEBUG_OFF = "0"; + private static final boolean FOCUS_DEBUG = !PROP_FOCUS_DEBUG_OFF + .equals(SystemProperties.get(PROP_FOCUS_DEBUG_KEY, PROP_FOCUS_DEBUG_OFF)); + /** Default JPEG encoding quality. */ private static final Byte JPEG_QUALITY = 90; + /** Width and height of touch metering region as fraction of longest edge. */ + private static final float METERING_REGION_EDGE = 0.1f; + /** Metering region weight between 0 and 1. */ + private static final float METERING_REGION_WEIGHT = 0.25f; + /** Duration to hold after manual focus tap. */ + private static final int FOCUS_HOLD_MILLIS = 3000; + + /** + * CaptureRequest tags. + *

+ */ + public static enum RequestTag { + /** Request that is part of a pre shot trigger. */ + PRESHOT_TRIGGERED_AF, + /** Capture request (purely for logging). */ + CAPTURE + } + + /** Current CONTROL_AF_MODE request to Camera2 API. */ + private int mLastRequestedControlAFMode = CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE; + /** Last OneCamera.AutoFocusState reported. */ + private AutoFocusState mLastResultAFState = AutoFocusState.INACTIVE; + /** Last OneCamera.AutoFocusMode reported. */ + private AutoFocusMode mLastResultAFMode = AutoFocusMode.CONTINUOUS_PICTURE; + /** Flag to take a picture when in AUTO mode and the lens is stopped. */ + private boolean mTakePictureWhenLensStoppedAndAuto = false; + /** Flag to take a picture when the lens is stopped. */ + private boolean mTakePictureWhenLensIsStopped = false; + /** Takes a (delayed) picture with appropriate parameters. */ + private Runnable mTakePictureRunnable; + /** Last time takePicture() was called in uptimeMillis. */ + private long mTakePictureStartMillis; + /** Runnable that returns to CONTROL_AF_MODE = AF_CONTINUOUS_PICTURE. */ + private Runnable mReturnToContinuousAFRunnable = new Runnable() { + @Override + public void run() { + repeatingPreviewWithReadyListener(null); + } + }; + + /** Current zoom value. 1.0 is no zoom. */ + private float mZoomValue = 1f; + /** If partial results was OK, don't need to process total result. */ + private boolean mAutoFocusStateListenerPartialOK = false; + + /** + * Common listener for preview frame metadata. + */ + private CameraCaptureSession.CaptureListener mAutoFocusStateListener = new + CameraCaptureSession.CaptureListener() { + // AF state information is sometimes available 1 frame before + // onCaptureCompleted(), so we take advantage of that. + @Override + public void onCaptureProgressed(CameraCaptureSession session, CaptureRequest request, + CaptureResult partialResult) { + + if (partialResult.get(CaptureResult.CONTROL_AF_STATE) != null) { + mAutoFocusStateListenerPartialOK = true; + autofocusStateChangeDispatcher(partialResult); + if (FOCUS_DEBUG) { + logExtraFocusInfo(partialResult); + } + } else { + mAutoFocusStateListenerPartialOK = false; + } + super.onCaptureProgressed(session, request, partialResult); + } + @Override + public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, + TotalCaptureResult result) { + if (!mAutoFocusStateListenerPartialOK) { + autofocusStateChangeDispatcher(result); + } + super.onCaptureCompleted(session, request, result); + } + }; + /** Thread on which the camera operations are running. */ private final HandlerThread mCameraThread; /** Handler of the {@link #mCameraThread}. */ @@ -113,6 +207,7 @@ public class OneCameraImpl extends AbstractOneCamera { // Since this is not an HDR+ session, we will just save the result. capture.session.startEmpty(); byte[] imageBytes = acquireJpegBytesAndClose(reader); + // TODO: The savePicture call here seems to block UI thread. savePicture(imageBytes, capture.parameters, capture.session); capture.parameters.callback.onPictureTaken(capture.session); } @@ -140,21 +235,70 @@ public class OneCameraImpl extends AbstractOneCamera { Log.d(TAG, "New Camera2 based OneCameraImpl created."); } + /** + * Take picture, initiating an auto focus scan if needed. + */ @Override - public void takePicture(PhotoCaptureParameters params, CaptureSession session) { + public void takePicture(final PhotoCaptureParameters params, final CaptureSession session) { + if (mTakePictureWhenLensStoppedAndAuto || mTakePictureWhenLensIsStopped) { + // Do not do anything when a picture is already in progress. + return; + } + + mTakePictureRunnable = new Runnable() { + @Override + public void run() { + takePictureNow(params, session); + } + }; + mTakePictureStartMillis = SystemClock.uptimeMillis(); + + if (mLastResultAFMode == AutoFocusMode.CONTINUOUS_PICTURE + && mLastResultAFState == AutoFocusState.STOPPED_UNFOCUSED) { + Log.v(TAG, "Unfocused: Triggering auto focus scan."); + // Trigger auto focus scan if in CONTINUOUS_PICTURE + unfocused. + mTakePictureWhenLensStoppedAndAuto = true; + repeatingPreviewWithAFTrigger(null, null, RequestTag.PRESHOT_TRIGGERED_AF); + } else if (mLastResultAFState == AutoFocusState.SCANNING) { + // Delay shot if scanning. + Log.v(TAG, "Waiting until scan is done before taking shot."); + mTakePictureWhenLensIsStopped = true; + } else { + takePictureNow(params, session); + } + } + + /** + * Take picture immediately. Parameters passed through from takePicture(). + */ + public void takePictureNow(PhotoCaptureParameters params, CaptureSession session) { + long dt = SystemClock.uptimeMillis() - mTakePictureStartMillis; + Log.v(TAG, "Taking shot with extra AF delay of " + dt + " ms."); // This will throw a RuntimeException, if parameters are not sane. params.checkSanity(); try { - // JPEG capture + // JPEG capture. CaptureRequest.Builder builder = mDevice .createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + // TODO: Check that these control modes are correct for AWB, AE. + if (mLastRequestedControlAFMode == CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE) { + builder.set(CaptureRequest.CONTROL_AF_MODE, + CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + Log.v(TAG, "CaptureRequest with CONTROL_AF_MODE_CONTINUOUS_PICTURE."); + } else if (mLastRequestedControlAFMode == CameraMetadata.CONTROL_AF_MODE_AUTO) { + builder.set(CaptureRequest.CONTROL_AF_MODE, CameraMetadata.CONTROL_AF_MODE_AUTO); + builder.set(CaptureRequest.CONTROL_AF_TRIGGER, + CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + Log.v(TAG, "CaptureRequest with AUTO."); + } + builder.setTag(RequestTag.CAPTURE); builder.set(CaptureRequest.JPEG_QUALITY, JPEG_QUALITY); builder.set(CaptureRequest.JPEG_ORIENTATION, getJpegRotation(params.orientation)); builder.addTarget(mPreviewSurface); builder.addTarget(mJpegImageReader.getSurface()); applyFlashMode(params.flashMode, builder); CaptureRequest request = builder.build(); - mCaptureSession.capture(request, null, mCameraHandler); + mCaptureSession.capture(request, mAutoFocusStateListener, mCameraHandler); } catch (CameraAccessException e) { Log.e(TAG, "Could not access camera for JPEG capture."); params.callback.onPictureTakenFailed(); @@ -190,6 +334,11 @@ public class OneCameraImpl extends AbstractOneCamera { Log.w(TAG, "Camera is already closed."); return; } + try { + mCaptureSession.abortCaptures(); + } catch (CameraAccessException e) { + Log.e(TAG, "Could not abort captures in progress."); + } mIsClosed = true; mCloseCallback = closeCallback; mCameraThread.quitSafely(); @@ -305,7 +454,7 @@ public class OneCameraImpl extends AbstractOneCamera { @Override public void onConfigured(CameraCaptureSession session) { mCaptureSession = session; - startPreviewInternal(listener); + repeatingPreviewWithReadyListener(listener); } @Override @@ -323,29 +472,203 @@ public class OneCameraImpl extends AbstractOneCamera { } /** - * Configures and creates the request for starting the preview. + * Request preview capture stream with AF_MODE_CONTINUOUS_PICTURE. * - * @param listener called when request was build and sent, or if setting up - * the request failed. + * @param readyListener called when request was build and sent, or if + * setting up the request failed. */ - private void startPreviewInternal(CaptureReadyCallback listener) { - Log.v(TAG, "issuePreviewCaptureRequest."); + private void repeatingPreviewWithReadyListener(CaptureReadyCallback readyListener) { try { - CaptureRequest.Builder builder = mDevice - .createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + CaptureRequest.Builder builder = mDevice. + createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + builder.addTarget(mPreviewSurface); builder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); + mLastRequestedControlAFMode = CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE; builder.set(CaptureRequest.CONTROL_AF_MODE, CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + builder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + mCaptureSession.setRepeatingRequest(builder.build(), mAutoFocusStateListener, + mCameraHandler); + Log.v(TAG, "Sent preview request with AF_MODE_CONTINUOUS_PICTURE."); + if (readyListener != null) { + readyListener.onReadyForCapture(); + } + } catch (CameraAccessException ex) { + Log.e(TAG, "Could not access camera setting up preview.", ex); + if (readyListener != null) { + readyListener.onSetupFailed(); + } + } + } + + /** + * Request preview capture stream with auto focus cycle. + * + * @param focusRegions focus regions, for tap to focus/expose. + * @param meteringRegions metering regions, for tap to focus/expose. + */ + private void repeatingPreviewWithAFTrigger(MeteringRectangle[] focusRegions, + MeteringRectangle[] meteringRegions, Object tag) { + try { + CaptureRequest.Builder builder; + builder = mDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); builder.addTarget(mPreviewSurface); - mCaptureSession.setRepeatingRequest(builder.build(), null, mCameraHandler); - listener.onReadyForCapture(); - } catch (CameraAccessException e) { - Log.e(TAG, "Could not access camera setting up preview."); - listener.onSetupFailed(); + builder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); + if (focusRegions != null) { + builder.set(CaptureRequest.CONTROL_AF_REGIONS, focusRegions); + } + if (meteringRegions != null) { + builder.set(CaptureRequest.CONTROL_AE_REGIONS, meteringRegions); + } + builder.set(CaptureRequest.CONTROL_AF_MODE, CameraMetadata.CONTROL_AF_MODE_AUTO); + mLastRequestedControlAFMode = CameraMetadata.CONTROL_AF_MODE_AUTO; + + // Step 1: Request single frame CONTROL_AF_TRIGGER_START. + builder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + mCaptureSession.capture(builder.build(), mAutoFocusStateListener, mCameraHandler); + + // Step 2: Request continuous frames CONTROL_AF_TRIGGER_IDLE. + builder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); + builder.setTag(tag); + mCaptureSession.setRepeatingRequest(builder.build(), mAutoFocusStateListener, + mCameraHandler); + resumeContinuousAFAfterDelay(FOCUS_HOLD_MILLIS); + } catch (CameraAccessException ex) { + Log.e(TAG, "Could not execute preview request.", ex); + } + } + + /** + * Resume AF_MODE_CONTINUOUS_PICTURE after FOCUS_HOLD_MILLIS. + */ + private void resumeContinuousAFAfterDelay(int millis) { + mCameraHandler.removeCallbacks(mReturnToContinuousAFRunnable); + mCameraHandler.postDelayed(mReturnToContinuousAFRunnable, millis); + } + + /** + * This method takes appropriate action if camera2 AF state changes. + *
    + *
  1. Reports changes in camera2 AF state to OneCamera.FocusStateListener.
  2. + *
  3. Take picture after AF scan.
  4. + *
  5. TODO: Take picture after AE_PRECAPTURE sequence for flash.
  6. + *
+ */ + private void autofocusStateChangeDispatcher(CaptureResult result) { + Integer nativeAFControlState = result.get(CaptureResult.CONTROL_AF_STATE); + Integer nativeAFControlMode = result.get(CaptureResult.CONTROL_AF_MODE); + Object tag = result.getRequest().getTag(); + + // Convert to OneCamera mode and state. + AutoFocusMode resultAFMode = modeFromCamera2Mode(nativeAFControlMode); + AutoFocusState resultAFState = stateFromCamera2State(nativeAFControlState); + + boolean lensIsStopped = (resultAFState == AutoFocusState.STOPPED_FOCUSED || + resultAFState == AutoFocusState.STOPPED_UNFOCUSED); + if (tag == RequestTag.PRESHOT_TRIGGERED_AF && lensIsStopped && + mTakePictureWhenLensStoppedAndAuto) { + // Take the shot. + mCameraHandler.post(mTakePictureRunnable); + // Return to passive scanning. + mCameraHandler.post(new Runnable() { + @Override + public void run() { + repeatingPreviewWithReadyListener(null); + } + }); + mTakePictureWhenLensStoppedAndAuto = false; + } + if (mTakePictureWhenLensIsStopped && lensIsStopped) { + // Take the shot. + mCameraHandler.post(mTakePictureRunnable); + mTakePictureWhenLensIsStopped = false; + } + + // Report state change when mode or state has changed. + if (resultAFState != mLastResultAFState || resultAFMode != mLastResultAFMode + && mFocusStateListener != null) { + mFocusStateListener.onFocusStatusUpdate(resultAFMode, resultAFState); + } + mLastResultAFState = resultAFState; + mLastResultAFMode = resultAFMode; + } + + /** + * Convert reported camera2 AF state to OneCamera AutoFocusState. + */ + private static AutoFocusState stateFromCamera2State(int state) { + switch (state) { + case CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN: + case CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN: + return AutoFocusState.SCANNING; + case CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED: + case CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED: + return AutoFocusState.STOPPED_FOCUSED; + case CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED: + case CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED: + return AutoFocusState.STOPPED_UNFOCUSED; + default: + return AutoFocusState.INACTIVE; } } /** + * Convert reported camera2 AF state to OneCamera AutoFocusMode. + */ + private static AutoFocusMode modeFromCamera2Mode(int mode) { + if (mode == CaptureResult.CONTROL_AF_MODE_AUTO) { + return AutoFocusMode.AUTO; + } else { + // CONTROL_AF_MODE_CONTINUOUS_PICTURE is the other mode used. + return AutoFocusMode.CONTINUOUS_PICTURE; + } + } + + @Override + public void triggerAutoFocus() { + Log.v(TAG, "triggerAutoFocus()"); + repeatingPreviewWithAFTrigger(null, null, null); + } + + @Override + public void triggerFocusAndMeterAtPoint(float nx, float ny) { + Log.v(TAG, "triggerFocusAndMeterAtPoint(" + nx + "," + ny + ")"); + float points[] = new float[]{nx, ny}; + // Make sure the points are in [0,1] range. + points[0] = CameraUtil.clamp(points[0], 0f, 1f); + points[1] = CameraUtil.clamp(points[1], 0f, 1f); + + // Shrink points towards center if zoomed. + if (mZoomValue > 1f) { + Matrix zoomMatrix = new Matrix(); + zoomMatrix.postScale(1f / mZoomValue, 1f / mZoomValue, 0.5f, 0.5f); + zoomMatrix.mapPoints(points); + } + + // TODO: Make this work when preview aspect ratio != sensor aspect ratio. + Rect sensor = mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + int edge = (int) (METERING_REGION_EDGE * Math.max(sensor.width(), sensor.height())); + // x0 and y0 in sensor coordinate system, rotated 90 degrees from portrait. + int x0 = (int) (sensor.width() * points[1]); + int y0 = (int) (sensor.height() * (1f - points[0])); + int x1 = x0 + edge; + int y1 = y0 + edge; + + // Make sure regions are inside the sensor area. + x0 = CameraUtil.clamp(x0, 0, sensor.width() - 1); + x1 = CameraUtil.clamp(x1, 0, sensor.width() - 1); + y0 = CameraUtil.clamp(y0, 0, sensor.height() - 1); + y1 = CameraUtil.clamp(y1, 0, sensor.height() - 1); + int wt = (int) ((1 - METERING_REGION_WEIGHT) * (float) MeteringRectangle.METERING_WEIGHT_MIN + + METERING_REGION_WEIGHT * (float) MeteringRectangle.METERING_WEIGHT_MAX); + + Log.v(TAG, "sensor 3A @ x0=" + x0 + " y0=" + y0 + " dx=" + (x1 - x0) + " dy=" + (y1 - y0)); + MeteringRectangle[] regions = new MeteringRectangle[]{ + new MeteringRectangle(x0, y0, x1 - x0, y1 - y0, wt)}; + repeatingPreviewWithAFTrigger(regions, regions, null); + } + + /** * Calculate the aspect ratio of the full size capture on this device. * * @param characteristics the characteristics of the camera device. @@ -419,4 +742,44 @@ public class OneCameraImpl extends AbstractOneCamera { break; } } + + /** + * Utility function: converts CaptureResult.CONTROL_AF_STATE* to String. + */ + private static String camera2ControlAFStateDesc(int aFState) { + switch (aFState) { + case CaptureResult.CONTROL_AF_STATE_INACTIVE: + return "inactive"; + case CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN: + return "passive_scan"; + case CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED: + return "passive_focused"; + case CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN: + return "active_scan"; + case CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED: + return "focus_locked"; + case CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED: + return "not_focus_locked"; + case CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED: + return "passive_unfocused"; + default: + return "unknown"; + } + } + + private void logExtraFocusInfo(CaptureResult result) { + Object tag = result.getRequest().getTag(); + // Nexus 5 has a bug where CONTROL_AF_STATE is missing sometimes. + if (result.get(CaptureResult.CONTROL_AF_STATE) == null) { + //throw new IllegalStateException("CaptureResult missing CONTROL_AF_STATE."); + Log.e(TAG, "\n!!!! TotalCaptureResult missing CONTROL_AF_STATE. !!!!\n "); + return; + } + Log.v(TAG, "camera2 AF state: " + camera2ControlAFStateDesc(result. + get(CaptureResult.CONTROL_AF_STATE)) + + (tag == null ? "" : (" tag: " + tag))); + if (result.get(CaptureResult.LENS_FOCUS_DISTANCE) != null) { + Log.v(TAG, " lens @ " + result.get(CaptureResult.LENS_FOCUS_DISTANCE)); + } + } } -- 2.11.0