/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.dialer.callcomposer.camera.camerafocus; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; import android.hardware.Camera.Area; import android.hardware.Camera.Parameters; import android.os.Handler; import android.os.Looper; import android.os.Message; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import java.util.ArrayList; import java.util.List; /** * A class that handles everything about focus in still picture mode. This also handles the metering * area because it is the same as focus area. * *

The test cases: (1) The camera has continuous autofocus. Move the camera. Take a picture when * CAF is not in progress. (2) The camera has continuous autofocus. Move the camera. Take a picture * when CAF is in progress. (3) The camera has face detection. Point the camera at some faces. Hold * the shutter. Release to take a picture. (4) The camera has face detection. Point the camera at * some faces. Single tap the shutter to take a picture. (5) The camera has autofocus. Single tap * the shutter to take a picture. (6) The camera has autofocus. Hold the shutter. Release to take a * picture. (7) The camera has no autofocus. Single tap the shutter and take a picture. (8) The * camera has autofocus and supports focus area. Touch the screen to trigger autofocus. Take a * picture. (9) The camera has autofocus and supports focus area. Touch the screen to trigger * autofocus. Wait until it times out. (10) The camera has no autofocus and supports metering area. * Touch the screen to change metering area. */ public class FocusOverlayManager { private static final String TRUE = "true"; private static final String AUTO_EXPOSURE_LOCK_SUPPORTED = "auto-exposure-lock-supported"; private static final String AUTO_WHITE_BALANCE_LOCK_SUPPORTED = "auto-whitebalance-lock-supported"; private static final int RESET_TOUCH_FOCUS = 0; private static final int RESET_TOUCH_FOCUS_DELAY = 3000; private int mState = STATE_IDLE; private static final int STATE_IDLE = 0; // Focus is not active. private static final int STATE_FOCUSING = 1; // Focus is in progress. // Focus is in progress and the camera should take a picture after focus finishes. private static final int STATE_FOCUSING_SNAP_ON_FINISH = 2; private static final int STATE_SUCCESS = 3; // Focus finishes and succeeds. private static final int STATE_FAIL = 4; // Focus finishes and fails. private boolean mInitialized; private boolean mFocusAreaSupported; private boolean mMeteringAreaSupported; private boolean mLockAeAwbNeeded; private boolean mAeAwbLock; private Matrix mMatrix; private PieRenderer mPieRenderer; private int mPreviewWidth; // The width of the preview frame layout. private int mPreviewHeight; // The height of the preview frame layout. private boolean mMirror; // true if the camera is front-facing. private List mFocusArea; // focus area in driver format private List mMeteringArea; // metering area in driver format private String mFocusMode; private Parameters mParameters; private Handler mHandler; private Listener mListener; /** Listener used for the focus indicator to communicate back to the camera. */ public interface Listener { void autoFocus(); void cancelAutoFocus(); boolean capture(); void setFocusParameters(); } private class MainHandler extends Handler { public MainHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { switch (msg.what) { case RESET_TOUCH_FOCUS: { cancelAutoFocus(); break; } } } } public FocusOverlayManager(Listener listener, Looper looper) { mHandler = new MainHandler(looper); mMatrix = new Matrix(); mListener = listener; } public void setFocusRenderer(PieRenderer renderer) { mPieRenderer = renderer; mInitialized = (mMatrix != null); } public void setParameters(Parameters parameters) { // parameters can only be null when onConfigurationChanged is called // before camera is open. We will just return in this case, because // parameters will be set again later with the right parameters after // camera is open. if (parameters == null) { return; } mParameters = parameters; mFocusAreaSupported = isFocusAreaSupported(parameters); mMeteringAreaSupported = isMeteringAreaSupported(parameters); mLockAeAwbNeeded = (isAutoExposureLockSupported(mParameters) || isAutoWhiteBalanceLockSupported(mParameters)); } public void setPreviewSize(int previewWidth, int previewHeight) { if (mPreviewWidth != previewWidth || mPreviewHeight != previewHeight) { mPreviewWidth = previewWidth; mPreviewHeight = previewHeight; setMatrix(); } } public void setMirror(boolean mirror) { mMirror = mirror; setMatrix(); } private void setMatrix() { if (mPreviewWidth != 0 && mPreviewHeight != 0) { Matrix matrix = new Matrix(); prepareMatrix(matrix, mMirror, mPreviewWidth, mPreviewHeight); // In face detection, the matrix converts the driver coordinates to UI // coordinates. In tap focus, the inverted matrix converts the UI // coordinates to driver coordinates. matrix.invert(mMatrix); mInitialized = (mPieRenderer != null); } } private void lockAeAwbIfNeeded() { if (mLockAeAwbNeeded && !mAeAwbLock) { mAeAwbLock = true; mListener.setFocusParameters(); } } public void onAutoFocus(boolean focused, boolean shutterButtonPressed) { if (mState == STATE_FOCUSING_SNAP_ON_FINISH) { // Take the picture no matter focus succeeds or fails. No need // to play the AF sound if we're about to play the shutter // sound. if (focused) { mState = STATE_SUCCESS; } else { mState = STATE_FAIL; } updateFocusUI(); capture(); } else if (mState == STATE_FOCUSING) { // This happens when (1) user is half-pressing the focus key or // (2) touch focus is triggered. Play the focus tone. Do not // take the picture now. if (focused) { mState = STATE_SUCCESS; } else { mState = STATE_FAIL; } updateFocusUI(); // If this is triggered by touch focus, cancel focus after a // while. if (mFocusArea != null) { mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY); } if (shutterButtonPressed) { // Lock AE & AWB so users can half-press shutter and recompose. lockAeAwbIfNeeded(); } } else if (mState == STATE_IDLE) { // User has released the focus key before focus completes. // Do nothing. } } public void onAutoFocusMoving(boolean moving) { if (!mInitialized) { return; } // Ignore if we have requested autofocus. This method only handles // continuous autofocus. if (mState != STATE_IDLE) { return; } if (moving) { mPieRenderer.showStart(); } else { mPieRenderer.showSuccess(true); } } private void initializeFocusAreas( int focusWidth, int focusHeight, int x, int y, int previewWidth, int previewHeight) { if (mFocusArea == null) { mFocusArea = new ArrayList<>(); mFocusArea.add(new Area(new Rect(), 1)); } // Convert the coordinates to driver format. calculateTapArea( focusWidth, focusHeight, 1f, x, y, previewWidth, previewHeight, mFocusArea.get(0).rect); } private void initializeMeteringAreas( int focusWidth, int focusHeight, int x, int y, int previewWidth, int previewHeight) { if (mMeteringArea == null) { mMeteringArea = new ArrayList<>(); mMeteringArea.add(new Area(new Rect(), 1)); } // Convert the coordinates to driver format. // AE area is bigger because exposure is sensitive and // easy to over- or underexposure if area is too small. calculateTapArea( focusWidth, focusHeight, 1.5f, x, y, previewWidth, previewHeight, mMeteringArea.get(0).rect); } public void onSingleTapUp(int x, int y) { if (!mInitialized || mState == STATE_FOCUSING_SNAP_ON_FINISH) { return; } // Let users be able to cancel previous touch focus. if ((mFocusArea != null) && (mState == STATE_FOCUSING || mState == STATE_SUCCESS || mState == STATE_FAIL)) { cancelAutoFocus(); } // Initialize variables. int focusWidth = mPieRenderer.getSize(); int focusHeight = mPieRenderer.getSize(); if (focusWidth == 0 || mPieRenderer.getWidth() == 0 || mPieRenderer.getHeight() == 0) { return; } int previewWidth = mPreviewWidth; int previewHeight = mPreviewHeight; // Initialize mFocusArea. if (mFocusAreaSupported) { initializeFocusAreas(focusWidth, focusHeight, x, y, previewWidth, previewHeight); } // Initialize mMeteringArea. if (mMeteringAreaSupported) { initializeMeteringAreas(focusWidth, focusHeight, x, y, previewWidth, previewHeight); } // Use margin to set the focus indicator to the touched area. mPieRenderer.setFocus(x, y); // Set the focus area and metering area. mListener.setFocusParameters(); if (mFocusAreaSupported) { autoFocus(); } else { // Just show the indicator in all other cases. updateFocusUI(); // Reset the metering area in 3 seconds. mHandler.removeMessages(RESET_TOUCH_FOCUS); mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY); } } public void onPreviewStarted() { mState = STATE_IDLE; } public void onPreviewStopped() { // If auto focus was in progress, it would have been stopped. mState = STATE_IDLE; resetTouchFocus(); updateFocusUI(); } public void onCameraReleased() { onPreviewStopped(); } private void autoFocus() { LogUtil.v("FocusOverlayManager.autoFocus", "Start autofocus."); mListener.autoFocus(); mState = STATE_FOCUSING; updateFocusUI(); mHandler.removeMessages(RESET_TOUCH_FOCUS); } public void cancelAutoFocus() { LogUtil.v("FocusOverlayManager.cancelAutoFocus", "Cancel autofocus."); // Reset the tap area before calling mListener.cancelAutofocus. // Otherwise, focus mode stays at auto and the tap area passed to the // driver is not reset. resetTouchFocus(); mListener.cancelAutoFocus(); mState = STATE_IDLE; updateFocusUI(); mHandler.removeMessages(RESET_TOUCH_FOCUS); } private void capture() { if (mListener.capture()) { mState = STATE_IDLE; mHandler.removeMessages(RESET_TOUCH_FOCUS); } } public String getFocusMode() { List supportedFocusModes = mParameters.getSupportedFocusModes(); if (mFocusAreaSupported && mFocusArea != null) { // Always use autofocus in tap-to-focus. mFocusMode = Parameters.FOCUS_MODE_AUTO; } else { mFocusMode = Parameters.FOCUS_MODE_CONTINUOUS_PICTURE; } if (!isSupported(mFocusMode, supportedFocusModes)) { // For some reasons, the driver does not support the current // focus mode. Fall back to auto. if (isSupported(Parameters.FOCUS_MODE_AUTO, mParameters.getSupportedFocusModes())) { mFocusMode = Parameters.FOCUS_MODE_AUTO; } else { mFocusMode = mParameters.getFocusMode(); } } return mFocusMode; } public List getFocusAreas() { return mFocusArea; } public List getMeteringAreas() { return mMeteringArea; } private void updateFocusUI() { if (!mInitialized) { return; } FocusIndicator focusIndicator = mPieRenderer; if (mState == STATE_IDLE) { if (mFocusArea == null) { focusIndicator.clear(); } else { // Users touch on the preview and the indicator represents the // metering area. Either focus area is not supported or // autoFocus call is not required. focusIndicator.showStart(); } } else if (mState == STATE_FOCUSING || mState == STATE_FOCUSING_SNAP_ON_FINISH) { focusIndicator.showStart(); } else { if (Parameters.FOCUS_MODE_CONTINUOUS_PICTURE.equals(mFocusMode)) { // TODO: check HAL behavior and decide if this can be removed. focusIndicator.showSuccess(false); } else if (mState == STATE_SUCCESS) { focusIndicator.showSuccess(false); } else if (mState == STATE_FAIL) { focusIndicator.showFail(false); } } } private void resetTouchFocus() { if (!mInitialized) { return; } // Put focus indicator to the center. clear reset position mPieRenderer.clear(); mFocusArea = null; mMeteringArea = null; } private void calculateTapArea( int focusWidth, int focusHeight, float areaMultiple, int x, int y, int previewWidth, int previewHeight, Rect rect) { int areaWidth = (int) (focusWidth * areaMultiple); int areaHeight = (int) (focusHeight * areaMultiple); final int maxW = previewWidth - areaWidth; int left = maxW > 0 ? clamp(x - areaWidth / 2, 0, maxW) : 0; final int maxH = previewHeight - areaHeight; int top = maxH > 0 ? clamp(y - areaHeight / 2, 0, maxH) : 0; RectF rectF = new RectF(left, top, left + areaWidth, top + areaHeight); mMatrix.mapRect(rectF); rectFToRect(rectF, rect); } private int clamp(int x, int min, int max) { Assert.checkArgument(max >= min); if (x > max) { return max; } if (x < min) { return min; } return x; } private boolean isAutoExposureLockSupported(Parameters params) { return TRUE.equals(params.get(AUTO_EXPOSURE_LOCK_SUPPORTED)); } private boolean isAutoWhiteBalanceLockSupported(Parameters params) { return TRUE.equals(params.get(AUTO_WHITE_BALANCE_LOCK_SUPPORTED)); } private boolean isSupported(String value, List supported) { return supported != null && supported.indexOf(value) >= 0; } private boolean isMeteringAreaSupported(Parameters params) { return params.getMaxNumMeteringAreas() > 0; } private boolean isFocusAreaSupported(Parameters params) { return (params.getMaxNumFocusAreas() > 0 && isSupported(Parameters.FOCUS_MODE_AUTO, params.getSupportedFocusModes())); } private void prepareMatrix(Matrix matrix, boolean mirror, int viewWidth, int viewHeight) { // Need mirror for front camera. matrix.setScale(mirror ? -1 : 1, 1); // Camera driver coordinates range from (-1000, -1000) to (1000, 1000). // UI coordinates range from (0, 0) to (width, height). matrix.postScale(viewWidth / 2000f, viewHeight / 2000f); matrix.postTranslate(viewWidth / 2f, viewHeight / 2f); } private void rectFToRect(RectF rectF, Rect rect) { rect.left = Math.round(rectF.left); rect.top = Math.round(rectF.top); rect.right = Math.round(rectF.right); rect.bottom = Math.round(rectF.bottom); } }