blob: 1c5ac380c4617e20f81aec89b1ec716f74617f0e [file] [log] [blame]
/*
* 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.
*
* <p>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<Area> mFocusArea; // focus area in driver format
private List<Area> 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<String> 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<Area> getFocusAreas() {
return mFocusArea;
}
public List<Area> 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<String> 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);
}
}