/* * Copyright (C) 2011 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.camera; import com.android.camera.ui.FocusRectangle; import com.android.camera.ui.FaceView; import android.graphics.Rect; import android.hardware.Camera.Area; import android.hardware.Camera.Parameters; import android.media.AudioManager; import android.media.ToneGenerator; import android.os.Handler; import android.os.Message; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.RelativeLayout; 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. public class FocusManager { private static final String TAG = "FocusManager"; private static final int RESET_TOUCH_FOCUS = 0; private static final int FOCUS_BEEP_VOLUME = 100; 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 mContinuousFocusFail; private ToneGenerator mFocusToneGenerator; private FocusRectangle mFocusRectangle; private View mPreviewFrame; private FaceView mFaceView; private List<Area> mTapArea; // focus area in driver format private String mFocusMode; private String mDefaultFocusMode; private String mOverrideFocusMode; private Parameters mParameters; private ComboPreferences mPreferences; private Handler mHandler; Listener mListener; public interface Listener { public void autoFocus(); public void cancelAutoFocus(); public boolean capture(); public void startFaceDetection(); public void stopFaceDetection(); public void setFocusParameters(); } private class MainHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case RESET_TOUCH_FOCUS: { cancelAutoFocus(); mListener.startFaceDetection(); break; } } } } public FocusManager(ComboPreferences preferences, String defaultFocusMode) { mPreferences = preferences; mDefaultFocusMode = defaultFocusMode; mHandler = new MainHandler(); } // This has to be initialized before initialize(). public void initializeParameters(Parameters parameters) { mParameters = parameters; mFocusAreaSupported = (mParameters.getMaxNumFocusAreas() > 0 && isSupported(Parameters.FOCUS_MODE_AUTO, mParameters.getSupportedFocusModes())); } public void initialize(FocusRectangle focusRectangle, View previewFrame, FaceView faceView, Listener listener) { mFocusRectangle = focusRectangle; mPreviewFrame = previewFrame; mFaceView = faceView; mListener = listener; if (mParameters != null) { mInitialized = true; } else { Log.e(TAG, "mParameters is not initialized."); } } public void doFocus(boolean pressed) { if (!mInitialized) return; if (!(getFocusMode().equals(Parameters.FOCUS_MODE_INFINITY) || getFocusMode().equals(Parameters.FOCUS_MODE_FIXED) || getFocusMode().equals(Parameters.FOCUS_MODE_EDOF))) { if (pressed) { // Focus key down. // Do not focus if touch focus has been triggered. if (mState != STATE_SUCCESS && mState != STATE_FAIL) { autoFocus(); } } else { // Focus key up. // User releases half-pressed focus key. if (mState == STATE_FOCUSING || mState == STATE_SUCCESS || mState == STATE_FAIL) { cancelAutoFocus(); } } } } public void doSnap() { if (!mInitialized) return; // If the user has half-pressed the shutter and focus is completed, we // can take the photo right away. If the focus mode is infinity, we can // also take the photo. if (getFocusMode().equals(Parameters.FOCUS_MODE_INFINITY) || getFocusMode().equals(Parameters.FOCUS_MODE_FIXED) || getFocusMode().equals(Parameters.FOCUS_MODE_EDOF) || (mState == STATE_SUCCESS || mState == STATE_FAIL)) { capture(); } else if (mState == STATE_FOCUSING) { // Half pressing the shutter (i.e. the focus button event) will // already have requested AF for us, so just request capture on // focus here. mState = STATE_FOCUSING_SNAP_ON_FINISH; } else if (mState == STATE_IDLE) { // Focus key down event is dropped for some reasons. Just ignore. } } public void onShutter() { resetTouchFocus(); updateFocusUI(); if (mFaceView != null) mFaceView.clearFaces(); } public void onAutoFocus(boolean focused) { // Do a full autofocus if the scene is not focused in continuous // focus mode, if (getFocusMode().equals(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE) && !focused) { mContinuousFocusFail = true; mListener.setFocusParameters(); autoFocus(); mContinuousFocusFail = false; } else 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; if (mFocusToneGenerator != null) { mFocusToneGenerator.startTone(ToneGenerator.TONE_PROP_BEEP2); } } else { mState = STATE_FAIL; } updateFocusUI(); // If this is triggered by touch focus, cancel focus after a // while. if (mTapArea != null) { mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY); } } else if (mState == STATE_IDLE) { // User has released the focus key before focus completes. // Do nothing. } } public boolean onTouch(MotionEvent e) { if (!mInitialized || mState == STATE_FOCUSING_SNAP_ON_FINISH) return false; // Let users be able to cancel previous touch focus. if ((mTapArea != null) && (e.getAction() == MotionEvent.ACTION_DOWN) && (mState == STATE_FOCUSING || mState == STATE_SUCCESS || mState == STATE_FAIL)) { cancelAutoFocus(); } // Initialize variables. int x = Math.round(e.getX()); int y = Math.round(e.getY()); int focusWidth = mFocusRectangle.getWidth(); int focusHeight = mFocusRectangle.getHeight(); int previewWidth = mPreviewFrame.getWidth(); int previewHeight = mPreviewFrame.getHeight(); if (mTapArea == null) { mTapArea = new ArrayList<Area>(); mTapArea.add(new Area(new Rect(), 1)); } // Convert the coordinates to driver format. The actual focus area is two times bigger than // UI because a huge rectangle looks strange. int areaWidth = focusWidth * 2; int areaHeight = focusHeight * 2; int areaLeft = Util.clamp(x - areaWidth / 2, 0, previewWidth - areaWidth); int areaTop = Util.clamp(y - areaHeight / 2, 0, previewHeight - areaHeight); Rect rect = mTapArea.get(0).rect; convertToFocusArea(areaLeft, areaTop, areaWidth, areaHeight, previewWidth, previewHeight, mTapArea.get(0).rect); // Use margin to set the focus rectangle to the touched area. RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) mFocusRectangle.getLayoutParams(); int left = Util.clamp(x - focusWidth / 2, 0, previewWidth - focusWidth); int top = Util.clamp(y - focusHeight / 2, 0, previewHeight - focusHeight); p.setMargins(left, top, 0, 0); // Disable "center" rule because we no longer want to put it in the center. int[] rules = p.getRules(); rules[RelativeLayout.CENTER_IN_PARENT] = 0; mFocusRectangle.requestLayout(); // Stop face detection because we want to specify focus and metering area. mListener.stopFaceDetection(); // Set the focus area and metering area. mListener.setFocusParameters(); if (mFocusAreaSupported && (e.getAction() == MotionEvent.ACTION_UP)) { autoFocus(); } else { // Just show the rectangle 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); } return true; } public void onPreviewStarted() { mState = STATE_IDLE; } public void onPreviewStopped() { mState = STATE_IDLE; resetTouchFocus(); // If auto focus was in progress, it would have been canceled. updateFocusUI(); } public void onCameraReleased() { onPreviewStopped(); } private void autoFocus() { Log.v(TAG, "Start autofocus."); mListener.autoFocus(); mState = STATE_FOCUSING; // Pause the face view because the driver will keep sending face // callbacks after the focus completes. if (mFaceView != null) mFaceView.pause(); updateFocusUI(); mHandler.removeMessages(RESET_TOUCH_FOCUS); } private void cancelAutoFocus() { Log.v(TAG, "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(); if (mFaceView != null) mFaceView.resume(); mState = STATE_IDLE; updateFocusUI(); mHandler.removeMessages(RESET_TOUCH_FOCUS); } private void capture() { if (mListener.capture()) { mState = STATE_IDLE; mHandler.removeMessages(RESET_TOUCH_FOCUS); } } public void initializeToneGenerator() { // Initialize focus tone generator. try { mFocusToneGenerator = new ToneGenerator( AudioManager.STREAM_SYSTEM, FOCUS_BEEP_VOLUME); } catch (Throwable ex) { Log.w(TAG, "Exception caught while creating tone generator: ", ex); mFocusToneGenerator = null; } } public void releaseToneGenerator() { if (mFocusToneGenerator != null) { mFocusToneGenerator.release(); mFocusToneGenerator = null; } } // This can only be called after mParameters is initialized. public String getFocusMode() { if (mOverrideFocusMode != null) return mOverrideFocusMode; if ((mFocusAreaSupported && mTapArea != null) || mContinuousFocusFail) { // Always use autofocus in tap-to-focus or when continuous focus fails. mFocusMode = Parameters.FOCUS_MODE_AUTO; } else { // The default is continuous autofocus. mFocusMode = mPreferences.getString( CameraSettings.KEY_FOCUS_MODE, mDefaultFocusMode); } if (!isSupported(mFocusMode, mParameters.getSupportedFocusModes())) { // 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> getTapArea() { return mTapArea; } public void updateFocusUI() { if (!mInitialized) return; // Set the length of focus rectangle according to preview frame size. int len = Math.min(mPreviewFrame.getWidth(), mPreviewFrame.getHeight()) / 4; ViewGroup.LayoutParams layout = mFocusRectangle.getLayoutParams(); layout.width = len; layout.height = len; if (mState == STATE_IDLE) { if (mTapArea == null) { mFocusRectangle.clear(); } else { // Users touch on the preview and the rectangle indicates the // metering area. Either focus area is not supported or // autoFocus call is not required. mFocusRectangle.showStart(); } return; } // Do not show focus rectangle if there is any face rectangle. if (mFaceView != null && mFaceView.faceExists()) return; if (mState == STATE_FOCUSING || mState == STATE_FOCUSING_SNAP_ON_FINISH) { mFocusRectangle.showStart(); } else if (mState == STATE_SUCCESS) { mFocusRectangle.showSuccess(); } else if (mState == STATE_FAIL) { mFocusRectangle.showFail(); } } public void resetTouchFocus() { if (!mInitialized) return; // Put focus rectangle to the center. RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) mFocusRectangle.getLayoutParams(); int[] rules = p.getRules(); rules[RelativeLayout.CENTER_IN_PARENT] = RelativeLayout.TRUE; p.setMargins(0, 0, 0, 0); mTapArea = null; } // Convert the touch point to the focus area in driver format. public static void convertToFocusArea(int left, int top, int focusWidth, int focusHeight, int previewWidth, int previewHeight, Rect rect) { rect.left = Math.round((float) left / previewWidth * 2000 - 1000); rect.top = Math.round((float) top / previewHeight * 2000 - 1000); rect.right = Math.round((float) (left + focusWidth) / previewWidth * 2000 - 1000); rect.bottom = Math.round((float) (top + focusHeight) / previewHeight * 2000 - 1000); } public boolean isFocusCompleted() { return mState == STATE_SUCCESS || mState == STATE_FAIL; } public void removeMessages() { mHandler.removeMessages(RESET_TOUCH_FOCUS); } public void overrideFocusMode(String focusMode) { mOverrideFocusMode = focusMode; } private static boolean isSupported(String value, List<String> supported) { return supported == null ? false : supported.indexOf(value) >= 0; } }