summaryrefslogtreecommitdiffstats
path: root/src/com/android/camera
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/camera')
-rw-r--r--src/com/android/camera/AndroidCameraManagerImpl.java779
-rw-r--r--src/com/android/camera/CameraActivity.java571
-rw-r--r--src/com/android/camera/CameraBackupAgent.java32
-rw-r--r--src/com/android/camera/CameraButtonIntentReceiver.java53
-rw-r--r--src/com/android/camera/CameraDisabledException.java24
-rw-r--r--src/com/android/camera/CameraErrorCallback.java35
-rw-r--r--src/com/android/camera/CameraHardwareException.java28
-rw-r--r--src/com/android/camera/CameraHolder.java299
-rw-r--r--src/com/android/camera/CameraManager.java317
-rw-r--r--src/com/android/camera/CameraManagerFactory.java37
-rw-r--r--src/com/android/camera/CameraModule.java70
-rw-r--r--src/com/android/camera/CameraPreference.java63
-rw-r--r--src/com/android/camera/CameraScreenNail.java524
-rw-r--r--src/com/android/camera/CameraSettings.java570
-rw-r--r--src/com/android/camera/CaptureAnimManager.java228
-rw-r--r--src/com/android/camera/ComboPreferences.java335
-rw-r--r--src/com/android/camera/CountDownTimerPreference.java48
-rw-r--r--src/com/android/camera/DisableCameraReceiver.java85
-rw-r--r--src/com/android/camera/EffectsRecorder.java1239
-rw-r--r--src/com/android/camera/Exif.java54
-rw-r--r--src/com/android/camera/FocusOverlayManager.java558
-rw-r--r--src/com/android/camera/IconListPreference.java117
-rw-r--r--src/com/android/camera/ImageTaskManager.java48
-rw-r--r--src/com/android/camera/IntArray.java45
-rw-r--r--src/com/android/camera/ListPreference.java202
-rw-r--r--src/com/android/camera/LocationManager.java181
-rw-r--r--src/com/android/camera/MediaSaveService.java233
-rw-r--r--src/com/android/camera/OnClickAttr.java31
-rw-r--r--src/com/android/camera/OnScreenHint.java190
-rw-r--r--src/com/android/camera/OnScreenIndicators.java190
-rw-r--r--src/com/android/camera/PhotoController.java65
-rw-r--r--src/com/android/camera/PhotoMenu.java200
-rw-r--r--src/com/android/camera/PhotoModule.java2006
-rw-r--r--src/com/android/camera/PhotoUI.java864
-rw-r--r--src/com/android/camera/PieController.java259
-rw-r--r--src/com/android/camera/PreferenceGroup.java79
-rw-r--r--src/com/android/camera/PreferenceInflater.java108
-rw-r--r--src/com/android/camera/PreviewFrameLayout.java136
-rw-r--r--src/com/android/camera/PreviewGestures.java199
-rw-r--r--src/com/android/camera/ProxyLauncher.java46
-rw-r--r--src/com/android/camera/RecordLocationPreference.java58
-rw-r--r--src/com/android/camera/RotateDialogController.java169
-rw-r--r--src/com/android/camera/SecureCameraActivity.java23
-rwxr-xr-xsrc/com/android/camera/ShutterButton.java130
-rw-r--r--src/com/android/camera/SoundClips.java197
-rw-r--r--src/com/android/camera/StaticBitmapScreenNail.java32
-rw-r--r--src/com/android/camera/Storage.java181
-rw-r--r--src/com/android/camera/SurfaceTextureRenderer.java224
-rw-r--r--src/com/android/camera/SwitchAnimManager.java146
-rw-r--r--src/com/android/camera/Thumbnail.java68
-rw-r--r--src/com/android/camera/Util.java804
-rw-r--r--src/com/android/camera/VideoController.java42
-rw-r--r--src/com/android/camera/VideoMenu.java205
-rw-r--r--src/com/android/camera/VideoModule.java2233
-rw-r--r--src/com/android/camera/VideoUI.java698
-rw-r--r--src/com/android/camera/data/AbstractLocalDataAdapterWrapper.java90
-rw-r--r--src/com/android/camera/data/CameraDataAdapter.java348
-rw-r--r--src/com/android/camera/data/CameraPreviewData.java63
-rw-r--r--src/com/android/camera/data/FixedFirstDataAdapter.java154
-rw-r--r--src/com/android/camera/data/FixedLastDataAdapter.java127
-rw-r--r--src/com/android/camera/data/LocalData.java726
-rw-r--r--src/com/android/camera/data/LocalDataAdapter.java91
-rw-r--r--src/com/android/camera/data/PanoramaMetadataLoader.java106
-rw-r--r--src/com/android/camera/drawable/TextDrawable.java121
-rw-r--r--src/com/android/camera/ui/AbstractSettingPopup.java44
-rw-r--r--src/com/android/camera/ui/CameraControls.java262
-rw-r--r--src/com/android/camera/ui/CameraRootView.java181
-rw-r--r--src/com/android/camera/ui/CameraSwitcher.java378
-rw-r--r--src/com/android/camera/ui/CheckedLinearLayout.java60
-rw-r--r--src/com/android/camera/ui/CountDownView.java131
-rw-r--r--src/com/android/camera/ui/CountdownTimerPopup.java145
-rw-r--r--src/com/android/camera/ui/EffectSettingPopup.java214
-rw-r--r--src/com/android/camera/ui/ExpandedGridView.java36
-rw-r--r--src/com/android/camera/ui/FaceView.java226
-rw-r--r--src/com/android/camera/ui/FilmStripGestureRecognizer.java112
-rw-r--r--src/com/android/camera/ui/FilmStripView.java1720
-rw-r--r--src/com/android/camera/ui/FocusIndicator.java24
-rw-r--r--src/com/android/camera/ui/InLineSettingCheckBox.java83
-rw-r--r--src/com/android/camera/ui/InLineSettingItem.java94
-rw-r--r--src/com/android/camera/ui/InLineSettingMenu.java78
-rw-r--r--src/com/android/camera/ui/LayoutChangeHelper.java43
-rw-r--r--src/com/android/camera/ui/LayoutChangeNotifier.java28
-rw-r--r--src/com/android/camera/ui/LayoutNotifyView.java48
-rw-r--r--src/com/android/camera/ui/ListPrefSettingPopup.java127
-rw-r--r--src/com/android/camera/ui/MoreSettingPopup.java203
-rw-r--r--src/com/android/camera/ui/OnIndicatorEventListener.java25
-rw-r--r--src/com/android/camera/ui/OverlayRenderer.java95
-rw-r--r--src/com/android/camera/ui/PieItem.java170
-rw-r--r--src/com/android/camera/ui/PieMenuButton.java62
-rw-r--r--src/com/android/camera/ui/PieRenderer.java1091
-rw-r--r--src/com/android/camera/ui/PopupManager.java66
-rw-r--r--src/com/android/camera/ui/PreviewSurfaceView.java50
-rw-r--r--src/com/android/camera/ui/RenderOverlay.java176
-rw-r--r--src/com/android/camera/ui/Rotatable.java22
-rw-r--r--src/com/android/camera/ui/RotatableLayout.java283
-rw-r--r--src/com/android/camera/ui/RotateImageView.java176
-rw-r--r--src/com/android/camera/ui/RotateLayout.java203
-rw-r--r--src/com/android/camera/ui/RotateTextToast.java59
-rw-r--r--src/com/android/camera/ui/Switch.java505
-rw-r--r--src/com/android/camera/ui/TimeIntervalPopup.java164
-rw-r--r--src/com/android/camera/ui/TwoStateImageView.java55
-rw-r--r--src/com/android/camera/ui/ZoomRenderer.java158
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();
+ }
+ }
+
+}