diff options
Diffstat (limited to 'src/com/android/camera')
102 files changed, 25781 insertions, 0 deletions
diff --git a/src/com/android/camera/AndroidCameraManagerImpl.java b/src/com/android/camera/AndroidCameraManagerImpl.java new file mode 100644 index 000000000..897aa9252 --- /dev/null +++ b/src/com/android/camera/AndroidCameraManagerImpl.java @@ -0,0 +1,779 @@ +/* + * Copyright (C) 2013 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 static com.android.camera.Util.Assert; + +import android.annotation.TargetApi; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.hardware.Camera.AutoFocusCallback; +import android.hardware.Camera.AutoFocusMoveCallback; +import android.hardware.Camera.ErrorCallback; +import android.hardware.Camera.FaceDetectionListener; +import android.hardware.Camera.OnZoomChangeListener; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.PictureCallback; +import android.hardware.Camera.PreviewCallback; +import android.hardware.Camera.ShutterCallback; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.view.SurfaceHolder; + +import com.android.gallery3d.common.ApiHelper; + +import java.io.IOException; + +/** + * A class to implement {@link CameraManager} of the Android camera framework. + */ +class AndroidCameraManagerImpl implements CameraManager { + private static final String TAG = "CAM_" + + AndroidCameraManagerImpl.class.getSimpleName(); + + private Parameters mParameters; + private boolean mParametersIsDirty; + private IOException mReconnectIOException; + + /* Messages used in CameraHandler. */ + // Camera initialization/finalization + private static final int OPEN_CAMERA = 1; + private static final int RELEASE = 2; + private static final int RECONNECT = 3; + private static final int UNLOCK = 4; + private static final int LOCK = 5; + // Preview + private static final int SET_PREVIEW_TEXTURE_ASYNC = 101; + private static final int START_PREVIEW_ASYNC = 102; + private static final int STOP_PREVIEW = 103; + private static final int SET_PREVIEW_CALLBACK_WITH_BUFFER = 104; + private static final int ADD_CALLBACK_BUFFER = 105; + private static final int SET_PREVIEW_DISPLAY_ASYNC = 106; + private static final int SET_PREVIEW_CALLBACK = 107; + // Parameters + private static final int SET_PARAMETERS = 201; + private static final int GET_PARAMETERS = 202; + private static final int REFRESH_PARAMETERS = 203; + // Focus, Zoom + private static final int AUTO_FOCUS = 301; + private static final int CANCEL_AUTO_FOCUS = 302; + private static final int SET_AUTO_FOCUS_MOVE_CALLBACK = 303; + private static final int SET_ZOOM_CHANGE_LISTENER = 304; + // Face detection + private static final int SET_FACE_DETECTION_LISTENER = 461; + private static final int START_FACE_DETECTION = 462; + private static final int STOP_FACE_DETECTION = 463; + private static final int SET_ERROR_CALLBACK = 464; + // Presentation + private static final int ENABLE_SHUTTER_SOUND = 501; + private static final int SET_DISPLAY_ORIENTATION = 502; + + private CameraHandler mCameraHandler; + private android.hardware.Camera mCamera; + + // Used to retain a copy of Parameters for setting parameters. + private Parameters mParamsToSet; + + AndroidCameraManagerImpl() { + HandlerThread ht = new HandlerThread("Camera Handler Thread"); + ht.start(); + mCameraHandler = new CameraHandler(ht.getLooper()); + } + + private class CameraHandler extends Handler { + CameraHandler(Looper looper) { + super(looper); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void startFaceDetection() { + mCamera.startFaceDetection(); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void stopFaceDetection() { + mCamera.stopFaceDetection(); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void setFaceDetectionListener(FaceDetectionListener listener) { + mCamera.setFaceDetectionListener(listener); + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + private void setPreviewTexture(Object surfaceTexture) { + try { + mCamera.setPreviewTexture((SurfaceTexture) surfaceTexture); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN_MR1) + private void enableShutterSound(boolean enable) { + mCamera.enableShutterSound(enable); + } + + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + private void setAutoFocusMoveCallback( + android.hardware.Camera camera, Object cb) { + camera.setAutoFocusMoveCallback((AutoFocusMoveCallback) cb); + } + + public void requestTakePicture( + final ShutterCallback shutter, + final PictureCallback raw, + final PictureCallback postView, + final PictureCallback jpeg) { + post(new Runnable() { + @Override + public void run() { + try { + mCamera.takePicture(shutter, raw, postView, jpeg); + } catch (RuntimeException e) { + // TODO: output camera state and focus state for debugging. + Log.e(TAG, "take picture failed."); + throw e; + } + } + }); + } + + /** + * Waits for all the {@code Message} and {@code Runnable} currently in the queue + * are processed. + * + * @return {@code false} if the wait was interrupted, {@code true} otherwise. + */ + public boolean waitDone() { + final Object waitDoneLock = new Object(); + final Runnable unlockRunnable = new Runnable() { + @Override + public void run() { + synchronized (waitDoneLock) { + waitDoneLock.notifyAll(); + } + } + }; + + synchronized (waitDoneLock) { + mCameraHandler.post(unlockRunnable); + try { + waitDoneLock.wait(); + } catch (InterruptedException ex) { + Log.v(TAG, "waitDone interrupted"); + return false; + } + } + return true; + } + + /** + * This method does not deal with the API level check. Everyone should + * check first for supported operations before sending message to this handler. + */ + @Override + public void handleMessage(final Message msg) { + try { + switch (msg.what) { + case OPEN_CAMERA: + mCamera = android.hardware.Camera.open(msg.arg1); + if (mCamera != null) { + mParametersIsDirty = true; + + // Get a instance of Camera.Parameters for later use. + if (mParamsToSet == null) { + mParamsToSet = mCamera.getParameters(); + } + } + return; + + case RELEASE: + mCamera.release(); + mCamera = null; + return; + + case RECONNECT: + mReconnectIOException = null; + try { + mCamera.reconnect(); + } catch (IOException ex) { + mReconnectIOException = ex; + } + return; + + case UNLOCK: + mCamera.unlock(); + return; + + case LOCK: + mCamera.lock(); + return; + + case SET_PREVIEW_TEXTURE_ASYNC: + setPreviewTexture(msg.obj); + return; + + case SET_PREVIEW_DISPLAY_ASYNC: + try { + mCamera.setPreviewDisplay((SurfaceHolder) msg.obj); + } catch (IOException e) { + throw new RuntimeException(e); + } + return; + + case START_PREVIEW_ASYNC: + mCamera.startPreview(); + return; + + case STOP_PREVIEW: + mCamera.stopPreview(); + return; + + case SET_PREVIEW_CALLBACK_WITH_BUFFER: + mCamera.setPreviewCallbackWithBuffer( + (PreviewCallback) msg.obj); + return; + + case ADD_CALLBACK_BUFFER: + mCamera.addCallbackBuffer((byte[]) msg.obj); + return; + + case AUTO_FOCUS: + mCamera.autoFocus((AutoFocusCallback) msg.obj); + return; + + case CANCEL_AUTO_FOCUS: + mCamera.cancelAutoFocus(); + return; + + case SET_AUTO_FOCUS_MOVE_CALLBACK: + setAutoFocusMoveCallback(mCamera, msg.obj); + return; + + case SET_DISPLAY_ORIENTATION: + mCamera.setDisplayOrientation(msg.arg1); + return; + + case SET_ZOOM_CHANGE_LISTENER: + mCamera.setZoomChangeListener( + (OnZoomChangeListener) msg.obj); + return; + + case SET_FACE_DETECTION_LISTENER: + setFaceDetectionListener((FaceDetectionListener) msg.obj); + return; + + case START_FACE_DETECTION: + startFaceDetection(); + return; + + case STOP_FACE_DETECTION: + stopFaceDetection(); + return; + + case SET_ERROR_CALLBACK: + mCamera.setErrorCallback((ErrorCallback) msg.obj); + return; + + case SET_PARAMETERS: + mParametersIsDirty = true; + mParamsToSet.unflatten((String) msg.obj); + mCamera.setParameters(mParamsToSet); + return; + + case GET_PARAMETERS: + if (mParametersIsDirty) { + mParameters = mCamera.getParameters(); + mParametersIsDirty = false; + } + return; + + case SET_PREVIEW_CALLBACK: + mCamera.setPreviewCallback((PreviewCallback) msg.obj); + return; + + case ENABLE_SHUTTER_SOUND: + enableShutterSound((msg.arg1 == 1) ? true : false); + return; + + case REFRESH_PARAMETERS: + mParametersIsDirty = true; + return; + + default: + throw new RuntimeException("Invalid CameraProxy message=" + msg.what); + } + } catch (RuntimeException e) { + if (msg.what != RELEASE && mCamera != null) { + try { + mCamera.release(); + } catch (Exception ex) { + Log.e(TAG, "Fail to release the camera."); + } + mCamera = null; + } + throw e; + } + } + } + + @Override + public CameraManager.CameraProxy cameraOpen(int cameraId) { + mCameraHandler.obtainMessage(OPEN_CAMERA, cameraId, 0).sendToTarget(); + mCameraHandler.waitDone(); + if (mCamera != null) { + return new AndroidCameraProxyImpl(); + } else { + return null; + } + } + + /** + * A class which implements {@link CameraManager.CameraProxy} and + * camera handler thread. + */ + public class AndroidCameraProxyImpl implements CameraManager.CameraProxy { + + private AndroidCameraProxyImpl() { + Assert(mCamera != null); + } + + @Override + public android.hardware.Camera getCamera() { + return mCamera; + } + + @Override + public void release() { + // release() must be synchronous so we know exactly when the camera + // is released and can continue on. + mCameraHandler.sendEmptyMessage(RELEASE); + mCameraHandler.waitDone(); + } + + @Override + public void reconnect() throws IOException { + mCameraHandler.sendEmptyMessage(RECONNECT); + mCameraHandler.waitDone(); + if (mReconnectIOException != null) { + throw mReconnectIOException; + } + } + + @Override + public void unlock() { + mCameraHandler.sendEmptyMessage(UNLOCK); + mCameraHandler.waitDone(); + } + + @Override + public void lock() { + mCameraHandler.sendEmptyMessage(LOCK); + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + @Override + public void setPreviewTexture(SurfaceTexture surfaceTexture) { + mCameraHandler.obtainMessage(SET_PREVIEW_TEXTURE_ASYNC, surfaceTexture).sendToTarget(); + } + + @Override + public void setPreviewDisplay(SurfaceHolder surfaceHolder) { + mCameraHandler.obtainMessage(SET_PREVIEW_DISPLAY_ASYNC, surfaceHolder).sendToTarget(); + } + + @Override + public void startPreview() { + mCameraHandler.sendEmptyMessage(START_PREVIEW_ASYNC); + } + + @Override + public void stopPreview() { + mCameraHandler.sendEmptyMessage(STOP_PREVIEW); + mCameraHandler.waitDone(); + } + + @Override + public void setPreviewDataCallback( + Handler handler, CameraPreviewDataCallback cb) { + mCameraHandler.obtainMessage( + SET_PREVIEW_CALLBACK, + PreviewCallbackForward.getNewInstance(handler, this, cb)).sendToTarget(); + } + + @Override + public void setPreviewDataCallbackWithBuffer( + Handler handler, CameraPreviewDataCallback cb) { + mCameraHandler.obtainMessage( + SET_PREVIEW_CALLBACK_WITH_BUFFER, + PreviewCallbackForward.getNewInstance(handler, this, cb)).sendToTarget(); + } + + @Override + public void addCallbackBuffer(byte[] callbackBuffer) { + mCameraHandler.obtainMessage(ADD_CALLBACK_BUFFER, callbackBuffer).sendToTarget(); + } + + @Override + public void autoFocus(Handler handler, CameraAFCallback cb) { + mCameraHandler.obtainMessage( + AUTO_FOCUS, + AFCallbackForward.getNewInstance(handler, this, cb)).sendToTarget(); + } + + @Override + public void cancelAutoFocus() { + mCameraHandler.removeMessages(AUTO_FOCUS); + mCameraHandler.sendEmptyMessage(CANCEL_AUTO_FOCUS); + } + + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + @Override + public void setAutoFocusMoveCallback( + Handler handler, CameraAFMoveCallback cb) { + mCameraHandler.obtainMessage( + SET_AUTO_FOCUS_MOVE_CALLBACK, + AFMoveCallbackForward.getNewInstance(handler, this, cb)).sendToTarget(); + } + + @Override + public void takePicture( + Handler handler, + CameraShutterCallback shutter, + CameraPictureCallback raw, + CameraPictureCallback post, + CameraPictureCallback jpeg) { + mCameraHandler.requestTakePicture( + ShutterCallbackForward.getNewInstance(handler, this, shutter), + PictureCallbackForward.getNewInstance(handler, this, raw), + PictureCallbackForward.getNewInstance(handler, this, post), + PictureCallbackForward.getNewInstance(handler, this, jpeg)); + } + + @Override + public void setDisplayOrientation(int degrees) { + mCameraHandler.obtainMessage(SET_DISPLAY_ORIENTATION, degrees, 0) + .sendToTarget(); + } + + @Override + public void setZoomChangeListener(OnZoomChangeListener listener) { + mCameraHandler.obtainMessage(SET_ZOOM_CHANGE_LISTENER, listener).sendToTarget(); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + public void setFaceDetectionCallback( + Handler handler, CameraFaceDetectionCallback cb) { + mCameraHandler.obtainMessage( + SET_FACE_DETECTION_LISTENER, + FaceDetectionCallbackForward.getNewInstance(handler, this, cb)).sendToTarget(); + } + + @Override + public void startFaceDetection() { + mCameraHandler.sendEmptyMessage(START_FACE_DETECTION); + } + + @Override + public void stopFaceDetection() { + mCameraHandler.sendEmptyMessage(STOP_FACE_DETECTION); + } + + @Override + public void setErrorCallback(ErrorCallback cb) { + mCameraHandler.obtainMessage(SET_ERROR_CALLBACK, cb).sendToTarget(); + } + + @Override + public void setParameters(Parameters params) { + if (params == null) { + Log.v(TAG, "null parameters in setParameters()"); + return; + } + mCameraHandler.obtainMessage(SET_PARAMETERS, params.flatten()) + .sendToTarget(); + } + + @Override + public Parameters getParameters() { + mCameraHandler.sendEmptyMessage(GET_PARAMETERS); + mCameraHandler.waitDone(); + return mParameters; + } + + @Override + public void refreshParameters() { + mCameraHandler.sendEmptyMessage(REFRESH_PARAMETERS); + } + + @Override + public void enableShutterSound(boolean enable) { + mCameraHandler.obtainMessage( + ENABLE_SHUTTER_SOUND, (enable ? 1 : 0), 0).sendToTarget(); + } + } + + /** + * A helper class to forward AutoFocusCallback to another thread. + */ + private static class AFCallbackForward implements AutoFocusCallback { + private final Handler mHandler; + private final CameraProxy mCamera; + private final CameraAFCallback mCallback; + + /** + * Returns a new instance of {@link AFCallbackForward}. + * + * @param handler The handler in which the callback will be invoked in. + * @param camera The {@link CameraProxy} which the callback is from. + * @param cb The callback to be invoked. + * @return The instance of the {@link AFCallbackForward}, + * or null if any parameter is null. + */ + public static AFCallbackForward getNewInstance( + Handler handler, CameraProxy camera, CameraAFCallback cb) { + if (handler == null || camera == null || cb == null) return null; + return new AFCallbackForward(handler, camera, cb); + } + + private AFCallbackForward( + Handler h, CameraProxy camera, CameraAFCallback cb) { + mHandler = h; + mCamera = camera; + mCallback = cb; + } + + @Override + public void onAutoFocus(final boolean b, Camera camera) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onAutoFocus(b, mCamera); + } + }); + } + } + + /** A helper class to forward AutoFocusMoveCallback to another thread. */ + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + private static class AFMoveCallbackForward implements AutoFocusMoveCallback { + private final Handler mHandler; + private final CameraAFMoveCallback mCallback; + private final CameraProxy mCamera; + + /** + * Returns a new instance of {@link AFMoveCallbackForward}. + * + * @param handler The handler in which the callback will be invoked in. + * @param camera The {@link CameraProxy} which the callback is from. + * @param cb The callback to be invoked. + * @return The instance of the {@link AFMoveCallbackForward}, + * or null if any parameter is null. + */ + public static AFMoveCallbackForward getNewInstance( + Handler handler, CameraProxy camera, CameraAFMoveCallback cb) { + if (handler == null || camera == null || cb == null) return null; + return new AFMoveCallbackForward(handler, camera, cb); + } + + private AFMoveCallbackForward( + Handler h, CameraProxy camera, CameraAFMoveCallback cb) { + mHandler = h; + mCamera = camera; + mCallback = cb; + } + + @Override + public void onAutoFocusMoving( + final boolean moving, android.hardware.Camera camera) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onAutoFocusMoving(moving, mCamera); + } + }); + } + } + + /** + * A helper class to forward ShutterCallback to to another thread. + */ + private static class ShutterCallbackForward implements ShutterCallback { + private final Handler mHandler; + private final CameraShutterCallback mCallback; + private final CameraProxy mCamera; + + /** + * Returns a new instance of {@link ShutterCallbackForward}. + * + * @param handler The handler in which the callback will be invoked in. + * @param camera The {@link CameraProxy} which the callback is from. + * @param cb The callback to be invoked. + * @return The instance of the {@link ShutterCallbackForward}, + * or null if any parameter is null. + */ + public static ShutterCallbackForward getNewInstance( + Handler handler, CameraProxy camera, CameraShutterCallback cb) { + if (handler == null || camera == null || cb == null) return null; + return new ShutterCallbackForward(handler, camera, cb); + } + + private ShutterCallbackForward( + Handler h, CameraProxy camera, CameraShutterCallback cb) { + mHandler = h; + mCamera = camera; + mCallback = cb; + } + + @Override + public void onShutter() { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onShutter(mCamera); + } + }); + } + } + + /** + * A helper class to forward PictureCallback to another thread. + */ + private static class PictureCallbackForward implements PictureCallback { + private final Handler mHandler; + private final CameraPictureCallback mCallback; + private final CameraProxy mCamera; + + /** + * Returns a new instance of {@link PictureCallbackForward}. + * + * @param handler The handler in which the callback will be invoked in. + * @param camera The {@link CameraProxy} which the callback is from. + * @param cb The callback to be invoked. + * @return The instance of the {@link PictureCallbackForward}, + * or null if any parameters is null. + */ + public static PictureCallbackForward getNewInstance( + Handler handler, CameraProxy camera, CameraPictureCallback cb) { + if (handler == null || camera == null || cb == null) return null; + return new PictureCallbackForward(handler, camera, cb); + } + + private PictureCallbackForward( + Handler h, CameraProxy camera, CameraPictureCallback cb) { + mHandler = h; + mCamera = camera; + mCallback = cb; + } + + @Override + public void onPictureTaken( + final byte[] data, android.hardware.Camera camera) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onPictureTaken(data, mCamera); + } + }); + } + } + + /** + * A helper class to forward PreviewCallback to another thread. + */ + private static class PreviewCallbackForward implements PreviewCallback { + private final Handler mHandler; + private final CameraPreviewDataCallback mCallback; + private final CameraProxy mCamera; + + /** + * Returns a new instance of {@link PreviewCallbackForward}. + * + * @param handler The handler in which the callback will be invoked in. + * @param camera The {@link CameraProxy} which the callback is from. + * @param cb The callback to be invoked. + * @return The instance of the {@link PreviewCallbackForward}, + * or null if any parameters is null. + */ + public static PreviewCallbackForward getNewInstance( + Handler handler, CameraProxy camera, CameraPreviewDataCallback cb) { + if (handler == null || camera == null || cb == null) return null; + return new PreviewCallbackForward(handler, camera, cb); + } + + private PreviewCallbackForward( + Handler h, CameraProxy camera, CameraPreviewDataCallback cb) { + mHandler = h; + mCamera = camera; + mCallback = cb; + } + + @Override + public void onPreviewFrame( + final byte[] data, android.hardware.Camera camera) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onPreviewFrame(data, mCamera); + } + }); + } + } + + private static class FaceDetectionCallbackForward implements FaceDetectionListener { + private final Handler mHandler; + private final CameraFaceDetectionCallback mCallback; + private final CameraProxy mCamera; + + /** + * Returns a new instance of {@link FaceDetectionCallbackForward}. + * + * @param handler The handler in which the callback will be invoked in. + * @param camera The {@link CameraProxy} which the callback is from. + * @param cb The callback to be invoked. + * @return The instance of the {@link FaceDetectionCallbackForward}, + * or null if any parameter is null. + */ + public static FaceDetectionCallbackForward getNewInstance( + Handler handler, CameraProxy camera, CameraFaceDetectionCallback cb) { + if (handler == null || camera == null || cb == null) return null; + return new FaceDetectionCallbackForward(handler, camera, cb); + } + + private FaceDetectionCallbackForward( + Handler h, CameraProxy camera, CameraFaceDetectionCallback cb) { + mHandler = h; + mCamera = camera; + mCallback = cb; + } + + @Override + public void onFaceDetection( + final Camera.Face[] faces, Camera camera) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onFaceDetection(faces, mCamera); + } + }); + } + } +} diff --git a/src/com/android/camera/CameraActivity.java b/src/com/android/camera/CameraActivity.java new file mode 100644 index 000000000..7f71d5f31 --- /dev/null +++ b/src/com/android/camera/CameraActivity.java @@ -0,0 +1,571 @@ +/* + * Copyright (C) 2012 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 android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.graphics.drawable.ColorDrawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.provider.Settings; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.OrientationEventListener; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.ImageView; + +import com.android.camera.data.CameraDataAdapter; +import com.android.camera.data.CameraPreviewData; +import com.android.camera.data.FixedFirstDataAdapter; +import com.android.camera.data.FixedLastDataAdapter; +import com.android.camera.data.LocalData; +import com.android.camera.data.LocalDataAdapter; +import com.android.camera.ui.CameraSwitcher; +import com.android.camera.ui.CameraSwitcher.CameraSwitchListener; +import com.android.camera.ui.FilmStripView; +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.util.LightCycleHelper; +import com.android.gallery3d.util.RefocusHelper; +import com.android.gallery3d.util.LightCycleHelper.PanoramaViewHelper; + +public class CameraActivity extends Activity + implements CameraSwitchListener { + + private static final String TAG = "CAM_Activity"; + + private static final String INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE = + "android.media.action.STILL_IMAGE_CAMERA_SECURE"; + public static final String ACTION_IMAGE_CAPTURE_SECURE = + "android.media.action.IMAGE_CAPTURE_SECURE"; + + // The intent extra for camera from secure lock screen. True if the gallery + // should only show newly captured pictures. sSecureAlbumId does not + // increment. This is used when switching between camera, camcorder, and + // panorama. If the extra is not set, it is in the normal camera mode. + public static final String SECURE_CAMERA_EXTRA = "secure_camera"; + + /** This data adapter is used by FilmStirpView. */ + private LocalDataAdapter mDataAdapter; + /** This data adapter represents the real local camera data. */ + private LocalDataAdapter mWrappedDataAdapter; + + private PanoramaStitchingManager mPanoramaManager; + private int mCurrentModuleIndex; + private CameraModule mCurrentModule; + private View mRootView; + private FilmStripView mFilmStripView; + private int mResultCodeForTesting; + private Intent mResultDataForTesting; + private OnScreenHint mStorageHint; + private long mStorageSpace = Storage.LOW_STORAGE_THRESHOLD; + private boolean mAutoRotateScreen; + private boolean mSecureCamera; + // This is a hack to speed up the start of SecureCamera. + private static boolean sFirstStartAfterScreenOn = true; + private boolean mShowCameraPreview; + private int mLastRawOrientation; + private MyOrientationEventListener mOrientationListener; + private Handler mMainHandler; + private PanoramaViewHelper mPanoramaViewHelper; + private CameraPreviewData mCameraPreviewData; + + private class MyOrientationEventListener + extends OrientationEventListener { + public MyOrientationEventListener(Context context) { + super(context); + } + + @Override + public void onOrientationChanged(int orientation) { + // We keep the last known orientation. So if the user first orient + // the camera then point the camera to floor or sky, we still have + // the correct orientation. + if (orientation == ORIENTATION_UNKNOWN) return; + mLastRawOrientation = orientation; + mCurrentModule.onOrientationChanged(orientation); + } + } + + private MediaSaveService mMediaSaveService; + private ServiceConnection mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName className, IBinder b) { + mMediaSaveService = ((MediaSaveService.LocalBinder) b).getService(); + mCurrentModule.onMediaSaveServiceConnected(mMediaSaveService); + } + @Override + public void onServiceDisconnected(ComponentName className) { + mMediaSaveService = null; + }}; + + // close activity when screen turns off + private BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + finish(); + } + }; + + private static BroadcastReceiver sScreenOffReceiver; + private static class ScreenOffReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + sFirstStartAfterScreenOn = true; + } + } + + public static boolean isFirstStartAfterScreenOn() { + return sFirstStartAfterScreenOn; + } + + public static void resetFirstStartAfterScreenOn() { + sFirstStartAfterScreenOn = false; + } + + private FilmStripView.Listener mFilmStripListener = new FilmStripView.Listener() { + @Override + public void onDataPromoted(int dataID) { + removeData(dataID); + } + + @Override + public void onDataDemoted(int dataID) { + removeData(dataID); + } + + @Override + public void onDataFullScreenChange(int dataID, boolean full) { + } + + @Override + public void onSwitchMode(boolean toCamera) { + mCurrentModule.onSwitchMode(toCamera); + } + }; + + private Runnable mDeletionRunnable = new Runnable() { + @Override + public void run() { + mDataAdapter.executeDeletion(CameraActivity.this); + } + }; + + private ImageTaskManager.TaskListener mStitchingListener = + new ImageTaskManager.TaskListener() { + @Override + public void onTaskQueued(String filePath, Uri imageUri) { + } + + @Override + public void onTaskDone(String filePath, Uri imageUri) { + } + + @Override + public void onTaskProgress( + String filePath, Uri imageUri, int progress) { + } + }; + + public MediaSaveService getMediaSaveService() { + return mMediaSaveService; + } + + public void notifyNewMedia(Uri uri) { + ContentResolver cr = getContentResolver(); + String mimeType = cr.getType(uri); + if (mimeType.startsWith("video/")) { + sendBroadcast(new Intent(Util.ACTION_NEW_VIDEO, uri)); + mDataAdapter.addNewVideo(cr, uri); + } else if (mimeType.startsWith("image/")) { + Util.broadcastNewPicture(this, uri); + mDataAdapter.addNewPhoto(cr, uri); + } else { + android.util.Log.w(TAG, "Unknown new media with MIME type:" + + mimeType + ", uri:" + uri); + } + } + + private void removeData(int dataID) { + mDataAdapter.removeData(CameraActivity.this, dataID); + mMainHandler.removeCallbacks(mDeletionRunnable); + mMainHandler.postDelayed(mDeletionRunnable, 3000); + } + + private void bindMediaSaveService() { + Intent intent = new Intent(this, MediaSaveService.class); + startService(intent); // start service before binding it so the + // service won't be killed if we unbind it. + bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + } + + private void unbindMediaSaveService() { + if (mMediaSaveService != null) { + mMediaSaveService.setListener(null); + } + if (mConnection != null) { + unbindService(mConnection); + } + } + + @Override + public void onCreate(Bundle state) { + super.onCreate(state); + setContentView(R.layout.camera_filmstrip); + if (ApiHelper.HAS_ROTATION_ANIMATION) { + setRotationAnimation(); + } + // Check if this is in the secure camera mode. + Intent intent = getIntent(); + String action = intent.getAction(); + if (INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE.equals(action) + || ACTION_IMAGE_CAPTURE_SECURE.equals(action)) { + mSecureCamera = true; + } else { + mSecureCamera = intent.getBooleanExtra(SECURE_CAMERA_EXTRA, false); + } + + if (mSecureCamera) { + // Change the window flags so that secure camera can show when locked + Window win = getWindow(); + WindowManager.LayoutParams params = win.getAttributes(); + params.flags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; + win.setAttributes(params); + + // Filter for screen off so that we can finish secure camera activity + // when screen is off. + IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF); + registerReceiver(mScreenOffReceiver, filter); + // TODO: This static screen off event receiver is a workaround to the + // double onResume() invocation (onResume->onPause->onResume). We should + // find a better solution to this. + if (sScreenOffReceiver == null) { + sScreenOffReceiver = new ScreenOffReceiver(); + registerReceiver(sScreenOffReceiver, filter); + } + } + mPanoramaManager = new PanoramaStitchingManager(CameraActivity.this); + mPanoramaManager.addTaskListener(mStitchingListener); + LayoutInflater inflater = getLayoutInflater(); + View rootLayout = inflater.inflate(R.layout.camera, null, false); + mRootView = rootLayout.findViewById(R.id.camera_app_root); + mCameraPreviewData = new CameraPreviewData(rootLayout, + FilmStripView.ImageData.SIZE_FULL, + FilmStripView.ImageData.SIZE_FULL); + // Put a CameraPreviewData at the first position. + mWrappedDataAdapter = new FixedFirstDataAdapter( + new CameraDataAdapter(new ColorDrawable( + getResources().getColor(R.color.photo_placeholder))), + mCameraPreviewData); + mFilmStripView = (FilmStripView) findViewById(R.id.filmstrip_view); + mFilmStripView.setViewGap( + getResources().getDimensionPixelSize(R.dimen.camera_film_strip_gap)); + mPanoramaViewHelper = new PanoramaViewHelper(this); + mPanoramaViewHelper.onCreate(); + mFilmStripView.setPanoramaViewHelper(mPanoramaViewHelper); + // Set up the camera preview first so the preview shows up ASAP. + mFilmStripView.setListener(mFilmStripListener); + mCurrentModule = new PhotoModule(); + mCurrentModule.init(this, mRootView); + mOrientationListener = new MyOrientationEventListener(this); + mMainHandler = new Handler(getMainLooper()); + bindMediaSaveService(); + + if (!mSecureCamera) { + mDataAdapter = mWrappedDataAdapter; + mDataAdapter.requestLoad(getContentResolver()); + } else { + // Put a lock placeholder as the last image by setting its date to 0. + ImageView v = (ImageView) getLayoutInflater().inflate( + R.layout.secure_album_placeholder, null); + mDataAdapter = new FixedLastDataAdapter( + mWrappedDataAdapter, + new LocalData.LocalViewData( + v, + v.getDrawable().getIntrinsicWidth(), + v.getDrawable().getIntrinsicHeight(), + 0, 0)); + // Flush out all the original data. + mDataAdapter.flush(); + } + mFilmStripView.setDataAdapter(mDataAdapter); + } + + private void setRotationAnimation() { + int rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE; + rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE; + Window win = getWindow(); + WindowManager.LayoutParams winParams = win.getAttributes(); + winParams.rotationAnimation = rotationAnimation; + win.setAttributes(winParams); + } + + @Override + public void onUserInteraction() { + super.onUserInteraction(); + mCurrentModule.onUserInteraction(); + } + + @Override + public void onPause() { + mOrientationListener.disable(); + mCurrentModule.onPauseBeforeSuper(); + super.onPause(); + mCurrentModule.onPauseAfterSuper(); + } + + @Override + public void onResume() { + if (Settings.System.getInt(getContentResolver(), + Settings.System.ACCELEROMETER_ROTATION, 0) == 0) {// auto-rotate off + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + mAutoRotateScreen = false; + } else { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR); + mAutoRotateScreen = true; + } + mOrientationListener.enable(); + mCurrentModule.onResumeBeforeSuper(); + super.onResume(); + mCurrentModule.onResumeAfterSuper(); + + setSwipingEnabled(true); + } + + @Override + public void onStart() { + super.onStart(); + + mPanoramaViewHelper.onStart(); + } + + @Override + protected void onStop() { + super.onStop(); + mPanoramaViewHelper.onStop(); + } + + @Override + public void onDestroy() { + unbindMediaSaveService(); + if (mSecureCamera) unregisterReceiver(mScreenOffReceiver); + super.onDestroy(); + } + + @Override + public void onConfigurationChanged(Configuration config) { + super.onConfigurationChanged(config); + mCurrentModule.onConfigurationChanged(config); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (mCurrentModule.onKeyDown(keyCode, event)) return true; + // Prevent software keyboard or voice search from showing up. + if (keyCode == KeyEvent.KEYCODE_SEARCH + || keyCode == KeyEvent.KEYCODE_MENU) { + if (event.isLongPress()) return true; + } + if (keyCode == KeyEvent.KEYCODE_MENU && mShowCameraPreview) { + return true; + } + + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (mCurrentModule.onKeyUp(keyCode, event)) return true; + if (keyCode == KeyEvent.KEYCODE_MENU && mShowCameraPreview) { + return true; + } + return super.onKeyUp(keyCode, event); + } + + public boolean isAutoRotateScreen() { + return mAutoRotateScreen; + } + + protected void updateStorageSpace() { + mStorageSpace = Storage.getAvailableSpace(); + } + + protected long getStorageSpace() { + return mStorageSpace; + } + + protected void updateStorageSpaceAndHint() { + updateStorageSpace(); + updateStorageHint(mStorageSpace); + } + + protected void updateStorageHint() { + updateStorageHint(mStorageSpace); + } + + protected boolean updateStorageHintOnResume() { + return true; + } + + protected void updateStorageHint(long storageSpace) { + String message = null; + if (storageSpace == Storage.UNAVAILABLE) { + message = getString(R.string.no_storage); + } else if (storageSpace == Storage.PREPARING) { + message = getString(R.string.preparing_sd); + } else if (storageSpace == Storage.UNKNOWN_SIZE) { + message = getString(R.string.access_sd_fail); + } else if (storageSpace <= Storage.LOW_STORAGE_THRESHOLD) { + message = getString(R.string.spaceIsLow_content); + } + + if (message != null) { + if (mStorageHint == null) { + mStorageHint = OnScreenHint.makeText(this, message); + } else { + mStorageHint.setText(message); + } + mStorageHint.show(); + } else if (mStorageHint != null) { + mStorageHint.cancel(); + mStorageHint = null; + } + } + + protected void setResultEx(int resultCode) { + mResultCodeForTesting = resultCode; + setResult(resultCode); + } + + protected void setResultEx(int resultCode, Intent data) { + mResultCodeForTesting = resultCode; + mResultDataForTesting = data; + setResult(resultCode, data); + } + + public int getResultCode() { + return mResultCodeForTesting; + } + + public Intent getResultData() { + return mResultDataForTesting; + } + + public boolean isSecureCamera() { + return mSecureCamera; + } + + @Override + public void onCameraSelected(int i) { + if (mCurrentModuleIndex == i) return; + + CameraHolder.instance().keep(); + closeModule(mCurrentModule); + mCurrentModuleIndex = i; + switch (i) { + case CameraSwitcher.VIDEO_MODULE_INDEX: + mCurrentModule = new VideoModule(); + break; + case CameraSwitcher.PHOTO_MODULE_INDEX: + mCurrentModule = new PhotoModule(); + break; + case CameraSwitcher.LIGHTCYCLE_MODULE_INDEX: + mCurrentModule = LightCycleHelper.createPanoramaModule(); + break; + case CameraSwitcher.REFOCUS_MODULE_INDEX: + mCurrentModule = RefocusHelper.createRefocusModule(); + break; + default: + break; + } + + openModule(mCurrentModule); + mCurrentModule.onOrientationChanged(mLastRawOrientation); + if (mMediaSaveService != null) { + mCurrentModule.onMediaSaveServiceConnected(mMediaSaveService); + } + } + + private void openModule(CameraModule module) { + module.init(this, mRootView); + module.onResumeBeforeSuper(); + module.onResumeAfterSuper(); + } + + private void closeModule(CameraModule module) { + module.onPauseBeforeSuper(); + module.onPauseAfterSuper(); + ((ViewGroup) mRootView).removeAllViews(); + } + + @Override + public void onShowSwitcherPopup() { + } + + public void setSwipingEnabled(boolean enable) { + mCameraPreviewData.lockPreview(!enable); + } + + // Accessor methods for getting latency times used in performance testing + public long getAutoFocusTime() { + return (mCurrentModule instanceof PhotoModule) ? + ((PhotoModule) mCurrentModule).mAutoFocusTime : -1; + } + + public long getShutterLag() { + return (mCurrentModule instanceof PhotoModule) ? + ((PhotoModule) mCurrentModule).mShutterLag : -1; + } + + public long getShutterToPictureDisplayedTime() { + return (mCurrentModule instanceof PhotoModule) ? + ((PhotoModule) mCurrentModule).mShutterToPictureDisplayedTime : -1; + } + + public long getPictureDisplayedToJpegCallbackTime() { + return (mCurrentModule instanceof PhotoModule) ? + ((PhotoModule) mCurrentModule).mPictureDisplayedToJpegCallbackTime : -1; + } + + public long getJpegCallbackFinishTime() { + return (mCurrentModule instanceof PhotoModule) ? + ((PhotoModule) mCurrentModule).mJpegCallbackFinishTime : -1; + } + + public long getCaptureStartTime() { + return (mCurrentModule instanceof PhotoModule) ? + ((PhotoModule) mCurrentModule).mCaptureStartTime : -1; + } + + public boolean isRecording() { + return (mCurrentModule instanceof VideoModule) ? + ((VideoModule) mCurrentModule).isRecording() : false; + } +} diff --git a/src/com/android/camera/CameraBackupAgent.java b/src/com/android/camera/CameraBackupAgent.java new file mode 100644 index 000000000..30ba212df --- /dev/null +++ b/src/com/android/camera/CameraBackupAgent.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2012 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 android.app.backup.BackupAgentHelper; +import android.app.backup.SharedPreferencesBackupHelper; +import android.content.Context; + +public class CameraBackupAgent extends BackupAgentHelper { + private static final String CAMERA_BACKUP_KEY = "camera_prefs"; + + public void onCreate () { + Context context = getApplicationContext(); + String prefNames[] = ComboPreferences.getSharedPreferencesNames(context); + + addHelper(CAMERA_BACKUP_KEY, new SharedPreferencesBackupHelper(context, prefNames)); + } +} diff --git a/src/com/android/camera/CameraButtonIntentReceiver.java b/src/com/android/camera/CameraButtonIntentReceiver.java new file mode 100644 index 000000000..a65942d57 --- /dev/null +++ b/src/com/android/camera/CameraButtonIntentReceiver.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2007 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 android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * {@code CameraButtonIntentReceiver} is invoked when the camera button is + * long-pressed. + * + * It is declared in {@code AndroidManifest.xml} to receive the + * {@code android.intent.action.CAMERA_BUTTON} intent. + * + * After making sure we can use the camera hardware, it starts the Camera + * activity. + */ +public class CameraButtonIntentReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + // Try to get the camera hardware + CameraHolder holder = CameraHolder.instance(); + ComboPreferences pref = new ComboPreferences(context); + int cameraId = CameraSettings.readPreferredCameraId(pref); + if (holder.tryOpen(cameraId) == null) return; + + // We are going to launch the camera, so hold the camera for later use + holder.keep(); + holder.release(); + Intent i = new Intent(Intent.ACTION_MAIN); + i.setClass(context, CameraActivity.class); + i.addCategory(Intent.CATEGORY_LAUNCHER); + i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + context.startActivity(i); + } +} diff --git a/src/com/android/camera/CameraDisabledException.java b/src/com/android/camera/CameraDisabledException.java new file mode 100644 index 000000000..512809be6 --- /dev/null +++ b/src/com/android/camera/CameraDisabledException.java @@ -0,0 +1,24 @@ +/* + * 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; + +/** + * This class represents the condition that device policy manager has disabled + * the camera. + */ +public class CameraDisabledException extends Exception { +} diff --git a/src/com/android/camera/CameraErrorCallback.java b/src/com/android/camera/CameraErrorCallback.java new file mode 100644 index 000000000..22f800ef9 --- /dev/null +++ b/src/com/android/camera/CameraErrorCallback.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2010 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 android.util.Log; + +public class CameraErrorCallback + implements android.hardware.Camera.ErrorCallback { + private static final String TAG = "CameraErrorCallback"; + + @Override + public void onError(int error, android.hardware.Camera camera) { + Log.e(TAG, "Got camera error callback. error=" + error); + if (error == android.hardware.Camera.CAMERA_ERROR_SERVER_DIED) { + // We are not sure about the current state of the app (in preview or + // snapshot or recording). Closing the app is better than creating a + // new Camera object. + throw new RuntimeException("Media server died."); + } + } +} diff --git a/src/com/android/camera/CameraHardwareException.java b/src/com/android/camera/CameraHardwareException.java new file mode 100644 index 000000000..82090554d --- /dev/null +++ b/src/com/android/camera/CameraHardwareException.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2009 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; + +/** + * This class represents the condition that we cannot open the camera hardware + * successfully. For example, another process is using the camera. + */ +public class CameraHardwareException extends Exception { + + public CameraHardwareException(Throwable t) { + super(t); + } +} diff --git a/src/com/android/camera/CameraHolder.java b/src/com/android/camera/CameraHolder.java new file mode 100644 index 000000000..d913df709 --- /dev/null +++ b/src/com/android/camera/CameraHolder.java @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2009 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 static com.android.camera.Util.Assert; + +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.Parameters; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import com.android.camera.CameraManager.CameraProxy; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; + +/** + * The class is used to hold an {@code android.hardware.Camera} instance. + * + * <p>The {@code open()} and {@code release()} calls are similar to the ones + * in {@code android.hardware.Camera}. The difference is if {@code keep()} is + * called before {@code release()}, CameraHolder will try to hold the {@code + * android.hardware.Camera} instance for a while, so if {@code open()} is + * called soon after, we can avoid the cost of {@code open()} in {@code + * android.hardware.Camera}. + * + * <p>This is used in switching between different modules. + */ +public class CameraHolder { + private static final String TAG = "CameraHolder"; + private static final int KEEP_CAMERA_TIMEOUT = 3000; // 3 seconds + private CameraProxy mCameraDevice; + private long mKeepBeforeTime; // Keep the Camera before this time. + private final Handler mHandler; + private boolean mCameraOpened; // true if camera is opened + private final int mNumberOfCameras; + private int mCameraId = -1; // current camera id + private int mBackCameraId = -1; + private int mFrontCameraId = -1; + private final CameraInfo[] mInfo; + private static CameraProxy mMockCamera[]; + private static CameraInfo mMockCameraInfo[]; + + /* Debug double-open issue */ + private static final boolean DEBUG_OPEN_RELEASE = true; + private static class OpenReleaseState { + long time; + int id; + String device; + String[] stack; + } + private static ArrayList<OpenReleaseState> sOpenReleaseStates = + new ArrayList<OpenReleaseState>(); + private static SimpleDateFormat sDateFormat = new SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss.SSS"); + + private static synchronized void collectState(int id, CameraProxy device) { + OpenReleaseState s = new OpenReleaseState(); + s.time = System.currentTimeMillis(); + s.id = id; + if (device == null) { + s.device = "(null)"; + } else { + s.device = device.toString(); + } + + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + String[] lines = new String[stack.length]; + for (int i = 0; i < stack.length; i++) { + lines[i] = stack[i].toString(); + } + s.stack = lines; + + if (sOpenReleaseStates.size() > 10) { + sOpenReleaseStates.remove(0); + } + sOpenReleaseStates.add(s); + } + + private static synchronized void dumpStates() { + for (int i = sOpenReleaseStates.size() - 1; i >= 0; i--) { + OpenReleaseState s = sOpenReleaseStates.get(i); + String date = sDateFormat.format(new Date(s.time)); + Log.d(TAG, "State " + i + " at " + date); + Log.d(TAG, "mCameraId = " + s.id + ", mCameraDevice = " + s.device); + Log.d(TAG, "Stack:"); + for (int j = 0; j < s.stack.length; j++) { + Log.d(TAG, " " + s.stack[j]); + } + } + } + + // We store the camera parameters when we actually open the device, + // so we can restore them in the subsequent open() requests by the user. + // This prevents the parameters set by PhotoModule used by VideoModule + // inadvertently. + private Parameters mParameters; + + // Use a singleton. + private static CameraHolder sHolder; + public static synchronized CameraHolder instance() { + if (sHolder == null) { + sHolder = new CameraHolder(); + } + return sHolder; + } + + private static final int RELEASE_CAMERA = 1; + private class MyHandler extends Handler { + MyHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch(msg.what) { + case RELEASE_CAMERA: + synchronized (CameraHolder.this) { + // In 'CameraHolder.open', the 'RELEASE_CAMERA' message + // will be removed if it is found in the queue. However, + // there is a chance that this message has been handled + // before being removed. So, we need to add a check + // here: + if (!mCameraOpened) release(); + } + break; + } + } + } + + public static void injectMockCamera(CameraInfo[] info, CameraProxy[] camera) { + mMockCameraInfo = info; + mMockCamera = camera; + sHolder = new CameraHolder(); + } + + private CameraHolder() { + HandlerThread ht = new HandlerThread("CameraHolder"); + ht.start(); + mHandler = new MyHandler(ht.getLooper()); + if (mMockCameraInfo != null) { + mNumberOfCameras = mMockCameraInfo.length; + mInfo = mMockCameraInfo; + } else { + mNumberOfCameras = android.hardware.Camera.getNumberOfCameras(); + mInfo = new CameraInfo[mNumberOfCameras]; + for (int i = 0; i < mNumberOfCameras; i++) { + mInfo[i] = new CameraInfo(); + android.hardware.Camera.getCameraInfo(i, mInfo[i]); + } + } + + // get the first (smallest) back and first front camera id + for (int i = 0; i < mNumberOfCameras; i++) { + if (mBackCameraId == -1 && mInfo[i].facing == CameraInfo.CAMERA_FACING_BACK) { + mBackCameraId = i; + } else if (mFrontCameraId == -1 && mInfo[i].facing == CameraInfo.CAMERA_FACING_FRONT) { + mFrontCameraId = i; + } + } + } + + public int getNumberOfCameras() { + return mNumberOfCameras; + } + + public CameraInfo[] getCameraInfo() { + return mInfo; + } + + public synchronized CameraProxy open(int cameraId) + throws CameraHardwareException { + if (DEBUG_OPEN_RELEASE) { + collectState(cameraId, mCameraDevice); + if (mCameraOpened) { + Log.e(TAG, "double open"); + dumpStates(); + } + } + Assert(!mCameraOpened); + if (mCameraDevice != null && mCameraId != cameraId) { + mCameraDevice.release(); + mCameraDevice = null; + mCameraId = -1; + } + if (mCameraDevice == null) { + try { + Log.v(TAG, "open camera " + cameraId); + if (mMockCameraInfo == null) { + mCameraDevice = CameraManagerFactory + .getAndroidCameraManager().cameraOpen(cameraId); + } else { + if (mMockCamera == null) + throw new RuntimeException(); + mCameraDevice = mMockCamera[cameraId]; + } + mCameraId = cameraId; + } catch (RuntimeException e) { + Log.e(TAG, "fail to connect Camera", e); + throw new CameraHardwareException(e); + } + mParameters = mCameraDevice.getParameters(); + } else { + try { + mCameraDevice.reconnect(); + } catch (IOException e) { + Log.e(TAG, "reconnect failed."); + throw new CameraHardwareException(e); + } + mCameraDevice.setParameters(mParameters); + } + mCameraOpened = true; + mHandler.removeMessages(RELEASE_CAMERA); + mKeepBeforeTime = 0; + return mCameraDevice; + } + + /** + * Tries to open the hardware camera. If the camera is being used or + * unavailable then return {@code null}. + */ + public synchronized CameraProxy tryOpen(int cameraId) { + try { + return !mCameraOpened ? open(cameraId) : null; + } catch (CameraHardwareException e) { + // In eng build, we throw the exception so that test tool + // can detect it and report it + if ("eng".equals(Build.TYPE)) { + throw new RuntimeException(e); + } + return null; + } + } + + public synchronized void release() { + if (DEBUG_OPEN_RELEASE) { + collectState(mCameraId, mCameraDevice); + } + + if (mCameraDevice == null) return; + + long now = System.currentTimeMillis(); + if (now < mKeepBeforeTime) { + if (mCameraOpened) { + mCameraOpened = false; + mCameraDevice.stopPreview(); + } + mHandler.sendEmptyMessageDelayed(RELEASE_CAMERA, + mKeepBeforeTime - now); + return; + } + mCameraOpened = false; + mCameraDevice.release(); + mCameraDevice = null; + // We must set this to null because it has a reference to Camera. + // Camera has references to the listeners. + mParameters = null; + mCameraId = -1; + } + + public void keep() { + keep(KEEP_CAMERA_TIMEOUT); + } + + public synchronized void keep(int time) { + // We allow mCameraOpened in either state for the convenience of the + // calling activity. The activity may not have a chance to call open() + // before the user switches to another activity. + mKeepBeforeTime = System.currentTimeMillis() + time; + } + + public int getBackCameraId() { + return mBackCameraId; + } + + public int getFrontCameraId() { + return mFrontCameraId; + } +} diff --git a/src/com/android/camera/CameraManager.java b/src/com/android/camera/CameraManager.java new file mode 100644 index 000000000..90a838ca6 --- /dev/null +++ b/src/com/android/camera/CameraManager.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2012 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 android.annotation.TargetApi; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.hardware.Camera.ErrorCallback; +import android.hardware.Camera.OnZoomChangeListener; +import android.hardware.Camera.Parameters; +import android.os.Handler; +import android.view.SurfaceHolder; + +import com.android.gallery3d.common.ApiHelper; + +import java.io.IOException; + +/** + * An interface which provides possible camera device operations. + * + * The client should call {@code CameraManager.cameraOpen} to get an instance + * of {@link CameraManager.CameraProxy} to control the camera. Classes + * implementing this interface should have its own one unique {@code Thread} + * other than the main thread for camera operations. Camera device callbacks + * are wrapped since the client should not deal with + * {@code android.hardware.Camera} directly. + * + * TODO: provide callback interfaces for: + * {@code android.hardware.Camera.ErrorCallback}, + * {@code android.hardware.Camera.OnZoomChangeListener}, and + * {@code android.hardware.Camera.Parameters}. + */ +public interface CameraManager { + + /** + * An interface which wraps + * {@link android.hardware.Camera.AutoFocusCallback}. + */ + public interface CameraAFCallback { + public void onAutoFocus(boolean focused, CameraProxy camera); + } + + /** + * An interface which wraps + * {@link android.hardware.Camera.AutoFocusMoveCallback}. + */ + public interface CameraAFMoveCallback { + public void onAutoFocusMoving(boolean moving, CameraProxy camera); + } + + /** + * An interface which wraps + * {@link android.hardware.Camera.ShutterCallback}. + */ + public interface CameraShutterCallback { + public void onShutter(CameraProxy camera); + } + + /** + * An interface which wraps + * {@link android.hardware.Camera.PictureCallback}. + */ + public interface CameraPictureCallback { + public void onPictureTaken(byte[] data, CameraProxy camera); + } + + /** + * An interface which wraps + * {@link android.hardware.Camera.PreviewCallback}. + */ + public interface CameraPreviewDataCallback { + public void onPreviewFrame(byte[] data, CameraProxy camera); + } + + /** + * An interface which wraps + * {@link android.hardware.Camera.FaceDetectionListener}. + */ + public interface CameraFaceDetectionCallback { + /** + * Callback for face detection. + * + * @param faces Recognized face in the preview. + * @param camera The camera which the preview image comes from. + */ + public void onFaceDetection(Camera.Face[] faces, CameraProxy camera); + } + + /** + * Opens the camera of the specified ID synchronously. + * + * @param cameraId The camera ID to open. + * @return An instance of {@link CameraProxy} on success. null on failure. + */ + public CameraProxy cameraOpen(int cameraId); + + /** + * An interface that takes camera operation requests and post messages to the + * camera handler thread. All camera operations made through this interface is + * asynchronous by default except those mentioned specifically. + */ + public interface CameraProxy { + + /** + * Returns the underlying {@link android.hardware.Camera} object used + * by this proxy. This method should only be used when handing the + * camera device over to {@link android.media.MediaRecorder} for + * recording. + */ + public android.hardware.Camera getCamera(); + + /** + * Releases the camera device synchronously. + * This function must be synchronous so the caller knows exactly when the camera + * is released and can continue on. + */ + public void release(); + + /** + * Reconnects to the camera device. + * + * @see android.hardware.Camera#reconnect() + */ + public void reconnect() throws IOException; + + /** + * Unlocks the camera device. + * + * @see android.hardware.Camera#unlock() + */ + public void unlock(); + + /** + * Locks the camera device. + * @see android.hardware.Camera#lock() + */ + public void lock(); + + /** + * Sets the {@link android.graphics.SurfaceTexture} for preview. + * + * @param surfaceTexture The {@link SurfaceTexture} for preview. + */ + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + public void setPreviewTexture(final SurfaceTexture surfaceTexture); + + /** + * Sets the {@link android.view.SurfaceHolder} for preview. + * + * @param surfaceHolder The {@link SurfaceHolder} for preview. + */ + public void setPreviewDisplay(final SurfaceHolder surfaceHolder); + + /** + * Starts the camera preview. + */ + public void startPreview(); + + /** + * Stops the camera preview synchronously. + * {@code stopPreview()} must be synchronous to ensure that the caller can + * continues to release resources related to camera preview. + */ + public void stopPreview(); + + /** + * Sets the callback for preview data. + * + * @param handler handler in which the callback was handled. + * @param cb The callback to be invoked when the preview data is available. + * @see android.hardware.Camera#setPreviewCallback(android.hardware.Camera.PreviewCallback) + */ + public void setPreviewDataCallback(Handler handler, CameraPreviewDataCallback cb); + + /** + * Sets the callback for preview data. + * + * @param handler The handler in which the callback will be invoked. + * @param cb The callback to be invoked when the preview data is available. + * @see android.hardware.Camera#setPreviewCallbackWithBuffer(android.hardware.Camera.PreviewCallback) + */ + public void setPreviewDataCallbackWithBuffer(Handler handler, CameraPreviewDataCallback cb); + + /** + * Adds buffer for the preview callback. + * + * @param callbackBuffer The buffer allocated for the preview data. + */ + public void addCallbackBuffer(byte[] callbackBuffer); + + /** + * Starts the auto-focus process. The result will be returned through the callback. + * + * @param handler The handler in which the callback will be invoked. + * @param cb The auto-focus callback. + */ + public void autoFocus(Handler handler, CameraAFCallback cb); + + /** + * Cancels the auto-focus process. + */ + public void cancelAutoFocus(); + + /** + * Sets the auto-focus callback + * + * @param handler The handler in which the callback will be invoked. + * @param cb The callback to be invoked when the preview data is available. + */ + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + public void setAutoFocusMoveCallback(Handler handler, CameraAFMoveCallback cb); + + /** + * Instrument the camera to take a picture. + * + * @param handler The handler in which the callback will be invoked. + * @param shutter The callback for shutter action, may be null. + * @param raw The callback for uncompressed data, may be null. + * @param postview The callback for postview image data, may be null. + * @param jpeg The callback for jpeg image data, may be null. + * @see android.hardware.Camera#takePicture( + * android.hardware.Camera.ShutterCallback, + * android.hardware.Camera.PictureCallback, + * android.hardware.Camera.PictureCallback) + */ + public void takePicture( + Handler handler, + CameraShutterCallback shutter, + CameraPictureCallback raw, + CameraPictureCallback postview, + CameraPictureCallback jpeg); + + /** + * Sets the display orientation for camera to adjust the preview orientation. + * + * @param degrees The rotation in degrees. Should be 0, 90, 180 or 270. + */ + public void setDisplayOrientation(int degrees); + + /** + * Sets the listener for zoom change. + * + * @param listener The listener. + */ + public void setZoomChangeListener(OnZoomChangeListener listener); + + /** + * Sets the face detection listener. + * + * @param handler The handler in which the callback will be invoked. + * @param callback The callback for face detection results. + */ + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + public void setFaceDetectionCallback(Handler handler, CameraFaceDetectionCallback callback); + + /** + * Starts the face detection. + */ + public void startFaceDetection(); + + /** + * Stops the face detection. + */ + public void stopFaceDetection(); + + /** + * Registers an error callback. + * + * @param cb The error callback. + * @see android.hardware.Camera#setErrorCallback(android.hardware.Camera.ErrorCallback) + */ + public void setErrorCallback(ErrorCallback cb); + + /** + * Sets the camera parameters. + * + * @param params The camera parameters to use. + */ + public void setParameters(Parameters params); + + /** + * Gets the current camera parameters synchronously. This method is + * synchronous since the caller has to wait for the camera to return + * the parameters. If the parameters are already cached, it returns + * immediately. + */ + public Parameters getParameters(); + + /** + * Forces {@code CameraProxy} to update the cached version of the camera + * parameters regardless of the dirty bit. + */ + public void refreshParameters(); + + /** + * Enables/Disables the camera shutter sound. + * + * @param enable {@code true} to enable the shutter sound, + * {@code false} to disable it. + */ + public void enableShutterSound(boolean enable); + } +} diff --git a/src/com/android/camera/CameraManagerFactory.java b/src/com/android/camera/CameraManagerFactory.java new file mode 100644 index 000000000..914ebb265 --- /dev/null +++ b/src/com/android/camera/CameraManagerFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2013 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; + +/** + * A factory class for {@link CameraManager}. + */ +public class CameraManagerFactory { + + private static AndroidCameraManagerImpl sAndroidCameraManager; + + /** + * Returns the android camera implementation of {@link CameraManager}. + * + * @return The {@link CameraManager} to control the camera device. + */ + public static synchronized CameraManager getAndroidCameraManager() { + if (sAndroidCameraManager == null) { + sAndroidCameraManager = new AndroidCameraManagerImpl(); + } + return sAndroidCameraManager; + } +} diff --git a/src/com/android/camera/CameraModule.java b/src/com/android/camera/CameraModule.java new file mode 100644 index 000000000..bcfe98d65 --- /dev/null +++ b/src/com/android/camera/CameraModule.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2012 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 android.content.Intent; +import android.content.res.Configuration; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; + +public interface CameraModule { + + public void init(CameraActivity activity, View frame); + + public void onSwitchMode(boolean toCamera); + + public void onPauseBeforeSuper(); + + public void onPauseAfterSuper(); + + public void onResumeBeforeSuper(); + + public void onResumeAfterSuper(); + + public void onConfigurationChanged(Configuration config); + + public void onStop(); + + public void installIntentFilter(); + + public void onActivityResult(int requestCode, int resultCode, Intent data); + + public boolean onBackPressed(); + + public boolean onKeyDown(int keyCode, KeyEvent event); + + public boolean onKeyUp(int keyCode, KeyEvent event); + + public void onSingleTapUp(View view, int x, int y); + + public void onPreviewTextureCopied(); + + public void onCaptureTextureCopied(); + + public void onUserInteraction(); + + public boolean updateStorageHintOnResume(); + + public void updateCameraAppView(); + + public void onOrientationChanged(int orientation); + + public void onShowSwitcherPopup(); + + public void onMediaSaveServiceConnected(MediaSaveService s); +} diff --git a/src/com/android/camera/CameraPreference.java b/src/com/android/camera/CameraPreference.java new file mode 100644 index 000000000..5ddd86dbc --- /dev/null +++ b/src/com/android/camera/CameraPreference.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2009 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 android.content.Context; +import android.content.SharedPreferences; +import android.content.res.TypedArray; +import android.util.AttributeSet; + +import com.android.gallery3d.R; + +/** + * The base class of all Preferences used in Camera. The preferences can be + * loaded from XML resource by <code>PreferenceInflater</code>. + */ +public abstract class CameraPreference { + + private final String mTitle; + private SharedPreferences mSharedPreferences; + private final Context mContext; + + static public interface OnPreferenceChangedListener { + public void onSharedPreferenceChanged(); + public void onRestorePreferencesClicked(); + public void onOverriddenPreferencesClicked(); + public void onCameraPickerClicked(int cameraId); + } + + public CameraPreference(Context context, AttributeSet attrs) { + mContext = context; + TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.CameraPreference, 0, 0); + mTitle = a.getString(R.styleable.CameraPreference_title); + a.recycle(); + } + + public String getTitle() { + return mTitle; + } + + public SharedPreferences getSharedPreferences() { + if (mSharedPreferences == null) { + mSharedPreferences = ComboPreferences.get(mContext); + } + return mSharedPreferences; + } + + public abstract void reloadValue(); +} diff --git a/src/com/android/camera/CameraScreenNail.java b/src/com/android/camera/CameraScreenNail.java new file mode 100644 index 000000000..993a7d336 --- /dev/null +++ b/src/com/android/camera/CameraScreenNail.java @@ -0,0 +1,524 @@ +/* + * Copyright (C) 2012 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 android.annotation.TargetApi; +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.opengl.Matrix; +import android.util.Log; + +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.RawTexture; +import com.android.gallery3d.ui.SurfaceTextureScreenNail; + +/* + * This is a ScreenNail which can display camera's preview. + */ +@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) +public class CameraScreenNail extends SurfaceTextureScreenNail { + private static final String TAG = "CAM_ScreenNail"; + private static final int ANIM_NONE = 0; + // Capture animation is about to start. + private static final int ANIM_CAPTURE_START = 1; + // Capture animation is running. + private static final int ANIM_CAPTURE_RUNNING = 2; + // Switch camera animation needs to copy texture. + private static final int ANIM_SWITCH_COPY_TEXTURE = 3; + // Switch camera animation shows the initial feedback by darkening the + // preview. + private static final int ANIM_SWITCH_DARK_PREVIEW = 4; + // Switch camera animation is waiting for the first frame. + private static final int ANIM_SWITCH_WAITING_FIRST_FRAME = 5; + // Switch camera animation is about to start. + private static final int ANIM_SWITCH_START = 6; + // Switch camera animation is running. + private static final int ANIM_SWITCH_RUNNING = 7; + + private boolean mVisible; + // True if first onFrameAvailable has been called. If screen nail is drawn + // too early, it will be all white. + private boolean mFirstFrameArrived; + private Listener mListener; + private final float[] mTextureTransformMatrix = new float[16]; + + // Animation. + private CaptureAnimManager mCaptureAnimManager; + private SwitchAnimManager mSwitchAnimManager = new SwitchAnimManager(); + private int mAnimState = ANIM_NONE; + private RawTexture mAnimTexture; + // Some methods are called by GL thread and some are called by main thread. + // This protects mAnimState, mVisible, and surface texture. This also makes + // sure some code are atomic. For example, requestRender and setting + // mAnimState. + private Object mLock = new Object(); + + private OnFrameDrawnListener mOneTimeFrameDrawnListener; + private int mRenderWidth; + private int mRenderHeight; + // This represents the scaled, uncropped size of the texture + // Needed for FaceView + private int mUncroppedRenderWidth; + private int mUncroppedRenderHeight; + private float mScaleX = 1f, mScaleY = 1f; + private boolean mFullScreen; + private boolean mEnableAspectRatioClamping = false; + private boolean mAcquireTexture = false; + private final DrawClient mDefaultDraw = new DrawClient() { + @Override + public void onDraw(GLCanvas canvas, int x, int y, int width, int height) { + CameraScreenNail.super.draw(canvas, x, y, width, height); + } + + @Override + public boolean requiresSurfaceTexture() { + return true; + } + + @Override + public RawTexture copyToTexture(GLCanvas c, RawTexture texture, int w, int h) { + // We shouldn't be here since requireSurfaceTexture() returns true. + return null; + } + }; + private DrawClient mDraw = mDefaultDraw; + private float mAlpha = 1f; + private Runnable mOnFrameDrawnListener; + + public interface Listener { + void requestRender(); + // Preview has been copied to a texture. + void onPreviewTextureCopied(); + + void onCaptureTextureCopied(); + } + + public interface OnFrameDrawnListener { + void onFrameDrawn(CameraScreenNail c); + } + + public interface DrawClient { + void onDraw(GLCanvas canvas, int x, int y, int width, int height); + + boolean requiresSurfaceTexture(); + // The client should implement this if requiresSurfaceTexture() is false; + RawTexture copyToTexture(GLCanvas c, RawTexture texture, int width, int height); + } + + public CameraScreenNail(Listener listener, Context ctx) { + mListener = listener; + mCaptureAnimManager = new CaptureAnimManager(ctx); + } + + public void setFullScreen(boolean full) { + synchronized (mLock) { + mFullScreen = full; + } + } + + /** + * returns the uncropped, but scaled, width of the rendered texture + */ + public int getUncroppedRenderWidth() { + return mUncroppedRenderWidth; + } + + /** + * returns the uncropped, but scaled, width of the rendered texture + */ + public int getUncroppedRenderHeight() { + return mUncroppedRenderHeight; + } + + @Override + public int getWidth() { + return mEnableAspectRatioClamping ? mRenderWidth : getTextureWidth(); + } + + @Override + public int getHeight() { + return mEnableAspectRatioClamping ? mRenderHeight : getTextureHeight(); + } + + private int getTextureWidth() { + return super.getWidth(); + } + + private int getTextureHeight() { + return super.getHeight(); + } + + @Override + public void setSize(int w, int h) { + super.setSize(w, h); + mEnableAspectRatioClamping = false; + if (mRenderWidth == 0) { + mRenderWidth = w; + mRenderHeight = h; + } + updateRenderSize(); + } + + /** + * Tells the ScreenNail to override the default aspect ratio scaling + * and instead perform custom scaling to basically do a centerCrop instead + * of the default centerInside + * + * Note that calls to setSize will disable this + */ + public void enableAspectRatioClamping() { + mEnableAspectRatioClamping = true; + updateRenderSize(); + } + + private void setPreviewLayoutSize(int w, int h) { + Log.i(TAG, "preview layout size: "+w+"/"+h); + mRenderWidth = w; + mRenderHeight = h; + updateRenderSize(); + } + + private void updateRenderSize() { + if (!mEnableAspectRatioClamping) { + mScaleX = mScaleY = 1f; + mUncroppedRenderWidth = getTextureWidth(); + mUncroppedRenderHeight = getTextureHeight(); + Log.i(TAG, "aspect ratio clamping disabled"); + return; + } + + float aspectRatio; + if (getTextureWidth() > getTextureHeight()) { + aspectRatio = (float) getTextureWidth() / (float) getTextureHeight(); + } else { + aspectRatio = (float) getTextureHeight() / (float) getTextureWidth(); + } + float scaledTextureWidth, scaledTextureHeight; + if (mRenderWidth > mRenderHeight) { + scaledTextureWidth = Math.max(mRenderWidth, + (int) (mRenderHeight * aspectRatio)); + scaledTextureHeight = Math.max(mRenderHeight, + (int)(mRenderWidth / aspectRatio)); + } else { + scaledTextureWidth = Math.max(mRenderWidth, + (int) (mRenderHeight / aspectRatio)); + scaledTextureHeight = Math.max(mRenderHeight, + (int) (mRenderWidth * aspectRatio)); + } + mScaleX = mRenderWidth / scaledTextureWidth; + mScaleY = mRenderHeight / scaledTextureHeight; + mUncroppedRenderWidth = Math.round(scaledTextureWidth); + mUncroppedRenderHeight = Math.round(scaledTextureHeight); + Log.i(TAG, "aspect ratio clamping enabled, surfaceTexture scale: " + mScaleX + ", " + mScaleY); + } + + public void acquireSurfaceTexture() { + synchronized (mLock) { + mFirstFrameArrived = false; + mAnimTexture = new RawTexture(getTextureWidth(), getTextureHeight(), true); + mAcquireTexture = true; + } + mListener.requestRender(); + } + + @Override + public void releaseSurfaceTexture() { + synchronized (mLock) { + if (mAcquireTexture) { + mAcquireTexture = false; + mLock.notifyAll(); + } else { + if (super.getSurfaceTexture() != null) { + super.releaseSurfaceTexture(); + } + mAnimState = ANIM_NONE; // stop the animation + } + } + } + + public void copyTexture() { + synchronized (mLock) { + mListener.requestRender(); + mAnimState = ANIM_SWITCH_COPY_TEXTURE; + } + } + + public void animateSwitchCamera() { + Log.v(TAG, "animateSwitchCamera"); + synchronized (mLock) { + if (mAnimState == ANIM_SWITCH_DARK_PREVIEW) { + // Do not request render here because camera has been just + // started. We do not want to draw black frames. + mAnimState = ANIM_SWITCH_WAITING_FIRST_FRAME; + } + } + } + + public void animateCapture(int displayRotation) { + synchronized (mLock) { + mCaptureAnimManager.setOrientation(displayRotation); + mCaptureAnimManager.animateFlashAndSlide(); + mListener.requestRender(); + mAnimState = ANIM_CAPTURE_START; + } + } + + public RawTexture getAnimationTexture() { + return mAnimTexture; + } + + public void animateFlash(int displayRotation) { + synchronized (mLock) { + mCaptureAnimManager.setOrientation(displayRotation); + mCaptureAnimManager.animateFlash(); + mListener.requestRender(); + mAnimState = ANIM_CAPTURE_START; + } + } + + public void animateSlide() { + synchronized (mLock) { + mCaptureAnimManager.animateSlide(); + mListener.requestRender(); + } + } + + private void callbackIfNeeded() { + if (mOneTimeFrameDrawnListener != null) { + mOneTimeFrameDrawnListener.onFrameDrawn(this); + mOneTimeFrameDrawnListener = null; + } + } + + @Override + protected void updateTransformMatrix(float[] matrix) { + super.updateTransformMatrix(matrix); + Matrix.translateM(matrix, 0, .5f, .5f, 0); + Matrix.scaleM(matrix, 0, mScaleX, mScaleY, 1f); + Matrix.translateM(matrix, 0, -.5f, -.5f, 0); + } + + public void directDraw(GLCanvas canvas, int x, int y, int width, int height) { + DrawClient draw; + synchronized (mLock) { + draw = mDraw; + } + draw.onDraw(canvas, x, y, width, height); + } + + public void setDraw(DrawClient draw) { + synchronized (mLock) { + if (draw == null) { + mDraw = mDefaultDraw; + } else { + mDraw = draw; + } + } + mListener.requestRender(); + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int width, int height) { + synchronized (mLock) { + allocateTextureIfRequested(canvas); + if (!mVisible) mVisible = true; + SurfaceTexture surfaceTexture = getSurfaceTexture(); + if (mDraw.requiresSurfaceTexture() && (surfaceTexture == null || !mFirstFrameArrived)) { + return; + } + if (mOnFrameDrawnListener != null) { + mOnFrameDrawnListener.run(); + mOnFrameDrawnListener = null; + } + float oldAlpha = canvas.getAlpha(); + canvas.setAlpha(mAlpha); + + switch (mAnimState) { + case ANIM_NONE: + directDraw(canvas, x, y, width, height); + break; + case ANIM_SWITCH_COPY_TEXTURE: + copyPreviewTexture(canvas); + mSwitchAnimManager.setReviewDrawingSize(width, height); + mListener.onPreviewTextureCopied(); + mAnimState = ANIM_SWITCH_DARK_PREVIEW; + // The texture is ready. Fall through to draw darkened + // preview. + case ANIM_SWITCH_DARK_PREVIEW: + case ANIM_SWITCH_WAITING_FIRST_FRAME: + // Consume the frame. If the buffers are full, + // onFrameAvailable will not be called. Animation state + // relies on onFrameAvailable. + surfaceTexture.updateTexImage(); + mSwitchAnimManager.drawDarkPreview(canvas, x, y, width, + height, mAnimTexture); + break; + case ANIM_SWITCH_START: + mSwitchAnimManager.startAnimation(); + mAnimState = ANIM_SWITCH_RUNNING; + break; + case ANIM_CAPTURE_START: + copyPreviewTexture(canvas); + mListener.onCaptureTextureCopied(); + mCaptureAnimManager.startAnimation(); + mAnimState = ANIM_CAPTURE_RUNNING; + break; + } + + if (mAnimState == ANIM_CAPTURE_RUNNING || mAnimState == ANIM_SWITCH_RUNNING) { + boolean drawn; + if (mAnimState == ANIM_CAPTURE_RUNNING) { + if (!mFullScreen) { + // Skip the animation if no longer in full screen mode + drawn = false; + } else { + drawn = mCaptureAnimManager.drawAnimation(canvas, this, mAnimTexture, + x, y, width, height); + } + } else { + drawn = mSwitchAnimManager.drawAnimation(canvas, x, y, + width, height, this, mAnimTexture); + } + if (drawn) { + mListener.requestRender(); + } else { + // Continue to the normal draw procedure if the animation is + // not drawn. + mAnimState = ANIM_NONE; + directDraw(canvas, x, y, width, height); + } + } + canvas.setAlpha(oldAlpha); + callbackIfNeeded(); + } // mLock + } + + private void copyPreviewTexture(GLCanvas canvas) { + if (!mDraw.requiresSurfaceTexture()) { + mAnimTexture = mDraw.copyToTexture( + canvas, mAnimTexture, getTextureWidth(), getTextureHeight()); + } else { + int width = mAnimTexture.getWidth(); + int height = mAnimTexture.getHeight(); + canvas.beginRenderTarget(mAnimTexture); + // Flip preview texture vertically. OpenGL uses bottom left point + // as the origin (0, 0). + canvas.translate(0, height); + canvas.scale(1, -1, 1); + getSurfaceTexture().getTransformMatrix(mTextureTransformMatrix); + updateTransformMatrix(mTextureTransformMatrix); + canvas.drawTexture(mExtTexture, mTextureTransformMatrix, 0, 0, width, height); + canvas.endRenderTarget(); + } + } + + @Override + public void noDraw() { + synchronized (mLock) { + mVisible = false; + } + } + + @Override + public void recycle() { + synchronized (mLock) { + mVisible = false; + } + } + + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + synchronized (mLock) { + if (getSurfaceTexture() != surfaceTexture) { + return; + } + mFirstFrameArrived = true; + if (mVisible) { + if (mAnimState == ANIM_SWITCH_WAITING_FIRST_FRAME) { + mAnimState = ANIM_SWITCH_START; + } + // We need to ask for re-render if the SurfaceTexture receives a new + // frame. + mListener.requestRender(); + } + } + } + + // We need to keep track of the size of preview frame on the screen because + // it's needed when we do switch-camera animation. See comments in + // SwitchAnimManager.java. This is based on the natural orientation, not the + // view system orientation. + public void setPreviewFrameLayoutSize(int width, int height) { + synchronized (mLock) { + mSwitchAnimManager.setPreviewFrameLayoutSize(width, height); + setPreviewLayoutSize(width, height); + } + } + + public void setOneTimeOnFrameDrawnListener(OnFrameDrawnListener l) { + synchronized (mLock) { + mFirstFrameArrived = false; + mOneTimeFrameDrawnListener = l; + } + } + + @Override + public SurfaceTexture getSurfaceTexture() { + synchronized (mLock) { + SurfaceTexture surfaceTexture = super.getSurfaceTexture(); + if (surfaceTexture == null && mAcquireTexture) { + try { + mLock.wait(); + surfaceTexture = super.getSurfaceTexture(); + } catch (InterruptedException e) { + Log.w(TAG, "unexpected interruption"); + } + } + return surfaceTexture; + } + } + + private void allocateTextureIfRequested(GLCanvas canvas) { + synchronized (mLock) { + if (mAcquireTexture) { + super.acquireSurfaceTexture(canvas); + mAcquireTexture = false; + mLock.notifyAll(); + } + } + } + + public void setOnFrameDrawnOneShot(Runnable run) { + synchronized (mLock) { + mOnFrameDrawnListener = run; + } + } + + public float getAlpha() { + synchronized (mLock) { + return mAlpha; + } + } + + public void setAlpha(float alpha) { + synchronized (mLock) { + mAlpha = alpha; + mListener.requestRender(); + } + } +} diff --git a/src/com/android/camera/CameraSettings.java b/src/com/android/camera/CameraSettings.java new file mode 100644 index 000000000..3558014cc --- /dev/null +++ b/src/com/android/camera/CameraSettings.java @@ -0,0 +1,570 @@ +/* + * Copyright (C) 2009 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 android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.Size; +import android.media.CamcorderProfile; +import android.util.FloatMath; +import android.util.Log; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Provides utilities and keys for Camera settings. + */ +public class CameraSettings { + private static final int NOT_FOUND = -1; + + public static final String KEY_VERSION = "pref_version_key"; + public static final String KEY_LOCAL_VERSION = "pref_local_version_key"; + public static final String KEY_RECORD_LOCATION = "pref_camera_recordlocation_key"; + public static final String KEY_VIDEO_QUALITY = "pref_video_quality_key"; + public static final String KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL = "pref_video_time_lapse_frame_interval_key"; + public static final String KEY_PICTURE_SIZE = "pref_camera_picturesize_key"; + public static final String KEY_JPEG_QUALITY = "pref_camera_jpegquality_key"; + public static final String KEY_FOCUS_MODE = "pref_camera_focusmode_key"; + public static final String KEY_FLASH_MODE = "pref_camera_flashmode_key"; + public static final String KEY_VIDEOCAMERA_FLASH_MODE = "pref_camera_video_flashmode_key"; + public static final String KEY_WHITE_BALANCE = "pref_camera_whitebalance_key"; + public static final String KEY_SCENE_MODE = "pref_camera_scenemode_key"; + public static final String KEY_EXPOSURE = "pref_camera_exposure_key"; + public static final String KEY_TIMER = "pref_camera_timer_key"; + public static final String KEY_TIMER_SOUND_EFFECTS = "pref_camera_timer_sound_key"; + public static final String KEY_VIDEO_EFFECT = "pref_video_effect_key"; + public static final String KEY_CAMERA_ID = "pref_camera_id_key"; + public static final String KEY_CAMERA_HDR = "pref_camera_hdr_key"; + public static final String KEY_CAMERA_FIRST_USE_HINT_SHOWN = "pref_camera_first_use_hint_shown_key"; + public static final String KEY_VIDEO_FIRST_USE_HINT_SHOWN = "pref_video_first_use_hint_shown_key"; + public static final String KEY_PHOTOSPHERE_PICTURESIZE = "pref_photosphere_picturesize_key"; + + public static final String EXPOSURE_DEFAULT_VALUE = "0"; + + public static final int CURRENT_VERSION = 5; + public static final int CURRENT_LOCAL_VERSION = 2; + + private static final String TAG = "CameraSettings"; + + private final Context mContext; + private final Parameters mParameters; + private final CameraInfo[] mCameraInfo; + private final int mCameraId; + + public CameraSettings(Activity activity, Parameters parameters, + int cameraId, CameraInfo[] cameraInfo) { + mContext = activity; + mParameters = parameters; + mCameraId = cameraId; + mCameraInfo = cameraInfo; + } + + public PreferenceGroup getPreferenceGroup(int preferenceRes) { + PreferenceInflater inflater = new PreferenceInflater(mContext); + PreferenceGroup group = + (PreferenceGroup) inflater.inflate(preferenceRes); + if (mParameters != null) initPreference(group); + return group; + } + + public static String getSupportedHighestVideoQuality(int cameraId, + String defaultQuality) { + // When launching the camera app first time, we will set the video quality + // to the first one (i.e. highest quality) in the supported list + List<String> supported = getSupportedVideoQuality(cameraId); + if (supported == null) { + Log.e(TAG, "No supported video quality is found"); + return defaultQuality; + } + return supported.get(0); + } + + public static void initialCameraPictureSize( + Context context, Parameters parameters) { + // When launching the camera app first time, we will set the picture + // size to the first one in the list defined in "arrays.xml" and is also + // supported by the driver. + List<Size> supported = parameters.getSupportedPictureSizes(); + if (supported == null) return; + for (String candidate : context.getResources().getStringArray( + R.array.pref_camera_picturesize_entryvalues)) { + if (setCameraPictureSize(candidate, supported, parameters)) { + SharedPreferences.Editor editor = ComboPreferences + .get(context).edit(); + editor.putString(KEY_PICTURE_SIZE, candidate); + editor.apply(); + return; + } + } + Log.e(TAG, "No supported picture size found"); + } + + public static void removePreferenceFromScreen( + PreferenceGroup group, String key) { + removePreference(group, key); + } + + public static boolean setCameraPictureSize( + String candidate, List<Size> supported, Parameters parameters) { + int index = candidate.indexOf('x'); + if (index == NOT_FOUND) return false; + int width = Integer.parseInt(candidate.substring(0, index)); + int height = Integer.parseInt(candidate.substring(index + 1)); + for (Size size : supported) { + if (size.width == width && size.height == height) { + parameters.setPictureSize(width, height); + return true; + } + } + return false; + } + + public static int getMaxVideoDuration(Context context) { + int duration = 0; // in milliseconds, 0 means unlimited. + try { + duration = context.getResources().getInteger(R.integer.max_video_recording_length); + } catch (Resources.NotFoundException ex) { + } + return duration; + } + + private void initPreference(PreferenceGroup group) { + ListPreference videoQuality = group.findPreference(KEY_VIDEO_QUALITY); + ListPreference timeLapseInterval = group.findPreference(KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL); + ListPreference pictureSize = group.findPreference(KEY_PICTURE_SIZE); + ListPreference whiteBalance = group.findPreference(KEY_WHITE_BALANCE); + ListPreference sceneMode = group.findPreference(KEY_SCENE_MODE); + ListPreference flashMode = group.findPreference(KEY_FLASH_MODE); + ListPreference focusMode = group.findPreference(KEY_FOCUS_MODE); + IconListPreference exposure = + (IconListPreference) group.findPreference(KEY_EXPOSURE); + CountDownTimerPreference timer = + (CountDownTimerPreference) group.findPreference(KEY_TIMER); + ListPreference countDownSoundEffects = group.findPreference(KEY_TIMER_SOUND_EFFECTS); + IconListPreference cameraIdPref = + (IconListPreference) group.findPreference(KEY_CAMERA_ID); + ListPreference videoFlashMode = + group.findPreference(KEY_VIDEOCAMERA_FLASH_MODE); + ListPreference videoEffect = group.findPreference(KEY_VIDEO_EFFECT); + ListPreference cameraHdr = group.findPreference(KEY_CAMERA_HDR); + + // Since the screen could be loaded from different resources, we need + // to check if the preference is available here + if (videoQuality != null) { + filterUnsupportedOptions(group, videoQuality, getSupportedVideoQuality(mCameraId)); + } + + if (pictureSize != null) { + filterUnsupportedOptions(group, pictureSize, sizeListToStringList( + mParameters.getSupportedPictureSizes())); + filterSimilarPictureSize(group, pictureSize); + } + if (whiteBalance != null) { + filterUnsupportedOptions(group, + whiteBalance, mParameters.getSupportedWhiteBalance()); + } + if (sceneMode != null) { + filterUnsupportedOptions(group, + sceneMode, mParameters.getSupportedSceneModes()); + } + if (flashMode != null) { + filterUnsupportedOptions(group, + flashMode, mParameters.getSupportedFlashModes()); + } + if (focusMode != null) { + if (!Util.isFocusAreaSupported(mParameters)) { + filterUnsupportedOptions(group, + focusMode, mParameters.getSupportedFocusModes()); + } else { + // Remove the focus mode if we can use tap-to-focus. + removePreference(group, focusMode.getKey()); + } + } + if (videoFlashMode != null) { + filterUnsupportedOptions(group, + videoFlashMode, mParameters.getSupportedFlashModes()); + } + if (exposure != null) buildExposureCompensation(group, exposure); + if (cameraIdPref != null) buildCameraId(group, cameraIdPref); + + if (timeLapseInterval != null) { + if (ApiHelper.HAS_TIME_LAPSE_RECORDING) { + resetIfInvalid(timeLapseInterval); + } else { + removePreference(group, timeLapseInterval.getKey()); + } + } + if (videoEffect != null) { + if (ApiHelper.HAS_EFFECTS_RECORDING) { + initVideoEffect(group, videoEffect); + resetIfInvalid(videoEffect); + } else { + filterUnsupportedOptions(group, videoEffect, null); + } + } + if (cameraHdr != null && (!ApiHelper.HAS_CAMERA_HDR + || !Util.isCameraHdrSupported(mParameters))) { + removePreference(group, cameraHdr.getKey()); + } + } + + private void buildExposureCompensation( + PreferenceGroup group, IconListPreference exposure) { + int max = mParameters.getMaxExposureCompensation(); + int min = mParameters.getMinExposureCompensation(); + if (max == 0 && min == 0) { + removePreference(group, exposure.getKey()); + return; + } + float step = mParameters.getExposureCompensationStep(); + + // show only integer values for exposure compensation + int maxValue = Math.min(3, (int) FloatMath.floor(max * step)); + int minValue = Math.max(-3, (int) FloatMath.ceil(min * step)); + String explabel = mContext.getResources().getString(R.string.pref_exposure_label); + CharSequence entries[] = new CharSequence[maxValue - minValue + 1]; + CharSequence entryValues[] = new CharSequence[maxValue - minValue + 1]; + CharSequence labels[] = new CharSequence[maxValue - minValue + 1]; + int[] icons = new int[maxValue - minValue + 1]; + TypedArray iconIds = mContext.getResources().obtainTypedArray( + R.array.pref_camera_exposure_icons); + for (int i = minValue; i <= maxValue; ++i) { + entryValues[i - minValue] = Integer.toString(Math.round(i / step)); + StringBuilder builder = new StringBuilder(); + if (i > 0) builder.append('+'); + entries[i - minValue] = builder.append(i).toString(); + labels[i - minValue] = explabel + " " + builder.toString(); + icons[i - minValue] = iconIds.getResourceId(3 + i, 0); + } + exposure.setUseSingleIcon(true); + exposure.setEntries(entries); + exposure.setLabels(labels); + exposure.setEntryValues(entryValues); + exposure.setLargeIconIds(icons); + } + + private void buildCameraId( + PreferenceGroup group, IconListPreference preference) { + int numOfCameras = mCameraInfo.length; + if (numOfCameras < 2) { + removePreference(group, preference.getKey()); + return; + } + + CharSequence[] entryValues = new CharSequence[numOfCameras]; + for (int i = 0; i < numOfCameras; ++i) { + entryValues[i] = "" + i; + } + preference.setEntryValues(entryValues); + } + + private static boolean removePreference(PreferenceGroup group, String key) { + for (int i = 0, n = group.size(); i < n; i++) { + CameraPreference child = group.get(i); + if (child instanceof PreferenceGroup) { + if (removePreference((PreferenceGroup) child, key)) { + return true; + } + } + if (child instanceof ListPreference && + ((ListPreference) child).getKey().equals(key)) { + group.removePreference(i); + return true; + } + } + return false; + } + + private void filterUnsupportedOptions(PreferenceGroup group, + ListPreference pref, List<String> supported) { + + // Remove the preference if the parameter is not supported or there is + // only one options for the settings. + if (supported == null || supported.size() <= 1) { + removePreference(group, pref.getKey()); + return; + } + + pref.filterUnsupported(supported); + if (pref.getEntries().length <= 1) { + removePreference(group, pref.getKey()); + return; + } + + resetIfInvalid(pref); + } + + private void filterSimilarPictureSize(PreferenceGroup group, + ListPreference pref) { + pref.filterDuplicated(); + if (pref.getEntries().length <= 1) { + removePreference(group, pref.getKey()); + return; + } + resetIfInvalid(pref); + } + + private void resetIfInvalid(ListPreference pref) { + // Set the value to the first entry if it is invalid. + String value = pref.getValue(); + if (pref.findIndexOfValue(value) == NOT_FOUND) { + pref.setValueIndex(0); + } + } + + private static List<String> sizeListToStringList(List<Size> sizes) { + ArrayList<String> list = new ArrayList<String>(); + for (Size size : sizes) { + list.add(String.format(Locale.ENGLISH, "%dx%d", size.width, size.height)); + } + return list; + } + + public static void upgradeLocalPreferences(SharedPreferences pref) { + int version; + try { + version = pref.getInt(KEY_LOCAL_VERSION, 0); + } catch (Exception ex) { + version = 0; + } + if (version == CURRENT_LOCAL_VERSION) return; + + SharedPreferences.Editor editor = pref.edit(); + if (version == 1) { + // We use numbers to represent the quality now. The quality definition is identical to + // that of CamcorderProfile.java. + editor.remove("pref_video_quality_key"); + } + editor.putInt(KEY_LOCAL_VERSION, CURRENT_LOCAL_VERSION); + editor.apply(); + } + + public static void upgradeGlobalPreferences(SharedPreferences pref) { + upgradeOldVersion(pref); + upgradeCameraId(pref); + } + + private static void upgradeOldVersion(SharedPreferences pref) { + int version; + try { + version = pref.getInt(KEY_VERSION, 0); + } catch (Exception ex) { + version = 0; + } + if (version == CURRENT_VERSION) return; + + SharedPreferences.Editor editor = pref.edit(); + if (version == 0) { + // We won't use the preference which change in version 1. + // So, just upgrade to version 1 directly + version = 1; + } + if (version == 1) { + // Change jpeg quality {65,75,85} to {normal,fine,superfine} + String quality = pref.getString(KEY_JPEG_QUALITY, "85"); + if (quality.equals("65")) { + quality = "normal"; + } else if (quality.equals("75")) { + quality = "fine"; + } else { + quality = "superfine"; + } + editor.putString(KEY_JPEG_QUALITY, quality); + version = 2; + } + if (version == 2) { + editor.putString(KEY_RECORD_LOCATION, + pref.getBoolean(KEY_RECORD_LOCATION, false) + ? RecordLocationPreference.VALUE_ON + : RecordLocationPreference.VALUE_NONE); + version = 3; + } + if (version == 3) { + // Just use video quality to replace it and + // ignore the current settings. + editor.remove("pref_camera_videoquality_key"); + editor.remove("pref_camera_video_duration_key"); + } + + editor.putInt(KEY_VERSION, CURRENT_VERSION); + editor.apply(); + } + + private static void upgradeCameraId(SharedPreferences pref) { + // The id stored in the preference may be out of range if we are running + // inside the emulator and a webcam is removed. + // Note: This method accesses the global preferences directly, not the + // combo preferences. + int cameraId = readPreferredCameraId(pref); + if (cameraId == 0) return; // fast path + + int n = CameraHolder.instance().getNumberOfCameras(); + if (cameraId < 0 || cameraId >= n) { + writePreferredCameraId(pref, 0); + } + } + + public static int readPreferredCameraId(SharedPreferences pref) { + return Integer.parseInt(pref.getString(KEY_CAMERA_ID, "0")); + } + + public static void writePreferredCameraId(SharedPreferences pref, + int cameraId) { + Editor editor = pref.edit(); + editor.putString(KEY_CAMERA_ID, Integer.toString(cameraId)); + editor.apply(); + } + + public static int readExposure(ComboPreferences preferences) { + String exposure = preferences.getString( + CameraSettings.KEY_EXPOSURE, + EXPOSURE_DEFAULT_VALUE); + try { + return Integer.parseInt(exposure); + } catch (Exception ex) { + Log.e(TAG, "Invalid exposure: " + exposure); + } + return 0; + } + + public static int readEffectType(SharedPreferences pref) { + String effectSelection = pref.getString(KEY_VIDEO_EFFECT, "none"); + if (effectSelection.equals("none")) { + return EffectsRecorder.EFFECT_NONE; + } else if (effectSelection.startsWith("goofy_face")) { + return EffectsRecorder.EFFECT_GOOFY_FACE; + } else if (effectSelection.startsWith("backdropper")) { + return EffectsRecorder.EFFECT_BACKDROPPER; + } + Log.e(TAG, "Invalid effect selection: " + effectSelection); + return EffectsRecorder.EFFECT_NONE; + } + + public static Object readEffectParameter(SharedPreferences pref) { + String effectSelection = pref.getString(KEY_VIDEO_EFFECT, "none"); + if (effectSelection.equals("none")) { + return null; + } + int separatorIndex = effectSelection.indexOf('/'); + String effectParameter = + effectSelection.substring(separatorIndex + 1); + if (effectSelection.startsWith("goofy_face")) { + if (effectParameter.equals("squeeze")) { + return EffectsRecorder.EFFECT_GF_SQUEEZE; + } else if (effectParameter.equals("big_eyes")) { + return EffectsRecorder.EFFECT_GF_BIG_EYES; + } else if (effectParameter.equals("big_mouth")) { + return EffectsRecorder.EFFECT_GF_BIG_MOUTH; + } else if (effectParameter.equals("small_mouth")) { + return EffectsRecorder.EFFECT_GF_SMALL_MOUTH; + } else if (effectParameter.equals("big_nose")) { + return EffectsRecorder.EFFECT_GF_BIG_NOSE; + } else if (effectParameter.equals("small_eyes")) { + return EffectsRecorder.EFFECT_GF_SMALL_EYES; + } + } else if (effectSelection.startsWith("backdropper")) { + // Parameter is a string that either encodes the URI to use, + // or specifies 'gallery'. + return effectParameter; + } + + Log.e(TAG, "Invalid effect selection: " + effectSelection); + return null; + } + + public static void restorePreferences(Context context, + ComboPreferences preferences, Parameters parameters) { + int currentCameraId = readPreferredCameraId(preferences); + + // Clear the preferences of both cameras. + int backCameraId = CameraHolder.instance().getBackCameraId(); + if (backCameraId != -1) { + preferences.setLocalId(context, backCameraId); + Editor editor = preferences.edit(); + editor.clear(); + editor.apply(); + } + int frontCameraId = CameraHolder.instance().getFrontCameraId(); + if (frontCameraId != -1) { + preferences.setLocalId(context, frontCameraId); + Editor editor = preferences.edit(); + editor.clear(); + editor.apply(); + } + + // Switch back to the preferences of the current camera. Otherwise, + // we may write the preference to wrong camera later. + preferences.setLocalId(context, currentCameraId); + + upgradeGlobalPreferences(preferences.getGlobal()); + upgradeLocalPreferences(preferences.getLocal()); + + // Write back the current camera id because parameters are related to + // the camera. Otherwise, we may switch to the front camera but the + // initial picture size is that of the back camera. + initialCameraPictureSize(context, parameters); + writePreferredCameraId(preferences, currentCameraId); + } + + private static ArrayList<String> getSupportedVideoQuality(int cameraId) { + ArrayList<String> supported = new ArrayList<String>(); + // Check for supported quality + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) { + supported.add(Integer.toString(CamcorderProfile.QUALITY_1080P)); + } + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) { + supported.add(Integer.toString(CamcorderProfile.QUALITY_720P)); + } + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) { + supported.add(Integer.toString(CamcorderProfile.QUALITY_480P)); + } + return supported; + } + + private void initVideoEffect(PreferenceGroup group, ListPreference videoEffect) { + CharSequence[] values = videoEffect.getEntryValues(); + + boolean goofyFaceSupported = + EffectsRecorder.isEffectSupported(EffectsRecorder.EFFECT_GOOFY_FACE); + boolean backdropperSupported = + EffectsRecorder.isEffectSupported(EffectsRecorder.EFFECT_BACKDROPPER) && + Util.isAutoExposureLockSupported(mParameters) && + Util.isAutoWhiteBalanceLockSupported(mParameters); + + ArrayList<String> supported = new ArrayList<String>(); + for (CharSequence value : values) { + String effectSelection = value.toString(); + if (!goofyFaceSupported && effectSelection.startsWith("goofy_face")) continue; + if (!backdropperSupported && effectSelection.startsWith("backdropper")) continue; + supported.add(effectSelection); + } + + filterUnsupportedOptions(group, videoEffect, supported); + } +} diff --git a/src/com/android/camera/CaptureAnimManager.java b/src/com/android/camera/CaptureAnimManager.java new file mode 100644 index 000000000..6e8092566 --- /dev/null +++ b/src/com/android/camera/CaptureAnimManager.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2012 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 android.content.Context; +import android.content.res.Resources; +import android.graphics.Color; +import android.os.SystemClock; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.android.gallery3d.R; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.NinePatchTexture; +import com.android.gallery3d.glrenderer.RawTexture; + +/** + * Class to handle the capture animation. + */ +public class CaptureAnimManager { + @SuppressWarnings("unused") + private static final String TAG = "CAM_Capture"; + // times mark endpoint of animation phase + private static final int TIME_FLASH = 200; + private static final int TIME_HOLD = 400; + private static final int TIME_SLIDE = 800; + private static final int TIME_HOLD2 = 3300; + private static final int TIME_SLIDE2 = 4100; + + private static final int ANIM_BOTH = 0; + private static final int ANIM_FLASH = 1; + private static final int ANIM_SLIDE = 2; + private static final int ANIM_HOLD2 = 3; + private static final int ANIM_SLIDE2 = 4; + + private final Interpolator mSlideInterpolator = new DecelerateInterpolator(); + + private volatile int mAnimOrientation; // Could be 0, 90, 180 or 270 degrees. + private long mAnimStartTime; // milliseconds. + private float mX; // The center of the whole view including preview and review. + private float mY; + private int mDrawWidth; + private int mDrawHeight; + private int mAnimType; + + private int mHoldX; + private int mHoldY; + private int mHoldW; + private int mHoldH; + + private int mOffset; + + private int mMarginRight; + private int mMarginTop; + private int mSize; + private Resources mResources; + private NinePatchTexture mBorder; + private int mShadowSize; + + public static int getAnimationDuration() { + return TIME_SLIDE2; + } + + /* preview: camera preview view. + * review: view of picture just taken. + */ + public CaptureAnimManager(Context ctx) { + mBorder = new NinePatchTexture(ctx, R.drawable.capture_thumbnail_shadow); + mResources = ctx.getResources(); + } + + public void setOrientation(int displayRotation) { + mAnimOrientation = (360 - displayRotation) % 360; + } + + public void animateSlide() { + if (mAnimType != ANIM_FLASH) { + return; + } + mAnimType = ANIM_SLIDE; + mAnimStartTime = SystemClock.uptimeMillis(); + } + + public void animateFlash() { + mAnimType = ANIM_FLASH; + } + + public void animateFlashAndSlide() { + mAnimType = ANIM_BOTH; + } + + public void startAnimation() { + mAnimStartTime = SystemClock.uptimeMillis(); + } + + private void setAnimationGeometry(int x, int y, int w, int h) { + mMarginRight = mResources.getDimensionPixelSize(R.dimen.capture_margin_right); + mMarginTop = mResources.getDimensionPixelSize(R.dimen.capture_margin_top); + mSize = mResources.getDimensionPixelSize(R.dimen.capture_size); + mShadowSize = mResources.getDimensionPixelSize(R.dimen.capture_border); + mOffset = mMarginRight + mSize; + // Set the views to the initial positions. + mDrawWidth = w; + mDrawHeight = h; + mX = x; + mY = y; + mHoldW = mSize; + mHoldH = mSize; + switch (mAnimOrientation) { + case 0: // Preview is on the left. + mHoldX = x + w - mMarginRight - mSize; + mHoldY = y + mMarginTop; + break; + case 90: // Preview is below. + mHoldX = x + mMarginTop; + mHoldY = y + mMarginRight; + break; + case 180: // Preview on the right. + mHoldX = x + mMarginRight; + mHoldY = y + h - mMarginTop - mSize; + break; + case 270: // Preview is above. + mHoldX = x + w - mMarginTop - mSize; + mHoldY = y + h - mMarginRight - mSize; + break; + } + } + + // Returns true if the animation has been drawn. + public boolean drawAnimation(GLCanvas canvas, CameraScreenNail preview, + RawTexture review, int lx, int ly, int lw, int lh) { + setAnimationGeometry(lx, ly, lw, lh); + long timeDiff = SystemClock.uptimeMillis() - mAnimStartTime; + // Check if the animation is over + if (mAnimType == ANIM_SLIDE && timeDiff > TIME_SLIDE2 - TIME_HOLD) return false; + if (mAnimType == ANIM_BOTH && timeDiff > TIME_SLIDE2) return false; + + // determine phase and time in phase + int animStep = mAnimType; + if (mAnimType == ANIM_SLIDE) { + timeDiff += TIME_HOLD; + } + if (mAnimType == ANIM_SLIDE || mAnimType == ANIM_BOTH) { + if (timeDiff < TIME_HOLD) { + animStep = ANIM_FLASH; + } else if (timeDiff < TIME_SLIDE) { + animStep = ANIM_SLIDE; + timeDiff -= TIME_HOLD; + } else if (timeDiff < TIME_HOLD2) { + animStep = ANIM_HOLD2; + timeDiff -= TIME_SLIDE; + } else { + // SLIDE2 + animStep = ANIM_SLIDE2; + timeDiff -= TIME_HOLD2; + } + } + + if (animStep == ANIM_FLASH) { + review.draw(canvas, (int) mX, (int) mY, mDrawWidth, mDrawHeight); + if (timeDiff < TIME_FLASH) { + float f = 0.3f - 0.3f * timeDiff / TIME_FLASH; + int color = Color.argb((int) (255 * f), 255, 255, 255); + canvas.fillRect(mX, mY, mDrawWidth, mDrawHeight, color); + } + } else if (animStep == ANIM_SLIDE) { + float fraction = mSlideInterpolator.getInterpolation((float) (timeDiff) / (TIME_SLIDE - TIME_HOLD)); + float x = mX; + float y = mY; + float w = 0; + float h = 0; + x = interpolate(mX, mHoldX, fraction); + y = interpolate(mY, mHoldY, fraction); + w = interpolate(mDrawWidth, mHoldW, fraction); + h = interpolate(mDrawHeight, mHoldH, fraction); + preview.directDraw(canvas, (int) mX, (int) mY, mDrawWidth, mDrawHeight); + review.draw(canvas, (int) x, (int) y, (int) w, (int) h); + } else if (animStep == ANIM_HOLD2) { + preview.directDraw(canvas, (int) mX, (int) mY, mDrawWidth, mDrawHeight); + review.draw(canvas, mHoldX, mHoldY, mHoldW, mHoldH); + mBorder.draw(canvas, (int) mHoldX - mShadowSize, (int) mHoldY - mShadowSize, + (int) mHoldW + 2 * mShadowSize, (int) mHoldH + 2 * mShadowSize); + } else if (animStep == ANIM_SLIDE2) { + float fraction = (float)(timeDiff) / (TIME_SLIDE2 - TIME_HOLD2); + float x = mHoldX; + float y = mHoldY; + float d = mOffset * fraction; + switch (mAnimOrientation) { + case 0: + x = mHoldX + d; + break; + case 180: + x = mHoldX - d; + break; + case 90: + y = mHoldY - d; + break; + case 270: + y = mHoldY + d; + break; + } + preview.directDraw(canvas, (int) mX, (int) mY, mDrawWidth, mDrawHeight); + mBorder.draw(canvas, (int) x - mShadowSize, (int) y - mShadowSize, + (int) mHoldW + 2 * mShadowSize, (int) mHoldH + 2 * mShadowSize); + review.draw(canvas, (int) x, (int) y, mHoldW, mHoldH); + } + return true; + } + + private static float interpolate(float start, float end, float fraction) { + return start + (end - start) * fraction; + } + +} diff --git a/src/com/android/camera/ComboPreferences.java b/src/com/android/camera/ComboPreferences.java new file mode 100644 index 000000000..e17e47aa8 --- /dev/null +++ b/src/com/android/camera/ComboPreferences.java @@ -0,0 +1,335 @@ +/* + * Copyright (C) 2010 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 android.app.backup.BackupManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.preference.PreferenceManager; + +import com.android.gallery3d.util.UsageStatistics; + +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +public class ComboPreferences implements + SharedPreferences, + OnSharedPreferenceChangeListener { + private SharedPreferences mPrefGlobal; // global preferences + private SharedPreferences mPrefLocal; // per-camera preferences + private String mPackageName; + private CopyOnWriteArrayList<OnSharedPreferenceChangeListener> mListeners; + // TODO: Remove this WeakHashMap in the camera code refactoring + private static WeakHashMap<Context, ComboPreferences> sMap = + new WeakHashMap<Context, ComboPreferences>(); + + public ComboPreferences(Context context) { + mPackageName = context.getPackageName(); + mPrefGlobal = context.getSharedPreferences( + getGlobalSharedPreferencesName(context), Context.MODE_PRIVATE); + mPrefGlobal.registerOnSharedPreferenceChangeListener(this); + + synchronized (sMap) { + sMap.put(context, this); + } + mListeners = new CopyOnWriteArrayList<OnSharedPreferenceChangeListener>(); + + // The global preferences was previously stored in the default + // shared preferences file. They should be stored in the camera-specific + // shared preferences file so we can backup them solely. + SharedPreferences oldprefs = + PreferenceManager.getDefaultSharedPreferences(context); + if (!mPrefGlobal.contains(CameraSettings.KEY_VERSION) + && oldprefs.contains(CameraSettings.KEY_VERSION)) { + moveGlobalPrefsFrom(oldprefs); + } + } + + public static ComboPreferences get(Context context) { + synchronized (sMap) { + return sMap.get(context); + } + } + + private static String getLocalSharedPreferencesName( + Context context, int cameraId) { + return context.getPackageName() + "_preferences_" + cameraId; + } + + private static String getGlobalSharedPreferencesName(Context context) { + return context.getPackageName() + "_preferences_camera"; + } + + private void movePrefFrom( + Map<String, ?> m, String key, SharedPreferences src) { + if (m.containsKey(key)) { + Object v = m.get(key); + if (v instanceof String) { + mPrefGlobal.edit().putString(key, (String) v).apply(); + } else if (v instanceof Integer) { + mPrefGlobal.edit().putInt(key, (Integer) v).apply(); + } else if (v instanceof Long) { + mPrefGlobal.edit().putLong(key, (Long) v).apply(); + } else if (v instanceof Float) { + mPrefGlobal.edit().putFloat(key, (Float) v).apply(); + } else if (v instanceof Boolean) { + mPrefGlobal.edit().putBoolean(key, (Boolean) v).apply(); + } + src.edit().remove(key).apply(); + } + } + + private void moveGlobalPrefsFrom(SharedPreferences src) { + Map<String, ?> prefMap = src.getAll(); + movePrefFrom(prefMap, CameraSettings.KEY_VERSION, src); + movePrefFrom(prefMap, CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL, src); + movePrefFrom(prefMap, CameraSettings.KEY_CAMERA_ID, src); + movePrefFrom(prefMap, CameraSettings.KEY_RECORD_LOCATION, src); + movePrefFrom(prefMap, CameraSettings.KEY_CAMERA_FIRST_USE_HINT_SHOWN, src); + movePrefFrom(prefMap, CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, src); + movePrefFrom(prefMap, CameraSettings.KEY_VIDEO_EFFECT, src); + } + + public static String[] getSharedPreferencesNames(Context context) { + int numOfCameras = CameraHolder.instance().getNumberOfCameras(); + String prefNames[] = new String[numOfCameras + 1]; + prefNames[0] = getGlobalSharedPreferencesName(context); + for (int i = 0; i < numOfCameras; i++) { + prefNames[i + 1] = getLocalSharedPreferencesName(context, i); + } + return prefNames; + } + + // Sets the camera id and reads its preferences. Each camera has its own + // preferences. + public void setLocalId(Context context, int cameraId) { + String prefName = getLocalSharedPreferencesName(context, cameraId); + if (mPrefLocal != null) { + mPrefLocal.unregisterOnSharedPreferenceChangeListener(this); + } + mPrefLocal = context.getSharedPreferences( + prefName, Context.MODE_PRIVATE); + mPrefLocal.registerOnSharedPreferenceChangeListener(this); + } + + public SharedPreferences getGlobal() { + return mPrefGlobal; + } + + public SharedPreferences getLocal() { + return mPrefLocal; + } + + @Override + public Map<String, ?> getAll() { + throw new UnsupportedOperationException(); // Can be implemented if needed. + } + + private static boolean isGlobal(String key) { + return key.equals(CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL) + || key.equals(CameraSettings.KEY_CAMERA_ID) + || key.equals(CameraSettings.KEY_RECORD_LOCATION) + || key.equals(CameraSettings.KEY_CAMERA_FIRST_USE_HINT_SHOWN) + || key.equals(CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN) + || key.equals(CameraSettings.KEY_VIDEO_EFFECT) + || key.equals(CameraSettings.KEY_TIMER) + || key.equals(CameraSettings.KEY_TIMER_SOUND_EFFECTS) + || key.equals(CameraSettings.KEY_PHOTOSPHERE_PICTURESIZE); + } + + @Override + public String getString(String key, String defValue) { + if (isGlobal(key) || !mPrefLocal.contains(key)) { + return mPrefGlobal.getString(key, defValue); + } else { + return mPrefLocal.getString(key, defValue); + } + } + + @Override + public int getInt(String key, int defValue) { + if (isGlobal(key) || !mPrefLocal.contains(key)) { + return mPrefGlobal.getInt(key, defValue); + } else { + return mPrefLocal.getInt(key, defValue); + } + } + + @Override + public long getLong(String key, long defValue) { + if (isGlobal(key) || !mPrefLocal.contains(key)) { + return mPrefGlobal.getLong(key, defValue); + } else { + return mPrefLocal.getLong(key, defValue); + } + } + + @Override + public float getFloat(String key, float defValue) { + if (isGlobal(key) || !mPrefLocal.contains(key)) { + return mPrefGlobal.getFloat(key, defValue); + } else { + return mPrefLocal.getFloat(key, defValue); + } + } + + @Override + public boolean getBoolean(String key, boolean defValue) { + if (isGlobal(key) || !mPrefLocal.contains(key)) { + return mPrefGlobal.getBoolean(key, defValue); + } else { + return mPrefLocal.getBoolean(key, defValue); + } + } + + // This method is not used. + @Override + public Set<String> getStringSet(String key, Set<String> defValues) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean contains(String key) { + return mPrefLocal.contains(key) || mPrefGlobal.contains(key); + } + + private class MyEditor implements Editor { + private Editor mEditorGlobal; + private Editor mEditorLocal; + + MyEditor() { + mEditorGlobal = mPrefGlobal.edit(); + mEditorLocal = mPrefLocal.edit(); + } + + @Override + public boolean commit() { + boolean result1 = mEditorGlobal.commit(); + boolean result2 = mEditorLocal.commit(); + return result1 && result2; + } + + @Override + public void apply() { + mEditorGlobal.apply(); + mEditorLocal.apply(); + } + + // Note: clear() and remove() affects both local and global preferences. + @Override + public Editor clear() { + mEditorGlobal.clear(); + mEditorLocal.clear(); + return this; + } + + @Override + public Editor remove(String key) { + mEditorGlobal.remove(key); + mEditorLocal.remove(key); + return this; + } + + @Override + public Editor putString(String key, String value) { + if (isGlobal(key)) { + mEditorGlobal.putString(key, value); + } else { + mEditorLocal.putString(key, value); + } + return this; + } + + @Override + public Editor putInt(String key, int value) { + if (isGlobal(key)) { + mEditorGlobal.putInt(key, value); + } else { + mEditorLocal.putInt(key, value); + } + return this; + } + + @Override + public Editor putLong(String key, long value) { + if (isGlobal(key)) { + mEditorGlobal.putLong(key, value); + } else { + mEditorLocal.putLong(key, value); + } + return this; + } + + @Override + public Editor putFloat(String key, float value) { + if (isGlobal(key)) { + mEditorGlobal.putFloat(key, value); + } else { + mEditorLocal.putFloat(key, value); + } + return this; + } + + @Override + public Editor putBoolean(String key, boolean value) { + if (isGlobal(key)) { + mEditorGlobal.putBoolean(key, value); + } else { + mEditorLocal.putBoolean(key, value); + } + return this; + } + + // This method is not used. + @Override + public Editor putStringSet(String key, Set<String> values) { + throw new UnsupportedOperationException(); + } + } + + // Note the remove() and clear() of the returned Editor may not work as + // expected because it doesn't touch the global preferences at all. + @Override + public Editor edit() { + return new MyEditor(); + } + + @Override + public void registerOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener) { + mListeners.add(listener); + } + + @Override + public void unregisterOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener) { + mListeners.remove(listener); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, + String key) { + for (OnSharedPreferenceChangeListener listener : mListeners) { + listener.onSharedPreferenceChanged(this, key); + } + BackupManager.dataChanged(mPackageName); + UsageStatistics.onEvent("CameraSettingsChange", null, key); + } +} diff --git a/src/com/android/camera/CountDownTimerPreference.java b/src/com/android/camera/CountDownTimerPreference.java new file mode 100644 index 000000000..9c66dda8c --- /dev/null +++ b/src/com/android/camera/CountDownTimerPreference.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2013 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 android.content.Context; +import android.util.AttributeSet; + +import com.android.gallery3d.R; + +public class CountDownTimerPreference extends ListPreference { + private static final int[] DURATIONS = { + 0, 1, 2, 3, 4, 5, 10, 15, 20, 30, 60 + }; + public CountDownTimerPreference(Context context, AttributeSet attrs) { + super(context, attrs); + initCountDownDurationChoices(context); + } + + private void initCountDownDurationChoices(Context context) { + CharSequence[] entryValues = new CharSequence[DURATIONS.length]; + CharSequence[] entries = new CharSequence[DURATIONS.length]; + for (int i = 0; i < DURATIONS.length; i++) { + entryValues[i] = Integer.toString(DURATIONS[i]); + if (i == 0) { + entries[0] = context.getString(R.string.setting_off); // Off + } else { + entries[i] = context.getResources() + .getQuantityString(R.plurals.pref_camera_timer_entry, i, i); + } + } + setEntries(entries); + setEntryValues(entryValues); + } +} diff --git a/src/com/android/camera/DisableCameraReceiver.java b/src/com/android/camera/DisableCameraReceiver.java new file mode 100644 index 000000000..351740541 --- /dev/null +++ b/src/com/android/camera/DisableCameraReceiver.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2012 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 android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.hardware.Camera.CameraInfo; +import android.util.Log; + +// We want to disable camera-related activities if there is no camera. This +// receiver runs when BOOT_COMPLETED intent is received. After running once +// this receiver will be disabled, so it will not run again. +public class DisableCameraReceiver extends BroadcastReceiver { + private static final String TAG = "DisableCameraReceiver"; + private static final boolean CHECK_BACK_CAMERA_ONLY = true; + private static final String ACTIVITIES[] = { + "com.android.camera.CameraLauncher", + }; + + @Override + public void onReceive(Context context, Intent intent) { + // Disable camera-related activities if there is no camera. + boolean needCameraActivity = CHECK_BACK_CAMERA_ONLY + ? hasBackCamera() + : hasCamera(); + + if (!needCameraActivity) { + Log.i(TAG, "disable all camera activities"); + for (int i = 0; i < ACTIVITIES.length; i++) { + disableComponent(context, ACTIVITIES[i]); + } + } + + // Disable this receiver so it won't run again. + disableComponent(context, "com.android.camera.DisableCameraReceiver"); + } + + private boolean hasCamera() { + int n = android.hardware.Camera.getNumberOfCameras(); + Log.i(TAG, "number of camera: " + n); + return (n > 0); + } + + private boolean hasBackCamera() { + int n = android.hardware.Camera.getNumberOfCameras(); + CameraInfo info = new CameraInfo(); + for (int i = 0; i < n; i++) { + android.hardware.Camera.getCameraInfo(i, info); + if (info.facing == CameraInfo.CAMERA_FACING_BACK) { + Log.i(TAG, "back camera found: " + i); + return true; + } + } + Log.i(TAG, "no back camera"); + return false; + } + + private void disableComponent(Context context, String klass) { + ComponentName name = new ComponentName(context, klass); + PackageManager pm = context.getPackageManager(); + + // We need the DONT_KILL_APP flag, otherwise we will be killed + // immediately because we are in the same app. + pm.setComponentEnabledSetting(name, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + } +} diff --git a/src/com/android/camera/EffectsRecorder.java b/src/com/android/camera/EffectsRecorder.java new file mode 100644 index 000000000..4bf8d411e --- /dev/null +++ b/src/com/android/camera/EffectsRecorder.java @@ -0,0 +1,1239 @@ +/* + * 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 android.annotation.TargetApi; +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.media.CamcorderProfile; +import android.media.MediaRecorder; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; + +import java.io.FileDescriptor; +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + + +/** + * Encapsulates the mobile filter framework components needed to record video + * with effects applied. Modeled after MediaRecorder. + */ +@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) // uses SurfaceTexture +public class EffectsRecorder { + private static final String TAG = "EffectsRecorder"; + + private static Class<?> sClassFilter; + private static Method sFilterIsAvailable; + private static EffectsRecorder sEffectsRecorder; + // The index of the current effects recorder. + private static int sEffectsRecorderIndex; + + private static boolean sReflectionInited = false; + + private static Class<?> sClsLearningDoneListener; + private static Class<?> sClsOnRunnerDoneListener; + private static Class<?> sClsOnRecordingDoneListener; + private static Class<?> sClsSurfaceTextureSourceListener; + + private static Method sFilterSetInputValue; + + private static Constructor<?> sCtPoint; + private static Constructor<?> sCtQuad; + + private static Method sLearningDoneListenerOnLearningDone; + + private static Method sObjectEquals; + private static Method sObjectToString; + + private static Class<?> sClsGraphRunner; + private static Method sGraphRunnerGetGraph; + private static Method sGraphRunnerSetDoneCallback; + private static Method sGraphRunnerRun; + private static Method sGraphRunnerGetError; + private static Method sGraphRunnerStop; + + private static Method sFilterGraphGetFilter; + private static Method sFilterGraphTearDown; + + private static Method sOnRunnerDoneListenerOnRunnerDone; + + private static Class<?> sClsGraphEnvironment; + private static Constructor<?> sCtGraphEnvironment; + private static Method sGraphEnvironmentCreateGLEnvironment; + private static Method sGraphEnvironmentGetRunner; + private static Method sGraphEnvironmentAddReferences; + private static Method sGraphEnvironmentLoadGraph; + private static Method sGraphEnvironmentGetContext; + + private static Method sFilterContextGetGLEnvironment; + private static Method sGLEnvironmentIsActive; + private static Method sGLEnvironmentActivate; + private static Method sGLEnvironmentDeactivate; + private static Method sSurfaceTextureTargetDisconnect; + private static Method sOnRecordingDoneListenerOnRecordingDone; + private static Method sSurfaceTextureSourceListenerOnSurfaceTextureSourceReady; + + private Object mLearningDoneListener; + private Object mRunnerDoneCallback; + private Object mSourceReadyCallback; + // A callback to finalize the media after the recording is done. + private Object mRecordingDoneListener; + + static { + try { + sClassFilter = Class.forName("android.filterfw.core.Filter"); + sFilterIsAvailable = sClassFilter.getMethod("isAvailable", + String.class); + } catch (ClassNotFoundException ex) { + Log.v(TAG, "Can't find the class android.filterfw.core.Filter"); + } catch (NoSuchMethodException e) { + Log.v(TAG, "Can't find the method Filter.isAvailable"); + } + } + + public static final int EFFECT_NONE = 0; + public static final int EFFECT_GOOFY_FACE = 1; + public static final int EFFECT_BACKDROPPER = 2; + + public static final int EFFECT_GF_SQUEEZE = 0; + public static final int EFFECT_GF_BIG_EYES = 1; + public static final int EFFECT_GF_BIG_MOUTH = 2; + public static final int EFFECT_GF_SMALL_MOUTH = 3; + public static final int EFFECT_GF_BIG_NOSE = 4; + public static final int EFFECT_GF_SMALL_EYES = 5; + public static final int NUM_OF_GF_EFFECTS = EFFECT_GF_SMALL_EYES + 1; + + public static final int EFFECT_MSG_STARTED_LEARNING = 0; + public static final int EFFECT_MSG_DONE_LEARNING = 1; + public static final int EFFECT_MSG_SWITCHING_EFFECT = 2; + public static final int EFFECT_MSG_EFFECTS_STOPPED = 3; + public static final int EFFECT_MSG_RECORDING_DONE = 4; + public static final int EFFECT_MSG_PREVIEW_RUNNING = 5; + + private Context mContext; + private Handler mHandler; + + private CameraManager.CameraProxy mCameraDevice; + private CamcorderProfile mProfile; + private double mCaptureRate = 0; + private SurfaceTexture mPreviewSurfaceTexture; + private int mPreviewWidth; + private int mPreviewHeight; + private MediaRecorder.OnInfoListener mInfoListener; + private MediaRecorder.OnErrorListener mErrorListener; + + private String mOutputFile; + private FileDescriptor mFd; + private int mOrientationHint = 0; + private long mMaxFileSize = 0; + private int mMaxDurationMs = 0; + private int mCameraFacing = Camera.CameraInfo.CAMERA_FACING_BACK; + private int mCameraDisplayOrientation; + + private int mEffect = EFFECT_NONE; + private int mCurrentEffect = EFFECT_NONE; + private EffectsListener mEffectsListener; + + private Object mEffectParameter; + + private Object mGraphEnv; + private int mGraphId; + private Object mRunner = null; + private Object mOldRunner = null; + + private SurfaceTexture mTextureSource; + + private static final int STATE_CONFIGURE = 0; + private static final int STATE_WAITING_FOR_SURFACE = 1; + private static final int STATE_STARTING_PREVIEW = 2; + private static final int STATE_PREVIEW = 3; + private static final int STATE_RECORD = 4; + private static final int STATE_RELEASED = 5; + private int mState = STATE_CONFIGURE; + + private boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE); + private SoundClips.Player mSoundPlayer; + + /** Determine if a given effect is supported at runtime + * Some effects require libraries not available on all devices + */ + public static boolean isEffectSupported(int effectId) { + if (sFilterIsAvailable == null) return false; + + try { + switch (effectId) { + case EFFECT_GOOFY_FACE: + return (Boolean) sFilterIsAvailable.invoke(null, + "com.google.android.filterpacks.facedetect.GoofyRenderFilter"); + case EFFECT_BACKDROPPER: + return (Boolean) sFilterIsAvailable.invoke(null, + "android.filterpacks.videoproc.BackDropperFilter"); + default: + return false; + } + } catch (Exception ex) { + Log.e(TAG, "Fail to check filter", ex); + } + return false; + } + + public EffectsRecorder(Context context) { + if (mLogVerbose) Log.v(TAG, "EffectsRecorder created (" + this + ")"); + + if (!sReflectionInited) { + try { + sFilterSetInputValue = sClassFilter.getMethod("setInputValue", + new Class[] {String.class, Object.class}); + + Class<?> clsPoint = Class.forName("android.filterfw.geometry.Point"); + sCtPoint = clsPoint.getConstructor(new Class[] {float.class, + float.class}); + + Class<?> clsQuad = Class.forName("android.filterfw.geometry.Quad"); + sCtQuad = clsQuad.getConstructor(new Class[] {clsPoint, clsPoint, + clsPoint, clsPoint}); + + Class<?> clsBackDropperFilter = Class.forName( + "android.filterpacks.videoproc.BackDropperFilter"); + sClsLearningDoneListener = Class.forName( + "android.filterpacks.videoproc.BackDropperFilter$LearningDoneListener"); + sLearningDoneListenerOnLearningDone = sClsLearningDoneListener + .getMethod("onLearningDone", new Class[] {clsBackDropperFilter}); + + sObjectEquals = Object.class.getMethod("equals", new Class[] {Object.class}); + sObjectToString = Object.class.getMethod("toString"); + + sClsOnRunnerDoneListener = Class.forName( + "android.filterfw.core.GraphRunner$OnRunnerDoneListener"); + sOnRunnerDoneListenerOnRunnerDone = sClsOnRunnerDoneListener.getMethod( + "onRunnerDone", new Class[] {int.class}); + + sClsGraphRunner = Class.forName("android.filterfw.core.GraphRunner"); + sGraphRunnerGetGraph = sClsGraphRunner.getMethod("getGraph"); + sGraphRunnerSetDoneCallback = sClsGraphRunner.getMethod( + "setDoneCallback", new Class[] {sClsOnRunnerDoneListener}); + sGraphRunnerRun = sClsGraphRunner.getMethod("run"); + sGraphRunnerGetError = sClsGraphRunner.getMethod("getError"); + sGraphRunnerStop = sClsGraphRunner.getMethod("stop"); + + Class<?> clsFilterContext = Class.forName("android.filterfw.core.FilterContext"); + sFilterContextGetGLEnvironment = clsFilterContext.getMethod( + "getGLEnvironment"); + + Class<?> clsFilterGraph = Class.forName("android.filterfw.core.FilterGraph"); + sFilterGraphGetFilter = clsFilterGraph.getMethod("getFilter", + new Class[] {String.class}); + sFilterGraphTearDown = clsFilterGraph.getMethod("tearDown", + new Class[] {clsFilterContext}); + + sClsGraphEnvironment = Class.forName("android.filterfw.GraphEnvironment"); + sCtGraphEnvironment = sClsGraphEnvironment.getConstructor(); + sGraphEnvironmentCreateGLEnvironment = sClsGraphEnvironment.getMethod( + "createGLEnvironment"); + sGraphEnvironmentGetRunner = sClsGraphEnvironment.getMethod( + "getRunner", new Class[] {int.class, int.class}); + sGraphEnvironmentAddReferences = sClsGraphEnvironment.getMethod( + "addReferences", new Class[] {Object[].class}); + sGraphEnvironmentLoadGraph = sClsGraphEnvironment.getMethod( + "loadGraph", new Class[] {Context.class, int.class}); + sGraphEnvironmentGetContext = sClsGraphEnvironment.getMethod( + "getContext"); + + Class<?> clsGLEnvironment = Class.forName("android.filterfw.core.GLEnvironment"); + sGLEnvironmentIsActive = clsGLEnvironment.getMethod("isActive"); + sGLEnvironmentActivate = clsGLEnvironment.getMethod("activate"); + sGLEnvironmentDeactivate = clsGLEnvironment.getMethod("deactivate"); + + Class<?> clsSurfaceTextureTarget = Class.forName( + "android.filterpacks.videosrc.SurfaceTextureTarget"); + sSurfaceTextureTargetDisconnect = clsSurfaceTextureTarget.getMethod( + "disconnect", new Class[] {clsFilterContext}); + + sClsOnRecordingDoneListener = Class.forName( + "android.filterpacks.videosink.MediaEncoderFilter$OnRecordingDoneListener"); + sOnRecordingDoneListenerOnRecordingDone = + sClsOnRecordingDoneListener.getMethod("onRecordingDone"); + + sClsSurfaceTextureSourceListener = Class.forName( + "android.filterpacks.videosrc.SurfaceTextureSource$SurfaceTextureSourceListener"); + sSurfaceTextureSourceListenerOnSurfaceTextureSourceReady = + sClsSurfaceTextureSourceListener.getMethod( + "onSurfaceTextureSourceReady", + new Class[] {SurfaceTexture.class}); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + + sReflectionInited = true; + } + + sEffectsRecorderIndex++; + Log.v(TAG, "Current effects recorder index is " + sEffectsRecorderIndex); + sEffectsRecorder = this; + SerializableInvocationHandler sih = new SerializableInvocationHandler( + sEffectsRecorderIndex); + mLearningDoneListener = Proxy.newProxyInstance( + sClsLearningDoneListener.getClassLoader(), + new Class[] {sClsLearningDoneListener}, sih); + mRunnerDoneCallback = Proxy.newProxyInstance( + sClsOnRunnerDoneListener.getClassLoader(), + new Class[] {sClsOnRunnerDoneListener}, sih); + mSourceReadyCallback = Proxy.newProxyInstance( + sClsSurfaceTextureSourceListener.getClassLoader(), + new Class[] {sClsSurfaceTextureSourceListener}, sih); + mRecordingDoneListener = Proxy.newProxyInstance( + sClsOnRecordingDoneListener.getClassLoader(), + new Class[] {sClsOnRecordingDoneListener}, sih); + + mContext = context; + mHandler = new Handler(Looper.getMainLooper()); + mSoundPlayer = SoundClips.getPlayer(context); + } + + public synchronized void setCamera(CameraManager.CameraProxy cameraDevice) { + switch (mState) { + case STATE_PREVIEW: + throw new RuntimeException("setCamera cannot be called while previewing!"); + case STATE_RECORD: + throw new RuntimeException("setCamera cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException("setCamera called on an already released recorder!"); + default: + break; + } + + mCameraDevice = cameraDevice; + } + + public void setProfile(CamcorderProfile profile) { + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("setProfile cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException("setProfile called on an already released recorder!"); + default: + break; + } + mProfile = profile; + } + + public void setOutputFile(String outputFile) { + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("setOutputFile cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException("setOutputFile called on an already released recorder!"); + default: + break; + } + + mOutputFile = outputFile; + mFd = null; + } + + public void setOutputFile(FileDescriptor fd) { + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("setOutputFile cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException("setOutputFile called on an already released recorder!"); + default: + break; + } + + mOutputFile = null; + mFd = fd; + } + + /** + * Sets the maximum filesize (in bytes) of the recording session. + * This will be passed on to the MediaEncoderFilter and then to the + * MediaRecorder ultimately. If zero or negative, the MediaRecorder will + * disable the limit + */ + public synchronized void setMaxFileSize(long maxFileSize) { + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("setMaxFileSize cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException( + "setMaxFileSize called on an already released recorder!"); + default: + break; + } + mMaxFileSize = maxFileSize; + } + + /** + * Sets the maximum recording duration (in ms) for the next recording session + * Setting it to zero (the default) disables the limit. + */ + public synchronized void setMaxDuration(int maxDurationMs) { + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("setMaxDuration cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException( + "setMaxDuration called on an already released recorder!"); + default: + break; + } + mMaxDurationMs = maxDurationMs; + } + + + public void setCaptureRate(double fps) { + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("setCaptureRate cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException( + "setCaptureRate called on an already released recorder!"); + default: + break; + } + + if (mLogVerbose) Log.v(TAG, "Setting time lapse capture rate to " + fps + " fps"); + mCaptureRate = fps; + } + + public void setPreviewSurfaceTexture(SurfaceTexture previewSurfaceTexture, + int previewWidth, + int previewHeight) { + if (mLogVerbose) Log.v(TAG, "setPreviewSurfaceTexture(" + this + ")"); + switch (mState) { + case STATE_RECORD: + throw new RuntimeException( + "setPreviewSurfaceTexture cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException( + "setPreviewSurfaceTexture called on an already released recorder!"); + default: + break; + } + + mPreviewSurfaceTexture = previewSurfaceTexture; + mPreviewWidth = previewWidth; + mPreviewHeight = previewHeight; + + switch (mState) { + case STATE_WAITING_FOR_SURFACE: + startPreview(); + break; + case STATE_STARTING_PREVIEW: + case STATE_PREVIEW: + initializeEffect(true); + break; + } + } + + public void setEffect(int effect, Object effectParameter) { + if (mLogVerbose) Log.v(TAG, + "setEffect: effect ID " + effect + + ", parameter " + effectParameter.toString()); + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("setEffect cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException("setEffect called on an already released recorder!"); + default: + break; + } + + mEffect = effect; + mEffectParameter = effectParameter; + + if (mState == STATE_PREVIEW || + mState == STATE_STARTING_PREVIEW) { + initializeEffect(false); + } + } + + public interface EffectsListener { + public void onEffectsUpdate(int effectId, int effectMsg); + public void onEffectsError(Exception exception, String filePath); + } + + public void setEffectsListener(EffectsListener listener) { + mEffectsListener = listener; + } + + private void setFaceDetectOrientation() { + if (mCurrentEffect == EFFECT_GOOFY_FACE) { + Object rotateFilter = getGraphFilter(mRunner, "rotate"); + Object metaRotateFilter = getGraphFilter(mRunner, "metarotate"); + setInputValue(rotateFilter, "rotation", mOrientationHint); + int reverseDegrees = (360 - mOrientationHint) % 360; + setInputValue(metaRotateFilter, "rotation", reverseDegrees); + } + } + + private void setRecordingOrientation() { + if (mState != STATE_RECORD && mRunner != null) { + Object bl = newInstance(sCtPoint, new Object[] {0, 0}); + Object br = newInstance(sCtPoint, new Object[] {1, 0}); + Object tl = newInstance(sCtPoint, new Object[] {0, 1}); + Object tr = newInstance(sCtPoint, new Object[] {1, 1}); + Object recordingRegion; + if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_BACK) { + // The back camera is not mirrored, so use a identity transform + recordingRegion = newInstance(sCtQuad, new Object[] {bl, br, tl, tr}); + } else { + // Recording region needs to be tweaked for front cameras, since they + // mirror their preview + if (mOrientationHint == 0 || mOrientationHint == 180) { + // Horizontal flip in landscape + recordingRegion = newInstance(sCtQuad, new Object[] {br, bl, tr, tl}); + } else { + // Horizontal flip in portrait + recordingRegion = newInstance(sCtQuad, new Object[] {tl, tr, bl, br}); + } + } + Object recorder = getGraphFilter(mRunner, "recorder"); + setInputValue(recorder, "inputRegion", recordingRegion); + } + } + public void setOrientationHint(int degrees) { + switch (mState) { + case STATE_RELEASED: + throw new RuntimeException( + "setOrientationHint called on an already released recorder!"); + default: + break; + } + if (mLogVerbose) Log.v(TAG, "Setting orientation hint to: " + degrees); + mOrientationHint = degrees; + setFaceDetectOrientation(); + setRecordingOrientation(); + } + + public void setCameraDisplayOrientation(int orientation) { + if (mState != STATE_CONFIGURE) { + throw new RuntimeException( + "setCameraDisplayOrientation called after configuration!"); + } + mCameraDisplayOrientation = orientation; + } + + public void setCameraFacing(int facing) { + switch (mState) { + case STATE_RELEASED: + throw new RuntimeException( + "setCameraFacing called on alrady released recorder!"); + default: + break; + } + mCameraFacing = facing; + setRecordingOrientation(); + } + + public void setOnInfoListener(MediaRecorder.OnInfoListener infoListener) { + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("setInfoListener cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException( + "setInfoListener called on an already released recorder!"); + default: + break; + } + mInfoListener = infoListener; + } + + public void setOnErrorListener(MediaRecorder.OnErrorListener errorListener) { + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("setErrorListener cannot be called while recording!"); + case STATE_RELEASED: + throw new RuntimeException( + "setErrorListener called on an already released recorder!"); + default: + break; + } + mErrorListener = errorListener; + } + + private void initializeFilterFramework() { + mGraphEnv = newInstance(sCtGraphEnvironment); + invoke(mGraphEnv, sGraphEnvironmentCreateGLEnvironment); + + int videoFrameWidth = mProfile.videoFrameWidth; + int videoFrameHeight = mProfile.videoFrameHeight; + if (mCameraDisplayOrientation == 90 || mCameraDisplayOrientation == 270) { + int tmp = videoFrameWidth; + videoFrameWidth = videoFrameHeight; + videoFrameHeight = tmp; + } + + invoke(mGraphEnv, sGraphEnvironmentAddReferences, + new Object[] {new Object[] { + "textureSourceCallback", mSourceReadyCallback, + "recordingWidth", videoFrameWidth, + "recordingHeight", videoFrameHeight, + "recordingProfile", mProfile, + "learningDoneListener", mLearningDoneListener, + "recordingDoneListener", mRecordingDoneListener}}); + mRunner = null; + mGraphId = -1; + mCurrentEffect = EFFECT_NONE; + } + + private synchronized void initializeEffect(boolean forceReset) { + if (forceReset || + mCurrentEffect != mEffect || + mCurrentEffect == EFFECT_BACKDROPPER) { + + invoke(mGraphEnv, sGraphEnvironmentAddReferences, + new Object[] {new Object[] { + "previewSurfaceTexture", mPreviewSurfaceTexture, + "previewWidth", mPreviewWidth, + "previewHeight", mPreviewHeight, + "orientation", mOrientationHint}}); + if (mState == STATE_PREVIEW || + mState == STATE_STARTING_PREVIEW) { + // Switching effects while running. Inform video camera. + sendMessage(mCurrentEffect, EFFECT_MSG_SWITCHING_EFFECT); + } + + switch (mEffect) { + case EFFECT_GOOFY_FACE: + mGraphId = (Integer) invoke(mGraphEnv, + sGraphEnvironmentLoadGraph, + new Object[] {mContext, R.raw.goofy_face}); + break; + case EFFECT_BACKDROPPER: + sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_STARTED_LEARNING); + mGraphId = (Integer) invoke(mGraphEnv, + sGraphEnvironmentLoadGraph, + new Object[] {mContext, R.raw.backdropper}); + break; + default: + throw new RuntimeException("Unknown effect ID" + mEffect + "!"); + } + mCurrentEffect = mEffect; + + mOldRunner = mRunner; + mRunner = invoke(mGraphEnv, sGraphEnvironmentGetRunner, + new Object[] {mGraphId, + getConstant(sClsGraphEnvironment, "MODE_ASYNCHRONOUS")}); + invoke(mRunner, sGraphRunnerSetDoneCallback, new Object[] {mRunnerDoneCallback}); + if (mLogVerbose) { + Log.v(TAG, "New runner: " + mRunner + + ". Old runner: " + mOldRunner); + } + if (mState == STATE_PREVIEW || + mState == STATE_STARTING_PREVIEW) { + // Switching effects while running. Stop existing runner. + // The stop callback will take care of starting new runner. + mCameraDevice.stopPreview(); + mCameraDevice.setPreviewTexture(null); + invoke(mOldRunner, sGraphRunnerStop); + } + } + + switch (mCurrentEffect) { + case EFFECT_GOOFY_FACE: + tryEnableVideoStabilization(true); + Object goofyFilter = getGraphFilter(mRunner, "goofyrenderer"); + setInputValue(goofyFilter, "currentEffect", + ((Integer) mEffectParameter).intValue()); + break; + case EFFECT_BACKDROPPER: + tryEnableVideoStabilization(false); + Object backgroundSrc = getGraphFilter(mRunner, "background"); + if (ApiHelper.HAS_EFFECTS_RECORDING_CONTEXT_INPUT) { + // Set the context first before setting sourceUrl to + // guarantee the content URI get resolved properly. + setInputValue(backgroundSrc, "context", mContext); + } + setInputValue(backgroundSrc, "sourceUrl", mEffectParameter); + // For front camera, the background video needs to be mirrored in the + // backdropper filter + if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + Object replacer = getGraphFilter(mRunner, "replacer"); + setInputValue(replacer, "mirrorBg", true); + if (mLogVerbose) Log.v(TAG, "Setting the background to be mirrored"); + } + break; + default: + break; + } + setFaceDetectOrientation(); + setRecordingOrientation(); + } + + public synchronized void startPreview() { + if (mLogVerbose) Log.v(TAG, "Starting preview (" + this + ")"); + + switch (mState) { + case STATE_STARTING_PREVIEW: + case STATE_PREVIEW: + // Already running preview + Log.w(TAG, "startPreview called when already running preview"); + return; + case STATE_RECORD: + throw new RuntimeException("Cannot start preview when already recording!"); + case STATE_RELEASED: + throw new RuntimeException("setEffect called on an already released recorder!"); + default: + break; + } + + if (mEffect == EFFECT_NONE) { + throw new RuntimeException("No effect selected!"); + } + if (mEffectParameter == null) { + throw new RuntimeException("No effect parameter provided!"); + } + if (mProfile == null) { + throw new RuntimeException("No recording profile provided!"); + } + if (mPreviewSurfaceTexture == null) { + if (mLogVerbose) Log.v(TAG, "Passed a null surface; waiting for valid one"); + mState = STATE_WAITING_FOR_SURFACE; + return; + } + if (mCameraDevice == null) { + throw new RuntimeException("No camera to record from!"); + } + + if (mLogVerbose) Log.v(TAG, "Initializing filter framework and running the graph."); + initializeFilterFramework(); + + initializeEffect(true); + + mState = STATE_STARTING_PREVIEW; + invoke(mRunner, sGraphRunnerRun); + // Rest of preview startup handled in mSourceReadyCallback + } + + private Object invokeObjectEquals(Object proxy, Object[] args) { + return Boolean.valueOf(proxy == args[0]); + } + + private Object invokeObjectToString() { + return "Proxy-" + toString(); + } + + private void invokeOnLearningDone() { + if (mLogVerbose) Log.v(TAG, "Learning done callback triggered"); + // Called in a processing thread, so have to post message back to UI + // thread + sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_DONE_LEARNING); + enable3ALocks(true); + } + + private void invokeOnRunnerDone(Object[] args) { + int runnerDoneResult = (Integer) args[0]; + synchronized (EffectsRecorder.this) { + if (mLogVerbose) { + Log.v(TAG, + "Graph runner done (" + EffectsRecorder.this + + ", mRunner " + mRunner + + ", mOldRunner " + mOldRunner + ")"); + } + if (runnerDoneResult == + (Integer) getConstant(sClsGraphRunner, "RESULT_ERROR")) { + // Handle error case + Log.e(TAG, "Error running filter graph!"); + Exception e = null; + if (mRunner != null) { + e = (Exception) invoke(mRunner, sGraphRunnerGetError); + } else if (mOldRunner != null) { + e = (Exception) invoke(mOldRunner, sGraphRunnerGetError); + } + raiseError(e); + } + if (mOldRunner != null) { + // Tear down old graph if available + if (mLogVerbose) Log.v(TAG, "Tearing down old graph."); + Object glEnv = getContextGLEnvironment(mGraphEnv); + if (glEnv != null && !(Boolean) invoke(glEnv, sGLEnvironmentIsActive)) { + invoke(glEnv, sGLEnvironmentActivate); + } + getGraphTearDown(mOldRunner, + invoke(mGraphEnv, sGraphEnvironmentGetContext)); + if (glEnv != null && (Boolean) invoke(glEnv, sGLEnvironmentIsActive)) { + invoke(glEnv, sGLEnvironmentDeactivate); + } + mOldRunner = null; + } + if (mState == STATE_PREVIEW || + mState == STATE_STARTING_PREVIEW) { + // Switching effects, start up the new runner + if (mLogVerbose) { + Log.v(TAG, "Previous effect halted. Running graph again. state: " + + mState); + } + tryEnable3ALocks(false); + // In case of an error, the graph restarts from beginning and in case + // of the BACKDROPPER effect, the learner re-learns the background. + // Hence, we need to show the learning dialogue to the user + // to avoid recording before the learning is done. Else, the user + // could start recording before the learning is done and the new + // background comes up later leading to an end result video + // with a heterogeneous background. + // For BACKDROPPER effect, this path is also executed sometimes at + // the end of a normal recording session. In such a case, the graph + // does not restart and hence the learner does not re-learn. So we + // do not want to show the learning dialogue then. + if (runnerDoneResult == (Integer) getConstant( + sClsGraphRunner, "RESULT_ERROR") + && mCurrentEffect == EFFECT_BACKDROPPER) { + sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_STARTED_LEARNING); + } + invoke(mRunner, sGraphRunnerRun); + } else if (mState != STATE_RELEASED) { + // Shutting down effects + if (mLogVerbose) Log.v(TAG, "Runner halted, restoring direct preview"); + tryEnable3ALocks(false); + sendMessage(EFFECT_NONE, EFFECT_MSG_EFFECTS_STOPPED); + } else { + // STATE_RELEASED - camera will be/has been released as well, do nothing. + } + } + } + + private void invokeOnSurfaceTextureSourceReady(Object[] args) { + SurfaceTexture source = (SurfaceTexture) args[0]; + if (mLogVerbose) Log.v(TAG, "SurfaceTexture ready callback received"); + synchronized (EffectsRecorder.this) { + mTextureSource = source; + + if (mState == STATE_CONFIGURE) { + // Stop preview happened while the runner was doing startup tasks + // Since we haven't started anything up, don't do anything + // Rest of cleanup will happen in onRunnerDone + if (mLogVerbose) Log.v(TAG, "Ready callback: Already stopped, skipping."); + return; + } + if (mState == STATE_RELEASED) { + // EffectsRecorder has been released, so don't touch the camera device + // or anything else + if (mLogVerbose) Log.v(TAG, "Ready callback: Already released, skipping."); + return; + } + if (source == null) { + if (mLogVerbose) { + Log.v(TAG, "Ready callback: source null! Looks like graph was closed!"); + } + if (mState == STATE_PREVIEW || + mState == STATE_STARTING_PREVIEW || + mState == STATE_RECORD) { + // A null source here means the graph is shutting down + // unexpectedly, so we need to turn off preview before + // the surface texture goes away. + if (mLogVerbose) { + Log.v(TAG, "Ready callback: State: " + mState + + ". stopCameraPreview"); + } + + stopCameraPreview(); + } + return; + } + + // Lock AE/AWB to reduce transition flicker + tryEnable3ALocks(true); + + mCameraDevice.stopPreview(); + if (mLogVerbose) Log.v(TAG, "Runner active, connecting effects preview"); + mCameraDevice.setPreviewTexture(mTextureSource); + + mCameraDevice.startPreview(); + + // Unlock AE/AWB after preview started + tryEnable3ALocks(false); + + mState = STATE_PREVIEW; + + if (mLogVerbose) Log.v(TAG, "Start preview/effect switch complete"); + + // Sending a message to listener that preview is complete + sendMessage(mCurrentEffect, EFFECT_MSG_PREVIEW_RUNNING); + } + } + + private void invokeOnRecordingDone() { + // Forward the callback to the VideoModule object (as an asynchronous event). + if (mLogVerbose) Log.v(TAG, "Recording done callback triggered"); + sendMessage(EFFECT_NONE, EFFECT_MSG_RECORDING_DONE); + } + + public synchronized void startRecording() { + if (mLogVerbose) Log.v(TAG, "Starting recording (" + this + ")"); + + switch (mState) { + case STATE_RECORD: + throw new RuntimeException("Already recording, cannot begin anew!"); + case STATE_RELEASED: + throw new RuntimeException( + "startRecording called on an already released recorder!"); + default: + break; + } + + if ((mOutputFile == null) && (mFd == null)) { + throw new RuntimeException("No output file name or descriptor provided!"); + } + + if (mState == STATE_CONFIGURE) { + startPreview(); + } + + Object recorder = getGraphFilter(mRunner, "recorder"); + if (mFd != null) { + setInputValue(recorder, "outputFileDescriptor", mFd); + } else { + setInputValue(recorder, "outputFile", mOutputFile); + } + // It is ok to set the audiosource without checking for timelapse here + // since that check will be done in the MediaEncoderFilter itself + setInputValue(recorder, "audioSource", MediaRecorder.AudioSource.CAMCORDER); + setInputValue(recorder, "recordingProfile", mProfile); + setInputValue(recorder, "orientationHint", mOrientationHint); + // Important to set the timelapseinterval to 0 if the capture rate is not >0 + // since the recorder does not get created every time the recording starts. + // The recorder infers whether the capture is timelapsed based on the value of + // this interval + boolean captureTimeLapse = mCaptureRate > 0; + if (captureTimeLapse) { + double timeBetweenFrameCapture = 1 / mCaptureRate; + setInputValue(recorder, "timelapseRecordingIntervalUs", + (long) (1000000 * timeBetweenFrameCapture)); + + } else { + setInputValue(recorder, "timelapseRecordingIntervalUs", 0L); + } + + if (mInfoListener != null) { + setInputValue(recorder, "infoListener", mInfoListener); + } + if (mErrorListener != null) { + setInputValue(recorder, "errorListener", mErrorListener); + } + setInputValue(recorder, "maxFileSize", mMaxFileSize); + setInputValue(recorder, "maxDurationMs", mMaxDurationMs); + setInputValue(recorder, "recording", true); + mSoundPlayer.play(SoundClips.START_VIDEO_RECORDING); + mState = STATE_RECORD; + } + + public synchronized void stopRecording() { + if (mLogVerbose) Log.v(TAG, "Stop recording (" + this + ")"); + + switch (mState) { + case STATE_CONFIGURE: + case STATE_STARTING_PREVIEW: + case STATE_PREVIEW: + Log.w(TAG, "StopRecording called when recording not active!"); + return; + case STATE_RELEASED: + throw new RuntimeException("stopRecording called on released EffectsRecorder!"); + default: + break; + } + Object recorder = getGraphFilter(mRunner, "recorder"); + setInputValue(recorder, "recording", false); + mSoundPlayer.play(SoundClips.STOP_VIDEO_RECORDING); + mState = STATE_PREVIEW; + } + + // Called to tell the filter graph that the display surfacetexture is not valid anymore. + // So the filter graph should not hold any reference to the surface created with that. + public synchronized void disconnectDisplay() { + if (mLogVerbose) Log.v(TAG, "Disconnecting the graph from the " + + "SurfaceTexture"); + Object display = getGraphFilter(mRunner, "display"); + invoke(display, sSurfaceTextureTargetDisconnect, new Object[] { + invoke(mGraphEnv, sGraphEnvironmentGetContext)}); + } + + // The VideoModule will call this to notify that the camera is being + // released to the outside world. This call should happen after the + // stopRecording call. Else, the effects may throw an exception. + // With the recording stopped, the stopPreview call will not try to + // release the camera again. + // This must be called in onPause() if the effects are ON. + public synchronized void disconnectCamera() { + if (mLogVerbose) Log.v(TAG, "Disconnecting the effects from Camera"); + stopCameraPreview(); + mCameraDevice = null; + } + + // In a normal case, when the disconnect is not called, we should not + // set the camera device to null, since on return callback, we try to + // enable 3A locks, which need the cameradevice. + public synchronized void stopCameraPreview() { + if (mLogVerbose) Log.v(TAG, "Stopping camera preview."); + if (mCameraDevice == null) { + Log.d(TAG, "Camera already null. Nothing to disconnect"); + return; + } + mCameraDevice.stopPreview(); + mCameraDevice.setPreviewTexture(null); + } + + // Stop and release effect resources + public synchronized void stopPreview() { + if (mLogVerbose) Log.v(TAG, "Stopping preview (" + this + ")"); + switch (mState) { + case STATE_CONFIGURE: + Log.w(TAG, "StopPreview called when preview not active!"); + return; + case STATE_RELEASED: + throw new RuntimeException("stopPreview called on released EffectsRecorder!"); + default: + break; + } + + if (mState == STATE_RECORD) { + stopRecording(); + } + + mCurrentEffect = EFFECT_NONE; + + // This will not do anything if the camera has already been disconnected. + stopCameraPreview(); + + mState = STATE_CONFIGURE; + mOldRunner = mRunner; + invoke(mRunner, sGraphRunnerStop); + mRunner = null; + // Rest of stop and release handled in mRunnerDoneCallback + } + + // Try to enable/disable video stabilization if supported; otherwise return false + // It is called from a synchronized block. + boolean tryEnableVideoStabilization(boolean toggle) { + if (mLogVerbose) Log.v(TAG, "tryEnableVideoStabilization."); + if (mCameraDevice == null) { + Log.d(TAG, "Camera already null. Not enabling video stabilization."); + return false; + } + Camera.Parameters params = mCameraDevice.getParameters(); + + String vstabSupported = params.get("video-stabilization-supported"); + if ("true".equals(vstabSupported)) { + if (mLogVerbose) Log.v(TAG, "Setting video stabilization to " + toggle); + params.set("video-stabilization", toggle ? "true" : "false"); + mCameraDevice.setParameters(params); + return true; + } + if (mLogVerbose) Log.v(TAG, "Video stabilization not supported"); + return false; + } + + // Try to enable/disable 3A locks if supported; otherwise return false + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + synchronized boolean tryEnable3ALocks(boolean toggle) { + if (mLogVerbose) Log.v(TAG, "tryEnable3ALocks"); + if (mCameraDevice == null) { + Log.d(TAG, "Camera already null. Not tryenabling 3A locks."); + return false; + } + Camera.Parameters params = mCameraDevice.getParameters(); + if (Util.isAutoExposureLockSupported(params) && + Util.isAutoWhiteBalanceLockSupported(params)) { + params.setAutoExposureLock(toggle); + params.setAutoWhiteBalanceLock(toggle); + mCameraDevice.setParameters(params); + return true; + } + return false; + } + + // Try to enable/disable 3A locks if supported; otherwise, throw error + // Use this when locks are essential to success + synchronized void enable3ALocks(boolean toggle) { + if (mLogVerbose) Log.v(TAG, "Enable3ALocks"); + if (mCameraDevice == null) { + Log.d(TAG, "Camera already null. Not enabling 3A locks."); + return; + } + Camera.Parameters params = mCameraDevice.getParameters(); + if (!tryEnable3ALocks(toggle)) { + throw new RuntimeException("Attempt to lock 3A on camera with no locking support!"); + } + } + + static class SerializableInvocationHandler + implements InvocationHandler, Serializable { + private final int mEffectsRecorderIndex; + public SerializableInvocationHandler(int index) { + mEffectsRecorderIndex = index; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + if (sEffectsRecorder == null) return null; + if (mEffectsRecorderIndex != sEffectsRecorderIndex) { + Log.v(TAG, "Ignore old callback " + mEffectsRecorderIndex); + return null; + } + if (method.equals(sObjectEquals)) { + return sEffectsRecorder.invokeObjectEquals(proxy, args); + } else if (method.equals(sObjectToString)) { + return sEffectsRecorder.invokeObjectToString(); + } else if (method.equals(sLearningDoneListenerOnLearningDone)) { + sEffectsRecorder.invokeOnLearningDone(); + } else if (method.equals(sOnRunnerDoneListenerOnRunnerDone)) { + sEffectsRecorder.invokeOnRunnerDone(args); + } else if (method.equals( + sSurfaceTextureSourceListenerOnSurfaceTextureSourceReady)) { + sEffectsRecorder.invokeOnSurfaceTextureSourceReady(args); + } else if (method.equals(sOnRecordingDoneListenerOnRecordingDone)) { + sEffectsRecorder.invokeOnRecordingDone(); + } + return null; + } + } + + // Indicates that all camera/recording activity needs to halt + public synchronized void release() { + if (mLogVerbose) Log.v(TAG, "Releasing (" + this + ")"); + + switch (mState) { + case STATE_RECORD: + case STATE_STARTING_PREVIEW: + case STATE_PREVIEW: + stopPreview(); + // Fall-through + default: + if (mSoundPlayer != null) { + mSoundPlayer.release(); + mSoundPlayer = null; + } + mState = STATE_RELEASED; + break; + } + sEffectsRecorder = null; + } + + private void sendMessage(final int effect, final int msg) { + if (mEffectsListener != null) { + mHandler.post(new Runnable() { + @Override + public void run() { + mEffectsListener.onEffectsUpdate(effect, msg); + } + }); + } + } + + private void raiseError(final Exception exception) { + if (mEffectsListener != null) { + mHandler.post(new Runnable() { + @Override + public void run() { + if (mFd != null) { + mEffectsListener.onEffectsError(exception, null); + } else { + mEffectsListener.onEffectsError(exception, mOutputFile); + } + } + }); + } + } + + // invoke method on receiver with no arguments + private Object invoke(Object receiver, Method method) { + try { + return method.invoke(receiver); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + // invoke method on receiver with arguments + private Object invoke(Object receiver, Method method, Object[] args) { + try { + return method.invoke(receiver, args); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private void setInputValue(Object receiver, String key, Object value) { + try { + sFilterSetInputValue.invoke(receiver, new Object[] {key, value}); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private Object newInstance(Constructor<?> ct, Object[] initArgs) { + try { + return ct.newInstance(initArgs); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private Object newInstance(Constructor<?> ct) { + try { + return ct.newInstance(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private Object getGraphFilter(Object receiver, String name) { + try { + return sFilterGraphGetFilter.invoke(sGraphRunnerGetGraph + .invoke(receiver), new Object[] {name}); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private Object getContextGLEnvironment(Object receiver) { + try { + return sFilterContextGetGLEnvironment + .invoke(sGraphEnvironmentGetContext.invoke(receiver)); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private void getGraphTearDown(Object receiver, Object filterContext) { + try { + sFilterGraphTearDown.invoke(sGraphRunnerGetGraph.invoke(receiver), + new Object[]{filterContext}); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private Object getConstant(Class<?> cls, String name) { + try { + return cls.getDeclaredField(name).get(null); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/src/com/android/camera/Exif.java b/src/com/android/camera/Exif.java new file mode 100644 index 000000000..c6ec6af50 --- /dev/null +++ b/src/com/android/camera/Exif.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2010 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 android.util.Log; + +import com.android.gallery3d.exif.ExifInterface; + +import java.io.IOException; + +public class Exif { + private static final String TAG = "CameraExif"; + + public static ExifInterface getExif(byte[] jpegData) { + ExifInterface exif = new ExifInterface(); + try { + exif.readExif(jpegData); + } catch (IOException e) { + Log.w(TAG, "Failed to read EXIF data", e); + } + return exif; + } + + // Returns the degrees in clockwise. Values are 0, 90, 180, or 270. + public static int getOrientation(ExifInterface exif) { + Integer val = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION); + if (val == null) { + return 0; + } else { + return ExifInterface.getRotationForOrientationValue(val.shortValue()); + } + } + + public static int getOrientation(byte[] jpegData) { + if (jpegData == null) return 0; + + ExifInterface exif = getExif(jpegData); + return getOrientation(exif); + } +} diff --git a/src/com/android/camera/FocusOverlayManager.java b/src/com/android/camera/FocusOverlayManager.java new file mode 100644 index 000000000..8bcb52fe5 --- /dev/null +++ b/src/com/android/camera/FocusOverlayManager.java @@ -0,0 +1,558 @@ +/* + * Copyright (C) 2012 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 android.annotation.TargetApi; +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 android.util.Log; + +import com.android.gallery3d.common.ApiHelper; + +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 TAG = "CAM_FocusManager"; + + 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 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 int mDisplayOrientation; + private List<Object> mFocusArea; // focus area in driver format + private List<Object> mMeteringArea; // metering area in driver format + private String mFocusMode; + private String[] mDefaultFocusModes; + private String mOverrideFocusMode; + private Parameters mParameters; + private ComboPreferences mPreferences; + private Handler mHandler; + Listener mListener; + private boolean mPreviousMoving; + private boolean mFocusDefault; + + private FocusUI mUI; + + public interface FocusUI { + public boolean hasFaces(); + public void clearFocus(); + public void setFocusPosition(int x, int y); + public void onFocusStarted(); + public void onFocusSucceeded(boolean timeOut); + public void onFocusFailed(boolean timeOut); + public void pauseFaceDetection(); + public void resumeFaceDetection(); + } + + 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 { + public MainHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case RESET_TOUCH_FOCUS: { + cancelAutoFocus(); + mListener.startFaceDetection(); + break; + } + } + } + } + + public FocusOverlayManager(ComboPreferences preferences, String[] defaultFocusModes, + Parameters parameters, Listener listener, + boolean mirror, Looper looper, FocusUI ui) { + mHandler = new MainHandler(looper); + mMatrix = new Matrix(); + mPreferences = preferences; + mDefaultFocusModes = defaultFocusModes; + setParameters(parameters); + mListener = listener; + setMirror(mirror); + mFocusDefault = true; + mUI = ui; + } + + 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 = Util.isFocusAreaSupported(parameters); + mMeteringAreaSupported = Util.isMeteringAreaSupported(parameters); + mLockAeAwbNeeded = (Util.isAutoExposureLockSupported(mParameters) || + Util.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(); + } + + public void setDisplayOrientation(int displayOrientation) { + mDisplayOrientation = displayOrientation; + setMatrix(); + } + + private void setMatrix() { + if (mPreviewWidth != 0 && mPreviewHeight != 0) { + Matrix matrix = new Matrix(); + Util.prepareMatrix(matrix, mMirror, mDisplayOrientation, + 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 = true; + } + } + + private void lockAeAwbIfNeeded() { + if (mLockAeAwbNeeded && !mAeAwbLock) { + mAeAwbLock = true; + mListener.setFocusParameters(); + } + } + + private void unlockAeAwbIfNeeded() { + if (mLockAeAwbNeeded && mAeAwbLock && (mState != STATE_FOCUSING_SNAP_ON_FINISH)) { + mAeAwbLock = false; + mListener.setFocusParameters(); + } + } + + public void onShutterDown() { + if (!mInitialized) return; + + boolean autoFocusCalled = false; + if (needAutoFocusCall()) { + // Do not focus if touch focus has been triggered. + if (mState != STATE_SUCCESS && mState != STATE_FAIL) { + autoFocus(); + autoFocusCalled = true; + } + } + + if (!autoFocusCalled) lockAeAwbIfNeeded(); + } + + public void onShutterUp() { + if (!mInitialized) return; + + if (needAutoFocusCall()) { + // User releases half-pressed focus key. + if (mState == STATE_FOCUSING || mState == STATE_SUCCESS + || mState == STATE_FAIL) { + cancelAutoFocus(); + } + } + + // Unlock AE and AWB after cancelAutoFocus. Camera API does not + // guarantee setParameters can be called during autofocus. + unlockAeAwbIfNeeded(); + } + + 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 (!needAutoFocusCall() || (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) { + // We didn't do focus. This can happen if the user press focus key + // while the snapshot is still in progress. The user probably wants + // the next snapshot as soon as possible, so we just do a snapshot + // without focusing again. + capture(); + } + } + + 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 (!mFocusDefault) { + 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 the camera has detected some faces. + if (mUI.hasFaces()) { + mUI.clearFocus(); + return; + } + + // Ignore if we have requested autofocus. This method only handles + // continuous autofocus. + if (mState != STATE_IDLE) return; + + // animate on false->true trasition only b/8219520 + if (moving && !mPreviousMoving) { + mUI.onFocusStarted(); + } else if (!moving) { + mUI.onFocusSucceeded(true); + } + mPreviousMoving = moving; + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void initializeFocusAreas(int x, int y) { + if (mFocusArea == null) { + mFocusArea = new ArrayList<Object>(); + mFocusArea.add(new Area(new Rect(), 1)); + } + + // Convert the coordinates to driver format. + calculateTapArea(x, y, 1f, ((Area) mFocusArea.get(0)).rect); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void initializeMeteringAreas(int x, int y) { + if (mMeteringArea == null) { + mMeteringArea = new ArrayList<Object>(); + 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(x, y, 1.5f, ((Area) 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 ((!mFocusDefault) && (mState == STATE_FOCUSING || + mState == STATE_SUCCESS || mState == STATE_FAIL)) { + cancelAutoFocus(); + } + if (mPreviewWidth == 0 || mPreviewHeight == 0) return; + mFocusDefault = false; + // Initialize mFocusArea. + if (mFocusAreaSupported) { + initializeFocusAreas(x, y); + } + // Initialize mMeteringArea. + if (mMeteringAreaSupported) { + initializeMeteringAreas(x, y); + } + + // Use margin to set the focus indicator to the touched area. + mUI.setFocusPosition(x, y); + + // 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) { + 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() { + 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. + mUI.pauseFaceDetection(); + 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(); + mUI.resumeFaceDetection(); + 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() { + if (mOverrideFocusMode != null) return mOverrideFocusMode; + if (mParameters == null) return Parameters.FOCUS_MODE_AUTO; + List<String> supportedFocusModes = mParameters.getSupportedFocusModes(); + + if (mFocusAreaSupported && !mFocusDefault) { + // Always use autofocus in tap-to-focus. + mFocusMode = Parameters.FOCUS_MODE_AUTO; + } else { + // The default is continuous autofocus. + mFocusMode = mPreferences.getString( + CameraSettings.KEY_FOCUS_MODE, null); + + // Try to find a supported focus mode from the default list. + if (mFocusMode == null) { + for (int i = 0; i < mDefaultFocusModes.length; i++) { + String mode = mDefaultFocusModes[i]; + if (Util.isSupported(mode, supportedFocusModes)) { + mFocusMode = mode; + break; + } + } + } + } + if (!Util.isSupported(mFocusMode, supportedFocusModes)) { + // For some reasons, the driver does not support the current + // focus mode. Fall back to auto. + if (Util.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; + } + + public void updateFocusUI() { + if (!mInitialized) return; + // Show only focus indicator or face indicator. + + if (mState == STATE_IDLE) { + if (mFocusDefault) { + mUI.clearFocus(); + } 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. + mUI.onFocusStarted(); + } + } else if (mState == STATE_FOCUSING || mState == STATE_FOCUSING_SNAP_ON_FINISH) { + mUI.onFocusStarted(); + } else { + if (Util.FOCUS_MODE_CONTINUOUS_PICTURE.equals(mFocusMode)) { + // TODO: check HAL behavior and decide if this can be removed. + mUI.onFocusSucceeded(false); + } else if (mState == STATE_SUCCESS) { + mUI.onFocusSucceeded(false); + } else if (mState == STATE_FAIL) { + mUI.onFocusFailed(false); + } + } + } + + public void resetTouchFocus() { + if (!mInitialized) return; + + // Put focus indicator to the center. clear reset position + mUI.clearFocus(); + // Initialize mFocusArea. + if (mFocusAreaSupported) { + initializeFocusAreas(mPreviewWidth / 2, mPreviewHeight / 2); + } + // Initialize mMeteringArea. + if (mMeteringAreaSupported) { + initializeMeteringAreas(mPreviewWidth / 2, mPreviewHeight / 2); + } + mFocusDefault = true; + } + + private void calculateTapArea(int x, int y, float areaMultiple, Rect rect) { + int areaSize = (int) (Math.min(mPreviewWidth, mPreviewHeight) * areaMultiple / 20); + int left = Util.clamp(x - areaSize, 0, mPreviewWidth - 2 * areaSize); + int top = Util.clamp(y - areaSize, 0, mPreviewHeight - 2 * areaSize); + + RectF rectF = new RectF(left, top, left + 2 * areaSize, top + 2 * areaSize); + mMatrix.mapRect(rectF); + Util.rectFToRect(rectF, rect); + } + + /* package */ int getFocusState() { + return mState; + } + + public boolean isFocusCompleted() { + return mState == STATE_SUCCESS || mState == STATE_FAIL; + } + + public boolean isFocusingSnapOnFinish() { + return mState == STATE_FOCUSING_SNAP_ON_FINISH; + } + + public void removeMessages() { + mHandler.removeMessages(RESET_TOUCH_FOCUS); + } + + public void overrideFocusMode(String focusMode) { + mOverrideFocusMode = focusMode; + } + + public void setAeAwbLock(boolean lock) { + mAeAwbLock = lock; + } + + public boolean getAeAwbLock() { + return mAeAwbLock; + } + + private boolean needAutoFocusCall() { + String focusMode = getFocusMode(); + return !(focusMode.equals(Parameters.FOCUS_MODE_INFINITY) + || focusMode.equals(Parameters.FOCUS_MODE_FIXED) + || focusMode.equals(Parameters.FOCUS_MODE_EDOF)); + } +} diff --git a/src/com/android/camera/IconListPreference.java b/src/com/android/camera/IconListPreference.java new file mode 100644 index 000000000..e5f75d3a5 --- /dev/null +++ b/src/com/android/camera/IconListPreference.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2009 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 android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.util.AttributeSet; + +import com.android.gallery3d.R; + +import java.util.List; + +/** A {@code ListPreference} where each entry has a corresponding icon. */ +public class IconListPreference extends ListPreference { + private int mSingleIconId; + private int mIconIds[]; + private int mLargeIconIds[]; + private int mImageIds[]; + private boolean mUseSingleIcon; + + public IconListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.IconListPreference, 0, 0); + Resources res = context.getResources(); + mSingleIconId = a.getResourceId( + R.styleable.IconListPreference_singleIcon, 0); + mIconIds = getIds(res, a.getResourceId( + R.styleable.IconListPreference_icons, 0)); + mLargeIconIds = getIds(res, a.getResourceId( + R.styleable.IconListPreference_largeIcons, 0)); + mImageIds = getIds(res, a.getResourceId( + R.styleable.IconListPreference_images, 0)); + a.recycle(); + } + + public int getSingleIcon() { + return mSingleIconId; + } + + public int[] getIconIds() { + return mIconIds; + } + + public int[] getLargeIconIds() { + return mLargeIconIds; + } + + public int[] getImageIds() { + return mImageIds; + } + + public boolean getUseSingleIcon() { + return mUseSingleIcon; + } + + public void setIconIds(int[] iconIds) { + mIconIds = iconIds; + } + + public void setLargeIconIds(int[] largeIconIds) { + mLargeIconIds = largeIconIds; + } + + public void setUseSingleIcon(boolean useSingle) { + mUseSingleIcon = useSingle; + } + + private int[] getIds(Resources res, int iconsRes) { + if (iconsRes == 0) return null; + TypedArray array = res.obtainTypedArray(iconsRes); + int n = array.length(); + int ids[] = new int[n]; + for (int i = 0; i < n; ++i) { + ids[i] = array.getResourceId(i, 0); + } + array.recycle(); + return ids; + } + + @Override + public void filterUnsupported(List<String> supported) { + CharSequence entryValues[] = getEntryValues(); + IntArray iconIds = new IntArray(); + IntArray largeIconIds = new IntArray(); + IntArray imageIds = new IntArray(); + + for (int i = 0, len = entryValues.length; i < len; i++) { + if (supported.indexOf(entryValues[i].toString()) >= 0) { + if (mIconIds != null) iconIds.add(mIconIds[i]); + if (mLargeIconIds != null) largeIconIds.add(mLargeIconIds[i]); + if (mImageIds != null) imageIds.add(mImageIds[i]); + } + } + if (mIconIds != null) mIconIds = iconIds.toArray(new int[iconIds.size()]); + if (mLargeIconIds != null) { + mLargeIconIds = largeIconIds.toArray(new int[largeIconIds.size()]); + } + if (mImageIds != null) mImageIds = imageIds.toArray(new int[imageIds.size()]); + super.filterUnsupported(supported); + } +} diff --git a/src/com/android/camera/ImageTaskManager.java b/src/com/android/camera/ImageTaskManager.java new file mode 100644 index 000000000..1324942fd --- /dev/null +++ b/src/com/android/camera/ImageTaskManager.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2013 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 android.net.Uri; + +/** + * The interface for background image processing task manager. + */ +interface ImageTaskManager { + + /** + * Callback interface for task events. + */ + public interface TaskListener { + public void onTaskQueued(String filePath, Uri imageUri); + public void onTaskDone(String filePath, Uri imageUri); + public void onTaskProgress( + String filePath, Uri imageUri, int progress); + } + + public void addTaskListener(TaskListener l); + + public void removeTaskListener(TaskListener l); + + /** + * Get task progress by Uri. + * + * @param uri The Uri of the final image file to identify the task. + * @return Integer from 0 to 100, or -1. The percentage of the task done + * so far. -1 means not found. + */ + public int getTaskProgress(Uri uri); +} diff --git a/src/com/android/camera/IntArray.java b/src/com/android/camera/IntArray.java new file mode 100644 index 000000000..a2550dbd8 --- /dev/null +++ b/src/com/android/camera/IntArray.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2010 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; + +public class IntArray { + private static final int INIT_CAPACITY = 8; + + private int mData[] = new int[INIT_CAPACITY]; + private int mSize = 0; + + public void add(int value) { + if (mData.length == mSize) { + int temp[] = new int[mSize + mSize]; + System.arraycopy(mData, 0, temp, 0, mSize); + mData = temp; + } + mData[mSize++] = value; + } + + public int size() { + return mSize; + } + + public int[] toArray(int[] result) { + if (result == null || result.length < mSize) { + result = new int[mSize]; + } + System.arraycopy(mData, 0, result, 0, mSize); + return result; + } +} diff --git a/src/com/android/camera/ListPreference.java b/src/com/android/camera/ListPreference.java new file mode 100644 index 000000000..38866de9d --- /dev/null +++ b/src/com/android/camera/ListPreference.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2009 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 android.content.Context; +import android.content.SharedPreferences; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; + +import com.android.gallery3d.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * A type of <code>CameraPreference</code> whose number of possible values + * is limited. + */ +public class ListPreference extends CameraPreference { + private static final String TAG = "ListPreference"; + private final String mKey; + private String mValue; + private final CharSequence[] mDefaultValues; + + private CharSequence[] mEntries; + private CharSequence[] mEntryValues; + private CharSequence[] mLabels; + private boolean mLoaded = false; + + public ListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.ListPreference, 0, 0); + + mKey = Util.checkNotNull( + a.getString(R.styleable.ListPreference_key)); + + // We allow the defaultValue attribute to be a string or an array of + // strings. The reason we need multiple default values is that some + // of them may be unsupported on a specific platform (for example, + // continuous auto-focus). In that case the first supported value + // in the array will be used. + int attrDefaultValue = R.styleable.ListPreference_defaultValue; + TypedValue tv = a.peekValue(attrDefaultValue); + if (tv != null && tv.type == TypedValue.TYPE_REFERENCE) { + mDefaultValues = a.getTextArray(attrDefaultValue); + } else { + mDefaultValues = new CharSequence[1]; + mDefaultValues[0] = a.getString(attrDefaultValue); + } + + setEntries(a.getTextArray(R.styleable.ListPreference_entries)); + setEntryValues(a.getTextArray( + R.styleable.ListPreference_entryValues)); + setLabels(a.getTextArray( + R.styleable.ListPreference_labelList)); + a.recycle(); + } + + public String getKey() { + return mKey; + } + + public CharSequence[] getEntries() { + return mEntries; + } + + public CharSequence[] getEntryValues() { + return mEntryValues; + } + + public CharSequence[] getLabels() { + return mLabels; + } + + public void setEntries(CharSequence entries[]) { + mEntries = entries == null ? new CharSequence[0] : entries; + } + + public void setEntryValues(CharSequence values[]) { + mEntryValues = values == null ? new CharSequence[0] : values; + } + + public void setLabels(CharSequence labels[]) { + mLabels = labels == null ? new CharSequence[0] : labels; + } + + public String getValue() { + if (!mLoaded) { + mValue = getSharedPreferences().getString(mKey, + findSupportedDefaultValue()); + mLoaded = true; + } + return mValue; + } + + // Find the first value in mDefaultValues which is supported. + private String findSupportedDefaultValue() { + for (int i = 0; i < mDefaultValues.length; i++) { + for (int j = 0; j < mEntryValues.length; j++) { + // Note that mDefaultValues[i] may be null (if unspecified + // in the xml file). + if (mEntryValues[j].equals(mDefaultValues[i])) { + return mDefaultValues[i].toString(); + } + } + } + return null; + } + + public void setValue(String value) { + if (findIndexOfValue(value) < 0) throw new IllegalArgumentException(); + mValue = value; + persistStringValue(value); + } + + public void setValueIndex(int index) { + setValue(mEntryValues[index].toString()); + } + + public int findIndexOfValue(String value) { + for (int i = 0, n = mEntryValues.length; i < n; ++i) { + if (Util.equals(mEntryValues[i], value)) return i; + } + return -1; + } + + public int getCurrentIndex() { + return findIndexOfValue(getValue()); + } + + public String getEntry() { + return mEntries[findIndexOfValue(getValue())].toString(); + } + + public String getLabel() { + return mLabels[findIndexOfValue(getValue())].toString(); + } + + protected void persistStringValue(String value) { + SharedPreferences.Editor editor = getSharedPreferences().edit(); + editor.putString(mKey, value); + editor.apply(); + } + + @Override + public void reloadValue() { + this.mLoaded = false; + } + + public void filterUnsupported(List<String> supported) { + ArrayList<CharSequence> entries = new ArrayList<CharSequence>(); + ArrayList<CharSequence> entryValues = new ArrayList<CharSequence>(); + for (int i = 0, len = mEntryValues.length; i < len; i++) { + if (supported.indexOf(mEntryValues[i].toString()) >= 0) { + entries.add(mEntries[i]); + entryValues.add(mEntryValues[i]); + } + } + int size = entries.size(); + mEntries = entries.toArray(new CharSequence[size]); + mEntryValues = entryValues.toArray(new CharSequence[size]); + } + + public void filterDuplicated() { + ArrayList<CharSequence> entries = new ArrayList<CharSequence>(); + ArrayList<CharSequence> entryValues = new ArrayList<CharSequence>(); + for (int i = 0, len = mEntryValues.length; i < len; i++) { + if (!entries.contains(mEntries[i])) { + entries.add(mEntries[i]); + entryValues.add(mEntryValues[i]); + } + } + int size = entries.size(); + mEntries = entries.toArray(new CharSequence[size]); + mEntryValues = entryValues.toArray(new CharSequence[size]); + } + + public void print() { + Log.v(TAG, "Preference key=" + getKey() + ". value=" + getValue()); + for (int i = 0; i < mEntryValues.length; i++) { + Log.v(TAG, "entryValues[" + i + "]=" + mEntryValues[i]); + } + } +} diff --git a/src/com/android/camera/LocationManager.java b/src/com/android/camera/LocationManager.java new file mode 100644 index 000000000..fcf21b60f --- /dev/null +++ b/src/com/android/camera/LocationManager.java @@ -0,0 +1,181 @@ +/* + * 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 android.content.Context; +import android.location.Location; +import android.location.LocationProvider; +import android.os.Bundle; +import android.util.Log; + +/** + * A class that handles everything about location. + */ +public class LocationManager { + private static final String TAG = "LocationManager"; + + private Context mContext; + private Listener mListener; + private android.location.LocationManager mLocationManager; + private boolean mRecordLocation; + + LocationListener [] mLocationListeners = new LocationListener[] { + new LocationListener(android.location.LocationManager.GPS_PROVIDER), + new LocationListener(android.location.LocationManager.NETWORK_PROVIDER) + }; + + public interface Listener { + public void showGpsOnScreenIndicator(boolean hasSignal); + public void hideGpsOnScreenIndicator(); + } + + public LocationManager(Context context, Listener listener) { + mContext = context; + mListener = listener; + } + + public Location getCurrentLocation() { + if (!mRecordLocation) return null; + + // go in best to worst order + for (int i = 0; i < mLocationListeners.length; i++) { + Location l = mLocationListeners[i].current(); + if (l != null) return l; + } + Log.d(TAG, "No location received yet."); + return null; + } + + public void recordLocation(boolean recordLocation) { + if (mRecordLocation != recordLocation) { + mRecordLocation = recordLocation; + if (recordLocation) { + startReceivingLocationUpdates(); + } else { + stopReceivingLocationUpdates(); + } + } + } + + private void startReceivingLocationUpdates() { + if (mLocationManager == null) { + mLocationManager = (android.location.LocationManager) + mContext.getSystemService(Context.LOCATION_SERVICE); + } + if (mLocationManager != null) { + try { + mLocationManager.requestLocationUpdates( + android.location.LocationManager.NETWORK_PROVIDER, + 1000, + 0F, + mLocationListeners[1]); + } catch (SecurityException ex) { + Log.i(TAG, "fail to request location update, ignore", ex); + } catch (IllegalArgumentException ex) { + Log.d(TAG, "provider does not exist " + ex.getMessage()); + } + try { + mLocationManager.requestLocationUpdates( + android.location.LocationManager.GPS_PROVIDER, + 1000, + 0F, + mLocationListeners[0]); + if (mListener != null) mListener.showGpsOnScreenIndicator(false); + } catch (SecurityException ex) { + Log.i(TAG, "fail to request location update, ignore", ex); + } catch (IllegalArgumentException ex) { + Log.d(TAG, "provider does not exist " + ex.getMessage()); + } + Log.d(TAG, "startReceivingLocationUpdates"); + } + } + + private void stopReceivingLocationUpdates() { + if (mLocationManager != null) { + for (int i = 0; i < mLocationListeners.length; i++) { + try { + mLocationManager.removeUpdates(mLocationListeners[i]); + } catch (Exception ex) { + Log.i(TAG, "fail to remove location listners, ignore", ex); + } + } + Log.d(TAG, "stopReceivingLocationUpdates"); + } + if (mListener != null) mListener.hideGpsOnScreenIndicator(); + } + + private class LocationListener + implements android.location.LocationListener { + Location mLastLocation; + boolean mValid = false; + String mProvider; + + public LocationListener(String provider) { + mProvider = provider; + mLastLocation = new Location(mProvider); + } + + @Override + public void onLocationChanged(Location newLocation) { + if (newLocation.getLatitude() == 0.0 + && newLocation.getLongitude() == 0.0) { + // Hack to filter out 0.0,0.0 locations + return; + } + // If GPS is available before start camera, we won't get status + // update so update GPS indicator when we receive data. + if (mListener != null && mRecordLocation && + android.location.LocationManager.GPS_PROVIDER.equals(mProvider)) { + mListener.showGpsOnScreenIndicator(true); + } + if (!mValid) { + Log.d(TAG, "Got first location."); + } + mLastLocation.set(newLocation); + mValid = true; + } + + @Override + public void onProviderEnabled(String provider) { + } + + @Override + public void onProviderDisabled(String provider) { + mValid = false; + } + + @Override + public void onStatusChanged( + String provider, int status, Bundle extras) { + switch(status) { + case LocationProvider.OUT_OF_SERVICE: + case LocationProvider.TEMPORARILY_UNAVAILABLE: { + mValid = false; + if (mListener != null && mRecordLocation && + android.location.LocationManager.GPS_PROVIDER.equals(provider)) { + mListener.showGpsOnScreenIndicator(false); + } + break; + } + } + } + + public Location current() { + return mValid ? mLastLocation : null; + } + } +} diff --git a/src/com/android/camera/MediaSaveService.java b/src/com/android/camera/MediaSaveService.java new file mode 100644 index 000000000..40675b8c0 --- /dev/null +++ b/src/com/android/camera/MediaSaveService.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2013 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 android.app.Service; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Intent; +import android.location.Location; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Binder; +import android.os.IBinder; +import android.provider.MediaStore.Video; +import android.util.Log; + +import com.android.gallery3d.exif.ExifInterface; + +import java.io.File; + +/* + * Service for saving images in the background thread. + */ +public class MediaSaveService extends Service { + // The memory limit for unsaved image is 20MB. + private static final int SAVE_TASK_MEMORY_LIMIT = 20 * 1024 * 1024; + private static final String TAG = "CAM_" + MediaSaveService.class.getSimpleName(); + + private final IBinder mBinder = new LocalBinder(); + private Listener mListener; + // Memory used by the total queued save request, in bytes. + private long mMemoryUse; + + interface Listener { + public void onQueueStatus(boolean full); + } + + interface OnMediaSavedListener { + public void onMediaSaved(Uri uri); + } + + class LocalBinder extends Binder { + public MediaSaveService getService() { + return MediaSaveService.this; + } + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + @Override + public int onStartCommand(Intent intent, int flag, int startId) { + return START_STICKY; + } + + @Override + public void onDestroy() { + } + + @Override + public void onCreate() { + mMemoryUse = 0; + } + + public boolean isQueueFull() { + return (mMemoryUse >= SAVE_TASK_MEMORY_LIMIT); + } + + public void addImage(final byte[] data, String title, long date, Location loc, + int width, int height, int orientation, ExifInterface exif, + OnMediaSavedListener l, ContentResolver resolver) { + if (isQueueFull()) { + Log.e(TAG, "Cannot add image when the queue is full"); + return; + } + ImageSaveTask t = new ImageSaveTask(data, title, date, + (loc == null) ? null : new Location(loc), + width, height, orientation, exif, resolver, l); + + mMemoryUse += data.length; + if (isQueueFull()) { + onQueueFull(); + } + t.execute(); + } + + public void addImage(final byte[] data, String title, Location loc, + int width, int height, int orientation, ExifInterface exif, + OnMediaSavedListener l, ContentResolver resolver) { + addImage(data, title, System.currentTimeMillis(), loc, width, height, + orientation, exif, l, resolver); + } + + public void addVideo(String path, long duration, ContentValues values, + OnMediaSavedListener l, ContentResolver resolver) { + // We don't set a queue limit for video saving because the file + // is already in the storage. Only updating the database. + new VideoSaveTask(path, duration, values, l, resolver).execute(); + } + + public void setListener(Listener l) { + mListener = l; + if (l == null) return; + l.onQueueStatus(isQueueFull()); + } + + private void onQueueFull() { + if (mListener != null) mListener.onQueueStatus(true); + } + + private void onQueueAvailable() { + if (mListener != null) mListener.onQueueStatus(false); + } + + private class ImageSaveTask extends AsyncTask <Void, Void, Uri> { + private byte[] data; + private String title; + private long date; + private Location loc; + private int width, height; + private int orientation; + private ExifInterface exif; + private ContentResolver resolver; + private OnMediaSavedListener listener; + + public ImageSaveTask(byte[] data, String title, long date, Location loc, + int width, int height, int orientation, ExifInterface exif, + ContentResolver resolver, OnMediaSavedListener listener) { + this.data = data; + this.title = title; + this.date = date; + this.loc = loc; + this.width = width; + this.height = height; + this.orientation = orientation; + this.exif = exif; + this.resolver = resolver; + this.listener = listener; + } + + @Override + protected void onPreExecute() { + // do nothing. + } + + @Override + protected Uri doInBackground(Void... v) { + return Storage.addImage( + resolver, title, date, loc, orientation, exif, data, width, height); + } + + @Override + protected void onPostExecute(Uri uri) { + if (listener != null) listener.onMediaSaved(uri); + boolean previouslyFull = isQueueFull(); + mMemoryUse -= data.length; + if (isQueueFull() != previouslyFull) onQueueAvailable(); + } + } + + private class VideoSaveTask extends AsyncTask <Void, Void, Uri> { + private String path; + private long duration; + private ContentValues values; + private OnMediaSavedListener listener; + private ContentResolver resolver; + + public VideoSaveTask(String path, long duration, ContentValues values, + OnMediaSavedListener l, ContentResolver r) { + this.path = path; + this.duration = duration; + this.values = new ContentValues(values); + this.listener = l; + this.resolver = r; + } + + @Override + protected void onPreExecute() { + // do nothing. + } + + @Override + protected Uri doInBackground(Void... v) { + values.put(Video.Media.SIZE, new File(path).length()); + values.put(Video.Media.DURATION, duration); + Uri uri = null; + try { + Uri videoTable = Uri.parse("content://media/external/video/media"); + uri = resolver.insert(videoTable, values); + + // Rename the video file to the final name. This avoids other + // apps reading incomplete data. We need to do it after we are + // certain that the previous insert to MediaProvider is completed. + String finalName = values.getAsString( + Video.Media.DATA); + if (new File(path).renameTo(new File(finalName))) { + path = finalName; + } + + resolver.update(uri, values, null, null); + } catch (Exception e) { + // We failed to insert into the database. This can happen if + // the SD card is unmounted. + Log.e(TAG, "failed to add video to media store", e); + uri = null; + } finally { + Log.v(TAG, "Current video URI: " + uri); + } + return uri; + } + + @Override + protected void onPostExecute(Uri uri) { + if (listener != null) listener.onMediaSaved(uri); + } + } +} diff --git a/src/com/android/camera/OnClickAttr.java b/src/com/android/camera/OnClickAttr.java new file mode 100644 index 000000000..07a10635b --- /dev/null +++ b/src/com/android/camera/OnClickAttr.java @@ -0,0 +1,31 @@ +/* + * 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 java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/** + * Interface for OnClickAttr annotation. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface OnClickAttr { +} diff --git a/src/com/android/camera/OnScreenHint.java b/src/com/android/camera/OnScreenHint.java new file mode 100644 index 000000000..4d7fa7088 --- /dev/null +++ b/src/com/android/camera/OnScreenHint.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2009 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 android.content.Context; +import android.graphics.PixelFormat; +import android.os.Handler; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.TextView; + +import com.android.gallery3d.R; + +/** + * A on-screen hint is a view containing a little message for the user and will + * be shown on the screen continuously. This class helps you create and show + * those. + * + * <p> + * When the view is shown to the user, appears as a floating view over the + * application. + * <p> + * The easiest way to use this class is to call one of the static methods that + * constructs everything you need and returns a new {@code OnScreenHint} object. + */ +public class OnScreenHint { + static final String TAG = "OnScreenHint"; + + int mGravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; + int mX, mY; + float mHorizontalMargin; + float mVerticalMargin; + View mView; + View mNextView; + + private final WindowManager.LayoutParams mParams = + new WindowManager.LayoutParams(); + private final WindowManager mWM; + private final Handler mHandler = new Handler(); + + /** + * Construct an empty OnScreenHint object. + * + * @param context The context to use. Usually your + * {@link android.app.Application} or + * {@link android.app.Activity} object. + */ + private OnScreenHint(Context context) { + mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + mY = context.getResources().getDimensionPixelSize( + R.dimen.hint_y_offset); + + mParams.height = WindowManager.LayoutParams.WRAP_CONTENT; + mParams.width = WindowManager.LayoutParams.WRAP_CONTENT; + mParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + mParams.format = PixelFormat.TRANSLUCENT; + mParams.windowAnimations = R.style.Animation_OnScreenHint; + mParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; + mParams.setTitle("OnScreenHint"); + } + + /** + * Show the view on the screen. + */ + public void show() { + if (mNextView == null) { + throw new RuntimeException("View is not initialized"); + } + mHandler.post(mShow); + } + + /** + * Close the view if it's showing. + */ + public void cancel() { + mHandler.post(mHide); + } + + /** + * Make a standard hint that just contains a text view. + * + * @param context The context to use. Usually your + * {@link android.app.Application} or + * {@link android.app.Activity} object. + * @param text The text to show. Can be formatted text. + * + */ + public static OnScreenHint makeText(Context context, CharSequence text) { + OnScreenHint result = new OnScreenHint(context); + + LayoutInflater inflate = + (LayoutInflater) context.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + View v = inflate.inflate(R.layout.on_screen_hint, null); + TextView tv = (TextView) v.findViewById(R.id.message); + tv.setText(text); + + result.mNextView = v; + + return result; + } + + /** + * Update the text in a OnScreenHint that was previously created using one + * of the makeText() methods. + * @param s The new text for the OnScreenHint. + */ + public void setText(CharSequence s) { + if (mNextView == null) { + throw new RuntimeException("This OnScreenHint was not " + + "created with OnScreenHint.makeText()"); + } + TextView tv = (TextView) mNextView.findViewById(R.id.message); + if (tv == null) { + throw new RuntimeException("This OnScreenHint was not " + + "created with OnScreenHint.makeText()"); + } + tv.setText(s); + } + + private synchronized void handleShow() { + if (mView != mNextView) { + // remove the old view if necessary + handleHide(); + mView = mNextView; + final int gravity = mGravity; + mParams.gravity = gravity; + if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) + == Gravity.FILL_HORIZONTAL) { + mParams.horizontalWeight = 1.0f; + } + if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) + == Gravity.FILL_VERTICAL) { + mParams.verticalWeight = 1.0f; + } + mParams.x = mX; + mParams.y = mY; + mParams.verticalMargin = mVerticalMargin; + mParams.horizontalMargin = mHorizontalMargin; + if (mView.getParent() != null) { + mWM.removeView(mView); + } + mWM.addView(mView, mParams); + } + } + + private synchronized void handleHide() { + if (mView != null) { + // note: checking parent() just to make sure the view has + // been added... i have seen cases where we get here when + // the view isn't yet added, so let's try not to crash. + if (mView.getParent() != null) { + mWM.removeView(mView); + } + mView = null; + } + } + + private final Runnable mShow = new Runnable() { + @Override + public void run() { + handleShow(); + } + }; + + private final Runnable mHide = new Runnable() { + @Override + public void run() { + handleHide(); + } + }; +} + diff --git a/src/com/android/camera/OnScreenIndicators.java b/src/com/android/camera/OnScreenIndicators.java new file mode 100644 index 000000000..77c8fafc0 --- /dev/null +++ b/src/com/android/camera/OnScreenIndicators.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2013 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 android.content.Context; +import android.content.res.TypedArray; +import android.hardware.Camera; +import android.hardware.Camera.Parameters; +import android.view.View; +import android.widget.ImageView; + +import com.android.gallery3d.R; + +/** + * The on-screen indicators of the pie menu button. They show the camera + * settings in the viewfinder. + */ +public class OnScreenIndicators { + private final int[] mWBArray; + private final View mOnScreenIndicators; + private final ImageView mExposureIndicator; + private final ImageView mFlashIndicator; + private final ImageView mSceneIndicator; + private final ImageView mLocationIndicator; + private final ImageView mTimerIndicator; + private final ImageView mWBIndicator; + + public OnScreenIndicators(Context ctx, View onScreenIndicatorsView) { + TypedArray iconIds = ctx.getResources().obtainTypedArray( + R.array.camera_wb_indicators); + final int n = iconIds.length(); + mWBArray = new int[n]; + for (int i = 0; i < n; i++) { + mWBArray[i] = iconIds.getResourceId(i, R.drawable.ic_indicator_wb_off); + } + mOnScreenIndicators = onScreenIndicatorsView; + mExposureIndicator = (ImageView) onScreenIndicatorsView.findViewById( + R.id.menu_exposure_indicator); + mFlashIndicator = (ImageView) onScreenIndicatorsView.findViewById( + R.id.menu_flash_indicator); + mSceneIndicator = (ImageView) onScreenIndicatorsView.findViewById( + R.id.menu_scenemode_indicator); + mLocationIndicator = (ImageView) onScreenIndicatorsView.findViewById( + R.id.menu_location_indicator); + mTimerIndicator = (ImageView) onScreenIndicatorsView.findViewById( + R.id.menu_timer_indicator); + mWBIndicator = (ImageView) onScreenIndicatorsView.findViewById( + R.id.menu_wb_indicator); + } + + /** + * Resets all indicators to show the default values. + */ + public void resetToDefault() { + updateExposureOnScreenIndicator(0); + updateFlashOnScreenIndicator(Parameters.FLASH_MODE_OFF); + updateSceneOnScreenIndicator(Parameters.SCENE_MODE_AUTO); + updateWBIndicator(2); + updateTimerIndicator(false); + updateLocationIndicator(false); + } + + /** + * Sets the exposure indicator using exposure compensations step rounding. + */ + public void updateExposureOnScreenIndicator(Camera.Parameters params, int value) { + if (mExposureIndicator == null) { + return; + } + float step = params.getExposureCompensationStep(); + value = Math.round(value * step); + updateExposureOnScreenIndicator(value); + } + + /** + * Set the exposure indicator to the given value. + * + * @param value Value between -3 and 3. If outside this range, 0 is used by + * default. + */ + public void updateExposureOnScreenIndicator(int value) { + int id = 0; + switch(value) { + case -3: + id = R.drawable.ic_indicator_ev_n3; + break; + case -2: + id = R.drawable.ic_indicator_ev_n2; + break; + case -1: + id = R.drawable.ic_indicator_ev_n1; + break; + case 0: + id = R.drawable.ic_indicator_ev_0; + break; + case 1: + id = R.drawable.ic_indicator_ev_p1; + break; + case 2: + id = R.drawable.ic_indicator_ev_p2; + break; + case 3: + id = R.drawable.ic_indicator_ev_p3; + break; + } + mExposureIndicator.setImageResource(id); + } + + public void updateWBIndicator(int wbIndex) { + if (mWBIndicator == null) return; + mWBIndicator.setImageResource(mWBArray[wbIndex]); + } + + public void updateTimerIndicator(boolean on) { + if (mTimerIndicator == null) return; + mTimerIndicator.setImageResource(on ? R.drawable.ic_indicator_timer_on + : R.drawable.ic_indicator_timer_off); + } + + public void updateLocationIndicator(boolean on) { + if (mLocationIndicator == null) return; + mLocationIndicator.setImageResource(on ? R.drawable.ic_indicator_loc_on + : R.drawable.ic_indicator_loc_off); + } + + /** + * Set the flash indicator to the given value. + * + * @param value One of Parameters.FLASH_MODE_OFF, + * Parameters.FLASH_MODE_AUTO, Parameters.FLASH_MODE_ON. + */ + public void updateFlashOnScreenIndicator(String value) { + if (mFlashIndicator == null) { + return; + } + if (value == null || Parameters.FLASH_MODE_OFF.equals(value)) { + mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_off); + } else { + if (Parameters.FLASH_MODE_AUTO.equals(value)) { + mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_auto); + } else if (Parameters.FLASH_MODE_ON.equals(value) + || Parameters.FLASH_MODE_TORCH.equals(value)) { + mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_on); + } else { + mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_off); + } + } + } + + /** + * Set the scene indicator depending on the given scene mode. + * + * @param value the current Parameters.SCENE_MODE_* value. + */ + public void updateSceneOnScreenIndicator(String value) { + if (mSceneIndicator == null) { + return; + } + if ((value == null) || Parameters.SCENE_MODE_AUTO.equals(value)) { + mSceneIndicator.setImageResource(R.drawable.ic_indicator_sce_off); + } else if (Parameters.SCENE_MODE_HDR.equals(value)) { + mSceneIndicator.setImageResource(R.drawable.ic_indicator_sce_hdr); + } else { + mSceneIndicator.setImageResource(R.drawable.ic_indicator_sce_on); + } + } + + /** + * Sets the visibility of all indicators. + * + * @param visibility View.VISIBLE, View.GONE etc. + */ + public void setVisibility(int visibility) { + mOnScreenIndicators.setVisibility(visibility); + } +} diff --git a/src/com/android/camera/PhotoController.java b/src/com/android/camera/PhotoController.java new file mode 100644 index 000000000..bc824d917 --- /dev/null +++ b/src/com/android/camera/PhotoController.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2013 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 android.view.SurfaceHolder; +import android.view.View; + +import com.android.camera.ShutterButton.OnShutterButtonListener; + + +public interface PhotoController extends OnShutterButtonListener { + + public static final int PREVIEW_STOPPED = 0; + public static final int IDLE = 1; // preview is active + // Focus is in progress. The exact focus state is in Focus.java. + public static final int FOCUSING = 2; + public static final int SNAPSHOT_IN_PROGRESS = 3; + // Switching between cameras. + public static final int SWITCHING_CAMERA = 4; + + // returns the actual set zoom value + public int onZoomChanged(int requestedZoom); + + public boolean isImageCaptureIntent(); + + public boolean isCameraIdle(); + + public void onCaptureDone(); + + public void onCaptureCancelled(); + + public void onCaptureRetake(); + + public void cancelAutoFocus(); + + public void stopPreview(); + + public int getCameraState(); + + public void onSingleTapUp(View view, int x, int y); + + public void onSurfaceCreated(SurfaceHolder holder); + + public void onCountDownFinished(); + + public void onScreenSizeChanged(int width, int height, int previewWidth, int previewHeight); + + public void updateCameraOrientation(); + + public void enableRecordingLocation(boolean enable); +} diff --git a/src/com/android/camera/PhotoMenu.java b/src/com/android/camera/PhotoMenu.java new file mode 100644 index 000000000..6c1e2d085 --- /dev/null +++ b/src/com/android/camera/PhotoMenu.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2012 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 android.content.res.Resources; +import android.hardware.Camera.Parameters; + +import com.android.camera.ui.AbstractSettingPopup; +import com.android.camera.ui.CountdownTimerPopup; +import com.android.camera.ui.ListPrefSettingPopup; +import com.android.camera.ui.PieItem; +import com.android.camera.ui.PieItem.OnClickListener; +import com.android.camera.ui.PieRenderer; +import com.android.gallery3d.R; + +import java.util.Locale; + +public class PhotoMenu extends PieController + implements CountdownTimerPopup.Listener, + ListPrefSettingPopup.Listener { + private static String TAG = "CAM_photomenu"; + + private final String mSettingOff; + + private PhotoUI mUI; + private AbstractSettingPopup mPopup; + private CameraActivity mActivity; + + public PhotoMenu(CameraActivity activity, PhotoUI ui, PieRenderer pie) { + super(activity, pie); + mUI = ui; + mSettingOff = activity.getString(R.string.setting_off_value); + mActivity = activity; + } + + public void initialize(PreferenceGroup group) { + super.initialize(group); + mPopup = null; + PieItem item = null; + final Resources res = mActivity.getResources(); + Locale locale = res.getConfiguration().locale; + // the order is from left to right in the menu + + // hdr + if (group.findPreference(CameraSettings.KEY_CAMERA_HDR) != null) { + item = makeSwitchItem(CameraSettings.KEY_CAMERA_HDR, true); + mRenderer.addItem(item); + } + // exposure compensation + if (group.findPreference(CameraSettings.KEY_EXPOSURE) != null) { + item = makeItem(CameraSettings.KEY_EXPOSURE); + item.setLabel(res.getString(R.string.pref_exposure_label)); + mRenderer.addItem(item); + } + // more settings + PieItem more = makeItem(R.drawable.ic_settings_holo_light); + more.setLabel(res.getString(R.string.camera_menu_more_label)); + mRenderer.addItem(more); + // flash + if (group.findPreference(CameraSettings.KEY_FLASH_MODE) != null) { + item = makeItem(CameraSettings.KEY_FLASH_MODE); + item.setLabel(res.getString(R.string.pref_camera_flashmode_label)); + mRenderer.addItem(item); + } + // camera switcher + if (group.findPreference(CameraSettings.KEY_CAMERA_ID) != null) { + item = makeSwitchItem(CameraSettings.KEY_CAMERA_ID, false); + final PieItem fitem = item; + item.setOnClickListener(new OnClickListener() { + @Override + public void onClick(PieItem item) { + // Find the index of next camera. + ListPreference pref = mPreferenceGroup + .findPreference(CameraSettings.KEY_CAMERA_ID); + if (pref != null) { + int index = pref.findIndexOfValue(pref.getValue()); + CharSequence[] values = pref.getEntryValues(); + index = (index + 1) % values.length; + pref.setValueIndex(index); + mListener.onCameraPickerClicked(index); + } + updateItem(fitem, CameraSettings.KEY_CAMERA_ID); + } + }); + mRenderer.addItem(item); + } + // location + if (group.findPreference(CameraSettings.KEY_RECORD_LOCATION) != null) { + item = makeSwitchItem(CameraSettings.KEY_RECORD_LOCATION, true); + more.addItem(item); + if (mActivity.isSecureCamera()) { + // Prevent location preference from getting changed in secure camera mode + item.setEnabled(false); + } + } + // countdown timer + final ListPreference ctpref = group.findPreference(CameraSettings.KEY_TIMER); + final ListPreference beeppref = group.findPreference(CameraSettings.KEY_TIMER_SOUND_EFFECTS); + item = makeItem(R.drawable.ic_timer); + item.setLabel(res.getString(R.string.pref_camera_timer_title).toUpperCase(locale)); + item.setOnClickListener(new OnClickListener() { + @Override + public void onClick(PieItem item) { + CountdownTimerPopup timerPopup = (CountdownTimerPopup) mActivity.getLayoutInflater().inflate( + R.layout.countdown_setting_popup, null, false); + timerPopup.initialize(ctpref, beeppref); + timerPopup.setSettingChangedListener(PhotoMenu.this); + mUI.dismissPopup(); + mPopup = timerPopup; + mUI.showPopup(mPopup); + } + }); + more.addItem(item); + // image size + item = makeItem(R.drawable.ic_imagesize); + final ListPreference sizePref = group.findPreference(CameraSettings.KEY_PICTURE_SIZE); + item.setLabel(res.getString(R.string.pref_camera_picturesize_title).toUpperCase(locale)); + item.setOnClickListener(new OnClickListener() { + @Override + public void onClick(PieItem item) { + ListPrefSettingPopup popup = (ListPrefSettingPopup) mActivity.getLayoutInflater().inflate( + R.layout.list_pref_setting_popup, null, false); + popup.initialize(sizePref); + popup.setSettingChangedListener(PhotoMenu.this); + mUI.dismissPopup(); + mPopup = popup; + mUI.showPopup(mPopup); + } + }); + more.addItem(item); + // white balance + if (group.findPreference(CameraSettings.KEY_WHITE_BALANCE) != null) { + item = makeItem(CameraSettings.KEY_WHITE_BALANCE); + item.setLabel(res.getString(R.string.pref_camera_whitebalance_label)); + more.addItem(item); + } + // scene mode + if (group.findPreference(CameraSettings.KEY_SCENE_MODE) != null) { + IconListPreference pref = (IconListPreference) group.findPreference( + CameraSettings.KEY_SCENE_MODE); + pref.setUseSingleIcon(true); + item = makeItem(CameraSettings.KEY_SCENE_MODE); + more.addItem(item); + } + } + + @Override + // Hit when an item in a popup gets selected + public void onListPrefChanged(ListPreference pref) { + if (mPopup != null) { + mUI.dismissPopup(); + } + onSettingChanged(pref); + } + + public void popupDismissed() { + if (mPopup != null) { + mPopup = null; + } + } + + // Return true if the preference has the specified key but not the value. + private static boolean notSame(ListPreference pref, String key, String value) { + return (key.equals(pref.getKey()) && !value.equals(pref.getValue())); + } + + private void setPreference(String key, String value) { + ListPreference pref = mPreferenceGroup.findPreference(key); + if (pref != null && !value.equals(pref.getValue())) { + pref.setValue(value); + reloadPreferences(); + } + } + + @Override + public void onSettingChanged(ListPreference pref) { + // Reset the scene mode if HDR is set to on. Reset HDR if scene mode is + // set to non-auto. + if (notSame(pref, CameraSettings.KEY_CAMERA_HDR, mSettingOff)) { + setPreference(CameraSettings.KEY_SCENE_MODE, Parameters.SCENE_MODE_AUTO); + } else if (notSame(pref, CameraSettings.KEY_SCENE_MODE, Parameters.SCENE_MODE_AUTO)) { + setPreference(CameraSettings.KEY_CAMERA_HDR, mSettingOff); + } + super.onSettingChanged(pref); + } +} diff --git a/src/com/android/camera/PhotoModule.java b/src/com/android/camera/PhotoModule.java new file mode 100644 index 000000000..c65a49ef9 --- /dev/null +++ b/src/com/android/camera/PhotoModule.java @@ -0,0 +1,2006 @@ +/* + * Copyright (C) 2012 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 android.annotation.TargetApi; +import android.app.Activity; +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences.Editor; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.SurfaceTexture; +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.Size; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.location.Location; +import android.media.CameraProfile; +import android.net.Uri; +import android.os.Bundle; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.MessageQueue; +import android.os.SystemClock; +import android.provider.MediaStore; +import android.util.Log; +import android.view.KeyEvent; +import android.view.OrientationEventListener; +import android.view.SurfaceHolder; +import android.view.View; +import android.view.WindowManager; + +import com.android.camera.CameraManager.CameraAFCallback; +import com.android.camera.CameraManager.CameraAFMoveCallback; +import com.android.camera.CameraManager.CameraPictureCallback; +import com.android.camera.CameraManager.CameraProxy; +import com.android.camera.CameraManager.CameraShutterCallback; +import com.android.camera.ui.CountDownView.OnCountDownFinishedListener; +import com.android.camera.ui.PopupManager; +import com.android.camera.ui.RotateTextToast; +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.exif.ExifInterface; +import com.android.gallery3d.exif.ExifTag; +import com.android.gallery3d.exif.Rational; +import com.android.gallery3d.filtershow.crop.CropActivity; +import com.android.gallery3d.filtershow.crop.CropExtras; +import com.android.gallery3d.util.UsageStatistics; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Formatter; +import java.util.List; + +public class PhotoModule + implements CameraModule, + PhotoController, + FocusOverlayManager.Listener, + CameraPreference.OnPreferenceChangedListener, + ShutterButton.OnShutterButtonListener, + MediaSaveService.Listener, + OnCountDownFinishedListener, + SensorEventListener { + + private static final String TAG = "CAM_PhotoModule"; + + // We number the request code from 1000 to avoid collision with Gallery. + private static final int REQUEST_CROP = 1000; + + private static final int SETUP_PREVIEW = 1; + private static final int FIRST_TIME_INIT = 2; + private static final int CLEAR_SCREEN_DELAY = 3; + private static final int SET_CAMERA_PARAMETERS_WHEN_IDLE = 4; + private static final int CHECK_DISPLAY_ROTATION = 5; + private static final int SHOW_TAP_TO_FOCUS_TOAST = 6; + private static final int SWITCH_CAMERA = 7; + private static final int SWITCH_CAMERA_START_ANIMATION = 8; + private static final int CAMERA_OPEN_DONE = 9; + private static final int START_PREVIEW_DONE = 10; + private static final int OPEN_CAMERA_FAIL = 11; + private static final int CAMERA_DISABLED = 12; + private static final int CAPTURE_ANIMATION_DONE = 13; + + // The subset of parameters we need to update in setCameraParameters(). + private static final int UPDATE_PARAM_INITIALIZE = 1; + private static final int UPDATE_PARAM_ZOOM = 2; + private static final int UPDATE_PARAM_PREFERENCE = 4; + private static final int UPDATE_PARAM_ALL = -1; + + // This is the timeout to keep the camera in onPause for the first time + // after screen on if the activity is started from secure lock screen. + private static final int KEEP_CAMERA_TIMEOUT = 1000; // ms + + // copied from Camera hierarchy + private CameraActivity mActivity; + private CameraProxy mCameraDevice; + private int mCameraId; + private Parameters mParameters; + private boolean mPaused; + + private PhotoUI mUI; + + // The activity is going to switch to the specified camera id. This is + // needed because texture copy is done in GL thread. -1 means camera is not + // switching. + protected int mPendingSwitchCameraId = -1; + private boolean mOpenCameraFail; + private boolean mCameraDisabled; + + // When setCameraParametersWhenIdle() is called, we accumulate the subsets + // needed to be updated in mUpdateSet. + private int mUpdateSet; + + private static final int SCREEN_DELAY = 2 * 60 * 1000; + + private int mZoomValue; // The current zoom value. + + private Parameters mInitialParams; + private boolean mFocusAreaSupported; + private boolean mMeteringAreaSupported; + private boolean mAeLockSupported; + private boolean mAwbLockSupported; + private boolean mContinousFocusSupported; + + // The degrees of the device rotated clockwise from its natural orientation. + private int mOrientation = OrientationEventListener.ORIENTATION_UNKNOWN; + private ComboPreferences mPreferences; + + private static final String sTempCropFilename = "crop-temp"; + + private ContentProviderClient mMediaProviderClient; + private boolean mFaceDetectionStarted = false; + + // mCropValue and mSaveUri are used only if isImageCaptureIntent() is true. + private String mCropValue; + private Uri mSaveUri; + + // We use a queue to generated names of the images to be used later + // when the image is ready to be saved. + private NamedImages mNamedImages; + + private Runnable mDoSnapRunnable = new Runnable() { + @Override + public void run() { + onShutterButtonClick(); + } + }; + + private Runnable mFlashRunnable = new Runnable() { + @Override + public void run() { + animateFlash(); + } + }; + + private final StringBuilder mBuilder = new StringBuilder(); + private final Formatter mFormatter = new Formatter(mBuilder); + private final Object[] mFormatterArgs = new Object[1]; + + /** + * An unpublished intent flag requesting to return as soon as capturing + * is completed. + * + * TODO: consider publishing by moving into MediaStore. + */ + private static final String EXTRA_QUICK_CAPTURE = + "android.intent.extra.quickCapture"; + + // The display rotation in degrees. This is only valid when mCameraState is + // not PREVIEW_STOPPED. + private int mDisplayRotation; + // The value for android.hardware.Camera.setDisplayOrientation. + private int mCameraDisplayOrientation; + // The value for UI components like indicators. + private int mDisplayOrientation; + // The value for android.hardware.Camera.Parameters.setRotation. + private int mJpegRotation; + private boolean mFirstTimeInitialized; + private boolean mIsImageCaptureIntent; + + private int mCameraState = PREVIEW_STOPPED; + private boolean mSnapshotOnIdle = false; + + private ContentResolver mContentResolver; + + private LocationManager mLocationManager; + + private final PostViewPictureCallback mPostViewPictureCallback = + new PostViewPictureCallback(); + private final RawPictureCallback mRawPictureCallback = + new RawPictureCallback(); + private final AutoFocusCallback mAutoFocusCallback = + new AutoFocusCallback(); + private final Object mAutoFocusMoveCallback = + ApiHelper.HAS_AUTO_FOCUS_MOVE_CALLBACK + ? new AutoFocusMoveCallback() + : null; + + private final CameraErrorCallback mErrorCallback = new CameraErrorCallback(); + + private long mFocusStartTime; + private long mShutterCallbackTime; + private long mPostViewPictureCallbackTime; + private long mRawPictureCallbackTime; + private long mJpegPictureCallbackTime; + private long mOnResumeTime; + private byte[] mJpegImageData; + + // These latency time are for the CameraLatency test. + public long mAutoFocusTime; + public long mShutterLag; + public long mShutterToPictureDisplayedTime; + public long mPictureDisplayedToJpegCallbackTime; + public long mJpegCallbackFinishTime; + public long mCaptureStartTime; + + // This handles everything about focus. + private FocusOverlayManager mFocusManager; + + private String mSceneMode; + + private final Handler mHandler = new MainHandler(); + private PreferenceGroup mPreferenceGroup; + + private boolean mQuickCapture; + private SensorManager mSensorManager; + private float[] mGData = new float[3]; + private float[] mMData = new float[3]; + private float[] mR = new float[16]; + private int mHeading = -1; + + CameraStartUpThread mCameraStartUpThread; + ConditionVariable mStartPreviewPrerequisiteReady = new ConditionVariable(); + + private MediaSaveService.OnMediaSavedListener mOnMediaSavedListener = + new MediaSaveService.OnMediaSavedListener() { + @Override + public void onMediaSaved(Uri uri) { + if (uri != null) { + mActivity.notifyNewMedia(uri); + } + } + }; + + // The purpose is not to block the main thread in onCreate and onResume. + private class CameraStartUpThread extends Thread { + private volatile boolean mCancelled; + + public void cancel() { + mCancelled = true; + interrupt(); + } + + public boolean isCanceled() { + return mCancelled; + } + + @Override + public void run() { + try { + // We need to check whether the activity is paused before long + // operations to ensure that onPause() can be done ASAP. + if (mCancelled) return; + mCameraDevice = Util.openCamera(mActivity, mCameraId); + mParameters = mCameraDevice.getParameters(); + // Wait until all the initialization needed by startPreview are + // done. + mStartPreviewPrerequisiteReady.block(); + + initializeCapabilities(); + if (mFocusManager == null) initializeFocusManager(); + if (mCancelled) return; + setCameraParameters(UPDATE_PARAM_ALL); + mHandler.sendEmptyMessage(CAMERA_OPEN_DONE); + if (mCancelled) return; + startPreview(); + mHandler.sendEmptyMessage(START_PREVIEW_DONE); + mOnResumeTime = SystemClock.uptimeMillis(); + mHandler.sendEmptyMessage(CHECK_DISPLAY_ROTATION); + } catch (CameraHardwareException e) { + mHandler.sendEmptyMessage(OPEN_CAMERA_FAIL); + } catch (CameraDisabledException e) { + mHandler.sendEmptyMessage(CAMERA_DISABLED); + } + } + } + + /** + * This Handler is used to post message back onto the main thread of the + * application + */ + private class MainHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case SETUP_PREVIEW: { + setupPreview(); + break; + } + + case CLEAR_SCREEN_DELAY: { + mActivity.getWindow().clearFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + break; + } + + case FIRST_TIME_INIT: { + initializeFirstTime(); + break; + } + + case SET_CAMERA_PARAMETERS_WHEN_IDLE: { + setCameraParametersWhenIdle(0); + break; + } + + case CHECK_DISPLAY_ROTATION: { + // Set the display orientation if display rotation has changed. + // Sometimes this happens when the device is held upside + // down and camera app is opened. Rotation animation will + // take some time and the rotation value we have got may be + // wrong. Framework does not have a callback for this now. + if (Util.getDisplayRotation(mActivity) != mDisplayRotation) { + setDisplayOrientation(); + } + if (SystemClock.uptimeMillis() - mOnResumeTime < 5000) { + mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100); + } + break; + } + + case SHOW_TAP_TO_FOCUS_TOAST: { + showTapToFocusToast(); + break; + } + + case SWITCH_CAMERA: { + switchCamera(); + break; + } + + case SWITCH_CAMERA_START_ANIMATION: { + // TODO: Need to revisit + // ((CameraScreenNail) mActivity.mCameraScreenNail).animateSwitchCamera(); + break; + } + + case CAMERA_OPEN_DONE: { + onCameraOpened(); + break; + } + + case START_PREVIEW_DONE: { + onPreviewStarted(); + break; + } + + case OPEN_CAMERA_FAIL: { + mCameraStartUpThread = null; + mOpenCameraFail = true; + Util.showErrorAndFinish(mActivity, + R.string.cannot_connect_camera); + break; + } + + case CAMERA_DISABLED: { + mCameraStartUpThread = null; + mCameraDisabled = true; + Util.showErrorAndFinish(mActivity, + R.string.camera_disabled); + break; + } + case CAPTURE_ANIMATION_DONE: { + mUI.enablePreviewThumb(false); + break; + } + } + } + } + + @Override + public void init(CameraActivity activity, View parent) { + mActivity = activity; + mUI = new PhotoUI(activity, this, parent); + mPreferences = new ComboPreferences(mActivity); + CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal()); + mCameraId = getPreferredCameraId(mPreferences); + + mContentResolver = mActivity.getContentResolver(); + + // To reduce startup time, open the camera and start the preview in + // another thread. + mCameraStartUpThread = new CameraStartUpThread(); + mCameraStartUpThread.start(); + + // Surface texture is from camera screen nail and startPreview needs it. + // This must be done before startPreview. + mIsImageCaptureIntent = isImageCaptureIntent(); + + mPreferences.setLocalId(mActivity, mCameraId); + CameraSettings.upgradeLocalPreferences(mPreferences.getLocal()); + // we need to reset exposure for the preview + resetExposureCompensation(); + // Starting the preview needs preferences, camera screen nail, and + // focus area indicator. + mStartPreviewPrerequisiteReady.open(); + + initializeControlByIntent(); + mQuickCapture = mActivity.getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false); + mLocationManager = new LocationManager(mActivity, mUI); + mSensorManager = (SensorManager)(mActivity.getSystemService(Context.SENSOR_SERVICE)); + } + + private void initializeControlByIntent() { + mUI.initializeControlByIntent(); + if (mIsImageCaptureIntent) { + setupCaptureParams(); + } + } + + private void onPreviewStarted() { + mCameraStartUpThread = null; + setCameraState(IDLE); + startFaceDetection(); + locationFirstRun(); + } + + // Prompt the user to pick to record location for the very first run of + // camera only + private void locationFirstRun() { + if (RecordLocationPreference.isSet(mPreferences)) { + return; + } + if (mActivity.isSecureCamera()) return; + // Check if the back camera exists + int backCameraId = CameraHolder.instance().getBackCameraId(); + if (backCameraId == -1) { + // If there is no back camera, do not show the prompt. + return; + } + mUI.showLocationDialog(); + } + + public void enableRecordingLocation(boolean enable) { + setLocationPreference(enable ? RecordLocationPreference.VALUE_ON + : RecordLocationPreference.VALUE_OFF); + } + + private void setLocationPreference(String value) { + mPreferences.edit() + .putString(CameraSettings.KEY_RECORD_LOCATION, value) + .apply(); + // TODO: Fix this to use the actual onSharedPreferencesChanged listener + // instead of invoking manually + onSharedPreferenceChanged(); + } + + private void onCameraOpened() { + View root = mUI.getRootView(); + // These depend on camera parameters. + + int width = root.getWidth(); + int height = root.getHeight(); + mFocusManager.setPreviewSize(width, height); + openCameraCommon(); + } + + private void switchCamera() { + if (mPaused) return; + + Log.v(TAG, "Start to switch camera. id=" + mPendingSwitchCameraId); + mCameraId = mPendingSwitchCameraId; + mPendingSwitchCameraId = -1; + setCameraId(mCameraId); + + // from onPause + closeCamera(); + mUI.collapseCameraControls(); + mUI.clearFaces(); + if (mFocusManager != null) mFocusManager.removeMessages(); + + // Restart the camera and initialize the UI. From onCreate. + mPreferences.setLocalId(mActivity, mCameraId); + CameraSettings.upgradeLocalPreferences(mPreferences.getLocal()); + try { + mCameraDevice = Util.openCamera(mActivity, mCameraId); + mParameters = mCameraDevice.getParameters(); + } catch (CameraHardwareException e) { + Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera); + return; + } catch (CameraDisabledException e) { + Util.showErrorAndFinish(mActivity, R.string.camera_disabled); + return; + } + initializeCapabilities(); + CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId]; + boolean mirror = (info.facing == CameraInfo.CAMERA_FACING_FRONT); + mFocusManager.setMirror(mirror); + mFocusManager.setParameters(mInitialParams); + setupPreview(); + + // reset zoom value index + mZoomValue = 0; + openCameraCommon(); + + if (ApiHelper.HAS_SURFACE_TEXTURE) { + // Start switch camera animation. Post a message because + // onFrameAvailable from the old camera may already exist. + mHandler.sendEmptyMessage(SWITCH_CAMERA_START_ANIMATION); + } + } + + protected void setCameraId(int cameraId) { + ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID); + pref.setValue("" + cameraId); + } + + // either open a new camera or switch cameras + private void openCameraCommon() { + loadCameraPreferences(); + + mUI.onCameraOpened(mPreferenceGroup, mPreferences, mParameters, this); + updateSceneMode(); + showTapToFocusToastIfNeeded(); + + + } + + public void onScreenSizeChanged(int width, int height, int previewWidth, int previewHeight) { + if (mFocusManager != null) mFocusManager.setPreviewSize(width, height); + } + + private void resetExposureCompensation() { + String value = mPreferences.getString(CameraSettings.KEY_EXPOSURE, + CameraSettings.EXPOSURE_DEFAULT_VALUE); + if (!CameraSettings.EXPOSURE_DEFAULT_VALUE.equals(value)) { + Editor editor = mPreferences.edit(); + editor.putString(CameraSettings.KEY_EXPOSURE, "0"); + editor.apply(); + } + } + + private void keepMediaProviderInstance() { + // We want to keep a reference to MediaProvider in camera's lifecycle. + // TODO: Utilize mMediaProviderClient instance to replace + // ContentResolver calls. + if (mMediaProviderClient == null) { + mMediaProviderClient = mContentResolver + .acquireContentProviderClient(MediaStore.AUTHORITY); + } + } + + // Snapshots can only be taken after this is called. It should be called + // once only. We could have done these things in onCreate() but we want to + // make preview screen appear as soon as possible. + private void initializeFirstTime() { + if (mFirstTimeInitialized) return; + + // Initialize location service. + boolean recordLocation = RecordLocationPreference.get( + mPreferences, mContentResolver); + mLocationManager.recordLocation(recordLocation); + + keepMediaProviderInstance(); + + mUI.initializeFirstTime(); + MediaSaveService s = mActivity.getMediaSaveService(); + // We set the listener only when both service and shutterbutton + // are initialized. + if (s != null) { + s.setListener(this); + } + + mNamedImages = new NamedImages(); + + mFirstTimeInitialized = true; + addIdleHandler(); + + mActivity.updateStorageSpaceAndHint(); + } + + // If the activity is paused and resumed, this method will be called in + // onResume. + private void initializeSecondTime() { + // Start location update if needed. + boolean recordLocation = RecordLocationPreference.get( + mPreferences, mContentResolver); + mLocationManager.recordLocation(recordLocation); + MediaSaveService s = mActivity.getMediaSaveService(); + if (s != null) { + s.setListener(this); + } + mNamedImages = new NamedImages(); + mUI.initializeSecondTime(mParameters); + keepMediaProviderInstance(); + } + + @Override + public void onSurfaceCreated(SurfaceHolder holder) { + // Do not access the camera if camera start up thread is not finished. + if (mCameraDevice == null || mCameraStartUpThread != null) + return; + + mCameraDevice.setPreviewDisplay(holder); + // This happens when onConfigurationChanged arrives, surface has been + // destroyed, and there is no onFullScreenChanged. + if (mCameraState == PREVIEW_STOPPED) { + setupPreview(); + } + } + + private void showTapToFocusToastIfNeeded() { + // Show the tap to focus toast if this is the first start. + if (mFocusAreaSupported && + mPreferences.getBoolean(CameraSettings.KEY_CAMERA_FIRST_USE_HINT_SHOWN, true)) { + // Delay the toast for one second to wait for orientation. + mHandler.sendEmptyMessageDelayed(SHOW_TAP_TO_FOCUS_TOAST, 1000); + } + } + + private void addIdleHandler() { + MessageQueue queue = Looper.myQueue(); + queue.addIdleHandler(new MessageQueue.IdleHandler() { + @Override + public boolean queueIdle() { + Storage.ensureOSXCompatible(); + return false; + } + }); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + @Override + public void startFaceDetection() { + if (!ApiHelper.HAS_FACE_DETECTION) return; + if (mFaceDetectionStarted) return; + if (mParameters.getMaxNumDetectedFaces() > 0) { + mFaceDetectionStarted = true; + CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId]; + mUI.onStartFaceDetection(mDisplayOrientation, + (info.facing == CameraInfo.CAMERA_FACING_FRONT)); + mCameraDevice.setFaceDetectionCallback(mHandler, mUI); + mCameraDevice.startFaceDetection(); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + @Override + public void stopFaceDetection() { + if (!ApiHelper.HAS_FACE_DETECTION) return; + if (!mFaceDetectionStarted) return; + if (mParameters.getMaxNumDetectedFaces() > 0) { + mFaceDetectionStarted = false; + mCameraDevice.setFaceDetectionCallback(null, null); + mCameraDevice.stopFaceDetection(); + mUI.clearFaces(); + } + } + + private final class ShutterCallback + implements CameraShutterCallback { + + private boolean mAnimateFlash; + + public ShutterCallback(boolean animateFlash) { + mAnimateFlash = animateFlash; + } + + @Override + public void onShutter(CameraProxy camera) { + mShutterCallbackTime = System.currentTimeMillis(); + mShutterLag = mShutterCallbackTime - mCaptureStartTime; + Log.v(TAG, "mShutterLag = " + mShutterLag + "ms"); + if (mAnimateFlash) { + mActivity.runOnUiThread(mFlashRunnable); + } + } + } + + private final class PostViewPictureCallback + implements CameraPictureCallback { + @Override + public void onPictureTaken(byte [] data, CameraProxy camera) { + mPostViewPictureCallbackTime = System.currentTimeMillis(); + Log.v(TAG, "mShutterToPostViewCallbackTime = " + + (mPostViewPictureCallbackTime - mShutterCallbackTime) + + "ms"); + } + } + + private final class RawPictureCallback + implements CameraPictureCallback { + @Override + public void onPictureTaken(byte [] rawData, CameraProxy camera) { + mRawPictureCallbackTime = System.currentTimeMillis(); + Log.v(TAG, "mShutterToRawCallbackTime = " + + (mRawPictureCallbackTime - mShutterCallbackTime) + "ms"); + } + } + + private final class JpegPictureCallback + implements CameraPictureCallback { + Location mLocation; + + public JpegPictureCallback(Location loc) { + mLocation = loc; + } + + @Override + public void onPictureTaken(final byte [] jpegData, CameraProxy camera) { + if (mPaused) { + return; + } + //TODO: We should show the picture taken rather than frozen preview here + if (mIsImageCaptureIntent) { + stopPreview(); + } + if (mSceneMode == Util.SCENE_MODE_HDR) { + mUI.showSwitcher(); + mUI.setSwipingEnabled(true); + } + + mJpegPictureCallbackTime = System.currentTimeMillis(); + // If postview callback has arrived, the captured image is displayed + // in postview callback. If not, the captured image is displayed in + // raw picture callback. + if (mPostViewPictureCallbackTime != 0) { + mShutterToPictureDisplayedTime = + mPostViewPictureCallbackTime - mShutterCallbackTime; + mPictureDisplayedToJpegCallbackTime = + mJpegPictureCallbackTime - mPostViewPictureCallbackTime; + } else { + mShutterToPictureDisplayedTime = + mRawPictureCallbackTime - mShutterCallbackTime; + mPictureDisplayedToJpegCallbackTime = + mJpegPictureCallbackTime - mRawPictureCallbackTime; + } + Log.v(TAG, "mPictureDisplayedToJpegCallbackTime = " + + mPictureDisplayedToJpegCallbackTime + "ms"); + + /*TODO: + // Only animate when in full screen capture mode + // i.e. If monkey/a user swipes to the gallery during picture taking, + // don't show animation + if (ApiHelper.HAS_SURFACE_TEXTURE && !mIsImageCaptureIntent + && mActivity.mShowCameraAppView) { + // Finish capture animation + mHandler.removeMessages(CAPTURE_ANIMATION_DONE); + ((CameraScreenNail) mActivity.mCameraScreenNail).animateSlide(); + mHandler.sendEmptyMessageDelayed(CAPTURE_ANIMATION_DONE, + CaptureAnimManager.getAnimationDuration()); + } */ + mFocusManager.updateFocusUI(); // Ensure focus indicator is hidden. + if (!mIsImageCaptureIntent) { + if (ApiHelper.CAN_START_PREVIEW_IN_JPEG_CALLBACK) { + setupPreview(); + } else { + // Camera HAL of some devices have a bug. Starting preview + // immediately after taking a picture will fail. Wait some + // time before starting the preview. + mHandler.sendEmptyMessageDelayed(SETUP_PREVIEW, 300); + } + } + + if (!mIsImageCaptureIntent) { + // Calculate the width and the height of the jpeg. + Size s = mParameters.getPictureSize(); + ExifInterface exif = Exif.getExif(jpegData); + int orientation = Exif.getOrientation(exif); + int width, height; + if ((mJpegRotation + orientation) % 180 == 0) { + width = s.width; + height = s.height; + } else { + width = s.height; + height = s.width; + } + String title = mNamedImages.getTitle(); + long date = mNamedImages.getDate(); + if (title == null) { + Log.e(TAG, "Unbalanced name/data pair"); + } else { + if (date == -1) date = mCaptureStartTime; + if (mHeading >= 0) { + // heading direction has been updated by the sensor. + ExifTag directionRefTag = exif.buildTag( + ExifInterface.TAG_GPS_IMG_DIRECTION_REF, + ExifInterface.GpsTrackRef.MAGNETIC_DIRECTION); + ExifTag directionTag = exif.buildTag( + ExifInterface.TAG_GPS_IMG_DIRECTION, + new Rational(mHeading, 1)); + exif.setTag(directionRefTag); + exif.setTag(directionTag); + } + mActivity.getMediaSaveService().addImage( + jpegData, title, date, mLocation, width, height, + orientation, exif, mOnMediaSavedListener, mContentResolver); + } + } else { + mJpegImageData = jpegData; + if (!mQuickCapture) { + mUI.showPostCaptureAlert(); + } else { + onCaptureDone(); + } + } + + // Check this in advance of each shot so we don't add to shutter + // latency. It's true that someone else could write to the SD card in + // the mean time and fill it, but that could have happened between the + // shutter press and saving the JPEG too. + mActivity.updateStorageSpaceAndHint(); + + long now = System.currentTimeMillis(); + mJpegCallbackFinishTime = now - mJpegPictureCallbackTime; + Log.v(TAG, "mJpegCallbackFinishTime = " + + mJpegCallbackFinishTime + "ms"); + mJpegPictureCallbackTime = 0; + } + } + + private final class AutoFocusCallback implements CameraAFCallback { + @Override + public void onAutoFocus( + boolean focused, CameraProxy camera) { + if (mPaused) return; + + mAutoFocusTime = System.currentTimeMillis() - mFocusStartTime; + Log.v(TAG, "mAutoFocusTime = " + mAutoFocusTime + "ms"); + setCameraState(IDLE); + mFocusManager.onAutoFocus(focused, mUI.isShutterPressed()); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + private final class AutoFocusMoveCallback + implements CameraAFMoveCallback { + @Override + public void onAutoFocusMoving( + boolean moving, CameraProxy camera) { + mFocusManager.onAutoFocusMoving(moving); + } + } + + private static class NamedImages { + private ArrayList<NamedEntity> mQueue; + private boolean mStop; + private NamedEntity mNamedEntity; + + public NamedImages() { + mQueue = new ArrayList<NamedEntity>(); + } + + public void nameNewImage(ContentResolver resolver, long date) { + NamedEntity r = new NamedEntity(); + r.title = Util.createJpegName(date); + r.date = date; + mQueue.add(r); + } + + public String getTitle() { + if (mQueue.isEmpty()) { + mNamedEntity = null; + return null; + } + mNamedEntity = mQueue.get(0); + mQueue.remove(0); + + return mNamedEntity.title; + } + + // Must be called after getTitle(). + public long getDate() { + if (mNamedEntity == null) return -1; + return mNamedEntity.date; + } + + private static class NamedEntity { + String title; + long date; + } + } + + private void setCameraState(int state) { + mCameraState = state; + switch (state) { + case PhotoController.PREVIEW_STOPPED: + case PhotoController.SNAPSHOT_IN_PROGRESS: + case PhotoController.SWITCHING_CAMERA: + mUI.enableGestures(false); + break; + case PhotoController.IDLE: + mUI.enableGestures(true); + break; + } + } + + private void animateFlash() { + // Only animate when in full screen capture mode + // i.e. If monkey/a user swipes to the gallery during picture taking, + // don't show animation + if (!mIsImageCaptureIntent) { + mUI.animateFlash(); + + // TODO: mUI.enablePreviewThumb(true); + // mHandler.sendEmptyMessageDelayed(CAPTURE_ANIMATION_DONE, + // CaptureAnimManager.getAnimationDuration()); + } + } + + @Override + public boolean capture() { + // If we are already in the middle of taking a snapshot or the image save request + // is full then ignore. + if (mCameraDevice == null || mCameraState == SNAPSHOT_IN_PROGRESS + || mCameraState == SWITCHING_CAMERA + || mActivity.getMediaSaveService().isQueueFull()) { + return false; + } + mCaptureStartTime = System.currentTimeMillis(); + mPostViewPictureCallbackTime = 0; + mJpegImageData = null; + + final boolean animateBefore = (mSceneMode == Util.SCENE_MODE_HDR); + + if (animateBefore) { + animateFlash(); + } + + // Set rotation and gps data. + int orientation; + // We need to be consistent with the framework orientation (i.e. the + // orientation of the UI.) when the auto-rotate screen setting is on. + if (mActivity.isAutoRotateScreen()) { + orientation = (360 - mDisplayRotation) % 360; + } else { + orientation = mOrientation; + } + mJpegRotation = Util.getJpegRotation(mCameraId, orientation); + mParameters.setRotation(mJpegRotation); + Location loc = mLocationManager.getCurrentLocation(); + Util.setGpsParameters(mParameters, loc); + mCameraDevice.setParameters(mParameters); + + mCameraDevice.takePicture(mHandler, + new ShutterCallback(!animateBefore), + mRawPictureCallback, mPostViewPictureCallback, + new JpegPictureCallback(loc)); + + mNamedImages.nameNewImage(mContentResolver, mCaptureStartTime); + + mFaceDetectionStarted = false; + setCameraState(SNAPSHOT_IN_PROGRESS); + UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA, + UsageStatistics.ACTION_CAPTURE_DONE, "Photo"); + return true; + } + + @Override + public void setFocusParameters() { + setCameraParameters(UPDATE_PARAM_PREFERENCE); + } + + private int getPreferredCameraId(ComboPreferences preferences) { + int intentCameraId = Util.getCameraFacingIntentExtras(mActivity); + if (intentCameraId != -1) { + // Testing purpose. Launch a specific camera through the intent + // extras. + return intentCameraId; + } else { + return CameraSettings.readPreferredCameraId(preferences); + } + } + + private void updateSceneMode() { + // If scene mode is set, we cannot set flash mode, white balance, and + // focus mode, instead, we read it from driver + if (!Parameters.SCENE_MODE_AUTO.equals(mSceneMode)) { + overrideCameraSettings(mParameters.getFlashMode(), + mParameters.getWhiteBalance(), mParameters.getFocusMode()); + } else { + overrideCameraSettings(null, null, null); + } + } + + private void overrideCameraSettings(final String flashMode, + final String whiteBalance, final String focusMode) { + mUI.overrideSettings( + CameraSettings.KEY_FLASH_MODE, flashMode, + CameraSettings.KEY_WHITE_BALANCE, whiteBalance, + CameraSettings.KEY_FOCUS_MODE, focusMode); + } + + private void loadCameraPreferences() { + CameraSettings settings = new CameraSettings(mActivity, mInitialParams, + mCameraId, CameraHolder.instance().getCameraInfo()); + mPreferenceGroup = settings.getPreferenceGroup(R.xml.camera_preferences); + } + + @Override + public void onOrientationChanged(int orientation) { + // We keep the last known orientation. So if the user first orient + // the camera then point the camera to floor or sky, we still have + // the correct orientation. + if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) return; + mOrientation = Util.roundOrientation(orientation, mOrientation); + + // Show the toast after getting the first orientation changed. + if (mHandler.hasMessages(SHOW_TAP_TO_FOCUS_TOAST)) { + mHandler.removeMessages(SHOW_TAP_TO_FOCUS_TOAST); + showTapToFocusToast(); + } + } + + @Override + public void onStop() { + if (mMediaProviderClient != null) { + mMediaProviderClient.release(); + mMediaProviderClient = null; + } + } + + @Override + public void onCaptureCancelled() { + mActivity.setResultEx(Activity.RESULT_CANCELED, new Intent()); + mActivity.finish(); + } + + @Override + public void onCaptureRetake() { + if (mPaused) + return; + mUI.hidePostCaptureAlert(); + setupPreview(); + } + + @Override + public void onCaptureDone() { + if (mPaused) { + return; + } + + byte[] data = mJpegImageData; + + if (mCropValue == null) { + // First handle the no crop case -- just return the value. If the + // caller specifies a "save uri" then write the data to its + // stream. Otherwise, pass back a scaled down version of the bitmap + // directly in the extras. + if (mSaveUri != null) { + OutputStream outputStream = null; + try { + outputStream = mContentResolver.openOutputStream(mSaveUri); + outputStream.write(data); + outputStream.close(); + + mActivity.setResultEx(Activity.RESULT_OK); + mActivity.finish(); + } catch (IOException ex) { + // ignore exception + } finally { + Util.closeSilently(outputStream); + } + } else { + ExifInterface exif = Exif.getExif(data); + int orientation = Exif.getOrientation(exif); + Bitmap bitmap = Util.makeBitmap(data, 50 * 1024); + bitmap = Util.rotate(bitmap, orientation); + mActivity.setResultEx(Activity.RESULT_OK, + new Intent("inline-data").putExtra("data", bitmap)); + mActivity.finish(); + } + } else { + // Save the image to a temp file and invoke the cropper + Uri tempUri = null; + FileOutputStream tempStream = null; + try { + File path = mActivity.getFileStreamPath(sTempCropFilename); + path.delete(); + tempStream = mActivity.openFileOutput(sTempCropFilename, 0); + tempStream.write(data); + tempStream.close(); + tempUri = Uri.fromFile(path); + } catch (FileNotFoundException ex) { + mActivity.setResultEx(Activity.RESULT_CANCELED); + mActivity.finish(); + return; + } catch (IOException ex) { + mActivity.setResultEx(Activity.RESULT_CANCELED); + mActivity.finish(); + return; + } finally { + Util.closeSilently(tempStream); + } + + Bundle newExtras = new Bundle(); + if (mCropValue.equals("circle")) { + newExtras.putString("circleCrop", "true"); + } + if (mSaveUri != null) { + newExtras.putParcelable(MediaStore.EXTRA_OUTPUT, mSaveUri); + } else { + newExtras.putBoolean(CropExtras.KEY_RETURN_DATA, true); + } + if (mActivity.isSecureCamera()) { + newExtras.putBoolean(CropExtras.KEY_SHOW_WHEN_LOCKED, true); + } + + Intent cropIntent = new Intent(CropActivity.CROP_ACTION); + + cropIntent.setData(tempUri); + cropIntent.putExtras(newExtras); + + mActivity.startActivityForResult(cropIntent, REQUEST_CROP); + } + } + + @Override + public void onShutterButtonFocus(boolean pressed) { + if (mPaused || mUI.collapseCameraControls() + || (mCameraState == SNAPSHOT_IN_PROGRESS) + || (mCameraState == PREVIEW_STOPPED)) return; + + // Do not do focus if there is not enough storage. + if (pressed && !canTakePicture()) return; + + if (pressed) { + mFocusManager.onShutterDown(); + } else { + // for countdown mode, we need to postpone the shutter release + // i.e. lock the focus during countdown. + if (!mUI.isCountingDown()) { + mFocusManager.onShutterUp(); + } + } + } + + @Override + public void onShutterButtonClick() { + if (mPaused || mUI.collapseCameraControls() + || (mCameraState == SWITCHING_CAMERA) + || (mCameraState == PREVIEW_STOPPED)) return; + + // Do not take the picture if there is not enough storage. + if (mActivity.getStorageSpace() <= Storage.LOW_STORAGE_THRESHOLD) { + Log.i(TAG, "Not enough space or storage not ready. remaining=" + + mActivity.getStorageSpace()); + return; + } + Log.v(TAG, "onShutterButtonClick: mCameraState=" + mCameraState); + + if (mSceneMode == Util.SCENE_MODE_HDR) { + mUI.hideSwitcher(); + mUI.setSwipingEnabled(false); + } + // If the user wants to do a snapshot while the previous one is still + // in progress, remember the fact and do it after we finish the previous + // one and re-start the preview. Snapshot in progress also includes the + // state that autofocus is focusing and a picture will be taken when + // focus callback arrives. + if ((mFocusManager.isFocusingSnapOnFinish() || mCameraState == SNAPSHOT_IN_PROGRESS) + && !mIsImageCaptureIntent) { + mSnapshotOnIdle = true; + return; + } + + String timer = mPreferences.getString( + CameraSettings.KEY_TIMER, + mActivity.getString(R.string.pref_camera_timer_default)); + boolean playSound = mPreferences.getString(CameraSettings.KEY_TIMER_SOUND_EFFECTS, + mActivity.getString(R.string.pref_camera_timer_sound_default)) + .equals(mActivity.getString(R.string.setting_on_value)); + + int seconds = Integer.parseInt(timer); + // When shutter button is pressed, check whether the previous countdown is + // finished. If not, cancel the previous countdown and start a new one. + if (mUI.isCountingDown()) { + mUI.cancelCountDown(); + } + if (seconds > 0) { + mUI.startCountDown(seconds, playSound); + } else { + mSnapshotOnIdle = false; + mFocusManager.doSnap(); + } + } + + @Override + public void installIntentFilter() { + } + + @Override + public boolean updateStorageHintOnResume() { + return mFirstTimeInitialized; + } + + @Override + public void updateCameraAppView() { + } + + @Override + public void onResumeBeforeSuper() { + mPaused = false; + } + + @Override + public void onResumeAfterSuper() { + if (mOpenCameraFail || mCameraDisabled) return; + + mJpegPictureCallbackTime = 0; + mZoomValue = 0; + // Start the preview if it is not started. + if (mCameraState == PREVIEW_STOPPED && mCameraStartUpThread == null) { + resetExposureCompensation(); + mCameraStartUpThread = new CameraStartUpThread(); + mCameraStartUpThread.start(); + } + + // If first time initialization is not finished, put it in the + // message queue. + if (!mFirstTimeInitialized) { + mHandler.sendEmptyMessage(FIRST_TIME_INIT); + } else { + initializeSecondTime(); + } + keepScreenOnAwhile(); + + // Dismiss open menu if exists. + PopupManager.getInstance(mActivity).notifyShowPopup(null); + UsageStatistics.onContentViewChanged( + UsageStatistics.COMPONENT_CAMERA, "PhotoModule"); + + Sensor gsensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + if (gsensor != null) { + mSensorManager.registerListener(this, gsensor, SensorManager.SENSOR_DELAY_NORMAL); + } + + Sensor msensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); + if (msensor != null) { + mSensorManager.registerListener(this, msensor, SensorManager.SENSOR_DELAY_NORMAL); + } + } + + void waitCameraStartUpThread() { + try { + if (mCameraStartUpThread != null) { + mCameraStartUpThread.cancel(); + mCameraStartUpThread.join(); + mCameraStartUpThread = null; + setCameraState(IDLE); + } + } catch (InterruptedException e) { + // ignore + } + } + + @Override + public void onPauseBeforeSuper() { + mPaused = true; + Sensor gsensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + if (gsensor != null) { + mSensorManager.unregisterListener(this, gsensor); + } + + Sensor msensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); + if (msensor != null) { + mSensorManager.unregisterListener(this, msensor); + } + } + + @Override + public void onPauseAfterSuper() { + // Wait the camera start up thread to finish. + waitCameraStartUpThread(); + + // When camera is started from secure lock screen for the first time + // after screen on, the activity gets onCreate->onResume->onPause->onResume. + // To reduce the latency, keep the camera for a short time so it does + // not need to be opened again. + if (mCameraDevice != null && mActivity.isSecureCamera() + && CameraActivity.isFirstStartAfterScreenOn()) { + CameraActivity.resetFirstStartAfterScreenOn(); + CameraHolder.instance().keep(KEEP_CAMERA_TIMEOUT); + } + // Reset the focus first. Camera CTS does not guarantee that + // cancelAutoFocus is allowed after preview stops. + if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) { + mCameraDevice.cancelAutoFocus(); + } + stopPreview(); + + mNamedImages = null; + + if (mLocationManager != null) mLocationManager.recordLocation(false); + + // If we are in an image capture intent and has taken + // a picture, we just clear it in onPause. + mJpegImageData = null; + + // Remove the messages in the event queue. + mHandler.removeMessages(SETUP_PREVIEW); + mHandler.removeMessages(FIRST_TIME_INIT); + mHandler.removeMessages(CHECK_DISPLAY_ROTATION); + mHandler.removeMessages(SWITCH_CAMERA); + mHandler.removeMessages(SWITCH_CAMERA_START_ANIMATION); + mHandler.removeMessages(CAMERA_OPEN_DONE); + mHandler.removeMessages(START_PREVIEW_DONE); + mHandler.removeMessages(OPEN_CAMERA_FAIL); + mHandler.removeMessages(CAMERA_DISABLED); + + closeCamera(); + + resetScreenOn(); + mUI.onPause(); + + mPendingSwitchCameraId = -1; + if (mFocusManager != null) mFocusManager.removeMessages(); + MediaSaveService s = mActivity.getMediaSaveService(); + if (s != null) { + s.setListener(null); + } + } + + /** + * The focus manager is the first UI related element to get initialized, + * and it requires the RenderOverlay, so initialize it here + */ + private void initializeFocusManager() { + // Create FocusManager object. startPreview needs it. + // if mFocusManager not null, reuse it + // otherwise create a new instance + if (mFocusManager != null) { + mFocusManager.removeMessages(); + } else { + CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId]; + boolean mirror = (info.facing == CameraInfo.CAMERA_FACING_FRONT); + String[] defaultFocusModes = mActivity.getResources().getStringArray( + R.array.pref_camera_focusmode_default_array); + mFocusManager = new FocusOverlayManager(mPreferences, defaultFocusModes, + mInitialParams, this, mirror, + mActivity.getMainLooper(), mUI); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + Log.v(TAG, "onConfigurationChanged"); + setDisplayOrientation(); + } + + @Override + public void updateCameraOrientation() { + if (mDisplayRotation != Util.getDisplayRotation(mActivity)) { + setDisplayOrientation(); + } + } + + @Override + public void onActivityResult( + int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_CROP: { + Intent intent = new Intent(); + if (data != null) { + Bundle extras = data.getExtras(); + if (extras != null) { + intent.putExtras(extras); + } + } + mActivity.setResultEx(resultCode, intent); + mActivity.finish(); + + File path = mActivity.getFileStreamPath(sTempCropFilename); + path.delete(); + + break; + } + } + } + + private boolean canTakePicture() { + return isCameraIdle() && (mActivity.getStorageSpace() > Storage.LOW_STORAGE_THRESHOLD); + } + + @Override + public void autoFocus() { + mFocusStartTime = System.currentTimeMillis(); + mCameraDevice.autoFocus(mHandler, mAutoFocusCallback); + setCameraState(FOCUSING); + } + + @Override + public void cancelAutoFocus() { + mCameraDevice.cancelAutoFocus(); + setCameraState(IDLE); + setCameraParameters(UPDATE_PARAM_PREFERENCE); + } + + // Preview area is touched. Handle touch focus. + @Override + public void onSingleTapUp(View view, int x, int y) { + if (mPaused || mCameraDevice == null || !mFirstTimeInitialized + || mCameraState == SNAPSHOT_IN_PROGRESS + || mCameraState == SWITCHING_CAMERA + || mCameraState == PREVIEW_STOPPED) { + return; + } + + // Do not trigger touch focus if popup window is opened. + if (mUI.removeTopLevelPopup()) return; + + // Check if metering area or focus area is supported. + if (!mFocusAreaSupported && !mMeteringAreaSupported) return; + mFocusManager.onSingleTapUp(x, y); + } + + @Override + public boolean onBackPressed() { + return mUI.onBackPressed(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: + case KeyEvent.KEYCODE_FOCUS: + if (/*TODO: mActivity.isInCameraApp() &&*/ mFirstTimeInitialized) { + if (event.getRepeatCount() == 0) { + onShutterButtonFocus(true); + } + return true; + } + return false; + case KeyEvent.KEYCODE_CAMERA: + if (mFirstTimeInitialized && event.getRepeatCount() == 0) { + onShutterButtonClick(); + } + return true; + case KeyEvent.KEYCODE_DPAD_CENTER: + // If we get a dpad center event without any focused view, move + // the focus to the shutter button and press it. + if (mFirstTimeInitialized && event.getRepeatCount() == 0) { + // Start auto-focus immediately to reduce shutter lag. After + // the shutter button gets the focus, onShutterButtonFocus() + // will be called again but it is fine. + if (mUI.removeTopLevelPopup()) return true; + onShutterButtonFocus(true); + mUI.pressShutterButton(); + } + return true; + } + return false; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: + if (/*mActivity.isInCameraApp() && */ mFirstTimeInitialized) { + onShutterButtonClick(); + return true; + } + return false; + case KeyEvent.KEYCODE_FOCUS: + if (mFirstTimeInitialized) { + onShutterButtonFocus(false); + } + return true; + } + return false; + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void closeCamera() { + if (mCameraDevice != null) { + mCameraDevice.setZoomChangeListener(null); + if(ApiHelper.HAS_FACE_DETECTION) { + mCameraDevice.setFaceDetectionCallback(null, null); + } + mCameraDevice.setErrorCallback(null); + CameraHolder.instance().release(); + mFaceDetectionStarted = false; + mCameraDevice = null; + setCameraState(PREVIEW_STOPPED); + mFocusManager.onCameraReleased(); + } + } + + private void setDisplayOrientation() { + mDisplayRotation = Util.getDisplayRotation(mActivity); + mDisplayOrientation = Util.getDisplayOrientation(mDisplayRotation, mCameraId); + mCameraDisplayOrientation = mDisplayOrientation; + mUI.setDisplayOrientation(mDisplayOrientation); + if (mFocusManager != null) { + mFocusManager.setDisplayOrientation(mDisplayOrientation); + } + // Change the camera display orientation + if (mCameraDevice != null) { + mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation); + } + } + + // Only called by UI thread. + private void setupPreview() { + mFocusManager.resetTouchFocus(); + startPreview(); + setCameraState(IDLE); + startFaceDetection(); + } + + // This can be called by UI Thread or CameraStartUpThread. So this should + // not modify the views. + private void startPreview() { + mCameraDevice.setErrorCallback(mErrorCallback); + + // ICS camera frameworks has a bug. Face detection state is not cleared + // after taking a picture. Stop the preview to work around it. The bug + // was fixed in JB. + if (mCameraState != PREVIEW_STOPPED) stopPreview(); + + setDisplayOrientation(); + + if (!mSnapshotOnIdle) { + // If the focus mode is continuous autofocus, call cancelAutoFocus to + // resume it because it may have been paused by autoFocus call. + if (Util.FOCUS_MODE_CONTINUOUS_PICTURE.equals(mFocusManager.getFocusMode())) { + mCameraDevice.cancelAutoFocus(); + } + mFocusManager.setAeAwbLock(false); // Unlock AE and AWB. + } + setCameraParameters(UPDATE_PARAM_ALL); + // Let UI set its expected aspect ratio + mUI.setPreviewSize(mParameters.getPreviewSize()); + Object st = mUI.getSurfaceTexture(); + if (st != null) { + mCameraDevice.setPreviewTexture((SurfaceTexture) st); + } + + Log.v(TAG, "startPreview"); + mCameraDevice.startPreview(); + mFocusManager.onPreviewStarted(); + + if (mSnapshotOnIdle) { + mHandler.post(mDoSnapRunnable); + } + } + + @Override + public void stopPreview() { + if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) { + Log.v(TAG, "stopPreview"); + mCameraDevice.stopPreview(); + mFaceDetectionStarted = false; + } + setCameraState(PREVIEW_STOPPED); + if (mFocusManager != null) mFocusManager.onPreviewStopped(); + } + + @SuppressWarnings("deprecation") + private void updateCameraParametersInitialize() { + // Reset preview frame rate to the maximum because it may be lowered by + // video camera application. + int[] fpsRange = Util.getMaxPreviewFpsRange(mParameters); + if (fpsRange.length > 0) { + mParameters.setPreviewFpsRange( + fpsRange[Parameters.PREVIEW_FPS_MIN_INDEX], + fpsRange[Parameters.PREVIEW_FPS_MAX_INDEX]); + } + + mParameters.set(Util.RECORDING_HINT, Util.FALSE); + + // Disable video stabilization. Convenience methods not available in API + // level <= 14 + String vstabSupported = mParameters.get("video-stabilization-supported"); + if ("true".equals(vstabSupported)) { + mParameters.set("video-stabilization", "false"); + } + } + + private void updateCameraParametersZoom() { + // Set zoom. + if (mParameters.isZoomSupported()) { + mParameters.setZoom(mZoomValue); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + private void setAutoExposureLockIfSupported() { + if (mAeLockSupported) { + mParameters.setAutoExposureLock(mFocusManager.getAeAwbLock()); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + private void setAutoWhiteBalanceLockIfSupported() { + if (mAwbLockSupported) { + mParameters.setAutoWhiteBalanceLock(mFocusManager.getAeAwbLock()); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void setFocusAreasIfSupported() { + if (mFocusAreaSupported) { + mParameters.setFocusAreas(mFocusManager.getFocusAreas()); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void setMeteringAreasIfSupported() { + if (mMeteringAreaSupported) { + // Use the same area for focus and metering. + mParameters.setMeteringAreas(mFocusManager.getMeteringAreas()); + } + } + + private void updateCameraParametersPreference() { + setAutoExposureLockIfSupported(); + setAutoWhiteBalanceLockIfSupported(); + setFocusAreasIfSupported(); + setMeteringAreasIfSupported(); + + // Set picture size. + String pictureSize = mPreferences.getString( + CameraSettings.KEY_PICTURE_SIZE, null); + if (pictureSize == null) { + CameraSettings.initialCameraPictureSize(mActivity, mParameters); + } else { + List<Size> supported = mParameters.getSupportedPictureSizes(); + CameraSettings.setCameraPictureSize( + pictureSize, supported, mParameters); + } + Size size = mParameters.getPictureSize(); + + // Set a preview size that is closest to the viewfinder height and has + // the right aspect ratio. + List<Size> sizes = mParameters.getSupportedPreviewSizes(); + Size optimalSize = Util.getOptimalPreviewSize(mActivity, sizes, + (double) size.width / size.height); + Size original = mParameters.getPreviewSize(); + if (!original.equals(optimalSize)) { + mParameters.setPreviewSize(optimalSize.width, optimalSize.height); + + // Zoom related settings will be changed for different preview + // sizes, so set and read the parameters to get latest values + if (mHandler.getLooper() == Looper.myLooper()) { + // On UI thread only, not when camera starts up + setupPreview(); + } else { + mCameraDevice.setParameters(mParameters); + } + mParameters = mCameraDevice.getParameters(); + } + Log.v(TAG, "Preview size is " + optimalSize.width + "x" + optimalSize.height); + + // Since changing scene mode may change supported values, set scene mode + // first. HDR is a scene mode. To promote it in UI, it is stored in a + // separate preference. + String hdr = mPreferences.getString(CameraSettings.KEY_CAMERA_HDR, + mActivity.getString(R.string.pref_camera_hdr_default)); + if (mActivity.getString(R.string.setting_on_value).equals(hdr)) { + mSceneMode = Util.SCENE_MODE_HDR; + } else { + mSceneMode = mPreferences.getString( + CameraSettings.KEY_SCENE_MODE, + mActivity.getString(R.string.pref_camera_scenemode_default)); + } + if (Util.isSupported(mSceneMode, mParameters.getSupportedSceneModes())) { + if (!mParameters.getSceneMode().equals(mSceneMode)) { + mParameters.setSceneMode(mSceneMode); + + // Setting scene mode will change the settings of flash mode, + // white balance, and focus mode. Here we read back the + // parameters, so we can know those settings. + mCameraDevice.setParameters(mParameters); + mParameters = mCameraDevice.getParameters(); + } + } else { + mSceneMode = mParameters.getSceneMode(); + if (mSceneMode == null) { + mSceneMode = Parameters.SCENE_MODE_AUTO; + } + } + + // Set JPEG quality. + int jpegQuality = CameraProfile.getJpegEncodingQualityParameter(mCameraId, + CameraProfile.QUALITY_HIGH); + mParameters.setJpegQuality(jpegQuality); + + // For the following settings, we need to check if the settings are + // still supported by latest driver, if not, ignore the settings. + + // Set exposure compensation + int value = CameraSettings.readExposure(mPreferences); + int max = mParameters.getMaxExposureCompensation(); + int min = mParameters.getMinExposureCompensation(); + if (value >= min && value <= max) { + mParameters.setExposureCompensation(value); + } else { + Log.w(TAG, "invalid exposure range: " + value); + } + + if (Parameters.SCENE_MODE_AUTO.equals(mSceneMode)) { + // Set flash mode. + String flashMode = mPreferences.getString( + CameraSettings.KEY_FLASH_MODE, + mActivity.getString(R.string.pref_camera_flashmode_default)); + List<String> supportedFlash = mParameters.getSupportedFlashModes(); + if (Util.isSupported(flashMode, supportedFlash)) { + mParameters.setFlashMode(flashMode); + } else { + flashMode = mParameters.getFlashMode(); + if (flashMode == null) { + flashMode = mActivity.getString( + R.string.pref_camera_flashmode_no_flash); + } + } + + // Set white balance parameter. + String whiteBalance = mPreferences.getString( + CameraSettings.KEY_WHITE_BALANCE, + mActivity.getString(R.string.pref_camera_whitebalance_default)); + if (Util.isSupported(whiteBalance, + mParameters.getSupportedWhiteBalance())) { + mParameters.setWhiteBalance(whiteBalance); + } else { + whiteBalance = mParameters.getWhiteBalance(); + if (whiteBalance == null) { + whiteBalance = Parameters.WHITE_BALANCE_AUTO; + } + } + + // Set focus mode. + mFocusManager.overrideFocusMode(null); + mParameters.setFocusMode(mFocusManager.getFocusMode()); + } else { + mFocusManager.overrideFocusMode(mParameters.getFocusMode()); + } + + if (mContinousFocusSupported && ApiHelper.HAS_AUTO_FOCUS_MOVE_CALLBACK) { + updateAutoFocusMoveCallback(); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + private void updateAutoFocusMoveCallback() { + if (mParameters.getFocusMode().equals(Util.FOCUS_MODE_CONTINUOUS_PICTURE)) { + mCameraDevice.setAutoFocusMoveCallback(mHandler, + (CameraManager.CameraAFMoveCallback) mAutoFocusMoveCallback); + } else { + mCameraDevice.setAutoFocusMoveCallback(null, null); + } + } + + // We separate the parameters into several subsets, so we can update only + // the subsets actually need updating. The PREFERENCE set needs extra + // locking because the preference can be changed from GLThread as well. + private void setCameraParameters(int updateSet) { + if ((updateSet & UPDATE_PARAM_INITIALIZE) != 0) { + updateCameraParametersInitialize(); + } + + if ((updateSet & UPDATE_PARAM_ZOOM) != 0) { + updateCameraParametersZoom(); + } + + if ((updateSet & UPDATE_PARAM_PREFERENCE) != 0) { + updateCameraParametersPreference(); + } + + mCameraDevice.setParameters(mParameters); + } + + // If the Camera is idle, update the parameters immediately, otherwise + // accumulate them in mUpdateSet and update later. + private void setCameraParametersWhenIdle(int additionalUpdateSet) { + mUpdateSet |= additionalUpdateSet; + if (mCameraDevice == null) { + // We will update all the parameters when we open the device, so + // we don't need to do anything now. + mUpdateSet = 0; + return; + } else if (isCameraIdle()) { + setCameraParameters(mUpdateSet); + updateSceneMode(); + mUpdateSet = 0; + } else { + if (!mHandler.hasMessages(SET_CAMERA_PARAMETERS_WHEN_IDLE)) { + mHandler.sendEmptyMessageDelayed( + SET_CAMERA_PARAMETERS_WHEN_IDLE, 1000); + } + } + } + + public boolean isCameraIdle() { + return (mCameraState == IDLE) || + (mCameraState == PREVIEW_STOPPED) || + ((mFocusManager != null) && mFocusManager.isFocusCompleted() + && (mCameraState != SWITCHING_CAMERA)); + } + + public boolean isImageCaptureIntent() { + String action = mActivity.getIntent().getAction(); + return (MediaStore.ACTION_IMAGE_CAPTURE.equals(action) + || CameraActivity.ACTION_IMAGE_CAPTURE_SECURE.equals(action)); + } + + private void setupCaptureParams() { + Bundle myExtras = mActivity.getIntent().getExtras(); + if (myExtras != null) { + mSaveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT); + mCropValue = myExtras.getString("crop"); + } + } + + @Override + public void onSharedPreferenceChanged() { + // ignore the events after "onPause()" + if (mPaused) return; + + boolean recordLocation = RecordLocationPreference.get( + mPreferences, mContentResolver); + mLocationManager.recordLocation(recordLocation); + + setCameraParametersWhenIdle(UPDATE_PARAM_PREFERENCE); + mUI.updateOnScreenIndicators(mParameters, mPreferenceGroup, mPreferences); + } + + @Override + public void onCameraPickerClicked(int cameraId) { + if (mPaused || mPendingSwitchCameraId != -1) return; + + mPendingSwitchCameraId = cameraId; + + Log.v(TAG, "Start to switch camera. cameraId=" + cameraId); + // We need to keep a preview frame for the animation before + // releasing the camera. This will trigger onPreviewTextureCopied. + //TODO: Need to animate the camera switch + switchCamera(); + } + + // Preview texture has been copied. Now camera can be released and the + // animation can be started. + @Override + public void onPreviewTextureCopied() { + mHandler.sendEmptyMessage(SWITCH_CAMERA); + } + + @Override + public void onCaptureTextureCopied() { + } + + @Override + public void onUserInteraction() { + if (!mActivity.isFinishing()) keepScreenOnAwhile(); + } + + private void resetScreenOn() { + mHandler.removeMessages(CLEAR_SCREEN_DELAY); + mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + private void keepScreenOnAwhile() { + mHandler.removeMessages(CLEAR_SCREEN_DELAY); + mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY); + } + + @Override + public void onOverriddenPreferencesClicked() { + if (mPaused) return; + mUI.showPreferencesToast(); + } + + private void showTapToFocusToast() { + // TODO: Use a toast? + new RotateTextToast(mActivity, R.string.tap_to_focus, 0).show(); + // Clear the preference. + Editor editor = mPreferences.edit(); + editor.putBoolean(CameraSettings.KEY_CAMERA_FIRST_USE_HINT_SHOWN, false); + editor.apply(); + } + + private void initializeCapabilities() { + mInitialParams = mCameraDevice.getParameters(); + mFocusAreaSupported = Util.isFocusAreaSupported(mInitialParams); + mMeteringAreaSupported = Util.isMeteringAreaSupported(mInitialParams); + mAeLockSupported = Util.isAutoExposureLockSupported(mInitialParams); + mAwbLockSupported = Util.isAutoWhiteBalanceLockSupported(mInitialParams); + mContinousFocusSupported = mInitialParams.getSupportedFocusModes().contains( + Util.FOCUS_MODE_CONTINUOUS_PICTURE); + } + + @Override + public void onCountDownFinished() { + mSnapshotOnIdle = false; + mFocusManager.doSnap(); + mFocusManager.onShutterUp(); + } + + @Override + public void onShowSwitcherPopup() { + mUI.onShowSwitcherPopup(); + } + + @Override + public int onZoomChanged(int index) { + // Not useful to change zoom value when the activity is paused. + if (mPaused) return index; + mZoomValue = index; + if (mParameters == null || mCameraDevice == null) return index; + // Set zoom parameters asynchronously + mParameters.setZoom(mZoomValue); + mCameraDevice.setParameters(mParameters); + Parameters p = mCameraDevice.getParameters(); + if (p != null) return p.getZoom(); + return index; + } + + @Override + public int getCameraState() { + return mCameraState; + } + + @Override + public void onQueueStatus(boolean full) { + mUI.enableShutter(!full); + } + + @Override + public void onMediaSaveServiceConnected(MediaSaveService s) { + // We set the listener only when both service and shutterbutton + // are initialized. + if (mFirstTimeInitialized) { + s.setListener(this); + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + + @Override + public void onSensorChanged(SensorEvent event) { + int type = event.sensor.getType(); + float[] data; + if (type == Sensor.TYPE_ACCELEROMETER) { + data = mGData; + } else if (type == Sensor.TYPE_MAGNETIC_FIELD) { + data = mMData; + } else { + // we should not be here. + return; + } + for (int i = 0; i < 3 ; i++) { + data[i] = event.values[i]; + } + float[] orientation = new float[3]; + SensorManager.getRotationMatrix(mR, null, mGData, mMData); + SensorManager.getOrientation(mR, orientation); + mHeading = (int) (orientation[0] * 180f / Math.PI) % 360; + if (mHeading < 0) { + mHeading += 360; + } + } + + @Override + public void onSwitchMode(boolean toCamera) { + mUI.onSwitchMode(toCamera); + } + +/* Below is no longer needed, except to get rid of compile error + * TODO: Remove these + */ + + // TODO: Delete this function after old camera code is removed + @Override + public void onRestorePreferencesClicked() {} + +} diff --git a/src/com/android/camera/PhotoUI.java b/src/com/android/camera/PhotoUI.java new file mode 100644 index 000000000..d58ed7f13 --- /dev/null +++ b/src/com/android/camera/PhotoUI.java @@ -0,0 +1,864 @@ +/* + * Copyright (C) 2012 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 android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.graphics.Matrix; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.hardware.Camera.Face; +import android.hardware.Camera.FaceDetectionListener; +import android.hardware.Camera.Size; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.view.Gravity; +import android.view.TextureView; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLayoutChangeListener; +import android.view.ViewGroup; +import android.view.ViewStub; +import android.widget.FrameLayout; +import android.widget.FrameLayout.LayoutParams; +import android.widget.Toast; + +import com.android.camera.CameraPreference.OnPreferenceChangedListener; +import com.android.camera.FocusOverlayManager.FocusUI; +import com.android.camera.ui.AbstractSettingPopup; +import com.android.camera.ui.CameraControls; +import com.android.camera.ui.CameraRootView; +import com.android.camera.ui.CameraSwitcher; +import com.android.camera.ui.CameraSwitcher.CameraSwitchListener; +import com.android.camera.ui.CountDownView; +import com.android.camera.ui.CountDownView.OnCountDownFinishedListener; +import com.android.camera.ui.FaceView; +import com.android.camera.ui.FocusIndicator; +import com.android.camera.ui.PieRenderer; +import com.android.camera.ui.PieRenderer.PieListener; +import com.android.camera.ui.RenderOverlay; +import com.android.camera.ui.ZoomRenderer; +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; + +import java.util.List; + +public class PhotoUI implements PieListener, + PreviewGestures.SingleTapListener, + FocusUI, TextureView.SurfaceTextureListener, + LocationManager.Listener, CameraRootView.MyDisplayListener, + CameraManager.CameraFaceDetectionCallback { + + private static final String TAG = "CAM_UI"; + private static final int UPDATE_TRANSFORM_MATRIX = 1; + private CameraActivity mActivity; + private PhotoController mController; + private PreviewGestures mGestures; + + private View mRootView; + private Object mSurfaceTexture; + + private AbstractSettingPopup mPopup; + private ShutterButton mShutterButton; + private CountDownView mCountDownView; + + private FaceView mFaceView; + private RenderOverlay mRenderOverlay; + private View mReviewCancelButton; + private View mReviewDoneButton; + private View mReviewRetakeButton; + + private View mMenuButton; + private View mBlocker; + private PhotoMenu mMenu; + private CameraSwitcher mSwitcher; + private CameraControls mCameraControls; + private AlertDialog mLocationDialog; + + // Small indicators which show the camera settings in the viewfinder. + private OnScreenIndicators mOnScreenIndicators; + + private PieRenderer mPieRenderer; + private ZoomRenderer mZoomRenderer; + private Toast mNotSelectableToast; + + private int mZoomMax; + private List<Integer> mZoomRatios; + + private int mPreviewWidth = 0; + private int mPreviewHeight = 0; + private float mSurfaceTextureUncroppedWidth; + private float mSurfaceTextureUncroppedHeight; + + private View mPreviewThumb; + private ObjectAnimator mFlashAnim; + private View mFlashOverlay; + + private SurfaceTextureSizeChangedListener mSurfaceTextureSizeListener; + private TextureView mTextureView; + private Matrix mMatrix = null; + private float mAspectRatio = 4f / 3f; + private final Object mLock = new Object(); + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case UPDATE_TRANSFORM_MATRIX: + setTransformMatrix(mPreviewWidth, mPreviewHeight); + break; + default: + break; + } + } + }; + + public interface SurfaceTextureSizeChangedListener { + public void onSurfaceTextureSizeChanged(int uncroppedWidth, int uncroppedHeight); + } + + private OnLayoutChangeListener mLayoutListener = new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, + int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { + int width = right - left; + int height = bottom - top; + // Full-screen screennail + int w = width; + int h = height; + if (Util.getDisplayRotation(mActivity) % 180 != 0) { + w = height; + h = width; + } + if (mPreviewWidth != width || mPreviewHeight != height) { + mPreviewWidth = width; + mPreviewHeight = height; + onScreenSizeChanged(width, height, w, h); + mController.onScreenSizeChanged(width, height, w, h); + } + } + }; + + private ValueAnimator.AnimatorListener mAnimatorListener = + new ValueAnimator.AnimatorListener() { + + @Override + public void onAnimationCancel(Animator arg0) {} + + @Override + public void onAnimationEnd(Animator arg0) { + mFlashOverlay.setAlpha(0f); + mFlashOverlay.setVisibility(View.GONE); + mFlashAnim.removeListener(this); + } + + @Override + public void onAnimationRepeat(Animator arg0) {} + + @Override + public void onAnimationStart(Animator arg0) {} + }; + + public PhotoUI(CameraActivity activity, PhotoController controller, View parent) { + mActivity = activity; + mController = controller; + mRootView = parent; + + mActivity.getLayoutInflater().inflate(R.layout.photo_module, + (ViewGroup) mRootView, true); + mRenderOverlay = (RenderOverlay) mRootView.findViewById(R.id.render_overlay); + mFlashOverlay = mRootView.findViewById(R.id.flash_overlay); + // display the view + mTextureView = (TextureView) mRootView.findViewById(R.id.preview_content); + mTextureView.setSurfaceTextureListener(this); + mTextureView.addOnLayoutChangeListener(mLayoutListener); + initIndicators(); + + mShutterButton = (ShutterButton) mRootView.findViewById(R.id.shutter_button); + mSwitcher = (CameraSwitcher) mRootView.findViewById(R.id.camera_switcher); + mSwitcher.setCurrentIndex(CameraSwitcher.PHOTO_MODULE_INDEX); + mSwitcher.setSwitchListener((CameraSwitchListener) mActivity); + mMenuButton = mRootView.findViewById(R.id.menu); + if (ApiHelper.HAS_FACE_DETECTION) { + ViewStub faceViewStub = (ViewStub) mRootView + .findViewById(R.id.face_view_stub); + if (faceViewStub != null) { + faceViewStub.inflate(); + mFaceView = (FaceView) mRootView.findViewById(R.id.face_view); + setSurfaceTextureSizeChangedListener( + (SurfaceTextureSizeChangedListener) mFaceView); + } + } + mCameraControls = (CameraControls) mRootView.findViewById(R.id.camera_controls); + ((CameraRootView) mRootView).setDisplayChangeListener(this); + } + + public void onScreenSizeChanged(int width, int height, int previewWidth, int previewHeight) { + setTransformMatrix(width, height); + } + + public void setSurfaceTextureSizeChangedListener(SurfaceTextureSizeChangedListener listener) { + mSurfaceTextureSizeListener = listener; + } + + public void setPreviewSize(Size size) { + int width = size.width; + int height = size.height; + if (width == 0 || height == 0) { + Log.w(TAG, "Preview size should not be 0."); + return; + } + if (width > height) { + mAspectRatio = (float) width / height; + } else { + mAspectRatio = (float) height / width; + } + mHandler.sendEmptyMessage(UPDATE_TRANSFORM_MATRIX); + } + + private void setTransformMatrix(int width, int height) { + mMatrix = mTextureView.getTransform(mMatrix); + int orientation = Util.getDisplayRotation(mActivity); + float scaleX = 1f, scaleY = 1f; + float scaledTextureWidth, scaledTextureHeight; + if (width > height) { + scaledTextureWidth = Math.max(width, + (int) (height * mAspectRatio)); + scaledTextureHeight = Math.max(height, + (int)(width / mAspectRatio)); + } else { + scaledTextureWidth = Math.max(width, + (int) (height / mAspectRatio)); + scaledTextureHeight = Math.max(height, + (int) (width * mAspectRatio)); + } + + if (mSurfaceTextureUncroppedWidth != scaledTextureWidth || + mSurfaceTextureUncroppedHeight != scaledTextureHeight) { + mSurfaceTextureUncroppedWidth = scaledTextureWidth; + mSurfaceTextureUncroppedHeight = scaledTextureHeight; + if (mSurfaceTextureSizeListener != null) { + mSurfaceTextureSizeListener.onSurfaceTextureSizeChanged( + (int) mSurfaceTextureUncroppedWidth, (int) mSurfaceTextureUncroppedHeight); + } + } + scaleX = scaledTextureWidth / width; + scaleY = scaledTextureHeight / height; + mMatrix.setScale(scaleX, scaleY, (float) width / 2, (float) height / 2); + mTextureView.setTransform(mMatrix); + } + + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + synchronized (mLock) { + mSurfaceTexture = surface; + mLock.notifyAll(); + } + } + + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + // Ignored, Camera does all the work for us + } + + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + mSurfaceTexture = null; + mController.stopPreview(); + Log.w(TAG, "surfaceTexture is destroyed"); + return true; + } + + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + // Invoked every time there's a new Camera preview frame + } + + public View getRootView() { + return mRootView; + } + + private void initIndicators() { + mOnScreenIndicators = new OnScreenIndicators(mActivity, + mRootView.findViewById(R.id.on_screen_indicators)); + } + + public void onCameraOpened(PreferenceGroup prefGroup, ComboPreferences prefs, + Camera.Parameters params, OnPreferenceChangedListener listener) { + if (mPieRenderer == null) { + mPieRenderer = new PieRenderer(mActivity); + mPieRenderer.setPieListener(this); + mRenderOverlay.addRenderer(mPieRenderer); + } + + if (mMenu == null) { + mMenu = new PhotoMenu(mActivity, this, mPieRenderer); + mMenu.setListener(listener); + } + mMenu.initialize(prefGroup); + + if (mZoomRenderer == null) { + mZoomRenderer = new ZoomRenderer(mActivity); + mRenderOverlay.addRenderer(mZoomRenderer); + } + + if (mGestures == null) { + // this will handle gesture disambiguation and dispatching + mGestures = new PreviewGestures(mActivity, this, mZoomRenderer, mPieRenderer); + mRenderOverlay.setGestures(mGestures); + } + mGestures.setZoomEnabled(params.isZoomSupported()); + mGestures.setRenderOverlay(mRenderOverlay); + mRenderOverlay.requestLayout(); + + initializeZoom(params); + updateOnScreenIndicators(params, prefGroup, prefs); + } + + private void openMenu() { + if (mPieRenderer != null) { + // If autofocus is not finished, cancel autofocus so that the + // subsequent touch can be handled by PreviewGestures + if (mController.getCameraState() == PhotoController.FOCUSING) { + mController.cancelAutoFocus(); + } + mPieRenderer.showInCenter(); + } + } + + public void initializeControlByIntent() { + mBlocker = mRootView.findViewById(R.id.blocker); + mPreviewThumb = mRootView.findViewById(R.id.preview_thumb); + mPreviewThumb.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + // TODO: go to filmstrip + // mActivity.gotoGallery(); + } + }); + mMenuButton = mRootView.findViewById(R.id.menu); + mMenuButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + openMenu(); + } + }); + if (mController.isImageCaptureIntent()) { + hideSwitcher(); + ViewGroup cameraControls = (ViewGroup) mRootView.findViewById(R.id.camera_controls); + mActivity.getLayoutInflater().inflate(R.layout.review_module_control, cameraControls); + + mReviewDoneButton = mRootView.findViewById(R.id.btn_done); + mReviewCancelButton = mRootView.findViewById(R.id.btn_cancel); + mReviewRetakeButton = mRootView.findViewById(R.id.btn_retake); + mReviewCancelButton.setVisibility(View.VISIBLE); + + mReviewDoneButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mController.onCaptureDone(); + } + }); + mReviewCancelButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mController.onCaptureCancelled(); + } + }); + + mReviewRetakeButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mController.onCaptureRetake(); + } + }); + } + } + + public void hideUI() { + mCameraControls.setVisibility(View.INVISIBLE); + mSwitcher.closePopup(); + } + + public void showUI() { + mCameraControls.setVisibility(View.VISIBLE); + } + + public void hideSwitcher() { + mSwitcher.closePopup(); + mSwitcher.setVisibility(View.INVISIBLE); + } + + public void showSwitcher() { + mSwitcher.setVisibility(View.VISIBLE); + } + // called from onResume but only the first time + public void initializeFirstTime() { + // Initialize shutter button. + mShutterButton.setImageResource(R.drawable.btn_new_shutter); + mShutterButton.setOnShutterButtonListener(mController); + mShutterButton.setVisibility(View.VISIBLE); + } + + // called from onResume every other time + public void initializeSecondTime(Camera.Parameters params) { + initializeZoom(params); + if (mController.isImageCaptureIntent()) { + hidePostCaptureAlert(); + } + if (mMenu != null) { + mMenu.reloadPreferences(); + } + } + + public void showLocationDialog() { + mLocationDialog = new AlertDialog.Builder(mActivity) + .setTitle(R.string.remember_location_title) + .setMessage(R.string.remember_location_prompt) + .setPositiveButton(R.string.remember_location_yes, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int arg1) { + mController.enableRecordingLocation(true); + mLocationDialog = null; + } + }) + .setNegativeButton(R.string.remember_location_no, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int arg1) { + dialog.cancel(); + } + }) + .setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + mController.enableRecordingLocation(false); + mLocationDialog = null; + } + }) + .show(); + } + + public void initializeZoom(Camera.Parameters params) { + if ((params == null) || !params.isZoomSupported() + || (mZoomRenderer == null)) return; + mZoomMax = params.getMaxZoom(); + mZoomRatios = params.getZoomRatios(); + // Currently we use immediate zoom for fast zooming to get better UX and + // there is no plan to take advantage of the smooth zoom. + if (mZoomRenderer != null) { + mZoomRenderer.setZoomMax(mZoomMax); + mZoomRenderer.setZoom(params.getZoom()); + mZoomRenderer.setZoomValue(mZoomRatios.get(params.getZoom())); + mZoomRenderer.setOnZoomChangeListener(new ZoomChangeListener()); + } + } + + public void showGpsOnScreenIndicator(boolean hasSignal) { } + + public void hideGpsOnScreenIndicator() { } + + public void overrideSettings(final String ... keyvalues) { + mMenu.overrideSettings(keyvalues); + } + + public void updateOnScreenIndicators(Camera.Parameters params, + PreferenceGroup group, ComboPreferences prefs) { + if (params == null) return; + mOnScreenIndicators.updateSceneOnScreenIndicator(params.getSceneMode()); + mOnScreenIndicators.updateExposureOnScreenIndicator(params, + CameraSettings.readExposure(prefs)); + mOnScreenIndicators.updateFlashOnScreenIndicator(params.getFlashMode()); + int wbIndex = 2; + ListPreference pref = group.findPreference(CameraSettings.KEY_WHITE_BALANCE); + if (pref != null) { + wbIndex = pref.getCurrentIndex(); + } + mOnScreenIndicators.updateWBIndicator(wbIndex); + boolean location = RecordLocationPreference.get( + prefs, mActivity.getContentResolver()); + mOnScreenIndicators.updateLocationIndicator(location); + } + + public void setCameraState(int state) { + } + + public void animateFlash() { + // End the previous animation if the previous one is still running + if (mFlashAnim != null && mFlashAnim.isRunning()) { + mFlashAnim.end(); + } + // Start new flash animation. + mFlashOverlay.setVisibility(View.VISIBLE); + mFlashAnim = ObjectAnimator.ofFloat((Object) mFlashOverlay, "alpha", 0.3f, 0f); + mFlashAnim.setDuration(300); + mFlashAnim.addListener(mAnimatorListener); + mFlashAnim.start(); + } + + public void enableGestures(boolean enable) { + if (mGestures != null) { + mGestures.setEnabled(enable); + } + } + + // forward from preview gestures to controller + @Override + public void onSingleTapUp(View view, int x, int y) { + mController.onSingleTapUp(view, x, y); + } + + public boolean onBackPressed() { + if (mPieRenderer != null && mPieRenderer.showsItems()) { + mPieRenderer.hide(); + return true; + } + // In image capture mode, back button should: + // 1) if there is any popup, dismiss them, 2) otherwise, get out of + // image capture + if (mController.isImageCaptureIntent()) { + if (!removeTopLevelPopup()) { + // no popup to dismiss, cancel image capture + mController.onCaptureCancelled(); + } + return true; + } else if (!mController.isCameraIdle()) { + // ignore backs while we're taking a picture + return true; + } else { + return removeTopLevelPopup(); + } + } + + public void onSwitchMode(boolean toCamera) { + if (toCamera) { + showUI(); + } else { + hideUI(); + } + if (mFaceView != null) { + mFaceView.setBlockDraw(!toCamera); + } + if (mPopup != null) { + dismissPopup(toCamera); + } + if (mGestures != null) { + mGestures.setEnabled(toCamera); + } + if (mRenderOverlay != null) { + // this can not happen in capture mode + mRenderOverlay.setVisibility(toCamera ? View.VISIBLE : View.GONE); + } + if (mPieRenderer != null) { + mPieRenderer.setBlockFocus(!toCamera); + } + setShowMenu(toCamera); + if (!toCamera && mCountDownView != null) mCountDownView.cancelCountDown(); + } + + public void enablePreviewThumb(boolean enabled) { + if (enabled) { + mPreviewThumb.setVisibility(View.VISIBLE); + } else { + mPreviewThumb.setVisibility(View.GONE); + } + } + + public boolean removeTopLevelPopup() { + // Remove the top level popup or dialog box and return true if there's any + if (mPopup != null) { + dismissPopup(); + return true; + } + return false; + } + + public void showPopup(AbstractSettingPopup popup) { + hideUI(); + mBlocker.setVisibility(View.INVISIBLE); + setShowMenu(false); + mPopup = popup; + mPopup.setVisibility(View.VISIBLE); + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + lp.gravity = Gravity.CENTER; + ((FrameLayout) mRootView).addView(mPopup, lp); + } + + public void dismissPopup() { + dismissPopup(true); + } + + private void dismissPopup(boolean fullScreen) { + if (fullScreen) { + showUI(); + mBlocker.setVisibility(View.VISIBLE); + } + setShowMenu(fullScreen); + if (mPopup != null) { + ((FrameLayout) mRootView).removeView(mPopup); + mPopup = null; + } + mMenu.popupDismissed(); + } + + public void onShowSwitcherPopup() { + if (mPieRenderer != null && mPieRenderer.showsItems()) { + mPieRenderer.hide(); + } + } + + private void setShowMenu(boolean show) { + if (mOnScreenIndicators != null) { + mOnScreenIndicators.setVisibility(show ? View.VISIBLE : View.GONE); + } + if (mMenuButton != null) { + mMenuButton.setVisibility(show ? View.VISIBLE : View.GONE); + } + } + + public boolean collapseCameraControls() { + // Remove all the popups/dialog boxes + boolean ret = false; + if (mPopup != null) { + dismissPopup(); + ret = true; + } + onShowSwitcherPopup(); + return ret; + } + + protected void showPostCaptureAlert() { + mOnScreenIndicators.setVisibility(View.GONE); + mMenuButton.setVisibility(View.GONE); + Util.fadeIn(mReviewDoneButton); + mShutterButton.setVisibility(View.INVISIBLE); + Util.fadeIn(mReviewRetakeButton); + pauseFaceDetection(); + } + + protected void hidePostCaptureAlert() { + mOnScreenIndicators.setVisibility(View.VISIBLE); + mMenuButton.setVisibility(View.VISIBLE); + Util.fadeOut(mReviewDoneButton); + mShutterButton.setVisibility(View.VISIBLE); + Util.fadeOut(mReviewRetakeButton); + resumeFaceDetection(); + } + + public void setDisplayOrientation(int orientation) { + if (mFaceView != null) { + mFaceView.setDisplayOrientation(orientation); + } + } + + // shutter button handling + + public boolean isShutterPressed() { + return mShutterButton.isPressed(); + } + + public void enableShutter(boolean enabled) { + if (mShutterButton != null) { + mShutterButton.setEnabled(enabled); + } + } + + public void pressShutterButton() { + if (mShutterButton.isInTouchMode()) { + mShutterButton.requestFocusFromTouch(); + } else { + mShutterButton.requestFocus(); + } + mShutterButton.setPressed(true); + } + + private class ZoomChangeListener implements ZoomRenderer.OnZoomChangedListener { + @Override + public void onZoomValueChanged(int index) { + int newZoom = mController.onZoomChanged(index); + if (mZoomRenderer != null) { + mZoomRenderer.setZoomValue(mZoomRatios.get(newZoom)); + } + } + + @Override + public void onZoomStart() { + if (mPieRenderer != null) { + mPieRenderer.setBlockFocus(true); + } + } + + @Override + public void onZoomEnd() { + if (mPieRenderer != null) { + mPieRenderer.setBlockFocus(false); + } + } + } + + @Override + public void onPieOpened(int centerX, int centerY) { + setSwipingEnabled(false); + dismissPopup(); + if (mFaceView != null) { + mFaceView.setBlockDraw(true); + } + } + + @Override + public void onPieClosed() { + setSwipingEnabled(true); + if (mFaceView != null) { + mFaceView.setBlockDraw(false); + } + } + + public void setSwipingEnabled(boolean enable) { + mActivity.setSwipingEnabled(enable); + } + + public Object getSurfaceTexture() { + synchronized (mLock) { + if (mSurfaceTexture == null) { + try { + mLock.wait(); + } catch (InterruptedException e) { + Log.w(TAG, "Unexpected interruption when waiting to get surface texture"); + } + } + } + return mSurfaceTexture; + } + + // Countdown timer + + private void initializeCountDown() { + mActivity.getLayoutInflater().inflate(R.layout.count_down_to_capture, + (ViewGroup) mRootView, true); + mCountDownView = (CountDownView) (mRootView.findViewById(R.id.count_down_to_capture)); + mCountDownView.setCountDownFinishedListener((OnCountDownFinishedListener) mController); + } + + public boolean isCountingDown() { + return mCountDownView != null && mCountDownView.isCountingDown(); + } + + public void cancelCountDown() { + if (mCountDownView == null) return; + mCountDownView.cancelCountDown(); + } + + public void startCountDown(int sec, boolean playSound) { + if (mCountDownView == null) initializeCountDown(); + mCountDownView.startCountDown(sec, playSound); + } + + public void showPreferencesToast() { + if (mNotSelectableToast == null) { + String str = mActivity.getResources().getString(R.string.not_selectable_in_scene_mode); + mNotSelectableToast = Toast.makeText(mActivity, str, Toast.LENGTH_SHORT); + } + mNotSelectableToast.show(); + } + + public void onPause() { + cancelCountDown(); + + // Clear UI. + collapseCameraControls(); + if (mFaceView != null) mFaceView.clear(); + + if (mLocationDialog != null && mLocationDialog.isShowing()) { + mLocationDialog.dismiss(); + } + mLocationDialog = null; + mPreviewWidth = 0; + mPreviewHeight = 0; + } + + // focus UI implementation + + private FocusIndicator getFocusIndicator() { + return (mFaceView != null && mFaceView.faceExists()) ? mFaceView : mPieRenderer; + } + + @Override + public boolean hasFaces() { + return (mFaceView != null && mFaceView.faceExists()); + } + + public void clearFaces() { + if (mFaceView != null) mFaceView.clear(); + } + + @Override + public void clearFocus() { + FocusIndicator indicator = getFocusIndicator(); + if (indicator != null) indicator.clear(); + } + + @Override + public void setFocusPosition(int x, int y) { + mPieRenderer.setFocus(x, y); + } + + @Override + public void onFocusStarted() { + getFocusIndicator().showStart(); + } + + @Override + public void onFocusSucceeded(boolean timeout) { + getFocusIndicator().showSuccess(timeout); + } + + @Override + public void onFocusFailed(boolean timeout) { + getFocusIndicator().showFail(timeout); + } + + @Override + public void pauseFaceDetection() { + if (mFaceView != null) mFaceView.pause(); + } + + @Override + public void resumeFaceDetection() { + if (mFaceView != null) mFaceView.resume(); + } + + public void onStartFaceDetection(int orientation, boolean mirror) { + mFaceView.clear(); + mFaceView.setVisibility(View.VISIBLE); + mFaceView.setDisplayOrientation(orientation); + mFaceView.setMirror(mirror); + mFaceView.resume(); + } + + @Override + public void onFaceDetection(Face[] faces, CameraManager.CameraProxy camera) { + mFaceView.setFaces(faces); + } + + public void onDisplayChanged() { + mCameraControls.checkLayoutFlip(); + mController.updateCameraOrientation(); + } + +} diff --git a/src/com/android/camera/PieController.java b/src/com/android/camera/PieController.java new file mode 100644 index 000000000..3cbcb4bf5 --- /dev/null +++ b/src/com/android/camera/PieController.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2012 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 android.app.Activity; +import android.graphics.drawable.Drawable; +import android.util.Log; + +import com.android.camera.CameraPreference.OnPreferenceChangedListener; +import com.android.camera.drawable.TextDrawable; +import com.android.camera.ui.PieItem; +import com.android.camera.ui.PieItem.OnClickListener; +import com.android.camera.ui.PieRenderer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class PieController { + + private static String TAG = "CAM_piecontrol"; + + protected static final int MODE_PHOTO = 0; + protected static final int MODE_VIDEO = 1; + + protected static float CENTER = (float) Math.PI / 2; + protected static final float SWEEP = 0.06f; + + protected Activity mActivity; + protected PreferenceGroup mPreferenceGroup; + protected OnPreferenceChangedListener mListener; + protected PieRenderer mRenderer; + private List<IconListPreference> mPreferences; + private Map<IconListPreference, PieItem> mPreferenceMap; + private Map<IconListPreference, String> mOverrides; + + public void setListener(OnPreferenceChangedListener listener) { + mListener = listener; + } + + public PieController(Activity activity, PieRenderer pie) { + mActivity = activity; + mRenderer = pie; + mPreferences = new ArrayList<IconListPreference>(); + mPreferenceMap = new HashMap<IconListPreference, PieItem>(); + mOverrides = new HashMap<IconListPreference, String>(); + } + + public void initialize(PreferenceGroup group) { + mRenderer.clearItems(); + mPreferenceMap.clear(); + setPreferenceGroup(group); + } + + public void onSettingChanged(ListPreference pref) { + if (mListener != null) { + mListener.onSharedPreferenceChanged(); + } + } + + protected void setCameraId(int cameraId) { + ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID); + pref.setValue("" + cameraId); + } + + protected PieItem makeItem(int resId) { + // We need a mutable version as we change the alpha + Drawable d = mActivity.getResources().getDrawable(resId).mutate(); + return new PieItem(d, 0); + } + + protected PieItem makeItem(CharSequence value) { + TextDrawable drawable = new TextDrawable(mActivity.getResources(), value); + return new PieItem(drawable, 0); + } + + public PieItem makeItem(String prefKey) { + final IconListPreference pref = + (IconListPreference) mPreferenceGroup.findPreference(prefKey); + if (pref == null) return null; + int[] iconIds = pref.getLargeIconIds(); + int resid = -1; + if (!pref.getUseSingleIcon() && iconIds != null) { + // Each entry has a corresponding icon. + int index = pref.findIndexOfValue(pref.getValue()); + resid = iconIds[index]; + } else { + // The preference only has a single icon to represent it. + resid = pref.getSingleIcon(); + } + PieItem item = makeItem(resid); + item.setLabel(pref.getTitle().toUpperCase()); + mPreferences.add(pref); + mPreferenceMap.put(pref, item); + int nOfEntries = pref.getEntries().length; + if (nOfEntries > 1) { + for (int i = 0; i < nOfEntries; i++) { + PieItem inner = null; + if (iconIds != null) { + inner = makeItem(iconIds[i]); + } else { + inner = makeItem(pref.getEntries()[i]); + } + inner.setLabel(pref.getLabels()[i]); + item.addItem(inner); + final int index = i; + inner.setOnClickListener(new OnClickListener() { + @Override + public void onClick(PieItem item) { + pref.setValueIndex(index); + reloadPreference(pref); + onSettingChanged(pref); + } + }); + } + } + return item; + } + + public PieItem makeSwitchItem(final String prefKey, boolean addListener) { + final IconListPreference pref = + (IconListPreference) mPreferenceGroup.findPreference(prefKey); + if (pref == null) return null; + int[] iconIds = pref.getLargeIconIds(); + int resid = -1; + int index = pref.findIndexOfValue(pref.getValue()); + if (!pref.getUseSingleIcon() && iconIds != null) { + // Each entry has a corresponding icon. + resid = iconIds[index]; + } else { + // The preference only has a single icon to represent it. + resid = pref.getSingleIcon(); + } + PieItem item = makeItem(resid); + item.setLabel(pref.getLabels()[index]); + item.setImageResource(mActivity, resid); + mPreferences.add(pref); + mPreferenceMap.put(pref, item); + if (addListener) { + final PieItem fitem = item; + item.setOnClickListener(new OnClickListener() { + @Override + public void onClick(PieItem item) { + IconListPreference pref = (IconListPreference) mPreferenceGroup + .findPreference(prefKey); + int index = pref.findIndexOfValue(pref.getValue()); + CharSequence[] values = pref.getEntryValues(); + index = (index + 1) % values.length; + pref.setValueIndex(index); + fitem.setLabel(pref.getLabels()[index]); + fitem.setImageResource(mActivity, + ((IconListPreference) pref).getLargeIconIds()[index]); + reloadPreference(pref); + onSettingChanged(pref); + } + }); + } + return item; + } + + + public PieItem makeDialItem(ListPreference pref, int iconId, float center, float sweep) { + PieItem item = makeItem(iconId); + return item; + } + + public void addItem(String prefKey) { + PieItem item = makeItem(prefKey); + mRenderer.addItem(item); + } + + public void updateItem(PieItem item, String prefKey) { + IconListPreference pref = (IconListPreference) mPreferenceGroup + .findPreference(prefKey); + if (pref != null) { + int index = pref.findIndexOfValue(pref.getValue()); + item.setLabel(pref.getLabels()[index]); + item.setImageResource(mActivity, + ((IconListPreference) pref).getLargeIconIds()[index]); + } + } + + public void setPreferenceGroup(PreferenceGroup group) { + mPreferenceGroup = group; + } + + public void reloadPreferences() { + mPreferenceGroup.reloadValue(); + for (IconListPreference pref : mPreferenceMap.keySet()) { + reloadPreference(pref); + } + } + + private void reloadPreference(IconListPreference pref) { + if (pref.getUseSingleIcon()) return; + PieItem item = mPreferenceMap.get(pref); + String overrideValue = mOverrides.get(pref); + int[] iconIds = pref.getLargeIconIds(); + if (iconIds != null) { + // Each entry has a corresponding icon. + int index; + if (overrideValue == null) { + index = pref.findIndexOfValue(pref.getValue()); + } else { + index = pref.findIndexOfValue(overrideValue); + if (index == -1) { + // Avoid the crash if camera driver has bugs. + Log.e(TAG, "Fail to find override value=" + overrideValue); + pref.print(); + return; + } + } + item.setImageResource(mActivity, iconIds[index]); + } else { + // The preference only has a single icon to represent it. + item.setImageResource(mActivity, pref.getSingleIcon()); + } + } + + // Scene mode may override other camera settings (ex: flash mode). + public void overrideSettings(final String ... keyvalues) { + if (keyvalues.length % 2 != 0) { + throw new IllegalArgumentException(); + } + for (IconListPreference pref : mPreferenceMap.keySet()) { + override(pref, keyvalues); + } + } + + private void override(IconListPreference pref, final String ... keyvalues) { + mOverrides.remove(pref); + for (int i = 0; i < keyvalues.length; i += 2) { + String key = keyvalues[i]; + String value = keyvalues[i + 1]; + if (key.equals(pref.getKey())) { + mOverrides.put(pref, value); + PieItem item = mPreferenceMap.get(pref); + item.setEnabled(value == null); + break; + } + } + reloadPreference(pref); + } +} diff --git a/src/com/android/camera/PreferenceGroup.java b/src/com/android/camera/PreferenceGroup.java new file mode 100644 index 000000000..4d0519f4e --- /dev/null +++ b/src/com/android/camera/PreferenceGroup.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2009 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 android.content.Context; +import android.util.AttributeSet; + +import java.util.ArrayList; + +/** + * A collection of <code>CameraPreference</code>s. It may contain other + * <code>PreferenceGroup</code> and form a tree structure. + */ +public class PreferenceGroup extends CameraPreference { + private ArrayList<CameraPreference> list = + new ArrayList<CameraPreference>(); + + public PreferenceGroup(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void addChild(CameraPreference child) { + list.add(child); + } + + public void removePreference(int index) { + list.remove(index); + } + + public CameraPreference get(int index) { + return list.get(index); + } + + public int size() { + return list.size(); + } + + @Override + public void reloadValue() { + for (CameraPreference pref : list) { + pref.reloadValue(); + } + } + + /** + * Finds the preference with the given key recursively. Returns + * <code>null</code> if cannot find. + */ + public ListPreference findPreference(String key) { + // Find a leaf preference with the given key. Currently, the base + // type of all "leaf" preference is "ListPreference". If we add some + // other types later, we need to change the code. + for (CameraPreference pref : list) { + if (pref instanceof ListPreference) { + ListPreference listPref = (ListPreference) pref; + if(listPref.getKey().equals(key)) return listPref; + } else if(pref instanceof PreferenceGroup) { + ListPreference listPref = + ((PreferenceGroup) pref).findPreference(key); + if (listPref != null) return listPref; + } + } + return null; + } +} diff --git a/src/com/android/camera/PreferenceInflater.java b/src/com/android/camera/PreferenceInflater.java new file mode 100644 index 000000000..231c9833b --- /dev/null +++ b/src/com/android/camera/PreferenceInflater.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2010 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 android.content.Context; +import android.util.AttributeSet; +import android.util.Xml; +import android.view.InflateException; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Inflate <code>CameraPreference</code> from XML resource. + */ +public class PreferenceInflater { + private static final String PACKAGE_NAME = + PreferenceInflater.class.getPackage().getName(); + + private static final Class<?>[] CTOR_SIGNATURE = + new Class[] {Context.class, AttributeSet.class}; + private static final HashMap<String, Constructor<?>> sConstructorMap = + new HashMap<String, Constructor<?>>(); + + private Context mContext; + + public PreferenceInflater(Context context) { + mContext = context; + } + + public CameraPreference inflate(int resId) { + return inflate(mContext.getResources().getXml(resId)); + } + + private CameraPreference newPreference(String tagName, Object[] args) { + String name = PACKAGE_NAME + "." + tagName; + Constructor<?> constructor = sConstructorMap.get(name); + try { + if (constructor == null) { + // Class not found in the cache, see if it's real, and try to + // add it + Class<?> clazz = mContext.getClassLoader().loadClass(name); + constructor = clazz.getConstructor(CTOR_SIGNATURE); + sConstructorMap.put(name, constructor); + } + return (CameraPreference) constructor.newInstance(args); + } catch (NoSuchMethodException e) { + throw new InflateException("Error inflating class " + name, e); + } catch (ClassNotFoundException e) { + throw new InflateException("No such class: " + name, e); + } catch (Exception e) { + throw new InflateException("While create instance of" + name, e); + } + } + + private CameraPreference inflate(XmlPullParser parser) { + + AttributeSet attrs = Xml.asAttributeSet(parser); + ArrayList<CameraPreference> list = new ArrayList<CameraPreference>(); + Object args[] = new Object[]{mContext, attrs}; + + try { + for (int type = parser.next(); + type != XmlPullParser.END_DOCUMENT; type = parser.next()) { + if (type != XmlPullParser.START_TAG) continue; + CameraPreference pref = newPreference(parser.getName(), args); + + int depth = parser.getDepth(); + if (depth > list.size()) { + list.add(pref); + } else { + list.set(depth - 1, pref); + } + if (depth > 1) { + ((PreferenceGroup) list.get(depth - 2)).addChild(pref); + } + } + + if (list.size() == 0) { + throw new InflateException("No root element found"); + } + return list.get(0); + } catch (XmlPullParserException e) { + throw new InflateException(e); + } catch (IOException e) { + throw new InflateException(parser.getPositionDescription(), e); + } + } +} diff --git a/src/com/android/camera/PreviewFrameLayout.java b/src/com/android/camera/PreviewFrameLayout.java new file mode 100644 index 000000000..03ef91c60 --- /dev/null +++ b/src/com/android/camera/PreviewFrameLayout.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2009 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 android.content.Context; +import android.content.res.Configuration; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewStub; +import android.widget.RelativeLayout; + +import com.android.camera.ui.LayoutChangeHelper; +import com.android.camera.ui.LayoutChangeNotifier; +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; + +/** + * A layout which handles the preview aspect ratio. + */ +public class PreviewFrameLayout extends RelativeLayout implements LayoutChangeNotifier { + + private static final String TAG = "CAM_preview"; + + /** A callback to be invoked when the preview frame's size changes. */ + public interface OnSizeChangedListener { + public void onSizeChanged(int width, int height); + } + + private double mAspectRatio; + private View mBorder; + private OnSizeChangedListener mListener; + private LayoutChangeHelper mLayoutChangeHelper; + + public PreviewFrameLayout(Context context, AttributeSet attrs) { + super(context, attrs); + setAspectRatio(4.0 / 3.0); + mLayoutChangeHelper = new LayoutChangeHelper(this); + } + + @Override + protected void onFinishInflate() { + mBorder = findViewById(R.id.preview_border); + } + + public void setAspectRatio(double ratio) { + if (ratio <= 0.0) throw new IllegalArgumentException(); + + if (mAspectRatio != ratio) { + mAspectRatio = ratio; + requestLayout(); + } + } + + public void showBorder(boolean enabled) { + mBorder.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); + } + + public void fadeOutBorder() { + Util.fadeOut(mBorder); + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + int previewWidth = MeasureSpec.getSize(widthSpec); + int previewHeight = MeasureSpec.getSize(heightSpec); + + if (!ApiHelper.HAS_SURFACE_TEXTURE) { + // Get the padding of the border background. + int hPadding = getPaddingLeft() + getPaddingRight(); + int vPadding = getPaddingTop() + getPaddingBottom(); + + // Resize the preview frame with correct aspect ratio. + previewWidth -= hPadding; + previewHeight -= vPadding; + + boolean widthLonger = previewWidth > previewHeight; + int longSide = (widthLonger ? previewWidth : previewHeight); + int shortSide = (widthLonger ? previewHeight : previewWidth); + if (longSide > shortSide * mAspectRatio) { + longSide = (int) ((double) shortSide * mAspectRatio); + } else { + shortSide = (int) ((double) longSide / mAspectRatio); + } + if (widthLonger) { + previewWidth = longSide; + previewHeight = shortSide; + } else { + previewWidth = shortSide; + previewHeight = longSide; + } + + // Add the padding of the border. + previewWidth += hPadding; + previewHeight += vPadding; + } + + // Ask children to follow the new preview dimension. + super.onMeasure(MeasureSpec.makeMeasureSpec(previewWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(previewHeight, MeasureSpec.EXACTLY)); + } + + public void setOnSizeChangedListener(OnSizeChangedListener listener) { + mListener = listener; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + if (mListener != null) mListener.onSizeChanged(w, h); + } + + @Override + public void setOnLayoutChangeListener( + LayoutChangeNotifier.Listener listener) { + mLayoutChangeHelper.setOnLayoutChangeListener(listener); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mLayoutChangeHelper.onLayout(changed, l, t, r, b); + } +} diff --git a/src/com/android/camera/PreviewGestures.java b/src/com/android/camera/PreviewGestures.java new file mode 100644 index 000000000..466172b7c --- /dev/null +++ b/src/com/android/camera/PreviewGestures.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2013 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 android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.View; + +import com.android.camera.ui.PieRenderer; +import com.android.camera.ui.RenderOverlay; +import com.android.camera.ui.ZoomRenderer; + +/* PreviewGestures disambiguates touch events received on RenderOverlay + * and dispatch them to the proper recipient (i.e. zoom renderer or pie renderer). + * Touch events on CameraControls will be handled by framework. + * */ +public class PreviewGestures + implements ScaleGestureDetector.OnScaleGestureListener { + + private static final String TAG = "CAM_gestures"; + + private static final int MODE_NONE = 0; + private static final int MODE_ZOOM = 2; + + public static final int DIR_UP = 0; + public static final int DIR_DOWN = 1; + public static final int DIR_LEFT = 2; + public static final int DIR_RIGHT = 3; + + private SingleTapListener mTapListener; + private RenderOverlay mOverlay; + private PieRenderer mPie; + private ZoomRenderer mZoom; + private MotionEvent mDown; + private MotionEvent mCurrent; + private ScaleGestureDetector mScale; + private int mMode; + private boolean mZoomEnabled; + private boolean mEnabled; + private boolean mZoomOnly; + private GestureDetector mGestureDetector; + + private GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() { + @Override + public void onLongPress (MotionEvent e) { + // Open pie + if (!mZoomOnly && mPie != null && !mPie.showsItems()) { + openPie(); + } + } + + @Override + public boolean onSingleTapUp (MotionEvent e) { + // Tap to focus when pie is not open + if (mPie == null || !mPie.showsItems()) { + mTapListener.onSingleTapUp(null, (int) e.getX(), (int) e.getY()); + return true; + } + return false; + } + + @Override + public boolean onScroll (MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + if (mZoomOnly || mMode == MODE_ZOOM) return false; + int deltaX = (int) (e1.getX() - e2.getX()); + int deltaY = (int) (e1.getY() - e2.getY()); + if (deltaY > 2 * deltaX && deltaY > -2 * deltaX) { + // Open pie on swipe up + if (mPie != null && !mPie.showsItems()) { + openPie(); + return true; + } + } + return false; + } + }; + + public interface SingleTapListener { + public void onSingleTapUp(View v, int x, int y); + } + + public PreviewGestures(CameraActivity ctx, SingleTapListener tapListener, + ZoomRenderer zoom, PieRenderer pie) { + mTapListener = tapListener; + mPie = pie; + mZoom = zoom; + mMode = MODE_NONE; + mScale = new ScaleGestureDetector(ctx, this); + mEnabled = true; + mGestureDetector = new GestureDetector(mGestureListener); + } + + public void setRenderOverlay(RenderOverlay overlay) { + mOverlay = overlay; + } + + public void setEnabled(boolean enabled) { + mEnabled = enabled; + } + + public void setZoomEnabled(boolean enable) { + mZoomEnabled = enable; + } + + public void setZoomOnly(boolean zoom) { + mZoomOnly = zoom; + } + + public boolean isEnabled() { + return mEnabled; + } + + public boolean dispatchTouch(MotionEvent m) { + if (!mEnabled) { + return false; + } + mCurrent = m; + if (MotionEvent.ACTION_DOWN == m.getActionMasked()) { + mMode = MODE_NONE; + mDown = MotionEvent.obtain(m); + } + + // If pie is open, redirects all the touch events to pie. + if (mPie != null && mPie.isOpen()) { + return sendToPie(m); + } + + // If pie is not open, send touch events to gesture detector and scale + // listener to recognize the gesture. + mGestureDetector.onTouchEvent(m); + if (mZoom != null) { + mScale.onTouchEvent(m); + if (MotionEvent.ACTION_POINTER_DOWN == m.getActionMasked()) { + mMode = MODE_ZOOM; + if (mZoomEnabled) { + // Start showing zoom UI as soon as there is a second finger down + mZoom.onScaleBegin(mScale); + } + } else if (MotionEvent.ACTION_POINTER_UP == m.getActionMasked()) { + mZoom.onScaleEnd(mScale); + } + } + return true; + } + + private MotionEvent makeCancelEvent(MotionEvent m) { + MotionEvent c = MotionEvent.obtain(m); + c.setAction(MotionEvent.ACTION_CANCEL); + return c; + } + + private void openPie() { + mGestureDetector.onTouchEvent(makeCancelEvent(mDown)); + mScale.onTouchEvent(makeCancelEvent(mDown)); + mOverlay.directDispatchTouch(mDown, mPie); + } + + private boolean sendToPie(MotionEvent m) { + return mOverlay.directDispatchTouch(m, mPie); + } + + // OnScaleGestureListener implementation + @Override + public boolean onScale(ScaleGestureDetector detector) { + return mZoom.onScale(detector); + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + if (mPie == null || !mPie.isOpen()) { + mMode = MODE_ZOOM; + mGestureDetector.onTouchEvent(makeCancelEvent(mCurrent)); + if (!mZoomEnabled) return false; + return mZoom.onScaleBegin(detector); + } + return false; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mZoom.onScaleEnd(detector); + } +} + diff --git a/src/com/android/camera/ProxyLauncher.java b/src/com/android/camera/ProxyLauncher.java new file mode 100644 index 000000000..8c566214c --- /dev/null +++ b/src/com/android/camera/ProxyLauncher.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2012 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 android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +public class ProxyLauncher extends Activity { + + public static final int RESULT_USER_CANCELED = -2; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState == null) { + Intent intent = getIntent().getParcelableExtra(Intent.EXTRA_INTENT); + startActivityForResult(intent, 0); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_CANCELED) { + resultCode = RESULT_USER_CANCELED; + } + setResult(resultCode, data); + finish(); + } + +} diff --git a/src/com/android/camera/RecordLocationPreference.java b/src/com/android/camera/RecordLocationPreference.java new file mode 100644 index 000000000..9992afabb --- /dev/null +++ b/src/com/android/camera/RecordLocationPreference.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2009 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 android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.util.AttributeSet; + +/** + * {@code RecordLocationPreference} is used to keep the "store locaiton" + * option in {@code SharedPreference}. + */ +public class RecordLocationPreference extends IconListPreference { + + public static final String VALUE_NONE = "none"; + public static final String VALUE_ON = "on"; + public static final String VALUE_OFF = "off"; + + private final ContentResolver mResolver; + + public RecordLocationPreference(Context context, AttributeSet attrs) { + super(context, attrs); + mResolver = context.getContentResolver(); + } + + @Override + public String getValue() { + return get(getSharedPreferences(), mResolver) ? VALUE_ON : VALUE_OFF; + } + + public static boolean get( + SharedPreferences pref, ContentResolver resolver) { + String value = pref.getString( + CameraSettings.KEY_RECORD_LOCATION, VALUE_NONE); + return VALUE_ON.equals(value); + } + + public static boolean isSet(SharedPreferences pref) { + String value = pref.getString( + CameraSettings.KEY_RECORD_LOCATION, VALUE_NONE); + return !VALUE_NONE.equals(value); + } +} diff --git a/src/com/android/camera/RotateDialogController.java b/src/com/android/camera/RotateDialogController.java new file mode 100644 index 000000000..5d5e5e70f --- /dev/null +++ b/src/com/android/camera/RotateDialogController.java @@ -0,0 +1,169 @@ +/* + * 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 android.app.Activity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.android.camera.ui.Rotatable; +import com.android.camera.ui.RotateLayout; +import com.android.gallery3d.R; + +public class RotateDialogController implements Rotatable { + + @SuppressWarnings("unused") + private static final String TAG = "RotateDialogController"; + private static final long ANIM_DURATION = 150; // millis + + private Activity mActivity; + private int mLayoutResourceID; + private View mDialogRootLayout; + private RotateLayout mRotateDialog; + private View mRotateDialogTitleLayout; + private View mRotateDialogButtonLayout; + private TextView mRotateDialogTitle; + private ProgressBar mRotateDialogSpinner; + private TextView mRotateDialogText; + private TextView mRotateDialogButton1; + private TextView mRotateDialogButton2; + + private Animation mFadeInAnim, mFadeOutAnim; + + public RotateDialogController(Activity a, int layoutResource) { + mActivity = a; + mLayoutResourceID = layoutResource; + } + + private void inflateDialogLayout() { + if (mDialogRootLayout == null) { + ViewGroup layoutRoot = (ViewGroup) mActivity.getWindow().getDecorView(); + LayoutInflater inflater = mActivity.getLayoutInflater(); + View v = inflater.inflate(mLayoutResourceID, layoutRoot); + mDialogRootLayout = v.findViewById(R.id.rotate_dialog_root_layout); + mRotateDialog = (RotateLayout) v.findViewById(R.id.rotate_dialog_layout); + mRotateDialogTitleLayout = v.findViewById(R.id.rotate_dialog_title_layout); + mRotateDialogButtonLayout = v.findViewById(R.id.rotate_dialog_button_layout); + mRotateDialogTitle = (TextView) v.findViewById(R.id.rotate_dialog_title); + mRotateDialogSpinner = (ProgressBar) v.findViewById(R.id.rotate_dialog_spinner); + mRotateDialogText = (TextView) v.findViewById(R.id.rotate_dialog_text); + mRotateDialogButton1 = (Button) v.findViewById(R.id.rotate_dialog_button1); + mRotateDialogButton2 = (Button) v.findViewById(R.id.rotate_dialog_button2); + + mFadeInAnim = AnimationUtils.loadAnimation( + mActivity, android.R.anim.fade_in); + mFadeOutAnim = AnimationUtils.loadAnimation( + mActivity, android.R.anim.fade_out); + mFadeInAnim.setDuration(ANIM_DURATION); + mFadeOutAnim.setDuration(ANIM_DURATION); + } + } + + @Override + public void setOrientation(int orientation, boolean animation) { + inflateDialogLayout(); + mRotateDialog.setOrientation(orientation, animation); + } + + public void resetRotateDialog() { + inflateDialogLayout(); + mRotateDialogTitleLayout.setVisibility(View.GONE); + mRotateDialogSpinner.setVisibility(View.GONE); + mRotateDialogButton1.setVisibility(View.GONE); + mRotateDialogButton2.setVisibility(View.GONE); + mRotateDialogButtonLayout.setVisibility(View.GONE); + } + + private void fadeOutDialog() { + mDialogRootLayout.startAnimation(mFadeOutAnim); + mDialogRootLayout.setVisibility(View.GONE); + } + + private void fadeInDialog() { + mDialogRootLayout.startAnimation(mFadeInAnim); + mDialogRootLayout.setVisibility(View.VISIBLE); + } + + public void dismissDialog() { + if (mDialogRootLayout != null && mDialogRootLayout.getVisibility() != View.GONE) { + fadeOutDialog(); + } + } + + public void showAlertDialog(String title, String msg, String button1Text, + final Runnable r1, String button2Text, final Runnable r2) { + resetRotateDialog(); + + if (title != null) { + mRotateDialogTitle.setText(title); + mRotateDialogTitleLayout.setVisibility(View.VISIBLE); + } + + mRotateDialogText.setText(msg); + + if (button1Text != null) { + mRotateDialogButton1.setText(button1Text); + mRotateDialogButton1.setContentDescription(button1Text); + mRotateDialogButton1.setVisibility(View.VISIBLE); + mRotateDialogButton1.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (r1 != null) r1.run(); + dismissDialog(); + } + }); + mRotateDialogButtonLayout.setVisibility(View.VISIBLE); + } + if (button2Text != null) { + mRotateDialogButton2.setText(button2Text); + mRotateDialogButton2.setContentDescription(button2Text); + mRotateDialogButton2.setVisibility(View.VISIBLE); + mRotateDialogButton2.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (r2 != null) r2.run(); + dismissDialog(); + } + }); + mRotateDialogButtonLayout.setVisibility(View.VISIBLE); + } + + fadeInDialog(); + } + + public void showWaitingDialog(String msg) { + resetRotateDialog(); + + mRotateDialogText.setText(msg); + mRotateDialogSpinner.setVisibility(View.VISIBLE); + + fadeInDialog(); + } + + public int getVisibility() { + if (mDialogRootLayout != null) { + return mDialogRootLayout.getVisibility(); + } + return View.INVISIBLE; + } +} diff --git a/src/com/android/camera/SecureCameraActivity.java b/src/com/android/camera/SecureCameraActivity.java new file mode 100644 index 000000000..2fa68f8e6 --- /dev/null +++ b/src/com/android/camera/SecureCameraActivity.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2012 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; + +// Use a different activity for secure camera only. So it can have a different +// task affinity from others. This makes sure non-secure camera activity is not +// started in secure lock screen. +public class SecureCameraActivity extends CameraActivity { +} diff --git a/src/com/android/camera/ShutterButton.java b/src/com/android/camera/ShutterButton.java new file mode 100755 index 000000000..a1bbb1a0d --- /dev/null +++ b/src/com/android/camera/ShutterButton.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2008 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 android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ImageView; + +/** + * A button designed to be used for the on-screen shutter button. + * It's currently an {@code ImageView} that can call a delegate when the + * pressed state changes. + */ +public class ShutterButton extends ImageView { + + private boolean mTouchEnabled = true; + + /** + * A callback to be invoked when a ShutterButton's pressed state changes. + */ + public interface OnShutterButtonListener { + /** + * Called when a ShutterButton has been pressed. + * + * @param pressed The ShutterButton that was pressed. + */ + void onShutterButtonFocus(boolean pressed); + void onShutterButtonClick(); + } + + private OnShutterButtonListener mListener; + private boolean mOldPressed; + + public ShutterButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setOnShutterButtonListener(OnShutterButtonListener listener) { + mListener = listener; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent m) { + if (mTouchEnabled) { + return super.dispatchTouchEvent(m); + } else { + return false; + } + } + + public void enableTouch(boolean enable) { + mTouchEnabled = enable; + } + + /** + * Hook into the drawable state changing to get changes to isPressed -- the + * onPressed listener doesn't always get called when the pressed state + * changes. + */ + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + final boolean pressed = isPressed(); + if (pressed != mOldPressed) { + if (!pressed) { + // When pressing the physical camera button the sequence of + // events is: + // focus pressed, optional camera pressed, focus released. + // We want to emulate this sequence of events with the shutter + // button. When clicking using a trackball button, the view + // system changes the drawable state before posting click + // notification, so the sequence of events is: + // pressed(true), optional click, pressed(false) + // When clicking using touch events, the view system changes the + // drawable state after posting click notification, so the + // sequence of events is: + // pressed(true), pressed(false), optional click + // Since we're emulating the physical camera button, we want to + // have the same order of events. So we want the optional click + // callback to be delivered before the pressed(false) callback. + // + // To do this, we delay the posting of the pressed(false) event + // slightly by pushing it on the event queue. This moves it + // after the optional click notification, so our client always + // sees events in this sequence: + // pressed(true), optional click, pressed(false) + post(new Runnable() { + @Override + public void run() { + callShutterButtonFocus(pressed); + } + }); + } else { + callShutterButtonFocus(pressed); + } + mOldPressed = pressed; + } + } + + private void callShutterButtonFocus(boolean pressed) { + if (mListener != null) { + mListener.onShutterButtonFocus(pressed); + } + } + + @Override + public boolean performClick() { + boolean result = super.performClick(); + if (mListener != null && getVisibility() == View.VISIBLE) { + mListener.onShutterButtonClick(); + } + return result; + } +} diff --git a/src/com/android/camera/SoundClips.java b/src/com/android/camera/SoundClips.java new file mode 100644 index 000000000..8155c03dc --- /dev/null +++ b/src/com/android/camera/SoundClips.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2012 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 android.annotation.TargetApi; +import android.content.Context; +import android.media.AudioManager; +import android.media.MediaActionSound; +import android.media.SoundPool; +import android.util.Log; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; + +/* + * This class controls the sound playback according to the API level. + */ +public class SoundClips { + // Sound actions. + public static final int FOCUS_COMPLETE = 0; + public static final int START_VIDEO_RECORDING = 1; + public static final int STOP_VIDEO_RECORDING = 2; + + public interface Player { + public void release(); + public void play(int action); + } + + public static Player getPlayer(Context context) { + if (ApiHelper.HAS_MEDIA_ACTION_SOUND) { + return new MediaActionSoundPlayer(); + } else { + return new SoundPoolPlayer(context); + } + } + + public static int getAudioTypeForSoundPool() { + return ApiHelper.getIntFieldIfExists(AudioManager.class, + "STREAM_SYSTEM_ENFORCED", null, AudioManager.STREAM_RING); + } + + /** + * This class implements SoundClips.Player using MediaActionSound, + * which exists since API level 16. + */ + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + private static class MediaActionSoundPlayer implements Player { + private static final String TAG = "MediaActionSoundPlayer"; + private MediaActionSound mSound; + + @Override + public void release() { + if (mSound != null) { + mSound.release(); + mSound = null; + } + } + + public MediaActionSoundPlayer() { + mSound = new MediaActionSound(); + mSound.load(MediaActionSound.START_VIDEO_RECORDING); + mSound.load(MediaActionSound.STOP_VIDEO_RECORDING); + mSound.load(MediaActionSound.FOCUS_COMPLETE); + } + + @Override + public synchronized void play(int action) { + switch(action) { + case FOCUS_COMPLETE: + mSound.play(MediaActionSound.FOCUS_COMPLETE); + break; + case START_VIDEO_RECORDING: + mSound.play(MediaActionSound.START_VIDEO_RECORDING); + break; + case STOP_VIDEO_RECORDING: + mSound.play(MediaActionSound.STOP_VIDEO_RECORDING); + break; + default: + Log.w(TAG, "Unrecognized action:" + action); + } + } + } + + /** + * This class implements SoundClips.Player using SoundPool, which + * exists since API level 1. + */ + private static class SoundPoolPlayer implements + Player, SoundPool.OnLoadCompleteListener { + + private static final String TAG = "SoundPoolPlayer"; + private static final int NUM_SOUND_STREAMS = 1; + private static final int[] SOUND_RES = { // Soundtrack res IDs. + R.raw.focus_complete, + R.raw.video_record + }; + + // ID returned by load() should be non-zero. + private static final int ID_NOT_LOADED = 0; + + // Maps a sound action to the id; + private final int[] mSoundRes = {0, 1, 1}; + // Store the context for lazy loading. + private Context mContext; + // mSoundPool is created every time load() is called and cleared every + // time release() is called. + private SoundPool mSoundPool; + // Sound ID of each sound resources. Given when the sound is loaded. + private final int[] mSoundIDs; + private final boolean[] mSoundIDReady; + private int mSoundIDToPlay; + + public SoundPoolPlayer(Context context) { + mContext = context; + + mSoundIDToPlay = ID_NOT_LOADED; + + mSoundPool = new SoundPool(NUM_SOUND_STREAMS, getAudioTypeForSoundPool(), 0); + mSoundPool.setOnLoadCompleteListener(this); + + mSoundIDs = new int[SOUND_RES.length]; + mSoundIDReady = new boolean[SOUND_RES.length]; + for (int i = 0; i < SOUND_RES.length; i++) { + mSoundIDs[i] = mSoundPool.load(mContext, SOUND_RES[i], 1); + mSoundIDReady[i] = false; + } + } + + @Override + public synchronized void release() { + if (mSoundPool != null) { + mSoundPool.release(); + mSoundPool = null; + } + } + + @Override + public synchronized void play(int action) { + if (action < 0 || action >= mSoundRes.length) { + Log.e(TAG, "Resource ID not found for action:" + action + " in play()."); + return; + } + + int index = mSoundRes[action]; + if (mSoundIDs[index] == ID_NOT_LOADED) { + // Not loaded yet, load first and then play when the loading is complete. + mSoundIDs[index] = mSoundPool.load(mContext, SOUND_RES[index], 1); + mSoundIDToPlay = mSoundIDs[index]; + } else if (!mSoundIDReady[index]) { + // Loading and not ready yet. + mSoundIDToPlay = mSoundIDs[index]; + } else { + mSoundPool.play(mSoundIDs[index], 1f, 1f, 0, 0, 1f); + } + } + + @Override + public void onLoadComplete(SoundPool pool, int soundID, int status) { + if (status != 0) { + Log.e(TAG, "loading sound tracks failed (status=" + status + ")"); + for (int i = 0; i < mSoundIDs.length; i++ ) { + if (mSoundIDs[i] == soundID) { + mSoundIDs[i] = ID_NOT_LOADED; + break; + } + } + return; + } + + for (int i = 0; i < mSoundIDs.length; i++ ) { + if (mSoundIDs[i] == soundID) { + mSoundIDReady[i] = true; + break; + } + } + + if (soundID == mSoundIDToPlay) { + mSoundIDToPlay = ID_NOT_LOADED; + mSoundPool.play(soundID, 1f, 1f, 0, 0, 1f); + } + } + } +} diff --git a/src/com/android/camera/StaticBitmapScreenNail.java b/src/com/android/camera/StaticBitmapScreenNail.java new file mode 100644 index 000000000..10788c0fb --- /dev/null +++ b/src/com/android/camera/StaticBitmapScreenNail.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2012 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 android.graphics.Bitmap; + +import com.android.gallery3d.ui.BitmapScreenNail; + +public class StaticBitmapScreenNail extends BitmapScreenNail { + public StaticBitmapScreenNail(Bitmap bitmap) { + super(bitmap); + } + + @Override + public void recycle() { + // Always keep the bitmap in memory. + } +} diff --git a/src/com/android/camera/Storage.java b/src/com/android/camera/Storage.java new file mode 100644 index 000000000..ba995edef --- /dev/null +++ b/src/com/android/camera/Storage.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2010 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 android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.location.Location; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.os.StatFs; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; +import android.provider.MediaStore.MediaColumns; +import android.util.Log; + +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.exif.ExifInterface; + +import java.io.File; +import java.io.FileOutputStream; + +public class Storage { + private static final String TAG = "CameraStorage"; + + public static final String DCIM = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString(); + + public static final String DIRECTORY = DCIM + "/Camera"; + + // Match the code in MediaProvider.computeBucketValues(). + public static final String BUCKET_ID = + String.valueOf(DIRECTORY.toLowerCase().hashCode()); + + public static final long UNAVAILABLE = -1L; + public static final long PREPARING = -2L; + public static final long UNKNOWN_SIZE = -3L; + public static final long LOW_STORAGE_THRESHOLD = 50000000; + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private static void setImageSize(ContentValues values, int width, int height) { + // The two fields are available since ICS but got published in JB + if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) { + values.put(MediaColumns.WIDTH, width); + values.put(MediaColumns.HEIGHT, height); + } + } + + public static void writeFile(String path, byte[] data) { + FileOutputStream out = null; + try { + out = new FileOutputStream(path); + out.write(data); + } catch (Exception e) { + Log.e(TAG, "Failed to write data", e); + } finally { + try { + out.close(); + } catch (Exception e) { + } + } + } + + // Save the image and add it to media store. + public static Uri addImage(ContentResolver resolver, String title, + long date, Location location, int orientation, ExifInterface exif, + byte[] jpeg, int width, int height) { + // Save the image. + String path = generateFilepath(title); + if (exif != null) { + try { + exif.writeExif(jpeg, path); + } catch (Exception e) { + Log.e(TAG, "Failed to write data", e); + } + } else { + writeFile(path, jpeg); + } + return addImage(resolver, title, date, location, orientation, + jpeg.length, path, width, height); + } + + // Add the image to media store. + public static Uri addImage(ContentResolver resolver, String title, + long date, Location location, int orientation, int jpegLength, + String path, int width, int height) { + // Insert into MediaStore. + ContentValues values = new ContentValues(9); + values.put(ImageColumns.TITLE, title); + values.put(ImageColumns.DISPLAY_NAME, title + ".jpg"); + values.put(ImageColumns.DATE_TAKEN, date); + values.put(ImageColumns.MIME_TYPE, "image/jpeg"); + // Clockwise rotation in degrees. 0, 90, 180, or 270. + values.put(ImageColumns.ORIENTATION, orientation); + values.put(ImageColumns.DATA, path); + values.put(ImageColumns.SIZE, jpegLength); + + setImageSize(values, width, height); + + if (location != null) { + values.put(ImageColumns.LATITUDE, location.getLatitude()); + values.put(ImageColumns.LONGITUDE, location.getLongitude()); + } + + Uri uri = null; + try { + uri = resolver.insert(Images.Media.EXTERNAL_CONTENT_URI, values); + } catch (Throwable th) { + // This can happen when the external volume is already mounted, but + // MediaScanner has not notify MediaProvider to add that volume. + // The picture is still safe and MediaScanner will find it and + // insert it into MediaProvider. The only problem is that the user + // cannot click the thumbnail to review the picture. + Log.e(TAG, "Failed to write MediaStore" + th); + } + return uri; + } + + public static void deleteImage(ContentResolver resolver, Uri uri) { + try { + resolver.delete(uri, null, null); + } catch (Throwable th) { + Log.e(TAG, "Failed to delete image: " + uri); + } + } + + public static String generateFilepath(String title) { + return DIRECTORY + '/' + title + ".jpg"; + } + + public static long getAvailableSpace() { + String state = Environment.getExternalStorageState(); + Log.d(TAG, "External storage state=" + state); + if (Environment.MEDIA_CHECKING.equals(state)) { + return PREPARING; + } + if (!Environment.MEDIA_MOUNTED.equals(state)) { + return UNAVAILABLE; + } + + File dir = new File(DIRECTORY); + dir.mkdirs(); + if (!dir.isDirectory() || !dir.canWrite()) { + return UNAVAILABLE; + } + + try { + StatFs stat = new StatFs(DIRECTORY); + return stat.getAvailableBlocks() * (long) stat.getBlockSize(); + } catch (Exception e) { + Log.i(TAG, "Fail to access external storage", e); + } + return UNKNOWN_SIZE; + } + + /** + * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be + * imported. This is a temporary fix for bug#1655552. + */ + public static void ensureOSXCompatible() { + File nnnAAAAA = new File(DCIM, "100ANDRO"); + if (!(nnnAAAAA.exists() || nnnAAAAA.mkdirs())) { + Log.e(TAG, "Failed to create " + nnnAAAAA.getPath()); + } + } +} diff --git a/src/com/android/camera/SurfaceTextureRenderer.java b/src/com/android/camera/SurfaceTextureRenderer.java new file mode 100644 index 000000000..66f7aa219 --- /dev/null +++ b/src/com/android/camera/SurfaceTextureRenderer.java @@ -0,0 +1,224 @@ +/* + * 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 android.graphics.SurfaceTexture; +import android.os.Handler; +import android.util.Log; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL10; + +public class SurfaceTextureRenderer { + + public interface FrameDrawer { + public void onDrawFrame(GL10 gl); + } + + private static final String TAG = "CAM_" + SurfaceTextureRenderer.class.getSimpleName(); + private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + + private EGLConfig mEglConfig; + private EGLDisplay mEglDisplay; + private EGLContext mEglContext; + private EGLSurface mEglSurface; + private EGL10 mEgl; + private GL10 mGl; + + private Handler mEglHandler; + private FrameDrawer mFrameDrawer; + + private Object mRenderLock = new Object(); + private Runnable mRenderTask = new Runnable() { + @Override + public void run() { + synchronized (mRenderLock) { + mFrameDrawer.onDrawFrame(mGl); + mEgl.eglSwapBuffers(mEglDisplay, mEglSurface); + mRenderLock.notifyAll(); + } + } + }; + + public class RenderThread extends Thread { + private Boolean mRenderStopped = false; + + @Override + public void run() { + while (true) { + synchronized (mRenderStopped) { + if (mRenderStopped) return; + } + draw(true); + } + } + + public void stopRender() { + synchronized (mRenderStopped) { + mRenderStopped = true; + } + } + } + + public SurfaceTextureRenderer(SurfaceTexture tex, + Handler handler, FrameDrawer renderer) { + mEglHandler = handler; + mFrameDrawer = renderer; + + initialize(tex); + } + + public RenderThread createRenderThread() { + return new RenderThread(); + } + + public void release() { + mEglHandler.post(new Runnable() { + @Override + public void run() { + mEgl.eglDestroySurface(mEglDisplay, mEglSurface); + mEgl.eglDestroyContext(mEglDisplay, mEglContext); + mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_CONTEXT); + mEgl.eglTerminate(mEglDisplay); + mEglSurface = null; + mEglContext = null; + mEglDisplay = null; + } + }); + } + + /** + * Posts a render request to the GL thread. + * @param sync set <code>true</code> if the caller needs it to be + * a synchronous call. + */ + public void draw(boolean sync) { + synchronized (mRenderLock) { + mEglHandler.post(mRenderTask); + if (sync) { + try { + mRenderLock.wait(); + } catch (InterruptedException ex) { + Log.v(TAG, "RenderLock.wait() interrupted"); + } + } + } + } + + private void initialize(final SurfaceTexture target) { + mEglHandler.post(new Runnable() { + @Override + public void run() { + mEgl = (EGL10) EGLContext.getEGL(); + mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + if (mEglDisplay == EGL10.EGL_NO_DISPLAY) { + throw new RuntimeException("eglGetDisplay failed"); + } + int[] version = new int[2]; + if (!mEgl.eglInitialize(mEglDisplay, version)) { + throw new RuntimeException("eglInitialize failed"); + } else { + Log.v(TAG, "EGL version: " + version[0] + '.' + version[1]); + } + int[] attribList = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE }; + mEglConfig = chooseConfig(mEgl, mEglDisplay); + mEglContext = mEgl.eglCreateContext( + mEglDisplay, mEglConfig, EGL10.EGL_NO_CONTEXT, attribList); + + if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) { + throw new RuntimeException("failed to createContext"); + } + mEglSurface = mEgl.eglCreateWindowSurface( + mEglDisplay, mEglConfig, target, null); + if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) { + throw new RuntimeException("failed to createWindowSurface"); + } + + if (!mEgl.eglMakeCurrent( + mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { + throw new RuntimeException("failed to eglMakeCurrent"); + } + + mGl = (GL10) mEglContext.getGL(); + } + }); + waitDone(); + } + + private void waitDone() { + final Object lock = new Object(); + synchronized (lock) { + mEglHandler.post(new Runnable() { + @Override + public void run() { + synchronized (lock) { + lock.notifyAll(); + } + } + }); + try { + lock.wait(); + } catch (InterruptedException ex) { + Log.v(TAG, "waitDone() interrupted"); + } + } + } + + private static void checkEglError(String prompt, EGL10 egl) { + int error; + while ((error = egl.eglGetError()) != EGL10.EGL_SUCCESS) { + Log.e(TAG, String.format("%s: EGL error: 0x%x", prompt, error)); + } + } + + private static final int EGL_OPENGL_ES2_BIT = 4; + private static final int[] CONFIG_SPEC = new int[] { + EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL10.EGL_RED_SIZE, 8, + EGL10.EGL_GREEN_SIZE, 8, + EGL10.EGL_BLUE_SIZE, 8, + EGL10.EGL_ALPHA_SIZE, 0, + EGL10.EGL_DEPTH_SIZE, 0, + EGL10.EGL_STENCIL_SIZE, 0, + EGL10.EGL_NONE + }; + + private static EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + int[] numConfig = new int[1]; + if (!egl.eglChooseConfig(display, CONFIG_SPEC, null, 0, numConfig)) { + throw new IllegalArgumentException("eglChooseConfig failed"); + } + + int numConfigs = numConfig[0]; + if (numConfigs <= 0) { + throw new IllegalArgumentException("No configs match configSpec"); + } + + EGLConfig[] configs = new EGLConfig[numConfigs]; + if (!egl.eglChooseConfig( + display, CONFIG_SPEC, configs, numConfigs, numConfig)) { + throw new IllegalArgumentException("eglChooseConfig#2 failed"); + } + + return configs[0]; + } +} diff --git a/src/com/android/camera/SwitchAnimManager.java b/src/com/android/camera/SwitchAnimManager.java new file mode 100644 index 000000000..6ec88223e --- /dev/null +++ b/src/com/android/camera/SwitchAnimManager.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2012 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 android.os.SystemClock; +import android.util.Log; + +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.RawTexture; + +/** + * Class to handle the animation when switching between back and front cameras. + * An image of the previous camera zooms in and fades out. The preview of the + * new camera zooms in and fades in. The image of the previous camera is called + * review in this class. + */ +public class SwitchAnimManager { + private static final String TAG = "SwitchAnimManager"; + // The amount of change for zooming in and out. + private static final float ZOOM_DELTA_PREVIEW = 0.2f; + private static final float ZOOM_DELTA_REVIEW = 0.5f; + private static final float ANIMATION_DURATION = 400; // ms + public static final float INITIAL_DARKEN_ALPHA = 0.8f; + + private long mAnimStartTime; // milliseconds. + // The drawing width and height of the review image. This is saved when the + // texture is copied. + private int mReviewDrawingWidth; + private int mReviewDrawingHeight; + // The maximum width of the camera screen nail width from onDraw. We need to + // know how much the preview is scaled and scale the review the same amount. + // For example, the preview is not full screen in film strip mode. + private int mPreviewFrameLayoutWidth; + + public SwitchAnimManager() { + } + + public void setReviewDrawingSize(int width, int height) { + mReviewDrawingWidth = width; + mReviewDrawingHeight = height; + } + + // width: the width of PreviewFrameLayout view. + // height: the height of PreviewFrameLayout view. Not used. Kept for + // consistency. + public void setPreviewFrameLayoutSize(int width, int height) { + mPreviewFrameLayoutWidth = width; + } + + // w and h: the rectangle area where the animation takes place. + public void startAnimation() { + mAnimStartTime = SystemClock.uptimeMillis(); + } + + // Returns true if the animation has been drawn. + // preview: camera preview view. + // review: snapshot of the preview before switching the camera. + public boolean drawAnimation(GLCanvas canvas, int x, int y, int width, + int height, CameraScreenNail preview, RawTexture review) { + long timeDiff = SystemClock.uptimeMillis() - mAnimStartTime; + if (timeDiff > ANIMATION_DURATION) return false; + float fraction = timeDiff / ANIMATION_DURATION; + + // Calculate the position and the size of the preview. + float centerX = x + width / 2f; + float centerY = y + height / 2f; + float previewAnimScale = 1 - ZOOM_DELTA_PREVIEW * (1 - fraction); + float previewWidth = width * previewAnimScale; + float previewHeight = height * previewAnimScale; + int previewX = Math.round(centerX - previewWidth / 2); + int previewY = Math.round(centerY - previewHeight / 2); + + // Calculate the position and the size of the review. + float reviewAnimScale = 1 + ZOOM_DELTA_REVIEW * fraction; + + // Calculate how much preview is scaled. + // The scaling is done by PhotoView in Gallery so we don't have the + // scaling information but only the width and the height passed to this + // method. The inference of the scale ratio is done by matching the + // current width and the original width we have at first when the camera + // layout is inflated. + float scaleRatio = 1; + if (mPreviewFrameLayoutWidth != 0) { + scaleRatio = (float) width / mPreviewFrameLayoutWidth; + } else { + Log.e(TAG, "mPreviewFrameLayoutWidth is 0."); + } + float reviewWidth = mReviewDrawingWidth * reviewAnimScale * scaleRatio; + float reviewHeight = mReviewDrawingHeight * reviewAnimScale * scaleRatio; + int reviewX = Math.round(centerX - reviewWidth / 2); + int reviewY = Math.round(centerY - reviewHeight / 2); + + // Draw the preview. + float alpha = canvas.getAlpha(); + canvas.setAlpha(fraction); // fade in + preview.directDraw(canvas, previewX, previewY, Math.round(previewWidth), + Math.round(previewHeight)); + + // Draw the review. + canvas.setAlpha((1f - fraction) * INITIAL_DARKEN_ALPHA); // fade out + review.draw(canvas, reviewX, reviewY, Math.round(reviewWidth), + Math.round(reviewHeight)); + canvas.setAlpha(alpha); + return true; + } + + public boolean drawDarkPreview(GLCanvas canvas, int x, int y, int width, + int height, RawTexture review) { + // Calculate the position and the size. + float centerX = x + width / 2f; + float centerY = y + height / 2f; + float scaleRatio = 1; + if (mPreviewFrameLayoutWidth != 0) { + scaleRatio = (float) width / mPreviewFrameLayoutWidth; + } else { + Log.e(TAG, "mPreviewFrameLayoutWidth is 0."); + } + float reviewWidth = mReviewDrawingWidth * scaleRatio; + float reviewHeight = mReviewDrawingHeight * scaleRatio; + int reviewX = Math.round(centerX - reviewWidth / 2); + int reviewY = Math.round(centerY - reviewHeight / 2); + + // Draw the review. + float alpha = canvas.getAlpha(); + canvas.setAlpha(INITIAL_DARKEN_ALPHA); + review.draw(canvas, reviewX, reviewY, Math.round(reviewWidth), + Math.round(reviewHeight)); + canvas.setAlpha(alpha); + return true; + } + +} diff --git a/src/com/android/camera/Thumbnail.java b/src/com/android/camera/Thumbnail.java new file mode 100644 index 000000000..5f8483d6c --- /dev/null +++ b/src/com/android/camera/Thumbnail.java @@ -0,0 +1,68 @@ +/* + * 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 android.graphics.Bitmap; +import android.media.MediaMetadataRetriever; + +import java.io.FileDescriptor; + +public class Thumbnail { + public static Bitmap createVideoThumbnailBitmap(FileDescriptor fd, int targetWidth) { + return createVideoThumbnailBitmap(null, fd, targetWidth); + } + + public static Bitmap createVideoThumbnailBitmap(String filePath, int targetWidth) { + return createVideoThumbnailBitmap(filePath, null, targetWidth); + } + + private static Bitmap createVideoThumbnailBitmap(String filePath, FileDescriptor fd, + int targetWidth) { + Bitmap bitmap = null; + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + try { + if (filePath != null) { + retriever.setDataSource(filePath); + } else { + retriever.setDataSource(fd); + } + bitmap = retriever.getFrameAtTime(-1); + } catch (IllegalArgumentException ex) { + // Assume this is a corrupt video file + } catch (RuntimeException ex) { + // Assume this is a corrupt video file. + } finally { + try { + retriever.release(); + } catch (RuntimeException ex) { + // Ignore failures while cleaning up. + } + } + if (bitmap == null) return null; + + // Scale down the bitmap if it is bigger than we need. + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + if (width > targetWidth) { + float scale = (float) targetWidth / width; + int w = Math.round(scale * width); + int h = Math.round(scale * height); + bitmap = Bitmap.createScaledBitmap(bitmap, w, h, true); + } + return bitmap; + } +} diff --git a/src/com/android/camera/Util.java b/src/com/android/camera/Util.java new file mode 100644 index 000000000..ccc2d9079 --- /dev/null +++ b/src/com/android/camera/Util.java @@ -0,0 +1,804 @@ +/* + * Copyright (C) 2009 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 android.annotation.TargetApi; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.admin.DevicePolicyManager; +import android.content.ActivityNotFoundException; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.hardware.Camera; +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.Size; +import android.location.Location; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.os.ParcelFileDescriptor; +import android.telephony.TelephonyManager; +import android.util.DisplayMetrics; +import android.util.FloatMath; +import android.util.Log; +import android.util.TypedValue; +import android.view.Display; +import android.view.OrientationEventListener; +import android.view.Surface; +import android.view.View; +import android.view.WindowManager; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.widget.Toast; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.MovieActivity; +import com.android.gallery3d.common.ApiHelper; + +import java.io.Closeable; +import java.io.IOException; +import java.lang.reflect.Method; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.StringTokenizer; + +/** + * Collection of utility functions used in this package. + */ +public class Util { + private static final String TAG = "Util"; + + // Orientation hysteresis amount used in rounding, in degrees + public static final int ORIENTATION_HYSTERESIS = 5; + + public static final String REVIEW_ACTION = "com.android.camera.action.REVIEW"; + // See android.hardware.Camera.ACTION_NEW_PICTURE. + public static final String ACTION_NEW_PICTURE = "android.hardware.action.NEW_PICTURE"; + // See android.hardware.Camera.ACTION_NEW_VIDEO. + public static final String ACTION_NEW_VIDEO = "android.hardware.action.NEW_VIDEO"; + + // Fields from android.hardware.Camera.Parameters + public static final String FOCUS_MODE_CONTINUOUS_PICTURE = "continuous-picture"; + public static final String RECORDING_HINT = "recording-hint"; + 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 String VIDEO_SNAPSHOT_SUPPORTED = "video-snapshot-supported"; + public static final String SCENE_MODE_HDR = "hdr"; + public static final String TRUE = "true"; + public static final String FALSE = "false"; + + public static boolean isSupported(String value, List<String> supported) { + return supported == null ? false : supported.indexOf(value) >= 0; + } + + public static boolean isAutoExposureLockSupported(Parameters params) { + return TRUE.equals(params.get(AUTO_EXPOSURE_LOCK_SUPPORTED)); + } + + public static boolean isAutoWhiteBalanceLockSupported(Parameters params) { + return TRUE.equals(params.get(AUTO_WHITE_BALANCE_LOCK_SUPPORTED)); + } + + public static boolean isVideoSnapshotSupported(Parameters params) { + return TRUE.equals(params.get(VIDEO_SNAPSHOT_SUPPORTED)); + } + + public static boolean isCameraHdrSupported(Parameters params) { + List<String> supported = params.getSupportedSceneModes(); + return (supported != null) && supported.contains(SCENE_MODE_HDR); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + public static boolean isMeteringAreaSupported(Parameters params) { + if (ApiHelper.HAS_CAMERA_METERING_AREA) { + return params.getMaxNumMeteringAreas() > 0; + } + return false; + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + public static boolean isFocusAreaSupported(Parameters params) { + if (ApiHelper.HAS_CAMERA_FOCUS_AREA) { + return (params.getMaxNumFocusAreas() > 0 + && isSupported(Parameters.FOCUS_MODE_AUTO, + params.getSupportedFocusModes())); + } + return false; + } + + // Private intent extras. Test only. + private static final String EXTRAS_CAMERA_FACING = + "android.intent.extras.CAMERA_FACING"; + + private static float sPixelDensity = 1; + private static ImageFileNamer sImageFileNamer; + + private Util() { + } + + public static void initialize(Context context) { + DisplayMetrics metrics = new DisplayMetrics(); + WindowManager wm = (WindowManager) + context.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getMetrics(metrics); + sPixelDensity = metrics.density; + sImageFileNamer = new ImageFileNamer( + context.getString(R.string.image_file_name_format)); + } + + public static int dpToPixel(int dp) { + return Math.round(sPixelDensity * dp); + } + + // Rotates the bitmap by the specified degree. + // If a new bitmap is created, the original bitmap is recycled. + public static Bitmap rotate(Bitmap b, int degrees) { + return rotateAndMirror(b, degrees, false); + } + + // Rotates and/or mirrors the bitmap. If a new bitmap is created, the + // original bitmap is recycled. + public static Bitmap rotateAndMirror(Bitmap b, int degrees, boolean mirror) { + if ((degrees != 0 || mirror) && b != null) { + Matrix m = new Matrix(); + // Mirror first. + // horizontal flip + rotation = -rotation + horizontal flip + if (mirror) { + m.postScale(-1, 1); + degrees = (degrees + 360) % 360; + if (degrees == 0 || degrees == 180) { + m.postTranslate(b.getWidth(), 0); + } else if (degrees == 90 || degrees == 270) { + m.postTranslate(b.getHeight(), 0); + } else { + throw new IllegalArgumentException("Invalid degrees=" + degrees); + } + } + if (degrees != 0) { + // clockwise + m.postRotate(degrees, + (float) b.getWidth() / 2, (float) b.getHeight() / 2); + } + + try { + Bitmap b2 = Bitmap.createBitmap( + b, 0, 0, b.getWidth(), b.getHeight(), m, true); + if (b != b2) { + b.recycle(); + b = b2; + } + } catch (OutOfMemoryError ex) { + // We have no memory to rotate. Return the original bitmap. + } + } + return b; + } + + /* + * Compute the sample size as a function of minSideLength + * and maxNumOfPixels. + * minSideLength is used to specify that minimal width or height of a + * bitmap. + * maxNumOfPixels is used to specify the maximal size in pixels that is + * tolerable in terms of memory usage. + * + * The function returns a sample size based on the constraints. + * Both size and minSideLength can be passed in as -1 + * which indicates no care of the corresponding constraint. + * The functions prefers returning a sample size that + * generates a smaller bitmap, unless minSideLength = -1. + * + * Also, the function rounds up the sample size to a power of 2 or multiple + * of 8 because BitmapFactory only honors sample size this way. + * For example, BitmapFactory downsamples an image by 2 even though the + * request is 3. So we round up the sample size to avoid OOM. + */ + public static int computeSampleSize(BitmapFactory.Options options, + int minSideLength, int maxNumOfPixels) { + int initialSize = computeInitialSampleSize(options, minSideLength, + maxNumOfPixels); + + int roundedSize; + if (initialSize <= 8) { + roundedSize = 1; + while (roundedSize < initialSize) { + roundedSize <<= 1; + } + } else { + roundedSize = (initialSize + 7) / 8 * 8; + } + + return roundedSize; + } + + private static int computeInitialSampleSize(BitmapFactory.Options options, + int minSideLength, int maxNumOfPixels) { + double w = options.outWidth; + double h = options.outHeight; + + int lowerBound = (maxNumOfPixels < 0) ? 1 : + (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels)); + int upperBound = (minSideLength < 0) ? 128 : + (int) Math.min(Math.floor(w / minSideLength), + Math.floor(h / minSideLength)); + + if (upperBound < lowerBound) { + // return the larger one when there is no overlapping zone. + return lowerBound; + } + + if (maxNumOfPixels < 0 && minSideLength < 0) { + return 1; + } else if (minSideLength < 0) { + return lowerBound; + } else { + return upperBound; + } + } + + public static Bitmap makeBitmap(byte[] jpegData, int maxNumOfPixels) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length, + options); + if (options.mCancel || options.outWidth == -1 + || options.outHeight == -1) { + return null; + } + options.inSampleSize = computeSampleSize( + options, -1, maxNumOfPixels); + options.inJustDecodeBounds = false; + + options.inDither = false; + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + return BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length, + options); + } catch (OutOfMemoryError ex) { + Log.e(TAG, "Got oom exception ", ex); + return null; + } + } + + public static void closeSilently(Closeable c) { + if (c == null) return; + try { + c.close(); + } catch (Throwable t) { + // do nothing + } + } + + public static void Assert(boolean cond) { + if (!cond) { + throw new AssertionError(); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private static void throwIfCameraDisabled(Activity activity) throws CameraDisabledException { + // Check if device policy has disabled the camera. + if (ApiHelper.HAS_GET_CAMERA_DISABLED) { + DevicePolicyManager dpm = (DevicePolicyManager) activity.getSystemService( + Context.DEVICE_POLICY_SERVICE); + if (dpm.getCameraDisabled(null)) { + throw new CameraDisabledException(); + } + } + } + + public static CameraManager.CameraProxy openCamera( + Activity activity, int cameraId) + throws CameraHardwareException, CameraDisabledException { + throwIfCameraDisabled(activity); + + try { + return CameraHolder.instance().open(cameraId); + } catch (CameraHardwareException e) { + // In eng build, we throw the exception so that test tool + // can detect it and report it + if ("eng".equals(Build.TYPE)) { + throw new RuntimeException("openCamera failed", e); + } else { + throw e; + } + } + } + + public static void showErrorAndFinish(final Activity activity, int msgId) { + DialogInterface.OnClickListener buttonListener = + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + activity.finish(); + } + }; + TypedValue out = new TypedValue(); + activity.getTheme().resolveAttribute(android.R.attr.alertDialogIcon, out, true); + new AlertDialog.Builder(activity) + .setCancelable(false) + .setTitle(R.string.camera_error_title) + .setMessage(msgId) + .setNeutralButton(R.string.dialog_ok, buttonListener) + .setIcon(out.resourceId) + .show(); + } + + public static <T> T checkNotNull(T object) { + if (object == null) throw new NullPointerException(); + return object; + } + + public static boolean equals(Object a, Object b) { + return (a == b) || (a == null ? false : a.equals(b)); + } + + public static int nextPowerOf2(int n) { + n -= 1; + n |= n >>> 16; + n |= n >>> 8; + n |= n >>> 4; + n |= n >>> 2; + n |= n >>> 1; + return n + 1; + } + + public static float distance(float x, float y, float sx, float sy) { + float dx = x - sx; + float dy = y - sy; + return FloatMath.sqrt(dx * dx + dy * dy); + } + + public static int clamp(int x, int min, int max) { + if (x > max) return max; + if (x < min) return min; + return x; + } + + public static int getDisplayRotation(Activity activity) { + int rotation = activity.getWindowManager().getDefaultDisplay() + .getRotation(); + switch (rotation) { + case Surface.ROTATION_0: return 0; + case Surface.ROTATION_90: return 90; + case Surface.ROTATION_180: return 180; + case Surface.ROTATION_270: return 270; + } + return 0; + } + + public static int getDisplayOrientation(int degrees, int cameraId) { + // See android.hardware.Camera.setDisplayOrientation for + // documentation. + Camera.CameraInfo info = new Camera.CameraInfo(); + Camera.getCameraInfo(cameraId, info); + int result; + if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + result = (info.orientation + degrees) % 360; + result = (360 - result) % 360; // compensate the mirror + } else { // back-facing + result = (info.orientation - degrees + 360) % 360; + } + return result; + } + + public static int getCameraOrientation(int cameraId) { + Camera.CameraInfo info = new Camera.CameraInfo(); + Camera.getCameraInfo(cameraId, info); + return info.orientation; + } + + public static int roundOrientation(int orientation, int orientationHistory) { + boolean changeOrientation = false; + if (orientationHistory == OrientationEventListener.ORIENTATION_UNKNOWN) { + changeOrientation = true; + } else { + int dist = Math.abs(orientation - orientationHistory); + dist = Math.min( dist, 360 - dist ); + changeOrientation = ( dist >= 45 + ORIENTATION_HYSTERESIS ); + } + if (changeOrientation) { + return ((orientation + 45) / 90 * 90) % 360; + } + return orientationHistory; + } + + @SuppressWarnings("deprecation") + @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2) + private static Point getDefaultDisplaySize(Activity activity, Point size) { + Display d = activity.getWindowManager().getDefaultDisplay(); + if (Build.VERSION.SDK_INT >= ApiHelper.VERSION_CODES.HONEYCOMB_MR2) { + d.getSize(size); + } else { + size.set(d.getWidth(), d.getHeight()); + } + return size; + } + + public static Size getOptimalPreviewSize(Activity currentActivity, + List<Size> sizes, double targetRatio) { + // Use a very small tolerance because we want an exact match. + final double ASPECT_TOLERANCE = 0.001; + if (sizes == null) return null; + + Size optimalSize = null; + double minDiff = Double.MAX_VALUE; + + // Because of bugs of overlay and layout, we sometimes will try to + // layout the viewfinder in the portrait orientation and thus get the + // wrong size of preview surface. When we change the preview size, the + // new overlay will be created before the old one closed, which causes + // an exception. For now, just get the screen size. + Point point = getDefaultDisplaySize(currentActivity, new Point()); + int targetHeight = Math.min(point.x, point.y); + // Try to find an size match aspect ratio and size + for (Size size : sizes) { + double ratio = (double) size.width / size.height; + if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue; + if (Math.abs(size.height - targetHeight) < minDiff) { + optimalSize = size; + minDiff = Math.abs(size.height - targetHeight); + } + } + // Cannot find the one match the aspect ratio. This should not happen. + // Ignore the requirement. + if (optimalSize == null) { + Log.w(TAG, "No preview size match the aspect ratio"); + minDiff = Double.MAX_VALUE; + for (Size size : sizes) { + if (Math.abs(size.height - targetHeight) < minDiff) { + optimalSize = size; + minDiff = Math.abs(size.height - targetHeight); + } + } + } + return optimalSize; + } + + // Returns the largest picture size which matches the given aspect ratio. + public static Size getOptimalVideoSnapshotPictureSize( + List<Size> sizes, double targetRatio) { + // Use a very small tolerance because we want an exact match. + final double ASPECT_TOLERANCE = 0.001; + if (sizes == null) return null; + + Size optimalSize = null; + + // Try to find a size matches aspect ratio and has the largest width + for (Size size : sizes) { + double ratio = (double) size.width / size.height; + if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue; + if (optimalSize == null || size.width > optimalSize.width) { + optimalSize = size; + } + } + + // Cannot find one that matches the aspect ratio. This should not happen. + // Ignore the requirement. + if (optimalSize == null) { + Log.w(TAG, "No picture size match the aspect ratio"); + for (Size size : sizes) { + if (optimalSize == null || size.width > optimalSize.width) { + optimalSize = size; + } + } + } + return optimalSize; + } + + public static void dumpParameters(Parameters parameters) { + String flattened = parameters.flatten(); + StringTokenizer tokenizer = new StringTokenizer(flattened, ";"); + Log.d(TAG, "Dump all camera parameters:"); + while (tokenizer.hasMoreElements()) { + Log.d(TAG, tokenizer.nextToken()); + } + } + + /** + * Returns whether the device is voice-capable (meaning, it can do MMS). + */ + public static boolean isMmsCapable(Context context) { + TelephonyManager telephonyManager = (TelephonyManager) + context.getSystemService(Context.TELEPHONY_SERVICE); + if (telephonyManager == null) { + return false; + } + + try { + Class<?> partypes[] = new Class[0]; + Method sIsVoiceCapable = TelephonyManager.class.getMethod( + "isVoiceCapable", partypes); + + Object arglist[] = new Object[0]; + Object retobj = sIsVoiceCapable.invoke(telephonyManager, arglist); + return (Boolean) retobj; + } catch (java.lang.reflect.InvocationTargetException ite) { + // Failure, must be another device. + // Assume that it is voice capable. + } catch (IllegalAccessException iae) { + // Failure, must be an other device. + // Assume that it is voice capable. + } catch (NoSuchMethodException nsme) { + } + return true; + } + + // This is for test only. Allow the camera to launch the specific camera. + public static int getCameraFacingIntentExtras(Activity currentActivity) { + int cameraId = -1; + + int intentCameraId = + currentActivity.getIntent().getIntExtra(Util.EXTRAS_CAMERA_FACING, -1); + + if (isFrontCameraIntent(intentCameraId)) { + // Check if the front camera exist + int frontCameraId = CameraHolder.instance().getFrontCameraId(); + if (frontCameraId != -1) { + cameraId = frontCameraId; + } + } else if (isBackCameraIntent(intentCameraId)) { + // Check if the back camera exist + int backCameraId = CameraHolder.instance().getBackCameraId(); + if (backCameraId != -1) { + cameraId = backCameraId; + } + } + return cameraId; + } + + private static boolean isFrontCameraIntent(int intentCameraId) { + return (intentCameraId == android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT); + } + + private static boolean isBackCameraIntent(int intentCameraId) { + return (intentCameraId == android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK); + } + + private static int sLocation[] = new int[2]; + + // This method is not thread-safe. + public static boolean pointInView(float x, float y, View v) { + v.getLocationInWindow(sLocation); + return x >= sLocation[0] && x < (sLocation[0] + v.getWidth()) + && y >= sLocation[1] && y < (sLocation[1] + v.getHeight()); + } + + public static int[] getRelativeLocation(View reference, View view) { + reference.getLocationInWindow(sLocation); + int referenceX = sLocation[0]; + int referenceY = sLocation[1]; + view.getLocationInWindow(sLocation); + sLocation[0] -= referenceX; + sLocation[1] -= referenceY; + return sLocation; + } + + public static boolean isUriValid(Uri uri, ContentResolver resolver) { + if (uri == null) return false; + + try { + ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r"); + if (pfd == null) { + Log.e(TAG, "Fail to open URI. URI=" + uri); + return false; + } + pfd.close(); + } catch (IOException ex) { + return false; + } + return true; + } + + public static void viewUri(Uri uri, Context context) { + if (!isUriValid(uri, context.getContentResolver())) { + Log.e(TAG, "Uri invalid. uri=" + uri); + return; + } + + try { + context.startActivity(new Intent(Util.REVIEW_ACTION, uri)); + } catch (ActivityNotFoundException ex) { + try { + context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "review image fail. uri=" + uri, e); + } + } + } + + public static void dumpRect(RectF rect, String msg) { + Log.v(TAG, msg + "=(" + rect.left + "," + rect.top + + "," + rect.right + "," + rect.bottom + ")"); + } + + public static 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); + } + + public static void prepareMatrix(Matrix matrix, boolean mirror, int displayOrientation, + int viewWidth, int viewHeight) { + // Need mirror for front camera. + matrix.setScale(mirror ? -1 : 1, 1); + // This is the value for android.hardware.Camera.setDisplayOrientation. + matrix.postRotate(displayOrientation); + // 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); + } + + public static String createJpegName(long dateTaken) { + synchronized (sImageFileNamer) { + return sImageFileNamer.generateName(dateTaken); + } + } + + public static void broadcastNewPicture(Context context, Uri uri) { + context.sendBroadcast(new Intent(ACTION_NEW_PICTURE, uri)); + // Keep compatibility + context.sendBroadcast(new Intent("com.android.camera.NEW_PICTURE", uri)); + } + + public static void fadeIn(View view, float startAlpha, float endAlpha, long duration) { + if (view.getVisibility() == View.VISIBLE) return; + + view.setVisibility(View.VISIBLE); + Animation animation = new AlphaAnimation(startAlpha, endAlpha); + animation.setDuration(duration); + view.startAnimation(animation); + } + + public static void fadeIn(View view) { + fadeIn(view, 0F, 1F, 400); + + // We disabled the button in fadeOut(), so enable it here. + view.setEnabled(true); + } + + public static void fadeOut(View view) { + if (view.getVisibility() != View.VISIBLE) return; + + // Since the button is still clickable before fade-out animation + // ends, we disable the button first to block click. + view.setEnabled(false); + Animation animation = new AlphaAnimation(1F, 0F); + animation.setDuration(400); + view.startAnimation(animation); + view.setVisibility(View.GONE); + } + + public static int getJpegRotation(int cameraId, int orientation) { + // See android.hardware.Camera.Parameters.setRotation for + // documentation. + int rotation = 0; + if (orientation != OrientationEventListener.ORIENTATION_UNKNOWN) { + CameraInfo info = CameraHolder.instance().getCameraInfo()[cameraId]; + if (info.facing == CameraInfo.CAMERA_FACING_FRONT) { + rotation = (info.orientation - orientation + 360) % 360; + } else { // back-facing camera + rotation = (info.orientation + orientation) % 360; + } + } + return rotation; + } + + public static void setGpsParameters(Parameters parameters, Location loc) { + // Clear previous GPS location from the parameters. + parameters.removeGpsData(); + + // We always encode GpsTimeStamp + parameters.setGpsTimestamp(System.currentTimeMillis() / 1000); + + // Set GPS location. + if (loc != null) { + double lat = loc.getLatitude(); + double lon = loc.getLongitude(); + boolean hasLatLon = (lat != 0.0d) || (lon != 0.0d); + + if (hasLatLon) { + Log.d(TAG, "Set gps location"); + parameters.setGpsLatitude(lat); + parameters.setGpsLongitude(lon); + parameters.setGpsProcessingMethod(loc.getProvider().toUpperCase()); + if (loc.hasAltitude()) { + parameters.setGpsAltitude(loc.getAltitude()); + } else { + // for NETWORK_PROVIDER location provider, we may have + // no altitude information, but the driver needs it, so + // we fake one. + parameters.setGpsAltitude(0); + } + if (loc.getTime() != 0) { + // Location.getTime() is UTC in milliseconds. + // gps-timestamp is UTC in seconds. + long utcTimeSeconds = loc.getTime() / 1000; + parameters.setGpsTimestamp(utcTimeSeconds); + } + } else { + loc = null; + } + } + } + + + public static int[] getMaxPreviewFpsRange(Parameters params) { + List<int[]> frameRates = params.getSupportedPreviewFpsRange(); + if (frameRates != null && frameRates.size() > 0) { + // The list is sorted. Return the last element. + return frameRates.get(frameRates.size() - 1); + } + return new int[0]; + } + + private static class ImageFileNamer { + private SimpleDateFormat mFormat; + + // The date (in milliseconds) used to generate the last name. + private long mLastDate; + + // Number of names generated for the same second. + private int mSameSecondCount; + + public ImageFileNamer(String format) { + mFormat = new SimpleDateFormat(format); + } + + public String generateName(long dateTaken) { + Date date = new Date(dateTaken); + String result = mFormat.format(date); + + // If the last name was generated for the same second, + // we append _1, _2, etc to the name. + if (dateTaken / 1000 == mLastDate / 1000) { + mSameSecondCount++; + result += "_" + mSameSecondCount; + } else { + mLastDate = dateTaken; + mSameSecondCount = 0; + } + + return result; + } + } + + public static void playVideo(Context context, Uri uri, String title) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW) + .setDataAndType(uri, "video/*") + .putExtra(Intent.EXTRA_TITLE, title) + .putExtra(MovieActivity.KEY_TREAT_UP_AS_BACK, true); + context.startActivity(intent); + } catch (ActivityNotFoundException e) { + Toast.makeText(context, context.getString(R.string.video_err), + Toast.LENGTH_SHORT).show(); + } + } +} diff --git a/src/com/android/camera/VideoController.java b/src/com/android/camera/VideoController.java new file mode 100644 index 000000000..e84654821 --- /dev/null +++ b/src/com/android/camera/VideoController.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2013 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 android.view.View; + +import com.android.camera.ShutterButton.OnShutterButtonListener; + +public interface VideoController extends OnShutterButtonListener { + + public void onReviewDoneClicked(View view); + public void onReviewCancelClicked(View viwe); + public void onReviewPlayClicked(View view); + + public boolean isVideoCaptureIntent(); + public boolean isInReviewMode(); + public int onZoomChanged(int index); + + public void onSingleTapUp(View view, int x, int y); + + public void stopPreview(); + + public void updateCameraOrientation(); + + // Callbacks for camera preview UI events. + public void onPreviewUIReady(); + public void onPreviewUIDestroyed(); +} diff --git a/src/com/android/camera/VideoMenu.java b/src/com/android/camera/VideoMenu.java new file mode 100644 index 000000000..da0bde10e --- /dev/null +++ b/src/com/android/camera/VideoMenu.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2012 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 android.app.Activity; +import android.content.Context; +import android.view.LayoutInflater; + +import com.android.camera.ui.AbstractSettingPopup; +import com.android.camera.ui.ListPrefSettingPopup; +import com.android.camera.ui.MoreSettingPopup; +import com.android.camera.ui.PieItem; +import com.android.camera.ui.PieItem.OnClickListener; +import com.android.camera.ui.PieRenderer; +import com.android.camera.ui.TimeIntervalPopup; +import com.android.gallery3d.R; + +public class VideoMenu extends PieController + implements MoreSettingPopup.Listener, + ListPrefSettingPopup.Listener, + TimeIntervalPopup.Listener { + + private static String TAG = "CAM_VideoMenu"; + + private VideoUI mUI; + private String[] mOtherKeys; + private AbstractSettingPopup mPopup; + + private static final int POPUP_NONE = 0; + private static final int POPUP_FIRST_LEVEL = 1; + private static final int POPUP_SECOND_LEVEL = 2; + private int mPopupStatus; + private CameraActivity mActivity; + + public VideoMenu(CameraActivity activity, VideoUI ui, PieRenderer pie) { + super(activity, pie); + mUI = ui; + mActivity = activity; + } + + + public void initialize(PreferenceGroup group) { + super.initialize(group); + mPopup = null; + mPopupStatus = POPUP_NONE; + PieItem item = null; + // white balance + if (group.findPreference(CameraSettings.KEY_WHITE_BALANCE) != null) { + item = makeItem(CameraSettings.KEY_WHITE_BALANCE); + mRenderer.addItem(item); + } + // settings popup + mOtherKeys = new String[] { + CameraSettings.KEY_VIDEO_EFFECT, + CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL, + CameraSettings.KEY_VIDEO_QUALITY, + CameraSettings.KEY_RECORD_LOCATION + }; + item = makeItem(R.drawable.ic_settings_holo_light); + item.setLabel(mActivity.getResources().getString(R.string.camera_menu_settings_label)); + item.setOnClickListener(new OnClickListener() { + @Override + public void onClick(PieItem item) { + if (mPopup == null || mPopupStatus != POPUP_FIRST_LEVEL) { + initializePopup(); + mPopupStatus = POPUP_FIRST_LEVEL; + } + mUI.showPopup(mPopup); + } + }); + mRenderer.addItem(item); + // camera switcher + if (group.findPreference(CameraSettings.KEY_CAMERA_ID) != null) { + item = makeItem(R.drawable.ic_switch_back); + IconListPreference lpref = (IconListPreference) group.findPreference( + CameraSettings.KEY_CAMERA_ID); + item.setLabel(lpref.getLabel()); + item.setImageResource(mActivity, + ((IconListPreference) lpref).getIconIds() + [lpref.findIndexOfValue(lpref.getValue())]); + + final PieItem fitem = item; + item.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(PieItem item) { + // Find the index of next camera. + ListPreference pref = + mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID); + if (pref != null) { + int index = pref.findIndexOfValue(pref.getValue()); + CharSequence[] values = pref.getEntryValues(); + index = (index + 1) % values.length; + int newCameraId = Integer.parseInt((String) values[index]); + fitem.setImageResource(mActivity, + ((IconListPreference) pref).getIconIds()[index]); + fitem.setLabel(pref.getLabel()); + mListener.onCameraPickerClicked(newCameraId); + } + } + }); + mRenderer.addItem(item); + } + // flash + if (group.findPreference(CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE) != null) { + item = makeItem(CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE); + mRenderer.addItem(item); + } + } + + @Override + public void reloadPreferences() { + super.reloadPreferences(); + if (mPopup != null) { + mPopup.reloadPreference(); + } + } + + @Override + public void overrideSettings(final String ... keyvalues) { + super.overrideSettings(keyvalues); + if (mPopup == null || mPopupStatus != POPUP_FIRST_LEVEL) { + mPopupStatus = POPUP_FIRST_LEVEL; + initializePopup(); + } + ((MoreSettingPopup) mPopup).overrideSettings(keyvalues); + } + + @Override + // Hit when an item in the second-level popup gets selected + public void onListPrefChanged(ListPreference pref) { + if (mPopup != null) { + if (mPopupStatus == POPUP_SECOND_LEVEL) { + mUI.dismissPopup(true); + } + } + super.onSettingChanged(pref); + } + + protected void initializePopup() { + LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + + MoreSettingPopup popup = (MoreSettingPopup) inflater.inflate( + R.layout.more_setting_popup, null, false); + popup.setSettingChangedListener(this); + popup.initialize(mPreferenceGroup, mOtherKeys); + if (mActivity.isSecureCamera()) { + // Prevent location preference from getting changed in secure camera mode + popup.setPreferenceEnabled(CameraSettings.KEY_RECORD_LOCATION, false); + } + mPopup = popup; + } + + public void popupDismissed(boolean topPopupOnly) { + // if the 2nd level popup gets dismissed + if (mPopupStatus == POPUP_SECOND_LEVEL) { + initializePopup(); + mPopupStatus = POPUP_FIRST_LEVEL; + if (topPopupOnly) mUI.showPopup(mPopup); + } + } + + @Override + // Hit when an item in the first-level popup gets selected, then bring up + // the second-level popup + public void onPreferenceClicked(ListPreference pref) { + if (mPopupStatus != POPUP_FIRST_LEVEL) return; + + LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + + if (CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL.equals(pref.getKey())) { + TimeIntervalPopup timeInterval = (TimeIntervalPopup) inflater.inflate( + R.layout.time_interval_popup, null, false); + timeInterval.initialize((IconListPreference) pref); + timeInterval.setSettingChangedListener(this); + mUI.dismissPopup(true); + mPopup = timeInterval; + } else { + ListPrefSettingPopup basic = (ListPrefSettingPopup) inflater.inflate( + R.layout.list_pref_setting_popup, null, false); + basic.initialize(pref); + basic.setSettingChangedListener(this); + mUI.dismissPopup(true); + mPopup = basic; + } + mUI.showPopup(mPopup); + mPopupStatus = POPUP_SECOND_LEVEL; + } +} diff --git a/src/com/android/camera/VideoModule.java b/src/com/android/camera/VideoModule.java new file mode 100644 index 000000000..956890e5e --- /dev/null +++ b/src/com/android/camera/VideoModule.java @@ -0,0 +1,2233 @@ +/* + * Copyright (C) 2012 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 android.annotation.TargetApi; +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences.Editor; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.SurfaceTexture; +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.Size; +import android.location.Location; +import android.media.CamcorderProfile; +import android.media.CameraProfile; +import android.media.MediaRecorder; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.SystemClock; +import android.provider.MediaStore; +import android.provider.MediaStore.MediaColumns; +import android.provider.MediaStore.Video; +import android.util.Log; +import android.view.KeyEvent; +import android.view.OrientationEventListener; +import android.view.Surface; +import android.view.View; +import android.view.WindowManager; +import android.widget.Toast; + +import com.android.camera.CameraManager.CameraPictureCallback; +import com.android.camera.CameraManager.CameraProxy; +import com.android.camera.ui.PopupManager; +import com.android.camera.ui.RotateTextToast; +import com.android.gallery3d.R; +import com.android.gallery3d.app.OrientationManager; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.exif.ExifInterface; +import com.android.gallery3d.util.AccessibilityUtils; +import com.android.gallery3d.util.UsageStatistics; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Iterator; +import java.util.List; + +public class VideoModule implements CameraModule, + VideoController, + CameraPreference.OnPreferenceChangedListener, + ShutterButton.OnShutterButtonListener, + MediaRecorder.OnErrorListener, + MediaRecorder.OnInfoListener, + EffectsRecorder.EffectsListener { + + private static final String TAG = "CAM_VideoModule"; + + // We number the request code from 1000 to avoid collision with Gallery. + private static final int REQUEST_EFFECT_BACKDROPPER = 1000; + + private static final int CHECK_DISPLAY_ROTATION = 3; + private static final int CLEAR_SCREEN_DELAY = 4; + private static final int UPDATE_RECORD_TIME = 5; + private static final int ENABLE_SHUTTER_BUTTON = 6; + private static final int SHOW_TAP_TO_SNAPSHOT_TOAST = 7; + private static final int SWITCH_CAMERA = 8; + private static final int SWITCH_CAMERA_START_ANIMATION = 9; + private static final int HIDE_SURFACE_VIEW = 10; + private static final int CAPTURE_ANIMATION_DONE = 11; + + private static final int SCREEN_DELAY = 2 * 60 * 1000; + + private static final long SHUTTER_BUTTON_TIMEOUT = 500L; // 500ms + + /** + * An unpublished intent flag requesting to start recording straight away + * and return as soon as recording is stopped. + * TODO: consider publishing by moving into MediaStore. + */ + private static final String EXTRA_QUICK_CAPTURE = + "android.intent.extra.quickCapture"; + + private static final int MIN_THUMB_SIZE = 64; + // module fields + private CameraActivity mActivity; + private boolean mPaused; + private int mCameraId; + private Parameters mParameters; + + private boolean mIsInReviewMode; + private boolean mSnapshotInProgress = false; + + private static final String EFFECT_BG_FROM_GALLERY = "gallery"; + + private final CameraErrorCallback mErrorCallback = new CameraErrorCallback(); + + private ComboPreferences mPreferences; + private PreferenceGroup mPreferenceGroup; + // Preference must be read before starting preview. We check this before starting + // preview. + private boolean mPreferenceRead; + + private boolean mIsVideoCaptureIntent; + private boolean mQuickCapture; + + private MediaRecorder mMediaRecorder; + private EffectsRecorder mEffectsRecorder; + private boolean mEffectsDisplayResult; + + private int mEffectType = EffectsRecorder.EFFECT_NONE; + private Object mEffectParameter = null; + private String mEffectUriFromGallery = null; + private String mPrefVideoEffectDefault; + private boolean mResetEffect = true; + + private boolean mSwitchingCamera; + private boolean mMediaRecorderRecording = false; + private long mRecordingStartTime; + private boolean mRecordingTimeCountsDown = false; + private long mOnResumeTime; + // The video file that the hardware camera is about to record into + // (or is recording into.) + private String mVideoFilename; + private ParcelFileDescriptor mVideoFileDescriptor; + + // The video file that has already been recorded, and that is being + // examined by the user. + private String mCurrentVideoFilename; + private Uri mCurrentVideoUri; + private ContentValues mCurrentVideoValues; + + private CamcorderProfile mProfile; + + // The video duration limit. 0 menas no limit. + private int mMaxVideoDurationInMs; + + // Time Lapse parameters. + private boolean mCaptureTimeLapse = false; + // Default 0. If it is larger than 0, the camcorder is in time lapse mode. + private int mTimeBetweenTimeLapseFrameCaptureMs = 0; + + boolean mPreviewing = false; // True if preview is started. + // The display rotation in degrees. This is only valid when mPreviewing is + // true. + private int mDisplayRotation; + private int mCameraDisplayOrientation; + + private int mDesiredPreviewWidth; + private int mDesiredPreviewHeight; + private ContentResolver mContentResolver; + + private LocationManager mLocationManager; + private OrientationManager mOrientationManager; + + private Surface mSurface; + private int mPendingSwitchCameraId; + private boolean mOpenCameraFail; + private boolean mCameraDisabled; + private final Handler mHandler = new MainHandler(); + private VideoUI mUI; + private CameraProxy mCameraDevice; + + // The degrees of the device rotated clockwise from its natural orientation. + private int mOrientation = OrientationEventListener.ORIENTATION_UNKNOWN; + + private int mZoomValue; // The current zoom value. + + private boolean mRestoreFlash; // This is used to check if we need to restore the flash + // status when going back from gallery. + + private final MediaSaveService.OnMediaSavedListener mOnVideoSavedListener = + new MediaSaveService.OnMediaSavedListener() { + @Override + public void onMediaSaved(Uri uri) { + if (uri != null) { + mActivity.sendBroadcast( + new Intent(Util.ACTION_NEW_VIDEO, uri)); + Util.broadcastNewPicture(mActivity, uri); + } + } + }; + + private final MediaSaveService.OnMediaSavedListener mOnPhotoSavedListener = + new MediaSaveService.OnMediaSavedListener() { + @Override + public void onMediaSaved(Uri uri) { + if (uri != null) { + Util.broadcastNewPicture(mActivity, uri); + } + } + }; + + + protected class CameraOpenThread extends Thread { + @Override + public void run() { + openCamera(); + } + } + + private void openCamera() { + try { + if (mCameraDevice == null) { + mCameraDevice = Util.openCamera(mActivity, mCameraId); + } + mParameters = mCameraDevice.getParameters(); + } catch (CameraHardwareException e) { + mOpenCameraFail = true; + } catch (CameraDisabledException e) { + mCameraDisabled = true; + } + } + + // This Handler is used to post message back onto the main thread of the + // application + private class MainHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + + case ENABLE_SHUTTER_BUTTON: + mUI.enableShutter(true); + break; + + case CLEAR_SCREEN_DELAY: { + mActivity.getWindow().clearFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + break; + } + + case UPDATE_RECORD_TIME: { + updateRecordingTime(); + break; + } + + case CHECK_DISPLAY_ROTATION: { + // Restart the preview if display rotation has changed. + // Sometimes this happens when the device is held upside + // down and camera app is opened. Rotation animation will + // take some time and the rotation value we have got may be + // wrong. Framework does not have a callback for this now. + if ((Util.getDisplayRotation(mActivity) != mDisplayRotation) + && !mMediaRecorderRecording && !mSwitchingCamera) { + startPreview(); + } + if (SystemClock.uptimeMillis() - mOnResumeTime < 5000) { + mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100); + } + break; + } + + case SHOW_TAP_TO_SNAPSHOT_TOAST: { + showTapToSnapshotToast(); + break; + } + + case SWITCH_CAMERA: { + switchCamera(); + break; + } + + case SWITCH_CAMERA_START_ANIMATION: { + //TODO: + //((CameraScreenNail) mActivity.mCameraScreenNail).animateSwitchCamera(); + + // Enable all camera controls. + mSwitchingCamera = false; + break; + } + + case CAPTURE_ANIMATION_DONE: { + mUI.enablePreviewThumb(false); + break; + } + + default: + Log.v(TAG, "Unhandled message: " + msg.what); + break; + } + } + } + + private BroadcastReceiver mReceiver = null; + + private class MyBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(Intent.ACTION_MEDIA_EJECT)) { + stopVideoRecording(); + } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) { + Toast.makeText(mActivity, + mActivity.getResources().getString(R.string.wait), Toast.LENGTH_LONG).show(); + } + } + } + + private String createName(long dateTaken) { + Date date = new Date(dateTaken); + SimpleDateFormat dateFormat = new SimpleDateFormat( + mActivity.getString(R.string.video_file_name_format)); + + return dateFormat.format(date); + } + + private int getPreferredCameraId(ComboPreferences preferences) { + int intentCameraId = Util.getCameraFacingIntentExtras(mActivity); + if (intentCameraId != -1) { + // Testing purpose. Launch a specific camera through the intent + // extras. + return intentCameraId; + } else { + return CameraSettings.readPreferredCameraId(preferences); + } + } + + private void initializeSurfaceView() { + if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { // API level < 16 + mUI.initializeSurfaceView(); + } + } + + @Override + public void init(CameraActivity activity, View root) { + mActivity = activity; + mUI = new VideoUI(activity, this, root); + mPreferences = new ComboPreferences(mActivity); + CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal()); + mCameraId = getPreferredCameraId(mPreferences); + + mPreferences.setLocalId(mActivity, mCameraId); + CameraSettings.upgradeLocalPreferences(mPreferences.getLocal()); + + mPrefVideoEffectDefault = mActivity.getString(R.string.pref_video_effect_default); + resetEffect(); + mOrientationManager = new OrientationManager(mActivity); + + /* + * To reduce startup time, we start the preview in another thread. + * We make sure the preview is started at the end of onCreate. + */ + CameraOpenThread cameraOpenThread = new CameraOpenThread(); + cameraOpenThread.start(); + + mContentResolver = mActivity.getContentResolver(); + + // Surface texture is from camera screen nail and startPreview needs it. + // This must be done before startPreview. + mIsVideoCaptureIntent = isVideoCaptureIntent(); + initializeSurfaceView(); + + // Make sure camera device is opened. + try { + cameraOpenThread.join(); + if (mOpenCameraFail) { + Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera); + return; + } else if (mCameraDisabled) { + Util.showErrorAndFinish(mActivity, R.string.camera_disabled); + return; + } + } catch (InterruptedException ex) { + // ignore + } + + readVideoPreferences(); + mUI.setPrefChangedListener(this); + + mQuickCapture = mActivity.getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false); + mLocationManager = new LocationManager(mActivity, null); + + mUI.setOrientationIndicator(0, false); + setDisplayOrientation(); + + mUI.showTimeLapseUI(mCaptureTimeLapse); + initializeVideoSnapshot(); + resizeForPreviewAspectRatio(); + + initializeVideoControl(); + mPendingSwitchCameraId = -1; + mUI.updateOnScreenIndicators(mParameters, mPreferences); + + // Disable the shutter button if effects are ON since it might take + // a little more time for the effects preview to be ready. We do not + // want to allow recording before that happens. The shutter button + // will be enabled when we get the message from effectsrecorder that + // the preview is running. This becomes critical when the camera is + // swapped. + if (effectsActive()) { + mUI.enableShutter(false); + } + } + + // SingleTapListener + // Preview area is touched. Take a picture. + @Override + public void onSingleTapUp(View view, int x, int y) { + if (mMediaRecorderRecording && effectsActive()) { + new RotateTextToast(mActivity, R.string.disable_video_snapshot_hint, + mOrientation).show(); + return; + } + + MediaSaveService s = mActivity.getMediaSaveService(); + if (mPaused || mSnapshotInProgress || effectsActive() || s == null || s.isQueueFull()) { + return; + } + + if (!mMediaRecorderRecording) { + // check for dismissing popup + mUI.dismissPopup(true); + return; + } + + // Set rotation and gps data. + int rotation = Util.getJpegRotation(mCameraId, mOrientation); + mParameters.setRotation(rotation); + Location loc = mLocationManager.getCurrentLocation(); + Util.setGpsParameters(mParameters, loc); + mCameraDevice.setParameters(mParameters); + + Log.v(TAG, "Video snapshot start"); + mCameraDevice.takePicture(mHandler, + null, null, null, new JpegPictureCallback(loc)); + showVideoSnapshotUI(true); + mSnapshotInProgress = true; + UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA, + UsageStatistics.ACTION_CAPTURE_DONE, "VideoSnapshot"); + } + + @Override + public void onStop() {} + + private void loadCameraPreferences() { + CameraSettings settings = new CameraSettings(mActivity, mParameters, + mCameraId, CameraHolder.instance().getCameraInfo()); + // Remove the video quality preference setting when the quality is given in the intent. + mPreferenceGroup = filterPreferenceScreenByIntent( + settings.getPreferenceGroup(R.xml.video_preferences)); + } + + private void initializeVideoControl() { + loadCameraPreferences(); + mUI.initializePopup(mPreferenceGroup); + if (effectsActive()) { + mUI.overrideSettings( + CameraSettings.KEY_VIDEO_QUALITY, + Integer.toString(CamcorderProfile.QUALITY_480P)); + } + } + + @Override + public void onOrientationChanged(int orientation) { + // We keep the last known orientation. So if the user first orient + // the camera then point the camera to floor or sky, we still have + // the correct orientation. + if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) return; + int newOrientation = Util.roundOrientation(orientation, mOrientation); + + if (mOrientation != newOrientation) { + mOrientation = newOrientation; + // The input of effects recorder is affected by + // android.hardware.Camera.setDisplayOrientation. Its value only + // compensates the camera orientation (no Display.getRotation). + // So the orientation hint here should only consider sensor + // orientation. + if (effectsActive()) { + mEffectsRecorder.setOrientationHint(mOrientation); + } + } + + // Show the toast after getting the first orientation changed. + if (mHandler.hasMessages(SHOW_TAP_TO_SNAPSHOT_TOAST)) { + mHandler.removeMessages(SHOW_TAP_TO_SNAPSHOT_TOAST); + showTapToSnapshotToast(); + } + } + + private void startPlayVideoActivity() { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(mCurrentVideoUri, convertOutputFormatToMimeType(mProfile.fileFormat)); + try { + mActivity.startActivity(intent); + } catch (ActivityNotFoundException ex) { + Log.e(TAG, "Couldn't view video " + mCurrentVideoUri, ex); + } + } + + @OnClickAttr + public void onReviewPlayClicked(View v) { + startPlayVideoActivity(); + } + + @OnClickAttr + public void onReviewDoneClicked(View v) { + mIsInReviewMode = false; + doReturnToCaller(true); + } + + @OnClickAttr + public void onReviewCancelClicked(View v) { + mIsInReviewMode = false; + stopVideoRecording(); + doReturnToCaller(false); + } + + @Override + public boolean isInReviewMode() { + return mIsInReviewMode; + } + + private void onStopVideoRecording() { + mEffectsDisplayResult = true; + boolean recordFail = stopVideoRecording(); + if (mIsVideoCaptureIntent) { + if (!effectsActive()) { + if (mQuickCapture) { + doReturnToCaller(!recordFail); + } else if (!recordFail) { + showCaptureResult(); + } + } + } else if (!recordFail){ + // Start capture animation. + if (!mPaused && ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { + // The capture animation is disabled on ICS because we use SurfaceView + // for preview during recording. When the recording is done, we switch + // back to use SurfaceTexture for preview and we need to stop then start + // the preview. This will cause the preview flicker since the preview + // will not be continuous for a short period of time. + // TODO: need to get the capture animation to work + // ((CameraScreenNail) mActivity.mCameraScreenNail).animateCapture(mDisplayRotation); + + mUI.enablePreviewThumb(true); + + // Make sure to disable the thumbnail preview after the + // animation is done to disable the click target. + mHandler.removeMessages(CAPTURE_ANIMATION_DONE); + mHandler.sendEmptyMessageDelayed(CAPTURE_ANIMATION_DONE, + CaptureAnimManager.getAnimationDuration()); + } + } + } + + public void onProtectiveCurtainClick(View v) { + // Consume clicks + } + + @Override + public void onShutterButtonClick() { + if (mUI.collapseCameraControls() || mSwitchingCamera) return; + + boolean stop = mMediaRecorderRecording; + + if (stop) { + onStopVideoRecording(); + } else { + startVideoRecording(); + } + mUI.enableShutter(false); + + // Keep the shutter button disabled when in video capture intent + // mode and recording is stopped. It'll be re-enabled when + // re-take button is clicked. + if (!(mIsVideoCaptureIntent && stop)) { + mHandler.sendEmptyMessageDelayed( + ENABLE_SHUTTER_BUTTON, SHUTTER_BUTTON_TIMEOUT); + } + } + + @Override + public void onShutterButtonFocus(boolean pressed) { + mUI.setShutterPressed(pressed); + } + + private void readVideoPreferences() { + // The preference stores values from ListPreference and is thus string type for all values. + // We need to convert it to int manually. + String videoQuality = mPreferences.getString(CameraSettings.KEY_VIDEO_QUALITY, + null); + if (videoQuality == null) { + // check for highest quality before setting default value + videoQuality = CameraSettings.getSupportedHighestVideoQuality(mCameraId, + mActivity.getResources().getString(R.string.pref_video_quality_default)); + mPreferences.edit().putString(CameraSettings.KEY_VIDEO_QUALITY, videoQuality); + } + int quality = Integer.valueOf(videoQuality); + + // Set video quality. + Intent intent = mActivity.getIntent(); + if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) { + int extraVideoQuality = + intent.getIntExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0); + if (extraVideoQuality > 0) { + quality = CamcorderProfile.QUALITY_HIGH; + } else { // 0 is mms. + quality = CamcorderProfile.QUALITY_LOW; + } + } + + // Set video duration limit. The limit is read from the preference, + // unless it is specified in the intent. + if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) { + int seconds = + intent.getIntExtra(MediaStore.EXTRA_DURATION_LIMIT, 0); + mMaxVideoDurationInMs = 1000 * seconds; + } else { + mMaxVideoDurationInMs = CameraSettings.getMaxVideoDuration(mActivity); + } + + // Set effect + mEffectType = CameraSettings.readEffectType(mPreferences); + if (mEffectType != EffectsRecorder.EFFECT_NONE) { + mEffectParameter = CameraSettings.readEffectParameter(mPreferences); + // Set quality to be no higher than 480p. + CamcorderProfile profile = CamcorderProfile.get(mCameraId, quality); + if (profile.videoFrameHeight > 480) { + quality = CamcorderProfile.QUALITY_480P; + } + } else { + mEffectParameter = null; + } + // Read time lapse recording interval. + if (ApiHelper.HAS_TIME_LAPSE_RECORDING) { + String frameIntervalStr = mPreferences.getString( + CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL, + mActivity.getString(R.string.pref_video_time_lapse_frame_interval_default)); + mTimeBetweenTimeLapseFrameCaptureMs = Integer.parseInt(frameIntervalStr); + mCaptureTimeLapse = (mTimeBetweenTimeLapseFrameCaptureMs != 0); + } + // TODO: This should be checked instead directly +1000. + if (mCaptureTimeLapse) quality += 1000; + mProfile = CamcorderProfile.get(mCameraId, quality); + getDesiredPreviewSize(); + mPreferenceRead = true; + } + + private void writeDefaultEffectToPrefs() { + ComboPreferences.Editor editor = mPreferences.edit(); + editor.putString(CameraSettings.KEY_VIDEO_EFFECT, + mActivity.getString(R.string.pref_video_effect_default)); + editor.apply(); + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + private void getDesiredPreviewSize() { + mParameters = mCameraDevice.getParameters(); + if (ApiHelper.HAS_GET_SUPPORTED_VIDEO_SIZE) { + if (mParameters.getSupportedVideoSizes() == null || effectsActive()) { + mDesiredPreviewWidth = mProfile.videoFrameWidth; + mDesiredPreviewHeight = mProfile.videoFrameHeight; + } else { // Driver supports separates outputs for preview and video. + List<Size> sizes = mParameters.getSupportedPreviewSizes(); + Size preferred = mParameters.getPreferredPreviewSizeForVideo(); + int product = preferred.width * preferred.height; + Iterator<Size> it = sizes.iterator(); + // Remove the preview sizes that are not preferred. + while (it.hasNext()) { + Size size = it.next(); + if (size.width * size.height > product) { + it.remove(); + } + } + Size optimalSize = Util.getOptimalPreviewSize(mActivity, sizes, + (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight); + mDesiredPreviewWidth = optimalSize.width; + mDesiredPreviewHeight = optimalSize.height; + } + } else { + mDesiredPreviewWidth = mProfile.videoFrameWidth; + mDesiredPreviewHeight = mProfile.videoFrameHeight; + } + mUI.setPreviewSize(mDesiredPreviewWidth, mDesiredPreviewHeight); + Log.v(TAG, "mDesiredPreviewWidth=" + mDesiredPreviewWidth + + ". mDesiredPreviewHeight=" + mDesiredPreviewHeight); + } + + private void resizeForPreviewAspectRatio() { + mUI.setAspectRatio( + (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight); + } + + @Override + public void installIntentFilter() { + // install an intent filter to receive SD card related events. + IntentFilter intentFilter = + new IntentFilter(Intent.ACTION_MEDIA_EJECT); + intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED); + intentFilter.addDataScheme("file"); + mReceiver = new MyBroadcastReceiver(); + mActivity.registerReceiver(mReceiver, intentFilter); + } + + @Override + public void onResumeBeforeSuper() { + mPaused = false; + } + + @Override + public void onResumeAfterSuper() { + if (mOpenCameraFail || mCameraDisabled) + return; + mUI.enableShutter(false); + mZoomValue = 0; + + showVideoSnapshotUI(false); + + if (!mPreviewing) { + resetEffect(); + openCamera(); + if (mOpenCameraFail) { + Util.showErrorAndFinish(mActivity, + R.string.cannot_connect_camera); + return; + } else if (mCameraDisabled) { + Util.showErrorAndFinish(mActivity, R.string.camera_disabled); + return; + } + readVideoPreferences(); + resizeForPreviewAspectRatio(); + startPreview(); + } else { + // preview already started + mUI.enableShutter(true); + } + + // Initializing it here after the preview is started. + mUI.initializeZoom(mParameters); + + keepScreenOnAwhile(); + + // Initialize location service. + boolean recordLocation = RecordLocationPreference.get(mPreferences, + mContentResolver); + mLocationManager.recordLocation(recordLocation); + + if (mPreviewing) { + mOnResumeTime = SystemClock.uptimeMillis(); + mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100); + } + // Dismiss open menu if exists. + PopupManager.getInstance(mActivity).notifyShowPopup(null); + + UsageStatistics.onContentViewChanged( + UsageStatistics.COMPONENT_CAMERA, "VideoModule"); + } + + private void setDisplayOrientation() { + mDisplayRotation = Util.getDisplayRotation(mActivity); + mCameraDisplayOrientation = Util.getDisplayOrientation(mDisplayRotation, mCameraId); + // Change the camera display orientation + if (mCameraDevice != null) { + mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation); + } + } + + @Override + public void updateCameraOrientation() { + if (mMediaRecorderRecording) return; + if (mDisplayRotation != Util.getDisplayRotation(mActivity)) { + setDisplayOrientation(); + } + } + + @Override + public int onZoomChanged(int index) { + // Not useful to change zoom value when the activity is paused. + if (mPaused) return index; + mZoomValue = index; + if (mParameters == null || mCameraDevice == null) return index; + // Set zoom parameters asynchronously + mParameters.setZoom(mZoomValue); + mCameraDevice.setParameters(mParameters); + Parameters p = mCameraDevice.getParameters(); + if (p != null) return p.getZoom(); + return index; + } + + private void startPreview() { + Log.v(TAG, "startPreview"); + + SurfaceTexture surfaceTexture = mUI.getSurfaceTexture(); + if (!mPreferenceRead || surfaceTexture == null || mPaused == true) return; + + mCameraDevice.setErrorCallback(mErrorCallback); + if (mPreviewing == true) { + stopPreview(); + if (effectsActive() && mEffectsRecorder != null) { + mEffectsRecorder.release(); + mEffectsRecorder = null; + } + } + + setDisplayOrientation(); + mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation); + setCameraParameters(); + + try { + if (!effectsActive()) { + mCameraDevice.setPreviewTexture(surfaceTexture); + mCameraDevice.startPreview(); + mPreviewing = true; + onPreviewStarted(); + } else { + initializeEffectsPreview(); + mEffectsRecorder.startPreview(); + mPreviewing = true; + onPreviewStarted(); + } + } catch (Throwable ex) { + closeCamera(); + throw new RuntimeException("startPreview failed", ex); + } finally { + if (mOpenCameraFail) { + Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera); + } else if (mCameraDisabled) { + Util.showErrorAndFinish(mActivity, R.string.camera_disabled); + } + } + + } + + private void onPreviewStarted() { + mUI.enableShutter(true); + } + + @Override + public void stopPreview() { + if (!mPreviewing) return; + mCameraDevice.stopPreview(); + mPreviewing = false; + } + + // Closing the effects out. Will shut down the effects graph. + private void closeEffects() { + Log.v(TAG, "Closing effects"); + mEffectType = EffectsRecorder.EFFECT_NONE; + if (mEffectsRecorder == null) { + Log.d(TAG, "Effects are already closed. Nothing to do"); + return; + } + // This call can handle the case where the camera is already released + // after the recording has been stopped. + mEffectsRecorder.release(); + mEffectsRecorder = null; + } + + // By default, we want to close the effects as well with the camera. + private void closeCamera() { + closeCamera(true); + } + + // In certain cases, when the effects are active, we may want to shutdown + // only the camera related parts, and handle closing the effects in the + // effectsUpdate callback. + // For example, in onPause, we want to make the camera available to + // outside world immediately, however, want to wait till the effects + // callback to shut down the effects. In such a case, we just disconnect + // the effects from the camera by calling disconnectCamera. That way + // the effects can handle that when shutting down. + // + // @param closeEffectsAlso - indicates whether we want to close the + // effects also along with the camera. + private void closeCamera(boolean closeEffectsAlso) { + Log.v(TAG, "closeCamera"); + if (mCameraDevice == null) { + Log.d(TAG, "already stopped."); + return; + } + + if (mEffectsRecorder != null) { + // Disconnect the camera from effects so that camera is ready to + // be released to the outside world. + mEffectsRecorder.disconnectCamera(); + } + if (closeEffectsAlso) closeEffects(); + mCameraDevice.setZoomChangeListener(null); + mCameraDevice.setErrorCallback(null); + CameraHolder.instance().release(); + mCameraDevice = null; + mPreviewing = false; + mSnapshotInProgress = false; + } + + private void releasePreviewResources() { + if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { + mUI.hideSurfaceView(); + } + } + + @Override + public void onPauseBeforeSuper() { + mPaused = true; + + if (mMediaRecorderRecording) { + // Camera will be released in onStopVideoRecording. + onStopVideoRecording(); + } else { + closeCamera(); + if (!effectsActive()) releaseMediaRecorder(); + } + if (effectsActive()) { + // If the effects are active, make sure we tell the graph that the + // surfacetexture is not valid anymore. Disconnect the graph from + // the display. This should be done before releasing the surface + // texture. + mEffectsRecorder.disconnectDisplay(); + } else { + // Close the file descriptor and clear the video namer only if the + // effects are not active. If effects are active, we need to wait + // till we get the callback from the Effects that the graph is done + // recording. That also needs a change in the stopVideoRecording() + // call to not call closeCamera if the effects are active, because + // that will close down the effects are well, thus making this if + // condition invalid. + closeVideoFileDescriptor(); + } + + releasePreviewResources(); + + if (mReceiver != null) { + mActivity.unregisterReceiver(mReceiver); + mReceiver = null; + } + resetScreenOn(); + + if (mLocationManager != null) mLocationManager.recordLocation(false); + + mHandler.removeMessages(CHECK_DISPLAY_ROTATION); + mHandler.removeMessages(SWITCH_CAMERA); + mHandler.removeMessages(SWITCH_CAMERA_START_ANIMATION); + mPendingSwitchCameraId = -1; + mSwitchingCamera = false; + mPreferenceRead = false; + // Call onPause after stopping video recording. So the camera can be + // released as soon as possible. + } + + @Override + public void onPauseAfterSuper() { + } + + @Override + public void onUserInteraction() { + if (!mMediaRecorderRecording && !mActivity.isFinishing()) { + keepScreenOnAwhile(); + } + } + + @Override + public boolean onBackPressed() { + if (mPaused) return true; + if (mMediaRecorderRecording) { + onStopVideoRecording(); + return true; + } else if (mUI.hidePieRenderer()) { + return true; + } else { + return mUI.removeTopLevelPopup(); + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + // Do not handle any key if the activity is paused. + if (mPaused) { + return true; + } + + switch (keyCode) { + case KeyEvent.KEYCODE_CAMERA: + if (event.getRepeatCount() == 0) { + mUI.clickShutter(); + return true; + } + break; + case KeyEvent.KEYCODE_DPAD_CENTER: + if (event.getRepeatCount() == 0) { + mUI.clickShutter(); + return true; + } + break; + case KeyEvent.KEYCODE_MENU: + if (mMediaRecorderRecording) return true; + break; + } + return false; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_CAMERA: + mUI.pressShutter(false); + return true; + } + return false; + } + + @Override + public boolean isVideoCaptureIntent() { + String action = mActivity.getIntent().getAction(); + return (MediaStore.ACTION_VIDEO_CAPTURE.equals(action)); + } + + private void doReturnToCaller(boolean valid) { + Intent resultIntent = new Intent(); + int resultCode; + if (valid) { + resultCode = Activity.RESULT_OK; + resultIntent.setData(mCurrentVideoUri); + } else { + resultCode = Activity.RESULT_CANCELED; + } + mActivity.setResultEx(resultCode, resultIntent); + mActivity.finish(); + } + + private void cleanupEmptyFile() { + if (mVideoFilename != null) { + File f = new File(mVideoFilename); + if (f.length() == 0 && f.delete()) { + Log.v(TAG, "Empty video file deleted: " + mVideoFilename); + mVideoFilename = null; + } + } + } + + private void setupMediaRecorderPreviewDisplay() { + // Nothing to do here if using SurfaceTexture. + if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { + // We stop the preview here before unlocking the device because we + // need to change the SurfaceTexture to SurfaceView for preview. + stopPreview(); + mCameraDevice.setPreviewDisplay(mUI.getSurfaceHolder()); + // The orientation for SurfaceTexture is different from that for + // SurfaceView. For SurfaceTexture we don't need to consider the + // display rotation. Just consider the sensor's orientation and we + // will set the orientation correctly when showing the texture. + // Gallery will handle the orientation for the preview. For + // SurfaceView we will have to take everything into account so the + // display rotation is considered. + mCameraDevice.setDisplayOrientation( + Util.getDisplayOrientation(mDisplayRotation, mCameraId)); + mCameraDevice.startPreview(); + mPreviewing = true; + mMediaRecorder.setPreviewDisplay(mUI.getSurfaceHolder().getSurface()); + } + } + + // Prepares media recorder. + private void initializeRecorder() { + Log.v(TAG, "initializeRecorder"); + // If the mCameraDevice is null, then this activity is going to finish + if (mCameraDevice == null) return; + + if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { + // Set the SurfaceView to visible so the surface gets created. + // surfaceCreated() is called immediately when the visibility is + // changed to visible. Thus, mSurfaceViewReady should become true + // right after calling setVisibility(). + mUI.showSurfaceView(); + } + + Intent intent = mActivity.getIntent(); + Bundle myExtras = intent.getExtras(); + + long requestedSizeLimit = 0; + closeVideoFileDescriptor(); + if (mIsVideoCaptureIntent && myExtras != null) { + Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT); + if (saveUri != null) { + try { + mVideoFileDescriptor = + mContentResolver.openFileDescriptor(saveUri, "rw"); + mCurrentVideoUri = saveUri; + } catch (java.io.FileNotFoundException ex) { + // invalid uri + Log.e(TAG, ex.toString()); + } + } + requestedSizeLimit = myExtras.getLong(MediaStore.EXTRA_SIZE_LIMIT); + } + mMediaRecorder = new MediaRecorder(); + + setupMediaRecorderPreviewDisplay(); + // Unlock the camera object before passing it to media recorder. + mCameraDevice.unlock(); + mMediaRecorder.setCamera(mCameraDevice.getCamera()); + if (!mCaptureTimeLapse) { + mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); + } + mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); + mMediaRecorder.setProfile(mProfile); + mMediaRecorder.setMaxDuration(mMaxVideoDurationInMs); + if (mCaptureTimeLapse) { + double fps = 1000 / (double) mTimeBetweenTimeLapseFrameCaptureMs; + setCaptureRate(mMediaRecorder, fps); + } + + setRecordLocation(); + + // Set output file. + // Try Uri in the intent first. If it doesn't exist, use our own + // instead. + if (mVideoFileDescriptor != null) { + mMediaRecorder.setOutputFile(mVideoFileDescriptor.getFileDescriptor()); + } else { + generateVideoFilename(mProfile.fileFormat); + mMediaRecorder.setOutputFile(mVideoFilename); + } + + // Set maximum file size. + long maxFileSize = mActivity.getStorageSpace() - Storage.LOW_STORAGE_THRESHOLD; + if (requestedSizeLimit > 0 && requestedSizeLimit < maxFileSize) { + maxFileSize = requestedSizeLimit; + } + + try { + mMediaRecorder.setMaxFileSize(maxFileSize); + } catch (RuntimeException exception) { + // We are going to ignore failure of setMaxFileSize here, as + // a) The composer selected may simply not support it, or + // b) The underlying media framework may not handle 64-bit range + // on the size restriction. + } + + // See android.hardware.Camera.Parameters.setRotation for + // documentation. + // Note that mOrientation here is the device orientation, which is the opposite of + // what activity.getWindowManager().getDefaultDisplay().getRotation() would return, + // which is the orientation the graphics need to rotate in order to render correctly. + int rotation = 0; + if (mOrientation != OrientationEventListener.ORIENTATION_UNKNOWN) { + CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId]; + if (info.facing == CameraInfo.CAMERA_FACING_FRONT) { + rotation = (info.orientation - mOrientation + 360) % 360; + } else { // back-facing camera + rotation = (info.orientation + mOrientation) % 360; + } + } + mMediaRecorder.setOrientationHint(rotation); + + try { + mMediaRecorder.prepare(); + } catch (IOException e) { + Log.e(TAG, "prepare failed for " + mVideoFilename, e); + releaseMediaRecorder(); + throw new RuntimeException(e); + } + + mMediaRecorder.setOnErrorListener(this); + mMediaRecorder.setOnInfoListener(this); + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + private static void setCaptureRate(MediaRecorder recorder, double fps) { + recorder.setCaptureRate(fps); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private void setRecordLocation() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + Location loc = mLocationManager.getCurrentLocation(); + if (loc != null) { + mMediaRecorder.setLocation((float) loc.getLatitude(), + (float) loc.getLongitude()); + } + } + } + + private void initializeEffectsPreview() { + Log.v(TAG, "initializeEffectsPreview"); + // If the mCameraDevice is null, then this activity is going to finish + if (mCameraDevice == null) return; + + boolean inLandscape = (mActivity.getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE); + + CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId]; + + mEffectsDisplayResult = false; + mEffectsRecorder = new EffectsRecorder(mActivity); + + // TODO: Confirm none of the following need to go to initializeEffectsRecording() + // and none of these change even when the preview is not refreshed. + mEffectsRecorder.setCameraDisplayOrientation(mCameraDisplayOrientation); + mEffectsRecorder.setCamera(mCameraDevice); + mEffectsRecorder.setCameraFacing(info.facing); + mEffectsRecorder.setProfile(mProfile); + mEffectsRecorder.setEffectsListener(this); + mEffectsRecorder.setOnInfoListener(this); + mEffectsRecorder.setOnErrorListener(this); + + // The input of effects recorder is affected by + // android.hardware.Camera.setDisplayOrientation. Its value only + // compensates the camera orientation (no Display.getRotation). So the + // orientation hint here should only consider sensor orientation. + int orientation = 0; + if (mOrientation != OrientationEventListener.ORIENTATION_UNKNOWN) { + orientation = mOrientation; + } + mEffectsRecorder.setOrientationHint(orientation); + + mEffectsRecorder.setPreviewSurfaceTexture(mUI.getSurfaceTexture(), + mUI.getPreviewWidth(), mUI.getPreviewHeight()); + + if (mEffectType == EffectsRecorder.EFFECT_BACKDROPPER && + ((String) mEffectParameter).equals(EFFECT_BG_FROM_GALLERY)) { + mEffectsRecorder.setEffect(mEffectType, mEffectUriFromGallery); + } else { + mEffectsRecorder.setEffect(mEffectType, mEffectParameter); + } + } + + private void initializeEffectsRecording() { + Log.v(TAG, "initializeEffectsRecording"); + + Intent intent = mActivity.getIntent(); + Bundle myExtras = intent.getExtras(); + + long requestedSizeLimit = 0; + closeVideoFileDescriptor(); + if (mIsVideoCaptureIntent && myExtras != null) { + Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT); + if (saveUri != null) { + try { + mVideoFileDescriptor = + mContentResolver.openFileDescriptor(saveUri, "rw"); + mCurrentVideoUri = saveUri; + } catch (java.io.FileNotFoundException ex) { + // invalid uri + Log.e(TAG, ex.toString()); + } + } + requestedSizeLimit = myExtras.getLong(MediaStore.EXTRA_SIZE_LIMIT); + } + + mEffectsRecorder.setProfile(mProfile); + // important to set the capture rate to zero if not timelapsed, since the + // effectsrecorder object does not get created again for each recording + // session + if (mCaptureTimeLapse) { + mEffectsRecorder.setCaptureRate((1000 / (double) mTimeBetweenTimeLapseFrameCaptureMs)); + } else { + mEffectsRecorder.setCaptureRate(0); + } + + // Set output file + if (mVideoFileDescriptor != null) { + mEffectsRecorder.setOutputFile(mVideoFileDescriptor.getFileDescriptor()); + } else { + generateVideoFilename(mProfile.fileFormat); + mEffectsRecorder.setOutputFile(mVideoFilename); + } + + // Set maximum file size. + long maxFileSize = mActivity.getStorageSpace() - Storage.LOW_STORAGE_THRESHOLD; + if (requestedSizeLimit > 0 && requestedSizeLimit < maxFileSize) { + maxFileSize = requestedSizeLimit; + } + mEffectsRecorder.setMaxFileSize(maxFileSize); + mEffectsRecorder.setMaxDuration(mMaxVideoDurationInMs); + } + + + private void releaseMediaRecorder() { + Log.v(TAG, "Releasing media recorder."); + if (mMediaRecorder != null) { + cleanupEmptyFile(); + mMediaRecorder.reset(); + mMediaRecorder.release(); + mMediaRecorder = null; + } + mVideoFilename = null; + } + + private void releaseEffectsRecorder() { + Log.v(TAG, "Releasing effects recorder."); + if (mEffectsRecorder != null) { + cleanupEmptyFile(); + mEffectsRecorder.release(); + mEffectsRecorder = null; + } + mEffectType = EffectsRecorder.EFFECT_NONE; + mVideoFilename = null; + } + + private void generateVideoFilename(int outputFileFormat) { + long dateTaken = System.currentTimeMillis(); + String title = createName(dateTaken); + // Used when emailing. + String filename = title + convertOutputFormatToFileExt(outputFileFormat); + String mime = convertOutputFormatToMimeType(outputFileFormat); + String path = Storage.DIRECTORY + '/' + filename; + String tmpPath = path + ".tmp"; + mCurrentVideoValues = new ContentValues(9); + mCurrentVideoValues.put(Video.Media.TITLE, title); + mCurrentVideoValues.put(Video.Media.DISPLAY_NAME, filename); + mCurrentVideoValues.put(Video.Media.DATE_TAKEN, dateTaken); + mCurrentVideoValues.put(MediaColumns.DATE_MODIFIED, dateTaken / 1000); + mCurrentVideoValues.put(Video.Media.MIME_TYPE, mime); + mCurrentVideoValues.put(Video.Media.DATA, path); + mCurrentVideoValues.put(Video.Media.RESOLUTION, + Integer.toString(mProfile.videoFrameWidth) + "x" + + Integer.toString(mProfile.videoFrameHeight)); + Location loc = mLocationManager.getCurrentLocation(); + if (loc != null) { + mCurrentVideoValues.put(Video.Media.LATITUDE, loc.getLatitude()); + mCurrentVideoValues.put(Video.Media.LONGITUDE, loc.getLongitude()); + } + mVideoFilename = tmpPath; + Log.v(TAG, "New video filename: " + mVideoFilename); + } + + private void saveVideo() { + if (mVideoFileDescriptor == null) { + long duration = SystemClock.uptimeMillis() - mRecordingStartTime; + if (duration > 0) { + if (mCaptureTimeLapse) { + duration = getTimeLapseVideoLength(duration); + } + } else { + Log.w(TAG, "Video duration <= 0 : " + duration); + } + mActivity.getMediaSaveService().addVideo(mCurrentVideoFilename, + duration, mCurrentVideoValues, + mOnVideoSavedListener, mContentResolver); + } + mCurrentVideoValues = null; + } + + private void deleteVideoFile(String fileName) { + Log.v(TAG, "Deleting video " + fileName); + File f = new File(fileName); + if (!f.delete()) { + Log.v(TAG, "Could not delete " + fileName); + } + } + + private PreferenceGroup filterPreferenceScreenByIntent( + PreferenceGroup screen) { + Intent intent = mActivity.getIntent(); + if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) { + CameraSettings.removePreferenceFromScreen(screen, + CameraSettings.KEY_VIDEO_QUALITY); + } + + if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) { + CameraSettings.removePreferenceFromScreen(screen, + CameraSettings.KEY_VIDEO_QUALITY); + } + return screen; + } + + // from MediaRecorder.OnErrorListener + @Override + public void onError(MediaRecorder mr, int what, int extra) { + Log.e(TAG, "MediaRecorder error. what=" + what + ". extra=" + extra); + if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) { + // We may have run out of space on the sdcard. + stopVideoRecording(); + mActivity.updateStorageSpaceAndHint(); + } + } + + // from MediaRecorder.OnInfoListener + @Override + public void onInfo(MediaRecorder mr, int what, int extra) { + if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) { + if (mMediaRecorderRecording) onStopVideoRecording(); + } else if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) { + if (mMediaRecorderRecording) onStopVideoRecording(); + + // Show the toast. + Toast.makeText(mActivity, R.string.video_reach_size_limit, + Toast.LENGTH_LONG).show(); + } + } + + /* + * Make sure we're not recording music playing in the background, ask the + * MediaPlaybackService to pause playback. + */ + private void pauseAudioPlayback() { + // Shamelessly copied from MediaPlaybackService.java, which + // should be public, but isn't. + Intent i = new Intent("com.android.music.musicservicecommand"); + i.putExtra("command", "pause"); + + mActivity.sendBroadcast(i); + } + + // For testing. + public boolean isRecording() { + return mMediaRecorderRecording; + } + + private void startVideoRecording() { + Log.v(TAG, "startVideoRecording"); + mUI.enablePreviewThumb(false); + mUI.setSwipingEnabled(false); + + mActivity.updateStorageSpaceAndHint(); + if (mActivity.getStorageSpace() <= Storage.LOW_STORAGE_THRESHOLD) { + Log.v(TAG, "Storage issue, ignore the start request"); + return; + } + + //?? + //if (!mCameraDevice.waitDone()) return; + mCurrentVideoUri = null; + if (effectsActive()) { + initializeEffectsRecording(); + if (mEffectsRecorder == null) { + Log.e(TAG, "Fail to initialize effect recorder"); + return; + } + } else { + initializeRecorder(); + if (mMediaRecorder == null) { + Log.e(TAG, "Fail to initialize media recorder"); + return; + } + } + + pauseAudioPlayback(); + + if (effectsActive()) { + try { + mEffectsRecorder.startRecording(); + } catch (RuntimeException e) { + Log.e(TAG, "Could not start effects recorder. ", e); + releaseEffectsRecorder(); + return; + } + } else { + try { + mMediaRecorder.start(); // Recording is now started + } catch (RuntimeException e) { + Log.e(TAG, "Could not start media recorder. ", e); + releaseMediaRecorder(); + // If start fails, frameworks will not lock the camera for us. + mCameraDevice.lock(); + return; + } + } + + // Make sure the video recording has started before announcing + // this in accessibility. + AccessibilityUtils.makeAnnouncement(mUI.getShutterButton(), + mActivity.getString(R.string.video_recording_started)); + + // The parameters might have been altered by MediaRecorder already. + // We need to force mCameraDevice to refresh before getting it. + mCameraDevice.refreshParameters(); + // The parameters may have been changed by MediaRecorder upon starting + // recording. We need to alter the parameters if we support camcorder + // zoom. To reduce latency when setting the parameters during zoom, we + // update mParameters here once. + if (ApiHelper.HAS_ZOOM_WHEN_RECORDING) { + mParameters = mCameraDevice.getParameters(); + } + + mUI.enableCameraControls(false); + + mMediaRecorderRecording = true; + mOrientationManager.lockOrientation(); + mRecordingStartTime = SystemClock.uptimeMillis(); + mUI.showRecordingUI(true, mParameters.isZoomSupported()); + + updateRecordingTime(); + keepScreenOn(); + UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA, + UsageStatistics.ACTION_CAPTURE_START, "Video"); + } + + private void showCaptureResult() { + mIsInReviewMode = true; + Bitmap bitmap = null; + if (mVideoFileDescriptor != null) { + bitmap = Thumbnail.createVideoThumbnailBitmap(mVideoFileDescriptor.getFileDescriptor(), + mDesiredPreviewWidth); + } else if (mCurrentVideoFilename != null) { + bitmap = Thumbnail.createVideoThumbnailBitmap(mCurrentVideoFilename, + mDesiredPreviewWidth); + } + if (bitmap != null) { + // MetadataRetriever already rotates the thumbnail. We should rotate + // it to match the UI orientation (and mirror if it is front-facing camera). + CameraInfo[] info = CameraHolder.instance().getCameraInfo(); + boolean mirror = (info[mCameraId].facing == CameraInfo.CAMERA_FACING_FRONT); + bitmap = Util.rotateAndMirror(bitmap, 0, mirror); + mUI.showReviewImage(bitmap); + } + + mUI.showReviewControls(); + mUI.enableCameraControls(false); + mUI.showTimeLapseUI(false); + } + + private void hideAlert() { + mUI.enableCameraControls(true); + mUI.hideReviewUI(); + if (mCaptureTimeLapse) { + mUI.showTimeLapseUI(true); + } + } + + private boolean stopVideoRecording() { + Log.v(TAG, "stopVideoRecording"); + mUI.setSwipingEnabled(true); + mUI.showSwitcher(); + + boolean fail = false; + if (mMediaRecorderRecording) { + boolean shouldAddToMediaStoreNow = false; + + try { + if (effectsActive()) { + // This is asynchronous, so we can't add to media store now because thumbnail + // may not be ready. In such case saveVideo() is called later + // through a callback from the MediaEncoderFilter to EffectsRecorder, + // and then to the VideoModule. + mEffectsRecorder.stopRecording(); + } else { + mMediaRecorder.setOnErrorListener(null); + mMediaRecorder.setOnInfoListener(null); + mMediaRecorder.stop(); + shouldAddToMediaStoreNow = true; + } + mCurrentVideoFilename = mVideoFilename; + Log.v(TAG, "stopVideoRecording: Setting current video filename: " + + mCurrentVideoFilename); + AccessibilityUtils.makeAnnouncement(mUI.getShutterButton(), + mActivity.getString(R.string.video_recording_stopped)); + } catch (RuntimeException e) { + Log.e(TAG, "stop fail", e); + if (mVideoFilename != null) deleteVideoFile(mVideoFilename); + fail = true; + } + mMediaRecorderRecording = false; + mOrientationManager.unlockOrientation(); + + // If the activity is paused, this means activity is interrupted + // during recording. Release the camera as soon as possible because + // face unlock or other applications may need to use the camera. + // However, if the effects are active, then we can only release the + // camera and cannot release the effects recorder since that will + // stop the graph. It is possible to separate out the Camera release + // part and the effects release part. However, the effects recorder + // does hold on to the camera, hence, it needs to be "disconnected" + // from the camera in the closeCamera call. + if (mPaused) { + // Closing only the camera part if effects active. Effects will + // be closed in the callback from effects. + boolean closeEffects = !effectsActive(); + closeCamera(closeEffects); + } + + mUI.showRecordingUI(false, mParameters.isZoomSupported()); + if (!mIsVideoCaptureIntent) { + mUI.enableCameraControls(true); + } + // The orientation was fixed during video recording. Now make it + // reflect the device orientation as video recording is stopped. + mUI.setOrientationIndicator(0, true); + keepScreenOnAwhile(); + if (shouldAddToMediaStoreNow) { + saveVideo(); + } + } + // always release media recorder if no effects running + if (!effectsActive()) { + releaseMediaRecorder(); + if (!mPaused) { + mCameraDevice.lock(); + if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { + stopPreview(); + mUI.hideSurfaceView(); + // Switch back to use SurfaceTexture for preview. + startPreview(); + } + } + } + // Update the parameters here because the parameters might have been altered + // by MediaRecorder. + if (!mPaused) mParameters = mCameraDevice.getParameters(); + UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA, + fail ? UsageStatistics.ACTION_CAPTURE_FAIL : + UsageStatistics.ACTION_CAPTURE_DONE, "Video", + SystemClock.uptimeMillis() - mRecordingStartTime); + return fail; + } + + private void resetScreenOn() { + mHandler.removeMessages(CLEAR_SCREEN_DELAY); + mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + private void keepScreenOnAwhile() { + mHandler.removeMessages(CLEAR_SCREEN_DELAY); + mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY); + } + + private void keepScreenOn() { + mHandler.removeMessages(CLEAR_SCREEN_DELAY); + mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + private static String millisecondToTimeString(long milliSeconds, boolean displayCentiSeconds) { + long seconds = milliSeconds / 1000; // round down to compute seconds + long minutes = seconds / 60; + long hours = minutes / 60; + long remainderMinutes = minutes - (hours * 60); + long remainderSeconds = seconds - (minutes * 60); + + StringBuilder timeStringBuilder = new StringBuilder(); + + // Hours + if (hours > 0) { + if (hours < 10) { + timeStringBuilder.append('0'); + } + timeStringBuilder.append(hours); + + timeStringBuilder.append(':'); + } + + // Minutes + if (remainderMinutes < 10) { + timeStringBuilder.append('0'); + } + timeStringBuilder.append(remainderMinutes); + timeStringBuilder.append(':'); + + // Seconds + if (remainderSeconds < 10) { + timeStringBuilder.append('0'); + } + timeStringBuilder.append(remainderSeconds); + + // Centi seconds + if (displayCentiSeconds) { + timeStringBuilder.append('.'); + long remainderCentiSeconds = (milliSeconds - seconds * 1000) / 10; + if (remainderCentiSeconds < 10) { + timeStringBuilder.append('0'); + } + timeStringBuilder.append(remainderCentiSeconds); + } + + return timeStringBuilder.toString(); + } + + private long getTimeLapseVideoLength(long deltaMs) { + // For better approximation calculate fractional number of frames captured. + // This will update the video time at a higher resolution. + double numberOfFrames = (double) deltaMs / mTimeBetweenTimeLapseFrameCaptureMs; + return (long) (numberOfFrames / mProfile.videoFrameRate * 1000); + } + + private void updateRecordingTime() { + if (!mMediaRecorderRecording) { + return; + } + long now = SystemClock.uptimeMillis(); + long delta = now - mRecordingStartTime; + + // Starting a minute before reaching the max duration + // limit, we'll countdown the remaining time instead. + boolean countdownRemainingTime = (mMaxVideoDurationInMs != 0 + && delta >= mMaxVideoDurationInMs - 60000); + + long deltaAdjusted = delta; + if (countdownRemainingTime) { + deltaAdjusted = Math.max(0, mMaxVideoDurationInMs - deltaAdjusted) + 999; + } + String text; + + long targetNextUpdateDelay; + if (!mCaptureTimeLapse) { + text = millisecondToTimeString(deltaAdjusted, false); + targetNextUpdateDelay = 1000; + } else { + // The length of time lapse video is different from the length + // of the actual wall clock time elapsed. Display the video length + // only in format hh:mm:ss.dd, where dd are the centi seconds. + text = millisecondToTimeString(getTimeLapseVideoLength(delta), true); + targetNextUpdateDelay = mTimeBetweenTimeLapseFrameCaptureMs; + } + + mUI.setRecordingTime(text); + + if (mRecordingTimeCountsDown != countdownRemainingTime) { + // Avoid setting the color on every update, do it only + // when it needs changing. + mRecordingTimeCountsDown = countdownRemainingTime; + + int color = mActivity.getResources().getColor(countdownRemainingTime + ? R.color.recording_time_remaining_text + : R.color.recording_time_elapsed_text); + + mUI.setRecordingTimeTextColor(color); + } + + long actualNextUpdateDelay = targetNextUpdateDelay - (delta % targetNextUpdateDelay); + mHandler.sendEmptyMessageDelayed( + UPDATE_RECORD_TIME, actualNextUpdateDelay); + } + + private static boolean isSupported(String value, List<String> supported) { + return supported == null ? false : supported.indexOf(value) >= 0; + } + + @SuppressWarnings("deprecation") + private void setCameraParameters() { + mParameters.setPreviewSize(mDesiredPreviewWidth, mDesiredPreviewHeight); + int[] fpsRange = Util.getMaxPreviewFpsRange(mParameters); + if (fpsRange.length > 0) { + mParameters.setPreviewFpsRange( + fpsRange[Parameters.PREVIEW_FPS_MIN_INDEX], + fpsRange[Parameters.PREVIEW_FPS_MAX_INDEX]); + } else { + mParameters.setPreviewFrameRate(mProfile.videoFrameRate); + } + + // Set flash mode. + String flashMode; + if (mUI.isVisible()) { + flashMode = mPreferences.getString( + CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE, + mActivity.getString(R.string.pref_camera_video_flashmode_default)); + } else { + flashMode = Parameters.FLASH_MODE_OFF; + } + List<String> supportedFlash = mParameters.getSupportedFlashModes(); + if (isSupported(flashMode, supportedFlash)) { + mParameters.setFlashMode(flashMode); + } else { + flashMode = mParameters.getFlashMode(); + if (flashMode == null) { + flashMode = mActivity.getString( + R.string.pref_camera_flashmode_no_flash); + } + } + + // Set white balance parameter. + String whiteBalance = mPreferences.getString( + CameraSettings.KEY_WHITE_BALANCE, + mActivity.getString(R.string.pref_camera_whitebalance_default)); + if (isSupported(whiteBalance, + mParameters.getSupportedWhiteBalance())) { + mParameters.setWhiteBalance(whiteBalance); + } else { + whiteBalance = mParameters.getWhiteBalance(); + if (whiteBalance == null) { + whiteBalance = Parameters.WHITE_BALANCE_AUTO; + } + } + + // Set zoom. + if (mParameters.isZoomSupported()) { + mParameters.setZoom(mZoomValue); + } + + // Set continuous autofocus. + List<String> supportedFocus = mParameters.getSupportedFocusModes(); + if (isSupported(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, supportedFocus)) { + mParameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); + } + + mParameters.set(Util.RECORDING_HINT, Util.TRUE); + + // Enable video stabilization. Convenience methods not available in API + // level <= 14 + String vstabSupported = mParameters.get("video-stabilization-supported"); + if ("true".equals(vstabSupported)) { + mParameters.set("video-stabilization", "true"); + } + + // Set picture size. + // The logic here is different from the logic in still-mode camera. + // There we determine the preview size based on the picture size, but + // here we determine the picture size based on the preview size. + List<Size> supported = mParameters.getSupportedPictureSizes(); + Size optimalSize = Util.getOptimalVideoSnapshotPictureSize(supported, + (double) mDesiredPreviewWidth / mDesiredPreviewHeight); + Size original = mParameters.getPictureSize(); + if (!original.equals(optimalSize)) { + mParameters.setPictureSize(optimalSize.width, optimalSize.height); + } + Log.v(TAG, "Video snapshot size is " + optimalSize.width + "x" + + optimalSize.height); + + // Set JPEG quality. + int jpegQuality = CameraProfile.getJpegEncodingQualityParameter(mCameraId, + CameraProfile.QUALITY_HIGH); + mParameters.setJpegQuality(jpegQuality); + + mCameraDevice.setParameters(mParameters); + // Keep preview size up to date. + mParameters = mCameraDevice.getParameters(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_EFFECT_BACKDROPPER: + if (resultCode == Activity.RESULT_OK) { + // onActivityResult() runs before onResume(), so this parameter will be + // seen by startPreview from onResume() + mEffectUriFromGallery = data.getData().toString(); + Log.v(TAG, "Received URI from gallery: " + mEffectUriFromGallery); + mResetEffect = false; + } else { + mEffectUriFromGallery = null; + Log.w(TAG, "No URI from gallery"); + mResetEffect = true; + } + break; + } + } + + @Override + public void onEffectsUpdate(int effectId, int effectMsg) { + Log.v(TAG, "onEffectsUpdate. Effect Message = " + effectMsg); + if (effectMsg == EffectsRecorder.EFFECT_MSG_EFFECTS_STOPPED) { + // Effects have shut down. Hide learning message if any, + // and restart regular preview. + checkQualityAndStartPreview(); + } else if (effectMsg == EffectsRecorder.EFFECT_MSG_RECORDING_DONE) { + // This follows the codepath from onStopVideoRecording. + if (mEffectsDisplayResult) { + saveVideo(); + if (mIsVideoCaptureIntent) { + if (mQuickCapture) { + doReturnToCaller(true); + } else { + showCaptureResult(); + } + } + } + mEffectsDisplayResult = false; + // In onPause, these were not called if the effects were active. We + // had to wait till the effects recording is complete to do this. + if (mPaused) { + closeVideoFileDescriptor(); + } + } else if (effectMsg == EffectsRecorder.EFFECT_MSG_PREVIEW_RUNNING) { + // Enable the shutter button once the preview is complete. + mUI.enableShutter(true); + } + // In onPause, this was not called if the effects were active. We had to + // wait till the effects completed to do this. + if (mPaused) { + Log.v(TAG, "OnEffectsUpdate: closing effects if activity paused"); + closeEffects(); + } + } + + public void onCancelBgTraining(View v) { + // Write default effect out to shared prefs + writeDefaultEffectToPrefs(); + // Tell VideoCamer to re-init based on new shared pref values. + onSharedPreferenceChanged(); + } + + @Override + public synchronized void onEffectsError(Exception exception, String fileName) { + // TODO: Eventually we may want to show the user an error dialog, and then restart the + // camera and encoder gracefully. For now, we just delete the file and bail out. + if (fileName != null && new File(fileName).exists()) { + deleteVideoFile(fileName); + } + try { + if (Class.forName("android.filterpacks.videosink.MediaRecorderStopException") + .isInstance(exception)) { + Log.w(TAG, "Problem recoding video file. Removing incomplete file."); + return; + } + } catch (ClassNotFoundException ex) { + Log.w(TAG, ex); + } + throw new RuntimeException("Error during recording!", exception); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + Log.v(TAG, "onConfigurationChanged"); + setDisplayOrientation(); + } + + @Override + public void onOverriddenPreferencesClicked() { + } + + @Override + // TODO: Delete this after old camera code is removed + public void onRestorePreferencesClicked() { + } + + private boolean effectsActive() { + return (mEffectType != EffectsRecorder.EFFECT_NONE); + } + + @Override + public void onSharedPreferenceChanged() { + // ignore the events after "onPause()" or preview has not started yet + if (mPaused) return; + synchronized (mPreferences) { + // If mCameraDevice is not ready then we can set the parameter in + // startPreview(). + if (mCameraDevice == null) return; + + boolean recordLocation = RecordLocationPreference.get( + mPreferences, mContentResolver); + mLocationManager.recordLocation(recordLocation); + + // Check if the current effects selection has changed + if (updateEffectSelection()) return; + + readVideoPreferences(); + mUI.showTimeLapseUI(mCaptureTimeLapse); + // We need to restart the preview if preview size is changed. + Size size = mParameters.getPreviewSize(); + if (size.width != mDesiredPreviewWidth + || size.height != mDesiredPreviewHeight) { + if (!effectsActive()) { + stopPreview(); + } else { + mEffectsRecorder.release(); + mEffectsRecorder = null; + } + resizeForPreviewAspectRatio(); + startPreview(); // Parameters will be set in startPreview(). + } else { + setCameraParameters(); + } + mUI.updateOnScreenIndicators(mParameters, mPreferences); + } + } + + protected void setCameraId(int cameraId) { + ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID); + pref.setValue("" + cameraId); + } + + private void switchCamera() { + if (mPaused) return; + + Log.d(TAG, "Start to switch camera."); + mCameraId = mPendingSwitchCameraId; + mPendingSwitchCameraId = -1; + setCameraId(mCameraId); + + closeCamera(); + mUI.collapseCameraControls(); + // Restart the camera and initialize the UI. From onCreate. + mPreferences.setLocalId(mActivity, mCameraId); + CameraSettings.upgradeLocalPreferences(mPreferences.getLocal()); + openCamera(); + readVideoPreferences(); + startPreview(); + initializeVideoSnapshot(); + resizeForPreviewAspectRatio(); + initializeVideoControl(); + + // From onResume + mZoomValue = 0; + mUI.initializeZoom(mParameters); + mUI.setOrientationIndicator(0, false); + + // Start switch camera animation. Post a message because + // onFrameAvailable from the old camera may already exist. + mHandler.sendEmptyMessage(SWITCH_CAMERA_START_ANIMATION); + mUI.updateOnScreenIndicators(mParameters, mPreferences); + } + + // Preview texture has been copied. Now camera can be released and the + // animation can be started. + @Override + public void onPreviewTextureCopied() { + mHandler.sendEmptyMessage(SWITCH_CAMERA); + } + + @Override + public void onCaptureTextureCopied() { + } + + private boolean updateEffectSelection() { + int previousEffectType = mEffectType; + Object previousEffectParameter = mEffectParameter; + mEffectType = CameraSettings.readEffectType(mPreferences); + mEffectParameter = CameraSettings.readEffectParameter(mPreferences); + + if (mEffectType == previousEffectType) { + if (mEffectType == EffectsRecorder.EFFECT_NONE) return false; + if (mEffectParameter.equals(previousEffectParameter)) return false; + } + Log.v(TAG, "New effect selection: " + mPreferences.getString( + CameraSettings.KEY_VIDEO_EFFECT, "none")); + + if (mEffectType == EffectsRecorder.EFFECT_NONE) { + // Stop effects and return to normal preview + mEffectsRecorder.stopPreview(); + mPreviewing = false; + return true; + } + if (mEffectType == EffectsRecorder.EFFECT_BACKDROPPER && + ((String) mEffectParameter).equals(EFFECT_BG_FROM_GALLERY)) { + // Request video from gallery to use for background + Intent i = new Intent(Intent.ACTION_PICK); + i.setDataAndType(Video.Media.EXTERNAL_CONTENT_URI, + "video/*"); + i.putExtra(Intent.EXTRA_LOCAL_ONLY, true); + mActivity.startActivityForResult(i, REQUEST_EFFECT_BACKDROPPER); + return true; + } + if (previousEffectType == EffectsRecorder.EFFECT_NONE) { + // Stop regular preview and start effects. + stopPreview(); + checkQualityAndStartPreview(); + } else { + // Switch currently running effect + mEffectsRecorder.setEffect(mEffectType, mEffectParameter); + } + return true; + } + + // Verifies that the current preview view size is correct before starting + // preview. If not, resets the surface texture and resizes the view. + private void checkQualityAndStartPreview() { + readVideoPreferences(); + mUI.showTimeLapseUI(mCaptureTimeLapse); + Size size = mParameters.getPreviewSize(); + if (size.width != mDesiredPreviewWidth + || size.height != mDesiredPreviewHeight) { + resizeForPreviewAspectRatio(); + } + // Start up preview again + startPreview(); + } + + private void initializeVideoSnapshot() { + if (mParameters == null) return; + if (Util.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) { + // Show the tap to focus toast if this is the first start. + if (mPreferences.getBoolean( + CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, true)) { + // Delay the toast for one second to wait for orientation. + mHandler.sendEmptyMessageDelayed(SHOW_TAP_TO_SNAPSHOT_TOAST, 1000); + } + } + } + + void showVideoSnapshotUI(boolean enabled) { + if (mParameters == null) return; + if (Util.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) { + if (enabled) { + // TODO: ((CameraScreenNail) mActivity.mCameraScreenNail).animateCapture(mDisplayRotation); + } else { + mUI.showPreviewBorder(enabled); + } + mUI.enableShutter(!enabled); + } + } + + @Override + public void updateCameraAppView() { + if (!mPreviewing || mParameters.getFlashMode() == null) return; + + // When going to and back from gallery, we need to turn off/on the flash. + if (!mUI.isVisible()) { + if (mParameters.getFlashMode().equals(Parameters.FLASH_MODE_OFF)) { + mRestoreFlash = false; + return; + } + mRestoreFlash = true; + setCameraParameters(); + } else if (mRestoreFlash) { + mRestoreFlash = false; + setCameraParameters(); + } + } + + @Override + public void onSwitchMode(boolean toCamera) { + mUI.onSwitchMode(toCamera); + } + + private final class JpegPictureCallback implements CameraPictureCallback { + Location mLocation; + + public JpegPictureCallback(Location loc) { + mLocation = loc; + } + + @Override + public void onPictureTaken(byte [] jpegData, CameraProxy camera) { + Log.v(TAG, "onPictureTaken"); + mSnapshotInProgress = false; + showVideoSnapshotUI(false); + storeImage(jpegData, mLocation); + } + } + + private void storeImage(final byte[] data, Location loc) { + long dateTaken = System.currentTimeMillis(); + String title = Util.createJpegName(dateTaken); + ExifInterface exif = Exif.getExif(data); + int orientation = Exif.getOrientation(exif); + Size s = mParameters.getPictureSize(); + mActivity.getMediaSaveService().addImage( + data, title, dateTaken, loc, s.width, s.height, orientation, + exif, mOnPhotoSavedListener, mContentResolver); + } + + private boolean resetEffect() { + if (mResetEffect) { + String value = mPreferences.getString(CameraSettings.KEY_VIDEO_EFFECT, + mPrefVideoEffectDefault); + if (!mPrefVideoEffectDefault.equals(value)) { + writeDefaultEffectToPrefs(); + return true; + } + } + mResetEffect = true; + return false; + } + + private String convertOutputFormatToMimeType(int outputFileFormat) { + if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) { + return "video/mp4"; + } + return "video/3gpp"; + } + + private String convertOutputFormatToFileExt(int outputFileFormat) { + if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) { + return ".mp4"; + } + return ".3gp"; + } + + private void closeVideoFileDescriptor() { + if (mVideoFileDescriptor != null) { + try { + mVideoFileDescriptor.close(); + } catch (IOException e) { + Log.e(TAG, "Fail to close fd", e); + } + mVideoFileDescriptor = null; + } + } + + private void showTapToSnapshotToast() { + new RotateTextToast(mActivity, R.string.video_snapshot_hint, 0) + .show(); + // Clear the preference. + Editor editor = mPreferences.edit(); + editor.putBoolean(CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, false); + editor.apply(); + } + + @Override + public boolean updateStorageHintOnResume() { + return true; + } + + // required by OnPreferenceChangedListener + @Override + public void onCameraPickerClicked(int cameraId) { + if (mPaused || mPendingSwitchCameraId != -1) return; + + mPendingSwitchCameraId = cameraId; + Log.d(TAG, "Start to copy texture."); + // We need to keep a preview frame for the animation before + // releasing the camera. This will trigger onPreviewTextureCopied. + // TODO: ((CameraScreenNail) mActivity.mCameraScreenNail).copyTexture(); + // Disable all camera controls. + mSwitchingCamera = true; + + } + + @Override + public void onShowSwitcherPopup() { + mUI.onShowSwitcherPopup(); + } + + @Override + public void onMediaSaveServiceConnected(MediaSaveService s) { + // do nothing. + } + + @Override + public void onPreviewUIReady() { + startPreview(); + } + + @Override + public void onPreviewUIDestroyed() { + stopPreview(); + } +} diff --git a/src/com/android/camera/VideoUI.java b/src/com/android/camera/VideoUI.java new file mode 100644 index 000000000..551b72596 --- /dev/null +++ b/src/com/android/camera/VideoUI.java @@ -0,0 +1,698 @@ +/* + * Copyright (C) 2012 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 android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.SurfaceTexture; +import android.hardware.Camera.Parameters; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.view.Gravity; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.TextureView; +import android.view.TextureView.SurfaceTextureListener; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLayoutChangeListener; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.FrameLayout.LayoutParams; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.camera.CameraPreference.OnPreferenceChangedListener; +import com.android.camera.ui.AbstractSettingPopup; +import com.android.camera.ui.CameraControls; +import com.android.camera.ui.CameraRootView; +import com.android.camera.ui.CameraSwitcher; +import com.android.camera.ui.CameraSwitcher.CameraSwitchListener; +import com.android.camera.ui.PieRenderer; +import com.android.camera.ui.RenderOverlay; +import com.android.camera.ui.RotateLayout; +import com.android.camera.ui.ZoomRenderer; +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; + +import java.util.List; + +public class VideoUI implements PieRenderer.PieListener, + PreviewGestures.SingleTapListener, + CameraRootView.MyDisplayListener, + SurfaceTextureListener, SurfaceHolder.Callback { + private static final String TAG = "CAM_VideoUI"; + private static final int UPDATE_TRANSFORM_MATRIX = 1; + // module fields + private CameraActivity mActivity; + private View mRootView; + private TextureView mTextureView; + // An review image having same size as preview. It is displayed when + // recording is stopped in capture intent. + private ImageView mReviewImage; + private View mReviewCancelButton; + private View mReviewDoneButton; + private View mReviewPlayButton; + private ShutterButton mShutterButton; + private CameraSwitcher mSwitcher; + private TextView mRecordingTimeView; + private LinearLayout mLabelsLinearLayout; + private View mTimeLapseLabel; + private RenderOverlay mRenderOverlay; + private PieRenderer mPieRenderer; + private VideoMenu mVideoMenu; + private CameraControls mCameraControls; + private AbstractSettingPopup mPopup; + private ZoomRenderer mZoomRenderer; + private PreviewGestures mGestures; + private View mMenuButton; + private View mBlocker; + private OnScreenIndicators mOnScreenIndicators; + private RotateLayout mRecordingTimeRect; + private final Object mLock = new Object(); + private SurfaceTexture mSurfaceTexture; + private VideoController mController; + private int mZoomMax; + private List<Integer> mZoomRatios; + private View mPreviewThumb; + + private SurfaceView mSurfaceView = null; + private int mPreviewWidth = 0; + private int mPreviewHeight = 0; + private float mSurfaceTextureUncroppedWidth; + private float mSurfaceTextureUncroppedHeight; + private float mAspectRatio = 4f / 3f; + private Matrix mMatrix = null; + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case UPDATE_TRANSFORM_MATRIX: + setTransformMatrix(mPreviewWidth, mPreviewHeight); + break; + default: + break; + } + } + }; + private OnLayoutChangeListener mLayoutListener = new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, + int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { + int width = right - left; + int height = bottom - top; + // Full-screen screennail + int w = width; + int h = height; + if (Util.getDisplayRotation(mActivity) % 180 != 0) { + w = height; + h = width; + } + if (mPreviewWidth != width || mPreviewHeight != height) { + mPreviewWidth = width; + mPreviewHeight = height; + onScreenSizeChanged(width, height, w, h); + } + } + }; + + public VideoUI(CameraActivity activity, VideoController controller, View parent) { + mActivity = activity; + mController = controller; + mRootView = parent; + mActivity.getLayoutInflater().inflate(R.layout.video_module, (ViewGroup) mRootView, true); + mTextureView = (TextureView) mRootView.findViewById(R.id.preview_content); + mTextureView.setSurfaceTextureListener(this); + mRootView.addOnLayoutChangeListener(mLayoutListener); + ((CameraRootView) mRootView).setDisplayChangeListener(this); + mShutterButton = (ShutterButton) mRootView.findViewById(R.id.shutter_button); + mSwitcher = (CameraSwitcher) mRootView.findViewById(R.id.camera_switcher); + mSwitcher.setCurrentIndex(CameraSwitcher.VIDEO_MODULE_INDEX); + mSwitcher.setSwitchListener((CameraSwitchListener) mActivity); + initializeMiscControls(); + initializeControlByIntent(); + initializeOverlay(); + } + + + public void initializeSurfaceView() { + mSurfaceView = new SurfaceView(mActivity); + ((ViewGroup) mRootView).addView(mSurfaceView, 0); + mSurfaceView.getHolder().addCallback(this); + } + + private void initializeControlByIntent() { + mBlocker = mActivity.findViewById(R.id.blocker); + mMenuButton = mActivity.findViewById(R.id.menu); + mMenuButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mPieRenderer != null) { + mPieRenderer.showInCenter(); + } + } + }); + + mCameraControls = (CameraControls) mActivity.findViewById(R.id.camera_controls); + mOnScreenIndicators = new OnScreenIndicators(mActivity, + mActivity.findViewById(R.id.on_screen_indicators)); + mOnScreenIndicators.resetToDefault(); + if (mController.isVideoCaptureIntent()) { + hideSwitcher(); + mActivity.getLayoutInflater().inflate(R.layout.review_module_control, + (ViewGroup) mCameraControls); + // Cannot use RotateImageView for "done" and "cancel" button because + // the tablet layout uses RotateLayout, which cannot be cast to + // RotateImageView. + mReviewDoneButton = mActivity.findViewById(R.id.btn_done); + mReviewCancelButton = mActivity.findViewById(R.id.btn_cancel); + mReviewPlayButton = mActivity.findViewById(R.id.btn_play); + mReviewCancelButton.setVisibility(View.VISIBLE); + mReviewDoneButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mController.onReviewDoneClicked(v); + } + }); + mReviewCancelButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mController.onReviewCancelClicked(v); + } + }); + mReviewPlayButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mController.onReviewPlayClicked(v); + } + }); + } + } + + public void setPreviewSize(int width, int height) { + if (width == 0 || height == 0) { + Log.w(TAG, "Preview size should not be 0."); + return; + } + if (width > height) { + mAspectRatio = (float) width / height; + } else { + mAspectRatio = (float) height / width; + } + mHandler.sendEmptyMessage(UPDATE_TRANSFORM_MATRIX); + } + + public int getPreviewWidth() { + return mPreviewWidth; + } + + public int getPreviewHeight() { + return mPreviewHeight; + } + + public void onScreenSizeChanged(int width, int height, int previewWidth, int previewHeight) { + setTransformMatrix(width, height); + } + + private void setTransformMatrix(int width, int height) { + mMatrix = mTextureView.getTransform(mMatrix); + int orientation = Util.getDisplayRotation(mActivity); + float scaleX = 1f, scaleY = 1f; + float scaledTextureWidth, scaledTextureHeight; + if (width > height) { + scaledTextureWidth = Math.max(width, + (int) (height * mAspectRatio)); + scaledTextureHeight = Math.max(height, + (int)(width / mAspectRatio)); + } else { + scaledTextureWidth = Math.max(width, + (int) (height / mAspectRatio)); + scaledTextureHeight = Math.max(height, + (int) (width * mAspectRatio)); + } + + if (mSurfaceTextureUncroppedWidth != scaledTextureWidth || + mSurfaceTextureUncroppedHeight != scaledTextureHeight) { + mSurfaceTextureUncroppedWidth = scaledTextureWidth; + mSurfaceTextureUncroppedHeight = scaledTextureHeight; + } + scaleX = scaledTextureWidth / width; + scaleY = scaledTextureHeight / height; + mMatrix.setScale(scaleX, scaleY, (float) width / 2, (float) height / 2); + mTextureView.setTransform(mMatrix); + + if (mSurfaceView != null && mSurfaceView.getVisibility() == View.VISIBLE) { + LayoutParams lp = (LayoutParams) mSurfaceView.getLayoutParams(); + lp.width = (int) mSurfaceTextureUncroppedWidth; + lp.height = (int) mSurfaceTextureUncroppedHeight; + lp.gravity = Gravity.CENTER; + mSurfaceView.requestLayout(); + } + } + + public void hideUI() { + mCameraControls.setVisibility(View.INVISIBLE); + mSwitcher.closePopup(); + } + + public void showUI() { + mCameraControls.setVisibility(View.VISIBLE); + } + + public void hideSwitcher() { + mSwitcher.closePopup(); + mSwitcher.setVisibility(View.INVISIBLE); + } + + public void showSwitcher() { + mSwitcher.setVisibility(View.VISIBLE); + } + + public boolean collapseCameraControls() { + boolean ret = false; + if (mPopup != null) { + dismissPopup(false); + ret = true; + } + return ret; + } + + public boolean removeTopLevelPopup() { + if (mPopup != null) { + dismissPopup(true); + return true; + } + return false; + } + + public void enableCameraControls(boolean enable) { + if (mGestures != null) { + mGestures.setZoomOnly(!enable); + } + if (mPieRenderer != null && mPieRenderer.showsItems()) { + mPieRenderer.hide(); + } + } + + public void overrideSettings(final String... keyvalues) { + mVideoMenu.overrideSettings(keyvalues); + } + + public void setOrientationIndicator(int orientation, boolean animation) { + // We change the orientation of the linearlayout only for phone UI + // because when in portrait the width is not enough. + if (mLabelsLinearLayout != null) { + if (((orientation / 90) & 1) == 0) { + mLabelsLinearLayout.setOrientation(LinearLayout.VERTICAL); + } else { + mLabelsLinearLayout.setOrientation(LinearLayout.HORIZONTAL); + } + } + mRecordingTimeRect.setOrientation(0, animation); + } + + public SurfaceHolder getSurfaceHolder() { + return mSurfaceView.getHolder(); + } + + public void hideSurfaceView() { + mSurfaceView.setVisibility(View.GONE); + mTextureView.setVisibility(View.VISIBLE); + setTransformMatrix(mPreviewWidth, mPreviewHeight); + } + + public void showSurfaceView() { + mSurfaceView.setVisibility(View.VISIBLE); + mTextureView.setVisibility(View.GONE); + setTransformMatrix(mPreviewWidth, mPreviewHeight); + } + + private void initializeOverlay() { + mRenderOverlay = (RenderOverlay) mRootView.findViewById(R.id.render_overlay); + if (mPieRenderer == null) { + mPieRenderer = new PieRenderer(mActivity); + mVideoMenu = new VideoMenu(mActivity, this, mPieRenderer); + mPieRenderer.setPieListener(this); + } + mRenderOverlay.addRenderer(mPieRenderer); + if (mZoomRenderer == null) { + mZoomRenderer = new ZoomRenderer(mActivity); + } + mRenderOverlay.addRenderer(mZoomRenderer); + if (mGestures == null) { + mGestures = new PreviewGestures(mActivity, this, mZoomRenderer, mPieRenderer); + mRenderOverlay.setGestures(mGestures); + } + mGestures.setRenderOverlay(mRenderOverlay); + + mPreviewThumb = mActivity.findViewById(R.id.preview_thumb); + mPreviewThumb.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + // TODO: Go to filmstrip view + } + }); + } + + public void setPrefChangedListener(OnPreferenceChangedListener listener) { + mVideoMenu.setListener(listener); + } + + private void initializeMiscControls() { + mReviewImage = (ImageView) mRootView.findViewById(R.id.review_image); + mShutterButton.setImageResource(R.drawable.btn_new_shutter_video); + mShutterButton.setOnShutterButtonListener(mController); + mShutterButton.setVisibility(View.VISIBLE); + mShutterButton.requestFocus(); + mShutterButton.enableTouch(true); + mRecordingTimeView = (TextView) mRootView.findViewById(R.id.recording_time); + mRecordingTimeRect = (RotateLayout) mRootView.findViewById(R.id.recording_time_rect); + mTimeLapseLabel = mRootView.findViewById(R.id.time_lapse_label); + // The R.id.labels can only be found in phone layout. + // That is, mLabelsLinearLayout should be null in tablet layout. + mLabelsLinearLayout = (LinearLayout) mRootView.findViewById(R.id.labels); + } + + public void updateOnScreenIndicators(Parameters param, ComboPreferences prefs) { + mOnScreenIndicators.updateFlashOnScreenIndicator(param.getFlashMode()); + boolean location = RecordLocationPreference.get( + prefs, mActivity.getContentResolver()); + mOnScreenIndicators.updateLocationIndicator(location); + + } + + public void setAspectRatio(double ratio) { + // mPreviewFrameLayout.setAspectRatio(ratio); + } + + public void showTimeLapseUI(boolean enable) { + if (mTimeLapseLabel != null) { + mTimeLapseLabel.setVisibility(enable ? View.VISIBLE : View.GONE); + } + } + + private void openMenu() { + if (mPieRenderer != null) { + mPieRenderer.showInCenter(); + } + } + + public void showPopup(AbstractSettingPopup popup) { + hideUI(); + mBlocker.setVisibility(View.INVISIBLE); + setShowMenu(false); + mPopup = popup; + mPopup.setVisibility(View.VISIBLE); + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + lp.gravity = Gravity.CENTER; + ((FrameLayout) mRootView).addView(mPopup, lp); + } + + public void dismissPopup(boolean topLevelOnly) { + dismissPopup(topLevelOnly, true); + } + + public void dismissPopup(boolean topLevelPopupOnly, boolean fullScreen) { + // In review mode, we do not want to bring up the camera UI + if (mController.isInReviewMode()) return; + + if (fullScreen) { + showUI(); + mBlocker.setVisibility(View.VISIBLE); + } + setShowMenu(fullScreen); + if (mPopup != null) { + ((FrameLayout) mRootView).removeView(mPopup); + mPopup = null; + } + mVideoMenu.popupDismissed(topLevelPopupOnly); + } + + public void onShowSwitcherPopup() { + hidePieRenderer(); + } + + public boolean hidePieRenderer() { + if (mPieRenderer != null && mPieRenderer.showsItems()) { + mPieRenderer.hide(); + return true; + } + return false; + } + + // disable preview gestures after shutter is pressed + public void setShutterPressed(boolean pressed) { + if (mGestures == null) return; + mGestures.setEnabled(!pressed); + } + + public void enableShutter(boolean enable) { + if (mShutterButton != null) { + mShutterButton.setEnabled(enable); + } + } + + // PieListener + @Override + public void onPieOpened(int centerX, int centerY) { + setSwipingEnabled(false); + dismissPopup(false, true); + } + + @Override + public void onPieClosed() { + setSwipingEnabled(true); + } + + public void setSwipingEnabled(boolean enable) { + mActivity.setSwipingEnabled(enable); + } + + public void showPreviewBorder(boolean enable) { + // TODO: mPreviewFrameLayout.showBorder(enable); + } + + // SingleTapListener + // Preview area is touched. Take a picture. + @Override + public void onSingleTapUp(View view, int x, int y) { + mController.onSingleTapUp(view, x, y); + } + + public void showRecordingUI(boolean recording, boolean zoomSupported) { + mMenuButton.setVisibility(recording ? View.GONE : View.VISIBLE); + mOnScreenIndicators.setVisibility(recording ? View.GONE : View.VISIBLE); + if (recording) { + mShutterButton.setImageResource(R.drawable.btn_shutter_video_recording); + hideSwitcher(); + mRecordingTimeView.setText(""); + mRecordingTimeView.setVisibility(View.VISIBLE); + // The camera is not allowed to be accessed in older api levels during + // recording. It is therefore necessary to hide the zoom UI on older + // platforms. + // See the documentation of android.media.MediaRecorder.start() for + // further explanation. + if (!ApiHelper.HAS_ZOOM_WHEN_RECORDING && zoomSupported) { + // TODO: disable zoom UI here. + } + } else { + mShutterButton.setImageResource(R.drawable.btn_new_shutter_video); + showSwitcher(); + mRecordingTimeView.setVisibility(View.GONE); + if (!ApiHelper.HAS_ZOOM_WHEN_RECORDING && zoomSupported) { + // TODO: enable zoom UI here. + } + } + } + + public void showReviewImage(Bitmap bitmap) { + mReviewImage.setImageBitmap(bitmap); + mReviewImage.setVisibility(View.VISIBLE); + } + + public void showReviewControls() { + Util.fadeOut(mShutterButton); + Util.fadeIn(mReviewDoneButton); + Util.fadeIn(mReviewPlayButton); + mReviewImage.setVisibility(View.VISIBLE); + mMenuButton.setVisibility(View.GONE); + mOnScreenIndicators.setVisibility(View.GONE); + } + + public void hideReviewUI() { + mReviewImage.setVisibility(View.GONE); + mShutterButton.setEnabled(true); + mMenuButton.setVisibility(View.VISIBLE); + mOnScreenIndicators.setVisibility(View.VISIBLE); + Util.fadeOut(mReviewDoneButton); + Util.fadeOut(mReviewPlayButton); + Util.fadeIn(mShutterButton); + } + + private void setShowMenu(boolean show) { + if (mOnScreenIndicators != null) { + mOnScreenIndicators.setVisibility(show ? View.VISIBLE : View.GONE); + } + if (mMenuButton != null) { + mMenuButton.setVisibility(show ? View.VISIBLE : View.GONE); + } + } + + public void onSwitchMode(boolean toCamera) { + if (toCamera) { + showUI(); + } else { + hideUI(); + } + if (mGestures != null) { + mGestures.setEnabled(toCamera); + } + if (mPopup != null) { + dismissPopup(false, toCamera); + } + if (mRenderOverlay != null) { + // this can not happen in capture mode + mRenderOverlay.setVisibility(toCamera ? View.VISIBLE : View.GONE); + } + setShowMenu(toCamera); + } + + public void initializePopup(PreferenceGroup pref) { + mVideoMenu.initialize(pref); + } + + public void initializeZoom(Parameters param) { + if (param == null || !param.isZoomSupported()) { + mGestures.setZoomEnabled(false); + return; + } + mGestures.setZoomEnabled(true); + mZoomMax = param.getMaxZoom(); + mZoomRatios = param.getZoomRatios(); + // Currently we use immediate zoom for fast zooming to get better UX and + // there is no plan to take advantage of the smooth zoom. + mZoomRenderer.setZoomMax(mZoomMax); + mZoomRenderer.setZoom(param.getZoom()); + mZoomRenderer.setZoomValue(mZoomRatios.get(param.getZoom())); + mZoomRenderer.setOnZoomChangeListener(new ZoomChangeListener()); + } + + public void clickShutter() { + mShutterButton.performClick(); + } + + public void pressShutter(boolean pressed) { + mShutterButton.setPressed(pressed); + } + + public View getShutterButton() { + return mShutterButton; + } + + public void setRecordingTime(String text) { + mRecordingTimeView.setText(text); + } + + public void setRecordingTimeTextColor(int color) { + mRecordingTimeView.setTextColor(color); + } + + public boolean isVisible() { + return mTextureView.getVisibility() == View.VISIBLE; + } + + public void onDisplayChanged() { + mCameraControls.checkLayoutFlip(); + mController.updateCameraOrientation(); + } + + /** + * Enable or disable the preview thumbnail for click events. + */ + public void enablePreviewThumb(boolean enabled) { + if (enabled) { + mPreviewThumb.setVisibility(View.VISIBLE); + } else { + mPreviewThumb.setVisibility(View.GONE); + } + } + + private class ZoomChangeListener implements ZoomRenderer.OnZoomChangedListener { + @Override + public void onZoomValueChanged(int index) { + int newZoom = mController.onZoomChanged(index); + if (mZoomRenderer != null) { + mZoomRenderer.setZoomValue(mZoomRatios.get(newZoom)); + } + } + + @Override + public void onZoomStart() { + } + + @Override + public void onZoomEnd() { + } + } + + public SurfaceTexture getSurfaceTexture() { + return mSurfaceTexture; + } + + // SurfaceTexture callbacks + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + mSurfaceTexture = surface; + mController.onPreviewUIReady(); + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + mSurfaceTexture = null; + mController.onPreviewUIDestroyed(); + Log.d(TAG, "surfaceTexture is destroyed"); + return true; + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + } + + // SurfaceHolder callbacks + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + Log.v(TAG, "Surface changed. width=" + width + ". height=" + height); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + Log.v(TAG, "Surface created"); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + Log.v(TAG, "Surface destroyed"); + mController.stopPreview(); + } +} diff --git a/src/com/android/camera/data/AbstractLocalDataAdapterWrapper.java b/src/com/android/camera/data/AbstractLocalDataAdapterWrapper.java new file mode 100644 index 000000000..66c55850a --- /dev/null +++ b/src/com/android/camera/data/AbstractLocalDataAdapterWrapper.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2013 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.data; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; + +/** + * An abstract {@link LocalDataAdapter} implementation to wrap another + * {@link LocalDataAdapter}. All implementations related to data id is not + * addressed in this abstract class since wrapping another data adapter + * surely makes things different for data id. + * + * @see FixedFirstDataAdapter + * @see FixedLastDataAdapter + */ +public abstract class AbstractLocalDataAdapterWrapper implements LocalDataAdapter { + + protected final LocalDataAdapter mAdapter; + protected int mSuggestedWidth; + protected int mSuggestedHeight; + + /** + * Constructor. + * + * @param wrappedAdapter The {@link LocalDataAdapter} to be wrapped. + */ + AbstractLocalDataAdapterWrapper(LocalDataAdapter wrappedAdapter) { + if (wrappedAdapter == null) { + throw new AssertionError("data adapter is null"); + } + mAdapter = wrappedAdapter; + } + + @Override + public void suggestViewSizeBound(int w, int h) { + mSuggestedWidth = w; + mSuggestedHeight = h; + } + + @Override + public void setListener(Listener listener) { + mAdapter.setListener(listener); + } + + @Override + public void requestLoad(ContentResolver resolver) { + mAdapter.requestLoad(resolver); + } + + @Override + public void addNewVideo(ContentResolver resolver, Uri uri) { + mAdapter.addNewVideo(resolver, uri); + } + + @Override + public void addNewPhoto(ContentResolver resolver, Uri uri) { + mAdapter.addNewPhoto(resolver, uri); + } + + @Override + public void flush() { + mAdapter.flush(); + } + + @Override + public boolean executeDeletion(Context context) { + return mAdapter.executeDeletion(context); + } + + @Override + public boolean undoDataRemoval() { + return mAdapter.undoDataRemoval(); + } +} diff --git a/src/com/android/camera/data/CameraDataAdapter.java b/src/com/android/camera/data/CameraDataAdapter.java new file mode 100644 index 000000000..3605f7190 --- /dev/null +++ b/src/com/android/camera/data/CameraDataAdapter.java @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2013 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.data; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.provider.MediaStore; +import android.util.Log; +import android.view.View; + +import com.android.camera.Storage; +import com.android.camera.ui.FilmStripView.ImageData; +import com.android.gallery3d.util.LightCycleHelper.PanoramaViewHelper; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * A {@link LocalDataAdapter} that provides data in the camera folder. + */ +public class CameraDataAdapter implements LocalDataAdapter { + private static final String TAG = CameraDataAdapter.class.getSimpleName(); + + private static final int DEFAULT_DECODE_SIZE = 3000; + private static final String[] CAMERA_PATH = { Storage.DIRECTORY + "%" }; + + private List<LocalData> mImages; + + private Listener mListener; + private Drawable mPlaceHolder; + + private int mSuggestedWidth = DEFAULT_DECODE_SIZE; + private int mSuggestedHeight = DEFAULT_DECODE_SIZE; + + private LocalData mLocalDataToDelete; + + public CameraDataAdapter(Drawable placeHolder) { + mPlaceHolder = placeHolder; + } + + @Override + public void requestLoad(ContentResolver resolver) { + QueryTask qtask = new QueryTask(); + qtask.execute(resolver); + } + + @Override + public int getTotalNumber() { + if (mImages == null) { + return 0; + } + return mImages.size(); + } + + @Override + public ImageData getImageData(int id) { + return getData(id); + } + + @Override + public void suggestViewSizeBound(int w, int h) { + if (w <= 0 || h <= 0) { + mSuggestedWidth = mSuggestedHeight = DEFAULT_DECODE_SIZE; + } else { + mSuggestedWidth = (w < DEFAULT_DECODE_SIZE ? w : DEFAULT_DECODE_SIZE); + mSuggestedHeight = (h < DEFAULT_DECODE_SIZE ? h : DEFAULT_DECODE_SIZE); + } + } + + @Override + public View getView(Context c, int dataID) { + if (mImages == null) { + return null; + } + if (dataID >= mImages.size() || dataID < 0) { + return null; + } + + return mImages.get(dataID).getView( + c, mSuggestedWidth, mSuggestedHeight, + mPlaceHolder.getConstantState().newDrawable()); + } + + @Override + public void setListener(Listener listener) { + mListener = listener; + if (mImages != null) { + mListener.onDataLoaded(); + } + } + + @Override + public void onDataFullScreen(int dataID, boolean fullScreen) { + if (dataID < mImages.size() && dataID >= 0) { + mImages.get(dataID).onFullScreen(fullScreen); + } + } + + @Override + public void onDataCentered(int dataID, boolean centered) { + // do nothing. + } + + @Override + public boolean canSwipeInFullScreen(int dataID) { + if (dataID < mImages.size() && dataID > 0) { + return mImages.get(dataID).canSwipeInFullScreen(); + } + return true; + } + + @Override + public void removeData(Context c, int dataID) { + if (dataID >= mImages.size()) return; + LocalData d = mImages.remove(dataID); + // Delete previously removed data first. + executeDeletion(c); + mLocalDataToDelete = d; + mListener.onDataRemoved(dataID, d); + } + + private void insertData(LocalData data) { + if (mImages == null) { + mImages = new ArrayList<LocalData>(); + } + + // Since this function is mostly for adding the newest data, + // a simple linear search should yield the best performance over a + // binary search. + int pos = 0; + Comparator<LocalData> comp = new LocalData.NewestFirstComparator(); + for (; pos < mImages.size() + && comp.compare(data, mImages.get(pos)) > 0; pos++); + mImages.add(pos, data); + if (mListener != null) { + mListener.onDataInserted(pos, data); + } + } + + @Override + public void addNewVideo(ContentResolver cr, Uri uri) { + Cursor c = cr.query(uri, + LocalData.Video.QUERY_PROJECTION, + MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH, + LocalData.Video.QUERY_ORDER); + if (c != null && c.moveToFirst()) { + insertData(LocalData.Video.buildFromCursor(c)); + } + } + + @Override + public void addNewPhoto(ContentResolver cr, Uri uri) { + Cursor c = cr.query(uri, + LocalData.Photo.QUERY_PROJECTION, + MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH, + LocalData.Photo.QUERY_ORDER); + if (c != null && c.moveToFirst()) { + insertData(LocalData.Photo.buildFromCursor(c)); + } + } + + @Override + public int findDataByContentUri(Uri uri) { + // TODO: find the data. + return -1; + } + + @Override + public boolean undoDataRemoval() { + if (mLocalDataToDelete == null) return false; + LocalData d = mLocalDataToDelete; + mLocalDataToDelete = null; + insertData(d); + return true; + } + + @Override + public boolean executeDeletion(Context c) { + if (mLocalDataToDelete == null) return false; + + DeletionTask task = new DeletionTask(c); + task.execute(mLocalDataToDelete); + mLocalDataToDelete = null; + return true; + } + + @Override + public void flush() { + replaceData(null); + } + + private LocalData getData(int id) { + if (mImages == null || id >= mImages.size() || id < 0) { + return null; + } + return mImages.get(id); + } + + // Update all the data but keep the camera data if already set. + private void replaceData(List<LocalData> list) { + boolean changed = (list != mImages); + LocalData cameraData = null; + if (mImages != null && mImages.size() > 0) { + cameraData = mImages.get(0); + if (cameraData.getType() != ImageData.TYPE_CAMERA_PREVIEW) { + cameraData = null; + } + } + + mImages = list; + if (cameraData != null) { + // camera view exists, so we make sure at least 1 data is in the list. + if (mImages == null) { + mImages = new ArrayList<LocalData>(); + } + mImages.add(0, cameraData); + if (mListener != null) { + // Only the camera data is not changed, everything else is changed. + mListener.onDataUpdated(new UpdateReporter() { + @Override + public boolean isDataRemoved(int id) { + return false; + } + + @Override + public boolean isDataUpdated(int id) { + if (id == 0) return false; + return true; + } + }); + } + } else { + // both might be null. + if (changed) { + mListener.onDataLoaded(); + } + } + } + + private class QueryTask extends AsyncTask<ContentResolver, Void, List<LocalData>> { + @Override + protected List<LocalData> doInBackground(ContentResolver... resolver) { + List<LocalData> l = new ArrayList<LocalData>(); + // Photos + Cursor c = resolver[0].query( + LocalData.Photo.CONTENT_URI, + LocalData.Photo.QUERY_PROJECTION, + MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH, + LocalData.Photo.QUERY_ORDER); + if (c != null && c.moveToFirst()) { + // build up the list. + while (true) { + LocalData data = LocalData.Photo.buildFromCursor(c); + if (data != null) { + l.add(data); + } else { + Log.e(TAG, "Error loading data:" + + c.getString(LocalData.Photo.COL_DATA)); + } + if (c.isLast()) { + break; + } + c.moveToNext(); + } + } + if (c != null) { + c.close(); + } + + c = resolver[0].query( + LocalData.Video.CONTENT_URI, + LocalData.Video.QUERY_PROJECTION, + MediaStore.Video.Media.DATA + " like ? ", CAMERA_PATH, + LocalData.Video.QUERY_ORDER); + if (c != null && c.moveToFirst()) { + // build up the list. + c.moveToFirst(); + while (true) { + LocalData data = LocalData.Video.buildFromCursor(c); + if (data != null) { + l.add(data); + } else { + Log.e(TAG, "Error loading data:" + + c.getString(LocalData.Video.COL_DATA)); + } + if (!c.isLast()) { + c.moveToNext(); + } else { + break; + } + } + } + if (c != null) { + c.close(); + } + + if (l.size() == 0) return null; + + Collections.sort(l, new LocalData.NewestFirstComparator()); + return l; + } + + @Override + protected void onPostExecute(List<LocalData> l) { + replaceData(l); + } + } + + private class DeletionTask extends AsyncTask<LocalData, Void, Void> { + Context mContext; + + DeletionTask(Context context) { + mContext = context; + } + + @Override + protected Void doInBackground(LocalData... data) { + for (int i = 0; i < data.length; i++) { + if (!data[i].isDataActionSupported(LocalData.ACTION_DELETE)) { + Log.v(TAG, "Deletion is not supported:" + data[i]); + continue; + } + data[i].delete(mContext); + } + return null; + } + } +} diff --git a/src/com/android/camera/data/CameraPreviewData.java b/src/com/android/camera/data/CameraPreviewData.java new file mode 100644 index 000000000..8f8e2138d --- /dev/null +++ b/src/com/android/camera/data/CameraPreviewData.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2013 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.data; + +import android.view.View; + +import com.android.camera.ui.FilmStripView.ImageData; + +/** + * A class implementing {@link LocalData} to represent a camera preview. + */ +public class CameraPreviewData extends LocalData.LocalViewData { + + private boolean mPreviewLocked; + + /** + * Constructor. + * + * @param v The {@link android.view.View} for camera preview. + * @param width The width of the camera preview. + * @param height The height of the camera preview. + */ + public CameraPreviewData(View v, int width, int height) { + super(v, width, height, -1, -1); + mPreviewLocked = true; + } + + @Override + public int getType() { + return ImageData.TYPE_CAMERA_PREVIEW; + } + + @Override + public boolean canSwipeInFullScreen() { + return !mPreviewLocked; + } + + /** + * Locks the camera preview. When the camera preview is locked, swipe + * to film strip is not allowed. One case is when the video recording + * is in progress. + * + * @param lock {@code true} if the preview should be locked. {@code false} + * otherwise. + */ + public void lockPreview(boolean lock) { + mPreviewLocked = lock; + } +} diff --git a/src/com/android/camera/data/FixedFirstDataAdapter.java b/src/com/android/camera/data/FixedFirstDataAdapter.java new file mode 100644 index 000000000..34ba0a1a0 --- /dev/null +++ b/src/com/android/camera/data/FixedFirstDataAdapter.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2013 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.data; + +import android.content.Context; +import android.net.Uri; +import android.view.View; + +import com.android.camera.ui.FilmStripView; +import com.android.camera.ui.FilmStripView.DataAdapter; +import com.android.camera.ui.FilmStripView.ImageData; + +/** + * A {@link LocalDataAdapter} which puts a {@link LocalData} fixed at the first + * position. It's done by combining a {@link LocalData} and another + * {@link LocalDataAdapter}. + */ +public class FixedFirstDataAdapter extends AbstractLocalDataAdapterWrapper + implements DataAdapter.Listener { + + private final LocalData mFirstData; + private Listener mListener; + + /** + * Constructor. + * + * @param wrappedAdapter The {@link LocalDataAdapter} to be wrapped. + * @param firstData The {@link LocalData} to be placed at the first + * position. + */ + public FixedFirstDataAdapter( + LocalDataAdapter wrappedAdapter, + LocalData firstData) { + super(wrappedAdapter); + if (firstData == null) { + throw new AssertionError("data is null"); + } + mFirstData = firstData; + } + + @Override + public void removeData(Context context, int dataID) { + if (dataID > 0) { + mAdapter.removeData(context, dataID - 1); + } + } + + @Override + public int findDataByContentUri(Uri uri) { + int pos = mAdapter.findDataByContentUri(uri); + if (pos != -1) { + return pos + 1; + } + return -1; + } + + @Override + public int getTotalNumber() { + return (mAdapter.getTotalNumber() + 1); + } + + @Override + public View getView(Context context, int dataID) { + if (dataID == 0) { + return mFirstData.getView( + context, mSuggestedWidth, mSuggestedHeight, null); + } + return mAdapter.getView(context, dataID - 1); + } + + @Override + public ImageData getImageData(int dataID) { + if (dataID == 0) { + return mFirstData; + } + return mAdapter.getImageData(dataID - 1); + } + + @Override + public void onDataFullScreen(int dataID, boolean fullScreen) { + if (dataID == 0) { + mFirstData.onFullScreen(fullScreen); + } else { + mAdapter.onDataFullScreen(dataID - 1, fullScreen); + } + } + + @Override + public void onDataCentered(int dataID, boolean centered) { + if (dataID != 0) { + mAdapter.onDataCentered(dataID, centered); + } else { + // TODO: notify the data + } + } + + @Override + public void setListener(Listener listener) { + mListener = listener; + mAdapter.setListener((listener == null) ? null : this); + } + + @Override + public boolean canSwipeInFullScreen(int dataID) { + if (dataID == 0) { + return mFirstData.canSwipeInFullScreen(); + } + return mAdapter.canSwipeInFullScreen(dataID - 1); + } + + @Override + public void onDataLoaded() { + mListener.onDataLoaded(); + } + + @Override + public void onDataUpdated(final UpdateReporter reporter) { + mListener.onDataUpdated(new UpdateReporter() { + @Override + public boolean isDataRemoved(int dataID) { + return reporter.isDataRemoved(dataID + 1); + } + + @Override + public boolean isDataUpdated(int dataID) { + return reporter.isDataUpdated(dataID + 1); + } + }); + } + + @Override + public void onDataInserted(int dataID, ImageData data) { + mListener.onDataInserted(dataID + 1, data); + } + + @Override + public void onDataRemoved(int dataID, ImageData data) { + mListener.onDataRemoved(dataID + 1, data); + } +} diff --git a/src/com/android/camera/data/FixedLastDataAdapter.java b/src/com/android/camera/data/FixedLastDataAdapter.java new file mode 100644 index 000000000..16c047d1a --- /dev/null +++ b/src/com/android/camera/data/FixedLastDataAdapter.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2013 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.data; + +import android.content.Context; +import android.net.Uri; +import android.view.View; + +import com.android.camera.ui.FilmStripView; + +/** + * A {@link LocalDataAdapter} which puts a {@link LocalData} fixed at the last + * position. It's done by combining a {@link LocalData} and another + * {@link LocalDataAdapter}. + */ +public class FixedLastDataAdapter extends AbstractLocalDataAdapterWrapper { + + private final LocalData mLastData; + + /** + * Constructor. + * + * @param wrappedAdapter The {@link LocalDataAdapter} to be wrapped. + * @param lastData The {@link LocalData} to be placed at the last position. + */ + public FixedLastDataAdapter( + LocalDataAdapter wrappedAdapter, + LocalData lastData) { + super(wrappedAdapter); + if (lastData == null) { + throw new AssertionError("data is null"); + } + mLastData = lastData; + } + + @Override + public void removeData(Context context, int dataID) { + if (dataID < mAdapter.getTotalNumber()) { + mAdapter.removeData(context, dataID); + } + } + + @Override + public int findDataByContentUri(Uri uri) { + return mAdapter.findDataByContentUri(uri); + } + + @Override + public int getTotalNumber() { + return mAdapter.getTotalNumber() + 1; + } + + @Override + public View getView(Context context, int dataID) { + int totalNumber = mAdapter.getTotalNumber(); + + if (dataID < totalNumber) { + return mAdapter.getView(context, dataID); + } else if (dataID == totalNumber) { + return mLastData.getView(context, + mSuggestedWidth, mSuggestedHeight, null); + } + + return null; + } + + @Override + public FilmStripView.ImageData getImageData(int dataID) { + int totalNumber = mAdapter.getTotalNumber(); + + if (dataID < totalNumber) { + return mAdapter.getImageData(dataID); + } else if (dataID == totalNumber) { + return mLastData; + } + return null; + } + + @Override + public void onDataFullScreen(int dataID, boolean fullScreen) { + int totalNumber = mAdapter.getTotalNumber(); + + if (dataID < totalNumber) { + mAdapter.onDataFullScreen(dataID, fullScreen); + } else if (dataID == totalNumber) { + mLastData.onFullScreen(fullScreen); + } + } + + @Override + public void onDataCentered(int dataID, boolean centered) { + int totalNumber = mAdapter.getTotalNumber(); + + if (dataID < totalNumber) { + mAdapter.onDataCentered(dataID, centered); + } else if (dataID == totalNumber) { + // TODO: notify the data + } + } + + @Override + public boolean canSwipeInFullScreen(int dataID) { + int totalNumber = mAdapter.getTotalNumber(); + + if (dataID < totalNumber) { + return mAdapter.canSwipeInFullScreen(dataID); + } else if (dataID == totalNumber) { + return mLastData.canSwipeInFullScreen(); + } + return false; + } +} + diff --git a/src/com/android/camera/data/LocalData.java b/src/com/android/camera/data/LocalData.java new file mode 100644 index 000000000..efccfe332 --- /dev/null +++ b/src/com/android/camera/data/LocalData.java @@ -0,0 +1,726 @@ +/* + * Copyright (C) 2013 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.data; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.media.MediaMetadataRetriever; +import android.net.Uri; +import android.os.AsyncTask; +import android.provider.MediaStore; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; +import android.provider.MediaStore.Video.VideoColumns; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import com.android.camera.Util; +import com.android.camera.data.PanoramaMetadataLoader.PanoramaMetadataCallback; +import com.android.camera.ui.FilmStripView; +import com.android.gallery3d.R; +import com.android.gallery3d.util.LightCycleHelper.PanoramaMetadata; +import com.android.gallery3d.util.LightCycleHelper.PanoramaViewHelper; + +import java.io.File; +import java.util.Comparator; +import java.util.Date; + +/** + * An abstract interface that represents the local media data. Also implements + * Comparable interface so we can sort in DataAdapter. + */ +public interface LocalData extends FilmStripView.ImageData { + static final String TAG = "CAM_LocalData"; + + public static final int ACTION_NONE = 0; + public static final int ACTION_PLAY = 1; + public static final int ACTION_DELETE = (1 << 1); + + View getView(Context c, int width, int height, Drawable placeHolder); + long getDateTaken(); + long getDateModified(); + String getTitle(); + boolean isDataActionSupported(int action); + boolean delete(Context c); + void onFullScreen(boolean fullScreen); + boolean canSwipeInFullScreen(); + String getPath(); + + static class NewestFirstComparator implements Comparator<LocalData> { + + /** Compare taken/modified date of LocalData in descent order to make + newer data in the front. + The negative numbers here are always considered "bigger" than + positive ones. Thus, if any one of the numbers is negative, the logic + is reversed. */ + private static int compareDate(long v1, long v2) { + if (v1 >= 0 && v2 >= 0) { + return ((v1 < v2) ? 1 : ((v1 > v2) ? -1 : 0)); + } + return ((v2 < v1) ? 1 : ((v2 > v1) ? -1 : 0)); + } + + @Override + public int compare(LocalData d1, LocalData d2) { + int cmp = compareDate(d1.getDateTaken(), d2.getDateTaken()); + if (cmp == 0) { + cmp = compareDate(d1.getDateModified(), d2.getDateModified()); + } + if (cmp == 0) { + cmp = d1.getTitle().compareTo(d2.getTitle()); + } + return cmp; + } + } + + // Implementations below. + + /** +<<<<<<< HEAD + * A base class for all the local media files. The bitmap is loaded in + * background thread. Subclasses should implement their own background + * loading thread by subclassing BitmapLoadTask and overriding + * doInBackground() to return a bitmap. +======= + * A base class for all the local media files. The bitmap is loaded in background + * thread. Subclasses should implement their own background loading thread by + * sub-classing BitmapLoadTask and overriding doInBackground() to return a bitmap. +>>>>>>> Add LocalDataAdapter and wrappers. + */ + abstract static class LocalMediaData implements LocalData { + protected long id; + protected String title; + protected String mimeType; + protected long dateTaken; + protected long dateModified; + protected String path; + // width and height should be adjusted according to orientation. + protected int width; + protected int height; + + /** The panorama metadata information of this media data. */ + private PanoramaMetadata mPanoramaMetadata; + + /** Used to load photo sphere metadata from image files. */ + private PanoramaMetadataLoader mPanoramaMetadataLoader = null; + + // true if this data has a corresponding visible view. + protected Boolean mUsing = false; + + @Override + public long getDateTaken() { + return dateTaken; + } + + @Override + public long getDateModified() { + return dateModified; + } + + @Override + public String getTitle() { + return new String(title); + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public String getPath() { + return path; + } + + @Override + public boolean isUIActionSupported(int action) { + return false; + } + + @Override + public boolean isDataActionSupported(int action) { + return false; + } + + @Override + public boolean delete(Context ctx) { + File f = new File(path); + return f.delete(); + } + + @Override + public void viewPhotoSphere(PanoramaViewHelper helper) { + helper.showPanorama(getContentUri()); + } + + @Override + public void isPhotoSphere(Context context, final PanoramaSupportCallback callback) { + // If we already have metadata, use it. + if (mPanoramaMetadata != null) { + callback.panoramaInfoAvailable(mPanoramaMetadata.mUsePanoramaViewer, + mPanoramaMetadata.mIsPanorama360); + } + + // Otherwise prepare a loader, if we don't have one already. + if (mPanoramaMetadataLoader == null) { + mPanoramaMetadataLoader = new PanoramaMetadataLoader(getContentUri()); + } + + // Load the metadata asynchronously. + mPanoramaMetadataLoader.getPanoramaMetadata(context, new PanoramaMetadataCallback() { + @Override + public void onPanoramaMetadataLoaded(PanoramaMetadata metadata) { + // Store the metadata and remove the loader to free up space. + mPanoramaMetadata = metadata; + mPanoramaMetadataLoader = null; + callback.panoramaInfoAvailable(metadata.mUsePanoramaViewer, + metadata.mIsPanorama360); + } + }); + } + + @Override + public void onFullScreen(boolean fullScreen) { + // do nothing. + } + + @Override + public boolean canSwipeInFullScreen() { + return true; + } + + protected ImageView fillImageView(Context ctx, ImageView v, + int decodeWidth, int decodeHeight, Drawable placeHolder) { + v.setScaleType(ImageView.ScaleType.FIT_XY); + v.setImageDrawable(placeHolder); + + BitmapLoadTask task = getBitmapLoadTask(v, decodeWidth, decodeHeight); + task.execute(); + return v; + } + + @Override + public View getView(Context ctx, + int decodeWidth, int decodeHeight, Drawable placeHolder) { + return fillImageView(ctx, new ImageView(ctx), + decodeWidth, decodeHeight, placeHolder); + } + + @Override + public void prepare() { + synchronized (mUsing) { + mUsing = true; + } + } + + @Override + public void recycle() { + synchronized (mUsing) { + mUsing = false; + } + } + + protected boolean isUsing() { + synchronized (mUsing) { + return mUsing; + } + } + + /** + * Returns the content URI of this data item. + */ + private Uri getContentUri() { + Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI; + return baseUri.buildUpon().appendPath(String.valueOf(id)).build(); + } + + @Override + public abstract int getType(); + + protected abstract BitmapLoadTask getBitmapLoadTask( + ImageView v, int decodeWidth, int decodeHeight); + + /** + * An AsyncTask class that loads the bitmap in the background thread. + * Sub-classes should implement their own "protected Bitmap doInBackground(Void... )" + */ + protected abstract class BitmapLoadTask extends AsyncTask<Void, Void, Bitmap> { + protected ImageView mView; + + protected BitmapLoadTask(ImageView v) { + mView = v; + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (!isUsing()) return; + if (bitmap == null) { + Log.e(TAG, "Failed decoding bitmap for file:" + path); + return; + } + BitmapDrawable d = new BitmapDrawable(bitmap); + mView.setScaleType(ImageView.ScaleType.FIT_XY); + mView.setImageDrawable(d); + } + } + } + + static class Photo extends LocalMediaData { + public static final int COL_ID = 0; + public static final int COL_TITLE = 1; + public static final int COL_MIME_TYPE = 2; + public static final int COL_DATE_TAKEN = 3; + public static final int COL_DATE_MODIFIED = 4; + public static final int COL_DATA = 5; + public static final int COL_ORIENTATION = 6; + public static final int COL_WIDTH = 7; + public static final int COL_HEIGHT = 8; + + static final Uri CONTENT_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + + static final String QUERY_ORDER = ImageColumns.DATE_TAKEN + " DESC, " + + ImageColumns._ID + " DESC"; + /** + * These values should be kept in sync with column IDs (COL_*) above. + */ + static final String[] QUERY_PROJECTION = { + ImageColumns._ID, // 0, int + ImageColumns.TITLE, // 1, string + ImageColumns.MIME_TYPE, // 2, string + ImageColumns.DATE_TAKEN, // 3, int + ImageColumns.DATE_MODIFIED, // 4, int + ImageColumns.DATA, // 5, string + ImageColumns.ORIENTATION, // 6, int, 0, 90, 180, 270 + ImageColumns.WIDTH, // 7, int + ImageColumns.HEIGHT, // 8, int + }; + + private static final int mSupportedUIActions = + FilmStripView.ImageData.ACTION_DEMOTE + | FilmStripView.ImageData.ACTION_PROMOTE; + private static final int mSupportedDataActions = + LocalData.ACTION_DELETE; + + /** 32K buffer. */ + private static final byte[] DECODE_TEMP_STORAGE = new byte[32 * 1024]; + + /** from MediaStore, can only be 0, 90, 180, 270 */ + public int orientation; + + static Photo buildFromCursor(Cursor c) { + Photo d = new Photo(); + d.id = c.getLong(COL_ID); + d.title = c.getString(COL_TITLE); + d.mimeType = c.getString(COL_MIME_TYPE); + d.dateTaken = c.getLong(COL_DATE_TAKEN); + d.dateModified = c.getLong(COL_DATE_MODIFIED); + d.path = c.getString(COL_DATA); + d.orientation = c.getInt(COL_ORIENTATION); + d.width = c.getInt(COL_WIDTH); + d.height = c.getInt(COL_HEIGHT); + if (d.width <= 0 || d.height <= 0) { + Log.w(TAG, "Warning! zero dimension for " + + d.path + ":" + d.width + "x" + d.height); + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inJustDecodeBounds = true; + BitmapFactory.decodeFile(d.path, opts); + if (opts.outWidth != -1 && opts.outHeight != -1) { + d.width = opts.outWidth; + d.height = opts.outHeight; + } else { + Log.w(TAG, "Warning! dimension decode failed for " + d.path); + Bitmap b = BitmapFactory.decodeFile(d.path); + if (b == null) { + return null; + } + d.width = b.getWidth(); + d.height = b.getHeight(); + } + } + if (d.orientation == 90 || d.orientation == 270) { + int b = d.width; + d.width = d.height; + d.height = b; + } + return d; + } + + @Override + public String toString() { + return "Photo:" + ",data=" + path + ",mimeType=" + mimeType + + "," + width + "x" + height + ",orientation=" + orientation + + ",date=" + new Date(dateTaken); + } + + @Override + public int getType() { + return TYPE_PHOTO; + } + + @Override + public boolean isUIActionSupported(int action) { + return ((action & mSupportedUIActions) == action); + } + + @Override + public boolean isDataActionSupported(int action) { + return ((action & mSupportedDataActions) == action); + } + + @Override + public boolean delete(Context c) { + ContentResolver cr = c.getContentResolver(); + cr.delete(CONTENT_URI, ImageColumns._ID + "=" + id, null); + return super.delete(c); + } + + @Override + protected BitmapLoadTask getBitmapLoadTask( + ImageView v, int decodeWidth, int decodeHeight) { + return new PhotoBitmapLoadTask(v, decodeWidth, decodeHeight); + } + + private final class PhotoBitmapLoadTask extends BitmapLoadTask { + private int mDecodeWidth; + private int mDecodeHeight; + + public PhotoBitmapLoadTask(ImageView v, int decodeWidth, int decodeHeight) { + super(v); + mDecodeWidth = decodeWidth; + mDecodeHeight = decodeHeight; + } + + @Override + protected Bitmap doInBackground(Void... v) { + BitmapFactory.Options opts = null; + Bitmap b; + int sample = 1; + while (mDecodeWidth * sample < width + || mDecodeHeight * sample < height) { + sample *= 2; + } + opts = new BitmapFactory.Options(); + opts.inSampleSize = sample; + opts.inTempStorage = DECODE_TEMP_STORAGE; + if (isCancelled() || !isUsing()) { + return null; + } + b = BitmapFactory.decodeFile(path, opts); + if (orientation != 0) { + if (isCancelled() || !isUsing()) { + return null; + } + Matrix m = new Matrix(); + m.setRotate(orientation); + b = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, false); + } + return b; + } + } + } + + static class Video extends LocalMediaData { + public static final int COL_ID = 0; + public static final int COL_TITLE = 1; + public static final int COL_MIME_TYPE = 2; + public static final int COL_DATE_TAKEN = 3; + public static final int COL_DATE_MODIFIED = 4; + public static final int COL_DATA = 5; + public static final int COL_WIDTH = 6; + public static final int COL_HEIGHT = 7; + public static final int COL_RESOLUTION = 8; + + static final Uri CONTENT_URI = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + + private static final int mSupportedUIActions = + FilmStripView.ImageData.ACTION_DEMOTE + | FilmStripView.ImageData.ACTION_PROMOTE; + private static final int mSupportedDataActions = + LocalData.ACTION_DELETE + | LocalData.ACTION_PLAY; + + static final String QUERY_ORDER = VideoColumns.DATE_TAKEN + " DESC, " + + VideoColumns._ID + " DESC"; + /** + * These values should be kept in sync with column IDs (COL_*) above. + */ + static final String[] QUERY_PROJECTION = { + VideoColumns._ID, // 0, int + VideoColumns.TITLE, // 1, string + VideoColumns.MIME_TYPE, // 2, string + VideoColumns.DATE_TAKEN, // 3, int + VideoColumns.DATE_MODIFIED, // 4, int + VideoColumns.DATA, // 5, string + VideoColumns.WIDTH, // 6, int + VideoColumns.HEIGHT, // 7, int + VideoColumns.RESOLUTION // 8, string + }; + + private Uri mPlayUri; + + static Video buildFromCursor(Cursor c) { + Video d = new Video(); + d.id = c.getLong(COL_ID); + d.title = c.getString(COL_TITLE); + d.mimeType = c.getString(COL_MIME_TYPE); + d.dateTaken = c.getLong(COL_DATE_TAKEN); + d.dateModified = c.getLong(COL_DATE_MODIFIED); + d.path = c.getString(COL_DATA); + d.width = c.getInt(COL_WIDTH); + d.height = c.getInt(COL_HEIGHT); + d.mPlayUri = CONTENT_URI.buildUpon() + .appendPath(String.valueOf(d.id)).build(); + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(d.path); + String rotation = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); + if (d.width == 0 || d.height == 0) { + d.width = Integer.parseInt(retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)); + d.height = Integer.parseInt(retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)); + } + retriever.release(); + if (rotation != null + && (rotation.equals("90") || rotation.equals("270"))) { + int b = d.width; + d.width = d.height; + d.height = b; + } + return d; + } + + @Override + public String toString() { + return "Video:" + ",data=" + path + ",mimeType=" + mimeType + + "," + width + "x" + height + ",date=" + new Date(dateTaken); + } + + @Override + public int getType() { + return TYPE_PHOTO; + } + + @Override + public boolean isUIActionSupported(int action) { + return ((action & mSupportedUIActions) == action); + } + + @Override + public boolean isDataActionSupported(int action) { + return ((action & mSupportedDataActions) == action); + } + + @Override + public boolean delete(Context ctx) { + ContentResolver cr = ctx.getContentResolver(); + cr.delete(CONTENT_URI, VideoColumns._ID + "=" + id, null); + return super.delete(ctx); + } + + @Override + public View getView(final Context ctx, + int decodeWidth, int decodeHeight, Drawable placeHolder) { + + // ImageView for the bitmap. + ImageView iv = new ImageView(ctx); + iv.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER)); + fillImageView(ctx, iv, decodeWidth, decodeHeight, placeHolder); + + // ImageView for the play icon. + ImageView icon = new ImageView(ctx); + icon.setImageResource(R.drawable.ic_control_play); + icon.setScaleType(ImageView.ScaleType.CENTER); + icon.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); + icon.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + Util.playVideo(ctx, mPlayUri, title); + } + }); + + FrameLayout f = new FrameLayout(ctx); + f.addView(iv); + f.addView(icon); + return f; + } + + @Override + protected BitmapLoadTask getBitmapLoadTask( + ImageView v, int decodeWidth, int decodeHeight) { + return new VideoBitmapLoadTask(v); + } + + private final class VideoBitmapLoadTask extends BitmapLoadTask { + + public VideoBitmapLoadTask(ImageView v) { + super(v); + } + + @Override + protected Bitmap doInBackground(Void... v) { + if (isCancelled() || !isUsing()) { + return null; + } + android.media.MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(path); + byte[] data = retriever.getEmbeddedPicture(); + Bitmap bitmap = null; + if (isCancelled() || !isUsing()) { + retriever.release(); + return null; + } + if (data != null) { + bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); + } + if (bitmap == null) { + bitmap = retriever.getFrameAtTime(); + } + retriever.release(); + return bitmap; + } + } + } + + /** + * A LocalData that does nothing but only shows a view. + */ + public static class LocalViewData implements LocalData { + private int mWidth; + private int mHeight; + private View mView; + private long mDateTaken; + private long mDateModified; + + public LocalViewData(View v, + int width, int height, + int dateTaken, int dateModified) { + mView = v; + mWidth = width; + mHeight = height; + mDateTaken = dateTaken; + mDateModified = dateModified; + } + + @Override + public long getDateTaken() { + return mDateTaken; + } + + @Override + public long getDateModified() { + return mDateModified; + } + + @Override + public String getTitle() { + return ""; + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } + + @Override + public int getType() { + return FilmStripView.ImageData.TYPE_PHOTO; + } + + @Override + public String getPath() { + return ""; + } + + @Override + public boolean isUIActionSupported(int action) { + return false; + } + + @Override + public boolean isDataActionSupported(int action) { + return false; + } + + @Override + public boolean delete(Context c) { + return false; + } + + @Override + public View getView(Context c, int width, int height, Drawable placeHolder) { + return mView; + } + + @Override + public void prepare() { + // do nothing. + } + + @Override + public void recycle() { + // do nothing. + } + + @Override + public void isPhotoSphere(Context context, PanoramaSupportCallback callback) { + // Not a photo sphere panorama. + callback.panoramaInfoAvailable(false, false); + } + + @Override + public void viewPhotoSphere(PanoramaViewHelper helper) { + // do nothing. + } + + @Override + public void onFullScreen(boolean fullScreen) { + // do nothing. + } + + @Override + public boolean canSwipeInFullScreen() { + return true; + } + } +} + diff --git a/src/com/android/camera/data/LocalDataAdapter.java b/src/com/android/camera/data/LocalDataAdapter.java new file mode 100644 index 000000000..3b4f07dea --- /dev/null +++ b/src/com/android/camera/data/LocalDataAdapter.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2013 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.data; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; + +import static com.android.camera.ui.FilmStripView.DataAdapter; + +/** + * An interface which extends {@link DataAdapter} and defines operations on + * the data in the local camera folder. + */ +public interface LocalDataAdapter extends DataAdapter { + + /** + * Request for loading the local data. + * + * @param resolver {@link ContentResolver} used for data loading. + */ + public void requestLoad(ContentResolver resolver); + + /** + * Remove the data in the local camera folder. + * + * @param context {@link Context} used to remove the data. + * @param dataID ID of data to be deleted. + */ + public void removeData(Context context, int dataID); + + /** + * Add new local video data. + * + * @param resolver {@link ContentResolver} used to add the data. + * @param uri {@link Uri} of the video. + */ + public void addNewVideo(ContentResolver resolver, Uri uri); + + /** + * Adds new local photo data. + * + * @param resolver {@link ContentResolver} used to add the data. + * @param uri {@link Uri} of the photo. + */ + public void addNewPhoto(ContentResolver resolver, Uri uri); + + /** + * Finds the {@link LocalData} of the specified content Uri. + * + * @param Uri The content Uri of the {@link LocalData}. + * @return The index of the data. {@code -1} if not found. + */ + public int findDataByContentUri(Uri uri); + + /** + * Clears all the data currently loaded. + */ + public void flush(); + + /** + * Executes the deletion task. Delete the data waiting in the deletion queue. + * + * @param context The {@link Context} from the caller. + * @return {@code true} if task has been executed, {@code false} + * otherwise. + */ + public boolean executeDeletion(Context context); + + /** + * Undo a deletion. If there is any data waiting to be deleted in the queue, + * move it out of the deletion queue. + * + * @return {@code true} if there are items in the queue, {@code false} otherwise. + */ + public boolean undoDataRemoval(); +} diff --git a/src/com/android/camera/data/PanoramaMetadataLoader.java b/src/com/android/camera/data/PanoramaMetadataLoader.java new file mode 100644 index 000000000..21b5f8a3d --- /dev/null +++ b/src/com/android/camera/data/PanoramaMetadataLoader.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2013 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.data; + +import android.content.Context; +import android.net.Uri; + +import com.android.gallery3d.util.LightCycleHelper; +import com.android.gallery3d.util.LightCycleHelper.PanoramaMetadata; + +import java.util.ArrayList; + +/** + * This class breaks out the off-thread panorama support. + */ +public class PanoramaMetadataLoader { + /** + * Classes implementing this interface can get information about loaded + * photo sphere metadata. + */ + public static interface PanoramaMetadataCallback { + /** + * Called with the loaded metadata or <code>null</code>. + */ + public void onPanoramaMetadataLoaded(PanoramaMetadata metadata); + } + + private PanoramaMetadata mPanoramaMetadata; + private ArrayList<PanoramaMetadataCallback> mCallbacksWaiting; + private Uri mMediaUri; + + /** + * Instantiated the meta data loader for the image resource with the given + * URI. + */ + public PanoramaMetadataLoader(Uri uri) { + mMediaUri = uri; + } + + /** + * Asynchronously extract and return panorama metadata from the item with + * the given URI. + * <p> + * NOTE: This call is backed by a cache to speed up successive calls, which + * will return immediately. Use {@link #clearCachedValues()} is called. + */ + public synchronized void getPanoramaMetadata(final Context context, + PanoramaMetadataCallback callback) { + if (mPanoramaMetadata != null) { + // Return the cached data right away, no need to fetch it again. + callback.onPanoramaMetadataLoaded(mPanoramaMetadata); + } else { + if (mCallbacksWaiting == null) { + mCallbacksWaiting = new ArrayList<PanoramaMetadataCallback>(); + + // TODO: Don't create a new thread each time, use a pool or + // single instance. + (new Thread() { + @Override + public void run() { + onLoadingDone(LightCycleHelper.getPanoramaMetadata(context, + mMediaUri)); + } + }).start(); + } + mCallbacksWaiting.add(callback); + } + } + + /** + * Clear cached value and stop all running loading threads. + */ + public synchronized void clearCachedValues() { + if (mPanoramaMetadata != null) { + mPanoramaMetadata = null; + } + + // TODO: Cancel running loading thread if active. + } + + private synchronized void onLoadingDone(PanoramaMetadata metadata) { + mPanoramaMetadata = metadata; + if (mPanoramaMetadata == null) { + // Error getting panorama data from file. Treat as not panorama. + mPanoramaMetadata = LightCycleHelper.NOT_PANORAMA; + } + for (PanoramaMetadataCallback cb : mCallbacksWaiting) { + cb.onPanoramaMetadataLoaded(mPanoramaMetadata); + } + mCallbacksWaiting = null; + } +} diff --git a/src/com/android/camera/drawable/TextDrawable.java b/src/com/android/camera/drawable/TextDrawable.java new file mode 100644 index 000000000..60d8719c4 --- /dev/null +++ b/src/com/android/camera/drawable/TextDrawable.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2012 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.drawable; + +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.graphics.Paint.Align; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.TypedValue; + + +public class TextDrawable extends Drawable { + + private static final int DEFAULT_COLOR = Color.WHITE; + private static final int DEFAULT_TEXTSIZE = 15; + + private Paint mPaint; + private CharSequence mText; + private int mIntrinsicWidth; + private int mIntrinsicHeight; + private boolean mUseDropShadow; + + public TextDrawable(Resources res) { + this(res, ""); + } + + public TextDrawable(Resources res, CharSequence text) { + mText = text; + updatePaint(); + float textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, + DEFAULT_TEXTSIZE, res.getDisplayMetrics()); + mPaint.setTextSize(textSize); + mIntrinsicWidth = (int) (mPaint.measureText(mText, 0, mText.length()) + .5); + mIntrinsicHeight = mPaint.getFontMetricsInt(null); + } + + private void updatePaint() { + if (mPaint == null) { + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + } + mPaint.setColor(DEFAULT_COLOR); + mPaint.setTextAlign(Align.CENTER); + if (mUseDropShadow) { + mPaint.setTypeface(Typeface.DEFAULT_BOLD); + mPaint.setShadowLayer(10, 0, 0, 0xff000000); + } else { + mPaint.setTypeface(Typeface.DEFAULT); + mPaint.setShadowLayer(0, 0, 0, 0); + } + } + + public void setText(CharSequence txt) { + mText = txt; + if (txt == null) { + mIntrinsicWidth = 0; + mIntrinsicHeight = 0; + } else { + mIntrinsicWidth = (int) (mPaint.measureText(mText, 0, mText.length()) + .5); + mIntrinsicHeight = mPaint.getFontMetricsInt(null); + } + } + + @Override + public void draw(Canvas canvas) { + if (mText != null) { + Rect bounds = getBounds(); + canvas.drawText(mText, 0, mText.length(), + bounds.centerX(), bounds.centerY(), mPaint); + } + } + + public void setDropShadow(boolean shadow) { + mUseDropShadow = shadow; + updatePaint(); + } + + @Override + public int getOpacity() { + return mPaint.getAlpha(); + } + + @Override + public int getIntrinsicWidth() { + return mIntrinsicWidth; + } + + @Override + public int getIntrinsicHeight() { + return mIntrinsicHeight; + } + + @Override + public void setAlpha(int alpha) { + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter filter) { + mPaint.setColorFilter(filter); + } + +} diff --git a/src/com/android/camera/ui/AbstractSettingPopup.java b/src/com/android/camera/ui/AbstractSettingPopup.java new file mode 100644 index 000000000..783b6c771 --- /dev/null +++ b/src/com/android/camera/ui/AbstractSettingPopup.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2010 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.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.android.gallery3d.R; + +// A popup window that shows one or more camera settings. +abstract public class AbstractSettingPopup extends RotateLayout { + protected ViewGroup mSettingList; + protected TextView mTitle; + + public AbstractSettingPopup(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mTitle = (TextView) findViewById(R.id.title); + mSettingList = (ViewGroup) findViewById(R.id.settingList); + } + + abstract public void reloadPreference(); +} diff --git a/src/com/android/camera/ui/CameraControls.java b/src/com/android/camera/ui/CameraControls.java new file mode 100644 index 000000000..7fa6890a7 --- /dev/null +++ b/src/com/android/camera/ui/CameraControls.java @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2013 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.ui; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.camera.Util; +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; + +public class CameraControls extends RotatableLayout { + + private static final String TAG = "CAM_Controls"; + + private View mBackgroundView; + private View mShutter; + private View mSwitcher; + private View mMenu; + private View mIndicators; + private View mPreview; + + public CameraControls(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CameraControls(Context context) { + super(context); + } + + @Override + public void onFinishInflate() { + super.onFinishInflate(); + mBackgroundView = findViewById(R.id.blocker); + mSwitcher = findViewById(R.id.camera_switcher); + mShutter = findViewById(R.id.shutter_button); + mMenu = findViewById(R.id.menu); + mIndicators = findViewById(R.id.on_screen_indicators); + mPreview = findViewById(R.id.preview_thumb); + } + + @Override + public void onLayout(boolean changed, int l, int t, int r, int b) { + int orientation = getResources().getConfiguration().orientation; + int size = getResources().getDimensionPixelSize(R.dimen.camera_controls_size); + int rotation = getUnifiedRotation(); + adjustBackground(); + // As l,t,r,b are positions relative to parents, we need to convert them + // to child's coordinates + r = r - l; + b = b - t; + l = 0; + t = 0; + for (int i = 0; i < getChildCount(); i++) { + View v = getChildAt(i); + v.layout(l, t, r, b); + } + Rect shutter = new Rect(); + topRight(mPreview, l, t, r, b); + if (size > 0) { + // restrict controls to size + switch (rotation) { + case 0: + case 180: + l = (l + r - size) / 2; + r = l + size; + break; + case 90: + case 270: + t = (t + b - size) / 2; + b = t + size; + break; + } + } + center(mShutter, l, t, r, b, orientation, rotation, shutter); + center(mBackgroundView, l, t, r, b, orientation, rotation, new Rect()); + toLeft(mSwitcher, shutter, rotation); + toRight(mMenu, shutter, rotation); + toRight(mIndicators, shutter, rotation); + View retake = findViewById(R.id.btn_retake); + if (retake != null) { + center(retake, shutter, rotation); + View cancel = findViewById(R.id.btn_cancel); + toLeft(cancel, shutter, rotation); + View done = findViewById(R.id.btn_done); + toRight(done, shutter, rotation); + } + } + + private void center(View v, int l, int t, int r, int b, int orientation, int rotation, Rect result) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams(); + int tw = lp.leftMargin + v.getMeasuredWidth() + lp.rightMargin; + int th = lp.topMargin + v.getMeasuredHeight() + lp.bottomMargin; + switch (rotation) { + case 0: + // phone portrait; controls bottom + result.left = (r + l) / 2 - tw / 2 + lp.leftMargin; + result.right = (r + l) / 2 + tw / 2 - lp.rightMargin; + result.bottom = b - lp.bottomMargin; + result.top = b - th + lp.topMargin; + break; + case 90: + // phone landscape: controls right + result.right = r - lp.rightMargin; + result.left = r - tw + lp.leftMargin; + result.top = (b + t) / 2 - th / 2 + lp.topMargin; + result.bottom = (b + t) / 2 + th / 2 - lp.bottomMargin; + break; + case 180: + // phone upside down: controls top + result.left = (r + l) / 2 - tw / 2 + lp.leftMargin; + result.right = (r + l) / 2 + tw / 2 - lp.rightMargin; + result.top = t + lp.topMargin; + result.bottom = t + th - lp.bottomMargin; + break; + case 270: + // reverse landscape: controls left + result.left = l + lp.leftMargin; + result.right = l + tw - lp.rightMargin; + result.top = (b + t) / 2 - th / 2 + lp.topMargin; + result.bottom = (b + t) / 2 + th / 2 - lp.bottomMargin; + break; + } + v.layout(result.left, result.top, result.right, result.bottom); + } + + private void center(View v, Rect other, int rotation) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams(); + int tw = lp.leftMargin + v.getMeasuredWidth() + lp.rightMargin; + int th = lp.topMargin + v.getMeasuredHeight() + lp.bottomMargin; + int cx = (other.left + other.right) / 2; + int cy = (other.top + other.bottom) / 2; + v.layout(cx - tw / 2 + lp.leftMargin, + cy - th / 2 + lp.topMargin, + cx + tw / 2 - lp.rightMargin, + cy + th / 2 - lp.bottomMargin); + } + + private void toLeft(View v, Rect other, int rotation) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams(); + int tw = lp.leftMargin + v.getMeasuredWidth() + lp.rightMargin; + int th = lp.topMargin + v.getMeasuredHeight() + lp.bottomMargin; + int cx = (other.left + other.right) / 2; + int cy = (other.top + other.bottom) / 2; + int l = 0, r = 0, t = 0, b = 0; + switch (rotation) { + case 0: + // portrait, to left of anchor at bottom + l = other.left - tw + lp.leftMargin; + r = other.left - lp.rightMargin; + t = cy - th / 2 + lp.topMargin; + b = cy + th / 2 - lp.bottomMargin; + break; + case 90: + // phone landscape: below anchor on right + l = cx - tw / 2 + lp.leftMargin; + r = cx + tw / 2 - lp.rightMargin; + t = other.bottom + lp.topMargin; + b = other.bottom + th - lp.bottomMargin; + break; + case 180: + // phone upside down: right of anchor at top + l = other.right + lp.leftMargin; + r = other.right + tw - lp.rightMargin; + t = cy - th / 2 + lp.topMargin; + b = cy + th / 2 - lp.bottomMargin; + break; + case 270: + // reverse landscape: above anchor on left + l = cx - tw / 2 + lp.leftMargin; + r = cx + tw / 2 - lp.rightMargin; + t = other.top - th + lp.topMargin; + b = other.top - lp.bottomMargin; + break; + } + v.layout(l, t, r, b); + } + + private void toRight(View v, Rect other, int rotation) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams(); + int tw = lp.leftMargin + v.getMeasuredWidth() + lp.rightMargin; + int th = lp.topMargin + v.getMeasuredHeight() + lp.bottomMargin; + int cx = (other.left + other.right) / 2; + int cy = (other.top + other.bottom) / 2; + int l = 0, r = 0, t = 0, b = 0; + switch (rotation) { + case 0: + l = other.right + lp.leftMargin; + r = other.right + tw - lp.rightMargin; + t = cy - th / 2 + lp.topMargin; + b = cy + th / 2 - lp.bottomMargin; + break; + case 90: + l = cx - tw / 2 + lp.leftMargin; + r = cx + tw / 2 - lp.rightMargin; + t = other.top - th + lp.topMargin; + b = other.top - lp.bottomMargin; + break; + case 180: + l = other.left - tw + lp.leftMargin; + r = other.left - lp.rightMargin; + t = cy - th / 2 + lp.topMargin; + b = cy + th / 2 - lp.bottomMargin; + break; + case 270: + l = cx - tw / 2 + lp.leftMargin; + r = cx + tw / 2 - lp.rightMargin; + t = other.bottom + lp.topMargin; + b = other.bottom + th - lp.bottomMargin; + break; + } + v.layout(l, t, r, b); + } + + private void topRight(View v, int l, int t, int r, int b) { + // layout using the specific margins; the rotation code messes up the others + int mt = getContext().getResources().getDimensionPixelSize(R.dimen.capture_margin_top); + int mr = getContext().getResources().getDimensionPixelSize(R.dimen.capture_margin_right); + v.layout(r - v.getMeasuredWidth() - mr, t + mt, r - mr, t + mt + v.getMeasuredHeight()); + } + + private void adjustBackground() { + int rotation = getUnifiedRotation(); + // remove current drawable and reset rotation + mBackgroundView.setBackgroundDrawable(null); + mBackgroundView.setRotationX(0); + mBackgroundView.setRotationY(0); + // if the switcher background is top aligned we need to flip the background + // drawable vertically; if left aligned, flip horizontally + switch (rotation) { + case 180: + mBackgroundView.setRotationX(180); + break; + case 270: + mBackgroundView.setRotationY(180); + break; + default: + break; + } + mBackgroundView.setBackgroundResource(R.drawable.switcher_bg); + } + +} diff --git a/src/com/android/camera/ui/CameraRootView.java b/src/com/android/camera/ui/CameraRootView.java new file mode 100644 index 000000000..adda70697 --- /dev/null +++ b/src/com/android/camera/ui/CameraRootView.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2013 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.ui; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayManager.DisplayListener; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.camera.Util; +import com.android.gallery3d.common.ApiHelper; + +public class CameraRootView extends FrameLayout { + + private int mTopMargin = 0; + private int mBottomMargin = 0; + private int mLeftMargin = 0; + private int mRightMargin = 0; + private Rect mCurrentInsets; + private int mOffset = 0; + private Object mDisplayListener; + private MyDisplayListener mListener; + public interface MyDisplayListener { + public void onDisplayChanged(); + } + + public CameraRootView(Context context, AttributeSet attrs) { + super(context, attrs); + initDisplayListener(); + setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + + @Override + protected boolean fitSystemWindows(Rect insets) { + super.fitSystemWindows(insets); + mCurrentInsets = insets; + // insets include status bar, navigation bar, etc + // In this case, we are only concerned with the size of nav bar + if (mOffset > 0) return true; + + if (insets.bottom > 0) { + mOffset = insets.bottom; + } else if (insets.right > 0) { + mOffset = insets.right; + } + return true; + } + + public void initDisplayListener() { + if (ApiHelper.HAS_DISPLAY_LISTENER) { + mDisplayListener = new DisplayListener() { + + @Override + public void onDisplayAdded(int arg0) {} + + @Override + public void onDisplayChanged(int arg0) { + mListener.onDisplayChanged(); + } + + @Override + public void onDisplayRemoved(int arg0) {} + }; + } + } + + public void setDisplayChangeListener(MyDisplayListener listener) { + mListener = listener; + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + if (ApiHelper.HAS_DISPLAY_LISTENER) { + ((DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE)) + .registerDisplayListener((DisplayListener) mDisplayListener, null); + } + } + + @Override + public void onDetachedFromWindow () { + super.onDetachedFromWindow(); + if (ApiHelper.HAS_DISPLAY_LISTENER) { + ((DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE)) + .unregisterDisplayListener((DisplayListener) mDisplayListener); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int rotation = Util.getDisplayRotation((Activity) getContext()); + // all the layout code assumes camera device orientation to be portrait + // adjust rotation for landscape + int orientation = getResources().getConfiguration().orientation; + int camOrientation = (rotation % 180 == 0) ? Configuration.ORIENTATION_PORTRAIT + : Configuration.ORIENTATION_LANDSCAPE; + if (camOrientation != orientation) { + rotation = (rotation + 90) % 360; + } + // calculate margins + mLeftMargin = 0; + mRightMargin = 0; + mBottomMargin = 0; + mTopMargin = 0; + switch (rotation) { + case 0: + mBottomMargin += mOffset; + break; + case 90: + mRightMargin += mOffset; + break; + case 180: + mTopMargin += mOffset; + break; + case 270: + mLeftMargin += mOffset; + break; + } + if (mCurrentInsets != null) { + if (mCurrentInsets.right > 0) { + // navigation bar on the right + mRightMargin = mRightMargin > 0 ? mRightMargin : mCurrentInsets.right; + } else { + // navigation bar on the bottom + mBottomMargin = mBottomMargin > 0 ? mBottomMargin : mCurrentInsets.bottom; + } + } + // make sure all the children are resized + super.onMeasure(widthMeasureSpec - mLeftMargin - mRightMargin, + heightMeasureSpec - mTopMargin - mBottomMargin); + setMeasuredDimension(widthMeasureSpec, heightMeasureSpec); + } + + @Override + public void onLayout(boolean changed, int l, int t, int r, int b) { + r -= l; + b -= t; + l = 0; + t = 0; + int orientation = getResources().getConfiguration().orientation; + // Lay out children + for (int i = 0; i < getChildCount(); i++) { + View v = getChildAt(i); + if (v instanceof CameraControls) { + // Lay out camera controls to center on the short side of the screen + // so that they stay in place during rotation + int width = v.getMeasuredWidth(); + int height = v.getMeasuredHeight(); + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + int left = (l + r - width) / 2; + v.layout(left, t + mTopMargin, left + width, b - mBottomMargin); + } else { + int top = (t + b - height) / 2; + v.layout(l + mLeftMargin, top, r - mRightMargin, top + height); + } + } else { + v.layout(l + mLeftMargin, t + mTopMargin, r - mRightMargin, b - mBottomMargin); + } + } + } +} diff --git a/src/com/android/camera/ui/CameraSwitcher.java b/src/com/android/camera/ui/CameraSwitcher.java new file mode 100644 index 000000000..6e4321571 --- /dev/null +++ b/src/com/android/camera/ui/CameraSwitcher.java @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.AnimatorListenerAdapter; +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnTouchListener; +import android.view.ViewGroup; +import android.widget.FrameLayout.LayoutParams; +import android.widget.LinearLayout; + +import com.android.camera.Util; +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.util.LightCycleHelper; +import com.android.gallery3d.util.UsageStatistics; + +public class CameraSwitcher extends RotateImageView + implements OnClickListener, OnTouchListener { + + private static final String TAG = "CAM_Switcher"; + private static final int SWITCHER_POPUP_ANIM_DURATION = 200; + + public static final int PHOTO_MODULE_INDEX = 0; + public static final int VIDEO_MODULE_INDEX = 1; + public static final int LIGHTCYCLE_MODULE_INDEX = 2; + public static final int REFOCUS_MODULE_INDEX = 3; + private static final int[] DRAW_IDS = { + R.drawable.ic_switch_camera, + R.drawable.ic_switch_video, + R.drawable.ic_switch_photosphere, + R.drawable.ic_switch_refocus + }; + public interface CameraSwitchListener { + public void onCameraSelected(int i); + public void onShowSwitcherPopup(); + } + + private CameraSwitchListener mListener; + private int mCurrentIndex; + private int[] mModuleIds; + private int[] mDrawIds; + private int mItemSize; + private View mPopup; + private View mParent; + private boolean mShowingPopup; + private boolean mNeedsAnimationSetup; + private Drawable mIndicator; + + private float mTranslationX = 0; + private float mTranslationY = 0; + + private AnimatorListener mHideAnimationListener; + private AnimatorListener mShowAnimationListener; + + public CameraSwitcher(Context context) { + super(context); + init(context); + } + + public CameraSwitcher(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + private void init(Context context) { + mItemSize = context.getResources().getDimensionPixelSize(R.dimen.switcher_size); + setOnClickListener(this); + mIndicator = context.getResources().getDrawable(R.drawable.ic_switcher_menu_indicator); + initializeDrawables(context); + } + + public void initializeDrawables(Context context) { + int totaldrawid = (LightCycleHelper.hasLightCycleCapture(context) + ? DRAW_IDS.length : DRAW_IDS.length - 1); + + int[] drawids = new int[totaldrawid]; + int[] moduleids = new int[totaldrawid]; + int ix = 0; + for (int i = 0; i < DRAW_IDS.length; i++) { + if (i == LIGHTCYCLE_MODULE_INDEX && !LightCycleHelper.hasLightCycleCapture(context)) { + continue; // not enabled, so don't add to UI + } + moduleids[ix] = i; + drawids[ix++] = DRAW_IDS[i]; + } + setIds(moduleids, drawids); + } + + public void setIds(int[] moduleids, int[] drawids) { + mDrawIds = drawids; + mModuleIds = moduleids; + } + + public void setCurrentIndex(int i) { + mCurrentIndex = i; + setImageResource(mDrawIds[i]); + } + + public void setSwitchListener(CameraSwitchListener l) { + mListener = l; + } + + @Override + public void onClick(View v) { + showSwitcher(); + mListener.onShowSwitcherPopup(); + } + + private void onCameraSelected(int ix) { + hidePopup(); + if ((ix != mCurrentIndex) && (mListener != null)) { + UsageStatistics.onEvent("CameraModeSwitch", null, null); + UsageStatistics.setPendingTransitionCause( + UsageStatistics.TRANSITION_MENU_TAP); + setCurrentIndex(ix); + mListener.onCameraSelected(mModuleIds[ix]); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + mIndicator.setBounds(getDrawable().getBounds()); + mIndicator.draw(canvas); + } + + private void initPopup() { + mParent = LayoutInflater.from(getContext()).inflate(R.layout.switcher_popup, + (ViewGroup) getParent()); + LinearLayout content = (LinearLayout) mParent.findViewById(R.id.content); + mPopup = content; + // Set the gravity of the popup, so that it shows up at the right position + // on screen + LayoutParams lp = ((LayoutParams) mPopup.getLayoutParams()); + lp.gravity = ((LayoutParams) mParent.findViewById(R.id.camera_switcher) + .getLayoutParams()).gravity; + mPopup.setLayoutParams(lp); + + mPopup.setVisibility(View.INVISIBLE); + mNeedsAnimationSetup = true; + for (int i = mDrawIds.length - 1; i >= 0; i--) { + RotateImageView item = new RotateImageView(getContext()); + item.setImageResource(mDrawIds[i]); + item.setBackgroundResource(R.drawable.bg_pressed); + final int index = i; + item.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (showsPopup()) onCameraSelected(index); + } + }); + switch (mDrawIds[i]) { + case R.drawable.ic_switch_camera: + item.setContentDescription(getContext().getResources().getString( + R.string.accessibility_switch_to_camera)); + break; + case R.drawable.ic_switch_video: + item.setContentDescription(getContext().getResources().getString( + R.string.accessibility_switch_to_video)); + break; + case R.drawable.ic_switch_photosphere: + item.setContentDescription(getContext().getResources().getString( + R.string.accessibility_switch_to_new_panorama)); + break; + case R.drawable.ic_switch_refocus: + item.setContentDescription(getContext().getResources().getString( + R.string.accessibility_switch_to_refocus)); + break; + default: + break; + } + content.addView(item, new LinearLayout.LayoutParams(mItemSize, mItemSize)); + } + mPopup.measure(MeasureSpec.makeMeasureSpec(mParent.getWidth(), MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(mParent.getHeight(), MeasureSpec.AT_MOST)); + } + + public boolean showsPopup() { + return mShowingPopup; + } + + public boolean isInsidePopup(MotionEvent evt) { + if (!showsPopup()) return false; + int topLeft[] = new int[2]; + mPopup.getLocationOnScreen(topLeft); + int left = topLeft[0]; + int top = topLeft[1]; + int bottom = top + mPopup.getHeight(); + int right = left + mPopup.getWidth(); + return evt.getX() >= left && evt.getX() < right + && evt.getY() >= top && evt.getY() < bottom; + } + + private void hidePopup() { + mShowingPopup = false; + setVisibility(View.VISIBLE); + if (mPopup != null && !animateHidePopup()) { + mPopup.setVisibility(View.INVISIBLE); + } + mParent.setOnTouchListener(null); + } + + @Override + public void onConfigurationChanged(Configuration config) { + if (showsPopup()) { + ((ViewGroup) mParent).removeView(mPopup); + mPopup = null; + initPopup(); + mPopup.setVisibility(View.VISIBLE); + } + } + + private void showSwitcher() { + mShowingPopup = true; + if (mPopup == null) { + initPopup(); + } + layoutPopup(); + mPopup.setVisibility(View.VISIBLE); + if (!animateShowPopup()) { + setVisibility(View.INVISIBLE); + } + mParent.setOnTouchListener(this); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + closePopup(); + return true; + } + + public void closePopup() { + if (showsPopup()) { + hidePopup(); + } + } + + @Override + public void setOrientation(int degree, boolean animate) { + super.setOrientation(degree, animate); + ViewGroup content = (ViewGroup) mPopup; + if (content == null) return; + for (int i = 0; i < content.getChildCount(); i++) { + RotateImageView iv = (RotateImageView) content.getChildAt(i); + iv.setOrientation(degree, animate); + } + } + + private void layoutPopup() { + int orientation = Util.getDisplayRotation((Activity) getContext()); + int w = mPopup.getMeasuredWidth(); + int h = mPopup.getMeasuredHeight(); + if (orientation == 0) { + mPopup.layout(getRight() - w, getBottom() - h, getRight(), getBottom()); + mTranslationX = 0; + mTranslationY = h / 3; + } else if (orientation == 90) { + mTranslationX = w / 3; + mTranslationY = - h / 3; + mPopup.layout(getRight() - w, getTop(), getRight(), getTop() + h); + } else if (orientation == 180) { + mTranslationX = - w / 3; + mTranslationY = - h / 3; + mPopup.layout(getLeft(), getTop(), getLeft() + w, getTop() + h); + } else { + mTranslationX = - w / 3; + mTranslationY = h - getHeight(); + mPopup.layout(getLeft(), getBottom() - h, getLeft() + w, getBottom()); + } + } + + @Override + public void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (mPopup != null) { + layoutPopup(); + } + } + + private void popupAnimationSetup() { + if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) { + return; + } + layoutPopup(); + mPopup.setScaleX(0.3f); + mPopup.setScaleY(0.3f); + mPopup.setTranslationX(mTranslationX); + mPopup.setTranslationY(mTranslationY); + mNeedsAnimationSetup = false; + } + + private boolean animateHidePopup() { + if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) { + return false; + } + if (mHideAnimationListener == null) { + mHideAnimationListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Verify that we weren't canceled + if (!showsPopup() && mPopup != null) { + mPopup.setVisibility(View.INVISIBLE); + ((ViewGroup) mParent).removeView(mPopup); + mPopup = null; + } + } + }; + } + mPopup.animate() + .alpha(0f) + .scaleX(0.3f).scaleY(0.3f) + .translationX(mTranslationX) + .translationY(mTranslationY) + .setDuration(SWITCHER_POPUP_ANIM_DURATION) + .setListener(mHideAnimationListener); + animate().alpha(1f).setDuration(SWITCHER_POPUP_ANIM_DURATION) + .setListener(null); + return true; + } + + private boolean animateShowPopup() { + if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) { + return false; + } + if (mNeedsAnimationSetup) { + popupAnimationSetup(); + } + if (mShowAnimationListener == null) { + mShowAnimationListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Verify that we weren't canceled + if (showsPopup()) { + setVisibility(View.INVISIBLE); + // request layout to make sure popup is laid out correctly on ICS + mPopup.requestLayout(); + } + } + }; + } + mPopup.animate() + .alpha(1f) + .scaleX(1f).scaleY(1f) + .translationX(0) + .translationY(0) + .setDuration(SWITCHER_POPUP_ANIM_DURATION) + .setListener(null); + animate().alpha(0f).setDuration(SWITCHER_POPUP_ANIM_DURATION) + .setListener(mShowAnimationListener); + return true; + } +} diff --git a/src/com/android/camera/ui/CheckedLinearLayout.java b/src/com/android/camera/ui/CheckedLinearLayout.java new file mode 100644 index 000000000..4e7750499 --- /dev/null +++ b/src/com/android/camera/ui/CheckedLinearLayout.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Checkable; +import android.widget.LinearLayout; + +public class CheckedLinearLayout extends LinearLayout implements Checkable { + private static final int[] CHECKED_STATE_SET = { + android.R.attr.state_checked + }; + private boolean mChecked; + + public CheckedLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean isChecked() { + return mChecked; + } + + @Override + public void setChecked(boolean checked) { + if (mChecked != checked) { + mChecked = checked; + refreshDrawableState(); + } + } + + @Override + public void toggle() { + setChecked(!mChecked); + } + + @Override + public int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + if (mChecked) { + mergeDrawableStates(drawableState, CHECKED_STATE_SET); + } + return drawableState; + } +} diff --git a/src/com/android/camera/ui/CountDownView.java b/src/com/android/camera/ui/CountDownView.java new file mode 100644 index 000000000..907d33508 --- /dev/null +++ b/src/com/android/camera/ui/CountDownView.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2012 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.ui; + +import java.util.Locale; + +import android.content.Context; +import android.media.AudioManager; +import android.media.SoundPool; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.android.gallery3d.R; + +public class CountDownView extends FrameLayout { + + private static final String TAG = "CAM_CountDownView"; + private static final int SET_TIMER_TEXT = 1; + private TextView mRemainingSecondsView; + private int mRemainingSecs = 0; + private OnCountDownFinishedListener mListener; + private Animation mCountDownAnim; + private SoundPool mSoundPool; + private int mBeepTwice; + private int mBeepOnce; + private boolean mPlaySound; + private final Handler mHandler = new MainHandler(); + + public CountDownView(Context context, AttributeSet attrs) { + super(context, attrs); + mCountDownAnim = AnimationUtils.loadAnimation(context, R.anim.count_down_exit); + // Load the beeps + mSoundPool = new SoundPool(1, AudioManager.STREAM_NOTIFICATION, 0); + mBeepOnce = mSoundPool.load(context, R.raw.beep_once, 1); + mBeepTwice = mSoundPool.load(context, R.raw.beep_twice, 1); + } + + public boolean isCountingDown() { + return mRemainingSecs > 0; + }; + + public interface OnCountDownFinishedListener { + public void onCountDownFinished(); + } + + private void remainingSecondsChanged(int newVal) { + mRemainingSecs = newVal; + if (newVal == 0) { + // Countdown has finished + setVisibility(View.INVISIBLE); + mListener.onCountDownFinished(); + } else { + Locale locale = getResources().getConfiguration().locale; + String localizedValue = String.format(locale, "%d", newVal); + mRemainingSecondsView.setText(localizedValue); + // Fade-out animation + mCountDownAnim.reset(); + mRemainingSecondsView.clearAnimation(); + mRemainingSecondsView.startAnimation(mCountDownAnim); + + // Play sound effect for the last 3 seconds of the countdown + if (mPlaySound) { + if (newVal == 1) { + mSoundPool.play(mBeepTwice, 1.0f, 1.0f, 0, 0, 1.0f); + } else if (newVal <= 3) { + mSoundPool.play(mBeepOnce, 1.0f, 1.0f, 0, 0, 1.0f); + } + } + // Schedule the next remainingSecondsChanged() call in 1 second + mHandler.sendEmptyMessageDelayed(SET_TIMER_TEXT, 1000); + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mRemainingSecondsView = (TextView) findViewById(R.id.remaining_seconds); + } + + public void setCountDownFinishedListener(OnCountDownFinishedListener listener) { + mListener = listener; + } + + public void startCountDown(int sec, boolean playSound) { + if (sec <= 0) { + Log.w(TAG, "Invalid input for countdown timer: " + sec + " seconds"); + return; + } + setVisibility(View.VISIBLE); + mPlaySound = playSound; + remainingSecondsChanged(sec); + } + + public void cancelCountDown() { + if (mRemainingSecs > 0) { + mRemainingSecs = 0; + mHandler.removeMessages(SET_TIMER_TEXT); + setVisibility(View.INVISIBLE); + } + } + + private class MainHandler extends Handler { + @Override + public void handleMessage(Message message) { + if (message.what == SET_TIMER_TEXT) { + remainingSecondsChanged(mRemainingSecs -1); + } + } + } +}
\ No newline at end of file diff --git a/src/com/android/camera/ui/CountdownTimerPopup.java b/src/com/android/camera/ui/CountdownTimerPopup.java new file mode 100644 index 000000000..7c3572b55 --- /dev/null +++ b/src/com/android/camera/ui/CountdownTimerPopup.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.NumberPicker; +import android.widget.NumberPicker.OnValueChangeListener; + +import com.android.camera.ListPreference; +import com.android.gallery3d.R; + +import java.util.Locale; + +/** + * This is a popup window that allows users to specify a countdown timer + */ + +public class CountdownTimerPopup extends AbstractSettingPopup { + private static final String TAG = "TimerSettingPopup"; + private NumberPicker mNumberSpinner; + private String[] mDurations; + private ListPreference mTimer; + private ListPreference mBeep; + private Listener mListener; + private Button mConfirmButton; + private View mPickerTitle; + private CheckBox mTimerSound; + private View mSoundTitle; + + static public interface Listener { + public void onListPrefChanged(ListPreference pref); + } + + public void setSettingChangedListener(Listener listener) { + mListener = listener; + } + + public CountdownTimerPopup(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void initialize(ListPreference timer, ListPreference beep) { + mTimer = timer; + mBeep = beep; + // Set title. + mTitle.setText(mTimer.getTitle()); + + // Duration + CharSequence[] entries = mTimer.getEntryValues(); + mDurations = new String[entries.length]; + Locale locale = getResources().getConfiguration().locale; + mDurations[0] = getResources().getString(R.string.setting_off); // Off + for (int i = 1; i < entries.length; i++) + mDurations[i] = String.format(locale, "%d", Integer.parseInt(entries[i].toString())); + int durationCount = mDurations.length; + mNumberSpinner = (NumberPicker) findViewById(R.id.duration); + mNumberSpinner.setMinValue(0); + mNumberSpinner.setMaxValue(durationCount - 1); + mNumberSpinner.setDisplayedValues(mDurations); + mNumberSpinner.setWrapSelectorWheel(false); + mNumberSpinner.setOnValueChangedListener(new OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldValue, int newValue) { + setTimeSelectionEnabled(newValue != 0); + } + }); + mConfirmButton = (Button) findViewById(R.id.timer_set_button); + mPickerTitle = findViewById(R.id.set_time_interval_title); + + // Disable focus on the spinners to prevent keyboard from coming up + mNumberSpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS); + + mConfirmButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + updateInputState(); + } + }); + mTimerSound = (CheckBox) findViewById(R.id.sound_check_box); + mSoundTitle = findViewById(R.id.beep_title); + } + + private void restoreSetting() { + int index = mTimer.findIndexOfValue(mTimer.getValue()); + if (index == -1) { + Log.e(TAG, "Invalid preference value."); + mTimer.print(); + throw new IllegalArgumentException(); + } else { + setTimeSelectionEnabled(index != 0); + mNumberSpinner.setValue(index); + } + boolean checked = mBeep.findIndexOfValue(mBeep.getValue()) != 0; + mTimerSound.setChecked(checked); + } + + @Override + public void setVisibility(int visibility) { + if (visibility == View.VISIBLE) { + if (getVisibility() != View.VISIBLE) { + // Set the number pickers and on/off switch to be consistent + // with the preference + restoreSetting(); + } + } + super.setVisibility(visibility); + } + + protected void setTimeSelectionEnabled(boolean enabled) { + mPickerTitle.setVisibility(enabled ? VISIBLE : INVISIBLE); + mTimerSound.setEnabled(enabled); + mSoundTitle.setEnabled(enabled); + } + + @Override + public void reloadPreference() { + } + + private void updateInputState() { + mTimer.setValueIndex(mNumberSpinner.getValue()); + mBeep.setValueIndex(mTimerSound.isChecked() ? 1 : 0); + if (mListener != null) { + mListener.onListPrefChanged(mTimer); + mListener.onListPrefChanged(mBeep); + } + } +} diff --git a/src/com/android/camera/ui/EffectSettingPopup.java b/src/com/android/camera/ui/EffectSettingPopup.java new file mode 100644 index 000000000..568781a01 --- /dev/null +++ b/src/com/android/camera/ui/EffectSettingPopup.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2010 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.ui; + +import android.annotation.TargetApi; +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.AdapterView; +import android.widget.GridView; +import android.widget.SimpleAdapter; + +import com.android.camera.IconListPreference; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; + +import java.util.ArrayList; +import java.util.HashMap; + +// A popup window that shows video effect setting. It has two grid view. +// One shows the goofy face effects. The other shows the background replacer +// effects. +public class EffectSettingPopup extends AbstractSettingPopup implements + AdapterView.OnItemClickListener, View.OnClickListener { + private static final String TAG = "EffectSettingPopup"; + private String mNoEffect; + private IconListPreference mPreference; + private Listener mListener; + private View mClearEffects; + private GridView mSillyFacesGrid; + private GridView mBackgroundGrid; + + // Data for silly face items. (text, image, and preference value) + ArrayList<HashMap<String, Object>> mSillyFacesItem = + new ArrayList<HashMap<String, Object>>(); + + // Data for background replacer items. (text, image, and preference value) + ArrayList<HashMap<String, Object>> mBackgroundItem = + new ArrayList<HashMap<String, Object>>(); + + + static public interface Listener { + public void onSettingChanged(); + } + + public EffectSettingPopup(Context context, AttributeSet attrs) { + super(context, attrs); + mNoEffect = context.getString(R.string.pref_video_effect_default); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mClearEffects = findViewById(R.id.clear_effects); + mClearEffects.setOnClickListener(this); + mSillyFacesGrid = (GridView) findViewById(R.id.effect_silly_faces); + mBackgroundGrid = (GridView) findViewById(R.id.effect_background); + } + + public void initialize(IconListPreference preference) { + mPreference = preference; + Context context = getContext(); + CharSequence[] entries = mPreference.getEntries(); + CharSequence[] entryValues = mPreference.getEntryValues(); + int[] iconIds = mPreference.getImageIds(); + if (iconIds == null) { + iconIds = mPreference.getLargeIconIds(); + } + + // Set title. + mTitle.setText(mPreference.getTitle()); + + for(int i = 0; i < entries.length; ++i) { + String value = entryValues[i].toString(); + if (value.equals(mNoEffect)) continue; // no effect, skip it. + HashMap<String, Object> map = new HashMap<String, Object>(); + map.put("value", value); + map.put("text", entries[i].toString()); + if (iconIds != null) map.put("image", iconIds[i]); + if (value.startsWith("goofy_face")) { + mSillyFacesItem.add(map); + } else if (value.startsWith("backdropper")) { + mBackgroundItem.add(map); + } + } + + boolean hasSillyFaces = mSillyFacesItem.size() > 0; + boolean hasBackground = mBackgroundItem.size() > 0; + + // Initialize goofy face if it is supported. + if (hasSillyFaces) { + findViewById(R.id.effect_silly_faces_title).setVisibility(View.VISIBLE); + findViewById(R.id.effect_silly_faces_title_separator).setVisibility(View.VISIBLE); + mSillyFacesGrid.setVisibility(View.VISIBLE); + SimpleAdapter sillyFacesItemAdapter = new SimpleAdapter(context, + mSillyFacesItem, R.layout.effect_setting_item, + new String[] {"text", "image"}, + new int[] {R.id.text, R.id.image}); + mSillyFacesGrid.setAdapter(sillyFacesItemAdapter); + mSillyFacesGrid.setOnItemClickListener(this); + } + + if (hasSillyFaces && hasBackground) { + findViewById(R.id.effect_background_separator).setVisibility(View.VISIBLE); + } + + // Initialize background replacer if it is supported. + if (hasBackground) { + findViewById(R.id.effect_background_title).setVisibility(View.VISIBLE); + findViewById(R.id.effect_background_title_separator).setVisibility(View.VISIBLE); + mBackgroundGrid.setVisibility(View.VISIBLE); + SimpleAdapter backgroundItemAdapter = new SimpleAdapter(context, + mBackgroundItem, R.layout.effect_setting_item, + new String[] {"text", "image"}, + new int[] {R.id.text, R.id.image}); + mBackgroundGrid.setAdapter(backgroundItemAdapter); + mBackgroundGrid.setOnItemClickListener(this); + } + + reloadPreference(); + } + + @Override + public void setVisibility(int visibility) { + if (visibility == View.VISIBLE) { + if (getVisibility() != View.VISIBLE) { + // Do not show or hide "Clear effects" button when the popup + // is already visible. Otherwise it looks strange. + boolean noEffect = mPreference.getValue().equals(mNoEffect); + mClearEffects.setVisibility(noEffect ? View.GONE : View.VISIBLE); + } + reloadPreference(); + } + super.setVisibility(visibility); + } + + // The value of the preference may have changed. Update the UI. + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + @Override + public void reloadPreference() { + mBackgroundGrid.setItemChecked(mBackgroundGrid.getCheckedItemPosition(), false); + mSillyFacesGrid.setItemChecked(mSillyFacesGrid.getCheckedItemPosition(), false); + + String value = mPreference.getValue(); + if (value.equals(mNoEffect)) return; + + for (int i = 0; i < mSillyFacesItem.size(); i++) { + if (value.equals(mSillyFacesItem.get(i).get("value"))) { + mSillyFacesGrid.setItemChecked(i, true); + return; + } + } + + for (int i = 0; i < mBackgroundItem.size(); i++) { + if (value.equals(mBackgroundItem.get(i).get("value"))) { + mBackgroundGrid.setItemChecked(i, true); + return; + } + } + + Log.e(TAG, "Invalid preference value: " + value); + mPreference.print(); + } + + public void setSettingChangedListener(Listener listener) { + mListener = listener; + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, + int index, long id) { + String value; + if (parent == mSillyFacesGrid) { + value = (String) mSillyFacesItem.get(index).get("value"); + } else if (parent == mBackgroundGrid) { + value = (String) mBackgroundItem.get(index).get("value"); + } else { + return; + } + + // Tapping the selected effect will deselect it (clear effects). + if (value.equals(mPreference.getValue())) { + mPreference.setValue(mNoEffect); + } else { + mPreference.setValue(value); + } + reloadPreference(); + if (mListener != null) mListener.onSettingChanged(); + } + + @Override + public void onClick(View v) { + // Clear the effect. + mPreference.setValue(mNoEffect); + reloadPreference(); + if (mListener != null) mListener.onSettingChanged(); + } +} diff --git a/src/com/android/camera/ui/ExpandedGridView.java b/src/com/android/camera/ui/ExpandedGridView.java new file mode 100644 index 000000000..13cf58f34 --- /dev/null +++ b/src/com/android/camera/ui/ExpandedGridView.java @@ -0,0 +1,36 @@ +/* + * 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.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.GridView; + +public class ExpandedGridView extends GridView { + public ExpandedGridView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // If UNSPECIFIED is passed to GridView, it will show only one row. + // Here GridView is put in a ScrollView, so pass it a very big size with + // AT_MOST to show all the rows. + heightMeasureSpec = MeasureSpec.makeMeasureSpec(65536, MeasureSpec.AT_MOST); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} diff --git a/src/com/android/camera/ui/FaceView.java b/src/com/android/camera/ui/FaceView.java new file mode 100644 index 000000000..7d66dc079 --- /dev/null +++ b/src/com/android/camera/ui/FaceView.java @@ -0,0 +1,226 @@ +/* + * 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.ui; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.RectF; +import android.hardware.Camera.Face; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +import com.android.camera.PhotoUI; +import com.android.camera.Util; +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; + +@TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) +public class FaceView extends View + implements FocusIndicator, Rotatable, + PhotoUI.SurfaceTextureSizeChangedListener { + private static final String TAG = "CAM FaceView"; + private final boolean LOGV = false; + // The value for android.hardware.Camera.setDisplayOrientation. + private int mDisplayOrientation; + // The orientation compensation for the face indicator to make it look + // correctly in all device orientations. Ex: if the value is 90, the + // indicator should be rotated 90 degrees counter-clockwise. + private int mOrientation; + private boolean mMirror; + private boolean mPause; + private Matrix mMatrix = new Matrix(); + private RectF mRect = new RectF(); + // As face detection can be flaky, we add a layer of filtering on top of it + // to avoid rapid changes in state (eg, flickering between has faces and + // not having faces) + private Face[] mFaces; + private Face[] mPendingFaces; + private int mColor; + private final int mFocusingColor; + private final int mFocusedColor; + private final int mFailColor; + private Paint mPaint; + private volatile boolean mBlocked; + + private int mUncroppedWidth; + private int mUncroppedHeight; + private static final int MSG_SWITCH_FACES = 1; + private static final int SWITCH_DELAY = 70; + private boolean mStateSwitchPending = false; + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SWITCH_FACES: + mStateSwitchPending = false; + mFaces = mPendingFaces; + invalidate(); + break; + } + } + }; + + public FaceView(Context context, AttributeSet attrs) { + super(context, attrs); + Resources res = getResources(); + mFocusingColor = res.getColor(R.color.face_detect_start); + mFocusedColor = res.getColor(R.color.face_detect_success); + mFailColor = res.getColor(R.color.face_detect_fail); + mColor = mFocusingColor; + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setStyle(Style.STROKE); + mPaint.setStrokeWidth(res.getDimension(R.dimen.face_circle_stroke)); + } + + @Override + public void onSurfaceTextureSizeChanged(int uncroppedWidth, int uncroppedHeight) { + mUncroppedWidth = uncroppedWidth; + mUncroppedHeight = uncroppedHeight; + } + + public void setFaces(Face[] faces) { + if (LOGV) Log.v(TAG, "Num of faces=" + faces.length); + if (mPause) return; + if (mFaces != null) { + if ((faces.length > 0 && mFaces.length == 0) + || (faces.length == 0 && mFaces.length > 0)) { + mPendingFaces = faces; + if (!mStateSwitchPending) { + mStateSwitchPending = true; + mHandler.sendEmptyMessageDelayed(MSG_SWITCH_FACES, SWITCH_DELAY); + } + return; + } + } + if (mStateSwitchPending) { + mStateSwitchPending = false; + mHandler.removeMessages(MSG_SWITCH_FACES); + } + mFaces = faces; + invalidate(); + } + + public void setDisplayOrientation(int orientation) { + mDisplayOrientation = orientation; + if (LOGV) Log.v(TAG, "mDisplayOrientation=" + orientation); + } + + @Override + public void setOrientation(int orientation, boolean animation) { + mOrientation = orientation; + invalidate(); + } + + public void setMirror(boolean mirror) { + mMirror = mirror; + if (LOGV) Log.v(TAG, "mMirror=" + mirror); + } + + public boolean faceExists() { + return (mFaces != null && mFaces.length > 0); + } + + @Override + public void showStart() { + mColor = mFocusingColor; + invalidate(); + } + + // Ignore the parameter. No autofocus animation for face detection. + @Override + public void showSuccess(boolean timeout) { + mColor = mFocusedColor; + invalidate(); + } + + // Ignore the parameter. No autofocus animation for face detection. + @Override + public void showFail(boolean timeout) { + mColor = mFailColor; + invalidate(); + } + + @Override + public void clear() { + // Face indicator is displayed during preview. Do not clear the + // drawable. + mColor = mFocusingColor; + mFaces = null; + invalidate(); + } + + public void pause() { + mPause = true; + } + + public void resume() { + mPause = false; + } + + public void setBlockDraw(boolean block) { + mBlocked = block; + } + + @Override + protected void onDraw(Canvas canvas) { + if (!mBlocked && (mFaces != null) && (mFaces.length > 0)) { + int rw, rh; + rw = mUncroppedWidth; + rh = mUncroppedHeight; + // Prepare the matrix. + if (((rh > rw) && ((mDisplayOrientation == 0) || (mDisplayOrientation == 180))) + || ((rw > rh) && ((mDisplayOrientation == 90) || (mDisplayOrientation == 270)))) { + int temp = rw; + rw = rh; + rh = temp; + } + Util.prepareMatrix(mMatrix, mMirror, mDisplayOrientation, rw, rh); + int dx = (getWidth() - rw) / 2; + int dy = (getHeight() - rh) / 2; + + // Focus indicator is directional. Rotate the matrix and the canvas + // so it looks correctly in all orientations. + canvas.save(); + mMatrix.postRotate(mOrientation); // postRotate is clockwise + canvas.rotate(-mOrientation); // rotate is counter-clockwise (for canvas) + for (int i = 0; i < mFaces.length; i++) { + // Filter out false positives. + if (mFaces[i].score < 50) continue; + + // Transform the coordinates. + mRect.set(mFaces[i].rect); + if (LOGV) Util.dumpRect(mRect, "Original rect"); + mMatrix.mapRect(mRect); + if (LOGV) Util.dumpRect(mRect, "Transformed rect"); + mPaint.setColor(mColor); + mRect.offset(dx, dy); + canvas.drawOval(mRect, mPaint); + } + canvas.restore(); + } + super.onDraw(canvas); + } +} diff --git a/src/com/android/camera/ui/FilmStripGestureRecognizer.java b/src/com/android/camera/ui/FilmStripGestureRecognizer.java new file mode 100644 index 000000000..f870b5829 --- /dev/null +++ b/src/com/android/camera/ui/FilmStripGestureRecognizer.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2013 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.ui; + +import android.content.Context; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; + +// This class aggregates three gesture detectors: GestureDetector, +// ScaleGestureDetector. +public class FilmStripGestureRecognizer { + @SuppressWarnings("unused") + private static final String TAG = "FilmStripGestureRecognizer"; + + public interface Listener { + boolean onSingleTapUp(float x, float y); + boolean onDoubleTap(float x, float y); + boolean onScroll(float x, float y, float dx, float dy); + boolean onFling(float velocityX, float velocityY); + boolean onScaleBegin(float focusX, float focusY); + boolean onScale(float focusX, float focusY, float scale); + boolean onDown(float x, float y); + boolean onUp(float x, float y); + void onScaleEnd(); + } + + private final GestureDetector mGestureDetector; + private final ScaleGestureDetector mScaleDetector; + private final Listener mListener; + + public FilmStripGestureRecognizer(Context context, Listener listener) { + mListener = listener; + mGestureDetector = new GestureDetector(context, new MyGestureListener(), + null, true /* ignoreMultitouch */); + mScaleDetector = new ScaleGestureDetector( + context, new MyScaleListener()); + } + + public void onTouchEvent(MotionEvent event) { + mGestureDetector.onTouchEvent(event); + mScaleDetector.onTouchEvent(event); + if (event.getAction() == MotionEvent.ACTION_UP) { + mListener.onUp(event.getX(), event.getY()); + } + } + + private class MyGestureListener + extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onSingleTapUp(MotionEvent e) { + return mListener.onSingleTapUp(e.getX(), e.getY()); + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + return mListener.onDoubleTap(e.getX(), e.getY()); + } + + @Override + public boolean onScroll( + MotionEvent e1, MotionEvent e2, float dx, float dy) { + return mListener.onScroll(e2.getX(), e2.getY(), dx, dy); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + return mListener.onFling(velocityX, velocityY); + } + + @Override + public boolean onDown(MotionEvent e) { + mListener.onDown(e.getX(), e.getY()); + return super.onDown(e); + } + } + + private class MyScaleListener + extends ScaleGestureDetector.SimpleOnScaleGestureListener { + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + return mListener.onScaleBegin( + detector.getFocusX(), detector.getFocusY()); + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + return mListener.onScale(detector.getFocusX(), + detector.getFocusY(), detector.getScaleFactor()); + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mListener.onScaleEnd(); + } + } +} diff --git a/src/com/android/camera/ui/FilmStripView.java b/src/com/android/camera/ui/FilmStripView.java new file mode 100644 index 000000000..8a1a85a55 --- /dev/null +++ b/src/com/android/camera/ui/FilmStripView.java @@ -0,0 +1,1720 @@ +/* + * Copyright (C) 2013 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.ui; + +import android.animation.Animator; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.Scroller; + +import com.android.camera.ui.FilmStripView.ImageData.PanoramaSupportCallback; +import com.android.gallery3d.R; +import com.android.gallery3d.util.LightCycleHelper.PanoramaViewHelper; + +public class FilmStripView extends ViewGroup { + @SuppressWarnings("unused") + private static final String TAG = "FilmStripView"; + + private static final int BUFFER_SIZE = 5; + private static final int DURATION_GEOMETRY_ADJUST = 200; + private static final float FILM_STRIP_SCALE = 0.6f; + private static final float FULLSCREEN_SCALE = 1f; + // Only check for intercepting touch events within first 500ms + private static final int SWIPE_TIME_OUT = 500; + + private Context mContext; + private FilmStripGestureRecognizer mGestureRecognizer; + private DataAdapter mDataAdapter; + private int mViewGap; + private final Rect mDrawArea = new Rect(); + + private final int mCurrentInfo = (BUFFER_SIZE - 1) / 2; + private float mScale; + private MyController mController; + private int mCenterX = -1; + private ViewInfo[] mViewInfo = new ViewInfo[BUFFER_SIZE]; + + private Listener mListener; + + private MotionEvent mDown; + private boolean mCheckToIntercept = true; + private View mCameraView; + private int mSlop; + private TimeInterpolator mViewAnimInterpolator; + + private ImageButton mViewPhotoSphereButton; + private PanoramaViewHelper mPanoramaViewHelper; + private long mLastItemId = -1; + + // This is used to resolve the misalignment problem when the device + // orientation is changed. If the current item is in fullscreen, it might + // be shifted because mCenterX is not adjusted with the orientation. + // Set this to true when onSizeChanged is called to make sure we adjust + // mCenterX accordingly. + private boolean mAnchorPending; + + /** + * Common interface for all images in the filmstrip. + */ + public interface ImageData { + /** + * Interface that is used to tell the caller whether an image is a photo + * sphere. + */ + public static interface PanoramaSupportCallback { + /** + * Called then photo sphere info has been loaded. + * + * @param isPanorama whether the image is a valid photo sphere + * @param isPanorama360 whether the photo sphere is a full 360 + * degree horizontal panorama + */ + void panoramaInfoAvailable(boolean isPanorama, + boolean isPanorama360); + } + + // Image data types. + public static final int TYPE_NONE = 0; + public static final int TYPE_CAMERA_PREVIEW = 1; + public static final int TYPE_PHOTO = 2; + public static final int TYPE_VIDEO = 3; + + // Actions allowed to be performed on the image data. + // The actions are defined bit-wise so we can use bit operations like + // | and &. + public static final int ACTION_NONE = 0; + public static final int ACTION_PROMOTE = 1; + public static final int ACTION_DEMOTE = (1 << 1); + + /** + * SIZE_FULL can be returned by {@link ImageData#getWidth()} and + * {@link ImageData#getHeight()}. + * When SIZE_FULL is returned for width/height, it means the the + * width or height will be disregarded when deciding the view size + * of this ImageData, just use full screen size. + */ + public static final int SIZE_FULL = -2; + + /** + * Returns the width of the image. The final layout of the view returned + * by {@link DataAdapter#getView(android.content.Context, int)} will + * preserve the aspect ratio of + * {@link com.android.camera.ui.FilmStripView.ImageData#getWidth()} and + * {@link com.android.camera.ui.FilmStripView.ImageData#getHeight()}. + */ + public int getWidth(); + + + /** + * Returns the width of the image. The final layout of the view returned + * by {@link DataAdapter#getView(android.content.Context, int)} will + * preserve the aspect ratio of + * {@link com.android.camera.ui.FilmStripView.ImageData#getWidth()} and + * {@link com.android.camera.ui.FilmStripView.ImageData#getHeight()}. + */ + public int getHeight(); + + /** Returns the image data type */ + public int getType(); + + /** + * Checks if the UI action is supported. + * + * @param action The UI actions to check. + * @return {@code false} if at least one of the actions is not + * supported. {@code true} otherwise. + */ + public boolean isUIActionSupported(int action); + + /** + * Gives the data a hint when its view is going to be displayed. + * {@code FilmStripView} should always call this function before + * showing its corresponding view every time. + */ + public void prepare(); + + /** + * Gives the data a hint when its view is going to be removed from the + * view hierarchy. {@code FilmStripView} should always call this + * function after its corresponding view is removed from the view + * hierarchy. + */ + public void recycle(); + + /** + * Asynchronously checks if the image is a photo sphere. Notified the + * callback when the results are available. + */ + public void isPhotoSphere(Context context, PanoramaSupportCallback callback); + + /** + * If the item is a valid photo sphere panorama, this method will launch + * the viewer. + */ + public void viewPhotoSphere(PanoramaViewHelper helper); + } + + /** + * An interfaces which defines the interactions between the + * {@link ImageData} and the {@link FilmStripView}. + */ + public interface DataAdapter { + /** + * An interface which defines the update report used to return to + * the {@link com.android.camera.ui.FilmStripView.Listener}. + */ + public interface UpdateReporter { + /** Checks if the data of dataID is removed. */ + public boolean isDataRemoved(int dataID); + + /** Checks if the data of dataID is updated. */ + public boolean isDataUpdated(int dataID); + } + + /** + * An interface which defines the listener for UI actions over + * {@link ImageData}. + */ + public interface Listener { + // Called when the whole data loading is done. No any assumption + // on previous data. + public void onDataLoaded(); + + // Only some of the data is changed. The listener should check + // if any thing needs to be updated. + public void onDataUpdated(UpdateReporter reporter); + + public void onDataInserted(int dataID, ImageData data); + + public void onDataRemoved(int dataID, ImageData data); + } + + /** Returns the total number of image data */ + public int getTotalNumber(); + + /** + * Returns the view to visually present the image data. + * + * @param context The {@link Context} to create the view. + * @param dataID The ID of the image data to be presented. + * @return The view representing the image data. Null if + * unavailable or the {@code dataID} is out of range. + */ + public View getView(Context context, int dataID); + + /** + * Returns the {@link ImageData} specified by the ID. + * + * @param dataID The ID of the {@link ImageData}. + * @return The specified {@link ImageData}. Null if not available. + */ + public ImageData getImageData(int dataID); + + /** + * Suggests the data adapter the maximum possible size of the layout + * so the {@link DataAdapter} can optimize the view returned for the + * {@link ImageData}. + * + * @param w Maximum width. + * @param h Maximum height. + */ + public void suggestViewSizeBound(int w, int h); + + /** + * Sets the listener for FilmStripView UI actions over the ImageData. + * + * @param listener The listener to use. + */ + public void setListener(Listener listener); + + /** + * The callback when the item enters/leaves full-screen. + * TODO: Call this function actually. + * + * @param dataID The ID of the image data. + * @param fullScreen {@code true} if the data is entering full-screen. + * {@code false} otherwise. + */ + public void onDataFullScreen(int dataID, boolean fullScreen); + + /** + * The callback when the item is centered/off-centered. + * TODO: Calls this function actually. + * + * @param dataID The ID of the image data. + * @param centered {@code true} if the data is centered. + * {@code false} otherwise. + */ + public void onDataCentered(int dataID, boolean centered); + + /** + * Returns {@code true} if the view of the data can be moved by swipe + * gesture when in full-screen. + * + * @param dataID The ID of the data. + * @return {@code true} if the view can be moved, + * {@code false} otherwise. + */ + public boolean canSwipeInFullScreen(int dataID); + } + + /** + * An interface which defines the FilmStripView UI action listener. + */ + public interface Listener { + /** + * Callback when the data is promoted. + * + * @param dataID The ID of the promoted data. + */ + public void onDataPromoted(int dataID); + + /** + * Callback when the data is demoted. + * + * @param dataID The ID of the demoted data. + */ + public void onDataDemoted(int dataID); + + public void onDataFullScreenChange(int dataID, boolean full); + + /** + * Callback when entering/leaving camera mode. + * + * @param toCamera {@code true} if entering camera mode. Otherwise, + * {@code false} + */ + public void onSwitchMode(boolean toCamera); + } + + /** + * An interface which defines the controller of {@link FilmStripView}. + */ + public interface Controller { + public boolean isScalling(); + + public void scroll(float deltaX); + + public void fling(float velocity); + + public void scrollTo(int position, int duration, boolean interruptible); + + public boolean stopScrolling(); + + public boolean isScrolling(); + + public void lockAtCurrentView(); + + public void unlockPosition(); + + public void gotoCameraFullScreen(); + + public void gotoFilmStrip(); + + public void gotoFullScreen(); + } + + /** + * A helper class to tract and calculate the view coordination. + */ + private static class ViewInfo { + private int mDataID; + /** The position of the left of the view in the whole filmstrip. */ + private int mLeftPosition; + private View mView; + private RectF mViewArea; + + public ViewInfo(int id, View v) { + v.setPivotX(0f); + v.setPivotY(0f); + mDataID = id; + mView = v; + mLeftPosition = -1; + mViewArea = new RectF(); + } + + public int getID() { + return mDataID; + } + + public void setID(int id) { + mDataID = id; + } + + public void setLeftPosition(int pos) { + mLeftPosition = pos; + } + + public int getLeftPosition() { + return mLeftPosition; + } + + public float getTranslationY(float scale) { + return mView.getTranslationY() / scale; + } + + public float getTranslationX(float scale) { + return mView.getTranslationX(); + } + + public void setTranslationY(float transY, float scale) { + mView.setTranslationY(transY * scale); + } + + public void setTranslationX(float transX, float scale) { + mView.setTranslationX(transX * scale); + } + + public void translateXBy(float transX, float scale) { + mView.setTranslationX(mView.getTranslationX() + transX * scale); + } + + public int getCenterX() { + return mLeftPosition + mView.getWidth() / 2; + } + + public View getView() { + return mView; + } + + private void layoutAt(int left, int top) { + mView.layout(left, top, left + mView.getMeasuredWidth(), + top + mView.getMeasuredHeight()); + } + + public void layoutIn(Rect drawArea, int refCenter, float scale) { + // drawArea is where to layout in. + // refCenter is the absolute horizontal position of the center of + // drawArea. + int left = (int) (drawArea.centerX() + (mLeftPosition - refCenter) * scale); + int top = (int) (drawArea.centerY() - (mView.getMeasuredHeight() / 2) * scale); + layoutAt(left, top); + mView.setScaleX(scale); + mView.setScaleY(scale); + + // update mViewArea for touch detection. + int l = mView.getLeft(); + int t = mView.getTop(); + mViewArea.set(l, t, + l + mView.getWidth() * scale, + t + mView.getHeight() * scale); + } + + public boolean areaContains(float x, float y) { + return mViewArea.contains(x, y); + } + } + + /** Constructor. */ + public FilmStripView(Context context) { + super(context); + init(context); + } + + /** Constructor. */ + public FilmStripView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + /** Constructor. */ + public FilmStripView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + private void init(Context context) { + // This is for positioning camera controller at the same place in + // different orientations. + setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + + setWillNotDraw(false); + mContext = context; + mScale = 1.0f; + mController = new MyController(context); + mViewAnimInterpolator = new DecelerateInterpolator(); + mGestureRecognizer = + new FilmStripGestureRecognizer(context, new MyGestureReceiver()); + mSlop = (int) getContext().getResources().getDimension(R.dimen.pie_touch_slop); + } + + /** + * Returns the controller. + * + * @return The {@code Controller}. + */ + public Controller getController() { + return mController; + } + + public void setListener(Listener l) { + mListener = l; + } + + public void setViewGap(int viewGap) { + mViewGap = viewGap; + } + + /** + * Sets the helper that's to be used to open photo sphere panoramas. + */ + public void setPanoramaViewHelper(PanoramaViewHelper helper) { + mPanoramaViewHelper = helper; + } + + public float getScale() { + return mScale; + } + + public boolean isAnchoredTo(int id) { + if (mViewInfo[mCurrentInfo].getID() == id + && mViewInfo[mCurrentInfo].getCenterX() == mCenterX) { + return true; + } + return false; + } + + public int getCurrentType() { + if (mDataAdapter == null) { + return ImageData.TYPE_NONE; + } + ViewInfo curr = mViewInfo[mCurrentInfo]; + if (curr == null) { + return ImageData.TYPE_NONE; + } + return mDataAdapter.getImageData(curr.getID()).getType(); + } + + @Override + public void onDraw(Canvas c) { + if (mController.hasNewGeometry()) { + layoutChildren(); + } + } + + /** Returns [width, height] preserving image aspect ratio. */ + private int[] calculateChildDimension( + int imageWidth, int imageHeight, + int boundWidth, int boundHeight) { + + if (imageWidth == ImageData.SIZE_FULL + || imageHeight == ImageData.SIZE_FULL) { + imageWidth = boundWidth; + imageHeight = boundHeight; + } + + int[] ret = new int[2]; + ret[0] = boundWidth; + ret[1] = boundHeight; + + if (imageWidth * ret[1] > ret[0] * imageHeight) { + ret[1] = imageHeight * ret[0] / imageWidth; + } else { + ret[0] = imageWidth * ret[1] / imageHeight; + } + + return ret; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int boundWidth = MeasureSpec.getSize(widthMeasureSpec); + int boundHeight = MeasureSpec.getSize(heightMeasureSpec); + if (boundWidth == 0 || boundHeight == 0) { + // Either width or height is unknown, can't measure children yet. + return; + } + + if (mDataAdapter != null) { + mDataAdapter.suggestViewSizeBound(boundWidth / 2, boundHeight / 2); + } + + for (ViewInfo info : mViewInfo) { + if (info == null) continue; + + int id = info.getID(); + int[] dim = calculateChildDimension( + mDataAdapter.getImageData(id).getWidth(), + mDataAdapter.getImageData(id).getHeight(), + boundWidth, boundHeight); + + info.getView().measure( + MeasureSpec.makeMeasureSpec( + dim[0], MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec( + dim[1], MeasureSpec.EXACTLY)); + } + } + + @Override + protected boolean fitSystemWindows(Rect insets) { + if (mViewPhotoSphereButton != null) { + // Set the position of the "View Photo Sphere" button. + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mViewPhotoSphereButton + .getLayoutParams(); + params.bottomMargin = insets.bottom; + mViewPhotoSphereButton.setLayoutParams(params); + } + + return super.fitSystemWindows(insets); + } + + private int findTheNearestView(int pointX) { + + int nearest = 0; + // Find the first non-null ViewInfo. + while (nearest < BUFFER_SIZE + && (mViewInfo[nearest] == null || mViewInfo[nearest].getLeftPosition() == -1)) { + nearest++; + } + // No existing available ViewInfo + if (nearest == BUFFER_SIZE) { + return -1; + } + int min = Math.abs(pointX - mViewInfo[nearest].getCenterX()); + + for (int infoID = nearest + 1; infoID < BUFFER_SIZE && mViewInfo[infoID] != null; infoID++) { + // Not measured yet. + if (mViewInfo[infoID].getLeftPosition() == -1) + continue; + + int c = mViewInfo[infoID].getCenterX(); + int dist = Math.abs(pointX - c); + if (dist < min) { + min = dist; + nearest = infoID; + } + } + return nearest; + } + + private ViewInfo buildInfoFromData(int dataID) { + ImageData data = mDataAdapter.getImageData(dataID); + if (data == null) { + return null; + } + data.prepare(); + View v = mDataAdapter.getView(mContext, dataID); + if (v == null) { + return null; + } + ViewInfo info = new ViewInfo(dataID, v); + v = info.getView(); + if (v != mCameraView) { + addView(info.getView()); + } else { + v.setVisibility(View.VISIBLE); + } + return info; + } + + private void removeInfo(int infoID) { + if (infoID >= mViewInfo.length || mViewInfo[infoID] == null) { + return; + } + + ImageData data = mDataAdapter.getImageData(mViewInfo[infoID].getID()); + checkForRemoval(data, mViewInfo[infoID].getView()); + mViewInfo[infoID] = null; + } + + /** + * We try to keep the one closest to the center of the screen at position + * mCurrentInfo. + */ + private void stepIfNeeded() { + if (!inFilmStrip() && !inFullScreen()) { + // The good timing to step to the next view is when everything is + // not in + // transition. + return; + } + int nearest = findTheNearestView(mCenterX); + // no change made. + if (nearest == -1 || nearest == mCurrentInfo) + return; + + int adjust = nearest - mCurrentInfo; + if (adjust > 0) { + for (int k = 0; k < adjust; k++) { + removeInfo(k); + } + for (int k = 0; k + adjust < BUFFER_SIZE; k++) { + mViewInfo[k] = mViewInfo[k + adjust]; + } + for (int k = BUFFER_SIZE - adjust; k < BUFFER_SIZE; k++) { + mViewInfo[k] = null; + if (mViewInfo[k - 1] != null) { + mViewInfo[k] = buildInfoFromData(mViewInfo[k - 1].getID() + 1); + } + } + } else { + for (int k = BUFFER_SIZE - 1; k >= BUFFER_SIZE + adjust; k--) { + removeInfo(k); + } + for (int k = BUFFER_SIZE - 1; k + adjust >= 0; k--) { + mViewInfo[k] = mViewInfo[k + adjust]; + } + for (int k = -1 - adjust; k >= 0; k--) { + mViewInfo[k] = null; + if (mViewInfo[k + 1] != null) { + mViewInfo[k] = buildInfoFromData(mViewInfo[k + 1].getID() - 1); + } + } + } + } + + /** Don't go beyond the bound. */ + private void clampCenterX() { + ViewInfo curr = mViewInfo[mCurrentInfo]; + if (curr == null) { + return; + } + + if (curr.getID() == 0 && mCenterX < curr.getCenterX()) { + mCenterX = curr.getCenterX(); + if (mController.isScrolling()) { + mController.stopScrolling(); + } + if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW + && !mController.isScalling() + && mScale != FULLSCREEN_SCALE) { + mController.gotoFullScreen(); + } + } + if (curr.getID() == mDataAdapter.getTotalNumber() - 1 + && mCenterX > curr.getCenterX()) { + mCenterX = curr.getCenterX(); + if (!mController.isScrolling()) { + mController.stopScrolling(); + } + } + } + + private void adjustChildZOrder() { + for (int i = BUFFER_SIZE - 1; i >= 0; i--) { + if (mViewInfo[i] == null) + continue; + bringChildToFront(mViewInfo[i].getView()); + } + } + + /** + * If the current photo is a photo sphere, this will launch the Photo Sphere + * panorama viewer. + */ + private void showPhotoSphere() { + ViewInfo curr = mViewInfo[mCurrentInfo]; + if (curr != null) { + mDataAdapter.getImageData(curr.getID()).viewPhotoSphere(mPanoramaViewHelper); + } + } + + /** + * @return The ID of the current item, or -1. + */ + private int getCurrentId() { + ViewInfo current = mViewInfo[mCurrentInfo]; + if (current == null) { + return -1; + } + return current.getID(); + } + + /** + * Updates the visibility of the View Photo Sphere button. + */ + private void updatePhotoSphereViewButton() { + if (mViewPhotoSphereButton == null) { + mViewPhotoSphereButton = (ImageButton) ((View) getParent()) + .findViewById(R.id.filmstrip_bottom_control_panorama); + mViewPhotoSphereButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + showPhotoSphere(); + } + }); + } + final int requestId = getCurrentId(); + + // Check if the item has changed since the last time we updated the + // visibility status. Only then check of the current image is a photo + // sphere. + if (requestId == mLastItemId || requestId < 0) { + return; + } + + ImageData data = mDataAdapter.getImageData(requestId); + data.isPhotoSphere(mContext, new PanoramaSupportCallback() { + @Override + public void panoramaInfoAvailable(final boolean isPanorama, + boolean isPanorama360) { + // Make sure the returned data is for the current image. + if (requestId == getCurrentId()) { + mViewPhotoSphereButton.post(new Runnable() { + @Override + public void run() { + mViewPhotoSphereButton.setVisibility(isPanorama ? View.VISIBLE + : View.GONE); + } + }); + } + } + }); + } + + private void layoutChildren() { + if (mAnchorPending) { + mCenterX = mViewInfo[mCurrentInfo].getCenterX(); + mAnchorPending = false; + } + + if (mController.hasNewGeometry()) { + mCenterX = mController.getNewPosition(); + mScale = mController.getNewScale(); + } + + clampCenterX(); + + mViewInfo[mCurrentInfo].layoutIn(mDrawArea, mCenterX, mScale); + + int currentViewLeft = mViewInfo[mCurrentInfo].getLeftPosition(); + int currentViewCenter = mViewInfo[mCurrentInfo].getCenterX(); + int fullScreenWidth = mDrawArea.width() + mViewGap; + float scaleFraction = mViewAnimInterpolator.getInterpolation( + (mScale - FILM_STRIP_SCALE) / (FULLSCREEN_SCALE - FILM_STRIP_SCALE)); + + // images on the left + for (int infoID = mCurrentInfo - 1; infoID >= 0; infoID--) { + ViewInfo curr = mViewInfo[infoID]; + if (curr == null) { + continue; + } + + ViewInfo next = mViewInfo[infoID + 1]; + int myLeft = + next.getLeftPosition() - curr.getView().getMeasuredWidth() - mViewGap; + curr.setLeftPosition(myLeft); + curr.layoutIn(mDrawArea, mCenterX, mScale); + curr.getView().setAlpha(1f); + int infoDiff = mCurrentInfo - infoID; + curr.setTranslationX( + (currentViewCenter + - fullScreenWidth * infoDiff - curr.getCenterX()) * scaleFraction, + mScale); + } + + // images on the right + for (int infoID = mCurrentInfo + 1; infoID < BUFFER_SIZE; infoID++) { + ViewInfo curr = mViewInfo[infoID]; + if (curr == null) { + continue; + } + + ViewInfo prev = mViewInfo[infoID - 1]; + int myLeft = + prev.getLeftPosition() + prev.getView().getMeasuredWidth() + mViewGap; + curr.setLeftPosition(myLeft); + curr.layoutIn(mDrawArea, mCenterX, mScale); + if (infoID == mCurrentInfo + 1) { + curr.getView().setAlpha(1f - scaleFraction); + } else { + if (scaleFraction == 0f) { + curr.getView().setAlpha(1f); + } else { + curr.getView().setAlpha(0f); + } + } + curr.setTranslationX((currentViewLeft - myLeft) * scaleFraction, mScale); + } + + stepIfNeeded(); + adjustChildZOrder(); + invalidate(); + updatePhotoSphereViewButton(); + mLastItemId = getCurrentId(); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + if (mViewInfo[mCurrentInfo] == null) { + return; + } + + mDrawArea.left = l; + mDrawArea.top = t; + mDrawArea.right = r; + mDrawArea.bottom = b; + + layoutChildren(); + } + + // Keeps the view in the view hierarchy if it's camera preview. + // Remove from the hierarchy otherwise. + private void checkForRemoval(ImageData data, View v) { + if (data.getType() != ImageData.TYPE_CAMERA_PREVIEW) { + removeView(v); + data.recycle(); + } else { + v.setVisibility(View.INVISIBLE); + if (mCameraView != null && mCameraView != v) { + removeView(mCameraView); + } + mCameraView = v; + } + } + + private void slideViewBack(View v) { + v.animate().translationX(0) + .alpha(1f) + .setDuration(DURATION_GEOMETRY_ADJUST) + .setInterpolator(mViewAnimInterpolator) + .start(); + } + + private void updateRemoval(int dataID, final ImageData data) { + int removedInfo = findInfoByDataID(dataID); + + // adjust the data id to be consistent + for (int i = 0; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] == null || mViewInfo[i].getID() <= dataID) { + continue; + } + mViewInfo[i].setID(mViewInfo[i].getID() - 1); + } + if (removedInfo == -1) { + return; + } + + final View removedView = mViewInfo[removedInfo].getView(); + final int offsetX = removedView.getMeasuredWidth() + mViewGap; + + for (int i = removedInfo + 1; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] != null) { + mViewInfo[i].setLeftPosition(mViewInfo[i].getLeftPosition() - offsetX); + } + } + + if (removedInfo >= mCurrentInfo + && mViewInfo[removedInfo].getID() < mDataAdapter.getTotalNumber()) { + // Fill the removed info by left shift when the current one or + // anyone on the right is removed, and there's more data on the + // right available. + for (int i = removedInfo; i < BUFFER_SIZE - 1; i++) { + mViewInfo[i] = mViewInfo[i + 1]; + } + + // pull data out from the DataAdapter for the last one. + int curr = BUFFER_SIZE - 1; + int prev = curr - 1; + if (mViewInfo[prev] != null) { + mViewInfo[curr] = buildInfoFromData(mViewInfo[prev].getID() + 1); + } + + // Translate the views to their original places. + for (int i = removedInfo; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] != null) { + mViewInfo[i].setTranslationX(offsetX, mScale); + } + } + + // The end of the filmstrip might have been changed. + // The mCenterX might be out of the bound. + ViewInfo currInfo = mViewInfo[mCurrentInfo]; + if (currInfo.getID() == mDataAdapter.getTotalNumber() - 1 + && mCenterX > currInfo.getCenterX()) { + int adjustDiff = currInfo.getCenterX() - mCenterX; + mCenterX = currInfo.getCenterX(); + for (int i = 0; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] != null) { + mViewInfo[i].translateXBy(adjustDiff, mScale); + } + } + } + } else { + // fill the removed place by right shift + mCenterX -= offsetX; + + for (int i = removedInfo; i > 0; i--) { + mViewInfo[i] = mViewInfo[i - 1]; + } + + // pull data out from the DataAdapter for the first one. + int curr = 0; + int next = curr + 1; + if (mViewInfo[next] != null) { + mViewInfo[curr] = buildInfoFromData(mViewInfo[next].getID() - 1); + } + + // Translate the views to their original places. + for (int i = removedInfo; i >= 0; i--) { + if (mViewInfo[i] != null) { + mViewInfo[i].setTranslationX(-offsetX, mScale); + } + } + } + + // Now, slide every one back. + for (int i = 0; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] != null + && mViewInfo[i].getTranslationX(mScale) != 0f) { + slideViewBack(mViewInfo[i].getView()); + } + } + + int transY = getHeight() / 8; + if (removedView.getTranslationY() < 0) { + transY = -transY; + } + removedView.animate() + .alpha(0f) + .translationYBy(transY) + .setInterpolator(mViewAnimInterpolator) + .setDuration(DURATION_GEOMETRY_ADJUST) + .withEndAction(new Runnable() { + @Override + public void run() { + checkForRemoval(data, removedView); + } + }) + .start(); + layoutChildren(); + } + + // returns -1 on failure. + private int findInfoByDataID(int dataID) { + for (int i = 0; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] != null + && mViewInfo[i].getID() == dataID) { + return i; + } + } + return -1; + } + + private void updateInsertion(int dataID) { + int insertedInfo = findInfoByDataID(dataID); + if (insertedInfo == -1) { + // Not in the current info buffers. Check if it's inserted + // at the end. + if (dataID == mDataAdapter.getTotalNumber() - 1) { + int prev = findInfoByDataID(dataID - 1); + if (prev >= 0 && prev < BUFFER_SIZE - 1) { + // The previous data is in the buffer and we still + // have room for the inserted data. + insertedInfo = prev + 1; + } + } + } + + // adjust the data id to be consistent + for (int i = 0; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] == null || mViewInfo[i].getID() < dataID) { + continue; + } + mViewInfo[i].setID(mViewInfo[i].getID() + 1); + } + if (insertedInfo == -1) { + return; + } + + final ImageData data = mDataAdapter.getImageData(dataID); + int[] dim = calculateChildDimension( + data.getWidth(), data.getHeight(), + getMeasuredWidth(), getMeasuredHeight()); + final int offsetX = dim[0] + mViewGap; + ViewInfo viewInfo = buildInfoFromData(dataID); + + if (insertedInfo >= mCurrentInfo) { + if (insertedInfo == mCurrentInfo) { + viewInfo.setLeftPosition(mViewInfo[mCurrentInfo].getLeftPosition()); + } + // Shift right to make rooms for newly inserted item. + removeInfo(BUFFER_SIZE - 1); + for (int i = BUFFER_SIZE - 1; i > insertedInfo; i--) { + mViewInfo[i] = mViewInfo[i - 1]; + if (mViewInfo[i] != null) { + mViewInfo[i].setTranslationX(-offsetX, mScale); + slideViewBack(mViewInfo[i].getView()); + } + } + } else { + // Shift left. Put the inserted data on the left instead of the + // found position. + --insertedInfo; + if (insertedInfo < 0) { + return; + } + removeInfo(0); + for (int i = 1; i <= insertedInfo; i++) { + if (mViewInfo[i] != null) { + mViewInfo[i].setTranslationX(offsetX, mScale); + slideViewBack(mViewInfo[i].getView()); + mViewInfo[i - 1] = mViewInfo[i]; + } + } + } + + mViewInfo[insertedInfo] = viewInfo; + View insertedView = mViewInfo[insertedInfo].getView(); + insertedView.setAlpha(0f); + insertedView.setTranslationY(getHeight() / 8); + insertedView.animate() + .alpha(1f) + .translationY(0f) + .setInterpolator(mViewAnimInterpolator) + .setDuration(DURATION_GEOMETRY_ADJUST) + .start(); + invalidate(); + } + + public void setDataAdapter(DataAdapter adapter) { + mDataAdapter = adapter; + mDataAdapter.suggestViewSizeBound(getMeasuredWidth(), getMeasuredHeight()); + mDataAdapter.setListener(new DataAdapter.Listener() { + @Override + public void onDataLoaded() { + reload(); + } + + @Override + public void onDataUpdated(DataAdapter.UpdateReporter reporter) { + update(reporter); + } + + @Override + public void onDataInserted(int dataID, ImageData data) { + if (mViewInfo[mCurrentInfo] == null) { + // empty now, simply do a reload. + reload(); + return; + } + updateInsertion(dataID); + } + + @Override + public void onDataRemoved(int dataID, ImageData data) { + updateRemoval(dataID, data); + } + }); + } + + public boolean inFilmStrip() { + return (mScale == FILM_STRIP_SCALE); + } + + public boolean inFullScreen() { + return (mScale == FULLSCREEN_SCALE); + } + + public boolean inCameraFullscreen() { + return isAnchoredTo(0) && inFullScreen() + && (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (inFilmStrip()) { + return true; + } + + if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { + mCheckToIntercept = true; + mDown = MotionEvent.obtain(ev); + ViewInfo viewInfo = mViewInfo[mCurrentInfo]; + // Do not intercept touch if swipe is not enabled + if (viewInfo != null && !mDataAdapter.canSwipeInFullScreen(viewInfo.getID())) { + mCheckToIntercept = false; + } + return false; + } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { + // Do not intercept touch once child is in zoom mode + mCheckToIntercept = false; + return false; + } else { + if (!mCheckToIntercept) { + return false; + } + if (ev.getEventTime() - ev.getDownTime() > SWIPE_TIME_OUT) { + return false; + } + int deltaX = (int) (ev.getX() - mDown.getX()); + int deltaY = (int) (ev.getY() - mDown.getY()); + if (ev.getActionMasked() == MotionEvent.ACTION_MOVE + && deltaX < mSlop * (-1)) { + // intercept left swipe + if (Math.abs(deltaX) >= Math.abs(deltaY) * 2) { + return true; + } + } + } + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + mGestureRecognizer.onTouchEvent(ev); + return true; + } + + private void updateViewInfo(int infoID) { + ViewInfo info = mViewInfo[infoID]; + removeView(info.getView()); + mViewInfo[infoID] = buildInfoFromData(info.getID()); + } + + /** Some of the data is changed. */ + private void update(DataAdapter.UpdateReporter reporter) { + // No data yet. + if (mViewInfo[mCurrentInfo] == null) { + reload(); + return; + } + + // Check the current one. + ViewInfo curr = mViewInfo[mCurrentInfo]; + int dataID = curr.getID(); + if (reporter.isDataRemoved(dataID)) { + mCenterX = -1; + reload(); + return; + } + if (reporter.isDataUpdated(dataID)) { + updateViewInfo(mCurrentInfo); + } + + // Check left + for (int i = mCurrentInfo - 1; i >= 0; i--) { + curr = mViewInfo[i]; + if (curr != null) { + dataID = curr.getID(); + if (reporter.isDataRemoved(dataID) || reporter.isDataUpdated(dataID)) { + updateViewInfo(i); + } + } else { + ViewInfo next = mViewInfo[i + 1]; + if (next != null) { + mViewInfo[i] = buildInfoFromData(next.getID() - 1); + } + } + } + + // Check right + for (int i = mCurrentInfo + 1; i < BUFFER_SIZE; i++) { + curr = mViewInfo[i]; + if (curr != null) { + dataID = curr.getID(); + if (reporter.isDataRemoved(dataID) || reporter.isDataUpdated(dataID)) { + updateViewInfo(i); + } + } else { + ViewInfo prev = mViewInfo[i - 1]; + if (prev != null) { + mViewInfo[i] = buildInfoFromData(prev.getID() + 1); + } + } + } + } + + /** + * The whole data might be totally different. Flush all and load from the + * start. + */ + private void reload() { + removeAllViews(); + int dataNumber = mDataAdapter.getTotalNumber(); + if (dataNumber == 0) { + return; + } + + mViewInfo[mCurrentInfo] = buildInfoFromData(0); + mViewInfo[mCurrentInfo].setLeftPosition(0); + if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW) { + // we are in camera mode by default. + mController.lockAtCurrentView(); + } + for (int i = 1; mCurrentInfo + i < BUFFER_SIZE || mCurrentInfo - i >= 0; i++) { + int infoID = mCurrentInfo + i; + if (infoID < BUFFER_SIZE && mViewInfo[infoID - 1] != null) { + mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID - 1].getID() + 1); + } + infoID = mCurrentInfo - i; + if (infoID >= 0 && mViewInfo[infoID + 1] != null) { + mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID + 1].getID() - 1); + } + } + layoutChildren(); + } + + private void promoteData(int infoID, int dataID) { + if (mListener != null) { + mListener.onDataPromoted(dataID); + } + } + + private void demoteData(int infoID, int dataID) { + if (mListener != null) { + mListener.onDataDemoted(dataID); + } + } + + /** + * MyController controls all the geometry animations. It passively tells the + * geometry information on demand. + */ + private class MyController implements + Controller, + ValueAnimator.AnimatorUpdateListener, + Animator.AnimatorListener { + + private ValueAnimator mScaleAnimator; + private boolean mHasNewScale; + private float mNewScale; + + private Scroller mScroller; + private boolean mHasNewPosition; + private DecelerateInterpolator mDecelerateInterpolator; + + private boolean mCanStopScroll; + + private boolean mIsPositionLocked; + private int mLockedViewInfo; + + MyController(Context context) { + mScroller = new Scroller(context); + mHasNewPosition = false; + mScaleAnimator = new ValueAnimator(); + mScaleAnimator.addUpdateListener(MyController.this); + mScaleAnimator.addListener(MyController.this); + mDecelerateInterpolator = new DecelerateInterpolator(); + mCanStopScroll = true; + mHasNewScale = false; + } + + @Override + public boolean isScrolling() { + return !mScroller.isFinished(); + } + + @Override + public boolean isScalling() { + return mScaleAnimator.isRunning(); + } + + boolean hasNewGeometry() { + mHasNewPosition = mScroller.computeScrollOffset(); + if (!mHasNewPosition) { + mCanStopScroll = true; + } + // If the position is locked, then we always return true to force + // the position value to use the locked value. + return (mHasNewPosition || mHasNewScale || mIsPositionLocked); + } + + /** + * Always call {@link #hasNewGeometry()} before getting the new scale + * value. + */ + float getNewScale() { + if (!mHasNewScale) { + return mScale; + } + mHasNewScale = false; + return mNewScale; + } + + /** + * Always call {@link #hasNewGeometry()} before getting the new position + * value. + */ + int getNewPosition() { + if (mIsPositionLocked) { + if (mViewInfo[mLockedViewInfo] == null) + return mCenterX; + return mViewInfo[mLockedViewInfo].getCenterX(); + } + if (!mHasNewPosition) + return mCenterX; + return mScroller.getCurrX(); + } + + @Override + public void lockAtCurrentView() { + mIsPositionLocked = true; + mLockedViewInfo = mCurrentInfo; + } + + @Override + public void unlockPosition() { + if (mIsPositionLocked) { + // only when the position is previously locked we set the + // current position to make it consistent. + if (mViewInfo[mLockedViewInfo] != null) { + mCenterX = mViewInfo[mLockedViewInfo].getCenterX(); + } + mIsPositionLocked = false; + } + } + + private int estimateMinX(int dataID, int leftPos, int viewWidth) { + return leftPos - (dataID + 100) * (viewWidth + mViewGap); + } + + private int estimateMaxX(int dataID, int leftPos, int viewWidth) { + return leftPos + + (mDataAdapter.getTotalNumber() - dataID + 100) + * (viewWidth + mViewGap); + } + + @Override + public void scroll(float deltaX) { + if (mController.isScrolling()) { + return; + } + mCenterX += deltaX; + } + + @Override + public void fling(float velocityX) { + if (!stopScrolling() || mIsPositionLocked) { + return; + } + ViewInfo info = mViewInfo[mCurrentInfo]; + if (info == null) { + return; + } + + float scaledVelocityX = velocityX / mScale; + if (inCameraFullscreen() && scaledVelocityX < 0) { + // Swipe left in camera preview. + gotoFilmStrip(); + } + + int w = getWidth(); + // Estimation of possible length on the left. To ensure the + // velocity doesn't become too slow eventually, we add a huge number + // to the estimated maximum. + int minX = estimateMinX(info.getID(), info.getLeftPosition(), w); + // Estimation of possible length on the right. Likewise, exaggerate + // the possible maximum too. + int maxX = estimateMaxX(info.getID(), info.getLeftPosition(), w); + mScroller.fling(mCenterX, 0, (int) -velocityX, 0, minX, maxX, 0, 0); + + layoutChildren(); + } + + @Override + public boolean stopScrolling() { + if (!mCanStopScroll) + return false; + mScroller.forceFinished(true); + mHasNewPosition = false; + return true; + } + + private void stopScale() { + mScaleAnimator.cancel(); + mHasNewScale = false; + } + + @Override + public void scrollTo(int position, int duration, boolean interruptible) { + if (!stopScrolling() || mIsPositionLocked) + return; + mCanStopScroll = interruptible; + stopScrolling(); + mScroller.startScroll(mCenterX, 0, position - mCenterX, + 0, duration); + invalidate(); + } + + private void scaleTo(float scale, int duration) { + stopScale(); + mScaleAnimator.setDuration(duration); + mScaleAnimator.setFloatValues(mScale, scale); + mScaleAnimator.setInterpolator(mDecelerateInterpolator); + mScaleAnimator.start(); + mHasNewScale = true; + layoutChildren(); + } + + @Override + public void gotoFilmStrip() { + unlockPosition(); + scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST); + if (mListener != null) { + mListener.onSwitchMode(false); + } + } + + @Override + public void gotoFullScreen() { + if (mViewInfo[mCurrentInfo] != null) { + mController.scrollTo(mViewInfo[mCurrentInfo].getCenterX(), + DURATION_GEOMETRY_ADJUST, false); + } + enterFullScreen(); + } + + private void enterFullScreen() { + if (mListener != null) { + // TODO: After full size images snapping to fill the screen at + // the end of a scroll/fling is implemented, we should only make + // this call when the view on the center of the screen is + // camera preview + mListener.onSwitchMode(true); + } + if (inFullScreen()) { + return; + } + scaleTo(1f, DURATION_GEOMETRY_ADJUST); + } + + @Override + public void gotoCameraFullScreen() { + if (mDataAdapter.getImageData(0).getType() + != ImageData.TYPE_CAMERA_PREVIEW) { + return; + } + gotoFullScreen(); + scrollTo( + estimateMinX(mViewInfo[mCurrentInfo].getID(), + mViewInfo[mCurrentInfo].getLeftPosition(), + getWidth()), + DURATION_GEOMETRY_ADJUST, false); + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mHasNewScale = true; + mNewScale = (Float) animation.getAnimatedValue(); + layoutChildren(); + } + + @Override + public void onAnimationStart(Animator anim) { + } + + @Override + public void onAnimationEnd(Animator anim) { + ViewInfo info = mViewInfo[mCurrentInfo]; + if (info != null && mCenterX == info.getCenterX()) { + if (inFullScreen()) { + lockAtCurrentView(); + } else if (inFilmStrip()) { + unlockPosition(); + } + } + } + + @Override + public void onAnimationCancel(Animator anim) { + } + + @Override + public void onAnimationRepeat(Animator anim) { + } + } + + private class MyGestureReceiver implements FilmStripGestureRecognizer.Listener { + // Indicating the current trend of scaling is up (>1) or down (<1). + private float mScaleTrend; + + @Override + public boolean onSingleTapUp(float x, float y) { + if (inFilmStrip()) { + for (int i = 0; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] == null) { + continue; + } + + if (mViewInfo[i].areaContains(x, y)) { + mController.scrollTo(mViewInfo[i].getCenterX(), + DURATION_GEOMETRY_ADJUST, false); + return true; + } + } + } + return false; + } + + @Override + public boolean onDoubleTap(float x, float y) { + if (inFilmStrip()) { + ViewInfo centerInfo = mViewInfo[mCurrentInfo]; + if (centerInfo != null && centerInfo.areaContains(x, y)) { + mController.gotoFullScreen(); + return true; + } + } else if (inFullScreen()) { + mController.gotoFilmStrip(); + return true; + } + return false; + } + + @Override + public boolean onDown(float x, float y) { + if (mController.isScrolling()) { + mController.stopScrolling(); + } + return true; + } + + @Override + public boolean onUp(float x, float y) { + float halfH = getHeight() / 2; + for (int i = 0; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] == null) { + continue; + } + float transY = mViewInfo[i].getTranslationY(mScale); + if (transY == 0) { + continue; + } + int id = mViewInfo[i].getID(); + + if (mDataAdapter.getImageData(id) + .isUIActionSupported(ImageData.ACTION_DEMOTE) + && transY > halfH) { + demoteData(i, id); + } else if (mDataAdapter.getImageData(id) + .isUIActionSupported(ImageData.ACTION_PROMOTE) + && transY < -halfH) { + promoteData(i, id); + } else { + // put the view back. + mViewInfo[i].getView().animate() + .translationY(0f) + .alpha(1f) + .setDuration(DURATION_GEOMETRY_ADJUST) + .start(); + } + } + return false; + } + + @Override + public boolean onScroll(float x, float y, float dx, float dy) { + int deltaX = (int) (dx / mScale); + if (inFilmStrip()) { + if (Math.abs(dx) > Math.abs(dy)) { + if (deltaX > 0 && inCameraFullscreen()) { + mController.gotoFilmStrip(); + } + mController.scroll(deltaX); + } else { + // Vertical part. Promote or demote. + // int scaledDeltaY = (int) (dy * mScale); + int hit = 0; + Rect hitRect = new Rect(); + for (; hit < BUFFER_SIZE; hit++) { + if (mViewInfo[hit] == null) { + continue; + } + mViewInfo[hit].getView().getHitRect(hitRect); + if (hitRect.contains((int) x, (int) y)) { + break; + } + } + if (hit == BUFFER_SIZE) { + return false; + } + + ImageData data = mDataAdapter.getImageData(mViewInfo[hit].getID()); + float transY = mViewInfo[hit].getTranslationY(mScale) - dy / mScale; + if (!data.isUIActionSupported(ImageData.ACTION_DEMOTE) && transY > 0f) { + transY = 0f; + } + if (!data.isUIActionSupported(ImageData.ACTION_PROMOTE) && transY < 0f) { + transY = 0f; + } + mViewInfo[hit].setTranslationY(transY, mScale); + } + } else if (inFullScreen()) { + if (deltaX > 0 && inCameraFullscreen()) { + mController.gotoFilmStrip(); + } + mController.scroll(deltaX); + } + layoutChildren(); + + return true; + } + + @Override + public boolean onFling(float velocityX, float velocityY) { + if (Math.abs(velocityX) > Math.abs(velocityY)) { + mController.fling(velocityX); + } else { + // ignore vertical fling. + } + return true; + } + + @Override + public boolean onScaleBegin(float focusX, float focusY) { + if (inCameraFullscreen()) { + return false; + } + mScaleTrend = 1f; + return true; + } + + @Override + public boolean onScale(float focusX, float focusY, float scale) { + if (inCameraFullscreen()) { + return false; + } + + mScaleTrend = mScaleTrend * 0.3f + scale * 0.7f; + mScale *= scale; + if (mScale <= FILM_STRIP_SCALE) { + mScale = FILM_STRIP_SCALE; + } + if (mScale >= FULLSCREEN_SCALE) { + mScale = FULLSCREEN_SCALE; + } + layoutChildren(); + return true; + } + + @Override + public void onScaleEnd() { + if (mScaleTrend >= 1f) { + mController.gotoFullScreen(); + } else { + mController.gotoFilmStrip(); + } + mScaleTrend = 1f; + } + } +} diff --git a/src/com/android/camera/ui/FocusIndicator.java b/src/com/android/camera/ui/FocusIndicator.java new file mode 100644 index 000000000..e06057041 --- /dev/null +++ b/src/com/android/camera/ui/FocusIndicator.java @@ -0,0 +1,24 @@ +/* + * 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.ui; + +public interface FocusIndicator { + public void showStart(); + public void showSuccess(boolean timeout); + public void showFail(boolean timeout); + public void clear(); +} diff --git a/src/com/android/camera/ui/InLineSettingCheckBox.java b/src/com/android/camera/ui/InLineSettingCheckBox.java new file mode 100644 index 000000000..c1aa5a91c --- /dev/null +++ b/src/com/android/camera/ui/InLineSettingCheckBox.java @@ -0,0 +1,83 @@ +/* + * 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.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; + + +import com.android.camera.ListPreference; +import com.android.gallery3d.R; + +/* A check box setting control which turns on/off the setting. */ +public class InLineSettingCheckBox extends InLineSettingItem { + private CheckBox mCheckBox; + + OnCheckedChangeListener mCheckedChangeListener = new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean desiredState) { + changeIndex(desiredState ? 1 : 0); + } + }; + + public InLineSettingCheckBox(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mCheckBox = (CheckBox) findViewById(R.id.setting_check_box); + mCheckBox.setOnCheckedChangeListener(mCheckedChangeListener); + } + + @Override + public void initialize(ListPreference preference) { + super.initialize(preference); + // Add content descriptions for the increment and decrement buttons. + mCheckBox.setContentDescription(getContext().getResources().getString( + R.string.accessibility_check_box, mPreference.getTitle())); + } + + @Override + protected void updateView() { + mCheckBox.setOnCheckedChangeListener(null); + if (mOverrideValue == null) { + mCheckBox.setChecked(mIndex == 1); + } else { + int index = mPreference.findIndexOfValue(mOverrideValue); + mCheckBox.setChecked(index == 1); + } + mCheckBox.setOnCheckedChangeListener(mCheckedChangeListener); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + event.getText().add(mPreference.getTitle()); + return true; + } + + @Override + public void setEnabled(boolean enable) { + if (mTitle != null) mTitle.setEnabled(enable); + if (mCheckBox != null) mCheckBox.setEnabled(enable); + } +} diff --git a/src/com/android/camera/ui/InLineSettingItem.java b/src/com/android/camera/ui/InLineSettingItem.java new file mode 100644 index 000000000..839a77fd0 --- /dev/null +++ b/src/com/android/camera/ui/InLineSettingItem.java @@ -0,0 +1,94 @@ +/* + * 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.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.camera.ListPreference; +import com.android.gallery3d.R; + +/** + * A one-line camera setting could be one of three types: knob, switch or restore + * preference button. The setting includes a title for showing the preference + * title which is initialized in the SimpleAdapter. A knob also includes + * (ex: Picture size), a previous button, the current value (ex: 5MP), + * and a next button. A switch, i.e. the preference RecordLocationPreference, + * has only two values on and off which will be controlled in a switch button. + * Other setting popup window includes several InLineSettingItem items with + * different types if possible. + */ +public abstract class InLineSettingItem extends LinearLayout { + private Listener mListener; + protected ListPreference mPreference; + protected int mIndex; + // Scene mode can override the original preference value. + protected String mOverrideValue; + protected TextView mTitle; + + static public interface Listener { + public void onSettingChanged(ListPreference pref); + } + + public InLineSettingItem(Context context, AttributeSet attrs) { + super(context, attrs); + } + + protected void setTitle(ListPreference preference) { + mTitle = ((TextView) findViewById(R.id.title)); + mTitle.setText(preference.getTitle()); + } + + public void initialize(ListPreference preference) { + setTitle(preference); + if (preference == null) return; + mPreference = preference; + reloadPreference(); + } + + protected abstract void updateView(); + + protected boolean changeIndex(int index) { + if (index >= mPreference.getEntryValues().length || index < 0) return false; + mIndex = index; + mPreference.setValueIndex(mIndex); + if (mListener != null) { + mListener.onSettingChanged(mPreference); + } + updateView(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + return true; + } + + // The value of the preference may have changed. Update the UI. + public void reloadPreference() { + mIndex = mPreference.findIndexOfValue(mPreference.getValue()); + updateView(); + } + + public void setSettingChangedListener(Listener listener) { + mListener = listener; + } + + public void overrideSettings(String value) { + mOverrideValue = value; + updateView(); + } +} diff --git a/src/com/android/camera/ui/InLineSettingMenu.java b/src/com/android/camera/ui/InLineSettingMenu.java new file mode 100644 index 000000000..8e45c3e38 --- /dev/null +++ b/src/com/android/camera/ui/InLineSettingMenu.java @@ -0,0 +1,78 @@ +/* + * 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.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.accessibility.AccessibilityEvent; +import android.widget.TextView; + +import com.android.camera.ListPreference; +import com.android.gallery3d.R; + +/* Setting menu item that will bring up a menu when you click on it. */ +public class InLineSettingMenu extends InLineSettingItem { + private static final String TAG = "InLineSettingMenu"; + // The view that shows the current selected setting. Ex: 5MP + private TextView mEntry; + + public InLineSettingMenu(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mEntry = (TextView) findViewById(R.id.current_setting); + } + + @Override + public void initialize(ListPreference preference) { + super.initialize(preference); + //TODO: add contentDescription + } + + @Override + protected void updateView() { + if (mOverrideValue == null) { + mEntry.setText(mPreference.getEntry()); + } else { + int index = mPreference.findIndexOfValue(mOverrideValue); + if (index != -1) { + mEntry.setText(mPreference.getEntries()[index]); + } else { + // Avoid the crash if camera driver has bugs. + Log.e(TAG, "Fail to find override value=" + mOverrideValue); + mPreference.print(); + } + } + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + event.getText().add(mPreference.getTitle() + mPreference.getEntry()); + return true; + } + + @Override + public void setEnabled(boolean enable) { + super.setEnabled(enable); + if (mTitle != null) mTitle.setEnabled(enable); + if (mEntry != null) mEntry.setEnabled(enable); + } +} diff --git a/src/com/android/camera/ui/LayoutChangeHelper.java b/src/com/android/camera/ui/LayoutChangeHelper.java new file mode 100644 index 000000000..ef4eb6a7a --- /dev/null +++ b/src/com/android/camera/ui/LayoutChangeHelper.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.view.View; + +public class LayoutChangeHelper implements LayoutChangeNotifier { + private LayoutChangeNotifier.Listener mListener; + private boolean mFirstTimeLayout; + private View mView; + + public LayoutChangeHelper(View v) { + mView = v; + mFirstTimeLayout = true; + } + + @Override + public void setOnLayoutChangeListener(LayoutChangeNotifier.Listener listener) { + mListener = listener; + } + + public void onLayout(boolean changed, int l, int t, int r, int b) { + if (mListener == null) return; + if (mFirstTimeLayout || changed) { + mFirstTimeLayout = false; + mListener.onLayoutChange(mView, l, t, r, b); + } + } +} diff --git a/src/com/android/camera/ui/LayoutChangeNotifier.java b/src/com/android/camera/ui/LayoutChangeNotifier.java new file mode 100644 index 000000000..6261d34f6 --- /dev/null +++ b/src/com/android/camera/ui/LayoutChangeNotifier.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.view.View; + +public interface LayoutChangeNotifier { + public interface Listener { + // Invoked only when the layout has changed or it is the first layout. + public void onLayoutChange(View v, int l, int t, int r, int b); + } + + public void setOnLayoutChangeListener(LayoutChangeNotifier.Listener listener); +} diff --git a/src/com/android/camera/ui/LayoutNotifyView.java b/src/com/android/camera/ui/LayoutNotifyView.java new file mode 100644 index 000000000..6e118fc3a --- /dev/null +++ b/src/com/android/camera/ui/LayoutNotifyView.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; + +/* + * Customized view to support onLayoutChange() at or before API 10. + */ +public class LayoutNotifyView extends View implements LayoutChangeNotifier { + private LayoutChangeHelper mLayoutChangeHelper = new LayoutChangeHelper(this); + + public LayoutNotifyView(Context context) { + super(context); + } + + public LayoutNotifyView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void setOnLayoutChangeListener( + LayoutChangeNotifier.Listener listener) { + mLayoutChangeHelper.setOnLayoutChangeListener(listener); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mLayoutChangeHelper.onLayout(changed, l, t, r, b); + } +} diff --git a/src/com/android/camera/ui/ListPrefSettingPopup.java b/src/com/android/camera/ui/ListPrefSettingPopup.java new file mode 100644 index 000000000..cfef73f49 --- /dev/null +++ b/src/com/android/camera/ui/ListPrefSettingPopup.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2010 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.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.ListView; +import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.SimpleAdapter; + +import com.android.camera.IconListPreference; +import com.android.camera.ListPreference; +import com.android.gallery3d.R; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +// A popup window that shows one camera setting. The title is the name of the +// setting (ex: white-balance). The entries are the supported values (ex: +// daylight, incandescent, etc). If initialized with an IconListPreference, +// the entries will contain both text and icons. Otherwise, entries will be +// shown in text. +public class ListPrefSettingPopup extends AbstractSettingPopup implements + AdapterView.OnItemClickListener { + private static final String TAG = "ListPrefSettingPopup"; + private ListPreference mPreference; + private Listener mListener; + + static public interface Listener { + public void onListPrefChanged(ListPreference pref); + } + + public ListPrefSettingPopup(Context context, AttributeSet attrs) { + super(context, attrs); + } + + private class ListPrefSettingAdapter extends SimpleAdapter { + ListPrefSettingAdapter(Context context, List<? extends Map<String, ?>> data, + int resource, String[] from, int[] to) { + super(context, data, resource, from, to); + } + + @Override + public void setViewImage(ImageView v, String value) { + if ("".equals(value)) { + // Some settings have no icons. Ex: exposure compensation. + v.setVisibility(View.GONE); + } else { + super.setViewImage(v, value); + } + } + } + + public void initialize(ListPreference preference) { + mPreference = preference; + Context context = getContext(); + CharSequence[] entries = mPreference.getEntries(); + int[] iconIds = null; + if (preference instanceof IconListPreference) { + iconIds = ((IconListPreference) mPreference).getImageIds(); + if (iconIds == null) { + iconIds = ((IconListPreference) mPreference).getLargeIconIds(); + } + } + // Set title. + mTitle.setText(mPreference.getTitle()); + + // Prepare the ListView. + ArrayList<HashMap<String, Object>> listItem = + new ArrayList<HashMap<String, Object>>(); + for(int i = 0; i < entries.length; ++i) { + HashMap<String, Object> map = new HashMap<String, Object>(); + map.put("text", entries[i].toString()); + if (iconIds != null) map.put("image", iconIds[i]); + listItem.add(map); + } + SimpleAdapter listItemAdapter = new ListPrefSettingAdapter(context, listItem, + R.layout.setting_item, + new String[] {"text", "image"}, + new int[] {R.id.text, R.id.image}); + ((ListView) mSettingList).setAdapter(listItemAdapter); + ((ListView) mSettingList).setOnItemClickListener(this); + reloadPreference(); + } + + // The value of the preference may have changed. Update the UI. + @Override + public void reloadPreference() { + int index = mPreference.findIndexOfValue(mPreference.getValue()); + if (index != -1) { + ((ListView) mSettingList).setItemChecked(index, true); + } else { + Log.e(TAG, "Invalid preference value."); + mPreference.print(); + } + } + + public void setSettingChangedListener(Listener listener) { + mListener = listener; + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, + int index, long id) { + mPreference.setValueIndex(index); + if (mListener != null) mListener.onListPrefChanged(mPreference); + } +} diff --git a/src/com/android/camera/ui/MoreSettingPopup.java b/src/com/android/camera/ui/MoreSettingPopup.java new file mode 100644 index 000000000..5900058df --- /dev/null +++ b/src/com/android/camera/ui/MoreSettingPopup.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2010 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.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ListView; + +import com.android.camera.ListPreference; +import com.android.camera.PreferenceGroup; +import com.android.gallery3d.R; + +import java.util.ArrayList; + +/* A popup window that contains several camera settings. */ +public class MoreSettingPopup extends AbstractSettingPopup + implements InLineSettingItem.Listener, + AdapterView.OnItemClickListener { + @SuppressWarnings("unused") + private static final String TAG = "MoreSettingPopup"; + + private Listener mListener; + private ArrayList<ListPreference> mListItem = new ArrayList<ListPreference>(); + + // Keep track of which setting items are disabled + // e.g. White balance will be disabled when scene mode is set to non-auto + private boolean[] mEnabled; + + static public interface Listener { + public void onSettingChanged(ListPreference pref); + public void onPreferenceClicked(ListPreference pref); + } + + private class MoreSettingAdapter extends ArrayAdapter<ListPreference> { + LayoutInflater mInflater; + String mOnString; + String mOffString; + MoreSettingAdapter() { + super(MoreSettingPopup.this.getContext(), 0, mListItem); + Context context = getContext(); + mInflater = LayoutInflater.from(context); + mOnString = context.getString(R.string.setting_on); + mOffString = context.getString(R.string.setting_off); + } + + private int getSettingLayoutId(ListPreference pref) { + + if (isOnOffPreference(pref)) { + return R.layout.in_line_setting_check_box; + } + return R.layout.in_line_setting_menu; + } + + private boolean isOnOffPreference(ListPreference pref) { + CharSequence[] entries = pref.getEntries(); + if (entries.length != 2) return false; + String str1 = entries[0].toString(); + String str2 = entries[1].toString(); + return ((str1.equals(mOnString) && str2.equals(mOffString)) || + (str1.equals(mOffString) && str2.equals(mOnString))); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView != null) return convertView; + + ListPreference pref = mListItem.get(position); + + int viewLayoutId = getSettingLayoutId(pref); + InLineSettingItem view = (InLineSettingItem) + mInflater.inflate(viewLayoutId, parent, false); + + view.initialize(pref); // no init for restore one + view.setSettingChangedListener(MoreSettingPopup.this); + if (position >= 0 && position < mEnabled.length) { + view.setEnabled(mEnabled[position]); + } else { + Log.w(TAG, "Invalid input: enabled list length, " + mEnabled.length + + " position " + position); + } + return view; + } + + @Override + public boolean isEnabled(int position) { + if (position >= 0 && position < mEnabled.length) { + return mEnabled[position]; + } + return true; + } + } + + public void setSettingChangedListener(Listener listener) { + mListener = listener; + } + + public MoreSettingPopup(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void initialize(PreferenceGroup group, String[] keys) { + // Prepare the setting items. + for (int i = 0; i < keys.length; ++i) { + ListPreference pref = group.findPreference(keys[i]); + if (pref != null) mListItem.add(pref); + } + + ArrayAdapter<ListPreference> mListItemAdapter = new MoreSettingAdapter(); + ((ListView) mSettingList).setAdapter(mListItemAdapter); + ((ListView) mSettingList).setOnItemClickListener(this); + ((ListView) mSettingList).setSelector(android.R.color.transparent); + // Initialize mEnabled + mEnabled = new boolean[mListItem.size()]; + for (int i = 0; i < mEnabled.length; i++) { + mEnabled[i] = true; + } + } + + // When preferences are disabled, we will display them grayed out. Users + // will not be able to change the disabled preferences, but they can still see + // the current value of the preferences + public void setPreferenceEnabled(String key, boolean enable) { + int count = mEnabled == null ? 0 : mEnabled.length; + for (int j = 0; j < count; j++) { + ListPreference pref = mListItem.get(j); + if (pref != null && key.equals(pref.getKey())) { + mEnabled[j] = enable; + break; + } + } + } + + public void onSettingChanged(ListPreference pref) { + if (mListener != null) { + mListener.onSettingChanged(pref); + } + } + + // Scene mode can override other camera settings (ex: flash mode). + public void overrideSettings(final String ... keyvalues) { + int count = mEnabled == null ? 0 : mEnabled.length; + for (int i = 0; i < keyvalues.length; i += 2) { + String key = keyvalues[i]; + String value = keyvalues[i + 1]; + for (int j = 0; j < count; j++) { + ListPreference pref = mListItem.get(j); + if (pref != null && key.equals(pref.getKey())) { + // Change preference + if (value != null) pref.setValue(value); + // If the preference is overridden, disable the preference + boolean enable = value == null; + mEnabled[j] = enable; + if (mSettingList.getChildCount() > j) { + mSettingList.getChildAt(j).setEnabled(enable); + } + } + } + } + reloadPreference(); + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, + long id) { + if (mListener != null) { + ListPreference pref = mListItem.get(position); + mListener.onPreferenceClicked(pref); + } + } + + @Override + public void reloadPreference() { + int count = mSettingList.getChildCount(); + for (int i = 0; i < count; i++) { + ListPreference pref = mListItem.get(i); + if (pref != null) { + InLineSettingItem settingItem = + (InLineSettingItem) mSettingList.getChildAt(i); + settingItem.reloadPreference(); + } + } + } +} diff --git a/src/com/android/camera/ui/OnIndicatorEventListener.java b/src/com/android/camera/ui/OnIndicatorEventListener.java new file mode 100644 index 000000000..566f5c7a8 --- /dev/null +++ b/src/com/android/camera/ui/OnIndicatorEventListener.java @@ -0,0 +1,25 @@ +/* + * 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.ui; + +public interface OnIndicatorEventListener { + public static int EVENT_ENTER_SECOND_LEVEL_INDICATOR_BAR = 0; + public static int EVENT_LEAVE_SECOND_LEVEL_INDICATOR_BAR = 1; + public static int EVENT_ENTER_ZOOM_CONTROL = 2; + public static int EVENT_LEAVE_ZOOM_CONTROL = 3; + void onIndicatorEvent(int event); +} diff --git a/src/com/android/camera/ui/OverlayRenderer.java b/src/com/android/camera/ui/OverlayRenderer.java new file mode 100644 index 000000000..417e219aa --- /dev/null +++ b/src/com/android/camera/ui/OverlayRenderer.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.view.MotionEvent; + +public abstract class OverlayRenderer implements RenderOverlay.Renderer { + + private static final String TAG = "CAM OverlayRenderer"; + protected RenderOverlay mOverlay; + + protected int mLeft, mTop, mRight, mBottom; + + protected boolean mVisible; + + public void setVisible(boolean vis) { + mVisible = vis; + update(); + } + + public boolean isVisible() { + return mVisible; + } + + // default does not handle touch + @Override + public boolean handlesTouch() { + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent evt) { + return false; + } + + public abstract void onDraw(Canvas canvas); + + public void draw(Canvas canvas) { + if (mVisible) { + onDraw(canvas); + } + } + + @Override + public void setOverlay(RenderOverlay overlay) { + mOverlay = overlay; + } + + @Override + public void layout(int left, int top, int right, int bottom) { + mLeft = left; + mRight = right; + mTop = top; + mBottom = bottom; + } + + protected Context getContext() { + if (mOverlay != null) { + return mOverlay.getContext(); + } else { + return null; + } + } + + public int getWidth() { + return mRight - mLeft; + } + + public int getHeight() { + return mBottom - mTop; + } + + protected void update() { + if (mOverlay != null) { + mOverlay.update(); + } + } + +} diff --git a/src/com/android/camera/ui/PieItem.java b/src/com/android/camera/ui/PieItem.java new file mode 100644 index 000000000..47fe06758 --- /dev/null +++ b/src/com/android/camera/ui/PieItem.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.drawable.Drawable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Pie menu item + */ +public class PieItem { + + public static interface OnClickListener { + void onClick(PieItem item); + } + + private Drawable mDrawable; + private int level; + + private boolean mSelected; + private boolean mEnabled; + private List<PieItem> mItems; + private Path mPath; + private OnClickListener mOnClickListener; + private float mAlpha; + private CharSequence mLabel; + + // Gray out the view when disabled + private static final float ENABLED_ALPHA = 1; + private static final float DISABLED_ALPHA = (float) 0.3; + private boolean mChangeAlphaWhenDisabled = true; + + public PieItem(Drawable drawable, int level) { + mDrawable = drawable; + this.level = level; + if (drawable != null) { + setAlpha(1f); + } + mEnabled = true; + } + + public void setLabel(CharSequence txt) { + mLabel = txt; + } + + public CharSequence getLabel() { + return mLabel; + } + + public boolean hasItems() { + return mItems != null; + } + + public List<PieItem> getItems() { + return mItems; + } + + public void addItem(PieItem item) { + if (mItems == null) { + mItems = new ArrayList<PieItem>(); + } + mItems.add(item); + } + + public void clearItems() { + mItems = null; + } + + public void setLevel(int level) { + this.level = level; + } + + public void setPath(Path p) { + mPath = p; + } + + public Path getPath() { + return mPath; + } + + public void setChangeAlphaWhenDisabled (boolean enable) { + mChangeAlphaWhenDisabled = enable; + } + + public void setAlpha(float alpha) { + mAlpha = alpha; + mDrawable.setAlpha((int) (255 * alpha)); + } + + public void setEnabled(boolean enabled) { + mEnabled = enabled; + if (mChangeAlphaWhenDisabled) { + if (mEnabled) { + setAlpha(ENABLED_ALPHA); + } else { + setAlpha(DISABLED_ALPHA); + } + } + } + + public boolean isEnabled() { + return mEnabled; + } + + public void setSelected(boolean s) { + mSelected = s; + } + + public boolean isSelected() { + return mSelected; + } + + public int getLevel() { + return level; + } + + + public void setOnClickListener(OnClickListener listener) { + mOnClickListener = listener; + } + + public void performClick() { + if (mOnClickListener != null) { + mOnClickListener.onClick(this); + } + } + + public int getIntrinsicWidth() { + return mDrawable.getIntrinsicWidth(); + } + + public int getIntrinsicHeight() { + return mDrawable.getIntrinsicHeight(); + } + + public void setBounds(int left, int top, int right, int bottom) { + mDrawable.setBounds(left, top, right, bottom); + } + + public void draw(Canvas canvas) { + mDrawable.draw(canvas); + } + + public void setImageResource(Context context, int resId) { + Drawable d = context.getResources().getDrawable(resId).mutate(); + d.setBounds(mDrawable.getBounds()); + mDrawable = d; + setAlpha(mAlpha); + } + +} diff --git a/src/com/android/camera/ui/PieMenuButton.java b/src/com/android/camera/ui/PieMenuButton.java new file mode 100644 index 000000000..0e23226b2 --- /dev/null +++ b/src/com/android/camera/ui/PieMenuButton.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2013 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.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +public class PieMenuButton extends View { + private boolean mPressed; + private boolean mReadyToClick = false; + public PieMenuButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + mPressed = isPressed(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean handled = super.onTouchEvent(event); + if (MotionEvent.ACTION_UP == event.getAction() && mPressed) { + // Perform a customized click as soon as the ACTION_UP event + // is received. The reason for doing this is that Framework + // delays the performClick() call after ACTION_UP. But we do not + // want the delay because it affects an important state change + // for PieRenderer. + mReadyToClick = true; + performClick(); + } + return handled; + } + + @Override + public boolean performClick() { + if (mReadyToClick) { + // We only respond to our customized click which happens right + // after ACTION_UP event is received, with no delay. + mReadyToClick = false; + return super.performClick(); + } + return false; + } +};
\ No newline at end of file diff --git a/src/com/android/camera/ui/PieRenderer.java b/src/com/android/camera/ui/PieRenderer.java new file mode 100644 index 000000000..c78107ce9 --- /dev/null +++ b/src/com/android/camera/ui/PieRenderer.java @@ -0,0 +1,1091 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.RectF; +import android.os.Handler; +import android.os.Message; +import android.util.FloatMath; +import android.view.MotionEvent; +import android.view.ViewConfiguration; +import android.view.animation.Animation; +import android.view.animation.Transformation; + +import com.android.camera.drawable.TextDrawable; +import com.android.gallery3d.R; + +import java.util.ArrayList; +import java.util.List; + +public class PieRenderer extends OverlayRenderer + implements FocusIndicator { + + private static final String TAG = "CAM Pie"; + + // Sometimes continuous autofocus starts and stops several times quickly. + // These states are used to make sure the animation is run for at least some + // time. + private volatile int mState; + private ScaleAnimation mAnimation = new ScaleAnimation(); + private static final int STATE_IDLE = 0; + private static final int STATE_FOCUSING = 1; + private static final int STATE_FINISHING = 2; + private static final int STATE_PIE = 8; + + private static final float MATH_PI_2 = (float)(Math.PI / 2); + + private Runnable mDisappear = new Disappear(); + private Animation.AnimationListener mEndAction = new EndAction(); + private static final int SCALING_UP_TIME = 600; + private static final int SCALING_DOWN_TIME = 100; + private static final int DISAPPEAR_TIMEOUT = 200; + private static final int DIAL_HORIZONTAL = 157; + // fade out timings + private static final int PIE_FADE_OUT_DURATION = 600; + + private static final long PIE_FADE_IN_DURATION = 200; + private static final long PIE_XFADE_DURATION = 200; + private static final long PIE_SELECT_FADE_DURATION = 300; + private static final long PIE_OPEN_SUB_DELAY = 400; + private static final long PIE_SLICE_DURATION = 80; + + private static final int MSG_OPEN = 0; + private static final int MSG_CLOSE = 1; + private static final int MSG_OPENSUBMENU = 2; + + protected static float CENTER = (float) Math.PI / 2; + protected static float RAD24 = (float)(24 * Math.PI / 180); + protected static final float SWEEP_SLICE = 0.14f; + protected static final float SWEEP_ARC = 0.23f; + + // geometry + private int mRadius; + private int mRadiusInc; + + // the detection if touch is inside a slice is offset + // inbounds by this amount to allow the selection to show before the + // finger covers it + private int mTouchOffset; + + private List<PieItem> mOpen; + + private Paint mSelectedPaint; + private Paint mSubPaint; + private Paint mMenuArcPaint; + + // touch handling + private PieItem mCurrentItem; + + private Paint mFocusPaint; + private int mSuccessColor; + private int mFailColor; + private int mCircleSize; + private int mFocusX; + private int mFocusY; + private int mCenterX; + private int mCenterY; + private int mArcCenterY; + private int mSliceCenterY; + private int mPieCenterX; + private int mPieCenterY; + private int mSliceRadius; + private int mArcRadius; + private int mArcOffset; + + private int mDialAngle; + private RectF mCircle; + private RectF mDial; + private Point mPoint1; + private Point mPoint2; + private int mStartAnimationAngle; + private boolean mFocused; + private int mInnerOffset; + private int mOuterStroke; + private int mInnerStroke; + private boolean mTapMode; + private boolean mBlockFocus; + private int mTouchSlopSquared; + private Point mDown; + private boolean mOpening; + private ValueAnimator mXFade; + private ValueAnimator mFadeIn; + private ValueAnimator mFadeOut; + private ValueAnimator mSlice; + private volatile boolean mFocusCancelled; + private PointF mPolar = new PointF(); + private TextDrawable mLabel; + private int mDeadZone; + private int mAngleZone; + private float mCenterAngle; + + + + private Handler mHandler = new Handler() { + public void handleMessage(Message msg) { + switch(msg.what) { + case MSG_OPEN: + if (mListener != null) { + mListener.onPieOpened(mPieCenterX, mPieCenterY); + } + break; + case MSG_CLOSE: + if (mListener != null) { + mListener.onPieClosed(); + } + break; + case MSG_OPENSUBMENU: + onEnterOpen(); + break; + } + + } + }; + + private PieListener mListener; + + static public interface PieListener { + public void onPieOpened(int centerX, int centerY); + public void onPieClosed(); + } + + public void setPieListener(PieListener pl) { + mListener = pl; + } + + public PieRenderer(Context context) { + init(context); + } + + private void init(Context ctx) { + setVisible(false); + mOpen = new ArrayList<PieItem>(); + mOpen.add(new PieItem(null, 0)); + Resources res = ctx.getResources(); + mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start); + mRadiusInc = (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment); + mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset); + mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset); + mSelectedPaint = new Paint(); + mSelectedPaint.setColor(Color.argb(255, 51, 181, 229)); + mSelectedPaint.setAntiAlias(true); + mSubPaint = new Paint(); + mSubPaint.setAntiAlias(true); + mSubPaint.setColor(Color.argb(200, 250, 230, 128)); + mFocusPaint = new Paint(); + mFocusPaint.setAntiAlias(true); + mFocusPaint.setColor(Color.WHITE); + mFocusPaint.setStyle(Paint.Style.STROKE); + mSuccessColor = Color.GREEN; + mFailColor = Color.RED; + mCircle = new RectF(); + mDial = new RectF(); + mPoint1 = new Point(); + mPoint2 = new Point(); + mInnerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset); + mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke); + mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke); + mState = STATE_IDLE; + mBlockFocus = false; + mTouchSlopSquared = ViewConfiguration.get(ctx).getScaledTouchSlop(); + mTouchSlopSquared = mTouchSlopSquared * mTouchSlopSquared; + mDown = new Point(); + mMenuArcPaint = new Paint(); + mMenuArcPaint.setAntiAlias(true); + mMenuArcPaint.setColor(Color.argb(140, 255, 255, 255)); + mMenuArcPaint.setStrokeWidth(10); + mMenuArcPaint.setStyle(Paint.Style.STROKE); + mSliceRadius = res.getDimensionPixelSize(R.dimen.pie_item_radius); + mArcRadius = res.getDimensionPixelSize(R.dimen.pie_arc_radius); + mArcOffset = res.getDimensionPixelSize(R.dimen.pie_arc_offset); + mLabel = new TextDrawable(res); + mLabel.setDropShadow(true); + mDeadZone = res.getDimensionPixelSize(R.dimen.pie_deadzone_width); + mAngleZone = res.getDimensionPixelSize(R.dimen.pie_anglezone_width); + } + + private PieItem getRoot() { + return mOpen.get(0); + } + + public boolean showsItems() { + return mTapMode; + } + + public void addItem(PieItem item) { + // add the item to the pie itself + getRoot().addItem(item); + } + + public void clearItems() { + getRoot().clearItems(); + } + + public void showInCenter() { + if ((mState == STATE_PIE) && isVisible()) { + mTapMode = false; + show(false); + } else { + if (mState != STATE_IDLE) { + cancelFocus(); + } + mState = STATE_PIE; + resetPieCenter(); + setCenter(mPieCenterX, mPieCenterY); + mTapMode = true; + show(true); + } + } + + public void hide() { + show(false); + } + + /** + * guaranteed has center set + * @param show + */ + private void show(boolean show) { + if (show) { + if (mXFade != null) { + mXFade.cancel(); + } + mState = STATE_PIE; + // ensure clean state + mCurrentItem = null; + PieItem root = getRoot(); + for (PieItem openItem : mOpen) { + if (openItem.hasItems()) { + for (PieItem item : openItem.getItems()) { + item.setSelected(false); + } + } + } + mLabel.setText(""); + mOpen.clear(); + mOpen.add(root); + layoutPie(); + fadeIn(); + } else { + mState = STATE_IDLE; + mTapMode = false; + if (mXFade != null) { + mXFade.cancel(); + } + if (mLabel != null) { + mLabel.setText(""); + } + } + setVisible(show); + mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE); + } + + public boolean isOpen() { + return mState == STATE_PIE && isVisible(); + } + + private void fadeIn() { + mFadeIn = new ValueAnimator(); + mFadeIn.setFloatValues(0f, 1f); + mFadeIn.setDuration(PIE_FADE_IN_DURATION); + // linear interpolation + mFadeIn.setInterpolator(null); + mFadeIn.addListener(new AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + mFadeIn = null; + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + + @Override + public void onAnimationCancel(Animator arg0) { + } + }); + mFadeIn.start(); + } + + public void setCenter(int x, int y) { + mPieCenterX = x; + mPieCenterY = y; + mSliceCenterY = y + mSliceRadius - mArcOffset; + mArcCenterY = y - mArcOffset + mArcRadius; + } + + @Override + public void layout(int l, int t, int r, int b) { + super.layout(l, t, r, b); + mCenterX = (r - l) / 2; + mCenterY = (b - t) / 2; + + mFocusX = mCenterX; + mFocusY = mCenterY; + resetPieCenter(); + setCircle(mFocusX, mFocusY); + if (isVisible() && mState == STATE_PIE) { + setCenter(mPieCenterX, mPieCenterY); + layoutPie(); + } + } + + private void resetPieCenter() { + mPieCenterX = mCenterX; + mPieCenterY = (int) (getHeight() - 2.5f * mDeadZone); + } + + private void layoutPie() { + mCenterAngle = getCenterAngle(); + layoutItems(0, getRoot().getItems()); + layoutLabel(getLevel()); + } + + private void layoutLabel(int level) { + int x = mPieCenterX - (int) (FloatMath.sin(mCenterAngle - CENTER) + * (mArcRadius + (level + 2) * mRadiusInc)); + int y = mArcCenterY - mArcRadius - (level + 2) * mRadiusInc; + int w = mLabel.getIntrinsicWidth(); + int h = mLabel.getIntrinsicHeight(); + mLabel.setBounds(x - w/2, y - h/2, x + w/2, y + h/2); + } + + private void layoutItems(int level, List<PieItem> items) { + int extend = 1; + Path path = makeSlice(getDegrees(0) + extend, getDegrees(SWEEP_ARC) - extend, + mArcRadius, mArcRadius + mRadiusInc + mRadiusInc / 4, + mPieCenterX, mArcCenterY - level * mRadiusInc); + final int count = items.size(); + int pos = 0; + for (PieItem item : items) { + // shared between items + item.setPath(path); + float angle = getArcCenter(item, pos, count); + int w = item.getIntrinsicWidth(); + int h = item.getIntrinsicHeight(); + // move views to outer border + int r = mArcRadius + mRadiusInc * 2 / 3; + int x = (int) (r * Math.cos(angle)); + int y = mArcCenterY - (level * mRadiusInc) - (int) (r * Math.sin(angle)) - h / 2; + x = mPieCenterX + x - w / 2; + item.setBounds(x, y, x + w, y + h); + item.setLevel(level); + if (item.hasItems()) { + layoutItems(level + 1, item.getItems()); + } + pos++; + } + } + + private Path makeSlice(float start, float end, int inner, int outer, int cx, int cy) { + RectF bb = + new RectF(cx - outer, cy - outer, cx + outer, + cy + outer); + RectF bbi = + new RectF(cx - inner, cy - inner, cx + inner, + cy + inner); + Path path = new Path(); + path.arcTo(bb, start, end - start, true); + path.arcTo(bbi, end, start - end); + path.close(); + return path; + } + + private float getArcCenter(PieItem item, int pos, int count) { + return getCenter(pos, count, SWEEP_ARC); + } + + private float getSliceCenter(PieItem item, int pos, int count) { + float center = (getCenterAngle() - CENTER) * 0.5f + CENTER; + return center + (count - 1) * SWEEP_SLICE / 2f + - pos * SWEEP_SLICE; + } + + private float getCenter(int pos, int count, float sweep) { + return mCenterAngle + (count - 1) * sweep / 2f - pos * sweep; + } + + private float getCenterAngle() { + float center = CENTER; + if (mPieCenterX < mDeadZone + mAngleZone) { + center = CENTER - (mAngleZone - mPieCenterX + mDeadZone) * RAD24 + / (float) mAngleZone; + } else if (mPieCenterX > getWidth() - mDeadZone - mAngleZone) { + center = CENTER + (mPieCenterX - (getWidth() - mDeadZone - mAngleZone)) * RAD24 + / (float) mAngleZone; + } + return center; + } + + /** + * converts a + * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock) + * @return skia angle + */ + private float getDegrees(double angle) { + return (float) (360 - 180 * angle / Math.PI); + } + + private void startFadeOut(final PieItem item) { + if (mFadeIn != null) { + mFadeIn.cancel(); + } + if (mXFade != null) { + mXFade.cancel(); + } + mFadeOut = new ValueAnimator(); + mFadeOut.setFloatValues(1f, 0f); + mFadeOut.setDuration(PIE_FADE_OUT_DURATION); + mFadeOut.addListener(new AnimatorListener() { + @Override + public void onAnimationStart(Animator animator) { + } + + @Override + public void onAnimationEnd(Animator animator) { + item.performClick(); + mFadeOut = null; + deselect(); + show(false); + mOverlay.setAlpha(1); + } + + @Override + public void onAnimationRepeat(Animator animator) { + } + + @Override + public void onAnimationCancel(Animator animator) { + } + + }); + mFadeOut.start(); + } + + // root does not count + private boolean hasOpenItem() { + return mOpen.size() > 1; + } + + // pop an item of the open item stack + private PieItem closeOpenItem() { + PieItem item = getOpenItem(); + mOpen.remove(mOpen.size() -1); + return item; + } + + private PieItem getOpenItem() { + return mOpen.get(mOpen.size() - 1); + } + + // return the children either the root or parent of the current open item + private PieItem getParent() { + return mOpen.get(Math.max(0, mOpen.size() - 2)); + } + + private int getLevel() { + return mOpen.size() - 1; + } + + @Override + public void onDraw(Canvas canvas) { + float alpha = 1; + if (mXFade != null) { + alpha = (Float) mXFade.getAnimatedValue(); + } else if (mFadeIn != null) { + alpha = (Float) mFadeIn.getAnimatedValue(); + } else if (mFadeOut != null) { + alpha = (Float) mFadeOut.getAnimatedValue(); + } + int state = canvas.save(); + if (mFadeIn != null) { + float sf = 0.9f + alpha * 0.1f; + canvas.scale(sf, sf, mPieCenterX, mPieCenterY); + } + if (mState != STATE_PIE) { + drawFocus(canvas); + } + if (mState == STATE_FINISHING) { + canvas.restoreToCount(state); + return; + } + if (mState != STATE_PIE) return; + if (!hasOpenItem() || (mXFade != null)) { + // draw base menu + drawArc(canvas, getLevel(), getParent()); + List<PieItem> items = getParent().getItems(); + final int count = items.size(); + int pos = 0; + for (PieItem item : getParent().getItems()) { + drawItem(Math.max(0, mOpen.size() - 2), pos, count, canvas, item, alpha); + pos++; + } + mLabel.draw(canvas); + } + if (hasOpenItem()) { + int level = getLevel(); + drawArc(canvas, level, getOpenItem()); + List<PieItem> items = getOpenItem().getItems(); + final int count = items.size(); + int pos = 0; + for (PieItem inner : items) { + if (mFadeOut != null) { + drawItem(level, pos, count, canvas, inner, alpha); + } else { + drawItem(level, pos, count, canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1); + } + pos++; + } + mLabel.draw(canvas); + } + canvas.restoreToCount(state); + } + + private void drawArc(Canvas canvas, int level, PieItem item) { + // arc + if (mState == STATE_PIE) { + final int count = item.getItems().size(); + float start = mCenterAngle + (count * SWEEP_ARC / 2f); + float end = mCenterAngle - (count * SWEEP_ARC / 2f); + int cy = mArcCenterY - level * mRadiusInc; + canvas.drawArc(new RectF(mPieCenterX - mArcRadius, cy - mArcRadius, + mPieCenterX + mArcRadius, cy + mArcRadius), + getDegrees(end), getDegrees(start) - getDegrees(end), false, mMenuArcPaint); + } + } + + private void drawItem(int level, int pos, int count, Canvas canvas, PieItem item, float alpha) { + if (mState == STATE_PIE) { + if (item.getPath() != null) { + int y = mArcCenterY - level * mRadiusInc; + if (item.isSelected()) { + Paint p = mSelectedPaint; + int state = canvas.save(); + float angle = 0; + if (mSlice != null) { + angle = (Float) mSlice.getAnimatedValue(); + } else { + angle = getArcCenter(item, pos, count) - SWEEP_ARC / 2f; + } + angle = getDegrees(angle); + canvas.rotate(angle, mPieCenterX, y); + if (mFadeOut != null) { + p.setAlpha((int)(255 * alpha)); + } + canvas.drawPath(item.getPath(), p); + if (mFadeOut != null) { + p.setAlpha(255); + } + canvas.restoreToCount(state); + } + if (mFadeOut == null) { + alpha = alpha * (item.isEnabled() ? 1 : 0.3f); + // draw the item view + item.setAlpha(alpha); + } + item.draw(canvas); + } + } + } + + @Override + public boolean onTouchEvent(MotionEvent evt) { + float x = evt.getX(); + float y = evt.getY(); + int action = evt.getActionMasked(); + getPolar(x, y, !mTapMode, mPolar); + if (MotionEvent.ACTION_DOWN == action) { + if ((x < mDeadZone) || (x > getWidth() - mDeadZone)) { + return false; + } + mDown.x = (int) evt.getX(); + mDown.y = (int) evt.getY(); + mOpening = false; + if (mTapMode) { + PieItem item = findItem(mPolar); + if ((item != null) && (mCurrentItem != item)) { + mState = STATE_PIE; + onEnter(item); + } + } else { + setCenter((int) x, (int) y); + show(true); + } + return true; + } else if (MotionEvent.ACTION_UP == action) { + if (isVisible()) { + PieItem item = mCurrentItem; + if (mTapMode) { + item = findItem(mPolar); + if (mOpening) { + mOpening = false; + return true; + } + } + if (item == null) { + mTapMode = false; + show(false); + } else if (!mOpening && !item.hasItems()) { + startFadeOut(item); + mTapMode = false; + } else { + mTapMode = true; + } + return true; + } + } else if (MotionEvent.ACTION_CANCEL == action) { + if (isVisible() || mTapMode) { + show(false); + } + deselect(); + mHandler.removeMessages(MSG_OPENSUBMENU); + return false; + } else if (MotionEvent.ACTION_MOVE == action) { + if (pulledToCenter(mPolar)) { + mHandler.removeMessages(MSG_OPENSUBMENU); + if (hasOpenItem()) { + if (mCurrentItem != null) { + mCurrentItem.setSelected(false); + } + closeOpenItem(); + mCurrentItem = null; + } else { + deselect(); + } + mLabel.setText(""); + return false; + } + PieItem item = findItem(mPolar); + boolean moved = hasMoved(evt); + if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) { + mHandler.removeMessages(MSG_OPENSUBMENU); + // only select if we didn't just open or have moved past slop + if (moved) { + // switch back to swipe mode + mTapMode = false; + } + onEnterSelect(item); + mHandler.sendEmptyMessageDelayed(MSG_OPENSUBMENU, PIE_OPEN_SUB_DELAY); + } + } + return false; + } + + private boolean pulledToCenter(PointF polarCoords) { + return polarCoords.y < mArcRadius - mRadiusInc; + } + + private boolean inside(PointF polar, PieItem item, int pos, int count) { + float start = getSliceCenter(item, pos, count) - SWEEP_SLICE / 2f; + boolean res = (mArcRadius < polar.y) + && (start < polar.x) + && (start + SWEEP_SLICE > polar.x) + && (!mTapMode || (mArcRadius + mRadiusInc > polar.y)); + return res; + } + + private void getPolar(float x, float y, boolean useOffset, PointF res) { + // get angle and radius from x/y + res.x = (float) Math.PI / 2; + x = x - mPieCenterX; + float y1 = mSliceCenterY - getLevel() * mRadiusInc - y; + float y2 = mArcCenterY - getLevel() * mRadiusInc - y; + res.y = (float) Math.sqrt(x * x + y2 * y2); + if (x != 0) { + res.x = (float) Math.atan2(y1, x); + if (res.x < 0) { + res.x = (float) (2 * Math.PI + res.x); + } + } + res.y = res.y + (useOffset ? mTouchOffset : 0); + } + + private boolean hasMoved(MotionEvent e) { + return mTouchSlopSquared < (e.getX() - mDown.x) * (e.getX() - mDown.x) + + (e.getY() - mDown.y) * (e.getY() - mDown.y); + } + + private void onEnterSelect(PieItem item) { + if (mCurrentItem != null) { + mCurrentItem.setSelected(false); + } + if (item != null && item.isEnabled()) { + moveSelection(mCurrentItem, item); + item.setSelected(true); + mCurrentItem = item; + mLabel.setText(mCurrentItem.getLabel()); + layoutLabel(getLevel()); + } else { + mCurrentItem = null; + } + } + + private void onEnterOpen() { + if ((mCurrentItem != null) && (mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) { + openCurrentItem(); + } + } + + /** + * enter a slice for a view + * updates model only + * @param item + */ + private void onEnter(PieItem item) { + if (mCurrentItem != null) { + mCurrentItem.setSelected(false); + } + if (item != null && item.isEnabled()) { + item.setSelected(true); + mCurrentItem = item; + mLabel.setText(mCurrentItem.getLabel()); + if ((mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) { + openCurrentItem(); + layoutLabel(getLevel()); + } + } else { + mCurrentItem = null; + } + } + + private void deselect() { + if (mCurrentItem != null) { + mCurrentItem.setSelected(false); + } + if (hasOpenItem()) { + PieItem item = closeOpenItem(); + onEnter(item); + } else { + mCurrentItem = null; + } + } + + private int getItemPos(PieItem target) { + List<PieItem> items = getOpenItem().getItems(); + return items.indexOf(target); + } + + private int getCurrentCount() { + return getOpenItem().getItems().size(); + } + + private void moveSelection(PieItem from, PieItem to) { + final int count = getCurrentCount(); + final int fromPos = getItemPos(from); + final int toPos = getItemPos(to); + if (fromPos != -1 && toPos != -1) { + float startAngle = getArcCenter(from, getItemPos(from), count) + - SWEEP_ARC / 2f; + float endAngle = getArcCenter(to, getItemPos(to), count) + - SWEEP_ARC / 2f; + mSlice = new ValueAnimator(); + mSlice.setFloatValues(startAngle, endAngle); + // linear interpolater + mSlice.setInterpolator(null); + mSlice.setDuration(PIE_SLICE_DURATION); + mSlice.addListener(new AnimatorListener() { + @Override + public void onAnimationEnd(Animator arg0) { + mSlice = null; + } + + @Override + public void onAnimationRepeat(Animator arg0) { + } + + @Override + public void onAnimationStart(Animator arg0) { + } + + @Override + public void onAnimationCancel(Animator arg0) { + } + }); + mSlice.start(); + } + } + + private void openCurrentItem() { + if ((mCurrentItem != null) && mCurrentItem.hasItems()) { + mOpen.add(mCurrentItem); + layoutLabel(getLevel()); + mOpening = true; + if (mFadeIn != null) { + mFadeIn.cancel(); + } + mXFade = new ValueAnimator(); + mXFade.setFloatValues(1f, 0f); + mXFade.setDuration(PIE_XFADE_DURATION); + // Linear interpolation + mXFade.setInterpolator(null); + final PieItem ci = mCurrentItem; + mXFade.addListener(new AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + mXFade = null; + ci.setSelected(false); + mOpening = false; + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + + @Override + public void onAnimationCancel(Animator arg0) { + } + }); + mXFade.start(); + } + } + + /** + * @param polar x: angle, y: dist + * @return the item at angle/dist or null + */ + private PieItem findItem(PointF polar) { + // find the matching item: + List<PieItem> items = getOpenItem().getItems(); + final int count = items.size(); + int pos = 0; + for (PieItem item : items) { + if (inside(polar, item, pos, count)) { + return item; + } + pos++; + } + return null; + } + + + @Override + public boolean handlesTouch() { + return true; + } + + // focus specific code + + public void setBlockFocus(boolean blocked) { + mBlockFocus = blocked; + if (blocked) { + clear(); + } + } + + public void setFocus(int x, int y) { + mFocusX = x; + mFocusY = y; + setCircle(mFocusX, mFocusY); + } + + public void alignFocus(int x, int y) { + mOverlay.removeCallbacks(mDisappear); + mAnimation.cancel(); + mAnimation.reset(); + mFocusX = x; + mFocusY = y; + mDialAngle = DIAL_HORIZONTAL; + setCircle(x, y); + mFocused = false; + } + + public int getSize() { + return 2 * mCircleSize; + } + + private int getRandomRange() { + return (int)(-60 + 120 * Math.random()); + } + + private void setCircle(int cx, int cy) { + mCircle.set(cx - mCircleSize, cy - mCircleSize, + cx + mCircleSize, cy + mCircleSize); + mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset, + cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset); + } + + public void drawFocus(Canvas canvas) { + if (mBlockFocus) return; + mFocusPaint.setStrokeWidth(mOuterStroke); + canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint); + if (mState == STATE_PIE) return; + int color = mFocusPaint.getColor(); + if (mState == STATE_FINISHING) { + mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor); + } + mFocusPaint.setStrokeWidth(mInnerStroke); + drawLine(canvas, mDialAngle, mFocusPaint); + drawLine(canvas, mDialAngle + 45, mFocusPaint); + drawLine(canvas, mDialAngle + 180, mFocusPaint); + drawLine(canvas, mDialAngle + 225, mFocusPaint); + canvas.save(); + // rotate the arc instead of its offset to better use framework's shape caching + canvas.rotate(mDialAngle, mFocusX, mFocusY); + canvas.drawArc(mDial, 0, 45, false, mFocusPaint); + canvas.drawArc(mDial, 180, 45, false, mFocusPaint); + canvas.restore(); + mFocusPaint.setColor(color); + } + + private void drawLine(Canvas canvas, int angle, Paint p) { + convertCart(angle, mCircleSize - mInnerOffset, mPoint1); + convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2); + canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY, + mPoint2.x + mFocusX, mPoint2.y + mFocusY, p); + } + + private static void convertCart(int angle, int radius, Point out) { + double a = 2 * Math.PI * (angle % 360) / 360; + out.x = (int) (radius * Math.cos(a) + 0.5); + out.y = (int) (radius * Math.sin(a) + 0.5); + } + + @Override + public void showStart() { + if (mState == STATE_PIE) return; + cancelFocus(); + mStartAnimationAngle = 67; + int range = getRandomRange(); + startAnimation(SCALING_UP_TIME, + false, mStartAnimationAngle, mStartAnimationAngle + range); + mState = STATE_FOCUSING; + } + + @Override + public void showSuccess(boolean timeout) { + if (mState == STATE_FOCUSING) { + startAnimation(SCALING_DOWN_TIME, + timeout, mStartAnimationAngle); + mState = STATE_FINISHING; + mFocused = true; + } + } + + @Override + public void showFail(boolean timeout) { + if (mState == STATE_FOCUSING) { + startAnimation(SCALING_DOWN_TIME, + timeout, mStartAnimationAngle); + mState = STATE_FINISHING; + mFocused = false; + } + } + + private void cancelFocus() { + mFocusCancelled = true; + mOverlay.removeCallbacks(mDisappear); + if (mAnimation != null && !mAnimation.hasEnded()) { + mAnimation.cancel(); + } + mFocusCancelled = false; + mFocused = false; + mState = STATE_IDLE; + } + + @Override + public void clear() { + if (mState == STATE_PIE) return; + cancelFocus(); + mOverlay.post(mDisappear); + } + + private void startAnimation(long duration, boolean timeout, + float toScale) { + startAnimation(duration, timeout, mDialAngle, + toScale); + } + + private void startAnimation(long duration, boolean timeout, + float fromScale, float toScale) { + setVisible(true); + mAnimation.reset(); + mAnimation.setDuration(duration); + mAnimation.setScale(fromScale, toScale); + mAnimation.setAnimationListener(timeout ? mEndAction : null); + mOverlay.startAnimation(mAnimation); + update(); + } + + private class EndAction implements Animation.AnimationListener { + @Override + public void onAnimationEnd(Animation animation) { + // Keep the focus indicator for some time. + if (!mFocusCancelled) { + mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT); + } + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationStart(Animation animation) { + } + } + + private class Disappear implements Runnable { + @Override + public void run() { + if (mState == STATE_PIE) return; + setVisible(false); + mFocusX = mCenterX; + mFocusY = mCenterY; + mState = STATE_IDLE; + setCircle(mFocusX, mFocusY); + mFocused = false; + } + } + + private class ScaleAnimation extends Animation { + private float mFrom = 1f; + private float mTo = 1f; + + public ScaleAnimation() { + setFillAfter(true); + } + + public void setScale(float from, float to) { + mFrom = from; + mTo = to; + } + + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + mDialAngle = (int)(mFrom + (mTo - mFrom) * interpolatedTime); + } + } + +} diff --git a/src/com/android/camera/ui/PopupManager.java b/src/com/android/camera/ui/PopupManager.java new file mode 100644 index 000000000..0dcf34fd7 --- /dev/null +++ b/src/com/android/camera/ui/PopupManager.java @@ -0,0 +1,66 @@ +/* + * 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.ui; + +import android.content.Context; +import android.view.View; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * A manager which notifies the event of a new popup in order to dismiss the + * old popup if exists. + */ +public class PopupManager { + private static HashMap<Context, PopupManager> sMap = + new HashMap<Context, PopupManager>(); + + public interface OnOtherPopupShowedListener { + public void onOtherPopupShowed(); + } + + private PopupManager() {} + + private ArrayList<OnOtherPopupShowedListener> mListeners = new ArrayList<OnOtherPopupShowedListener>(); + + public void notifyShowPopup(View view) { + for (OnOtherPopupShowedListener listener : mListeners) { + if ((View) listener != view) { + listener.onOtherPopupShowed(); + } + } + } + + public void setOnOtherPopupShowedListener(OnOtherPopupShowedListener listener) { + mListeners.add(listener); + } + + public static PopupManager getInstance(Context context) { + PopupManager instance = sMap.get(context); + if (instance == null) { + instance = new PopupManager(); + sMap.put(context, instance); + } + return instance; + } + + public static void removeInstance(Context context) { + PopupManager instance = sMap.get(context); + sMap.remove(context); + } +} diff --git a/src/com/android/camera/ui/PreviewSurfaceView.java b/src/com/android/camera/ui/PreviewSurfaceView.java new file mode 100644 index 000000000..9a428e23c --- /dev/null +++ b/src/com/android/camera/ui/PreviewSurfaceView.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.ViewGroup; + +import com.android.gallery3d.common.ApiHelper; + +public class PreviewSurfaceView extends SurfaceView { + public PreviewSurfaceView(Context context, AttributeSet attrs) { + super(context, attrs); + setZOrderMediaOverlay(true); + getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + } + + public void shrink() { + setLayoutSize(1); + } + + public void expand() { + setLayoutSize(ViewGroup.LayoutParams.MATCH_PARENT); + } + + private void setLayoutSize(int size) { + ViewGroup.LayoutParams p = getLayoutParams(); + if (p.width != size || p.height != size) { + p.width = size; + p.height = size; + setLayoutParams(p); + } + } +} diff --git a/src/com/android/camera/ui/RenderOverlay.java b/src/com/android/camera/ui/RenderOverlay.java new file mode 100644 index 000000000..d82ce18b6 --- /dev/null +++ b/src/com/android/camera/ui/RenderOverlay.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.camera.PreviewGestures; + +import java.util.ArrayList; +import java.util.List; + +public class RenderOverlay extends FrameLayout { + + private static final String TAG = "CAM_Overlay"; + + interface Renderer { + + public boolean handlesTouch(); + public boolean onTouchEvent(MotionEvent evt); + public void setOverlay(RenderOverlay overlay); + public void layout(int left, int top, int right, int bottom); + public void draw(Canvas canvas); + + } + + private RenderView mRenderView; + private List<Renderer> mClients; + private PreviewGestures mGestures; + // reverse list of touch clients + private List<Renderer> mTouchClients; + private int[] mPosition = new int[2]; + + public RenderOverlay(Context context, AttributeSet attrs) { + super(context, attrs); + mRenderView = new RenderView(context); + addView(mRenderView, new LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT)); + mClients = new ArrayList<Renderer>(10); + mTouchClients = new ArrayList<Renderer>(10); + setWillNotDraw(false); + } + + public void setGestures(PreviewGestures gestures) { + mGestures = gestures; + } + + public void addRenderer(Renderer renderer) { + mClients.add(renderer); + renderer.setOverlay(this); + if (renderer.handlesTouch()) { + mTouchClients.add(0, renderer); + } + renderer.layout(getLeft(), getTop(), getRight(), getBottom()); + } + + public void addRenderer(int pos, Renderer renderer) { + mClients.add(pos, renderer); + renderer.setOverlay(this); + renderer.layout(getLeft(), getTop(), getRight(), getBottom()); + } + + public void remove(Renderer renderer) { + mClients.remove(renderer); + renderer.setOverlay(null); + } + + public int getClientSize() { + return mClients.size(); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent m) { + if (mGestures != null) { + if (!mGestures.isEnabled()) return false; + mGestures.dispatchTouch(m); + } + return true; + } + + public boolean directDispatchTouch(MotionEvent m, Renderer target) { + mRenderView.setTouchTarget(target); + boolean res = mRenderView.dispatchTouchEvent(m); + mRenderView.setTouchTarget(null); + return res; + } + + private void adjustPosition() { + getLocationInWindow(mPosition); + } + + public int getWindowPositionX() { + return mPosition[0]; + } + + public int getWindowPositionY() { + return mPosition[1]; + } + + public void update() { + mRenderView.invalidate(); + } + + private class RenderView extends View { + + private Renderer mTouchTarget; + + public RenderView(Context context) { + super(context); + setWillNotDraw(false); + } + + public void setTouchTarget(Renderer target) { + mTouchTarget = target; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent evt) { + + if (mTouchTarget != null) { + return mTouchTarget.onTouchEvent(evt); + } + if (mTouchClients != null) { + boolean res = false; + for (Renderer client : mTouchClients) { + res |= client.onTouchEvent(evt); + } + return res; + } + return false; + } + + @Override + public void onLayout(boolean changed, int left, int top, int right, int bottom) { + adjustPosition(); + super.onLayout(changed, left, top, right, bottom); + if (mClients == null) return; + for (Renderer renderer : mClients) { + renderer.layout(left, top, right, bottom); + } + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (mClients == null) return; + boolean redraw = false; + for (Renderer renderer : mClients) { + renderer.draw(canvas); + redraw = redraw || ((OverlayRenderer) renderer).isVisible(); + } + if (redraw) { + invalidate(); + } + } + } + +} diff --git a/src/com/android/camera/ui/Rotatable.java b/src/com/android/camera/ui/Rotatable.java new file mode 100644 index 000000000..6d428b8c6 --- /dev/null +++ b/src/com/android/camera/ui/Rotatable.java @@ -0,0 +1,22 @@ +/* + * 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.ui; + +public interface Rotatable { + // Set parameter 'animation' to true to have animation when rotation. + public void setOrientation(int orientation, boolean animation); +} diff --git a/src/com/android/camera/ui/RotatableLayout.java b/src/com/android/camera/ui/RotatableLayout.java new file mode 100644 index 000000000..965d62a90 --- /dev/null +++ b/src/com/android/camera/ui/RotatableLayout.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2013 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.ui; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import com.android.camera.Util; + +/* RotatableLayout rotates itself as well as all its children when orientation + * changes. Specifically, when going from portrait to landscape, camera + * controls move from the bottom of the screen to right side of the screen + * (i.e. counter clockwise). Similarly, when the screen changes to portrait, we + * need to move the controls from right side to the bottom of the screen, which + * is a clockwise rotation. + */ + +public class RotatableLayout extends FrameLayout { + + private static final String TAG = "RotatableLayout"; + // Initial orientation of the layout (ORIENTATION_PORTRAIT, or ORIENTATION_LANDSCAPE) + private int mInitialOrientation; + private int mPrevRotation; + private RotationListener mListener = null; + public interface RotationListener { + public void onRotation(int rotation); + } + public RotatableLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mInitialOrientation = getResources().getConfiguration().orientation; + } + + public RotatableLayout(Context context, AttributeSet attrs) { + super(context, attrs); + mInitialOrientation = getResources().getConfiguration().orientation; + } + + public RotatableLayout(Context context) { + super(context); + mInitialOrientation = getResources().getConfiguration().orientation; + } + + @Override + public void onAttachedToWindow() { + mPrevRotation = Util.getDisplayRotation((Activity) getContext()); + // check if there is any rotation before the view is attached to window + int currentOrientation = getResources().getConfiguration().orientation; + int orientation = getUnifiedRotation(); + if (mInitialOrientation == currentOrientation && orientation < 180) { + return; + } + + if (mInitialOrientation == Configuration.ORIENTATION_LANDSCAPE + && currentOrientation == Configuration.ORIENTATION_PORTRAIT) { + rotateLayout(true); + } else if (mInitialOrientation == Configuration.ORIENTATION_PORTRAIT + && currentOrientation == Configuration.ORIENTATION_LANDSCAPE) { + rotateLayout(false); + } + // In reverse landscape and reverse portrait, camera controls will be laid out + // on the wrong side of the screen. We need to make adjustment to move the controls + // to the USB side + if (orientation >= 180) { + flipChildren(); + } + } + + protected int getUnifiedRotation() { + // all the layout code assumes camera device orientation to be portrait + // adjust rotation for landscape + int orientation = getResources().getConfiguration().orientation; + int rotation = Util.getDisplayRotation((Activity) getContext()); + int camOrientation = (rotation % 180 == 0) ? Configuration.ORIENTATION_PORTRAIT + : Configuration.ORIENTATION_LANDSCAPE; + if (camOrientation != orientation) { + return (rotation + 90) % 360; + } + return rotation; + } + + public void checkLayoutFlip() { + int currentRotation = Util.getDisplayRotation((Activity) getContext()); + if ((currentRotation - mPrevRotation + 360) % 360 == 180) { + mPrevRotation = currentRotation; + flipChildren(); + getParent().requestLayout(); + } + } + + @Override + public void onWindowVisibilityChanged(int visibility) { + if (visibility == View.VISIBLE) { + // Make sure when coming back from onPause, the layout is rotated correctly + checkLayoutFlip(); + } + } + + @Override + public void onConfigurationChanged(Configuration config) { + super.onConfigurationChanged(config); + int rotation = Util.getDisplayRotation((Activity) getContext()); + int diff = (rotation - mPrevRotation + 360) % 360; + if ( diff == 0) { + // No rotation + return; + } else if (diff == 180) { + // 180-degree rotation + mPrevRotation = rotation; + flipChildren(); + return; + } + // 90 or 270-degree rotation + boolean clockwise = isClockWiseRotation(mPrevRotation, rotation); + mPrevRotation = rotation; + rotateLayout(clockwise); + } + + protected void rotateLayout(boolean clockwise) { + // Change the size of the layout + ViewGroup.LayoutParams lp = getLayoutParams(); + int width = lp.width; + int height = lp.height; + lp.height = width; + lp.width = height; + setLayoutParams(lp); + + // rotate all the children + rotateChildren(clockwise); + } + + protected void rotateChildren(boolean clockwise) { + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + rotate(child, clockwise); + } + if (mListener != null) mListener.onRotation(clockwise ? 90 : 270); + } + + protected void flipChildren() { + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + flip(child); + } + if (mListener != null) mListener.onRotation(180); + } + + public void setRotationListener(RotationListener listener) { + mListener = listener; + } + + public static boolean isClockWiseRotation(int prevRotation, int currentRotation) { + if (prevRotation == (currentRotation + 90) % 360) { + return true; + } + return false; + } + + public static void rotate(View view, boolean isClockwise) { + if (isClockwise) { + rotateClockwise(view); + } else { + rotateCounterClockwise(view); + } + } + + private static boolean contains(int value, int mask) { + return (value & mask) == mask; + } + + public static void rotateClockwise(View view) { + if (view == null) return; + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + int gravity = lp.gravity; + int ngravity = 0; + // rotate gravity + if (contains(gravity, Gravity.LEFT)) { + ngravity |= Gravity.TOP; + } + if (contains(gravity, Gravity.RIGHT)) { + ngravity |= Gravity.BOTTOM; + } + if (contains(gravity, Gravity.TOP)) { + ngravity |= Gravity.RIGHT; + } + if (contains(gravity, Gravity.BOTTOM)) { + ngravity |= Gravity.LEFT; + } + if (contains(gravity, Gravity.CENTER)) { + ngravity |= Gravity.CENTER; + } + if (contains(gravity, Gravity.CENTER_HORIZONTAL)) { + ngravity |= Gravity.CENTER_VERTICAL; + } + if (contains(gravity, Gravity.CENTER_VERTICAL)) { + ngravity |= Gravity.CENTER_HORIZONTAL; + } + lp.gravity = ngravity; + int ml = lp.leftMargin; + int mr = lp.rightMargin; + int mt = lp.topMargin; + int mb = lp.bottomMargin; + lp.leftMargin = mb; + lp.rightMargin = mt; + lp.topMargin = ml; + lp.bottomMargin = mr; + int width = lp.width; + int height = lp.height; + lp.width = height; + lp.height = width; + view.setLayoutParams(lp); + } + + public static void rotateCounterClockwise(View view) { + if (view == null) return; + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + int gravity = lp.gravity; + int ngravity = 0; + // change gravity + if (contains(gravity, Gravity.RIGHT)) { + ngravity |= Gravity.TOP; + } + if (contains(gravity, Gravity.LEFT)) { + ngravity |= Gravity.BOTTOM; + } + if (contains(gravity, Gravity.TOP)) { + ngravity |= Gravity.LEFT; + } + if (contains(gravity, Gravity.BOTTOM)) { + ngravity |= Gravity.RIGHT; + } + if (contains(gravity, Gravity.CENTER)) { + ngravity |= Gravity.CENTER; + } + if (contains(gravity, Gravity.CENTER_HORIZONTAL)) { + ngravity |= Gravity.CENTER_VERTICAL; + } + if (contains(gravity, Gravity.CENTER_VERTICAL)) { + ngravity |= Gravity.CENTER_HORIZONTAL; + } + lp.gravity = ngravity; + int ml = lp.leftMargin; + int mr = lp.rightMargin; + int mt = lp.topMargin; + int mb = lp.bottomMargin; + lp.leftMargin = mt; + lp.rightMargin = mb; + lp.topMargin = mr; + lp.bottomMargin = ml; + int width = lp.width; + int height = lp.height; + lp.width = height; + lp.height = width; + view.setLayoutParams(lp); + } + + // Rotate a given view 180 degrees + public static void flip(View view) { + rotateClockwise(view); + rotateClockwise(view); + } +}
\ No newline at end of file diff --git a/src/com/android/camera/ui/RotateImageView.java b/src/com/android/camera/ui/RotateImageView.java new file mode 100644 index 000000000..05e1a7c5b --- /dev/null +++ b/src/com/android/camera/ui/RotateImageView.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2009 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.ui; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; +import android.media.ThumbnailUtils; +import android.util.AttributeSet; +import android.view.ViewGroup.LayoutParams; +import android.view.animation.AnimationUtils; +import android.widget.ImageView; + +/** + * A @{code ImageView} which can rotate it's content. + */ +public class RotateImageView extends TwoStateImageView implements Rotatable { + + @SuppressWarnings("unused") + private static final String TAG = "RotateImageView"; + + private static final int ANIMATION_SPEED = 270; // 270 deg/sec + + private int mCurrentDegree = 0; // [0, 359] + private int mStartDegree = 0; + private int mTargetDegree = 0; + + private boolean mClockwise = false, mEnableAnimation = true; + + private long mAnimationStartTime = 0; + private long mAnimationEndTime = 0; + + public RotateImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public RotateImageView(Context context) { + super(context); + } + + protected int getDegree() { + return mTargetDegree; + } + + // Rotate the view counter-clockwise + @Override + public void setOrientation(int degree, boolean animation) { + mEnableAnimation = animation; + // make sure in the range of [0, 359] + degree = degree >= 0 ? degree % 360 : degree % 360 + 360; + if (degree == mTargetDegree) return; + + mTargetDegree = degree; + if (mEnableAnimation) { + mStartDegree = mCurrentDegree; + mAnimationStartTime = AnimationUtils.currentAnimationTimeMillis(); + + int diff = mTargetDegree - mCurrentDegree; + diff = diff >= 0 ? diff : 360 + diff; // make it in range [0, 359] + + // Make it in range [-179, 180]. That's the shorted distance between the + // two angles + diff = diff > 180 ? diff - 360 : diff; + + mClockwise = diff >= 0; + mAnimationEndTime = mAnimationStartTime + + Math.abs(diff) * 1000 / ANIMATION_SPEED; + } else { + mCurrentDegree = mTargetDegree; + } + + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + Drawable drawable = getDrawable(); + if (drawable == null) return; + + Rect bounds = drawable.getBounds(); + int w = bounds.right - bounds.left; + int h = bounds.bottom - bounds.top; + + if (w == 0 || h == 0) return; // nothing to draw + + if (mCurrentDegree != mTargetDegree) { + long time = AnimationUtils.currentAnimationTimeMillis(); + if (time < mAnimationEndTime) { + int deltaTime = (int)(time - mAnimationStartTime); + int degree = mStartDegree + ANIMATION_SPEED + * (mClockwise ? deltaTime : -deltaTime) / 1000; + degree = degree >= 0 ? degree % 360 : degree % 360 + 360; + mCurrentDegree = degree; + invalidate(); + } else { + mCurrentDegree = mTargetDegree; + } + } + + int left = getPaddingLeft(); + int top = getPaddingTop(); + int right = getPaddingRight(); + int bottom = getPaddingBottom(); + int width = getWidth() - left - right; + int height = getHeight() - top - bottom; + + int saveCount = canvas.getSaveCount(); + + // Scale down the image first if required. + if ((getScaleType() == ImageView.ScaleType.FIT_CENTER) && + ((width < w) || (height < h))) { + float ratio = Math.min((float) width / w, (float) height / h); + canvas.scale(ratio, ratio, width / 2.0f, height / 2.0f); + } + canvas.translate(left + width / 2, top + height / 2); + canvas.rotate(-mCurrentDegree); + canvas.translate(-w / 2, -h / 2); + drawable.draw(canvas); + canvas.restoreToCount(saveCount); + } + + private Bitmap mThumb; + private Drawable[] mThumbs; + private TransitionDrawable mThumbTransition; + + public void setBitmap(Bitmap bitmap) { + // Make sure uri and original are consistently both null or both + // non-null. + if (bitmap == null) { + mThumb = null; + mThumbs = null; + setImageDrawable(null); + setVisibility(GONE); + return; + } + + LayoutParams param = getLayoutParams(); + final int miniThumbWidth = param.width + - getPaddingLeft() - getPaddingRight(); + final int miniThumbHeight = param.height + - getPaddingTop() - getPaddingBottom(); + mThumb = ThumbnailUtils.extractThumbnail( + bitmap, miniThumbWidth, miniThumbHeight); + Drawable drawable; + if (mThumbs == null || !mEnableAnimation) { + mThumbs = new Drawable[2]; + mThumbs[1] = new BitmapDrawable(getContext().getResources(), mThumb); + setImageDrawable(mThumbs[1]); + } else { + mThumbs[0] = mThumbs[1]; + mThumbs[1] = new BitmapDrawable(getContext().getResources(), mThumb); + mThumbTransition = new TransitionDrawable(mThumbs); + setImageDrawable(mThumbTransition); + mThumbTransition.startTransition(500); + } + setVisibility(VISIBLE); + } +} diff --git a/src/com/android/camera/ui/RotateLayout.java b/src/com/android/camera/ui/RotateLayout.java new file mode 100644 index 000000000..86f5c814d --- /dev/null +++ b/src/com/android/camera/ui/RotateLayout.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2010 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.ui; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.util.MotionEventHelper; + +// A RotateLayout is designed to display a single item and provides the +// capabilities to rotate the item. +public class RotateLayout extends ViewGroup implements Rotatable { + @SuppressWarnings("unused") + private static final String TAG = "RotateLayout"; + private int mOrientation; + private Matrix mMatrix = new Matrix(); + protected View mChild; + + public RotateLayout(Context context, AttributeSet attrs) { + super(context, attrs); + // The transparent background here is a workaround of the render issue + // happened when the view is rotated as the device's orientation + // changed. The view looks fine in landscape. After rotation, the view + // is invisible. + setBackgroundResource(android.R.color.transparent); + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + @Override + protected void onFinishInflate() { + mChild = getChildAt(0); + if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) { + mChild.setPivotX(0); + mChild.setPivotY(0); + } + } + + @Override + protected void onLayout( + boolean change, int left, int top, int right, int bottom) { + int width = right - left; + int height = bottom - top; + switch (mOrientation) { + case 0: + case 180: + mChild.layout(0, 0, width, height); + break; + case 90: + case 270: + mChild.layout(0, 0, height, width); + break; + } + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + if (!ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) { + final int w = getMeasuredWidth(); + final int h = getMeasuredHeight(); + switch (mOrientation) { + case 0: + mMatrix.setTranslate(0, 0); + break; + case 90: + mMatrix.setTranslate(0, -h); + break; + case 180: + mMatrix.setTranslate(-w, -h); + break; + case 270: + mMatrix.setTranslate(-w, 0); + break; + } + mMatrix.postRotate(mOrientation); + event = MotionEventHelper.transformEvent(event, mMatrix); + } + return super.dispatchTouchEvent(event); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) { + super.dispatchDraw(canvas); + } else { + canvas.save(); + int w = getMeasuredWidth(); + int h = getMeasuredHeight(); + switch (mOrientation) { + case 0: + canvas.translate(0, 0); + break; + case 90: + canvas.translate(0, h); + break; + case 180: + canvas.translate(w, h); + break; + case 270: + canvas.translate(w, 0); + break; + } + canvas.rotate(-mOrientation, 0, 0); + super.dispatchDraw(canvas); + canvas.restore(); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + int w = 0, h = 0; + switch(mOrientation) { + case 0: + case 180: + measureChild(mChild, widthSpec, heightSpec); + w = mChild.getMeasuredWidth(); + h = mChild.getMeasuredHeight(); + break; + case 90: + case 270: + measureChild(mChild, heightSpec, widthSpec); + w = mChild.getMeasuredHeight(); + h = mChild.getMeasuredWidth(); + break; + } + setMeasuredDimension(w, h); + + if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) { + switch (mOrientation) { + case 0: + mChild.setTranslationX(0); + mChild.setTranslationY(0); + break; + case 90: + mChild.setTranslationX(0); + mChild.setTranslationY(h); + break; + case 180: + mChild.setTranslationX(w); + mChild.setTranslationY(h); + break; + case 270: + mChild.setTranslationX(w); + mChild.setTranslationY(0); + break; + } + mChild.setRotation(-mOrientation); + } + } + + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + + // Rotate the view counter-clockwise + @Override + public void setOrientation(int orientation, boolean animation) { + orientation = orientation % 360; + if (mOrientation == orientation) return; + mOrientation = orientation; + requestLayout(); + } + + public int getOrientation() { + return mOrientation; + } + + @Override + public ViewParent invalidateChildInParent(int[] location, Rect r) { + if (!ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES && mOrientation != 0) { + // The workaround invalidates the entire rotate layout. After + // rotation, the correct area to invalidate may be larger than the + // size of the child. Ex: ListView. There is no way to invalidate + // only the necessary area. + r.set(0, 0, getWidth(), getHeight()); + } + return super.invalidateChildInParent(location, r); + } +} diff --git a/src/com/android/camera/ui/RotateTextToast.java b/src/com/android/camera/ui/RotateTextToast.java new file mode 100644 index 000000000..c78a258b0 --- /dev/null +++ b/src/com/android/camera/ui/RotateTextToast.java @@ -0,0 +1,59 @@ +/* + * 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.ui; + +import android.app.Activity; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.android.camera.Util; +import com.android.gallery3d.R; + +public class RotateTextToast { + private static final int TOAST_DURATION = 5000; // milliseconds + ViewGroup mLayoutRoot; + RotateLayout mToast; + Handler mHandler; + + public RotateTextToast(Activity activity, int textResourceId, int orientation) { + mLayoutRoot = (ViewGroup) activity.getWindow().getDecorView(); + LayoutInflater inflater = activity.getLayoutInflater(); + View v = inflater.inflate(R.layout.rotate_text_toast, mLayoutRoot); + mToast = (RotateLayout) v.findViewById(R.id.rotate_toast); + TextView tv = (TextView) mToast.findViewById(R.id.message); + tv.setText(textResourceId); + mToast.setOrientation(orientation, false); + mHandler = new Handler(); + } + + private final Runnable mRunnable = new Runnable() { + @Override + public void run() { + Util.fadeOut(mToast); + mLayoutRoot.removeView(mToast); + mToast = null; + } + }; + + public void show() { + mToast.setVisibility(View.VISIBLE); + mHandler.postDelayed(mRunnable, TOAST_DURATION); + } +} diff --git a/src/com/android/camera/ui/Switch.java b/src/com/android/camera/ui/Switch.java new file mode 100644 index 000000000..ac21758a7 --- /dev/null +++ b/src/com/android/camera/ui/Switch.java @@ -0,0 +1,505 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.CompoundButton; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; + +import java.util.Arrays; + +/** + * A Switch is a two-state toggle switch widget that can select between two + * options. The user may drag the "thumb" back and forth to choose the selected option, + * or simply tap to toggle as if it were a checkbox. + */ +public class Switch extends CompoundButton { + private static final int TOUCH_MODE_IDLE = 0; + private static final int TOUCH_MODE_DOWN = 1; + private static final int TOUCH_MODE_DRAGGING = 2; + + private Drawable mThumbDrawable; + private Drawable mTrackDrawable; + private int mThumbTextPadding; + private int mSwitchMinWidth; + private int mSwitchTextMaxWidth; + private int mSwitchPadding; + private CharSequence mTextOn; + private CharSequence mTextOff; + + private int mTouchMode; + private int mTouchSlop; + private float mTouchX; + private float mTouchY; + private VelocityTracker mVelocityTracker = VelocityTracker.obtain(); + private int mMinFlingVelocity; + + private float mThumbPosition; + private int mSwitchWidth; + private int mSwitchHeight; + private int mThumbWidth; // Does not include padding + + private int mSwitchLeft; + private int mSwitchTop; + private int mSwitchRight; + private int mSwitchBottom; + + private TextPaint mTextPaint; + private ColorStateList mTextColors; + private Layout mOnLayout; + private Layout mOffLayout; + + @SuppressWarnings("hiding") + private final Rect mTempRect = new Rect(); + + private static final int[] CHECKED_STATE_SET = { + android.R.attr.state_checked + }; + + /** + * Construct a new Switch with default styling, overriding specific style + * attributes as requested. + * + * @param context The Context that will determine this widget's theming. + * @param attrs Specification of attributes that should deviate from default styling. + */ + public Switch(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.switchStyle); + } + + /** + * Construct a new Switch with a default style determined by the given theme attribute, + * overriding specific style attributes as requested. + * + * @param context The Context that will determine this widget's theming. + * @param attrs Specification of attributes that should deviate from the default styling. + * @param defStyle An attribute ID within the active theme containing a reference to the + * default style for this widget. e.g. android.R.attr.switchStyle. + */ + public Switch(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + Resources res = getResources(); + DisplayMetrics dm = res.getDisplayMetrics(); + mTextPaint.density = dm.density; + mThumbDrawable = res.getDrawable(R.drawable.switch_inner_holo_dark); + mTrackDrawable = res.getDrawable(R.drawable.switch_track_holo_dark); + mTextOn = res.getString(R.string.capital_on); + mTextOff = res.getString(R.string.capital_off); + mThumbTextPadding = res.getDimensionPixelSize(R.dimen.thumb_text_padding); + mSwitchMinWidth = res.getDimensionPixelSize(R.dimen.switch_min_width); + mSwitchTextMaxWidth = res.getDimensionPixelSize(R.dimen.switch_text_max_width); + mSwitchPadding = res.getDimensionPixelSize(R.dimen.switch_padding); + setSwitchTextAppearance(context, android.R.style.TextAppearance_Holo_Small); + + ViewConfiguration config = ViewConfiguration.get(context); + mTouchSlop = config.getScaledTouchSlop(); + mMinFlingVelocity = config.getScaledMinimumFlingVelocity(); + + // Refresh display with current params + refreshDrawableState(); + setChecked(isChecked()); + } + + /** + * Sets the switch text color, size, style, hint color, and highlight color + * from the specified TextAppearance resource. + */ + public void setSwitchTextAppearance(Context context, int resid) { + Resources res = getResources(); + mTextColors = getTextColors(); + int ts = res.getDimensionPixelSize(R.dimen.thumb_text_size); + if (ts != mTextPaint.getTextSize()) { + mTextPaint.setTextSize(ts); + requestLayout(); + } + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + if (mOnLayout == null) { + mOnLayout = makeLayout(mTextOn, mSwitchTextMaxWidth); + } + if (mOffLayout == null) { + mOffLayout = makeLayout(mTextOff, mSwitchTextMaxWidth); + } + + mTrackDrawable.getPadding(mTempRect); + final int maxTextWidth = Math.min(mSwitchTextMaxWidth, + Math.max(mOnLayout.getWidth(), mOffLayout.getWidth())); + final int switchWidth = Math.max(mSwitchMinWidth, + maxTextWidth * 2 + mThumbTextPadding * 4 + mTempRect.left + mTempRect.right); + final int switchHeight = mTrackDrawable.getIntrinsicHeight(); + + mThumbWidth = maxTextWidth + mThumbTextPadding * 2; + + mSwitchWidth = switchWidth; + mSwitchHeight = switchHeight; + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + final int measuredHeight = getMeasuredHeight(); + final int measuredWidth = getMeasuredWidth(); + if (measuredHeight < switchHeight) { + setMeasuredDimension(measuredWidth, switchHeight); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + @Override + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); + CharSequence text = isChecked() ? mOnLayout.getText() : mOffLayout.getText(); + if (!TextUtils.isEmpty(text)) { + event.getText().add(text); + } + } + + private Layout makeLayout(CharSequence text, int maxWidth) { + int actual_width = (int) Math.ceil(Layout.getDesiredWidth(text, mTextPaint)); + StaticLayout l = new StaticLayout(text, 0, text.length(), mTextPaint, + actual_width, + Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true, + TextUtils.TruncateAt.END, + (int) Math.min(actual_width, maxWidth)); + return l; + } + + /** + * @return true if (x, y) is within the target area of the switch thumb + */ + private boolean hitThumb(float x, float y) { + mThumbDrawable.getPadding(mTempRect); + final int thumbTop = mSwitchTop - mTouchSlop; + final int thumbLeft = mSwitchLeft + (int) (mThumbPosition + 0.5f) - mTouchSlop; + final int thumbRight = thumbLeft + mThumbWidth + + mTempRect.left + mTempRect.right + mTouchSlop; + final int thumbBottom = mSwitchBottom + mTouchSlop; + return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + mVelocityTracker.addMovement(ev); + final int action = ev.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + if (isEnabled() && hitThumb(x, y)) { + mTouchMode = TOUCH_MODE_DOWN; + mTouchX = x; + mTouchY = y; + } + break; + } + + case MotionEvent.ACTION_MOVE: { + switch (mTouchMode) { + case TOUCH_MODE_IDLE: + // Didn't target the thumb, treat normally. + break; + + case TOUCH_MODE_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + if (Math.abs(x - mTouchX) > mTouchSlop || + Math.abs(y - mTouchY) > mTouchSlop) { + mTouchMode = TOUCH_MODE_DRAGGING; + getParent().requestDisallowInterceptTouchEvent(true); + mTouchX = x; + mTouchY = y; + return true; + } + break; + } + + case TOUCH_MODE_DRAGGING: { + final float x = ev.getX(); + final float dx = x - mTouchX; + float newPos = Math.max(0, + Math.min(mThumbPosition + dx, getThumbScrollRange())); + if (newPos != mThumbPosition) { + mThumbPosition = newPos; + mTouchX = x; + invalidate(); + } + return true; + } + } + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + if (mTouchMode == TOUCH_MODE_DRAGGING) { + stopDrag(ev); + return true; + } + mTouchMode = TOUCH_MODE_IDLE; + mVelocityTracker.clear(); + break; + } + } + + return super.onTouchEvent(ev); + } + + private void cancelSuperTouch(MotionEvent ev) { + MotionEvent cancel = MotionEvent.obtain(ev); + cancel.setAction(MotionEvent.ACTION_CANCEL); + super.onTouchEvent(cancel); + cancel.recycle(); + } + + /** + * Called from onTouchEvent to end a drag operation. + * + * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL + */ + private void stopDrag(MotionEvent ev) { + mTouchMode = TOUCH_MODE_IDLE; + // Up and not canceled, also checks the switch has not been disabled during the drag + boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled(); + + cancelSuperTouch(ev); + + if (commitChange) { + boolean newState; + mVelocityTracker.computeCurrentVelocity(1000); + float xvel = mVelocityTracker.getXVelocity(); + if (Math.abs(xvel) > mMinFlingVelocity) { + newState = xvel > 0; + } else { + newState = getTargetCheckedState(); + } + animateThumbToCheckedState(newState); + } else { + animateThumbToCheckedState(isChecked()); + } + } + + private void animateThumbToCheckedState(boolean newCheckedState) { + setChecked(newCheckedState); + } + + private boolean getTargetCheckedState() { + return mThumbPosition >= getThumbScrollRange() / 2; + } + + private void setThumbPosition(boolean checked) { + mThumbPosition = checked ? getThumbScrollRange() : 0; + } + + @Override + public void setChecked(boolean checked) { + super.setChecked(checked); + setThumbPosition(checked); + invalidate(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + setThumbPosition(isChecked()); + + int switchRight; + int switchLeft; + + switchRight = getWidth() - getPaddingRight(); + switchLeft = switchRight - mSwitchWidth; + + int switchTop = 0; + int switchBottom = 0; + switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) { + default: + case Gravity.TOP: + switchTop = getPaddingTop(); + switchBottom = switchTop + mSwitchHeight; + break; + + case Gravity.CENTER_VERTICAL: + switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 - + mSwitchHeight / 2; + switchBottom = switchTop + mSwitchHeight; + break; + + case Gravity.BOTTOM: + switchBottom = getHeight() - getPaddingBottom(); + switchTop = switchBottom - mSwitchHeight; + break; + } + + mSwitchLeft = switchLeft; + mSwitchTop = switchTop; + mSwitchBottom = switchBottom; + mSwitchRight = switchRight; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Draw the switch + int switchLeft = mSwitchLeft; + int switchTop = mSwitchTop; + int switchRight = mSwitchRight; + int switchBottom = mSwitchBottom; + + mTrackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom); + mTrackDrawable.draw(canvas); + + canvas.save(); + + mTrackDrawable.getPadding(mTempRect); + int switchInnerLeft = switchLeft + mTempRect.left; + int switchInnerTop = switchTop + mTempRect.top; + int switchInnerRight = switchRight - mTempRect.right; + int switchInnerBottom = switchBottom - mTempRect.bottom; + canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom); + + mThumbDrawable.getPadding(mTempRect); + final int thumbPos = (int) (mThumbPosition + 0.5f); + int thumbLeft = switchInnerLeft - mTempRect.left + thumbPos; + int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + mTempRect.right; + + mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom); + mThumbDrawable.draw(canvas); + + // mTextColors should not be null, but just in case + if (mTextColors != null) { + mTextPaint.setColor(mTextColors.getColorForState(getDrawableState(), + mTextColors.getDefaultColor())); + } + mTextPaint.drawableState = getDrawableState(); + + Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout; + + canvas.translate((thumbLeft + thumbRight) / 2 - switchText.getEllipsizedWidth() / 2, + (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2); + switchText.draw(canvas); + + canvas.restore(); + } + + @Override + public int getCompoundPaddingRight() { + int padding = super.getCompoundPaddingRight() + mSwitchWidth; + if (!TextUtils.isEmpty(getText())) { + padding += mSwitchPadding; + } + return padding; + } + + private int getThumbScrollRange() { + if (mTrackDrawable == null) { + return 0; + } + mTrackDrawable.getPadding(mTempRect); + return mSwitchWidth - mThumbWidth - mTempRect.left - mTempRect.right; + } + + @Override + protected int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + + if (isChecked()) { + mergeDrawableStates(drawableState, CHECKED_STATE_SET); + } + return drawableState; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + + int[] myDrawableState = getDrawableState(); + + // Set the state of the Drawable + // Drawable may be null when checked state is set from XML, from super constructor + if (mThumbDrawable != null) mThumbDrawable.setState(myDrawableState); + if (mTrackDrawable != null) mTrackDrawable.setState(myDrawableState); + + invalidate(); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable; + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + @Override + public void jumpDrawablesToCurrentState() { + super.jumpDrawablesToCurrentState(); + mThumbDrawable.jumpToCurrentState(); + mTrackDrawable.jumpToCurrentState(); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(Switch.class.getName()); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(Switch.class.getName()); + CharSequence switchText = isChecked() ? mTextOn : mTextOff; + if (!TextUtils.isEmpty(switchText)) { + CharSequence oldText = info.getText(); + if (TextUtils.isEmpty(oldText)) { + info.setText(switchText); + } else { + StringBuilder newText = new StringBuilder(); + newText.append(oldText).append(' ').append(switchText); + info.setText(newText); + } + } + } +} diff --git a/src/com/android/camera/ui/TimeIntervalPopup.java b/src/com/android/camera/ui/TimeIntervalPopup.java new file mode 100644 index 000000000..18ad9f5da --- /dev/null +++ b/src/com/android/camera/ui/TimeIntervalPopup.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.content.Context; +import android.content.res.Resources; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.NumberPicker; +import android.widget.Switch; +import android.widget.TextView; + +import com.android.camera.IconListPreference; +import com.android.camera.ListPreference; +import com.android.gallery3d.R; + +/** + * This is a popup window that allows users to turn on/off time lapse feature, + * and to select a time interval for taking a time lapse video. + */ +public class TimeIntervalPopup extends AbstractSettingPopup { + private static final String TAG = "TimeIntervalPopup"; + private NumberPicker mNumberSpinner; + private NumberPicker mUnitSpinner; + private Switch mTimeLapseSwitch; + private final String[] mUnits; + private final String[] mDurations; + private IconListPreference mPreference; + private Listener mListener; + private Button mConfirmButton; + private TextView mHelpText; + private View mTimePicker; + + static public interface Listener { + public void onListPrefChanged(ListPreference pref); + } + + public void setSettingChangedListener(Listener listener) { + mListener = listener; + } + + public TimeIntervalPopup(Context context, AttributeSet attrs) { + super(context, attrs); + + Resources res = context.getResources(); + mUnits = res.getStringArray(R.array.pref_video_time_lapse_frame_interval_units); + mDurations = res + .getStringArray(R.array.pref_video_time_lapse_frame_interval_duration_values); + } + + public void initialize(IconListPreference preference) { + mPreference = preference; + + // Set title. + mTitle.setText(mPreference.getTitle()); + + // Duration + int durationCount = mDurations.length; + mNumberSpinner = (NumberPicker) findViewById(R.id.duration); + mNumberSpinner.setMinValue(0); + mNumberSpinner.setMaxValue(durationCount - 1); + mNumberSpinner.setDisplayedValues(mDurations); + mNumberSpinner.setWrapSelectorWheel(false); + + // Units for duration (i.e. seconds, minutes, etc) + mUnitSpinner = (NumberPicker) findViewById(R.id.duration_unit); + mUnitSpinner.setMinValue(0); + mUnitSpinner.setMaxValue(mUnits.length - 1); + mUnitSpinner.setDisplayedValues(mUnits); + mUnitSpinner.setWrapSelectorWheel(false); + + mTimePicker = findViewById(R.id.time_interval_picker); + mTimeLapseSwitch = (Switch) findViewById(R.id.time_lapse_switch); + mHelpText = (TextView) findViewById(R.id.set_time_interval_help_text); + mConfirmButton = (Button) findViewById(R.id.time_lapse_interval_set_button); + + // Disable focus on the spinners to prevent keyboard from coming up + mNumberSpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS); + mUnitSpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS); + + mTimeLapseSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + setTimeSelectionEnabled(isChecked); + } + }); + mConfirmButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + updateInputState(); + } + }); + } + + private void restoreSetting() { + int index = mPreference.findIndexOfValue(mPreference.getValue()); + if (index == -1) { + Log.e(TAG, "Invalid preference value."); + mPreference.print(); + throw new IllegalArgumentException(); + } else if (index == 0) { + // default choice: time lapse off + mTimeLapseSwitch.setChecked(false); + setTimeSelectionEnabled(false); + } else { + mTimeLapseSwitch.setChecked(true); + setTimeSelectionEnabled(true); + int durationCount = mNumberSpinner.getMaxValue() + 1; + int unit = (index - 1) / durationCount; + int number = (index - 1) % durationCount; + mUnitSpinner.setValue(unit); + mNumberSpinner.setValue(number); + } + } + + @Override + public void setVisibility(int visibility) { + if (visibility == View.VISIBLE) { + if (getVisibility() != View.VISIBLE) { + // Set the number pickers and on/off switch to be consistent + // with the preference + restoreSetting(); + } + } + super.setVisibility(visibility); + } + + protected void setTimeSelectionEnabled(boolean enabled) { + mHelpText.setVisibility(enabled ? GONE : VISIBLE); + mTimePicker.setVisibility(enabled ? VISIBLE : GONE); + } + + @Override + public void reloadPreference() { + } + + private void updateInputState() { + if (mTimeLapseSwitch.isChecked()) { + int newId = mUnitSpinner.getValue() * (mNumberSpinner.getMaxValue() + 1) + + mNumberSpinner.getValue() + 1; + mPreference.setValueIndex(newId); + } else { + mPreference.setValueIndex(0); + } + + if (mListener != null) { + mListener.onListPrefChanged(mPreference); + } + } +} diff --git a/src/com/android/camera/ui/TwoStateImageView.java b/src/com/android/camera/ui/TwoStateImageView.java new file mode 100644 index 000000000..cd5b27fc1 --- /dev/null +++ b/src/com/android/camera/ui/TwoStateImageView.java @@ -0,0 +1,55 @@ +/* + * 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.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; + +/** + * A @{code ImageView} which change the opacity of the icon if disabled. + */ +public class TwoStateImageView extends ImageView { + private static final int ENABLED_ALPHA = 255; + private static final int DISABLED_ALPHA = (int) (255 * 0.4); + private boolean mFilterEnabled = true; + + public TwoStateImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public TwoStateImageView(Context context) { + this(context, null); + } + + @SuppressWarnings("deprecation") + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + if (mFilterEnabled) { + if (enabled) { + setAlpha(ENABLED_ALPHA); + } else { + setAlpha(DISABLED_ALPHA); + } + } + } + + public void enableFilter(boolean enabled) { + mFilterEnabled = enabled; + } +} diff --git a/src/com/android/camera/ui/ZoomRenderer.java b/src/com/android/camera/ui/ZoomRenderer.java new file mode 100644 index 000000000..86b82b459 --- /dev/null +++ b/src/com/android/camera/ui/ZoomRenderer.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2012 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.ui; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.view.ScaleGestureDetector; + +import com.android.gallery3d.R; + +public class ZoomRenderer extends OverlayRenderer + implements ScaleGestureDetector.OnScaleGestureListener { + + private static final String TAG = "CAM_Zoom"; + + private int mMaxZoom; + private int mMinZoom; + private OnZoomChangedListener mListener; + + private ScaleGestureDetector mDetector; + private Paint mPaint; + private Paint mTextPaint; + private int mCircleSize; + private int mCenterX; + private int mCenterY; + private float mMaxCircle; + private float mMinCircle; + private int mInnerStroke; + private int mOuterStroke; + private int mZoomSig; + private int mZoomFraction; + private Rect mTextBounds; + + public interface OnZoomChangedListener { + void onZoomStart(); + void onZoomEnd(); + void onZoomValueChanged(int index); // only for immediate zoom + } + + public ZoomRenderer(Context ctx) { + Resources res = ctx.getResources(); + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setColor(Color.WHITE); + mPaint.setStyle(Paint.Style.STROKE); + mTextPaint = new Paint(mPaint); + mTextPaint.setStyle(Paint.Style.FILL); + mTextPaint.setTextSize(res.getDimensionPixelSize(R.dimen.zoom_font_size)); + mTextPaint.setTextAlign(Paint.Align.LEFT); + mTextPaint.setAlpha(192); + mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke); + mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke); + mDetector = new ScaleGestureDetector(ctx, this); + mMinCircle = res.getDimensionPixelSize(R.dimen.zoom_ring_min); + mTextBounds = new Rect(); + setVisible(false); + } + + // set from module + public void setZoomMax(int zoomMaxIndex) { + mMaxZoom = zoomMaxIndex; + mMinZoom = 0; + } + + public void setZoom(int index) { + mCircleSize = (int) (mMinCircle + index * (mMaxCircle - mMinCircle) / (mMaxZoom - mMinZoom)); + } + + public void setZoomValue(int value) { + value = value / 10; + mZoomSig = value / 10; + mZoomFraction = value % 10; + } + + public void setOnZoomChangeListener(OnZoomChangedListener listener) { + mListener = listener; + } + + @Override + public void layout(int l, int t, int r, int b) { + super.layout(l, t, r, b); + mCenterX = (r - l) / 2; + mCenterY = (b - t) / 2; + mMaxCircle = Math.min(getWidth(), getHeight()); + mMaxCircle = (mMaxCircle - mMinCircle) / 2; + } + + public boolean isScaling() { + return mDetector.isInProgress(); + } + + @Override + public void onDraw(Canvas canvas) { + mPaint.setStrokeWidth(mInnerStroke); + canvas.drawCircle(mCenterX, mCenterY, mMinCircle, mPaint); + canvas.drawCircle(mCenterX, mCenterY, mMaxCircle, mPaint); + canvas.drawLine(mCenterX - mMinCircle, mCenterY, + mCenterX - mMaxCircle - 4, mCenterY, mPaint); + mPaint.setStrokeWidth(mOuterStroke); + canvas.drawCircle((float) mCenterX, (float) mCenterY, + (float) mCircleSize, mPaint); + String txt = mZoomSig+"."+mZoomFraction+"x"; + mTextPaint.getTextBounds(txt, 0, txt.length(), mTextBounds); + canvas.drawText(txt, mCenterX - mTextBounds.centerX(), mCenterY - mTextBounds.centerY(), + mTextPaint); + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + final float sf = detector.getScaleFactor(); + float circle = (int) (mCircleSize * sf * sf); + circle = Math.max(mMinCircle, circle); + circle = Math.min(mMaxCircle, circle); + if (mListener != null && (int) circle != mCircleSize) { + mCircleSize = (int) circle; + int zoom = mMinZoom + (int) ((mCircleSize - mMinCircle) * (mMaxZoom - mMinZoom) / (mMaxCircle - mMinCircle)); + mListener.onZoomValueChanged(zoom); + } + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + setVisible(true); + if (mListener != null) { + mListener.onZoomStart(); + } + update(); + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + setVisible(false); + if (mListener != null) { + mListener.onZoomEnd(); + } + } + +} |