Move Camera Java/Native source into Gallery2

Change-Id: I968efe4d656e88a7760d3c0044f65b4adac2ddd1
diff --git a/src/com/android/camera/ActivityBase.java b/src/com/android/camera/ActivityBase.java
new file mode 100644
index 0000000..4e4143e
--- /dev/null
+++ b/src/com/android/camera/ActivityBase.java
@@ -0,0 +1,642 @@
+/*
+ * 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.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+import android.hardware.Camera.Parameters;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.DecelerateInterpolator;
+
+import com.android.camera.ui.LayoutChangeNotifier;
+import com.android.camera.ui.PopupManager;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.app.AppBridge;
+import com.android.gallery3d.app.FilmstripPage;
+import com.android.gallery3d.app.GalleryActionBar;
+import com.android.gallery3d.app.PhotoPage;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.util.MediaSetUtils;
+
+/**
+ * Superclass of camera activity.
+ */
+public abstract class ActivityBase extends AbstractGalleryActivity
+        implements LayoutChangeNotifier.Listener {
+
+    private static final String TAG = "ActivityBase";
+    private static final int CAMERA_APP_VIEW_TOGGLE_TIME = 100;  // milliseconds
+    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";
+
+    private int mResultCodeForTesting;
+    private Intent mResultDataForTesting;
+    private OnScreenHint mStorageHint;
+    private View mSingleTapArea;
+
+    protected boolean mOpenCameraFail;
+    protected boolean mCameraDisabled;
+    protected CameraManager.CameraProxy mCameraDevice;
+    protected Parameters mParameters;
+    // The activity is paused. The classes that extend this class should set
+    // mPaused the first thing in onResume/onPause.
+    protected boolean mPaused;
+    protected GalleryActionBar mActionBar;
+
+    // multiple cameras support
+    protected int mNumberOfCameras;
+    protected int mCameraId;
+    // 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;
+
+    protected MyAppBridge mAppBridge;
+    protected ScreenNail mCameraScreenNail; // This shows camera preview.
+    // The view containing only camera related widgets like control panel,
+    // indicator bar, focus indicator and etc.
+    protected View mCameraAppView;
+    protected boolean mShowCameraAppView = true;
+    private Animation mCameraAppViewFadeIn;
+    private Animation mCameraAppViewFadeOut;
+    // Secure album id. This should be incremented every time the camera is
+    // launched from the secure lock screen. The id should be the same when
+    // switching between camera, camcorder, and panorama.
+    protected static int sSecureAlbumId;
+    // True if the camera is started from secure lock screen.
+    protected boolean mSecureCamera;
+    private static boolean sFirstStartAfterScreenOn = true;
+
+    private long mStorageSpace = Storage.LOW_STORAGE_THRESHOLD;
+    private static final int UPDATE_STORAGE_HINT = 0;
+    private final Handler mHandler = new Handler() {
+            @Override
+            public void handleMessage(Message msg) {
+                switch (msg.what) {
+                    case UPDATE_STORAGE_HINT:
+                        updateStorageHint();
+                        return;
+                }
+            }
+    };
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(Intent.ACTION_MEDIA_MOUNTED)
+                    || action.equals(Intent.ACTION_MEDIA_UNMOUNTED)
+                    || action.equals(Intent.ACTION_MEDIA_CHECKING)
+                    || action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) {
+                updateStorageSpaceAndHint();
+            }
+        }
+    };
+
+    // 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;
+    }
+
+    protected class CameraOpenThread extends Thread {
+        @Override
+        public void run() {
+            try {
+                mCameraDevice = Util.openCamera(ActivityBase.this, mCameraId);
+                mParameters = mCameraDevice.getParameters();
+            } catch (CameraHardwareException e) {
+                mOpenCameraFail = true;
+            } catch (CameraDisabledException e) {
+                mCameraDisabled = true;
+            }
+        }
+    }
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.disableToggleStatusBar();
+        // Set a theme with action bar. It is not specified in manifest because
+        // we want to hide it by default. setTheme must happen before
+        // setContentView.
+        //
+        // This must be set before we call super.onCreate(), where the window's
+        // background is removed.
+        setTheme(R.style.Theme_Gallery);
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+        if (ApiHelper.HAS_ACTION_BAR) {
+            requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+        } else {
+            requestWindowFeature(Window.FEATURE_NO_TITLE);
+        }
+
+        // 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)) {
+            mSecureCamera = true;
+            // Use a new album when this is started from the lock screen.
+            sSecureAlbumId++;
+        } else if (ACTION_IMAGE_CAPTURE_SECURE.equals(action)) {
+            mSecureCamera = true;
+        } else {
+            mSecureCamera = intent.getBooleanExtra(SECURE_CAMERA_EXTRA, false);
+        }
+        if (mSecureCamera) {
+            IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
+            registerReceiver(mScreenOffReceiver, filter);
+            if (sScreenOffReceiver == null) {
+                sScreenOffReceiver = new ScreenOffReceiver();
+                getApplicationContext().registerReceiver(sScreenOffReceiver, filter);
+            }
+        }
+        super.onCreate(icicle);
+    }
+
+    public boolean isPanoramaActivity() {
+        return false;
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+
+        installIntentFilter();
+        if (updateStorageHintOnResume()) {
+            updateStorageSpace();
+            mHandler.sendEmptyMessageDelayed(UPDATE_STORAGE_HINT, 200);
+        }
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+
+        if (mStorageHint != null) {
+            mStorageHint.cancel();
+            mStorageHint = null;
+        }
+
+        unregisterReceiver(mReceiver);
+    }
+
+    @Override
+    public void setContentView(int layoutResID) {
+        super.setContentView(layoutResID);
+        // getActionBar() should be after setContentView
+        mActionBar = new GalleryActionBar(this);
+        mActionBar.hide();
+    }
+
+    @Override
+    public boolean onSearchRequested() {
+        return false;
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        // 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 && mShowCameraAppView) {
+            return true;
+        }
+
+        return super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_MENU && mShowCameraAppView) {
+            return true;
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
+    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;
+    }
+
+    @Override
+    protected void onDestroy() {
+        PopupManager.removeInstance(this);
+        if (mSecureCamera) unregisterReceiver(mScreenOffReceiver);
+        super.onDestroy();
+    }
+
+    protected void installIntentFilter() {
+        // install an intent filter to receive SD card related events.
+        IntentFilter intentFilter =
+                new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
+        intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
+        intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
+        intentFilter.addAction(Intent.ACTION_MEDIA_CHECKING);
+        intentFilter.addDataScheme("file");
+        registerReceiver(mReceiver, intentFilter);
+    }
+
+    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 gotoGallery() {
+        // Move the next picture with capture animation. "1" means next.
+        mAppBridge.switchWithCaptureAnimation(1);
+    }
+
+    // Call this after setContentView.
+    public ScreenNail createCameraScreenNail(boolean getPictures) {
+        mCameraAppView = findViewById(R.id.camera_app_root);
+        Bundle data = new Bundle();
+        String path;
+        if (getPictures) {
+            if (mSecureCamera) {
+                path = "/secure/all/" + sSecureAlbumId;
+            } else {
+                path = "/local/all/" + MediaSetUtils.CAMERA_BUCKET_ID;
+            }
+        } else {
+            path = "/local/all/0"; // Use 0 so gallery does not show anything.
+        }
+        data.putString(PhotoPage.KEY_MEDIA_SET_PATH, path);
+        data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, path);
+        data.putBoolean(PhotoPage.KEY_SHOW_WHEN_LOCKED, mSecureCamera);
+
+        // Send an AppBridge to gallery to enable the camera preview.
+        if (mAppBridge != null) {
+            mCameraScreenNail.recycle();
+        }
+        mAppBridge = new MyAppBridge();
+        data.putParcelable(PhotoPage.KEY_APP_BRIDGE, mAppBridge);
+        if (getStateManager().getStateCount() == 0) {
+            getStateManager().startState(FilmstripPage.class, data);
+        } else {
+            getStateManager().switchState(getStateManager().getTopState(),
+                    FilmstripPage.class, data);
+        }
+        mCameraScreenNail = mAppBridge.getCameraScreenNail();
+        return mCameraScreenNail;
+    }
+
+    // Call this after setContentView.
+    protected ScreenNail reuseCameraScreenNail(boolean getPictures) {
+        mCameraAppView = findViewById(R.id.camera_app_root);
+        Bundle data = new Bundle();
+        String path;
+        if (getPictures) {
+            if (mSecureCamera) {
+                path = "/secure/all/" + sSecureAlbumId;
+            } else {
+                path = "/local/all/" + MediaSetUtils.CAMERA_BUCKET_ID;
+            }
+        } else {
+            path = "/local/all/0"; // Use 0 so gallery does not show anything.
+        }
+        data.putString(PhotoPage.KEY_MEDIA_SET_PATH, path);
+        data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, path);
+        data.putBoolean(PhotoPage.KEY_SHOW_WHEN_LOCKED, mSecureCamera);
+
+        // Send an AppBridge to gallery to enable the camera preview.
+        if (mAppBridge == null) {
+            mAppBridge = new MyAppBridge();
+        }
+        data.putParcelable(PhotoPage.KEY_APP_BRIDGE, mAppBridge);
+        if (getStateManager().getStateCount() == 0) {
+            getStateManager().startState(FilmstripPage.class, data);
+        }
+        mCameraScreenNail = mAppBridge.getCameraScreenNail();
+        return mCameraScreenNail;
+    }
+
+    private class HideCameraAppView implements Animation.AnimationListener {
+        @Override
+        public void onAnimationEnd(Animation animation) {
+            // We cannot set this as GONE because we want to receive the
+            // onLayoutChange() callback even when we are invisible.
+            mCameraAppView.setVisibility(View.INVISIBLE);
+        }
+
+        @Override
+        public void onAnimationRepeat(Animation animation) {
+        }
+
+        @Override
+        public void onAnimationStart(Animation animation) {
+        }
+    }
+
+    protected void updateCameraAppView() {
+        // Initialize the animation.
+        if (mCameraAppViewFadeIn == null) {
+            mCameraAppViewFadeIn = new AlphaAnimation(0f, 1f);
+            mCameraAppViewFadeIn.setDuration(CAMERA_APP_VIEW_TOGGLE_TIME);
+            mCameraAppViewFadeIn.setInterpolator(new DecelerateInterpolator());
+
+            mCameraAppViewFadeOut = new AlphaAnimation(1f, 0f);
+            mCameraAppViewFadeOut.setDuration(CAMERA_APP_VIEW_TOGGLE_TIME);
+            mCameraAppViewFadeOut.setInterpolator(new DecelerateInterpolator());
+            mCameraAppViewFadeOut.setAnimationListener(new HideCameraAppView());
+        }
+
+        if (mShowCameraAppView) {
+            mCameraAppView.setVisibility(View.VISIBLE);
+            // The "transparent region" is not recomputed when a sibling of
+            // SurfaceView changes visibility (unless it involves GONE). It's
+            // been broken since 1.0. Call requestLayout to work around it.
+            mCameraAppView.requestLayout();
+            mCameraAppView.startAnimation(mCameraAppViewFadeIn);
+        } else {
+            mCameraAppView.startAnimation(mCameraAppViewFadeOut);
+        }
+    }
+
+    protected void onFullScreenChanged(boolean full) {
+        if (mShowCameraAppView == full) return;
+        mShowCameraAppView = full;
+        if (mPaused || isFinishing()) return;
+        updateCameraAppView();
+    }
+
+    @Override
+    public GalleryActionBar getGalleryActionBar() {
+        return mActionBar;
+    }
+
+    // Preview frame layout has changed.
+    @Override
+    public void onLayoutChange(View v, int left, int top, int right, int bottom) {
+        if (mAppBridge == null) return;
+
+        int width = right - left;
+        int height = bottom - top;
+        if (ApiHelper.HAS_SURFACE_TEXTURE) {
+            CameraScreenNail screenNail = (CameraScreenNail) mCameraScreenNail;
+            if (Util.getDisplayRotation(this) % 180 == 0) {
+                screenNail.setPreviewFrameLayoutSize(width, height);
+            } else {
+                // Swap the width and height. Camera screen nail draw() is based on
+                // natural orientation, not the view system orientation.
+                screenNail.setPreviewFrameLayoutSize(height, width);
+            }
+            notifyScreenNailChanged();
+        }
+    }
+
+    protected void setSingleTapUpListener(View singleTapArea) {
+        mSingleTapArea = singleTapArea;
+    }
+
+    private boolean onSingleTapUp(int x, int y) {
+        // Ignore if listener is null or the camera control is invisible.
+        if (mSingleTapArea == null || !mShowCameraAppView) return false;
+
+        int[] relativeLocation = Util.getRelativeLocation((View) getGLRoot(),
+                mSingleTapArea);
+        x -= relativeLocation[0];
+        y -= relativeLocation[1];
+        if (x >= 0 && x < mSingleTapArea.getWidth() && y >= 0
+                && y < mSingleTapArea.getHeight()) {
+            onSingleTapUp(mSingleTapArea, x, y);
+            return true;
+        }
+        return false;
+    }
+
+    protected void onSingleTapUp(View view, int x, int y) {
+    }
+
+    public void setSwipingEnabled(boolean enabled) {
+        mAppBridge.setSwipingEnabled(enabled);
+    }
+
+    public void notifyScreenNailChanged() {
+        mAppBridge.notifyScreenNailChanged();
+    }
+
+    protected void onPreviewTextureCopied() {
+    }
+
+    protected void onCaptureTextureCopied() {
+    }
+
+    protected void addSecureAlbumItemIfNeeded(boolean isVideo, Uri uri) {
+        if (mSecureCamera) {
+            int id = Integer.parseInt(uri.getLastPathSegment());
+            mAppBridge.addSecureAlbumItem(isVideo, id);
+        }
+    }
+
+    public boolean isSecureCamera() {
+        return mSecureCamera;
+    }
+
+    //////////////////////////////////////////////////////////////////////////
+    //  The is the communication interface between the Camera Application and
+    //  the Gallery PhotoPage.
+    //////////////////////////////////////////////////////////////////////////
+
+    class MyAppBridge extends AppBridge implements CameraScreenNail.Listener {
+        @SuppressWarnings("hiding")
+        private ScreenNail mCameraScreenNail;
+        private Server mServer;
+
+        @Override
+        public ScreenNail attachScreenNail() {
+            if (mCameraScreenNail == null) {
+                if (ApiHelper.HAS_SURFACE_TEXTURE) {
+                    mCameraScreenNail = new CameraScreenNail(this);
+                } else {
+                    Bitmap b = BitmapFactory.decodeResource(getResources(),
+                            R.drawable.wallpaper_picker_preview);
+                    mCameraScreenNail = new StaticBitmapScreenNail(b);
+                }
+            }
+            return mCameraScreenNail;
+        }
+
+        @Override
+        public void detachScreenNail() {
+            mCameraScreenNail = null;
+        }
+
+        public ScreenNail getCameraScreenNail() {
+            return mCameraScreenNail;
+        }
+
+        // Return true if the tap is consumed.
+        @Override
+        public boolean onSingleTapUp(int x, int y) {
+            return ActivityBase.this.onSingleTapUp(x, y);
+        }
+
+        // This is used to notify that the screen nail will be drawn in full screen
+        // or not in next draw() call.
+        @Override
+        public void onFullScreenChanged(boolean full) {
+            ActivityBase.this.onFullScreenChanged(full);
+        }
+
+        @Override
+        public void requestRender() {
+            getGLRoot().requestRenderForced();
+        }
+
+        @Override
+        public void onPreviewTextureCopied() {
+            ActivityBase.this.onPreviewTextureCopied();
+        }
+
+        @Override
+        public void onCaptureTextureCopied() {
+            ActivityBase.this.onCaptureTextureCopied();
+        }
+
+        @Override
+        public void setServer(Server s) {
+            mServer = s;
+        }
+
+        @Override
+        public boolean isPanorama() {
+            return ActivityBase.this.isPanoramaActivity();
+        }
+
+        @Override
+        public boolean isStaticCamera() {
+            return !ApiHelper.HAS_SURFACE_TEXTURE;
+        }
+
+        public void addSecureAlbumItem(boolean isVideo, int id) {
+            if (mServer != null) mServer.addSecureAlbumItem(isVideo, id);
+        }
+
+        private void setCameraRelativeFrame(Rect frame) {
+            if (mServer != null) mServer.setCameraRelativeFrame(frame);
+        }
+
+        private void switchWithCaptureAnimation(int offset) {
+            if (mServer != null) mServer.switchWithCaptureAnimation(offset);
+        }
+
+        private void setSwipingEnabled(boolean enabled) {
+            if (mServer != null) mServer.setSwipingEnabled(enabled);
+        }
+
+        private void notifyScreenNailChanged() {
+            if (mServer != null) mServer.notifyScreenNailChanged();
+        }
+    }
+}
diff --git a/src/com/android/camera/CameraActivity.java b/src/com/android/camera/CameraActivity.java
new file mode 100644
index 0000000..d79832b
--- /dev/null
+++ b/src/com/android/camera/CameraActivity.java
@@ -0,0 +1,437 @@
+/*
+ * 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.Intent;
+import android.content.res.Configuration;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.OrientationEventListener;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.android.camera.ui.CameraSwitcher;
+import com.android.gallery3d.app.PhotoPage;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.util.LightCycleHelper;
+
+public class CameraActivity extends ActivityBase
+        implements CameraSwitcher.CameraSwitchListener {
+    public static final int PHOTO_MODULE_INDEX = 0;
+    public static final int VIDEO_MODULE_INDEX = 1;
+    public static final int PANORAMA_MODULE_INDEX = 2;
+    public static final int LIGHTCYCLE_MODULE_INDEX = 3;
+
+    CameraModule mCurrentModule;
+    private FrameLayout mFrame;
+    private ShutterButton mShutter;
+    private CameraSwitcher mSwitcher;
+    private View mShutterSwitcher;
+    private View mControlsBackground;
+    private Drawable[] mDrawables;
+    private int mCurrentModuleIndex;
+    private MotionEvent mDown;
+
+    private MyOrientationEventListener mOrientationListener;
+    // The degrees of the device rotated clockwise from its natural orientation.
+    private int mLastRawOrientation = OrientationEventListener.ORIENTATION_UNKNOWN;
+
+    private static final String TAG = "CAM_activity";
+
+    private static final int[] DRAW_IDS = {
+            R.drawable.ic_switch_camera,
+            R.drawable.ic_switch_video,
+            R.drawable.ic_switch_pan,
+            R.drawable.ic_switch_photosphere
+    };
+
+    @Override
+    public void onCreate(Bundle state) {
+        super.onCreate(state);
+        setContentView(R.layout.camera_main);
+        mFrame = (FrameLayout) findViewById(R.id.main_content);
+        mDrawables = new Drawable[DRAW_IDS.length];
+        for (int i = 0; i < DRAW_IDS.length; i++) {
+            mDrawables[i] = getResources().getDrawable(DRAW_IDS[i]);
+        }
+        init();
+        if (MediaStore.INTENT_ACTION_VIDEO_CAMERA.equals(getIntent().getAction())
+                || MediaStore.ACTION_VIDEO_CAPTURE.equals(getIntent().getAction())) {
+            mCurrentModule = new VideoModule();
+            mCurrentModuleIndex = VIDEO_MODULE_INDEX;
+        } else {
+            mCurrentModule = new PhotoModule();
+            mCurrentModuleIndex = PHOTO_MODULE_INDEX;
+        }
+        mCurrentModule.init(this, mFrame, true);
+        mSwitcher.setCurrentIndex(mCurrentModuleIndex);
+        mOrientationListener = new MyOrientationEventListener(this);
+    }
+
+    public void init() {
+        mControlsBackground = findViewById(R.id.controls);
+        mShutterSwitcher = findViewById(R.id.camera_shutter_switcher);
+        mShutter = (ShutterButton) findViewById(R.id.shutter_button);
+        mSwitcher = (CameraSwitcher) findViewById(R.id.camera_switcher);
+        int totaldrawid = (LightCycleHelper.hasLightCycleCapture(this)
+                                ? DRAW_IDS.length : DRAW_IDS.length - 1);
+        if (!ApiHelper.HAS_OLD_PANORAMA) totaldrawid--;
+
+        int[] drawids = new int[totaldrawid];
+        int[] moduleids = new int[totaldrawid];
+        int ix = 0;
+        for (int i = 0; i < mDrawables.length; i++) {
+            if (i == PANORAMA_MODULE_INDEX && !ApiHelper.HAS_OLD_PANORAMA) {
+                continue; // not enabled, so don't add to UI
+            }
+            if (i == LIGHTCYCLE_MODULE_INDEX && !LightCycleHelper.hasLightCycleCapture(this)) {
+                continue; // not enabled, so don't add to UI
+            }
+            moduleids[ix] = i;
+            drawids[ix++] = DRAW_IDS[i];
+        }
+        mSwitcher.setIds(moduleids, drawids);
+        mSwitcher.setSwitchListener(this);
+        mSwitcher.setCurrentIndex(mCurrentModuleIndex);
+    }
+
+    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);
+        }
+    }
+
+    @Override
+    public void onCameraSelected(int i) {
+        if (mPaused) return;
+        if (i != mCurrentModuleIndex) {
+            mPaused = true;
+            boolean canReuse = canReuseScreenNail();
+            CameraHolder.instance().keep();
+            closeModule(mCurrentModule);
+            mCurrentModuleIndex = i;
+            switch (i) {
+                case VIDEO_MODULE_INDEX:
+                    mCurrentModule = new VideoModule();
+                    break;
+                case PHOTO_MODULE_INDEX:
+                    mCurrentModule = new PhotoModule();
+                    break;
+                case PANORAMA_MODULE_INDEX:
+                    mCurrentModule = new PanoramaModule();
+                    break;
+                case LIGHTCYCLE_MODULE_INDEX:
+                    mCurrentModule = LightCycleHelper.createPanoramaModule();
+                    break;
+            }
+            openModule(mCurrentModule, canReuse);
+            mCurrentModule.onOrientationChanged(mLastRawOrientation);
+        }
+    }
+
+    @Override
+    public void onShowSwitcherPopup() {
+        mCurrentModule.onShowSwitcherPopup();
+    }
+
+    private void openModule(CameraModule module, boolean canReuse) {
+        module.init(this, mFrame, canReuse && canReuseScreenNail());
+        mPaused = false;
+        module.onResumeBeforeSuper();
+        module.onResumeAfterSuper();
+    }
+
+    private void closeModule(CameraModule module) {
+        module.onPauseBeforeSuper();
+        module.onPauseAfterSuper();
+        mFrame.removeAllViews();
+    }
+
+    public ShutterButton getShutterButton() {
+        return mShutter;
+    }
+
+    public void hideUI() {
+        mControlsBackground.setVisibility(View.INVISIBLE);
+        hideSwitcher();
+        mShutter.setVisibility(View.GONE);
+    }
+
+    public void showUI() {
+        mControlsBackground.setVisibility(View.VISIBLE);
+        showSwitcher();
+        mShutter.setVisibility(View.VISIBLE);
+        // Force a layout change to show shutter button
+        mShutter.requestLayout();
+    }
+
+    public void hideSwitcher() {
+        mSwitcher.closePopup();
+        mSwitcher.setVisibility(View.INVISIBLE);
+    }
+
+    public void showSwitcher() {
+        if (mCurrentModule.needsSwitcher()) {
+            mSwitcher.setVisibility(View.VISIBLE);
+        }
+    }
+
+    public boolean isInCameraApp() {
+        return mShowCameraAppView;
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration config) {
+        super.onConfigurationChanged(config);
+
+        ViewGroup appRoot = (ViewGroup) findViewById(R.id.content);
+        // remove old switcher, shutter and shutter icon
+        View cameraControlsView = findViewById(R.id.camera_shutter_switcher);
+        appRoot.removeView(cameraControlsView);
+
+        // create new layout with the current orientation
+        LayoutInflater inflater = getLayoutInflater();
+        inflater.inflate(R.layout.camera_shutter_switcher, appRoot);
+        init();
+
+        if (mShowCameraAppView) {
+            showUI();
+        } else {
+            hideUI();
+        }
+        mCurrentModule.onConfigurationChanged(config);
+    }
+
+    @Override
+    public void onPause() {
+        mPaused = true;
+        mOrientationListener.disable();
+        mCurrentModule.onPauseBeforeSuper();
+        super.onPause();
+        mCurrentModule.onPauseAfterSuper();
+    }
+
+    @Override
+    public void onResume() {
+        mPaused = false;
+        mOrientationListener.enable();
+        mCurrentModule.onResumeBeforeSuper();
+        super.onResume();
+        mCurrentModule.onResumeAfterSuper();
+    }
+
+    @Override
+    protected void onFullScreenChanged(boolean full) {
+        if (full) {
+            showUI();
+        } else {
+            hideUI();
+        }
+        super.onFullScreenChanged(full);
+        mCurrentModule.onFullScreenChanged(full);
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        mCurrentModule.onStop();
+        getStateManager().clearTasks();
+    }
+
+    @Override
+    protected void onNewIntent(Intent intent) {
+        super.onNewIntent(intent);
+        getStateManager().clearActivityResult();
+    }
+
+    @Override
+    protected void installIntentFilter() {
+        super.installIntentFilter();
+        mCurrentModule.installIntentFilter();
+    }
+
+    @Override
+    protected void onActivityResult(
+            int requestCode, int resultCode, Intent data) {
+        // Only PhotoPage understands ProxyLauncher.RESULT_USER_CANCELED
+        if (resultCode == ProxyLauncher.RESULT_USER_CANCELED
+                && !(getStateManager().getTopState() instanceof PhotoPage)) {
+            resultCode = RESULT_CANCELED;
+        }
+        super.onActivityResult(requestCode, resultCode, data);
+        // Unmap cancel vs. reset
+        if (resultCode == ProxyLauncher.RESULT_USER_CANCELED) {
+            resultCode = RESULT_CANCELED;
+        }
+        mCurrentModule.onActivityResult(requestCode, resultCode, data);
+    }
+
+    // Preview area is touched. Handle touch focus.
+    @Override
+    protected void onSingleTapUp(View view, int x, int y) {
+        mCurrentModule.onSingleTapUp(view, x, y);
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (!mCurrentModule.onBackPressed()) {
+            super.onBackPressed();
+        }
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        return mCurrentModule.onKeyDown(keyCode,  event)
+                || super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        return mCurrentModule.onKeyUp(keyCode,  event)
+                || super.onKeyUp(keyCode, event);
+    }
+
+    public void cancelActivityTouchHandling() {
+        if (mDown != null) {
+            MotionEvent cancel = MotionEvent.obtain(mDown);
+            cancel.setAction(MotionEvent.ACTION_CANCEL);
+            super.dispatchTouchEvent(cancel);
+        }
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent m) {
+        if (m.getActionMasked() == MotionEvent.ACTION_DOWN) {
+            mDown = m;
+        }
+        if ((mSwitcher != null) && mSwitcher.showsPopup() && !mSwitcher.isInsidePopup(m)) {
+            return mSwitcher.onTouch(null, m);
+        } else {
+            return mShutterSwitcher.dispatchTouchEvent(m)
+                    || mCurrentModule.dispatchTouchEvent(m);
+        }
+    }
+
+    @Override
+    public void startActivityForResult(Intent intent, int requestCode) {
+        Intent proxyIntent = new Intent(this, ProxyLauncher.class);
+        proxyIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+        proxyIntent.putExtra(Intent.EXTRA_INTENT, intent);
+        super.startActivityForResult(proxyIntent, requestCode);
+    }
+
+    public boolean superDispatchTouchEvent(MotionEvent m) {
+        return super.dispatchTouchEvent(m);
+    }
+
+    // Preview texture has been copied. Now camera can be released and the
+    // animation can be started.
+    @Override
+    public void onPreviewTextureCopied() {
+        mCurrentModule.onPreviewTextureCopied();
+    }
+
+    @Override
+    public void onCaptureTextureCopied() {
+        mCurrentModule.onCaptureTextureCopied();
+    }
+
+    @Override
+    public void onUserInteraction() {
+        super.onUserInteraction();
+        mCurrentModule.onUserInteraction();
+    }
+
+    @Override
+    protected boolean updateStorageHintOnResume() {
+        return mCurrentModule.updateStorageHintOnResume();
+    }
+
+    @Override
+    public void updateCameraAppView() {
+        super.updateCameraAppView();
+        mCurrentModule.updateCameraAppView();
+    }
+
+    private boolean canReuseScreenNail() {
+        return mCurrentModuleIndex == PHOTO_MODULE_INDEX
+                || mCurrentModuleIndex == VIDEO_MODULE_INDEX
+                || mCurrentModuleIndex == LIGHTCYCLE_MODULE_INDEX;
+    }
+
+    @Override
+    public boolean isPanoramaActivity() {
+        return (mCurrentModuleIndex == PANORAMA_MODULE_INDEX);
+    }
+
+    // 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;
+    }
+
+    public CameraScreenNail getCameraScreenNail() {
+        return (CameraScreenNail) mCameraScreenNail;
+    }
+}
diff --git a/src/com/android/camera/CameraBackupAgent.java b/src/com/android/camera/CameraBackupAgent.java
new file mode 100644
index 0000000..30ba212
--- /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 0000000..a65942d
--- /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 0000000..512809b
--- /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 0000000..22f800e
--- /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 0000000..8209055
--- /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 0000000..5b7bbfd
--- /dev/null
+++ b/src/com/android/camera/CameraHolder.java
@@ -0,0 +1,298 @@
+/*
+ * 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 = CameraManager.instance().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 0000000..854e105
--- /dev/null
+++ b/src/com/android/camera/CameraManager.java
@@ -0,0 +1,490 @@
+/*
+ * 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 static com.android.camera.Util.Assert;
+
+import android.annotation.TargetApi;
+import android.graphics.SurfaceTexture;
+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.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.view.SurfaceHolder;
+import android.util.Log;
+
+import com.android.gallery3d.common.ApiHelper;
+
+import java.io.IOException;
+
+public class CameraManager {
+    private static final String TAG = "CameraManager";
+    private static CameraManager sCameraManager = new CameraManager();
+
+    // Thread progress signals
+    private ConditionVariable mSig = new ConditionVariable();
+
+    private Parameters mParameters;
+    private IOException mReconnectException;
+
+    private static final int RELEASE = 1;
+    private static final int RECONNECT = 2;
+    private static final int UNLOCK = 3;
+    private static final int LOCK = 4;
+    private static final int SET_PREVIEW_TEXTURE_ASYNC = 5;
+    private static final int START_PREVIEW_ASYNC = 6;
+    private static final int STOP_PREVIEW = 7;
+    private static final int SET_PREVIEW_CALLBACK_WITH_BUFFER = 8;
+    private static final int ADD_CALLBACK_BUFFER = 9;
+    private static final int AUTO_FOCUS = 10;
+    private static final int CANCEL_AUTO_FOCUS = 11;
+    private static final int SET_AUTO_FOCUS_MOVE_CALLBACK = 12;
+    private static final int SET_DISPLAY_ORIENTATION = 13;
+    private static final int SET_ZOOM_CHANGE_LISTENER = 14;
+    private static final int SET_FACE_DETECTION_LISTENER = 15;
+    private static final int START_FACE_DETECTION = 16;
+    private static final int STOP_FACE_DETECTION = 17;
+    private static final int SET_ERROR_CALLBACK = 18;
+    private static final int SET_PARAMETERS = 19;
+    private static final int GET_PARAMETERS = 20;
+    private static final int SET_PARAMETERS_ASYNC = 21;
+    private static final int WAIT_FOR_IDLE = 22;
+    private static final int SET_PREVIEW_DISPLAY_ASYNC = 23;
+    private static final int SET_PREVIEW_CALLBACK = 24;
+    private static final int ENABLE_SHUTTER_SOUND = 25;
+
+    private Handler mCameraHandler;
+    private CameraProxy mCameraProxy;
+    private android.hardware.Camera mCamera;
+
+    public static CameraManager instance() {
+        return sCameraManager;
+    }
+
+    private CameraManager() {
+        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);
+        }
+
+        /*
+         * This method does not deal with the build version check.  Everyone should
+         * check first before sending message to this handler.
+         */
+        @Override
+        public void handleMessage(final Message msg) {
+            try {
+                switch (msg.what) {
+                    case RELEASE:
+                        mCamera.release();
+                        mCamera = null;
+                        mCameraProxy = null;
+                        break;
+
+                    case RECONNECT:
+                        mReconnectException = null;
+                        try {
+                            mCamera.reconnect();
+                        } catch (IOException ex) {
+                            mReconnectException = ex;
+                        }
+                        break;
+
+                    case UNLOCK:
+                        mCamera.unlock();
+                        break;
+
+                    case LOCK:
+                        mCamera.lock();
+                        break;
+
+                    case SET_PREVIEW_TEXTURE_ASYNC:
+                        setPreviewTexture(msg.obj);
+                        return;  // no need to call mSig.open()
+
+                    case SET_PREVIEW_DISPLAY_ASYNC:
+                        try {
+                            mCamera.setPreviewDisplay((SurfaceHolder) msg.obj);
+                        } catch(IOException e) {
+                            throw new RuntimeException(e);
+                        }
+                        return;  // no need to call mSig.open()
+
+                    case START_PREVIEW_ASYNC:
+                        mCamera.startPreview();
+                        return;  // no need to call mSig.open()
+
+                    case STOP_PREVIEW:
+                        mCamera.stopPreview();
+                        break;
+
+                    case SET_PREVIEW_CALLBACK_WITH_BUFFER:
+                        mCamera.setPreviewCallbackWithBuffer(
+                            (PreviewCallback) msg.obj);
+                        break;
+
+                    case ADD_CALLBACK_BUFFER:
+                        mCamera.addCallbackBuffer((byte[]) msg.obj);
+                        break;
+
+                    case AUTO_FOCUS:
+                        mCamera.autoFocus((AutoFocusCallback) msg.obj);
+                        break;
+
+                    case CANCEL_AUTO_FOCUS:
+                        mCamera.cancelAutoFocus();
+                        break;
+
+                    case SET_AUTO_FOCUS_MOVE_CALLBACK:
+                        setAutoFocusMoveCallback(mCamera, msg.obj);
+                        break;
+
+                    case SET_DISPLAY_ORIENTATION:
+                        mCamera.setDisplayOrientation(msg.arg1);
+                        break;
+
+                    case SET_ZOOM_CHANGE_LISTENER:
+                        mCamera.setZoomChangeListener(
+                            (OnZoomChangeListener) msg.obj);
+                        break;
+
+                    case SET_FACE_DETECTION_LISTENER:
+                        setFaceDetectionListener((FaceDetectionListener) msg.obj);
+                        break;
+
+                    case START_FACE_DETECTION:
+                        startFaceDetection();
+                        break;
+
+                    case STOP_FACE_DETECTION:
+                        stopFaceDetection();
+                        break;
+
+                    case SET_ERROR_CALLBACK:
+                        mCamera.setErrorCallback((ErrorCallback) msg.obj);
+                        break;
+
+                    case SET_PARAMETERS:
+                        mCamera.setParameters((Parameters) msg.obj);
+                        break;
+
+                    case GET_PARAMETERS:
+                        mParameters = mCamera.getParameters();
+                        break;
+
+                    case SET_PARAMETERS_ASYNC:
+                        mCamera.setParameters((Parameters) msg.obj);
+                        return;  // no need to call mSig.open()
+
+                    case SET_PREVIEW_CALLBACK:
+                        mCamera.setPreviewCallback((PreviewCallback) msg.obj);
+                        break;
+
+                    case ENABLE_SHUTTER_SOUND:
+                        enableShutterSound((msg.arg1 == 1) ? true : false);
+                        break;
+
+                    case WAIT_FOR_IDLE:
+                        // do nothing
+                        break;
+
+                    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;
+                    mCameraProxy = null;
+                }
+                throw e;
+            }
+            mSig.open();
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+    private void setAutoFocusMoveCallback(android.hardware.Camera camera,
+            Object cb) {
+        camera.setAutoFocusMoveCallback((AutoFocusMoveCallback) cb);
+    }
+
+    // Open camera synchronously. This method is invoked in the context of a
+    // background thread.
+    CameraProxy cameraOpen(int cameraId) {
+        // Cannot open camera in mCameraHandler, otherwise all camera events
+        // will be routed to mCameraHandler looper, which in turn will call
+        // event handler like Camera.onFaceDetection, which in turn will modify
+        // UI and cause exception like this:
+        // CalledFromWrongThreadException: Only the original thread that created
+        // a view hierarchy can touch its views.
+        mCamera = android.hardware.Camera.open(cameraId);
+        if (mCamera != null) {
+            mCameraProxy = new CameraProxy();
+            return mCameraProxy;
+        } else {
+            return null;
+        }
+    }
+
+    public class CameraProxy {
+        private CameraProxy() {
+            Assert(mCamera != null);
+        }
+
+        public android.hardware.Camera getCamera() {
+            return mCamera;
+        }
+
+        public void release() {
+            mSig.close();
+            mCameraHandler.sendEmptyMessage(RELEASE);
+            mSig.block();
+        }
+
+        public void reconnect() throws IOException {
+            mSig.close();
+            mCameraHandler.sendEmptyMessage(RECONNECT);
+            mSig.block();
+            if (mReconnectException != null) {
+                throw mReconnectException;
+            }
+        }
+
+        public void unlock() {
+            mSig.close();
+            mCameraHandler.sendEmptyMessage(UNLOCK);
+            mSig.block();
+        }
+
+        public void lock() {
+            mSig.close();
+            mCameraHandler.sendEmptyMessage(LOCK);
+            mSig.block();
+        }
+
+        @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+        public void setPreviewTextureAsync(final SurfaceTexture surfaceTexture) {
+            mCameraHandler.obtainMessage(SET_PREVIEW_TEXTURE_ASYNC, surfaceTexture).sendToTarget();
+        }
+
+        public void setPreviewDisplayAsync(final SurfaceHolder surfaceHolder) {
+            mCameraHandler.obtainMessage(SET_PREVIEW_DISPLAY_ASYNC, surfaceHolder).sendToTarget();
+        }
+
+        public void startPreviewAsync() {
+            mCameraHandler.sendEmptyMessage(START_PREVIEW_ASYNC);
+        }
+
+        public void stopPreview() {
+            mSig.close();
+            mCameraHandler.sendEmptyMessage(STOP_PREVIEW);
+            mSig.block();
+        }
+
+        public void setPreviewCallback(final PreviewCallback cb) {
+            mSig.close();
+            mCameraHandler.obtainMessage(SET_PREVIEW_CALLBACK, cb).sendToTarget();
+            mSig.block();
+        }
+
+        public void setPreviewCallbackWithBuffer(final PreviewCallback cb) {
+            mSig.close();
+            mCameraHandler.obtainMessage(SET_PREVIEW_CALLBACK_WITH_BUFFER, cb).sendToTarget();
+            mSig.block();
+        }
+
+        public void addCallbackBuffer(byte[] callbackBuffer) {
+            mSig.close();
+            mCameraHandler.obtainMessage(ADD_CALLBACK_BUFFER, callbackBuffer).sendToTarget();
+            mSig.block();
+        }
+
+        public void autoFocus(AutoFocusCallback cb) {
+            mSig.close();
+            mCameraHandler.obtainMessage(AUTO_FOCUS, cb).sendToTarget();
+            mSig.block();
+        }
+
+        public void cancelAutoFocus() {
+            mSig.close();
+            mCameraHandler.sendEmptyMessage(CANCEL_AUTO_FOCUS);
+            mSig.block();
+        }
+
+        @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+        public void setAutoFocusMoveCallback(AutoFocusMoveCallback cb) {
+            mSig.close();
+            mCameraHandler.obtainMessage(SET_AUTO_FOCUS_MOVE_CALLBACK, cb).sendToTarget();
+            mSig.block();
+        }
+
+        public void takePicture(final ShutterCallback shutter, final PictureCallback raw,
+                final PictureCallback postview, final PictureCallback jpeg) {
+            mSig.close();
+            // Too many parameters, so use post for simplicity
+            mCameraHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mCamera.takePicture(shutter, raw, postview, jpeg);
+                    mSig.open();
+                }
+            });
+            mSig.block();
+        }
+
+        public void takePicture2(final ShutterCallback shutter, final PictureCallback raw,
+                final PictureCallback postview, final PictureCallback jpeg,
+                final int cameraState, final int focusState) {
+            mSig.close();
+            // Too many parameters, so use post for simplicity
+            mCameraHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        mCamera.takePicture(shutter, raw, postview, jpeg);
+                    } catch (RuntimeException e) {
+                        Log.w(TAG, "take picture failed; cameraState:" + cameraState
+                            + ", focusState:" + focusState);
+                        throw e;
+                    }
+                    mSig.open();
+                }
+            });
+            mSig.block();
+        }
+
+        public void setDisplayOrientation(int degrees) {
+            mSig.close();
+            mCameraHandler.obtainMessage(SET_DISPLAY_ORIENTATION, degrees, 0)
+                    .sendToTarget();
+            mSig.block();
+        }
+
+        public void setZoomChangeListener(OnZoomChangeListener listener) {
+            mSig.close();
+            mCameraHandler.obtainMessage(SET_ZOOM_CHANGE_LISTENER, listener).sendToTarget();
+            mSig.block();
+        }
+
+        @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+        public void setFaceDetectionListener(FaceDetectionListener listener) {
+            mSig.close();
+            mCameraHandler.obtainMessage(SET_FACE_DETECTION_LISTENER, listener).sendToTarget();
+            mSig.block();
+        }
+
+        public void startFaceDetection() {
+            mSig.close();
+            mCameraHandler.sendEmptyMessage(START_FACE_DETECTION);
+            mSig.block();
+        }
+
+        public void stopFaceDetection() {
+            mSig.close();
+            mCameraHandler.sendEmptyMessage(STOP_FACE_DETECTION);
+            mSig.block();
+        }
+
+        public void setErrorCallback(ErrorCallback cb) {
+            mSig.close();
+            mCameraHandler.obtainMessage(SET_ERROR_CALLBACK, cb).sendToTarget();
+            mSig.block();
+        }
+
+        public void setParameters(Parameters params) {
+            mSig.close();
+            mCameraHandler.obtainMessage(SET_PARAMETERS, params).sendToTarget();
+            mSig.block();
+        }
+
+        public void setParametersAsync(Parameters params) {
+            mCameraHandler.removeMessages(SET_PARAMETERS_ASYNC);
+            mCameraHandler.obtainMessage(SET_PARAMETERS_ASYNC, params).sendToTarget();
+        }
+
+        public Parameters getParameters() {
+            mSig.close();
+            mCameraHandler.sendEmptyMessage(GET_PARAMETERS);
+            mSig.block();
+            Parameters parameters = mParameters;
+            mParameters = null;
+            return parameters;
+        }
+
+        public void enableShutterSound(boolean enable) {
+            mSig.close();
+            mCameraHandler.obtainMessage(
+                    ENABLE_SHUTTER_SOUND, (enable ? 1 : 0), 0).sendToTarget();
+            mSig.block();
+        }
+
+        public void waitForIdle() {
+            mSig.close();
+            mCameraHandler.sendEmptyMessage(WAIT_FOR_IDLE);
+            mSig.block();
+        }
+    }
+}
diff --git a/src/com/android/camera/CameraModule.java b/src/com/android/camera/CameraModule.java
new file mode 100644
index 0000000..8e022d6
--- /dev/null
+++ b/src/com/android/camera/CameraModule.java
@@ -0,0 +1,75 @@
+/*
+ * 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, boolean reuseScreenNail);
+
+    public void onFullScreenChanged(boolean full);
+
+    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 boolean dispatchTouchEvent(MotionEvent m);
+
+    public void onPreviewTextureCopied();
+
+    public void onCaptureTextureCopied();
+
+    public void onUserInteraction();
+
+    public boolean updateStorageHintOnResume();
+
+    public void updateCameraAppView();
+
+    public boolean collapseCameraControls();
+
+    public boolean needsSwitcher();
+
+    public void onOrientationChanged(int orientation);
+
+    public void onShowSwitcherPopup();
+
+}
diff --git a/src/com/android/camera/CameraPreference.java b/src/com/android/camera/CameraPreference.java
new file mode 100644
index 0000000..0a4e9b3
--- /dev/null
+++ b/src/com/android/camera/CameraPreference.java
@@ -0,0 +1,61 @@
+/*
+ * 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;
+
+/**
+ * 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 0000000..5d3c5c0
--- /dev/null
+++ b/src/com/android/camera/CameraScreenNail.java
@@ -0,0 +1,497 @@
+/*
+ * 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.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 = new CaptureAnimManager();
+    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;
+        }
+    };
+    private DrawClient mDraw = mDefaultDraw;
+
+    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();
+    }
+
+    public CameraScreenNail(Listener listener) {
+        mListener = listener;
+    }
+
+    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) {
+            // Ignore the case where animateFlash is skipped but animateSlide is called
+            // e.g. Double tap shutter and immediately swipe to gallery, and quickly swipe back
+            // to camera. This case only happens in monkey tests, not applicable to normal
+            // human beings.
+            if (mAnimState != ANIM_CAPTURE_RUNNING) {
+                Log.v(TAG, "Cannot animateSlide outside of animateCapture!"
+                        + " Animation state = " + mAnimState);
+                return;
+            }
+            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;
+            }
+
+            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(x, y, width, height);
+                    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);
+                    }
+                } 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);
+                }
+            }
+            callbackIfNeeded();
+        } // mLock
+    }
+
+    private void copyPreviewTexture(GLCanvas canvas) {
+        if (!mDraw.requiresSurfaceTexture() && mAnimTexture == null) {
+            mAnimTexture = new RawTexture(getTextureWidth(), getTextureHeight(), true);
+            mAnimTexture.setIsFlippedVertically(true);
+        }
+        int width = mAnimTexture.getWidth();
+        int height = mAnimTexture.getHeight();
+        canvas.beginRenderTarget(mAnimTexture);
+        if (!mDraw.requiresSurfaceTexture()) {
+            mDraw.onDraw(canvas, 0, 0, width, height);
+        } else {
+            // 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();
+            }
+        }
+    }
+}
diff --git a/src/com/android/camera/CameraSettings.java b/src/com/android/camera/CameraSettings.java
new file mode 100644
index 0000000..3bc58a0
--- /dev/null
+++ b/src/com/android/camera/CameraSettings.java
@@ -0,0 +1,582 @@
+/*
+ * 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.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 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;
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    public static String getDefaultVideoQuality(int cameraId,
+            String defaultQuality) {
+        if (ApiHelper.HAS_FINE_RESOLUTION_QUALITY_LEVELS) {
+            if (CamcorderProfile.hasProfile(
+                    cameraId, Integer.valueOf(defaultQuality))) {
+                return defaultQuality;
+            }
+        }
+        return Integer.toString(CamcorderProfile.QUALITY_HIGH);
+    }
+
+    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());
+        }
+
+        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 = (int) FloatMath.floor(max * step);
+        int minValue = (int) FloatMath.ceil(min * step);
+        CharSequence entries[] = new CharSequence[maxValue - minValue + 1];
+        CharSequence entryValues[] = 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[maxValue - i] = Integer.toString(Math.round(i / step));
+            StringBuilder builder = new StringBuilder();
+            if (i > 0) builder.append('+');
+            entries[maxValue - i] = builder.append(i).toString();
+            icons[maxValue - i] = iconIds.getResourceId(3 + i, 0);
+        }
+        exposure.setUseSingleIcon(true);
+        exposure.setEntries(entries);
+        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 ArrayList<String> getSupportedVideoQuality() {
+        ArrayList<String> supported = new ArrayList<String>();
+        // Check for supported quality
+        if (ApiHelper.HAS_FINE_RESOLUTION_QUALITY_LEVELS) {
+            getFineResolutionQuality(supported);
+        } else {
+            supported.add(Integer.toString(CamcorderProfile.QUALITY_HIGH));
+            CamcorderProfile high = CamcorderProfile.get(
+                    mCameraId, CamcorderProfile.QUALITY_HIGH);
+            CamcorderProfile low = CamcorderProfile.get(
+                    mCameraId, CamcorderProfile.QUALITY_LOW);
+            if (high.videoFrameHeight * high.videoFrameWidth >
+                    low.videoFrameHeight * low.videoFrameWidth) {
+                supported.add(Integer.toString(CamcorderProfile.QUALITY_LOW));
+            }
+        }
+
+        return supported;
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    private void getFineResolutionQuality(ArrayList<String> supported) {
+        if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_1080P)) {
+            supported.add(Integer.toString(CamcorderProfile.QUALITY_1080P));
+        }
+        if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_720P)) {
+            supported.add(Integer.toString(CamcorderProfile.QUALITY_720P));
+        }
+        if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_480P)) {
+            supported.add(Integer.toString(CamcorderProfile.QUALITY_480P));
+        }
+    }
+
+    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 0000000..64383af
--- /dev/null
+++ b/src/com/android/camera/CaptureAnimManager.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.graphics.Color;
+import android.os.SystemClock;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.RawTexture;
+
+/**
+ * Class to handle the capture animation.
+ */
+public class CaptureAnimManager {
+    @SuppressWarnings("unused")
+    private static final String TAG = "CAM_Capture";
+    private static final int TIME_FLASH = 200;
+    private static final int TIME_HOLD = 400;
+    private static final int TIME_SLIDE = 400;  // milliseconds.
+
+    private static final int ANIM_BOTH = 0;
+    private static final int ANIM_FLASH = 1;
+    private static final int ANIM_SLIDE = 2;
+
+    private final Interpolator mSlideInterpolator = new DecelerateInterpolator();
+
+    private 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 float mDelta;
+    private int mDrawWidth;
+    private int mDrawHeight;
+    private int mAnimType;
+
+    /* preview: camera preview view.
+     * review: view of picture just taken.
+     */
+    public CaptureAnimManager() {
+    }
+
+    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;
+    }
+
+    // x, y, w and h: the rectangle area where the animation takes place.
+    public void startAnimation(int x, int y, int w, int h) {
+        mAnimStartTime = SystemClock.uptimeMillis();
+        // Set the views to the initial positions.
+        mDrawWidth = w;
+        mDrawHeight = h;
+        mX = x;
+        mY = y;
+        switch (mAnimOrientation) {
+            case 0:  // Preview is on the left.
+                mDelta = w;
+                break;
+            case 90:  // Preview is below.
+                mDelta = -h;
+                break;
+            case 180:  // Preview on the right.
+                mDelta = -w;
+                break;
+            case 270:  // Preview is above.
+                mDelta = h;
+                break;
+        }
+    }
+
+    // Returns true if the animation has been drawn.
+    public boolean drawAnimation(GLCanvas canvas, CameraScreenNail preview,
+                RawTexture review) {
+        long timeDiff = SystemClock.uptimeMillis() - mAnimStartTime;
+        // Check if the animation is over
+        if (mAnimType == ANIM_SLIDE && timeDiff > TIME_SLIDE) return false;
+        if (mAnimType == ANIM_BOTH && timeDiff > TIME_HOLD + TIME_SLIDE) return false;
+
+        int animStep = mAnimType;
+        if (mAnimType == ANIM_BOTH) {
+            animStep = (timeDiff < TIME_HOLD) ? ANIM_FLASH : ANIM_SLIDE;
+            if (animStep == ANIM_SLIDE) {
+                timeDiff -= TIME_HOLD;
+            }
+        }
+
+        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 = (float) (timeDiff) / TIME_SLIDE;
+            float x = mX;
+            float y = mY;
+            if (mAnimOrientation == 0 || mAnimOrientation == 180) {
+                x = x + mDelta * mSlideInterpolator.getInterpolation(fraction);
+            } else {
+                y = y + mDelta * mSlideInterpolator.getInterpolation(fraction);
+            }
+            // float alpha = canvas.getAlpha();
+            // canvas.setAlpha(fraction);
+            preview.directDraw(canvas, (int) mX, (int) mY,
+                    mDrawWidth, mDrawHeight);
+            // canvas.setAlpha(alpha);
+
+            review.draw(canvas, (int) x, (int) y, mDrawWidth, mDrawHeight);
+        } else {
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/src/com/android/camera/ComboPreferences.java b/src/com/android/camera/ComboPreferences.java
new file mode 100644
index 0000000..af1476e
--- /dev/null
+++ b/src/com/android/camera/ComboPreferences.java
@@ -0,0 +1,332 @@
+/*
+ * 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 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 BackupManager mBackupManager;
+    private CopyOnWriteArrayList<OnSharedPreferenceChangeListener> mListeners;
+    private static WeakHashMap<Context, ComboPreferences> sMap =
+            new WeakHashMap<Context, ComboPreferences>();
+
+    public ComboPreferences(Context context) {
+        mPrefGlobal = context.getSharedPreferences(
+                getGlobalSharedPreferencesName(context), Context.MODE_PRIVATE);
+        mPrefGlobal.registerOnSharedPreferenceChangeListener(this);
+
+        synchronized (sMap) {
+            sMap.put(context, this);
+        }
+        mBackupManager = new BackupManager(context);
+        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);
+    }
+
+    @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) {
+        if (mPrefLocal.contains(key)) return true;
+        if (mPrefGlobal.contains(key)) return true;
+        return false;
+    }
+
+    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);
+        }
+        mBackupManager.dataChanged();
+    }
+}
diff --git a/src/com/android/camera/CountDownTimerPreference.java b/src/com/android/camera/CountDownTimerPreference.java
new file mode 100644
index 0000000..6c0f673
--- /dev/null
+++ b/src/com/android/camera/CountDownTimerPreference.java
@@ -0,0 +1,51 @@
+/*
+ * 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 java.util.List;
+
+/* CountDownTimerPreference generates entries (i.e. what users see in the UI),
+ * and entry values (the actual value recorded in preference) in
+ * initCountDownTimeChoices(Context context), rather than reading the entries
+ * from a predefined list. When the entry values are a continuous list of numbers,
+ * (e.g. 0-60), it is more efficient to auto generate the list than to predefine it.*/
+public class CountDownTimerPreference extends ListPreference {
+    private final static int MAX_DURATION = 60;
+    public CountDownTimerPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        initCountDownDurationChoices(context);
+    }
+
+    private void initCountDownDurationChoices(Context context) {
+        CharSequence[] entryValues = new CharSequence[MAX_DURATION + 1];
+        CharSequence[] entries = new CharSequence[MAX_DURATION + 1];
+        for (int i = 0; i <= MAX_DURATION; i++) {
+            entryValues[i] = Integer.toString(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 0000000..3517405
--- /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 0000000..4601ab9
--- /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.common.ApiHelper;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+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.setPreviewTextureAsync(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.setPreviewTextureAsync(mTextureSource);
+
+            mCameraDevice.startPreviewAsync();
+
+            // 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.setPreviewTextureAsync(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 0000000..6055565
--- /dev/null
+++ b/src/com/android/camera/Exif.java
@@ -0,0 +1,74 @@
+/*
+ * 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.ExifInvalidFormatException;
+import com.android.gallery3d.exif.ExifParser;
+import com.android.gallery3d.exif.ExifTag;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class Exif {
+    private static final String TAG = "CameraExif";
+
+    // Returns the degrees in clockwise. Values are 0, 90, 180, or 270.
+    public static int getOrientation(byte[] jpeg) {
+        if (jpeg == null) return 0;
+
+        InputStream is = new ByteArrayInputStream(jpeg);
+
+        try {
+            ExifParser parser = ExifParser.parse(is, ExifParser.OPTION_IFD_0);
+            int event = parser.next();
+            while(event != ExifParser.EVENT_END) {
+                if (event == ExifParser.EVENT_NEW_TAG) {
+                    ExifTag tag = parser.getTag();
+                    if (tag.getTagId() == ExifTag.TAG_ORIENTATION &&
+                            tag.hasValue()) {
+                        int orient = (int) tag.getValueAt(0);
+                        switch (orient) {
+                            case ExifTag.Orientation.TOP_LEFT:
+                                return 0;
+                            case ExifTag.Orientation.BOTTOM_LEFT:
+                                return 180;
+                            case ExifTag.Orientation.RIGHT_TOP:
+                                return 90;
+                            case ExifTag.Orientation.RIGHT_BOTTOM:
+                                return 270;
+                            default:
+                                Log.i(TAG, "Unsupported orientation");
+                                return 0;
+                        }
+                    }
+                }
+                event = parser.next();
+            }
+            Log.i(TAG, "Orientation not found");
+            return 0;
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to read EXIF orientation", e);
+            return 0;
+        } catch (ExifInvalidFormatException e) {
+            Log.w(TAG, "Failed to read EXIF orientation", e);
+            return 0;
+        }
+    }
+}
diff --git a/src/com/android/camera/FocusOverlayManager.java b/src/com/android/camera/FocusOverlayManager.java
new file mode 100644
index 0000000..2bec187
--- /dev/null
+++ b/src/com/android/camera/FocusOverlayManager.java
@@ -0,0 +1,560 @@
+/*
+ * 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.camera.ui.FaceView;
+import com.android.camera.ui.FocusIndicator;
+import com.android.camera.ui.PieRenderer;
+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 PieRenderer mPieRenderer;
+
+    private int mPreviewWidth; // The width of the preview frame layout.
+    private int mPreviewHeight; // The height of the preview frame layout.
+    private boolean mMirror; // true if the camera is front-facing.
+    private int mDisplayOrientation;
+    private FaceView mFaceView;
+    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;
+
+    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) {
+        mHandler = new MainHandler(looper);
+        mMatrix = new Matrix();
+        mPreferences = preferences;
+        mDefaultFocusModes = defaultFocusModes;
+        setParameters(parameters);
+        mListener = listener;
+        setMirror(mirror);
+    }
+
+    public void setFocusRenderer(PieRenderer renderer) {
+        mPieRenderer = renderer;
+        mInitialized = (mMatrix != null);
+    }
+
+    public void setParameters(Parameters parameters) {
+        // parameters can only be null when onConfigurationChanged is called
+        // before camera is open. We will just return in this case, because
+        // parameters will be set again later with the right parameters after
+        // camera is open.
+        if (parameters == null) return;
+        mParameters = parameters;
+        mFocusAreaSupported = 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();
+    }
+
+    public void setFaceView(FaceView faceView) {
+        mFaceView = faceView;
+    }
+
+    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 = (mPieRenderer != null);
+        }
+    }
+
+    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 (mFocusArea != null) {
+                mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY);
+            }
+            if (shutterButtonPressed) {
+                // Lock AE & AWB so users can half-press shutter and recompose.
+                lockAeAwbIfNeeded();
+            }
+        } else if (mState == STATE_IDLE) {
+            // User has released the focus key before focus completes.
+            // Do nothing.
+        }
+    }
+
+    public void onAutoFocusMoving(boolean moving) {
+        if (!mInitialized) return;
+        // Ignore if the camera has detected some faces.
+        if (mFaceView != null && mFaceView.faceExists()) {
+            mPieRenderer.clear();
+            return;
+        }
+
+        // Ignore if we have requested autofocus. This method only handles
+        // continuous autofocus.
+        if (mState != STATE_IDLE) return;
+
+        if (moving) {
+            mPieRenderer.showStart();
+        } else {
+            mPieRenderer.showSuccess(true);
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    private void initializeFocusAreas(int focusWidth, int focusHeight,
+            int x, int y, int previewWidth, int previewHeight) {
+        if (mFocusArea == null) {
+            mFocusArea = new ArrayList<Object>();
+            mFocusArea.add(new Area(new Rect(), 1));
+        }
+
+        // Convert the coordinates to driver format.
+        calculateTapArea(focusWidth, focusHeight, 1f, x, y, previewWidth, previewHeight,
+                ((Area) mFocusArea.get(0)).rect);
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    private void initializeMeteringAreas(int focusWidth, int focusHeight,
+            int x, int y, int previewWidth, int previewHeight) {
+        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(focusWidth, focusHeight, 1.5f, x, y, previewWidth, previewHeight,
+                ((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 ((mFocusArea != null) && (mState == STATE_FOCUSING ||
+                    mState == STATE_SUCCESS || mState == STATE_FAIL)) {
+            cancelAutoFocus();
+        }
+        // Initialize variables.
+        int focusWidth = mPieRenderer.getSize();
+        int focusHeight = mPieRenderer.getSize();
+        if (focusWidth == 0 || mPieRenderer.getWidth() == 0
+                || mPieRenderer.getHeight() == 0) return;
+        int previewWidth = mPreviewWidth;
+        int previewHeight = mPreviewHeight;
+        // Initialize mFocusArea.
+        if (mFocusAreaSupported) {
+            initializeFocusAreas(
+                    focusWidth, focusHeight, x, y, previewWidth, previewHeight);
+        }
+        // Initialize mMeteringArea.
+        if (mMeteringAreaSupported) {
+            initializeMeteringAreas(
+                    focusWidth, focusHeight, x, y, previewWidth, previewHeight);
+        }
+
+        // Use margin to set the focus indicator to the touched area.
+        mPieRenderer.setFocus(x, y);
+
+        // 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.
+        if (mFaceView != null) mFaceView.pause();
+        updateFocusUI();
+        mHandler.removeMessages(RESET_TOUCH_FOCUS);
+    }
+
+    private void cancelAutoFocus() {
+        Log.v(TAG, "Cancel autofocus.");
+
+        // Reset the tap area before calling mListener.cancelAutofocus.
+        // Otherwise, focus mode stays at auto and the tap area passed to the
+        // driver is not reset.
+        resetTouchFocus();
+        mListener.cancelAutoFocus();
+        if (mFaceView != null) mFaceView.resume();
+        mState = STATE_IDLE;
+        updateFocusUI();
+        mHandler.removeMessages(RESET_TOUCH_FOCUS);
+    }
+
+    private void capture() {
+        if (mListener.capture()) {
+            mState = STATE_IDLE;
+            mHandler.removeMessages(RESET_TOUCH_FOCUS);
+        }
+    }
+
+    public String getFocusMode() {
+        if (mOverrideFocusMode != null) return mOverrideFocusMode;
+        List<String> supportedFocusModes = mParameters.getSupportedFocusModes();
+
+        if (mFocusAreaSupported && mFocusArea != null) {
+            // 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.
+        boolean faceExists = (mFaceView != null && mFaceView.faceExists());
+        FocusIndicator focusIndicator = (faceExists) ? mFaceView : mPieRenderer;
+
+        if (mState == STATE_IDLE) {
+            if (mFocusArea == null) {
+                focusIndicator.clear();
+            } else {
+                // Users touch on the preview and the indicator represents the
+                // metering area. Either focus area is not supported or
+                // autoFocus call is not required.
+                focusIndicator.showStart();
+            }
+        } else if (mState == STATE_FOCUSING || mState == STATE_FOCUSING_SNAP_ON_FINISH) {
+            focusIndicator.showStart();
+        } else {
+            if (Util.FOCUS_MODE_CONTINUOUS_PICTURE.equals(mFocusMode)) {
+                // TODO: check HAL behavior and decide if this can be removed.
+                focusIndicator.showSuccess(false);
+            } else if (mState == STATE_SUCCESS) {
+                focusIndicator.showSuccess(false);
+            } else if (mState == STATE_FAIL) {
+                focusIndicator.showFail(false);
+            }
+        }
+    }
+
+    public void resetTouchFocus() {
+        if (!mInitialized) return;
+
+        // Put focus indicator to the center. clear reset position
+        mPieRenderer.clear();
+
+        mFocusArea = null;
+        mMeteringArea = null;
+    }
+
+    private void calculateTapArea(int focusWidth, int focusHeight, float areaMultiple,
+            int x, int y, int previewWidth, int previewHeight, Rect rect) {
+        int areaWidth = (int) (focusWidth * areaMultiple);
+        int areaHeight = (int) (focusHeight * areaMultiple);
+        int left = Util.clamp(x - areaWidth / 2, 0, previewWidth - areaWidth);
+        int top = Util.clamp(y - areaHeight / 2, 0, previewHeight - areaHeight);
+
+        RectF rectF = new RectF(left, top, left + areaWidth, top + areaHeight);
+        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 0000000..6bcd59d
--- /dev/null
+++ b/src/com/android/camera/IconListPreference.java
@@ -0,0 +1,115 @@
+/*
+ * 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 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/IntArray.java b/src/com/android/camera/IntArray.java
new file mode 100644
index 0000000..a2550db
--- /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 0000000..17266ea
--- /dev/null
+++ b/src/com/android/camera/ListPreference.java
@@ -0,0 +1,181 @@
+/*
+ * 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 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 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));
+        a.recycle();
+    }
+
+    public String getKey() {
+        return mKey;
+    }
+
+    public CharSequence[] getEntries() {
+        return mEntries;
+    }
+
+    public CharSequence[] getEntryValues() {
+        return mEntryValues;
+    }
+
+    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 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 String getEntry() {
+        return mEntries[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 0000000..fcf21b6
--- /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/MediaSaver.java b/src/com/android/camera/MediaSaver.java
new file mode 100644
index 0000000..a3d582e
--- /dev/null
+++ b/src/com/android/camera/MediaSaver.java
@@ -0,0 +1,149 @@
+/*
+ * 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.ContentResolver;
+import android.location.Location;
+import android.net.Uri;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+// We use a queue to store the SaveRequests that have not been completed
+// yet. The main thread puts the request into the queue. The saver thread
+// gets it from the queue, does the work, and removes it from the queue.
+//
+// The main thread needs to wait for the saver thread to finish all the work
+// in the queue, when the activity's onPause() is called, we need to finish
+// all the work, so other programs (like Gallery) can see all the images.
+//
+// If the queue becomes too long, adding a new request will block the main
+// thread until the queue length drops below the threshold (QUEUE_LIMIT).
+// If we don't do this, we may face several problems: (1) We may OOM
+// because we are holding all the jpeg data in memory. (2) We may ANR
+// when we need to wait for saver thread finishing all the work (in
+// onPause() or gotoGallery()) because the time to finishing a long queue
+// of work may be too long.
+class MediaSaver extends Thread {
+    private static final int SAVE_QUEUE_LIMIT = 3;
+    private static final String TAG = "MediaSaver";
+
+    private ArrayList<SaveRequest> mQueue;
+    private boolean mStop;
+    private ContentResolver mContentResolver;
+
+    public interface OnMediaSavedListener {
+        public void onMediaSaved(Uri uri);
+    }
+
+    public MediaSaver(ContentResolver resolver) {
+        mContentResolver = resolver;
+        mQueue = new ArrayList<SaveRequest>();
+        start();
+    }
+
+    // Runs in main thread
+    public synchronized boolean queueFull() {
+        return (mQueue.size() >= SAVE_QUEUE_LIMIT);
+    }
+
+    // Runs in main thread
+    public void addImage(final byte[] data, String title, long date, Location loc,
+                         int width, int height, int orientation, OnMediaSavedListener l) {
+        SaveRequest r = new SaveRequest();
+        r.data = data;
+        r.date = date;
+        r.title = title;
+        r.loc = (loc == null) ? null : new Location(loc);  // make a copy
+        r.width = width;
+        r.height = height;
+        r.orientation = orientation;
+        r.listener = l;
+        synchronized (this) {
+            while (mQueue.size() >= SAVE_QUEUE_LIMIT) {
+                try {
+                    wait();
+                } catch (InterruptedException ex) {
+                    // ignore.
+                }
+            }
+            mQueue.add(r);
+            notifyAll();  // Tell saver thread there is new work to do.
+        }
+    }
+
+    // Runs in saver thread
+    @Override
+    public void run() {
+        while (true) {
+            SaveRequest r;
+            synchronized (this) {
+                if (mQueue.isEmpty()) {
+                    notifyAll();  // notify main thread in waitDone
+
+                    // Note that we can only stop after we saved all images
+                    // in the queue.
+                    if (mStop) break;
+
+                    try {
+                        wait();
+                    } catch (InterruptedException ex) {
+                        // ignore.
+                    }
+                    continue;
+                }
+                if (mStop) break;
+                r = mQueue.remove(0);
+                notifyAll();  // the main thread may wait in addImage
+            }
+            Uri uri = storeImage(r.data, r.title, r.date, r.loc, r.width, r.height,
+                    r.orientation);
+            r.listener.onMediaSaved(uri);
+        }
+        if (!mQueue.isEmpty()) {
+            Log.e(TAG, "Media saver thread stopped with " + mQueue.size() + " images unsaved");
+            mQueue.clear();
+        }
+    }
+
+    // Runs in main thread
+    public void finish() {
+        synchronized (this) {
+            mStop = true;
+            notifyAll();
+        }
+    }
+
+    // Runs in saver thread
+    private Uri storeImage(final byte[] data, String title, long date,
+                           Location loc, int width, int height, int orientation) {
+        Uri uri = Storage.addImage(mContentResolver, title, date, loc,
+                                   orientation, data, width, height);
+        return uri;
+    }
+
+    // Each SaveRequest remembers the data needed to save an image.
+    private static class SaveRequest {
+        byte[] data;
+        String title;
+        long date;
+        Location loc;
+        int width, height;
+        int orientation;
+        OnMediaSavedListener listener;
+    }
+}
diff --git a/src/com/android/camera/Mosaic.java b/src/com/android/camera/Mosaic.java
new file mode 100644
index 0000000..78876c3
--- /dev/null
+++ b/src/com/android/camera/Mosaic.java
@@ -0,0 +1,206 @@
+/*
+ * 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;
+
+/**
+ * The Java interface to JNI calls regarding mosaic stitching.
+ *
+ * A high-level usage is:
+ *
+ * Mosaic mosaic = new Mosaic();
+ * mosaic.setSourceImageDimensions(width, height);
+ * mosaic.reset(blendType);
+ *
+ * while ((pixels = hasNextImage()) != null) {
+ *    mosaic.setSourceImage(pixels);
+ * }
+ *
+ * mosaic.createMosaic(highRes);
+ * byte[] result = mosaic.getFinalMosaic();
+ *
+ */
+public class Mosaic {
+    /**
+     * In this mode, the images are stitched together in the same spatial arrangement as acquired
+     * i.e. if the user follows a curvy trajectory, the image boundary of the resulting mosaic will
+     * be curved in the same manner. This mode is useful if the user wants to capture a mosaic as
+     * if "painting" the scene using the smart-phone device and does not want any corrective warps
+     * to distort the captured images.
+     */
+    public static final int BLENDTYPE_FULL = 0;
+
+    /**
+     * This mode is the same as BLENDTYPE_FULL except that the resulting mosaic is rotated
+     * to balance the first and last images to be approximately at the same vertical offset in the
+     * output mosaic. This is useful when acquiring a mosaic by a typical panning-like motion to
+     * remove a one-sided curve in the mosaic (typically due to the camera not staying horizontal
+     * during the video capture) and convert it to a more symmetrical "smiley-face" like output.
+     */
+    public static final int BLENDTYPE_PAN = 1;
+
+    /**
+     * This mode compensates for typical "smiley-face" like output in longer mosaics and creates
+     * a rectangular mosaic with minimal black borders (by unwrapping the mosaic onto an imaginary
+     * cylinder). If the user follows a curved trajectory (instead of a perfect panning trajectory),
+     * the resulting mosaic here may suffer from some image distortions in trying to map the
+     * trajectory to a cylinder.
+     */
+    public static final int BLENDTYPE_CYLINDERPAN = 2;
+
+    /**
+     * This mode is basically BLENDTYPE_CYLINDERPAN plus doing a rectangle cropping before returning
+     * the mosaic. The mode is useful for making the resulting mosaic have a rectangle shape.
+     */
+    public static final int BLENDTYPE_HORIZONTAL =3;
+
+    /**
+     * This strip type will use the default thin strips where the strips are
+     * spaced according to the image capture rate.
+     */
+    public static final int STRIPTYPE_THIN = 0;
+
+    /**
+     * This strip type will use wider strips for blending. The strip separation
+     * is controlled by a threshold on the native side. Since the strips are
+     * wider, there is an additional cross-fade blending step to make the seam
+     * boundaries smoother. Since this mode uses lesser image frames, it is
+     * computationally more efficient than the thin strip mode.
+     */
+    public static final int STRIPTYPE_WIDE = 1;
+
+    /**
+     * Return flags returned by createMosaic() are one of the following.
+     */
+    public static final int MOSAIC_RET_OK = 1;
+    public static final int MOSAIC_RET_ERROR = -1;
+    public static final int MOSAIC_RET_CANCELLED = -2;
+    public static final int MOSAIC_RET_LOW_TEXTURE = -3;
+    public static final int MOSAIC_RET_FEW_INLIERS = 2;
+
+
+    static {
+        System.loadLibrary("jni_mosaic");
+    }
+
+    /**
+     * Allocate memory for the image frames at the given resolution.
+     *
+     * @param width width of the input frames in pixels
+     * @param height height of the input frames in pixels
+     */
+    public native void allocateMosaicMemory(int width, int height);
+
+    /**
+     * Free memory allocated by allocateMosaicMemory.
+     *
+     */
+    public native void freeMosaicMemory();
+
+    /**
+     * Pass the input image frame to the native layer. Each time the a new
+     * source image t is set, the transformation matrix from the first source
+     * image to t is computed and returned.
+     *
+     * @param pixels source image of NV21 format.
+     * @return Float array of length 11; first 9 entries correspond to the 3x3
+     *         transformation matrix between the first frame and the passed frame;
+     *         the 10th entry is the number of the passed frame, where the counting
+     *         starts from 1; and the 11th entry is the returning code, whose value
+     *         is one of those MOSAIC_RET_* returning flags defined above.
+     */
+    public native float[] setSourceImage(byte[] pixels);
+
+    /**
+     * This is an alternative to the setSourceImage function above. This should
+     * be called when the image data is already on the native side in a fixed
+     * byte array. In implementation, this array is filled by the GL thread
+     * using glReadPixels directly from GPU memory (where it is accessed by
+     * an associated SurfaceTexture).
+     *
+     * @return Float array of length 11; first 9 entries correspond to the 3x3
+     *         transformation matrix between the first frame and the passed frame;
+     *         the 10th entry is the number of the passed frame, where the counting
+     *         starts from 1; and the 11th entry is the returning code, whose value
+     *         is one of those MOSAIC_RET_* returning flags defined above.
+     */
+    public native float[] setSourceImageFromGPU();
+
+    /**
+     * Set the type of blending.
+     *
+     * @param type the blending type defined in the class. {BLENDTYPE_FULL,
+     *        BLENDTYPE_PAN, BLENDTYPE_CYLINDERPAN, BLENDTYPE_HORIZONTAL}
+     */
+    public native void setBlendingType(int type);
+
+    /**
+     * Set the type of strips to use for blending.
+     * @param type the blending strip type to use {STRIPTYPE_THIN,
+     * STRIPTYPE_WIDE}.
+     */
+    public native void setStripType(int type);
+
+    /**
+     * Tell the native layer to create the final mosaic after all the input frame
+     * data have been collected.
+     * The case of generating high-resolution mosaic may take dozens of seconds to finish.
+     *
+     * @param value True means generating a high-resolution mosaic -
+     *        which is based on the original images set in setSourceImage().
+     *        False means generating a low-resolution version -
+     *        which is based on 1/4 downscaled images from the original images.
+     * @return Returns a status code suggesting if the mosaic building was
+     *        successful, in error, or was cancelled by the user.
+     */
+    public native int createMosaic(boolean value);
+
+    /**
+     * Get the data for the created mosaic.
+     *
+     * @return Returns an integer array which contains the final mosaic in the ARGB_8888 format.
+     *         The first MosaicWidth*MosaicHeight values contain the image data, followed by 2
+     *         integers corresponding to the values MosaicWidth and MosaicHeight respectively.
+     */
+    public native int[] getFinalMosaic();
+
+    /**
+     * Get the data for the created mosaic.
+     *
+     * @return Returns a byte array which contains the final mosaic in the NV21 format.
+     *         The first MosaicWidth*MosaicHeight*1.5 values contain the image data, followed by
+     *         8 bytes which pack the MosaicWidth and MosaicHeight integers into 4 bytes each
+     *         respectively.
+     */
+    public native byte[] getFinalMosaicNV21();
+
+    /**
+     * Reset the state of the frame arrays which maintain the captured frame data.
+     * Also re-initializes the native mosaic object to make it ready for capturing a new mosaic.
+     */
+    public native void reset();
+
+    /**
+     * Get the progress status of the mosaic computation process.
+     * @param hires Boolean flag to select whether to report progress of the
+     *              low-res or high-res mosaicer.
+     * @param cancelComputation Boolean flag to allow cancelling the
+     *              mosaic computation when needed from the GUI end.
+     * @return Returns a number from 0-100 where 50 denotes that the mosaic
+     *          computation is 50% done.
+     */
+    public native int reportProgress(boolean hires, boolean cancelComputation);
+}
diff --git a/src/com/android/camera/MosaicFrameProcessor.java b/src/com/android/camera/MosaicFrameProcessor.java
new file mode 100644
index 0000000..c59e6b9
--- /dev/null
+++ b/src/com/android/camera/MosaicFrameProcessor.java
@@ -0,0 +1,236 @@
+/*
+ * 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.util.Log;
+
+/**
+ * Class to handle the processing of each frame by Mosaicer.
+ */
+public class MosaicFrameProcessor {
+    private static final String TAG = "MosaicFrameProcessor";
+    private static final int NUM_FRAMES_IN_BUFFER = 2;
+    private static final int MAX_NUMBER_OF_FRAMES = 100;
+    private static final int MOSAIC_RET_CODE_INDEX = 10;
+    private static final int FRAME_COUNT_INDEX = 9;
+    private static final int X_COORD_INDEX = 2;
+    private static final int Y_COORD_INDEX = 5;
+    private static final int HR_TO_LR_DOWNSAMPLE_FACTOR = 4;
+    private static final int WINDOW_SIZE = 3;
+
+    private Mosaic mMosaicer;
+    private boolean mIsMosaicMemoryAllocated = false;
+    private float mTranslationLastX;
+    private float mTranslationLastY;
+
+    private int mFillIn = 0;
+    private int mTotalFrameCount = 0;
+    private int mLastProcessFrameIdx = -1;
+    private int mCurrProcessFrameIdx = -1;
+    private boolean mFirstRun;
+
+    // Panning rate is in unit of percentage of image content translation per
+    // frame. Use moving average to calculate the panning rate.
+    private float mPanningRateX;
+    private float mPanningRateY;
+
+    private float[] mDeltaX = new float[WINDOW_SIZE];
+    private float[] mDeltaY = new float[WINDOW_SIZE];
+    private int mOldestIdx = 0;
+    private float mTotalTranslationX = 0f;
+    private float mTotalTranslationY = 0f;
+
+    private ProgressListener mProgressListener;
+
+    private int mPreviewWidth;
+    private int mPreviewHeight;
+    private int mPreviewBufferSize;
+
+    private static MosaicFrameProcessor sMosaicFrameProcessor; // singleton
+
+    public interface ProgressListener {
+        public void onProgress(boolean isFinished, float panningRateX, float panningRateY,
+                float progressX, float progressY);
+    }
+
+    public static MosaicFrameProcessor getInstance() {
+        if (sMosaicFrameProcessor == null) {
+            sMosaicFrameProcessor = new MosaicFrameProcessor();
+        }
+        return sMosaicFrameProcessor;
+    }
+
+    private MosaicFrameProcessor() {
+        mMosaicer = new Mosaic();
+    }
+
+    public void setProgressListener(ProgressListener listener) {
+        mProgressListener = listener;
+    }
+
+    public int reportProgress(boolean hires, boolean cancel) {
+        return mMosaicer.reportProgress(hires, cancel);
+    }
+
+    public void initialize(int previewWidth, int previewHeight, int bufSize) {
+        mPreviewWidth = previewWidth;
+        mPreviewHeight = previewHeight;
+        mPreviewBufferSize = bufSize;
+        setupMosaicer(mPreviewWidth, mPreviewHeight, mPreviewBufferSize);
+        setStripType(Mosaic.STRIPTYPE_WIDE);
+        reset();
+    }
+
+    public void clear() {
+        if (mIsMosaicMemoryAllocated) {
+            mMosaicer.freeMosaicMemory();
+            mIsMosaicMemoryAllocated = false;
+        }
+        synchronized (this) {
+            notify();
+        }
+    }
+
+    public boolean isMosaicMemoryAllocated() {
+        return mIsMosaicMemoryAllocated;
+    }
+
+    public void setStripType(int type) {
+        mMosaicer.setStripType(type);
+    }
+
+    private void setupMosaicer(int previewWidth, int previewHeight, int bufSize) {
+        Log.v(TAG, "setupMosaicer w, h=" + previewWidth + ',' + previewHeight + ',' + bufSize);
+
+        if (mIsMosaicMemoryAllocated) throw new RuntimeException("MosaicFrameProcessor in use!");
+        mIsMosaicMemoryAllocated = true;
+        mMosaicer.allocateMosaicMemory(previewWidth, previewHeight);
+    }
+
+    public void reset() {
+        // reset() can be called even if MosaicFrameProcessor is not initialized.
+        // Only counters will be changed.
+        mFirstRun = true;
+        mTotalFrameCount = 0;
+        mFillIn = 0;
+        mTotalTranslationX = 0;
+        mTranslationLastX = 0;
+        mTotalTranslationY = 0;
+        mTranslationLastY = 0;
+        mPanningRateX = 0;
+        mPanningRateY = 0;
+        mLastProcessFrameIdx = -1;
+        mCurrProcessFrameIdx = -1;
+        for (int i = 0; i < WINDOW_SIZE; ++i) {
+            mDeltaX[i] = 0f;
+            mDeltaY[i] = 0f;
+        }
+        mMosaicer.reset();
+    }
+
+    public int createMosaic(boolean highRes) {
+        return mMosaicer.createMosaic(highRes);
+    }
+
+    public byte[] getFinalMosaicNV21() {
+        return mMosaicer.getFinalMosaicNV21();
+    }
+
+    // Processes the last filled image frame through the mosaicer and
+    // updates the UI to show progress.
+    // When done, processes and displays the final mosaic.
+    public void processFrame() {
+        if (!mIsMosaicMemoryAllocated) {
+            // clear() is called and buffers are cleared, stop computation.
+            // This can happen when the onPause() is called in the activity, but still some frames
+            // are not processed yet and thus the callback may be invoked.
+            return;
+        }
+
+        mCurrProcessFrameIdx = mFillIn;
+        mFillIn = ((mFillIn + 1) % NUM_FRAMES_IN_BUFFER);
+
+        // Check that we are trying to process a frame different from the
+        // last one processed (useful if this class was running asynchronously)
+        if (mCurrProcessFrameIdx != mLastProcessFrameIdx) {
+            mLastProcessFrameIdx = mCurrProcessFrameIdx;
+
+            // TODO: make the termination condition regarding reaching
+            // MAX_NUMBER_OF_FRAMES solely determined in the library.
+            if (mTotalFrameCount < MAX_NUMBER_OF_FRAMES) {
+                // If we are still collecting new frames for the current mosaic,
+                // process the new frame.
+                calculateTranslationRate();
+
+                // Publish progress of the ongoing processing
+                if (mProgressListener != null) {
+                    mProgressListener.onProgress(false, mPanningRateX, mPanningRateY,
+                            mTranslationLastX * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewWidth,
+                            mTranslationLastY * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewHeight);
+                }
+            } else {
+                if (mProgressListener != null) {
+                    mProgressListener.onProgress(true, mPanningRateX, mPanningRateY,
+                            mTranslationLastX * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewWidth,
+                            mTranslationLastY * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewHeight);
+                }
+            }
+        }
+    }
+
+    public void calculateTranslationRate() {
+        float[] frameData = mMosaicer.setSourceImageFromGPU();
+        int ret_code = (int) frameData[MOSAIC_RET_CODE_INDEX];
+        mTotalFrameCount  = (int) frameData[FRAME_COUNT_INDEX];
+        float translationCurrX = frameData[X_COORD_INDEX];
+        float translationCurrY = frameData[Y_COORD_INDEX];
+
+        if (mFirstRun) {
+            // First time: no need to update delta values.
+            mTranslationLastX = translationCurrX;
+            mTranslationLastY = translationCurrY;
+            mFirstRun = false;
+            return;
+        }
+
+        // Moving average: remove the oldest translation/deltaTime and
+        // add the newest translation/deltaTime in
+        int idx = mOldestIdx;
+        mTotalTranslationX -= mDeltaX[idx];
+        mTotalTranslationY -= mDeltaY[idx];
+        mDeltaX[idx] = Math.abs(translationCurrX - mTranslationLastX);
+        mDeltaY[idx] = Math.abs(translationCurrY - mTranslationLastY);
+        mTotalTranslationX += mDeltaX[idx];
+        mTotalTranslationY += mDeltaY[idx];
+
+        // The panning rate is measured as the rate of the translation percentage in
+        // image width/height. Take the horizontal panning rate for example, the image width
+        // used in finding the translation is (PreviewWidth / HR_TO_LR_DOWNSAMPLE_FACTOR).
+        // To get the horizontal translation percentage, the horizontal translation,
+        // (translationCurrX - mTranslationLastX), is divided by the
+        // image width. We then get the rate by dividing the translation percentage with the
+        // number of frames.
+        mPanningRateX = mTotalTranslationX /
+                (mPreviewWidth / HR_TO_LR_DOWNSAMPLE_FACTOR) / WINDOW_SIZE;
+        mPanningRateY = mTotalTranslationY /
+                (mPreviewHeight / HR_TO_LR_DOWNSAMPLE_FACTOR) / WINDOW_SIZE;
+
+        mTranslationLastX = translationCurrX;
+        mTranslationLastY = translationCurrY;
+        mOldestIdx = (mOldestIdx + 1) % WINDOW_SIZE;
+    }
+}
diff --git a/src/com/android/camera/MosaicPreviewRenderer.java b/src/com/android/camera/MosaicPreviewRenderer.java
new file mode 100644
index 0000000..e12fe43
--- /dev/null
+++ b/src/com/android/camera/MosaicPreviewRenderer.java
@@ -0,0 +1,264 @@
+/*
+ * 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.graphics.SurfaceTexture;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import com.android.gallery3d.common.ApiHelper;
+
+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;
+
+@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) // uses SurfaceTexture
+public class MosaicPreviewRenderer {
+    private static final String TAG = "MosaicPreviewRenderer";
+    private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
+    private static final boolean DEBUG = false;
+
+    private int mWidth; // width of the view in UI
+    private int mHeight; // height of the view in UI
+
+    private boolean mIsLandscape = true;
+    private final float[] mTransformMatrix = new float[16];
+
+    private ConditionVariable mEglThreadBlockVar = new ConditionVariable();
+    private HandlerThread mEglThread;
+    private EGLHandler mEglHandler;
+
+    private EGLConfig mEglConfig;
+    private EGLDisplay mEglDisplay;
+    private EGLContext mEglContext;
+    private EGLSurface mEglSurface;
+    private SurfaceTexture mMosaicOutputSurfaceTexture;
+    private SurfaceTexture mInputSurfaceTexture;
+    private EGL10 mEgl;
+    private GL10 mGl;
+
+    private class EGLHandler extends Handler {
+        public static final int MSG_INIT_EGL_SYNC = 0;
+        public static final int MSG_SHOW_PREVIEW_FRAME_SYNC = 1;
+        public static final int MSG_SHOW_PREVIEW_FRAME = 2;
+        public static final int MSG_ALIGN_FRAME_SYNC = 3;
+        public static final int MSG_RELEASE = 4;
+
+        public EGLHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_INIT_EGL_SYNC:
+                    doInitGL();
+                    mEglThreadBlockVar.open();
+                    break;
+                case MSG_SHOW_PREVIEW_FRAME_SYNC:
+                    doShowPreviewFrame();
+                    mEglThreadBlockVar.open();
+                    break;
+                case MSG_SHOW_PREVIEW_FRAME:
+                    doShowPreviewFrame();
+                    break;
+                case MSG_ALIGN_FRAME_SYNC:
+                    doAlignFrame();
+                    mEglThreadBlockVar.open();
+                    break;
+                case MSG_RELEASE:
+                    doRelease();
+                    break;
+            }
+        }
+
+        private void doAlignFrame() {
+            mInputSurfaceTexture.updateTexImage();
+            mInputSurfaceTexture.getTransformMatrix(mTransformMatrix);
+
+            MosaicRenderer.setWarping(true);
+            // Call preprocess to render it to low-res and high-res RGB textures.
+            MosaicRenderer.preprocess(mTransformMatrix);
+            // Now, transfer the textures from GPU to CPU memory for processing
+            MosaicRenderer.transferGPUtoCPU();
+            MosaicRenderer.updateMatrix();
+            draw();
+            mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
+        }
+
+        private void doShowPreviewFrame() {
+            mInputSurfaceTexture.updateTexImage();
+            mInputSurfaceTexture.getTransformMatrix(mTransformMatrix);
+
+            MosaicRenderer.setWarping(false);
+            // Call preprocess to render it to low-res and high-res RGB textures.
+            MosaicRenderer.preprocess(mTransformMatrix);
+            MosaicRenderer.updateMatrix();
+            draw();
+            mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
+        }
+
+        private void doInitGL() {
+            // These are copied from GLSurfaceView
+            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, mMosaicOutputSurfaceTexture, 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();
+
+            mInputSurfaceTexture = new SurfaceTexture(MosaicRenderer.init());
+            MosaicRenderer.reset(mWidth, mHeight, mIsLandscape);
+        }
+
+        private void doRelease() {
+            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;
+            releaseSurfaceTexture(mInputSurfaceTexture);
+            mEglThread.quit();
+        }
+
+        @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+        private void releaseSurfaceTexture(SurfaceTexture st) {
+            if (ApiHelper.HAS_RELEASE_SURFACE_TEXTURE) {
+                st.release();
+            }
+        }
+
+        // Should be called from other thread.
+        public void sendMessageSync(int msg) {
+            mEglThreadBlockVar.close();
+            sendEmptyMessage(msg);
+            mEglThreadBlockVar.block();
+        }
+
+    }
+
+    public MosaicPreviewRenderer(SurfaceTexture tex, int w, int h, boolean isLandscape) {
+        mMosaicOutputSurfaceTexture = tex;
+        mWidth = w;
+        mHeight = h;
+        mIsLandscape = isLandscape;
+
+        mEglThread = new HandlerThread("PanoramaRealtimeRenderer");
+        mEglThread.start();
+        mEglHandler = new EGLHandler(mEglThread.getLooper());
+
+        // We need to sync this because the generation of surface texture for input is
+        // done here and the client will continue with the assumption that the
+        // generation is completed.
+        mEglHandler.sendMessageSync(EGLHandler.MSG_INIT_EGL_SYNC);
+    }
+
+    public void release() {
+        mEglHandler.sendEmptyMessage(EGLHandler.MSG_RELEASE);
+    }
+
+    public void showPreviewFrameSync() {
+        mEglHandler.sendMessageSync(EGLHandler.MSG_SHOW_PREVIEW_FRAME_SYNC);
+    }
+
+    public void showPreviewFrame() {
+        mEglHandler.sendEmptyMessage(EGLHandler.MSG_SHOW_PREVIEW_FRAME);
+    }
+
+    public void alignFrameSync() {
+        mEglHandler.sendMessageSync(EGLHandler.MSG_ALIGN_FRAME_SYNC);
+    }
+
+    public SurfaceTexture getInputSurfaceTexture() {
+        return mInputSurfaceTexture;
+    }
+
+    private void draw() {
+        MosaicRenderer.step();
+    }
+
+    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_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/MosaicRenderer.java b/src/com/android/camera/MosaicRenderer.java
new file mode 100644
index 0000000..c50ca0d
--- /dev/null
+++ b/src/com/android/camera/MosaicRenderer.java
@@ -0,0 +1,89 @@
+/*
+ * 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;
+
+/**
+ * The Java interface to JNI calls regarding mosaic preview rendering.
+ *
+ */
+public class MosaicRenderer
+{
+     static
+     {
+         System.loadLibrary("jni_mosaic");
+     }
+
+     /**
+      * Function to be called in onSurfaceCreated() to initialize
+      * the GL context, load and link the shaders and create the
+      * program. Returns a texture ID to be used for SurfaceTexture.
+      *
+      * @return textureID the texture ID of the newly generated texture to
+      *          be assigned to the SurfaceTexture object.
+      */
+     public static native int init();
+
+     /**
+      * Pass the drawing surface's width and height to initialize the
+      * renderer viewports and FBO dimensions.
+      *
+      * @param width width of the drawing surface in pixels.
+      * @param height height of the drawing surface in pixels.
+      * @param isLandscapeOrientation is the orientation of the activity layout in landscape.
+      */
+     public static native void reset(int width, int height, boolean isLandscapeOrientation);
+
+     /**
+      * Calling this function will render the SurfaceTexture to a new 2D texture
+      * using the provided STMatrix.
+      *
+      * @param stMatrix texture coordinate transform matrix obtained from the
+      *        Surface texture
+      */
+     public static native void preprocess(float[] stMatrix);
+
+     /**
+      * This function calls glReadPixels to transfer both the low-res and high-res
+      * data from the GPU memory to the CPU memory for further processing by the
+      * mosaicing library.
+      */
+     public static native void transferGPUtoCPU();
+
+     /**
+      * Function to be called in onDrawFrame() to update the screen with
+      * the new frame data.
+      */
+     public static native void step();
+
+     /**
+      * Call this function when a new low-res frame has been processed by
+      * the mosaicing library. This will tell the renderer library to
+      * update its texture and warping transformation. Any calls to step()
+      * after this call will use the new image frame and transformation data.
+      */
+     public static native void updateMatrix();
+
+     /**
+      * This function allows toggling between showing the input image data
+      * (without applying any warp) and the warped image data. For running
+      * the renderer as a viewfinder, we set the flag to false. To see the
+      * preview mosaic, we set the flag to true.
+      *
+      * @param flag boolean flag to set the warping to true or false.
+      */
+     public static native void setWarping(boolean flag);
+}
diff --git a/src/com/android/camera/OnClickAttr.java b/src/com/android/camera/OnClickAttr.java
new file mode 100644
index 0000000..07a1063
--- /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 0000000..80063e4
--- /dev/null
+++ b/src/com/android/camera/OnScreenHint.java
@@ -0,0 +1,188 @@
+/*
+ * 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;
+
+/**
+ * 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/PanoProgressBar.java b/src/com/android/camera/PanoProgressBar.java
new file mode 100644
index 0000000..8dfb366
--- /dev/null
+++ b/src/com/android/camera/PanoProgressBar.java
@@ -0,0 +1,188 @@
+/*
+ * 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.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+class PanoProgressBar extends ImageView {
+    @SuppressWarnings("unused")
+    private static final String TAG = "PanoProgressBar";
+    public static final int DIRECTION_NONE = 0;
+    public static final int DIRECTION_LEFT = 1;
+    public static final int DIRECTION_RIGHT = 2;
+    private float mProgress = 0;
+    private float mMaxProgress = 0;
+    private float mLeftMostProgress = 0;
+    private float mRightMostProgress = 0;
+    private float mProgressOffset = 0;
+    private float mIndicatorWidth = 0;
+    private int mDirection = 0;
+    private final Paint mBackgroundPaint = new Paint();
+    private final Paint mDoneAreaPaint = new Paint();
+    private final Paint mIndicatorPaint = new Paint();
+    private float mWidth;
+    private float mHeight;
+    private RectF mDrawBounds;
+    private OnDirectionChangeListener mListener = null;
+
+    public interface OnDirectionChangeListener {
+        public void onDirectionChange(int direction);
+    }
+
+    public PanoProgressBar(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mDoneAreaPaint.setStyle(Paint.Style.FILL);
+        mDoneAreaPaint.setAlpha(0xff);
+
+        mBackgroundPaint.setStyle(Paint.Style.FILL);
+        mBackgroundPaint.setAlpha(0xff);
+
+        mIndicatorPaint.setStyle(Paint.Style.FILL);
+        mIndicatorPaint.setAlpha(0xff);
+
+        mDrawBounds = new RectF();
+    }
+
+    public void setOnDirectionChangeListener(OnDirectionChangeListener l) {
+        mListener = l;
+    }
+
+    private void setDirection(int direction) {
+        if (mDirection != direction) {
+            mDirection = direction;
+            if (mListener != null) {
+                mListener.onDirectionChange(mDirection);
+            }
+            invalidate();
+        }
+    }
+
+    public int getDirection() {
+        return mDirection;
+    }
+
+    @Override
+    public void setBackgroundColor(int color) {
+        mBackgroundPaint.setColor(color);
+        invalidate();
+    }
+
+    public void setDoneColor(int color) {
+        mDoneAreaPaint.setColor(color);
+        invalidate();
+    }
+
+    public void setIndicatorColor(int color) {
+        mIndicatorPaint.setColor(color);
+        invalidate();
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        mWidth = w;
+        mHeight = h;
+        mDrawBounds.set(0, 0, mWidth, mHeight);
+    }
+
+    public void setMaxProgress(int progress) {
+        mMaxProgress = progress;
+    }
+
+    public void setIndicatorWidth(float w) {
+        mIndicatorWidth = w;
+        invalidate();
+    }
+
+    public void setRightIncreasing(boolean rightIncreasing) {
+        if (rightIncreasing) {
+            mLeftMostProgress = 0;
+            mRightMostProgress = 0;
+            mProgressOffset = 0;
+            setDirection(DIRECTION_RIGHT);
+        } else {
+            mLeftMostProgress = mWidth;
+            mRightMostProgress = mWidth;
+            mProgressOffset = mWidth;
+            setDirection(DIRECTION_LEFT);
+        }
+        invalidate();
+    }
+
+    public void setProgress(int progress) {
+        // The panning direction will be decided after user pan more than 10 degrees in one
+        // direction.
+        if (mDirection == DIRECTION_NONE) {
+            if (progress > 10) {
+                setRightIncreasing(true);
+            } else if (progress < -10) {
+                setRightIncreasing(false);
+            }
+        }
+        // mDirection might be modified by setRightIncreasing() above. Need to check again.
+        if (mDirection != DIRECTION_NONE) {
+            mProgress = progress * mWidth / mMaxProgress + mProgressOffset;
+            // value bounds.
+            mProgress = Math.min(mWidth, Math.max(0, mProgress));
+            if (mDirection == DIRECTION_RIGHT) {
+                // The right most progress is adjusted.
+                mRightMostProgress = Math.max(mRightMostProgress, mProgress);
+            }
+            if (mDirection == DIRECTION_LEFT) {
+                // The left most progress is adjusted.
+                mLeftMostProgress = Math.min(mLeftMostProgress, mProgress);
+            }
+            invalidate();
+        }
+    }
+
+    public void reset() {
+        mProgress = 0;
+        mProgressOffset = 0;
+        setDirection(DIRECTION_NONE);
+        invalidate();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        // the background
+        canvas.drawRect(mDrawBounds, mBackgroundPaint);
+        if (mDirection != DIRECTION_NONE) {
+            // the progress area
+            canvas.drawRect(mLeftMostProgress, mDrawBounds.top, mRightMostProgress,
+                    mDrawBounds.bottom, mDoneAreaPaint);
+            // the indication bar
+            float l;
+            float r;
+            if (mDirection == DIRECTION_RIGHT) {
+                l = Math.max(mProgress - mIndicatorWidth, 0f);
+                r = mProgress;
+            } else {
+                l = mProgress;
+                r = Math.min(mProgress + mIndicatorWidth, mWidth);
+            }
+            canvas.drawRect(l, mDrawBounds.top, r, mDrawBounds.bottom, mIndicatorPaint);
+        }
+
+        // draw the mask image on the top for shaping.
+        super.onDraw(canvas);
+    }
+}
diff --git a/src/com/android/camera/PanoUtil.java b/src/com/android/camera/PanoUtil.java
new file mode 100644
index 0000000..e50eacc
--- /dev/null
+++ b/src/com/android/camera/PanoUtil.java
@@ -0,0 +1,86 @@
+/*
+ * 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.text.SimpleDateFormat;
+import java.util.Date;
+
+public class PanoUtil {
+    public static String createName(String format, long dateTaken) {
+        Date date = new Date(dateTaken);
+        SimpleDateFormat dateFormat = new SimpleDateFormat(format);
+        return dateFormat.format(date);
+    }
+
+    // TODO: Add comments about the range of these two arguments.
+    public static double calculateDifferenceBetweenAngles(double firstAngle,
+            double secondAngle) {
+        double difference1 = (secondAngle - firstAngle) % 360;
+        if (difference1 < 0) {
+            difference1 += 360;
+        }
+
+        double difference2 = (firstAngle - secondAngle) % 360;
+        if (difference2 < 0) {
+            difference2 += 360;
+        }
+
+        return Math.min(difference1, difference2);
+    }
+
+    public static void decodeYUV420SPQuarterRes(int[] rgb, byte[] yuv420sp, int width, int height) {
+        final int frameSize = width * height;
+
+        for (int j = 0, ypd = 0; j < height; j += 4) {
+            int uvp = frameSize + (j >> 1) * width, u = 0, v = 0;
+            for (int i = 0; i < width; i += 4, ypd++) {
+                int y = (0xff & (yuv420sp[j * width + i])) - 16;
+                if (y < 0) {
+                    y = 0;
+                }
+                if ((i & 1) == 0) {
+                    v = (0xff & yuv420sp[uvp++]) - 128;
+                    u = (0xff & yuv420sp[uvp++]) - 128;
+                    uvp += 2;  // Skip the UV values for the 4 pixels skipped in between
+                }
+                int y1192 = 1192 * y;
+                int r = (y1192 + 1634 * v);
+                int g = (y1192 - 833 * v - 400 * u);
+                int b = (y1192 + 2066 * u);
+
+                if (r < 0) {
+                    r = 0;
+                } else if (r > 262143) {
+                    r = 262143;
+                }
+                if (g < 0) {
+                    g = 0;
+                } else if (g > 262143) {
+                    g = 262143;
+                }
+                if (b < 0) {
+                    b = 0;
+                } else if (b > 262143) {
+                    b = 262143;
+                }
+
+                rgb[ypd] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) |
+                        ((b >> 10) & 0xff);
+            }
+        }
+    }
+}
diff --git a/src/com/android/camera/PanoramaModule.java b/src/com/android/camera/PanoramaModule.java
new file mode 100644
index 0000000..1829087
--- /dev/null
+++ b/src/com/android/camera/PanoramaModule.java
@@ -0,0 +1,1312 @@
+/*
+ * 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.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ImageFormat;
+import android.graphics.Matrix;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.graphics.YuvImage;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.Size;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PowerManager;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.OrientationEventListener;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.camera.CameraManager.CameraProxy;
+import com.android.camera.ui.LayoutChangeNotifier;
+import com.android.camera.ui.LayoutNotifyView;
+import com.android.camera.ui.PopupManager;
+import com.android.camera.ui.Rotatable;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.exif.ExifData;
+import com.android.gallery3d.exif.ExifInvalidFormatException;
+import com.android.gallery3d.exif.ExifOutputStream;
+import com.android.gallery3d.exif.ExifReader;
+import com.android.gallery3d.exif.ExifTag;
+import com.android.gallery3d.ui.GLRootView;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.TimeZone;
+
+/**
+ * Activity to handle panorama capturing.
+ */
+@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) // uses SurfaceTexture
+public class PanoramaModule implements CameraModule,
+        SurfaceTexture.OnFrameAvailableListener,
+        ShutterButton.OnShutterButtonListener,
+        LayoutChangeNotifier.Listener {
+
+    public static final int DEFAULT_SWEEP_ANGLE = 160;
+    public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL;
+    public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720;
+
+    private static final int MSG_LOW_RES_FINAL_MOSAIC_READY = 1;
+    private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 2;
+    private static final int MSG_END_DIALOG_RESET_TO_PREVIEW = 3;
+    private static final int MSG_CLEAR_SCREEN_DELAY = 4;
+    private static final int MSG_CONFIG_MOSAIC_PREVIEW = 5;
+    private static final int MSG_RESET_TO_PREVIEW = 6;
+
+    private static final int SCREEN_DELAY = 2 * 60 * 1000;
+
+    private static final String TAG = "CAM PanoModule";
+    private static final int PREVIEW_STOPPED = 0;
+    private static final int PREVIEW_ACTIVE = 1;
+    private static final int CAPTURE_STATE_VIEWFINDER = 0;
+    private static final int CAPTURE_STATE_MOSAIC = 1;
+    // The unit of speed is degrees per frame.
+    private static final float PANNING_SPEED_THRESHOLD = 2.5f;
+
+    private ContentResolver mContentResolver;
+
+    private GLRootView mGLRootView;
+    private ViewGroup mPanoLayout;
+    private LinearLayout mCaptureLayout;
+    private View mReviewLayout;
+    private ImageView mReview;
+    private View mCaptureIndicator;
+    private PanoProgressBar mPanoProgressBar;
+    private PanoProgressBar mSavingProgressBar;
+    private Matrix mProgressDirectionMatrix = new Matrix();
+    private float[] mProgressAngle = new float[2];
+    private LayoutNotifyView mPreviewArea;
+    private View mLeftIndicator;
+    private View mRightIndicator;
+    private MosaicPreviewRenderer mMosaicPreviewRenderer;
+    private Object mRendererLock = new Object();
+    private TextView mTooFastPrompt;
+    private ShutterButton mShutterButton;
+    private Object mWaitObject = new Object();
+
+    private String mPreparePreviewString;
+    private String mDialogTitle;
+    private String mDialogOkString;
+    private String mDialogPanoramaFailedString;
+    private String mDialogWaitingPreviousString;
+
+    private int mIndicatorColor;
+    private int mIndicatorColorFast;
+    private int mReviewBackground;
+
+    private boolean mUsingFrontCamera;
+    private int mPreviewWidth;
+    private int mPreviewHeight;
+    private int mCameraState;
+    private int mCaptureState;
+    private PowerManager.WakeLock mPartialWakeLock;
+    private MosaicFrameProcessor mMosaicFrameProcessor;
+    private boolean mMosaicFrameProcessorInitialized;
+    private AsyncTask <Void, Void, Void> mWaitProcessorTask;
+    private long mTimeTaken;
+    private Handler mMainHandler;
+    private SurfaceTexture mCameraTexture;
+    private boolean mThreadRunning;
+    private boolean mCancelComputation;
+    private float mHorizontalViewAngle;
+    private float mVerticalViewAngle;
+
+    // Prefer FOCUS_MODE_INFINITY to FOCUS_MODE_CONTINUOUS_VIDEO because of
+    // getting a better image quality by the former.
+    private String mTargetFocusMode = Parameters.FOCUS_MODE_INFINITY;
+
+    private PanoOrientationEventListener mOrientationEventListener;
+    // The value could be 0, 90, 180, 270 for the 4 different orientations measured in clockwise
+    // respectively.
+    private int mDeviceOrientation;
+    private int mDeviceOrientationAtCapture;
+    private int mCameraOrientation;
+    private int mOrientationCompensation;
+
+    private RotateDialogController mRotateDialog;
+
+    private SoundClips.Player mSoundPlayer;
+
+    private Runnable mOnFrameAvailableRunnable;
+
+    private CameraActivity mActivity;
+    private View mRootView;
+    private CameraProxy mCameraDevice;
+    private boolean mPaused;
+    private boolean mIsCreatingRenderer;
+    private boolean mIsConfigPending;
+
+    private class MosaicJpeg {
+        public MosaicJpeg(byte[] data, int width, int height) {
+            this.data = data;
+            this.width = width;
+            this.height = height;
+            this.isValid = true;
+        }
+
+        public MosaicJpeg() {
+            this.data = null;
+            this.width = 0;
+            this.height = 0;
+            this.isValid = false;
+        }
+
+        public final byte[] data;
+        public final int width;
+        public final int height;
+        public final boolean isValid;
+    }
+
+    private class PanoOrientationEventListener extends OrientationEventListener {
+        public PanoOrientationEventListener(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;
+            mDeviceOrientation = Util.roundOrientation(orientation, mDeviceOrientation);
+            // When the screen is unlocked, display rotation may change. Always
+            // calculate the up-to-date orientationCompensation.
+            int orientationCompensation = mDeviceOrientation
+                    + Util.getDisplayRotation(mActivity) % 360;
+            if (mOrientationCompensation != orientationCompensation) {
+                mOrientationCompensation = orientationCompensation;
+                mActivity.getGLRoot().requestLayoutContentPane();
+            }
+        }
+    }
+
+    @Override
+    public void init(CameraActivity activity, View parent, boolean reuseScreenNail) {
+        mActivity = activity;
+        mRootView = parent;
+
+        createContentView();
+
+        mContentResolver = mActivity.getContentResolver();
+        if (reuseScreenNail) {
+            mActivity.reuseCameraScreenNail(true);
+        } else {
+            mActivity.createCameraScreenNail(true);
+        }
+
+        // This runs in UI thread.
+        mOnFrameAvailableRunnable = new Runnable() {
+            @Override
+            public void run() {
+                // Frames might still be available after the activity is paused.
+                // If we call onFrameAvailable after pausing, the GL thread will crash.
+                if (mPaused) return;
+
+                MosaicPreviewRenderer renderer = null;
+                synchronized (mRendererLock) {
+                    try {
+                        while (mMosaicPreviewRenderer == null) {
+                            mRendererLock.wait();
+                        }
+                        renderer = mMosaicPreviewRenderer;
+                    } catch (InterruptedException e) {
+                        Log.e(TAG, "Unexpected interruption", e);
+                    }
+                }
+                if (mGLRootView.getVisibility() != View.VISIBLE) {
+                    renderer.showPreviewFrameSync();
+                    mGLRootView.setVisibility(View.VISIBLE);
+                } else {
+                    if (mCaptureState == CAPTURE_STATE_VIEWFINDER) {
+                        renderer.showPreviewFrame();
+                    } else {
+                        renderer.alignFrameSync();
+                        mMosaicFrameProcessor.processFrame();
+                    }
+                }
+            }
+        };
+
+        PowerManager pm = (PowerManager) mActivity.getSystemService(Context.POWER_SERVICE);
+        mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Panorama");
+
+        mOrientationEventListener = new PanoOrientationEventListener(mActivity);
+
+        mMosaicFrameProcessor = MosaicFrameProcessor.getInstance();
+
+        Resources appRes = mActivity.getResources();
+        mPreparePreviewString = appRes.getString(R.string.pano_dialog_prepare_preview);
+        mDialogTitle = appRes.getString(R.string.pano_dialog_title);
+        mDialogOkString = appRes.getString(R.string.dialog_ok);
+        mDialogPanoramaFailedString = appRes.getString(R.string.pano_dialog_panorama_failed);
+        mDialogWaitingPreviousString = appRes.getString(R.string.pano_dialog_waiting_previous);
+
+        mGLRootView = (GLRootView) mActivity.getGLRoot();
+
+        mMainHandler = new Handler() {
+            @Override
+            public void handleMessage(Message msg) {
+                switch (msg.what) {
+                    case MSG_LOW_RES_FINAL_MOSAIC_READY:
+                        onBackgroundThreadFinished();
+                        showFinalMosaic((Bitmap) msg.obj);
+                        saveHighResMosaic();
+                        break;
+                    case MSG_GENERATE_FINAL_MOSAIC_ERROR:
+                        onBackgroundThreadFinished();
+                        if (mPaused) {
+                            resetToPreview();
+                        } else {
+                            mRotateDialog.showAlertDialog(
+                                    mDialogTitle, mDialogPanoramaFailedString,
+                                    mDialogOkString, new Runnable() {
+                                        @Override
+                                        public void run() {
+                                            resetToPreview();
+                                        }},
+                                    null, null);
+                        }
+                        clearMosaicFrameProcessorIfNeeded();
+                        break;
+                    case MSG_END_DIALOG_RESET_TO_PREVIEW:
+                        onBackgroundThreadFinished();
+                        resetToPreview();
+                        clearMosaicFrameProcessorIfNeeded();
+                        break;
+                    case MSG_CLEAR_SCREEN_DELAY:
+                        mActivity.getWindow().clearFlags(WindowManager.LayoutParams.
+                                FLAG_KEEP_SCREEN_ON);
+                        break;
+                    case MSG_CONFIG_MOSAIC_PREVIEW:
+                        configMosaicPreview(msg.arg1, msg.arg2);
+                        break;
+                    case MSG_RESET_TO_PREVIEW:
+                        resetToPreview();
+                        break;
+                }
+            }
+        };
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent m) {
+        return mActivity.superDispatchTouchEvent(m);
+    }
+
+    private void setupCamera() throws CameraHardwareException, CameraDisabledException {
+        openCamera();
+        Parameters parameters = mCameraDevice.getParameters();
+        setupCaptureParams(parameters);
+        configureCamera(parameters);
+    }
+
+    private void releaseCamera() {
+        if (mCameraDevice != null) {
+            mCameraDevice.setPreviewCallbackWithBuffer(null);
+            CameraHolder.instance().release();
+            mCameraDevice = null;
+            mCameraState = PREVIEW_STOPPED;
+        }
+    }
+
+    private void openCamera() throws CameraHardwareException, CameraDisabledException {
+        int cameraId = CameraHolder.instance().getBackCameraId();
+        // If there is no back camera, use the first camera. Camera id starts
+        // from 0. Currently if a camera is not back facing, it is front facing.
+        // This is also forward compatible if we have a new facing other than
+        // back or front in the future.
+        if (cameraId == -1) cameraId = 0;
+        mCameraDevice = Util.openCamera(mActivity, cameraId);
+        mCameraOrientation = Util.getCameraOrientation(cameraId);
+        if (cameraId == CameraHolder.instance().getFrontCameraId()) mUsingFrontCamera = true;
+    }
+
+    private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
+            boolean needSmaller) {
+        int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
+        boolean hasFound = false;
+        for (Size size : supportedSizes) {
+            int h = size.height;
+            int w = size.width;
+            // we only want 4:3 format.
+            int d = DEFAULT_CAPTURE_PIXELS - h * w;
+            if (needSmaller && d < 0) { // no bigger preview than 960x720.
+                continue;
+            }
+            if (need4To3 && (h * 4 != w * 3)) {
+                continue;
+            }
+            d = Math.abs(d);
+            if (d < pixelsDiff) {
+                mPreviewWidth = w;
+                mPreviewHeight = h;
+                pixelsDiff = d;
+                hasFound = true;
+            }
+        }
+        return hasFound;
+    }
+
+    private void setupCaptureParams(Parameters parameters) {
+        List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
+        if (!findBestPreviewSize(supportedSizes, true, true)) {
+            Log.w(TAG, "No 4:3 ratio preview size supported.");
+            if (!findBestPreviewSize(supportedSizes, false, true)) {
+                Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
+                findBestPreviewSize(supportedSizes, false, false);
+            }
+        }
+        Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth);
+        parameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
+
+        List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
+        int last = frameRates.size() - 1;
+        int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
+        int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
+        parameters.setPreviewFpsRange(minFps, maxFps);
+        Log.v(TAG, "preview fps: " + minFps + ", " + maxFps);
+
+        List<String> supportedFocusModes = parameters.getSupportedFocusModes();
+        if (supportedFocusModes.indexOf(mTargetFocusMode) >= 0) {
+            parameters.setFocusMode(mTargetFocusMode);
+        } else {
+            // Use the default focus mode and log a message
+            Log.w(TAG, "Cannot set the focus mode to " + mTargetFocusMode +
+                  " becuase the mode is not supported.");
+        }
+
+        parameters.set(Util.RECORDING_HINT, Util.FALSE);
+
+        mHorizontalViewAngle = parameters.getHorizontalViewAngle();
+        mVerticalViewAngle =  parameters.getVerticalViewAngle();
+    }
+
+    public int getPreviewBufSize() {
+        PixelFormat pixelInfo = new PixelFormat();
+        PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo);
+        // TODO: remove this extra 32 byte after the driver bug is fixed.
+        return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
+    }
+
+    private void configureCamera(Parameters parameters) {
+        mCameraDevice.setParameters(parameters);
+    }
+
+    private void configMosaicPreview(final int w, final int h) {
+        synchronized (mRendererLock) {
+            if (mIsCreatingRenderer) {
+                mMainHandler.removeMessages(MSG_CONFIG_MOSAIC_PREVIEW);
+                mMainHandler.obtainMessage(MSG_CONFIG_MOSAIC_PREVIEW, w, h).sendToTarget();
+                mIsConfigPending = true;
+                return;
+            }
+            mIsCreatingRenderer = true;
+            mIsConfigPending = false;
+        }
+        stopCameraPreview();
+        CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail;
+        screenNail.setSize(w, h);
+        synchronized (mRendererLock) {
+            if (mMosaicPreviewRenderer != null) {
+                mMosaicPreviewRenderer.release();
+            }
+            mMosaicPreviewRenderer = null;
+            screenNail.releaseSurfaceTexture();
+            screenNail.acquireSurfaceTexture();
+        }
+        mActivity.notifyScreenNailChanged();
+        final boolean isLandscape = (mActivity.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE);
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail;
+                SurfaceTexture surfaceTexture = screenNail.getSurfaceTexture();
+                if (surfaceTexture == null) {
+                    synchronized (mRendererLock) {
+                        mIsConfigPending = true; // try config again later.
+                        mIsCreatingRenderer = false;
+                        mRendererLock.notifyAll();
+                        return;
+                    }
+                }
+                MosaicPreviewRenderer renderer = new MosaicPreviewRenderer(
+                        screenNail.getSurfaceTexture(), w, h, isLandscape);
+                synchronized (mRendererLock) {
+                    mMosaicPreviewRenderer = renderer;
+                    mCameraTexture = mMosaicPreviewRenderer.getInputSurfaceTexture();
+
+                    if (!mPaused && !mThreadRunning && mWaitProcessorTask == null) {
+                        mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW);
+                    }
+                    mIsCreatingRenderer = false;
+                    mRendererLock.notifyAll();
+                }
+            }
+        }).start();
+    }
+
+    // Receives the layout change event from the preview area. So we can set
+    // the camera preview screennail to the same size and initialize the mosaic
+    // preview renderer.
+    @Override
+    public void onLayoutChange(View v, int l, int t, int r, int b) {
+        Log.i(TAG, "layout change: "+(r - l) + "/" +(b - t));
+        mActivity.onLayoutChange(v, l, t, r, b);
+        configMosaicPreview(r - l, b - t);
+    }
+
+    @Override
+    public void onFrameAvailable(SurfaceTexture surface) {
+        /* This function may be called by some random thread,
+         * so let's be safe and jump back to ui thread.
+         * No OpenGL calls can be done here. */
+        mActivity.runOnUiThread(mOnFrameAvailableRunnable);
+    }
+
+    private void hideDirectionIndicators() {
+        mLeftIndicator.setVisibility(View.GONE);
+        mRightIndicator.setVisibility(View.GONE);
+    }
+
+    private void showDirectionIndicators(int direction) {
+        switch (direction) {
+            case PanoProgressBar.DIRECTION_NONE:
+                mLeftIndicator.setVisibility(View.VISIBLE);
+                mRightIndicator.setVisibility(View.VISIBLE);
+                break;
+            case PanoProgressBar.DIRECTION_LEFT:
+                mLeftIndicator.setVisibility(View.VISIBLE);
+                mRightIndicator.setVisibility(View.GONE);
+                break;
+            case PanoProgressBar.DIRECTION_RIGHT:
+                mLeftIndicator.setVisibility(View.GONE);
+                mRightIndicator.setVisibility(View.VISIBLE);
+                break;
+        }
+    }
+
+    public void startCapture() {
+        // Reset values so we can do this again.
+        mCancelComputation = false;
+        mTimeTaken = System.currentTimeMillis();
+        mActivity.setSwipingEnabled(false);
+        mActivity.hideSwitcher();
+        mShutterButton.setImageResource(R.drawable.btn_shutter_recording);
+        mCaptureState = CAPTURE_STATE_MOSAIC;
+        mCaptureIndicator.setVisibility(View.VISIBLE);
+        showDirectionIndicators(PanoProgressBar.DIRECTION_NONE);
+
+        mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
+            @Override
+            public void onProgress(boolean isFinished, float panningRateX, float panningRateY,
+                    float progressX, float progressY) {
+                float accumulatedHorizontalAngle = progressX * mHorizontalViewAngle;
+                float accumulatedVerticalAngle = progressY * mVerticalViewAngle;
+                if (isFinished
+                        || (Math.abs(accumulatedHorizontalAngle) >= DEFAULT_SWEEP_ANGLE)
+                        || (Math.abs(accumulatedVerticalAngle) >= DEFAULT_SWEEP_ANGLE)) {
+                    stopCapture(false);
+                } else {
+                    float panningRateXInDegree = panningRateX * mHorizontalViewAngle;
+                    float panningRateYInDegree = panningRateY * mVerticalViewAngle;
+                    updateProgress(panningRateXInDegree, panningRateYInDegree,
+                            accumulatedHorizontalAngle, accumulatedVerticalAngle);
+                }
+            }
+        });
+
+        mPanoProgressBar.reset();
+        // TODO: calculate the indicator width according to different devices to reflect the actual
+        // angle of view of the camera device.
+        mPanoProgressBar.setIndicatorWidth(20);
+        mPanoProgressBar.setMaxProgress(DEFAULT_SWEEP_ANGLE);
+        mPanoProgressBar.setVisibility(View.VISIBLE);
+        mDeviceOrientationAtCapture = mDeviceOrientation;
+        keepScreenOn();
+        mActivity.getOrientationManager().lockOrientation();
+        setupProgressDirectionMatrix();
+    }
+
+    void setupProgressDirectionMatrix() {
+        int degrees = Util.getDisplayRotation(mActivity);
+        int cameraId = CameraHolder.instance().getBackCameraId();
+        int orientation = Util.getDisplayOrientation(degrees, cameraId);
+        mProgressDirectionMatrix.reset();
+        mProgressDirectionMatrix.postRotate(orientation);
+    }
+
+    private void stopCapture(boolean aborted) {
+        mCaptureState = CAPTURE_STATE_VIEWFINDER;
+        mCaptureIndicator.setVisibility(View.GONE);
+        hideTooFastIndication();
+        hideDirectionIndicators();
+
+        mMosaicFrameProcessor.setProgressListener(null);
+        stopCameraPreview();
+
+        mCameraTexture.setOnFrameAvailableListener(null);
+
+        if (!aborted && !mThreadRunning) {
+            mRotateDialog.showWaitingDialog(mPreparePreviewString);
+            // Hide shutter button, shutter icon, etc when waiting for
+            // panorama to stitch
+            mActivity.hideUI();
+            runBackgroundThread(new Thread() {
+                @Override
+                public void run() {
+                    MosaicJpeg jpeg = generateFinalMosaic(false);
+
+                    if (jpeg != null && jpeg.isValid) {
+                        Bitmap bitmap = null;
+                        bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length);
+                        mMainHandler.sendMessage(mMainHandler.obtainMessage(
+                                MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap));
+                    } else {
+                        mMainHandler.sendMessage(mMainHandler.obtainMessage(
+                                MSG_END_DIALOG_RESET_TO_PREVIEW));
+                    }
+                }
+            });
+        }
+        keepScreenOnAwhile();
+    }
+
+    private void showTooFastIndication() {
+        mTooFastPrompt.setVisibility(View.VISIBLE);
+        // The PreviewArea also contains the border for "too fast" indication.
+        mPreviewArea.setVisibility(View.VISIBLE);
+        mPanoProgressBar.setIndicatorColor(mIndicatorColorFast);
+        mLeftIndicator.setEnabled(true);
+        mRightIndicator.setEnabled(true);
+    }
+
+    private void hideTooFastIndication() {
+        mTooFastPrompt.setVisibility(View.GONE);
+        // We set "INVISIBLE" instead of "GONE" here because we need mPreviewArea to have layout
+        // information so we can know the size and position for mCameraScreenNail.
+        mPreviewArea.setVisibility(View.INVISIBLE);
+        mPanoProgressBar.setIndicatorColor(mIndicatorColor);
+        mLeftIndicator.setEnabled(false);
+        mRightIndicator.setEnabled(false);
+    }
+
+    private void updateProgress(float panningRateXInDegree, float panningRateYInDegree,
+            float progressHorizontalAngle, float progressVerticalAngle) {
+        mGLRootView.requestRender();
+
+        if ((Math.abs(panningRateXInDegree) > PANNING_SPEED_THRESHOLD)
+            || (Math.abs(panningRateYInDegree) > PANNING_SPEED_THRESHOLD)) {
+            showTooFastIndication();
+        } else {
+            hideTooFastIndication();
+        }
+
+        // progressHorizontalAngle and progressVerticalAngle are relative to the
+        // camera. Convert them to UI direction.
+        mProgressAngle[0] = progressHorizontalAngle;
+        mProgressAngle[1] = progressVerticalAngle;
+        mProgressDirectionMatrix.mapPoints(mProgressAngle);
+
+        int angleInMajorDirection =
+                (Math.abs(mProgressAngle[0]) > Math.abs(mProgressAngle[1]))
+                ? (int) mProgressAngle[0]
+                : (int) mProgressAngle[1];
+        mPanoProgressBar.setProgress((angleInMajorDirection));
+    }
+
+    private void setViews(Resources appRes) {
+        mCaptureState = CAPTURE_STATE_VIEWFINDER;
+        mPanoProgressBar = (PanoProgressBar) mRootView.findViewById(R.id.pano_pan_progress_bar);
+        mPanoProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
+        mPanoProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done));
+        mPanoProgressBar.setIndicatorColor(mIndicatorColor);
+        mPanoProgressBar.setOnDirectionChangeListener(
+                new PanoProgressBar.OnDirectionChangeListener () {
+                    @Override
+                    public void onDirectionChange(int direction) {
+                        if (mCaptureState == CAPTURE_STATE_MOSAIC) {
+                            showDirectionIndicators(direction);
+                        }
+                    }
+                });
+
+        mLeftIndicator = mRootView.findViewById(R.id.pano_pan_left_indicator);
+        mRightIndicator = mRootView.findViewById(R.id.pano_pan_right_indicator);
+        mLeftIndicator.setEnabled(false);
+        mRightIndicator.setEnabled(false);
+        mTooFastPrompt = (TextView) mRootView.findViewById(R.id.pano_capture_too_fast_textview);
+        // This mPreviewArea also shows the border for visual "too fast" indication.
+        mPreviewArea = (LayoutNotifyView) mRootView.findViewById(R.id.pano_preview_area);
+        mPreviewArea.setOnLayoutChangeListener(this);
+
+        mSavingProgressBar = (PanoProgressBar) mRootView.findViewById(R.id.pano_saving_progress_bar);
+        mSavingProgressBar.setIndicatorWidth(0);
+        mSavingProgressBar.setMaxProgress(100);
+        mSavingProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
+        mSavingProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_indication));
+
+        mCaptureIndicator = mRootView.findViewById(R.id.pano_capture_indicator);
+
+        mReviewLayout = mRootView.findViewById(R.id.pano_review_layout);
+        mReview = (ImageView) mRootView.findViewById(R.id.pano_reviewarea);
+        mReview.setBackgroundColor(mReviewBackground);
+        View cancelButton = mRootView.findViewById(R.id.pano_review_cancel_button);
+        cancelButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View arg0) {
+                if (mPaused || mCameraTexture == null) return;
+                cancelHighResComputation();
+            }
+        });
+
+        mShutterButton = mActivity.getShutterButton();
+        mShutterButton.setImageResource(R.drawable.btn_new_shutter);
+        mShutterButton.setOnShutterButtonListener(this);
+
+        if (mActivity.getResources().getConfiguration().orientation
+                == Configuration.ORIENTATION_PORTRAIT) {
+            Rotatable view = (Rotatable) mRootView.findViewById(R.id.pano_rotate_reviewarea);
+            view.setOrientation(270, false);
+        }
+    }
+
+    private void createContentView() {
+        mActivity.getLayoutInflater().inflate(R.layout.panorama_module, (ViewGroup) mRootView);
+        Resources appRes = mActivity.getResources();
+        mCaptureLayout = (LinearLayout) mRootView.findViewById(R.id.camera_app_root);
+        mIndicatorColor = appRes.getColor(R.color.pano_progress_indication);
+        mReviewBackground = appRes.getColor(R.color.review_background);
+        mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast);
+        mPanoLayout = (ViewGroup) mRootView.findViewById(R.id.pano_layout);
+        mRotateDialog = new RotateDialogController(mActivity, R.layout.rotate_dialog);
+        setViews(appRes);
+    }
+
+    @Override
+    public void onShutterButtonClick() {
+        // If mCameraTexture == null then GL setup is not finished yet.
+        // No buttons can be pressed.
+        if (mPaused || mThreadRunning || mCameraTexture == null) return;
+        // Since this button will stay on the screen when capturing, we need to check the state
+        // right now.
+        switch (mCaptureState) {
+            case CAPTURE_STATE_VIEWFINDER:
+                if(mActivity.getStorageSpace() <= Storage.LOW_STORAGE_THRESHOLD) return;
+                mSoundPlayer.play(SoundClips.START_VIDEO_RECORDING);
+                startCapture();
+                break;
+            case CAPTURE_STATE_MOSAIC:
+                mSoundPlayer.play(SoundClips.STOP_VIDEO_RECORDING);
+                stopCapture(false);
+        }
+    }
+
+    @Override
+    public void onShutterButtonFocus(boolean pressed) {
+    }
+
+    public void reportProgress() {
+        mSavingProgressBar.reset();
+        mSavingProgressBar.setRightIncreasing(true);
+        Thread t = new Thread() {
+            @Override
+            public void run() {
+                while (mThreadRunning) {
+                    final int progress = mMosaicFrameProcessor.reportProgress(
+                            true, mCancelComputation);
+
+                    try {
+                        synchronized (mWaitObject) {
+                            mWaitObject.wait(50);
+                        }
+                    } catch (InterruptedException e) {
+                        throw new RuntimeException("Panorama reportProgress failed", e);
+                    }
+                    // Update the progress bar
+                    mActivity.runOnUiThread(new Runnable() {
+                        @Override
+                        public void run() {
+                            mSavingProgressBar.setProgress(progress);
+                        }
+                    });
+                }
+            }
+        };
+        t.start();
+    }
+
+    private int getCaptureOrientation() {
+        // The panorama image returned from the library is oriented based on the
+        // natural orientation of a camera. We need to set an orientation for the image
+        // in its EXIF header, so the image can be displayed correctly.
+        // The orientation is calculated from compensating the
+        // device orientation at capture and the camera orientation respective to
+        // the natural orientation of the device.
+        int orientation;
+        if (mUsingFrontCamera) {
+            // mCameraOrientation is negative with respect to the front facing camera.
+            // See document of android.hardware.Camera.Parameters.setRotation.
+            orientation = (mDeviceOrientationAtCapture - mCameraOrientation + 360) % 360;
+        } else {
+            orientation = (mDeviceOrientationAtCapture + mCameraOrientation) % 360;
+        }
+        return orientation;
+    }
+
+    public void saveHighResMosaic() {
+        runBackgroundThread(new Thread() {
+            @Override
+            public void run() {
+                mPartialWakeLock.acquire();
+                MosaicJpeg jpeg;
+                try {
+                    jpeg = generateFinalMosaic(true);
+                } finally {
+                    mPartialWakeLock.release();
+                }
+
+                if (jpeg == null) {  // Cancelled by user.
+                    mMainHandler.sendEmptyMessage(MSG_END_DIALOG_RESET_TO_PREVIEW);
+                } else if (!jpeg.isValid) {  // Error when generating mosaic.
+                    mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR);
+                } else {
+                    int orientation = getCaptureOrientation();
+                    Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation);
+                    if (uri != null) {
+                        mActivity.addSecureAlbumItemIfNeeded(false, uri);
+                        Util.broadcastNewPicture(mActivity, uri);
+                    }
+                    mMainHandler.sendMessage(
+                            mMainHandler.obtainMessage(MSG_END_DIALOG_RESET_TO_PREVIEW));
+                }
+            }
+        });
+        reportProgress();
+    }
+
+    private void runBackgroundThread(Thread thread) {
+        mThreadRunning = true;
+        thread.start();
+    }
+
+    private void onBackgroundThreadFinished() {
+        mThreadRunning = false;
+        mRotateDialog.dismissDialog();
+    }
+
+    private void cancelHighResComputation() {
+        mCancelComputation = true;
+        synchronized (mWaitObject) {
+            mWaitObject.notify();
+        }
+    }
+
+    // This function will be called upon the first camera frame is available.
+    private void reset() {
+        mCaptureState = CAPTURE_STATE_VIEWFINDER;
+
+        mActivity.getOrientationManager().unlockOrientation();
+        // We should set mGLRootView visible too. However, since there might be no
+        // frame available yet, setting mGLRootView visible should be done right after
+        // the first camera frame is available and therefore it is done by
+        // mOnFirstFrameAvailableRunnable.
+        mActivity.setSwipingEnabled(true);
+        mShutterButton.setImageResource(R.drawable.btn_new_shutter);
+        mReviewLayout.setVisibility(View.GONE);
+        mPanoProgressBar.setVisibility(View.GONE);
+        mGLRootView.setVisibility(View.VISIBLE);
+        // Orientation change will trigger onLayoutChange->configMosaicPreview->
+        // resetToPreview. Do not show the capture UI in film strip.
+        if (mActivity.mShowCameraAppView) {
+            mCaptureLayout.setVisibility(View.VISIBLE);
+            mActivity.showUI();
+        }
+        mMosaicFrameProcessor.reset();
+    }
+
+    private void resetToPreview() {
+        reset();
+        if (!mPaused) startCameraPreview();
+    }
+
+    private static class FlipBitmapDrawable extends BitmapDrawable {
+
+        public FlipBitmapDrawable(Resources res, Bitmap bitmap) {
+            super(res, bitmap);
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            Rect bounds = getBounds();
+            int cx = bounds.centerX();
+            int cy = bounds.centerY();
+            canvas.save(Canvas.MATRIX_SAVE_FLAG);
+            canvas.rotate(180, cx, cy);
+            super.draw(canvas);
+            canvas.restore();
+        }
+    }
+
+    private void showFinalMosaic(Bitmap bitmap) {
+        if (bitmap != null) {
+            int orientation = getCaptureOrientation();
+            if (orientation >= 180) {
+                // We need to flip the drawable to compensate
+                mReview.setImageDrawable(new FlipBitmapDrawable(
+                        mActivity.getResources(), bitmap));
+            } else {
+                mReview.setImageBitmap(bitmap);
+            }
+        }
+
+        mCaptureLayout.setVisibility(View.GONE);
+        mReviewLayout.setVisibility(View.VISIBLE);
+    }
+
+    private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) {
+        if (jpegData != null) {
+            String filename = PanoUtil.createName(
+                    mActivity.getResources().getString(R.string.pano_file_name_format), mTimeTaken);
+            String filepath = Storage.generateFilepath(filename);
+
+            ExifOutputStream out = null;
+            InputStream is = null;
+            try {
+                is = new ByteArrayInputStream(jpegData);
+                ExifReader reader = new ExifReader();
+                ExifData data = reader.read(is);
+
+                // Add Exif tags.
+                data.addGpsDateTimeStampTag(mTimeTaken);
+                data.addDateTimeStampTag(ExifTag.TAG_DATE_TIME, mTimeTaken, TimeZone.getDefault());
+                data.addTag(ExifTag.TAG_ORIENTATION).
+                        setValue(getExifOrientation(orientation));
+
+                out = new ExifOutputStream(new FileOutputStream(filepath));
+                out.setExifData(data);
+                out.write(jpegData);
+            } catch (IOException e) {
+                Log.e(TAG, "Cannot set EXIF for " + filepath, e);
+                Storage.writeFile(filepath, jpegData);
+            } catch (ExifInvalidFormatException e) {
+                Log.e(TAG, "Cannot set EXIF for " + filepath, e);
+                Storage.writeFile(filepath, jpegData);
+            } finally {
+                Util.closeSilently(out);
+                Util.closeSilently(is);
+            }
+
+            int jpegLength = (int) (new File(filepath).length());
+            return Storage.addImage(mContentResolver, filename, mTimeTaken,
+                    null, orientation, jpegLength, filepath, width, height);
+        }
+        return null;
+    }
+
+    private static int getExifOrientation(int orientation) {
+        switch (orientation) {
+            case 0:
+                return ExifTag.Orientation.TOP_LEFT;
+            case 90:
+                return ExifTag.Orientation.RIGHT_TOP;
+            case 180:
+                return ExifTag.Orientation.BOTTOM_LEFT;
+            case 270:
+                return ExifTag.Orientation.RIGHT_BOTTOM;
+            default:
+                throw new AssertionError("invalid: " + orientation);
+        }
+    }
+
+    private void clearMosaicFrameProcessorIfNeeded() {
+        if (!mPaused || mThreadRunning) return;
+        // Only clear the processor if it is initialized by this activity
+        // instance. Other activity instances may be using it.
+        if (mMosaicFrameProcessorInitialized) {
+            mMosaicFrameProcessor.clear();
+            mMosaicFrameProcessorInitialized = false;
+        }
+    }
+
+    private void initMosaicFrameProcessorIfNeeded() {
+        if (mPaused || mThreadRunning) return;
+        mMosaicFrameProcessor.initialize(
+                mPreviewWidth, mPreviewHeight, getPreviewBufSize());
+        mMosaicFrameProcessorInitialized = true;
+    }
+
+    @Override
+    public void onPauseBeforeSuper() {
+        mPaused = true;
+    }
+
+    @Override
+    public void onPauseAfterSuper() {
+        mOrientationEventListener.disable();
+        if (mCameraDevice == null) {
+            // Camera open failed. Nothing should be done here.
+            return;
+        }
+        // Stop the capturing first.
+        if (mCaptureState == CAPTURE_STATE_MOSAIC) {
+            stopCapture(true);
+            reset();
+        }
+
+        releaseCamera();
+        synchronized (mRendererLock) {
+            mCameraTexture = null;
+
+            // The preview renderer might not have a chance to be initialized
+            // before onPause().
+            if (mMosaicPreviewRenderer != null) {
+                mMosaicPreviewRenderer.release();
+                mMosaicPreviewRenderer = null;
+            }
+        }
+
+        clearMosaicFrameProcessorIfNeeded();
+        if (mWaitProcessorTask != null) {
+            mWaitProcessorTask.cancel(true);
+            mWaitProcessorTask = null;
+        }
+        resetScreenOn();
+        if (mSoundPlayer != null) {
+            mSoundPlayer.release();
+            mSoundPlayer = null;
+        }
+        CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail;
+        screenNail.releaseSurfaceTexture();
+        System.gc();
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+
+        Drawable lowResReview = null;
+        if (mThreadRunning) lowResReview = mReview.getDrawable();
+
+        // Change layout in response to configuration change
+        mCaptureLayout.setOrientation(
+                newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
+                ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
+        mCaptureLayout.removeAllViews();
+        LayoutInflater inflater = mActivity.getLayoutInflater();
+        inflater.inflate(R.layout.preview_frame_pano, mCaptureLayout);
+
+        mPanoLayout.removeView(mReviewLayout);
+        inflater.inflate(R.layout.pano_review, mPanoLayout);
+
+        setViews(mActivity.getResources());
+        if (mThreadRunning) {
+            mReview.setImageDrawable(lowResReview);
+            mCaptureLayout.setVisibility(View.GONE);
+            mReviewLayout.setVisibility(View.VISIBLE);
+        }
+    }
+
+    @Override
+    public void onOrientationChanged(int orientation) {
+    }
+
+    @Override
+    public void onResumeBeforeSuper() {
+        mPaused = false;
+    }
+
+    @Override
+    public void onResumeAfterSuper() {
+        mOrientationEventListener.enable();
+
+        mCaptureState = CAPTURE_STATE_VIEWFINDER;
+
+        try {
+            setupCamera();
+        } catch (CameraHardwareException e) {
+            Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera);
+            return;
+        } catch (CameraDisabledException e) {
+            Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
+            return;
+        }
+
+        // Set up sound playback for shutter button
+        mSoundPlayer = SoundClips.getPlayer(mActivity);
+
+        // Check if another panorama instance is using the mosaic frame processor.
+        mRotateDialog.dismissDialog();
+        if (!mThreadRunning && mMosaicFrameProcessor.isMosaicMemoryAllocated()) {
+            mGLRootView.setVisibility(View.GONE);
+            mRotateDialog.showWaitingDialog(mDialogWaitingPreviousString);
+            // If stitching is still going on, make sure switcher and shutter button
+            // are not showing
+            mActivity.hideUI();
+            mWaitProcessorTask = new WaitProcessorTask().execute();
+        } else {
+            mGLRootView.setVisibility(View.VISIBLE);
+            // Camera must be initialized before MosaicFrameProcessor is
+            // initialized. The preview size has to be decided by camera device.
+            initMosaicFrameProcessorIfNeeded();
+            int w = mPreviewArea.getWidth();
+            int h = mPreviewArea.getHeight();
+            if (w != 0 && h != 0) {  // The layout has been calculated.
+                configMosaicPreview(w, h);
+            }
+        }
+        keepScreenOnAwhile();
+
+        // Dismiss open menu if exists.
+        PopupManager.getInstance(mActivity).notifyShowPopup(null);
+        mRootView.requestLayout();
+    }
+
+    /**
+     * Generate the final mosaic image.
+     *
+     * @param highRes flag to indicate whether we want to get a high-res version.
+     * @return a MosaicJpeg with its isValid flag set to true if successful; null if the generation
+     *         process is cancelled; and a MosaicJpeg with its isValid flag set to false if there
+     *         is an error in generating the final mosaic.
+     */
+    public MosaicJpeg generateFinalMosaic(boolean highRes) {
+        int mosaicReturnCode = mMosaicFrameProcessor.createMosaic(highRes);
+        if (mosaicReturnCode == Mosaic.MOSAIC_RET_CANCELLED) {
+            return null;
+        } else if (mosaicReturnCode == Mosaic.MOSAIC_RET_ERROR) {
+            return new MosaicJpeg();
+        }
+
+        byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
+        if (imageData == null) {
+            Log.e(TAG, "getFinalMosaicNV21() returned null.");
+            return new MosaicJpeg();
+        }
+
+        int len = imageData.length - 8;
+        int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
+                + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
+        int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
+                + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
+        Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
+
+        if (width <= 0 || height <= 0) {
+            // TODO: pop up an error message indicating that the final result is not generated.
+            Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " +
+                    height);
+            return new MosaicJpeg();
+        }
+
+        YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
+        try {
+            out.close();
+        } catch (Exception e) {
+            Log.e(TAG, "Exception in storing final mosaic", e);
+            return new MosaicJpeg();
+        }
+        return new MosaicJpeg(out.toByteArray(), width, height);
+    }
+
+    private void startCameraPreview() {
+        if (mCameraDevice == null) {
+            // Camera open failed. Return.
+            return;
+        }
+
+        // This works around a driver issue. startPreview may fail if
+        // stopPreview/setPreviewTexture/startPreview are called several times
+        // in a row. mCameraTexture can be null after pressing home during
+        // mosaic generation and coming back. Preview will be started later in
+        // onLayoutChange->configMosaicPreview. This also reduces the latency.
+        synchronized (mRendererLock) {
+            if (mCameraTexture == null) return;
+
+            // If we're previewing already, stop the preview first (this will
+            // blank the screen).
+            if (mCameraState != PREVIEW_STOPPED) stopCameraPreview();
+
+            // Set the display orientation to 0, so that the underlying mosaic
+            // library can always get undistorted mPreviewWidth x mPreviewHeight
+            // image data from SurfaceTexture.
+            mCameraDevice.setDisplayOrientation(0);
+
+            mCameraTexture.setOnFrameAvailableListener(this);
+            mCameraDevice.setPreviewTextureAsync(mCameraTexture);
+        }
+        mCameraDevice.startPreviewAsync();
+        mCameraState = PREVIEW_ACTIVE;
+    }
+
+    private void stopCameraPreview() {
+        if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
+            Log.v(TAG, "stopPreview");
+            mCameraDevice.stopPreview();
+        }
+        mCameraState = PREVIEW_STOPPED;
+    }
+
+    @Override
+    public void onUserInteraction() {
+        if (mCaptureState != CAPTURE_STATE_MOSAIC) keepScreenOnAwhile();
+    }
+
+    @Override
+    public boolean onBackPressed() {
+        // If panorama is generating low res or high res mosaic, ignore back
+        // key. So the activity will not be destroyed.
+        if (mThreadRunning) return true;
+        return false;
+    }
+
+    private void resetScreenOn() {
+        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
+        mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+    }
+
+    private void keepScreenOnAwhile() {
+        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
+        mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+        mMainHandler.sendEmptyMessageDelayed(MSG_CLEAR_SCREEN_DELAY, SCREEN_DELAY);
+    }
+
+    private void keepScreenOn() {
+        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
+        mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+    }
+
+    private class WaitProcessorTask extends AsyncTask<Void, Void, Void> {
+        @Override
+        protected Void doInBackground(Void... params) {
+            synchronized (mMosaicFrameProcessor) {
+                while (!isCancelled() && mMosaicFrameProcessor.isMosaicMemoryAllocated()) {
+                    try {
+                        mMosaicFrameProcessor.wait();
+                    } catch (Exception e) {
+                        // ignore
+                    }
+                }
+            }
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void result) {
+            mWaitProcessorTask = null;
+            mRotateDialog.dismissDialog();
+            mGLRootView.setVisibility(View.VISIBLE);
+            initMosaicFrameProcessorIfNeeded();
+            int w = mPreviewArea.getWidth();
+            int h = mPreviewArea.getHeight();
+            if (w != 0 && h != 0) {  // The layout has been calculated.
+                configMosaicPreview(w, h);
+            }
+            resetToPreview();
+        }
+    }
+
+    @Override
+    public void onFullScreenChanged(boolean full) {
+    }
+
+
+    @Override
+    public void onStop() {
+    }
+
+    @Override
+    public void installIntentFilter() {
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+    }
+
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        return false;
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        return false;
+    }
+
+    @Override
+    public void onSingleTapUp(View view, int x, int y) {
+    }
+
+    @Override
+    public void onPreviewTextureCopied() {
+    }
+
+    @Override
+    public void onCaptureTextureCopied() {
+    }
+
+    @Override
+    public boolean updateStorageHintOnResume() {
+        return false;
+    }
+
+    @Override
+    public void updateCameraAppView() {
+    }
+
+    @Override
+    public boolean collapseCameraControls() {
+        return false;
+    }
+
+    @Override
+    public boolean needsSwitcher() {
+        return true;
+    }
+
+    @Override
+    public void onShowSwitcherPopup() {
+    }
+}
diff --git a/src/com/android/camera/PhotoController.java b/src/com/android/camera/PhotoController.java
new file mode 100644
index 0000000..ad8659e
--- /dev/null
+++ b/src/com/android/camera/PhotoController.java
@@ -0,0 +1,225 @@
+/*
+ * 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.hardware.Camera.Parameters;
+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.TimerSettingPopup;
+
+public class PhotoController extends PieController
+        implements MoreSettingPopup.Listener,
+        TimerSettingPopup.Listener,
+        ListPrefSettingPopup.Listener {
+    private static String TAG = "CAM_photocontrol";
+    private static float FLOAT_PI_DIVIDED_BY_TWO = (float) Math.PI / 2;
+    private final String mSettingOff;
+
+    private PhotoModule mModule;
+    private String[] mOtherKeys;
+    // First level popup
+    private MoreSettingPopup mPopup;
+    // Second level popup
+    private AbstractSettingPopup mSecondPopup;
+
+    public PhotoController(CameraActivity activity, PhotoModule module, PieRenderer pie) {
+        super(activity, pie);
+        mModule = module;
+        mSettingOff = activity.getString(R.string.setting_off_value);
+    }
+
+    public void initialize(PreferenceGroup group) {
+        super.initialize(group);
+        mPopup = null;
+        mSecondPopup = null;
+        float sweep = FLOAT_PI_DIVIDED_BY_TWO / 2;
+        addItem(CameraSettings.KEY_FLASH_MODE, FLOAT_PI_DIVIDED_BY_TWO - sweep, sweep);
+        addItem(CameraSettings.KEY_EXPOSURE, 3 * FLOAT_PI_DIVIDED_BY_TWO - sweep, sweep);
+        addItem(CameraSettings.KEY_WHITE_BALANCE, 3 * FLOAT_PI_DIVIDED_BY_TWO + sweep, sweep);
+        if (group.findPreference(CameraSettings.KEY_CAMERA_ID) != null) {
+            PieItem item = makeItem(R.drawable.ic_switch_photo_facing_holo_light);
+            item.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO + sweep, sweep);
+            item.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(PieItem item) {
+                    // Find the index of next camera.
+                    ListPreference camPref = mPreferenceGroup
+                            .findPreference(CameraSettings.KEY_CAMERA_ID);
+                    if (camPref != null) {
+                        int index = camPref.findIndexOfValue(camPref.getValue());
+                        CharSequence[] values = camPref.getEntryValues();
+                        index = (index + 1) % values.length;
+                        int newCameraId = Integer
+                                .parseInt((String) values[index]);
+                        mListener.onCameraPickerClicked(newCameraId);
+                    }
+                }
+            });
+            mRenderer.addItem(item);
+        }
+        if (group.findPreference(CameraSettings.KEY_CAMERA_HDR) != null) {
+            PieItem hdr = makeItem(R.drawable.ic_hdr);
+            hdr.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO, sweep);
+            hdr.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(PieItem item) {
+                    // Find the index of next camera.
+                    ListPreference pref = mPreferenceGroup
+                            .findPreference(CameraSettings.KEY_CAMERA_HDR);
+                    if (pref != null) {
+                        // toggle hdr value
+                        int index = (pref.findIndexOfValue(pref.getValue()) + 1) % 2;
+                        pref.setValueIndex(index);
+                        onSettingChanged(pref);
+                    }
+                }
+            });
+            mRenderer.addItem(hdr);
+        }
+        mOtherKeys = new String[] {
+                CameraSettings.KEY_SCENE_MODE,
+                CameraSettings.KEY_RECORD_LOCATION,
+                CameraSettings.KEY_PICTURE_SIZE,
+                CameraSettings.KEY_FOCUS_MODE,
+                CameraSettings.KEY_TIMER,
+                CameraSettings.KEY_TIMER_SOUND_EFFECTS,
+                };
+        PieItem item = makeItem(R.drawable.ic_settings_holo_light);
+        item.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO * 3, sweep);
+        item.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(PieItem item) {
+                if (mPopup == null) {
+                    initializePopup();
+                }
+                mModule.showPopup(mPopup);
+            }
+        });
+        mRenderer.addItem(item);
+    }
+
+    protected void setCameraId(int cameraId) {
+        ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID);
+        pref.setValue("" + cameraId);
+    }
+
+    @Override
+    public void reloadPreferences() {
+        super.reloadPreferences();
+        if (mPopup != null) {
+            mPopup.reloadPreference();
+        }
+    }
+
+    @Override
+    // Hit when an item in the second-level popup gets selected
+    public void onListPrefChanged(ListPreference pref) {
+        if (mPopup != null && mSecondPopup != null) {
+                mModule.dismissPopup(true);
+                mPopup.reloadPreference();
+        }
+        onSettingChanged(pref);
+    }
+
+    @Override
+    public void overrideSettings(final String ... keyvalues) {
+        super.overrideSettings(keyvalues);
+        if (mPopup == null) initializePopup();
+        mPopup.overrideSettings(keyvalues);
+    }
+
+    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 (mSecondPopup != null) {
+            mSecondPopup = null;
+            if (topPopupOnly) mModule.showPopup(mPopup);
+        }
+    }
+
+    // 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);
+    }
+
+    @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 (mSecondPopup != null) return;
+
+        LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+        if (CameraSettings.KEY_TIMER.equals(pref.getKey())) {
+            TimerSettingPopup timerPopup = (TimerSettingPopup) inflater.inflate(
+                    R.layout.timer_setting_popup, null, false);
+            timerPopup.initialize(pref);
+            timerPopup.setSettingChangedListener(this);
+            mModule.dismissPopup(true);
+            mSecondPopup = timerPopup;
+        } else {
+            ListPrefSettingPopup basic = (ListPrefSettingPopup) inflater.inflate(
+                    R.layout.list_pref_setting_popup, null, false);
+            basic.initialize(pref);
+            basic.setSettingChangedListener(this);
+            mModule.dismissPopup(true);
+            mSecondPopup = basic;
+        }
+        mModule.showPopup(mSecondPopup);
+    }
+}
diff --git a/src/com/android/camera/PhotoModule.java b/src/com/android/camera/PhotoModule.java
new file mode 100644
index 0000000..a283e59
--- /dev/null
+++ b/src/com/android/camera/PhotoModule.java
@@ -0,0 +1,2481 @@
+/*
+ * 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.app.AlertDialog;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.DialogInterface;
+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.Face;
+import android.hardware.Camera.FaceDetectionListener;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.PictureCallback;
+import android.hardware.Camera.Size;
+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.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.OrientationEventListener;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.ImageView;
+import android.widget.Toast;
+
+import com.android.camera.CameraManager.CameraProxy;
+import com.android.camera.ui.AbstractSettingPopup;
+import com.android.camera.ui.CountDownView;
+import com.android.camera.ui.FaceView;
+import com.android.camera.ui.PieRenderer;
+import com.android.camera.ui.PopupManager;
+import com.android.camera.ui.PreviewSurfaceView;
+import com.android.camera.ui.RenderOverlay;
+import com.android.camera.ui.Rotatable;
+import com.android.camera.ui.RotateTextToast;
+import com.android.camera.ui.TwoStateImageView;
+import com.android.camera.ui.ZoomRenderer;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.filtershow.CropExtras;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+
+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.Collections;
+import java.util.Formatter;
+import java.util.List;
+
+public class PhotoModule
+    implements CameraModule,
+    FocusOverlayManager.Listener,
+    CameraPreference.OnPreferenceChangedListener,
+    LocationManager.Listener,
+    PreviewFrameLayout.OnSizeChangedListener,
+    ShutterButton.OnShutterButtonListener,
+    SurfaceHolder.Callback,
+    PieRenderer.PieListener,
+    CountDownView.OnCountDownFinishedListener {
+
+    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 UPDATE_SECURE_ALBUM_ITEM = 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 View mRootView;
+    private CameraProxy mCameraDevice;
+    private int mCameraId;
+    private Parameters mParameters;
+    private boolean mPaused;
+    private AbstractSettingPopup mPopup;
+
+    // these are only used by Camera
+
+    // 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 int mZoomMax;
+    private List<Integer> mZoomRatios;
+
+    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 ShutterButton mShutterButton;
+    private boolean mFaceDetectionStarted = false;
+
+    private PreviewFrameLayout mPreviewFrameLayout;
+    private Object mSurfaceTexture;
+    private CountDownView mCountDownView;
+
+    // for API level 10
+    private PreviewSurfaceView mPreviewSurfaceView;
+    private volatile SurfaceHolder mCameraSurfaceHolder;
+
+    private FaceView mFaceView;
+    private RenderOverlay mRenderOverlay;
+    private Rotatable mReviewCancelButton;
+    private Rotatable mReviewDoneButton;
+    private View mReviewRetakeButton;
+
+    // mCropValue and mSaveUri are used only if isImageCaptureIntent() is true.
+    private String mCropValue;
+    private Uri mSaveUri;
+
+    private View mMenu;
+    private View mBlocker;
+
+    // Small indicators which show the camera settings in the viewfinder.
+    private ImageView mExposureIndicator;
+    private ImageView mFlashIndicator;
+    private ImageView mSceneIndicator;
+    private ImageView mHdrIndicator;
+    // A view group that contains all the small indicators.
+    private View mOnScreenIndicators;
+
+    // We use a thread in MediaSaver to do the work of saving images. This
+    // reduces the shot-to-shot time.
+    private MediaSaver mMediaSaver;
+    // Similarly, we use a thread to generate the name of the picture and insert
+    // it into MediaStore while picture taking is still in progress.
+    private NamedImages mNamedImages;
+
+    private Runnable mDoSnapRunnable = new Runnable() {
+        @Override
+        public void run() {
+            onShutterButtonClick();
+        }
+    };
+
+    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 static final int PREVIEW_STOPPED = 0;
+    private static final int IDLE = 1;  // preview is active
+    // Focus is in progress. The exact focus state is in Focus.java.
+    private static final int FOCUSING = 2;
+    private static final int SNAPSHOT_IN_PROGRESS = 3;
+    // Switching between cameras.
+    private static final int SWITCHING_CAMERA = 4;
+    private int mCameraState = PREVIEW_STOPPED;
+    private boolean mSnapshotOnIdle = false;
+
+    private ContentResolver mContentResolver;
+
+    private LocationManager mLocationManager;
+
+    private final ShutterCallback mShutterCallback = new ShutterCallback();
+    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 PieRenderer mPieRenderer;
+    private PhotoController mPhotoControl;
+
+    private ZoomRenderer mZoomRenderer;
+
+    private String mSceneMode;
+    private Toast mNotSelectableToast;
+
+    private final Handler mHandler = new MainHandler();
+    private PreferenceGroup mPreferenceGroup;
+
+    private boolean mQuickCapture;
+
+    CameraStartUpThread mCameraStartUpThread;
+    ConditionVariable mStartPreviewPrerequisiteReady = new ConditionVariable();
+
+    private PreviewGestures mGestures;
+
+    private MediaSaver.OnMediaSavedListener mOnMediaSavedListener = new MediaSaver.OnMediaSavedListener() {
+        @Override
+
+        public void onMediaSaved(Uri uri) {
+            if (uri != null) {
+                mHandler.obtainMessage(UPDATE_SECURE_ALBUM_ITEM, uri).sendToTarget();
+                Util.broadcastNewPicture(mActivity, 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: {
+                    ((CameraScreenNail) mActivity.mCameraScreenNail).animateSwitchCamera();
+                    break;
+                }
+
+                case CAMERA_OPEN_DONE: {
+                    initializeAfterCameraOpen();
+                    break;
+                }
+
+                case START_PREVIEW_DONE: {
+                    mCameraStartUpThread = null;
+                    setCameraState(IDLE);
+                    if (!ApiHelper.HAS_SURFACE_TEXTURE) {
+                        // This may happen if surfaceCreated has arrived.
+                        mCameraDevice.setPreviewDisplayAsync(mCameraSurfaceHolder);
+                    }
+                    startFaceDetection();
+                    locationFirstRun();
+                    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 UPDATE_SECURE_ALBUM_ITEM: {
+                    mActivity.addSecureAlbumItemIfNeeded(false, (Uri) msg.obj);
+                    break;
+                }
+            }
+        }
+    }
+
+    @Override
+    public void init(CameraActivity activity, View parent, boolean reuseNail) {
+        mActivity = activity;
+        mRootView = 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();
+
+        mActivity.getLayoutInflater().inflate(R.layout.photo_module, (ViewGroup) mRootView);
+
+        // Surface texture is from camera screen nail and startPreview needs it.
+        // This must be done before startPreview.
+        mIsImageCaptureIntent = isImageCaptureIntent();
+        if (reuseNail) {
+            mActivity.reuseCameraScreenNail(!mIsImageCaptureIntent);
+        } else {
+            mActivity.createCameraScreenNail(!mIsImageCaptureIntent);
+        }
+
+        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);
+        initializeMiscControls();
+        mLocationManager = new LocationManager(mActivity, this);
+        initOnScreenIndicator();
+        mCountDownView = (CountDownView) (mRootView.findViewById(R.id.count_down_to_capture));
+        mCountDownView.setCountDownFinishedListener(this);
+    }
+
+    // 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;
+        }
+
+        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) {
+                    setLocationPreference(RecordLocationPreference.VALUE_ON);
+                }
+            })
+            .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) {
+                    setLocationPreference(RecordLocationPreference.VALUE_OFF);
+                }
+            })
+            .show();
+    }
+
+    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 initializeRenderOverlay() {
+        if (mPieRenderer != null) {
+            mRenderOverlay.addRenderer(mPieRenderer);
+            mFocusManager.setFocusRenderer(mPieRenderer);
+        }
+        if (mZoomRenderer != null) {
+            mRenderOverlay.addRenderer(mZoomRenderer);
+        }
+        if (mGestures != null) {
+            mGestures.clearTouchReceivers();
+            mGestures.setRenderOverlay(mRenderOverlay);
+            mGestures.addTouchReceiver(mMenu);
+            mGestures.addTouchReceiver(mBlocker);
+
+            if (isImageCaptureIntent()) {
+                if (mReviewCancelButton != null) {
+                    mGestures.addTouchReceiver((View) mReviewCancelButton);
+                }
+                if (mReviewDoneButton != null) {
+                    mGestures.addTouchReceiver((View) mReviewDoneButton);
+                }
+            }
+        }
+        mRenderOverlay.requestLayout();
+    }
+
+    private void initializeAfterCameraOpen() {
+        if (mPieRenderer == null) {
+            mPieRenderer = new PieRenderer(mActivity);
+            mPhotoControl = new PhotoController(mActivity, this, mPieRenderer);
+            mPhotoControl.setListener(this);
+            mPieRenderer.setPieListener(this);
+        }
+        if (mZoomRenderer == null) {
+            mZoomRenderer = new ZoomRenderer(mActivity);
+        }
+        if (mGestures == null) {
+            // this will handle gesture disambiguation and dispatching
+            mGestures = new PreviewGestures(mActivity, this, mZoomRenderer, mPieRenderer);
+        }
+        initializeRenderOverlay();
+        initializePhotoControl();
+
+        // These depend on camera parameters.
+        setPreviewFrameLayoutAspectRatio();
+        mFocusManager.setPreviewSize(mPreviewFrameLayout.getWidth(),
+                mPreviewFrameLayout.getHeight());
+        loadCameraPreferences();
+        initializeZoom();
+        updateOnScreenIndicators();
+        showTapToFocusToastIfNeeded();
+        onFullScreenChanged(mActivity.isInCameraApp());
+    }
+
+    private void initializePhotoControl() {
+        loadCameraPreferences();
+        if (mPhotoControl != null) {
+            mPhotoControl.initialize(mPreferenceGroup);
+        }
+        updateSceneModeUI();
+    }
+
+
+    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();
+
+        // Initialize shutter button.
+        mShutterButton = mActivity.getShutterButton();
+        mShutterButton.setImageResource(R.drawable.btn_new_shutter);
+        mShutterButton.setOnShutterButtonListener(this);
+        mShutterButton.setVisibility(View.VISIBLE);
+
+        mMediaSaver = new MediaSaver(mContentResolver);
+        mNamedImages = new NamedImages();
+
+        mFirstTimeInitialized = true;
+        addIdleHandler();
+
+        mActivity.updateStorageSpaceAndHint();
+    }
+
+    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;
+            }
+        });
+    }
+
+    // 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);
+
+        mMediaSaver = new MediaSaver(mContentResolver);
+        mNamedImages = new NamedImages();
+        initializeZoom();
+        keepMediaProviderInstance();
+        hidePostCaptureAlert();
+
+        if (mPhotoControl != null) {
+            mPhotoControl.reloadPreferences();
+        }
+    }
+
+    private class ZoomChangeListener implements ZoomRenderer.OnZoomChangedListener {
+        @Override
+        public void onZoomValueChanged(int index) {
+            // Not useful to change zoom value when the activity is paused.
+            if (mPaused) return;
+            mZoomValue = index;
+            if (mParameters == null || mCameraDevice == null) return;
+            // Set zoom parameters asynchronously
+            mParameters.setZoom(mZoomValue);
+            mCameraDevice.setParametersAsync(mParameters);
+            if (mZoomRenderer != null) {
+                Parameters p = mCameraDevice.getParameters();
+                mZoomRenderer.setZoomValue(mZoomRatios.get(p.getZoom()));
+            }
+        }
+
+        @Override
+        public void onZoomStart() {
+            if (mPieRenderer != null) {
+                mPieRenderer.setBlockFocus(true);
+            }
+        }
+
+        @Override
+        public void onZoomEnd() {
+            if (mPieRenderer != null) {
+                mPieRenderer.setBlockFocus(false);
+            }
+        }
+    }
+
+    private void initializeZoom() {
+        if ((mParameters == null) || !mParameters.isZoomSupported()
+                || (mZoomRenderer == null)) return;
+        mZoomMax = mParameters.getMaxZoom();
+        mZoomRatios = mParameters.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(mParameters.getZoom());
+            mZoomRenderer.setZoomValue(mZoomRatios.get(mParameters.getZoom()));
+            mZoomRenderer.setOnZoomChangeListener(new ZoomChangeListener());
+        }
+    }
+
+    @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;
+            mFaceView.clear();
+            mFaceView.setVisibility(View.VISIBLE);
+            mFaceView.setDisplayOrientation(mDisplayOrientation);
+            CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
+            mFaceView.setMirror(info.facing == CameraInfo.CAMERA_FACING_FRONT);
+            mFaceView.resume();
+            mFocusManager.setFaceView(mFaceView);
+            mCameraDevice.setFaceDetectionListener(new FaceDetectionListener() {
+                @Override
+                public void onFaceDetection(Face[] faces, android.hardware.Camera camera) {
+                    mFaceView.setFaces(faces);
+                }
+            });
+            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.setFaceDetectionListener(null);
+            mCameraDevice.stopFaceDetection();
+            if (mFaceView != null) mFaceView.clear();
+        }
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent m) {
+        if (mCameraState == SWITCHING_CAMERA) return true;
+        if (mPopup != null) {
+            return mActivity.superDispatchTouchEvent(m);
+        } else if (mGestures != null && mRenderOverlay != null) {
+            return mGestures.dispatchTouch(m);
+        }
+        return false;
+    }
+
+    private void initOnScreenIndicator() {
+        mOnScreenIndicators = mRootView.findViewById(R.id.on_screen_indicators);
+        mExposureIndicator = (ImageView) mOnScreenIndicators.findViewById(R.id.menu_exposure_indicator);
+        mFlashIndicator = (ImageView) mOnScreenIndicators.findViewById(R.id.menu_flash_indicator);
+        mSceneIndicator = (ImageView) mOnScreenIndicators.findViewById(R.id.menu_scenemode_indicator);
+        mHdrIndicator = (ImageView) mOnScreenIndicators.findViewById(R.id.menu_hdr_indicator);
+    }
+
+    @Override
+    public void showGpsOnScreenIndicator(boolean hasSignal) { }
+
+    @Override
+    public void hideGpsOnScreenIndicator() { }
+
+    private void updateExposureOnScreenIndicator(int value) {
+        if (mExposureIndicator == null) {
+            return;
+        }
+        int id = 0;
+        float step = mParameters.getExposureCompensationStep();
+        value = (int) Math.round(value * step);
+        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);
+
+    }
+
+    private 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)) {
+                mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_on);
+            } else {
+                mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_off);
+            }
+        }
+    }
+
+    private void updateSceneOnScreenIndicator(String value) {
+        if (mSceneIndicator == null) {
+            return;
+        }
+        if ((value == null) || Parameters.SCENE_MODE_AUTO.equals(value)
+                || Parameters.SCENE_MODE_HDR.equals(value)) {
+            mSceneIndicator.setImageResource(R.drawable.ic_indicator_sce_off);
+        } else {
+            mSceneIndicator.setImageResource(R.drawable.ic_indicator_sce_on);
+        }
+    }
+
+    private void updateHdrOnScreenIndicator(String value) {
+        if (mHdrIndicator == null) {
+            return;
+        }
+        if ((value != null) && Parameters.SCENE_MODE_HDR.equals(value)) {
+            mHdrIndicator.setImageResource(R.drawable.ic_indicator_hdr_on);
+        } else {
+            mHdrIndicator.setImageResource(R.drawable.ic_indicator_hdr_off);
+        }
+    }
+
+    private void updateOnScreenIndicators() {
+        if (mParameters == null) return;
+        updateSceneOnScreenIndicator(mParameters.getSceneMode());
+        updateExposureOnScreenIndicator(CameraSettings.readExposure(mPreferences));
+        updateFlashOnScreenIndicator(mParameters.getFlashMode());
+        updateHdrOnScreenIndicator(mParameters.getSceneMode());
+    }
+
+    private final class ShutterCallback
+            implements android.hardware.Camera.ShutterCallback {
+        @Override
+        public void onShutter() {
+            mShutterCallbackTime = System.currentTimeMillis();
+            mShutterLag = mShutterCallbackTime - mCaptureStartTime;
+            Log.v(TAG, "mShutterLag = " + mShutterLag + "ms");
+        }
+    }
+
+    private final class PostViewPictureCallback implements PictureCallback {
+        @Override
+        public void onPictureTaken(
+                byte [] data, android.hardware.Camera camera) {
+            mPostViewPictureCallbackTime = System.currentTimeMillis();
+            Log.v(TAG, "mShutterToPostViewCallbackTime = "
+                    + (mPostViewPictureCallbackTime - mShutterCallbackTime)
+                    + "ms");
+        }
+    }
+
+    private final class RawPictureCallback implements PictureCallback {
+        @Override
+        public void onPictureTaken(
+                byte [] rawData, android.hardware.Camera camera) {
+            mRawPictureCallbackTime = System.currentTimeMillis();
+            Log.v(TAG, "mShutterToRawCallbackTime = "
+                    + (mRawPictureCallbackTime - mShutterCallbackTime) + "ms");
+        }
+    }
+
+    private final class JpegPictureCallback implements PictureCallback {
+        Location mLocation;
+
+        public JpegPictureCallback(Location loc) {
+            mLocation = loc;
+        }
+
+        @Override
+        public void onPictureTaken(
+                final byte [] jpegData, final android.hardware.Camera camera) {
+            if (mPaused) {
+                return;
+            }
+            if (mSceneMode == Util.SCENE_MODE_HDR) {
+                mActivity.showSwitcher();
+                mActivity.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");
+
+            // 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
+                ((CameraScreenNail) mActivity.mCameraScreenNail).animateSlide();
+            }
+            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();
+                int orientation = Exif.getOrientation(jpegData);
+                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;
+                    mMediaSaver.addImage(jpegData, title, date, mLocation, width, height,
+                            orientation, mOnMediaSavedListener);
+                }
+            } else {
+                mJpegImageData = jpegData;
+                if (!mQuickCapture) {
+                    showPostCaptureAlert();
+                } else {
+                    doAttach();
+                }
+            }
+
+            // 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 android.hardware.Camera.AutoFocusCallback {
+        @Override
+        public void onAutoFocus(
+                boolean focused, android.hardware.Camera camera) {
+            if (mPaused) return;
+
+            mAutoFocusTime = System.currentTimeMillis() - mFocusStartTime;
+            Log.v(TAG, "mAutoFocusTime = " + mAutoFocusTime + "ms");
+            setCameraState(IDLE);
+            mFocusManager.onAutoFocus(focused, mShutterButton.isPressed());
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+    private final class AutoFocusMoveCallback
+            implements android.hardware.Camera.AutoFocusMoveCallback {
+        @Override
+        public void onAutoFocusMoving(
+            boolean moving, android.hardware.Camera 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 PREVIEW_STOPPED:
+            case SNAPSHOT_IN_PROGRESS:
+            case FOCUSING:
+            case SWITCHING_CAMERA:
+                if (mGestures != null) mGestures.setEnabled(false);
+                break;
+            case IDLE:
+                if (mGestures != null && mActivity.mShowCameraAppView) {
+                    // Enable gestures only when the camera app view is visible
+                    mGestures.setEnabled(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 (ApiHelper.HAS_SURFACE_TEXTURE && !mIsImageCaptureIntent
+                && mActivity.mShowCameraAppView) {
+            // Start capture animation.
+            ((CameraScreenNail) mActivity.mCameraScreenNail).animateFlash(mDisplayRotation);
+        }
+    }
+
+    @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 || mMediaSaver.queueFull()) {
+            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.
+        mJpegRotation = Util.getJpegRotation(mCameraId, mOrientation);
+        mParameters.setRotation(mJpegRotation);
+        Location loc = mLocationManager.getCurrentLocation();
+        Util.setGpsParameters(mParameters, loc);
+        mCameraDevice.setParameters(mParameters);
+
+        mCameraDevice.takePicture2(mShutterCallback, mRawPictureCallback,
+                mPostViewPictureCallback, new JpegPictureCallback(loc),
+                mCameraState, mFocusManager.getFocusState());
+
+        if (!animateBefore) {
+            animateFlash();
+        }
+
+        mNamedImages.nameNewImage(mContentResolver, mCaptureStartTime);
+
+        mFaceDetectionStarted = false;
+        setCameraState(SNAPSHOT_IN_PROGRESS);
+        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 setShowMenu(boolean show) {
+        if (mOnScreenIndicators != null) {
+            mOnScreenIndicators.setVisibility(show ? View.VISIBLE : View.GONE);
+        }
+        if (mMenu != null) {
+            mMenu.setVisibility(show ? View.VISIBLE : View.GONE);
+        }
+    }
+
+    @Override
+    public void onFullScreenChanged(boolean full) {
+        if (mFaceView != null) {
+            mFaceView.setBlockDraw(!full);
+        }
+        if (mPopup != null) {
+            dismissPopup(false, full);
+        }
+        if (mGestures != null) {
+            mGestures.setEnabled(full);
+        }
+        if (mRenderOverlay != null) {
+            // this can not happen in capture mode
+            mRenderOverlay.setVisibility(full ? View.VISIBLE : View.GONE);
+        }
+        if (mPieRenderer != null) {
+            mPieRenderer.setBlockFocus(!full);
+        }
+        setShowMenu(full);
+        if (mBlocker != null) {
+            mBlocker.setVisibility(full ? View.VISIBLE : View.GONE);
+        }
+        if (!full && mCountDownView != null) mCountDownView.cancelCountDown();
+        if (ApiHelper.HAS_SURFACE_TEXTURE) {
+            if (mActivity.mCameraScreenNail != null) {
+                ((CameraScreenNail) mActivity.mCameraScreenNail).setFullScreen(full);
+            }
+            return;
+        }
+        if (full) {
+            mPreviewSurfaceView.expand();
+        } else {
+            mPreviewSurfaceView.shrink();
+        }
+    }
+
+    @Override
+    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+        Log.v(TAG, "surfaceChanged:" + holder + " width=" + width + ". height="
+                + height);
+    }
+
+    @Override
+    public void surfaceCreated(SurfaceHolder holder) {
+        Log.v(TAG, "surfaceCreated: " + holder);
+        mCameraSurfaceHolder = holder;
+        // Do not access the camera if camera start up thread is not finished.
+        if (mCameraDevice == null || mCameraStartUpThread != null) return;
+
+        mCameraDevice.setPreviewDisplayAsync(holder);
+        // This happens when onConfigurationChanged arrives, surface has been
+        // destroyed, and there is no onFullScreenChanged.
+        if (mCameraState == PREVIEW_STOPPED) {
+            setupPreview();
+        }
+    }
+
+    @Override
+    public void surfaceDestroyed(SurfaceHolder holder) {
+        Log.v(TAG, "surfaceDestroyed: " + holder);
+        mCameraSurfaceHolder = null;
+        stopPreview();
+    }
+
+    private void updateSceneModeUI() {
+        // 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) {
+        if (mPhotoControl != null) {
+//            mPieControl.enableFilter(true);
+            mPhotoControl.overrideSettings(
+                    CameraSettings.KEY_FLASH_MODE, flashMode,
+                    CameraSettings.KEY_WHITE_BALANCE, whiteBalance,
+                    CameraSettings.KEY_FOCUS_MODE, focusMode);
+//            mPieControl.enableFilter(false);
+        }
+    }
+
+    private void loadCameraPreferences() {
+        CameraSettings settings = new CameraSettings(mActivity, mInitialParams,
+                mCameraId, CameraHolder.instance().getCameraInfo());
+        mPreferenceGroup = settings.getPreferenceGroup(R.xml.camera_preferences);
+    }
+
+    @Override
+    public boolean collapseCameraControls() {
+        // Remove all the popups/dialog boxes
+        boolean ret = false;
+        if (mPopup != null) {
+            dismissPopup(false);
+            ret = true;
+        }
+        return ret;
+    }
+
+    public boolean removeTopLevelPopup() {
+        // Remove the top level popup or dialog box and return true if there's any
+        if (mPopup != null) {
+            dismissPopup(true);
+            return true;
+        }
+        return false;
+    }
+
+    @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;
+        }
+    }
+
+    // onClick handler for R.id.btn_done
+    @OnClickAttr
+    public void onReviewDoneClicked(View v) {
+        doAttach();
+    }
+
+    // onClick handler for R.id.btn_cancel
+    @OnClickAttr
+    public void onReviewCancelClicked(View v) {
+        doCancel();
+    }
+
+    // onClick handler for R.id.btn_retake
+    @OnClickAttr
+    public void onReviewRetakeClicked(View v) {
+        if (mPaused)
+            return;
+
+        hidePostCaptureAlert();
+        setupPreview();
+    }
+
+    private void doAttach() {
+        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 {
+                int orientation = Exif.getOrientation(data);
+                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(FilterShowActivity.CROP_ACTION);
+
+            cropIntent.setData(tempUri);
+            cropIntent.putExtras(newExtras);
+
+            mActivity.startActivityForResult(cropIntent, REQUEST_CROP);
+        }
+    }
+
+    private void doCancel() {
+        mActivity.setResultEx(Activity.RESULT_CANCELED, new Intent());
+        mActivity.finish();
+    }
+
+    @Override
+    public void onShutterButtonFocus(boolean pressed) {
+        if (mPaused || 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) {
+            if (mSceneMode == Util.SCENE_MODE_HDR) {
+                mActivity.hideSwitcher();
+                mActivity.setSwipingEnabled(false);
+            }
+            mFocusManager.onShutterDown();
+        } else {
+            mFocusManager.onShutterUp();
+        }
+    }
+
+    @Override
+    public void onShutterButtonClick() {
+        if (mPaused || 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 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 (mCountDownView.isCountingDown()) {
+            mCountDownView.cancelCountDown();
+            mCountDownView.startCountDown(seconds, playSound);
+        } else if (seconds > 0) {
+            mCountDownView.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);
+    }
+
+    void waitCameraStartUpThread() {
+        try {
+            if (mCameraStartUpThread != null) {
+                mCameraStartUpThread.cancel();
+                mCameraStartUpThread.join();
+                mCameraStartUpThread = null;
+                setCameraState(IDLE);
+            }
+        } catch (InterruptedException e) {
+            // ignore
+        }
+    }
+
+    @Override
+    public void onPauseBeforeSuper() {
+        mPaused = true;
+    }
+
+    @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()
+                && ActivityBase.isFirstStartAfterScreenOn()) {
+            ActivityBase.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();
+        mCountDownView.cancelCountDown();
+        // Close the camera now because other activities may need to use it.
+        closeCamera();
+        if (mSurfaceTexture != null) {
+            ((CameraScreenNail) mActivity.mCameraScreenNail).releaseSurfaceTexture();
+            mSurfaceTexture = null;
+        }
+        resetScreenOn();
+
+        // Clear UI.
+        collapseCameraControls();
+        if (mFaceView != null) mFaceView.clear();
+
+        if (mFirstTimeInitialized) {
+            if (mMediaSaver != null) {
+                mMediaSaver.finish();
+                mMediaSaver = null;
+                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);
+
+        mPendingSwitchCameraId = -1;
+        if (mFocusManager != null) mFocusManager.removeMessages();
+    }
+
+    private void initializeControlByIntent() {
+        mBlocker = mRootView.findViewById(R.id.blocker);
+        mMenu = mRootView.findViewById(R.id.menu);
+        mMenu.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (mPieRenderer != null) {
+                    // If autofocus is not finished, cancel autofocus so that the
+                    // subsequent touch can be handled by PreviewGestures
+                    if (mCameraState == FOCUSING) cancelAutoFocus();
+                    mPieRenderer.showInCenter();
+                }
+            }
+        });
+        if (mIsImageCaptureIntent) {
+
+            mActivity.hideSwitcher();
+            // Cannot use RotateImageView for "done" and "cancel" button because
+            // the tablet layout uses RotateLayout, which cannot be cast to
+            // RotateImageView.
+            mReviewDoneButton = (Rotatable) mRootView.findViewById(R.id.btn_done);
+            mReviewCancelButton = (Rotatable) mRootView.findViewById(R.id.btn_cancel);
+            mReviewRetakeButton = mRootView.findViewById(R.id.btn_retake);
+            ((View) mReviewCancelButton).setVisibility(View.VISIBLE);
+
+            ((View) mReviewDoneButton).setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    onReviewDoneClicked(v);
+                }
+            });
+            ((View) mReviewCancelButton).setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    onReviewCancelClicked(v);
+                }
+            });
+
+            mReviewRetakeButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    onReviewRetakeClicked(v);
+                }
+            });
+
+            // Not grayed out upon disabled, to make the follow-up fade-out
+            // effect look smooth. Note that the review done button in tablet
+            // layout is not a TwoStateImageView.
+            if (mReviewDoneButton instanceof TwoStateImageView) {
+                ((TwoStateImageView) mReviewDoneButton).enableFilter(false);
+            }
+
+            setupCaptureParams();
+        }
+    }
+
+    /**
+     * 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.
+        mRenderOverlay = (RenderOverlay) mRootView.findViewById(R.id.render_overlay);
+        // 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());
+        }
+    }
+
+    private void initializeMiscControls() {
+        // startPreview needs this.
+        mPreviewFrameLayout = (PreviewFrameLayout) mRootView.findViewById(R.id.frame);
+        // Set touch focus listener.
+        mActivity.setSingleTapUpListener(mPreviewFrameLayout);
+
+        mFaceView = (FaceView) mRootView.findViewById(R.id.face_view);
+        mPreviewFrameLayout.setOnSizeChangedListener(this);
+        mPreviewFrameLayout.setOnLayoutChangeListener(mActivity);
+        if (!ApiHelper.HAS_SURFACE_TEXTURE) {
+            mPreviewSurfaceView =
+                    (PreviewSurfaceView) mRootView.findViewById(R.id.preview_surface_view);
+            mPreviewSurfaceView.setVisibility(View.VISIBLE);
+            mPreviewSurfaceView.getHolder().addCallback(this);
+        }
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        Log.v(TAG, "onConfigurationChanged");
+        setDisplayOrientation();
+
+        // Only the views in photo_module_content need to be removed and recreated
+        // i.e. CountDownView won't be recreated
+        ViewGroup viewGroup = (ViewGroup) mRootView.findViewById(R.id.camera_app);
+        viewGroup.removeAllViews();
+        LayoutInflater inflater = mActivity.getLayoutInflater();
+        inflater.inflate(R.layout.photo_module_content, (ViewGroup) viewGroup);
+
+        // from onCreate()
+        initializeControlByIntent();
+
+        initializeFocusManager();
+        initializeMiscControls();
+        loadCameraPreferences();
+
+        // from initializeFirstTime()
+        mShutterButton = mActivity.getShutterButton();
+        mShutterButton.setOnShutterButtonListener(this);
+        initializeZoom();
+        initOnScreenIndicator();
+        updateOnScreenIndicators();
+        if (mFaceView != null) {
+            mFaceView.clear();
+            mFaceView.setVisibility(View.VISIBLE);
+            mFaceView.setDisplayOrientation(mDisplayOrientation);
+            CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
+            mFaceView.setMirror(info.facing == CameraInfo.CAMERA_FACING_FRONT);
+            mFaceView.resume();
+            mFocusManager.setFaceView(mFaceView);
+        }
+        initializeRenderOverlay();
+        onFullScreenChanged(mActivity.isInCameraApp());
+        if (mJpegImageData != null) {  // Jpeg data found, picture has been taken.
+            showPostCaptureAlert();
+        }
+    }
+
+    @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(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 (removeTopLevelPopup()) return;
+
+        // Check if metering area or focus area is supported.
+        if (!mFocusAreaSupported && !mMeteringAreaSupported) return;
+        mFocusManager.onSingleTapUp(x, y);
+    }
+
+    @Override
+    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 (mIsImageCaptureIntent) {
+            if (!removeTopLevelPopup()) {
+                // no popup to dismiss, cancel image capture
+                doCancel();
+            }
+            return true;
+        } else if (!isCameraIdle()) {
+            // ignore backs while we're taking a picture
+            return true;
+        } else {
+            return removeTopLevelPopup();
+        }
+    }
+
+    @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 (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 (removeTopLevelPopup()) return true;
+                onShutterButtonFocus(true);
+                if (mShutterButton.isInTouchMode()) {
+                    mShutterButton.requestFocusFromTouch();
+                } else {
+                    mShutterButton.requestFocus();
+                }
+                mShutterButton.setPressed(true);
+            }
+            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.setFaceDetectionListener(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 = Util.getDisplayOrientation(0, mCameraId);
+        if (mFaceView != null) {
+            mFaceView.setDisplayOrientation(mDisplayOrientation);
+        }
+        if (mFocusManager != null) {
+            mFocusManager.setDisplayOrientation(mDisplayOrientation);
+        }
+        // GLRoot also uses the DisplayRotation, and needs to be told to layout to update
+        mActivity.getGLRoot().requestLayoutContentPane();
+    }
+
+    // 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);
+
+        if (ApiHelper.HAS_SURFACE_TEXTURE) {
+            CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail;
+            if (mSurfaceTexture == null) {
+                Size size = mParameters.getPreviewSize();
+                if (mCameraDisplayOrientation % 180 == 0) {
+                    screenNail.setSize(size.width, size.height);
+                } else {
+                    screenNail.setSize(size.height, size.width);
+                }
+                screenNail.enableAspectRatioClamping();
+                mActivity.notifyScreenNailChanged();
+                screenNail.acquireSurfaceTexture();
+                CameraStartUpThread t = mCameraStartUpThread;
+                if (t != null && t.isCanceled()) {
+                    return; // Exiting, so no need to get the surface texture.
+                }
+                mSurfaceTexture = screenNail.getSurfaceTexture();
+            }
+            mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation);
+            if (mSurfaceTexture != null) {
+                mCameraDevice.setPreviewTextureAsync((SurfaceTexture) mSurfaceTexture);
+            }
+        } else {
+            mCameraDevice.setDisplayOrientation(mDisplayOrientation);
+            mCameraDevice.setPreviewDisplayAsync(mCameraSurfaceHolder);
+        }
+
+        Log.v(TAG, "startPreview");
+        mCameraDevice.startPreviewAsync();
+
+        mFocusManager.onPreviewStarted();
+
+        if (mSnapshotOnIdle) {
+            mHandler.post(mDoSnapRunnable);
+        }
+    }
+
+    private 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.
+        List<Integer> frameRates = mParameters.getSupportedPreviewFrameRates();
+        if (frameRates != null) {
+            Integer max = Collections.max(frameRates);
+            mParameters.setPreviewFrameRate(max);
+        }
+
+        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
+            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(
+                (AutoFocusMoveCallback) mAutoFocusMoveCallback);
+        } else {
+            mCameraDevice.setAutoFocusMoveCallback(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);
+            updateSceneModeUI();
+            mUpdateSet = 0;
+        } else {
+            if (!mHandler.hasMessages(SET_CAMERA_PARAMETERS_WHEN_IDLE)) {
+                mHandler.sendEmptyMessageDelayed(
+                        SET_CAMERA_PARAMETERS_WHEN_IDLE, 1000);
+            }
+        }
+    }
+
+    private boolean isCameraIdle() {
+        return (mCameraState == IDLE) ||
+                (mCameraState == PREVIEW_STOPPED) ||
+                ((mFocusManager != null) && mFocusManager.isFocusCompleted()
+                        && (mCameraState != SWITCHING_CAMERA));
+    }
+
+    private boolean isImageCaptureIntent() {
+        String action = mActivity.getIntent().getAction();
+        return (MediaStore.ACTION_IMAGE_CAPTURE.equals(action)
+                || ActivityBase.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");
+        }
+    }
+
+    private void showPostCaptureAlert() {
+        if (mIsImageCaptureIntent) {
+            mOnScreenIndicators.setVisibility(View.GONE);
+            mMenu.setVisibility(View.GONE);
+            Util.fadeIn((View) mReviewDoneButton);
+            mShutterButton.setVisibility(View.INVISIBLE);
+            Util.fadeIn(mReviewRetakeButton);
+        }
+    }
+
+    private void hidePostCaptureAlert() {
+        if (mIsImageCaptureIntent) {
+            mOnScreenIndicators.setVisibility(View.VISIBLE);
+            mMenu.setVisibility(View.VISIBLE);
+            Util.fadeOut((View) mReviewDoneButton);
+            mShutterButton.setVisibility(View.VISIBLE);
+            Util.fadeOut(mReviewRetakeButton);
+        }
+    }
+
+    @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);
+        setPreviewFrameLayoutAspectRatio();
+        updateOnScreenIndicators();
+    }
+
+    @Override
+    public void onCameraPickerClicked(int cameraId) {
+        if (mPaused || mPendingSwitchCameraId != -1) return;
+
+        mPendingSwitchCameraId = cameraId;
+        if (ApiHelper.HAS_SURFACE_TEXTURE) {
+            Log.v(TAG, "Start to copy texture. cameraId=" + cameraId);
+            // We need to keep a preview frame for the animation before
+            // releasing the camera. This will trigger onPreviewTextureCopied.
+            ((CameraScreenNail) mActivity.mCameraScreenNail).copyTexture();
+            // Disable all camera controls.
+            setCameraState(SWITCHING_CAMERA);
+        } else {
+            switchCamera();
+        }
+    }
+
+    private void switchCamera() {
+        if (mPaused) return;
+
+        Log.v(TAG, "Start to switch camera. id=" + mPendingSwitchCameraId);
+        mCameraId = mPendingSwitchCameraId;
+        mPendingSwitchCameraId = -1;
+        mPhotoControl.setCameraId(mCameraId);
+
+        // from onPause
+        closeCamera();
+        collapseCameraControls();
+        if (mFaceView != null) mFaceView.clear();
+        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();
+        loadCameraPreferences();
+        initializePhotoControl();
+
+        // from initializeFirstTime
+        initializeZoom();
+        updateOnScreenIndicators();
+        showTapToFocusToastIfNeeded();
+
+        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);
+        }
+    }
+
+    @Override
+    public void onPieOpened(int centerX, int centerY) {
+        mActivity.cancelActivityTouchHandling();
+        mActivity.setSwipingEnabled(false);
+        if (mFaceView != null) {
+            mFaceView.setBlockDraw(true);
+        }
+    }
+
+    @Override
+    public void onPieClosed() {
+        mActivity.setSwipingEnabled(true);
+        if (mFaceView != null) {
+            mFaceView.setBlockDraw(false);
+        }
+    }
+
+    // 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);
+    }
+
+    // TODO: Delete this function after old camera code is removed
+    @Override
+    public void onRestorePreferencesClicked() {
+    }
+
+    @Override
+    public void onOverriddenPreferencesClicked() {
+        if (mPaused) return;
+        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();
+    }
+
+    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);
+    }
+
+    // PreviewFrameLayout size has changed.
+    @Override
+    public void onSizeChanged(int width, int height) {
+        if (mFocusManager != null) mFocusManager.setPreviewSize(width, height);
+    }
+
+    @Override
+    public void onCountDownFinished() {
+        mSnapshotOnIdle = false;
+        mFocusManager.doSnap();
+    }
+
+    void setPreviewFrameLayoutAspectRatio() {
+        // Set the preview frame aspect ratio according to the picture size.
+        Size size = mParameters.getPictureSize();
+        mPreviewFrameLayout.setAspectRatio((double) size.width / size.height);
+    }
+
+    @Override
+    public boolean needsSwitcher() {
+        return !mIsImageCaptureIntent;
+    }
+
+    public void showPopup(AbstractSettingPopup popup) {
+        mActivity.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 topPopupOnly) {
+        dismissPopup(topPopupOnly, true);
+    }
+
+    private void dismissPopup(boolean topOnly, boolean fullScreen) {
+        if (fullScreen) {
+            mActivity.showUI();
+            mBlocker.setVisibility(View.VISIBLE);
+        }
+        setShowMenu(fullScreen);
+        if (mPopup != null) {
+            ((FrameLayout) mRootView).removeView(mPopup);
+            mPopup = null;
+        }
+        mPhotoControl.popupDismissed(topOnly);
+    }
+
+    @Override
+    public void onShowSwitcherPopup() {
+        if (mPieRenderer != null && mPieRenderer.showsItems()) {
+            mPieRenderer.hide();
+        }
+    }
+
+}
diff --git a/src/com/android/camera/PieController.java b/src/com/android/camera/PieController.java
new file mode 100644
index 0000000..8202fca
--- /dev/null
+++ b/src/com/android/camera/PieController.java
@@ -0,0 +1,191 @@
+/*
+ * 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.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 CameraActivity 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(CameraActivity 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();
+        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 void addItem(String prefKey, float center, float sweep) {
+        final IconListPreference pref =
+                (IconListPreference) mPreferenceGroup.findPreference(prefKey);
+        if (pref == null) return;
+        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);
+        // use center and sweep to determine layout
+        item.setFixedSlice(center, sweep);
+        mRenderer.addItem(item);
+        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]);
+                }
+                item.addItem(inner);
+                final int index = i;
+                inner.setOnClickListener(new OnClickListener() {
+                    @Override
+                    public void onClick(PieItem item) {
+                        pref.setValueIndex(index);
+                        reloadPreference(pref);
+                        onSettingChanged(pref);
+                    }
+                });
+            }
+        }
+    }
+
+    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 0000000..4d0519f
--- /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 0000000..231c983
--- /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 0000000..451a35a
--- /dev/null
+++ b/src/com/android/camera/PreviewFrameLayout.java
@@ -0,0 +1,144 @@
+/*
+ * 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.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);
+        if (ApiHelper.HAS_FACE_DETECTION) {
+            ViewStub faceViewStub = (ViewStub) findViewById(R.id.face_view_stub);
+            /* preview_frame_video.xml does not have face view stub, so we need to
+             * check that.
+             */
+            if (faceViewStub != null) {
+                faceViewStub.inflate();
+            }
+        }
+    }
+
+    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 0000000..2dccc3e
--- /dev/null
+++ b/src/com/android/camera/PreviewGestures.java
@@ -0,0 +1,329 @@
+/*
+ * 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.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import com.android.camera.ui.PieRenderer;
+import com.android.camera.ui.RenderOverlay;
+import com.android.camera.ui.ZoomRenderer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PreviewGestures
+        implements ScaleGestureDetector.OnScaleGestureListener {
+
+    private static final String TAG = "CAM_gestures";
+
+    private static final long TIMEOUT_PIE = 200;
+    private static final int MSG_PIE = 1;
+    private static final int MODE_NONE = 0;
+    private static final int MODE_PIE = 1;
+    private static final int MODE_ZOOM = 2;
+    private static final int MODE_MODULE = 3;
+    private static final int MODE_ALL = 4;
+
+    private CameraActivity mActivity;
+    private CameraModule mModule;
+    private RenderOverlay mOverlay;
+    private PieRenderer mPie;
+    private ZoomRenderer mZoom;
+    private MotionEvent mDown;
+    private MotionEvent mCurrent;
+    private ScaleGestureDetector mScale;
+    private List<View> mReceivers;
+    private int mMode;
+    private int mSlop;
+    private int mTapTimeout;
+    private boolean mEnabled;
+    private boolean mZoomOnly;
+    private int mOrientation;
+    private int[] mLocation;
+
+    private Handler mHandler = new Handler() {
+        public void handleMessage(Message msg) {
+            if (msg.what == MSG_PIE) {
+                mMode = MODE_PIE;
+                openPie();
+                cancelActivityTouchHandling(mDown);
+            }
+        }
+    };
+
+    public PreviewGestures(CameraActivity ctx, CameraModule module,
+            ZoomRenderer zoom, PieRenderer pie) {
+        mActivity = ctx;
+        mModule = module;
+        mPie = pie;
+        mZoom = zoom;
+        mMode = MODE_ALL;
+        mScale = new ScaleGestureDetector(ctx, this);
+        mSlop = (int) ctx.getResources().getDimension(R.dimen.pie_touch_slop);
+        mTapTimeout = ViewConfiguration.getTapTimeout();
+        mEnabled = true;
+        mLocation = new int[2];
+    }
+
+    public void setRenderOverlay(RenderOverlay overlay) {
+        mOverlay = overlay;
+    }
+
+    public void setOrientation(int orientation) {
+        mOrientation = orientation;
+    }
+
+    public void setEnabled(boolean enabled) {
+        mEnabled = enabled;
+        if (!enabled) {
+            cancelPie();
+        }
+    }
+
+    public void setZoomOnly(boolean zoom) {
+        mZoomOnly = zoom;
+    }
+
+    public void addTouchReceiver(View v) {
+        if (mReceivers == null) {
+            mReceivers = new ArrayList<View>();
+        }
+        mReceivers.add(v);
+    }
+
+    public void clearTouchReceivers() {
+        if (mReceivers != null) {
+            mReceivers.clear();
+        }
+    }
+
+    public boolean dispatchTouch(MotionEvent m) {
+        if (!mEnabled) {
+            return mActivity.superDispatchTouchEvent(m);
+        }
+        mCurrent = m;
+        if (MotionEvent.ACTION_DOWN == m.getActionMasked()) {
+            if (checkReceivers(m)) {
+                mMode = MODE_MODULE;
+                return mActivity.superDispatchTouchEvent(m);
+            } else {
+                mMode = MODE_ALL;
+                mDown = MotionEvent.obtain(m);
+                if (mPie != null && mPie.showsItems()) {
+                    mMode = MODE_PIE;
+                    return sendToPie(m);
+                }
+                if (mPie != null && !mZoomOnly) {
+                    mHandler.sendEmptyMessageDelayed(MSG_PIE, TIMEOUT_PIE);
+                }
+                if (mZoom != null) {
+                    mScale.onTouchEvent(m);
+                }
+                // make sure this is ok
+                return mActivity.superDispatchTouchEvent(m);
+            }
+        } else if (mMode == MODE_NONE) {
+            return false;
+        } else if (mMode == MODE_PIE) {
+            if (MotionEvent.ACTION_POINTER_DOWN == m.getActionMasked()) {
+                sendToPie(makeCancelEvent(m));
+                if (mZoom != null) {
+                    onScaleBegin(mScale);
+                }
+            } else {
+                return sendToPie(m);
+            }
+            return true;
+        } else if (mMode == MODE_ZOOM) {
+            mScale.onTouchEvent(m);
+            if (!mScale.isInProgress() && MotionEvent.ACTION_POINTER_UP == m.getActionMasked()) {
+                mMode = MODE_NONE;
+                onScaleEnd(mScale);
+            }
+            return true;
+        } else if (mMode == MODE_MODULE) {
+            return mActivity.superDispatchTouchEvent(m);
+        } else {
+            // didn't receive down event previously;
+            // assume module wasn't initialzed and ignore this event.
+            if (mDown == null) {
+                return true;
+            }
+            if (MotionEvent.ACTION_POINTER_DOWN == m.getActionMasked()) {
+                if (!mZoomOnly) {
+                    cancelPie();
+                    sendToPie(makeCancelEvent(m));
+                }
+                if (mZoom != null) {
+                    mScale.onTouchEvent(m);
+                    onScaleBegin(mScale);
+                }
+            } else if ((mMode == MODE_ZOOM) && !mScale.isInProgress()
+                    && MotionEvent.ACTION_POINTER_UP == m.getActionMasked()) {
+                // user initiated and stopped zoom gesture without zooming
+                mScale.onTouchEvent(m);
+                onScaleEnd(mScale);
+            }
+            // not zoom or pie mode and no timeout yet
+            if (mZoom != null) {
+                boolean res = mScale.onTouchEvent(m);
+                if (mScale.isInProgress()) {
+                    cancelPie();
+                    cancelActivityTouchHandling(m);
+                    return res;
+                }
+            }
+            if (MotionEvent.ACTION_UP == m.getActionMasked()) {
+                cancelPie();
+                cancelActivityTouchHandling(m);
+                // must have been tap
+                if (m.getEventTime() - mDown.getEventTime() < mTapTimeout) {
+                    mModule.onSingleTapUp(null,
+                            (int) mDown.getX() - mOverlay.getWindowPositionX(),
+                            (int) mDown.getY() - mOverlay.getWindowPositionY());
+                    return true;
+                } else {
+                    return mActivity.superDispatchTouchEvent(m);
+                }
+            } else if (MotionEvent.ACTION_MOVE == m.getActionMasked()) {
+                if ((Math.abs(m.getX() - mDown.getX()) > mSlop)
+                        || Math.abs(m.getY() - mDown.getY()) > mSlop) {
+                    // moved too far and no timeout yet, no focus or pie
+                    cancelPie();
+                    if (isSwipe(m, true)) {
+                        mMode = MODE_MODULE;
+                        return mActivity.superDispatchTouchEvent(m);
+                    } else {
+                        cancelActivityTouchHandling(m);
+                        if (isSwipe(m , false)) {
+                            mMode = MODE_NONE;
+                        } else if (!mZoomOnly) {
+                            mMode = MODE_PIE;
+                            openPie();
+                            sendToPie(m);
+                        }
+                    }
+                }
+            }
+            return false;
+        }
+    }
+
+    private boolean checkReceivers(MotionEvent m) {
+        if (mReceivers != null) {
+            for (View receiver : mReceivers) {
+                if (isInside(m, receiver)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    // left tests for finger moving right to left
+    private boolean isSwipe(MotionEvent m, boolean left) {
+        float dx = 0;
+        float dy = 0;
+        switch (mOrientation) {
+        case 0:
+            dx = m.getX() - mDown.getX();
+            dy = Math.abs(m.getY() - mDown.getY());
+            break;
+        case 90:
+            dx = - (m.getY() - mDown.getY());
+            dy = Math.abs(m.getX() - mDown.getX());
+            break;
+        case 180:
+            dx = -(m.getX() - mDown.getX());
+            dy = Math.abs(m.getY() - mDown.getY());
+            break;
+        case 270:
+            dx = m.getY() - mDown.getY();
+            dy = Math.abs(m.getX() - mDown.getX());
+            break;
+        }
+        if (left) {
+            return (dx < 0 && dy / -dx < 0.6f);
+        } else {
+            return (dx > 0 && dy / dx < 0.6f);
+        }
+    }
+
+    private boolean isInside(MotionEvent evt, View v) {
+        v.getLocationInWindow(mLocation);
+        return (v.getVisibility() == View.VISIBLE
+                && evt.getX() >= mLocation[0] && evt.getX() < mLocation[0] + v.getWidth()
+                && evt.getY() >= mLocation[1] && evt.getY() < mLocation[1] + v.getHeight());
+    }
+
+    public void cancelActivityTouchHandling(MotionEvent m) {
+        mActivity.superDispatchTouchEvent(makeCancelEvent(m));
+    }
+
+    private MotionEvent makeCancelEvent(MotionEvent m) {
+        MotionEvent c = MotionEvent.obtain(m);
+        c.setAction(MotionEvent.ACTION_CANCEL);
+        return c;
+    }
+
+    private void openPie() {
+        mDown.offsetLocation(-mOverlay.getWindowPositionX(),
+                -mOverlay.getWindowPositionY());
+        mOverlay.directDispatchTouch(mDown, mPie);
+    }
+
+    private void cancelPie() {
+        mHandler.removeMessages(MSG_PIE);
+    }
+
+    private boolean sendToPie(MotionEvent m) {
+        m.offsetLocation(-mOverlay.getWindowPositionX(),
+                -mOverlay.getWindowPositionY());
+        return mOverlay.directDispatchTouch(m, mPie);
+    }
+
+    @Override
+    public boolean onScale(ScaleGestureDetector detector) {
+        return mZoom.onScale(detector);
+    }
+
+    @Override
+    public boolean onScaleBegin(ScaleGestureDetector detector) {
+        if (mMode != MODE_ZOOM) {
+            mMode = MODE_ZOOM;
+            cancelActivityTouchHandling(mCurrent);
+        }
+        if (mCurrent.getActionMasked() != MotionEvent.ACTION_MOVE) {
+            return mZoom.onScaleBegin(detector);
+        } else {
+            return true;
+        }
+    }
+
+    @Override
+    public void onScaleEnd(ScaleGestureDetector detector) {
+        if (mCurrent.getActionMasked() != MotionEvent.ACTION_MOVE) {
+            mZoom.onScaleEnd(detector);
+        }
+    }
+}
diff --git a/src/com/android/camera/ProxyLauncher.java b/src/com/android/camera/ProxyLauncher.java
new file mode 100644
index 0000000..8c56621
--- /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 0000000..9992afa
--- /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 0000000..700d354
--- /dev/null
+++ b/src/com/android/camera/RotateDialogController.java
@@ -0,0 +1,168 @@
+/*
+ * 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;
+
+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 0000000..2fa68f8
--- /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 0000000..a1bbb1a
--- /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 0000000..b5e7831
--- /dev/null
+++ b/src/com/android/camera/SoundClips.java
@@ -0,0 +1,193 @@
+/*
+ * 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.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);
+        }
+    }
+
+    /**
+     * 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;
+            int audioType = ApiHelper.getIntFieldIfExists(AudioManager.class,
+                    "STREAM_SYSTEM_ENFORCED", null, AudioManager.STREAM_RING);
+
+            mSoundIDToPlay = ID_NOT_LOADED;
+
+            mSoundPool = new SoundPool(NUM_SOUND_STREAMS, audioType, 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 0000000..10788c0
--- /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 0000000..648fa7d
--- /dev/null
+++ b/src/com/android/camera/Storage.java
@@ -0,0 +1,172 @@
+/*
+ * 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 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, byte[] jpeg,
+            int width, int height) {
+        // Save the image.
+        String path = generateFilepath(title);
+        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/SwitchAnimManager.java b/src/com/android/camera/SwitchAnimManager.java
new file mode 100644
index 0000000..6ec8822
--- /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 0000000..5f8483d
--- /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 0000000..2953d6a
--- /dev/null
+++ b/src/com/android/camera/Util.java
@@ -0,0 +1,776 @@
+/*
+ * 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.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 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;
+            }
+        }
+    }
+
+    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;
+        }
+    }
+}
diff --git a/src/com/android/camera/VideoController.java b/src/com/android/camera/VideoController.java
new file mode 100644
index 0000000..d84c1ad
--- /dev/null
+++ b/src/com/android/camera/VideoController.java
@@ -0,0 +1,186 @@
+/*
+ * 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.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;
+
+public class VideoController extends PieController
+        implements MoreSettingPopup.Listener,
+        ListPrefSettingPopup.Listener,
+        TimeIntervalPopup.Listener {
+
+
+    private static String TAG = "CAM_videocontrol";
+    private static float FLOAT_PI_DIVIDED_BY_TWO = (float) Math.PI / 2;
+
+    private VideoModule mModule;
+    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;
+
+    public VideoController(CameraActivity activity, VideoModule module, PieRenderer pie) {
+        super(activity, pie);
+        mModule = module;
+    }
+
+    public void initialize(PreferenceGroup group) {
+        super.initialize(group);
+        mPopup = null;
+        mPopupStatus = POPUP_NONE;
+        float sweep = FLOAT_PI_DIVIDED_BY_TWO / 2;
+
+        addItem(CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE, FLOAT_PI_DIVIDED_BY_TWO - sweep, sweep);
+        addItem(CameraSettings.KEY_WHITE_BALANCE, 3 * FLOAT_PI_DIVIDED_BY_TWO + sweep, sweep);
+        PieItem item = makeItem(R.drawable.ic_switch_video_facing_holo_light);
+        item.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO + sweep,  sweep);
+        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]);
+                    mListener.onCameraPickerClicked(newCameraId);
+                }
+            }
+        });
+        mRenderer.addItem(item);
+        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.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO * 3, sweep);
+        item.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(PieItem item) {
+                if (mPopup == null || mPopupStatus != POPUP_FIRST_LEVEL) {
+                    initializePopup();
+                    mPopupStatus = POPUP_FIRST_LEVEL;
+                }
+                mModule.showPopup(mPopup);
+            }
+        });
+        mRenderer.addItem(item);
+    }
+
+    protected void setCameraId(int cameraId) {
+        ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID);
+        pref.setValue("" + cameraId);
+    }
+
+    @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) {
+                mModule.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) mModule.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);
+            mModule.dismissPopup(true);
+            mPopup = timeInterval;
+        } else {
+            ListPrefSettingPopup basic = (ListPrefSettingPopup) inflater.inflate(
+                    R.layout.list_pref_setting_popup, null, false);
+            basic.initialize(pref);
+            basic.setSettingChangedListener(this);
+            mModule.dismissPopup(true);
+            mPopup = basic;
+        }
+        mModule.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 0000000..d32234a
--- /dev/null
+++ b/src/com/android/camera/VideoModule.java
@@ -0,0 +1,2816 @@
+/*
+ * 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.PictureCallback;
+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.Video;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.OrientationEventListener;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.camera.ui.AbstractSettingPopup;
+import com.android.camera.ui.PieRenderer;
+import com.android.camera.ui.PopupManager;
+import com.android.camera.ui.PreviewSurfaceView;
+import com.android.camera.ui.RenderOverlay;
+import com.android.camera.ui.Rotatable;
+import com.android.camera.ui.RotateImageView;
+import com.android.camera.ui.RotateLayout;
+import com.android.camera.ui.RotateTextToast;
+import com.android.camera.ui.TwoStateImageView;
+import com.android.camera.ui.ZoomRenderer;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.util.AccessibilityUtils;
+
+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,
+    CameraPreference.OnPreferenceChangedListener,
+    ShutterButton.OnShutterButtonListener,
+    MediaRecorder.OnErrorListener,
+    MediaRecorder.OnInfoListener,
+    EffectsRecorder.EffectsListener,
+    PieRenderer.PieListener {
+
+    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 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 View mRootView;
+    private boolean mPaused;
+    private int mCameraId;
+    private Parameters mParameters;
+
+    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;
+
+    private PreviewFrameLayout mPreviewFrameLayout;
+    private boolean mSurfaceViewReady;
+    private SurfaceHolder.Callback mSurfaceViewCallback;
+    private PreviewSurfaceView mPreviewSurfaceView;
+    private CameraScreenNail.OnFrameDrawnListener mFrameDrawnListener;
+    private View mReviewControl;
+
+    // An review image having same size as preview. It is displayed when
+    // recording is stopped in capture intent.
+    private ImageView mReviewImage;
+    private Rotatable mReviewCancelButton;
+    private Rotatable mReviewDoneButton;
+    private RotateImageView mReviewPlayButton;
+    private ShutterButton mShutterButton;
+    private TextView mRecordingTimeView;
+    private RotateLayout mBgLearningMessageRotater;
+    private View mBgLearningMessageFrame;
+    private LinearLayout mLabelsLinearLayout;
+
+    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 RotateLayout mRecordingTimeRect;
+    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;
+    private View mTimeLapseLabel;
+
+    private int mDesiredPreviewWidth;
+    private int mDesiredPreviewHeight;
+
+    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 ContentResolver mContentResolver;
+
+    private LocationManager mLocationManager;
+
+    private VideoNamer mVideoNamer;
+
+    private RenderOverlay mRenderOverlay;
+    private PieRenderer mPieRenderer;
+
+    private VideoController mVideoControl;
+    private AbstractSettingPopup mPopup;
+    private int mPendingSwitchCameraId;
+
+    private ZoomRenderer mZoomRenderer;
+
+    private PreviewGestures mGestures;
+    private View mMenu;
+    private View mBlocker;
+    private View mOnScreenIndicators;
+    private ImageView mFlashIndicator;
+
+    private final Handler mHandler = new MainHandler();
+
+    // 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 int mZoomMax;
+    private List<Integer> mZoomRatios;
+    private boolean mRestoreFlash;  // This is used to check if we need to restore the flash
+                                    // status when going back from gallery.
+
+    protected class CameraOpenThread extends Thread {
+        @Override
+        public void run() {
+            openCamera();
+        }
+    }
+
+    private void openCamera() {
+        try {
+            mActivity.mCameraDevice = Util.openCamera(mActivity, mCameraId);
+            mParameters = mActivity.mCameraDevice.getParameters();
+        } catch (CameraHardwareException e) {
+            mActivity.mOpenCameraFail = true;
+        } catch (CameraDisabledException e) {
+            mActivity.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:
+                    mShutterButton.setEnabled(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: {
+                    ((CameraScreenNail) mActivity.mCameraScreenNail).animateSwitchCamera();
+
+                    // Enable all camera controls.
+                    mSwitchingCamera = false;
+                    break;
+                }
+
+                case HIDE_SURFACE_VIEW: {
+                    mPreviewSurfaceView.setVisibility(View.GONE);
+                    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() {
+        mPreviewSurfaceView = (PreviewSurfaceView) mRootView.findViewById(R.id.preview_surface_view);
+        if (!ApiHelper.HAS_SURFACE_TEXTURE) {  // API level < 11
+            if (mSurfaceViewCallback == null) {
+                mSurfaceViewCallback = new SurfaceViewCallback();
+            }
+            mPreviewSurfaceView.getHolder().addCallback(mSurfaceViewCallback);
+            mPreviewSurfaceView.setVisibility(View.VISIBLE);
+        } else if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {  // API level < 16
+            if (mSurfaceViewCallback == null) {
+                mSurfaceViewCallback = new SurfaceViewCallback();
+                mFrameDrawnListener = new CameraScreenNail.OnFrameDrawnListener() {
+                    @Override
+                    public void onFrameDrawn(CameraScreenNail c) {
+                        mHandler.sendEmptyMessage(HIDE_SURFACE_VIEW);
+                    }
+                };
+            }
+            mPreviewSurfaceView.getHolder().addCallback(mSurfaceViewCallback);
+        }
+    }
+
+    private void initializeOverlay() {
+        mRenderOverlay = (RenderOverlay) mRootView.findViewById(R.id.render_overlay);
+        if (mPieRenderer == null) {
+            mPieRenderer = new PieRenderer(mActivity);
+            mVideoControl = new VideoController(mActivity, this, mPieRenderer);
+            mVideoControl.setListener(this);
+            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);
+        }
+        mGestures.setRenderOverlay(mRenderOverlay);
+        mGestures.clearTouchReceivers();
+        mGestures.addTouchReceiver(mMenu);
+        mGestures.addTouchReceiver(mBlocker);
+
+        if (isVideoCaptureIntent()) {
+            if (mReviewCancelButton != null) {
+                mGestures.addTouchReceiver((View) mReviewCancelButton);
+            }
+            if (mReviewDoneButton != null) {
+                mGestures.addTouchReceiver((View) mReviewDoneButton);
+            }
+            if (mReviewPlayButton != null) {
+                mGestures.addTouchReceiver((View) mReviewPlayButton);
+            }
+        }
+    }
+
+    @Override
+    public void init(CameraActivity activity, View root, boolean reuseScreenNail) {
+        mActivity = activity;
+        mRootView = root;
+        mPreferences = new ComboPreferences(mActivity);
+        CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal());
+        mCameraId = getPreferredCameraId(mPreferences);
+
+        mPreferences.setLocalId(mActivity, mCameraId);
+        CameraSettings.upgradeLocalPreferences(mPreferences.getLocal());
+
+        mActivity.mNumberOfCameras = CameraHolder.instance().getNumberOfCameras();
+        mPrefVideoEffectDefault = mActivity.getString(R.string.pref_video_effect_default);
+        resetEffect();
+
+        /*
+         * 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();
+
+        mActivity.getLayoutInflater().inflate(R.layout.video_module, (ViewGroup) mRootView);
+
+        // Surface texture is from camera screen nail and startPreview needs it.
+        // This must be done before startPreview.
+        mIsVideoCaptureIntent = isVideoCaptureIntent();
+        if (reuseScreenNail) {
+            mActivity.reuseCameraScreenNail(!mIsVideoCaptureIntent);
+        } else {
+            mActivity.createCameraScreenNail(!mIsVideoCaptureIntent);
+        }
+        initializeSurfaceView();
+
+        // Make sure camera device is opened.
+        try {
+            cameraOpenThread.join();
+            if (mActivity.mOpenCameraFail) {
+                Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera);
+                return;
+            } else if (mActivity.mCameraDisabled) {
+                Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
+                return;
+            }
+        } catch (InterruptedException ex) {
+            // ignore
+        }
+
+        readVideoPreferences();
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                startPreview();
+            }
+        }).start();
+
+        initializeControlByIntent();
+        initializeOverlay();
+        initializeMiscControls();
+
+        mQuickCapture = mActivity.getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false);
+        mLocationManager = new LocationManager(mActivity, null);
+
+        setOrientationIndicator(0, false);
+        setDisplayOrientation();
+
+        showTimeLapseUI(mCaptureTimeLapse);
+        initializeVideoSnapshot();
+        resizeForPreviewAspectRatio();
+
+        initializeVideoControl();
+        mPendingSwitchCameraId = -1;
+        updateOnScreenIndicators();
+    }
+
+    @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));
+    }
+
+    @Override
+    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;
+    }
+
+    private void enableCameraControls(boolean enable) {
+        if (mGestures != null) {
+            mGestures.setZoomOnly(!enable);
+        }
+        if (mPieRenderer != null && mPieRenderer.showsItems()) {
+            mPieRenderer.hide();
+        }
+    }
+
+    private void initializeVideoControl() {
+        loadCameraPreferences();
+        mVideoControl.initialize(mPreferenceGroup);
+        if (effectsActive()) {
+            mVideoControl.overrideSettings(
+                    CameraSettings.KEY_VIDEO_QUALITY,
+                    Integer.toString(getLowVideoQuality()));
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    private static int getLowVideoQuality() {
+        if (ApiHelper.HAS_FINE_RESOLUTION_QUALITY_LEVELS) {
+            return CamcorderProfile.QUALITY_480P;
+        } else {
+            return CamcorderProfile.QUALITY_LOW;
+        }
+    }
+
+
+    @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 setOrientationIndicator(int orientation, boolean animation) {
+        Rotatable[] indicators = {
+                mBgLearningMessageRotater,
+                mReviewDoneButton, mReviewPlayButton};
+        for (Rotatable indicator : indicators) {
+            if (indicator != null) indicator.setOrientation(orientation, animation);
+        }
+        if (mGestures != null) {
+            mGestures.setOrientation(orientation);
+        }
+
+        // We change the orientation of the review cancel button only for tablet
+        // UI because there's a label along with the X icon. For phone UI, we
+        // don't change the orientation because there's only a symmetrical X
+        // icon.
+        if (mReviewCancelButton instanceof RotateLayout) {
+            mReviewCancelButton.setOrientation(orientation, 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);
+    }
+
+    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) {
+        doReturnToCaller(true);
+    }
+
+    @OnClickAttr
+    public void onReviewCancelClicked(View v) {
+        stopVideoRecording();
+        doReturnToCaller(false);
+    }
+
+    private void onStopVideoRecording() {
+        mEffectsDisplayResult = true;
+        boolean recordFail = stopVideoRecording();
+        if (mIsVideoCaptureIntent) {
+            if (!effectsActive()) {
+                if (mQuickCapture) {
+                    doReturnToCaller(!recordFail);
+                } else if (!recordFail) {
+                    showAlert();
+                }
+            }
+        } 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.
+                ((CameraScreenNail) mActivity.mCameraScreenNail).animateCapture(mDisplayRotation);
+            }
+        }
+    }
+
+    public void onProtectiveCurtainClick(View v) {
+        // Consume clicks
+    }
+
+    @Override
+    public void onShutterButtonClick() {
+        if (collapseCameraControls() || mSwitchingCamera) return;
+
+        boolean stop = mMediaRecorderRecording;
+
+        if (stop) {
+            onStopVideoRecording();
+        } else {
+            startVideoRecording();
+        }
+        mShutterButton.setEnabled(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) {
+        // Do nothing (everything happens in onShutterButtonClick).
+    }
+
+    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 defaultQuality = CameraSettings.getDefaultVideoQuality(mCameraId,
+                mActivity.getResources().getString(R.string.pref_video_quality_default));
+        String videoQuality =
+                mPreferences.getString(CameraSettings.KEY_VIDEO_QUALITY,
+                        defaultQuality);
+        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 = getLowVideoQuality();
+            }
+        } 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();
+    }
+
+    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 = mActivity.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;
+        }
+        Log.v(TAG, "mDesiredPreviewWidth=" + mDesiredPreviewWidth +
+                ". mDesiredPreviewHeight=" + mDesiredPreviewHeight);
+    }
+
+    private void resizeForPreviewAspectRatio() {
+        mPreviewFrameLayout.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 (mActivity.mOpenCameraFail || mActivity.mCameraDisabled)
+            return;
+
+        mZoomValue = 0;
+
+        showVideoSnapshotUI(false);
+
+
+        if (!mPreviewing) {
+            if (resetEffect()) {
+                mBgLearningMessageFrame.setVisibility(View.GONE);
+            }
+            openCamera();
+            if (mActivity.mOpenCameraFail) {
+                Util.showErrorAndFinish(mActivity,
+                        R.string.cannot_connect_camera);
+                return;
+            } else if (mActivity.mCameraDisabled) {
+                Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
+                return;
+            }
+            readVideoPreferences();
+            resizeForPreviewAspectRatio();
+            new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    startPreview();
+                }
+            }).start();
+        }
+
+        // Initializing it here after the preview is started.
+        initializeZoom();
+
+        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);
+
+        mVideoNamer = new VideoNamer();
+    }
+
+    private void setDisplayOrientation() {
+        mDisplayRotation = Util.getDisplayRotation(mActivity);
+        if (ApiHelper.HAS_SURFACE_TEXTURE) {
+            // The display rotation is handled by gallery.
+            mCameraDisplayOrientation = Util.getDisplayOrientation(0, mCameraId);
+        } else {
+            // We need to consider display rotation ourselves.
+            mCameraDisplayOrientation = Util.getDisplayOrientation(mDisplayRotation, mCameraId);
+        }
+        // GLRoot also uses the DisplayRotation, and needs to be told to layout to update
+        mActivity.getGLRoot().requestLayoutContentPane();
+    }
+
+    private void startPreview() {
+        Log.v(TAG, "startPreview");
+
+        mActivity.mCameraDevice.setErrorCallback(mErrorCallback);
+        if (mPreviewing == true) {
+            stopPreview();
+            if (effectsActive() && mEffectsRecorder != null) {
+                mEffectsRecorder.release();
+                mEffectsRecorder = null;
+            }
+        }
+
+        mPreviewing = true;
+
+        setDisplayOrientation();
+        mActivity.mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation);
+        setCameraParameters();
+
+        try {
+            if (!effectsActive()) {
+                if (ApiHelper.HAS_SURFACE_TEXTURE) {
+                    SurfaceTexture surfaceTexture = ((CameraScreenNail) mActivity.mCameraScreenNail)
+                            .getSurfaceTexture();
+                    if (surfaceTexture == null) {
+                        return; // The texture has been destroyed (pause, etc)
+                    }
+                    mActivity.mCameraDevice.setPreviewTextureAsync(surfaceTexture);
+                } else {
+                    mActivity.mCameraDevice.setPreviewDisplayAsync(mPreviewSurfaceView.getHolder());
+                }
+                mActivity.mCameraDevice.startPreviewAsync();
+            } else {
+                initializeEffectsPreview();
+                mEffectsRecorder.startPreview();
+            }
+        } catch (Throwable ex) {
+            closeCamera();
+            throw new RuntimeException("startPreview failed", ex);
+        } finally {
+            mActivity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    if (mActivity.mOpenCameraFail) {
+                        Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera);
+                    } else if (mActivity.mCameraDisabled) {
+                        Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
+                    }
+                }
+            });
+        }
+    }
+
+    private void stopPreview() {
+        mActivity.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 (mActivity.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();
+        mActivity.mCameraDevice.setZoomChangeListener(null);
+        mActivity.mCameraDevice.setErrorCallback(null);
+        CameraHolder.instance().release();
+        mActivity.mCameraDevice = null;
+        mPreviewing = false;
+        mSnapshotInProgress = false;
+    }
+
+    private void releasePreviewResources() {
+        if (ApiHelper.HAS_SURFACE_TEXTURE) {
+            CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail;
+            screenNail.releaseSurfaceTexture();
+            if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
+                mHandler.removeMessages(HIDE_SURFACE_VIEW);
+                mPreviewSurfaceView.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    @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();
+            clearVideoNamer();
+        }
+
+        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;
+        // 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 (mPieRenderer != null && mPieRenderer.showsItems()) {
+            mPieRenderer.hide();
+            return true;
+        } else {
+            return 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) {
+                    mShutterButton.performClick();
+                    return true;
+                }
+                break;
+            case KeyEvent.KEYCODE_DPAD_CENTER:
+                if (event.getRepeatCount() == 0) {
+                    mShutterButton.performClick();
+                    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:
+                mShutterButton.setPressed(false);
+                return true;
+        }
+        return false;
+    }
+
+    private 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) {
+            mMediaRecorder.setPreviewDisplay(mPreviewSurfaceView.getHolder().getSurface());
+        } else 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();
+            mActivity.mCameraDevice.setPreviewDisplayAsync(mPreviewSurfaceView.getHolder());
+            // 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.
+            mActivity.mCameraDevice.setDisplayOrientation(
+                    Util.getDisplayOrientation(mDisplayRotation, mCameraId));
+            mActivity.mCameraDevice.startPreviewAsync();
+            mPreviewing = true;
+            mMediaRecorder.setPreviewDisplay(mPreviewSurfaceView.getHolder().getSurface());
+        }
+    }
+
+    // Prepares media recorder.
+    private void initializeRecorder() {
+        Log.v(TAG, "initializeRecorder");
+        // If the mCameraDevice is null, then this activity is going to finish
+        if (mActivity.mCameraDevice == null) return;
+
+        if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING && ApiHelper.HAS_SURFACE_TEXTURE) {
+            // 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().
+            mPreviewSurfaceView.setVisibility(View.VISIBLE);
+            if (!mSurfaceViewReady) return;
+        }
+
+        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.
+        mActivity.mCameraDevice.unlock();
+        mMediaRecorder.setCamera(mActivity.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 (mActivity.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(mActivity.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);
+
+        CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail;
+        mEffectsRecorder.setPreviewSurfaceTexture(screenNail.getSurfaceTexture(),
+                screenNail.getWidth(), screenNail.getHeight());
+
+        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(7);
+        mCurrentVideoValues.put(Video.Media.TITLE, title);
+        mCurrentVideoValues.put(Video.Media.DISPLAY_NAME, filename);
+        mCurrentVideoValues.put(Video.Media.DATE_TAKEN, dateTaken);
+        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());
+        }
+        mVideoNamer.prepareUri(mContentResolver, mCurrentVideoValues);
+        mVideoFilename = tmpPath;
+        Log.v(TAG, "New video filename: " + mVideoFilename);
+    }
+
+    private boolean addVideoToMediaStore() {
+        boolean fail = false;
+        if (mVideoFileDescriptor == null) {
+            mCurrentVideoValues.put(Video.Media.SIZE,
+                    new File(mCurrentVideoFilename).length());
+            long duration = SystemClock.uptimeMillis() - mRecordingStartTime;
+            if (duration > 0) {
+                if (mCaptureTimeLapse) {
+                    duration = getTimeLapseVideoLength(duration);
+                }
+                mCurrentVideoValues.put(Video.Media.DURATION, duration);
+            } else {
+                Log.w(TAG, "Video duration <= 0 : " + duration);
+            }
+            try {
+                mCurrentVideoUri = mVideoNamer.getUri();
+                mActivity.addSecureAlbumItemIfNeeded(true, mCurrentVideoUri);
+
+                // Rename the video file to the final name. This avoids other
+                // apps reading incomplete data.  We need to do it after the
+                // above mVideoNamer.getUri() call, so we are certain that the
+                // previous insert to MediaProvider is completed.
+                String finalName = mCurrentVideoValues.getAsString(
+                        Video.Media.DATA);
+                if (new File(mCurrentVideoFilename).renameTo(new File(finalName))) {
+                    mCurrentVideoFilename = finalName;
+                }
+
+                mContentResolver.update(mCurrentVideoUri, mCurrentVideoValues
+                        , null, null);
+                mActivity.sendBroadcast(new Intent(Util.ACTION_NEW_VIDEO,
+                        mCurrentVideoUri));
+            } 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);
+                mCurrentVideoUri = null;
+                mCurrentVideoFilename = null;
+                fail = true;
+            } finally {
+                Log.v(TAG, "Current video URI: " + mCurrentVideoUri);
+            }
+        }
+        mCurrentVideoValues = null;
+        return fail;
+    }
+
+    private void deleteCurrentVideo() {
+        // Remove the video and the uri if the uri is not passed in by intent.
+        if (mCurrentVideoFilename != null) {
+            deleteVideoFile(mCurrentVideoFilename);
+            mCurrentVideoFilename = null;
+            if (mCurrentVideoUri != null) {
+                mContentResolver.delete(mCurrentVideoUri, null, null);
+                mCurrentVideoUri = null;
+            }
+        }
+        mActivity.updateStorageSpaceAndHint();
+    }
+
+    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");
+        mActivity.setSwipingEnabled(false);
+
+        mActivity.updateStorageSpaceAndHint();
+        if (mActivity.getStorageSpace() <= Storage.LOW_STORAGE_THRESHOLD) {
+            Log.v(TAG, "Storage issue, ignore the start request");
+            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.
+                mActivity.mCameraDevice.lock();
+                return;
+            }
+        }
+
+        // Make sure the video recording has started before announcing
+        // this in accessibility.
+        AccessibilityUtils.makeAnnouncement(mShutterButton,
+                mActivity.getString(R.string.video_recording_started));
+
+        // 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 = mActivity.mCameraDevice.getParameters();
+        }
+
+        enableCameraControls(false);
+
+        mMediaRecorderRecording = true;
+        mActivity.getOrientationManager().lockOrientation();
+        mRecordingStartTime = SystemClock.uptimeMillis();
+        showRecordingUI(true);
+
+        updateRecordingTime();
+        keepScreenOn();
+    }
+
+    private void showRecordingUI(boolean recording) {
+        mMenu.setVisibility(recording ? View.GONE : View.VISIBLE);
+        mOnScreenIndicators.setVisibility(recording ? View.GONE : View.VISIBLE);
+        if (recording) {
+            mShutterButton.setImageResource(R.drawable.btn_shutter_video_recording);
+            mActivity.hideSwitcher();
+            mRecordingTimeView.setText("");
+            mRecordingTimeView.setVisibility(View.VISIBLE);
+            if (mReviewControl != null) mReviewControl.setVisibility(View.GONE);
+            // 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
+                    && mParameters.isZoomSupported()) {
+                // TODO: disable zoom UI here.
+            }
+        } else {
+            mShutterButton.setImageResource(R.drawable.btn_new_shutter_video);
+            mActivity.showSwitcher();
+            mRecordingTimeView.setVisibility(View.GONE);
+            if (mReviewControl != null) mReviewControl.setVisibility(View.VISIBLE);
+            if (!ApiHelper.HAS_ZOOM_WHEN_RECORDING
+                    && mParameters.isZoomSupported()) {
+                // TODO: enable zoom UI here.
+            }
+        }
+    }
+
+    private void showAlert() {
+        Bitmap bitmap = null;
+        if (mVideoFileDescriptor != null) {
+            bitmap = Thumbnail.createVideoThumbnailBitmap(mVideoFileDescriptor.getFileDescriptor(),
+                    mPreviewFrameLayout.getWidth());
+        } else if (mCurrentVideoFilename != null) {
+            bitmap = Thumbnail.createVideoThumbnailBitmap(mCurrentVideoFilename,
+                    mPreviewFrameLayout.getWidth());
+        }
+        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);
+            mReviewImage.setImageBitmap(bitmap);
+            mReviewImage.setVisibility(View.VISIBLE);
+        }
+
+        Util.fadeOut(mShutterButton);
+
+        Util.fadeIn((View) mReviewDoneButton);
+        Util.fadeIn(mReviewPlayButton);
+        mMenu.setVisibility(View.GONE);
+        mOnScreenIndicators.setVisibility(View.GONE);
+        enableCameraControls(false);
+
+        showTimeLapseUI(false);
+    }
+
+    private void hideAlert() {
+        mReviewImage.setVisibility(View.GONE);
+        mShutterButton.setEnabled(true);
+        mMenu.setVisibility(View.VISIBLE);
+        mOnScreenIndicators.setVisibility(View.VISIBLE);
+        enableCameraControls(true);
+
+        Util.fadeOut((View) mReviewDoneButton);
+        Util.fadeOut(mReviewPlayButton);
+
+        Util.fadeIn(mShutterButton);
+
+        if (mCaptureTimeLapse) {
+            showTimeLapseUI(true);
+        }
+    }
+
+    private boolean stopVideoRecording() {
+        Log.v(TAG, "stopVideoRecording");
+        mActivity.setSwipingEnabled(true);
+        mActivity.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 addVideoToMediaStore 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(mShutterButton,
+                        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;
+            mActivity.getOrientationManager().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);
+            }
+
+            showRecordingUI(false);
+            if (!mIsVideoCaptureIntent) {
+                enableCameraControls(true);
+            }
+            // The orientation was fixed during video recording. Now make it
+            // reflect the device orientation as video recording is stopped.
+            setOrientationIndicator(0, true);
+            keepScreenOnAwhile();
+            if (shouldAddToMediaStoreNow) {
+                if (addVideoToMediaStore()) fail = true;
+            }
+        }
+        // always release media recorder if no effects running
+        if (!effectsActive()) {
+            releaseMediaRecorder();
+            if (!mPaused) {
+                mActivity.mCameraDevice.lock();
+                if (ApiHelper.HAS_SURFACE_TEXTURE &&
+                    !ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
+                    stopPreview();
+                    // Switch back to use SurfaceTexture for preview.
+                    ((CameraScreenNail) mActivity.mCameraScreenNail).setOneTimeOnFrameDrawnListener(
+                            mFrameDrawnListener);
+                    startPreview();
+                }
+            }
+        }
+        // Update the parameters here because the parameters might have been altered
+        // by MediaRecorder.
+        if (!mPaused) mParameters = mActivity.mCameraDevice.getParameters();
+        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;
+        }
+
+        mRecordingTimeView.setText(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);
+
+            mRecordingTimeView.setTextColor(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);
+        mParameters.setPreviewFrameRate(mProfile.videoFrameRate);
+
+        // Set flash mode.
+        String flashMode;
+        if (mActivity.mShowCameraAppView) {
+            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);
+
+        mActivity.mCameraDevice.setParameters(mParameters);
+        // Keep preview size up to date.
+        mParameters = mActivity.mCameraDevice.getParameters();
+
+        updateCameraScreenNailSize(mDesiredPreviewWidth, mDesiredPreviewHeight);
+    }
+
+    private void updateCameraScreenNailSize(int width, int height) {
+        if (!ApiHelper.HAS_SURFACE_TEXTURE) return;
+
+        if (mCameraDisplayOrientation % 180 != 0) {
+            int tmp = width;
+            width = height;
+            height = tmp;
+        }
+
+        CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail;
+        int oldWidth = screenNail.getWidth();
+        int oldHeight = screenNail.getHeight();
+
+        if (oldWidth != width || oldHeight != height) {
+            screenNail.setSize(width, height);
+            screenNail.enableAspectRatioClamping();
+            mActivity.notifyScreenNailChanged();
+        }
+
+        if (screenNail.getSurfaceTexture() == null) {
+            screenNail.acquireSurfaceTexture();
+        }
+    }
+
+    @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.
+            mBgLearningMessageFrame.setVisibility(View.GONE);
+            checkQualityAndStartPreview();
+        } else if (effectMsg == EffectsRecorder.EFFECT_MSG_RECORDING_DONE) {
+            // This follows the codepath from onStopVideoRecording.
+            if (mEffectsDisplayResult && !addVideoToMediaStore()) {
+                if (mIsVideoCaptureIntent) {
+                    if (mQuickCapture) {
+                        doReturnToCaller(true);
+                    } else {
+                        showAlert();
+                    }
+                }
+            }
+            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();
+                clearVideoNamer();
+            }
+        } else if (effectMsg == EffectsRecorder.EFFECT_MSG_PREVIEW_RUNNING) {
+            // Enable the shutter button once the preview is complete.
+            mShutterButton.setEnabled(true);
+        } else if (effectId == EffectsRecorder.EFFECT_BACKDROPPER) {
+            switch (effectMsg) {
+                case EffectsRecorder.EFFECT_MSG_STARTED_LEARNING:
+                    mBgLearningMessageFrame.setVisibility(View.VISIBLE);
+                    break;
+                case EffectsRecorder.EFFECT_MSG_DONE_LEARNING:
+                case EffectsRecorder.EFFECT_MSG_SWITCHING_EFFECT:
+                    mBgLearningMessageFrame.setVisibility(View.GONE);
+                    break;
+            }
+        }
+        // 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) {
+        // Remove training message
+        mBgLearningMessageFrame.setVisibility(View.GONE);
+        // 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);
+    }
+
+    private void initializeControlByIntent() {
+        mBlocker = mRootView.findViewById(R.id.blocker);
+        mMenu = mRootView.findViewById(R.id.menu);
+        mMenu.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (mPieRenderer != null) {
+                    mPieRenderer.showInCenter();
+                }
+            }
+        });
+        mOnScreenIndicators = mRootView.findViewById(R.id.on_screen_indicators);
+        mFlashIndicator = (ImageView) mRootView.findViewById(R.id.menu_flash_indicator);
+        if (mIsVideoCaptureIntent) {
+            mActivity.hideSwitcher();
+            // Cannot use RotateImageView for "done" and "cancel" button because
+            // the tablet layout uses RotateLayout, which cannot be cast to
+            // RotateImageView.
+            mReviewDoneButton = (Rotatable) mRootView.findViewById(R.id.btn_done);
+            mReviewCancelButton = (Rotatable) mRootView.findViewById(R.id.btn_cancel);
+            mReviewPlayButton = (RotateImageView) mRootView.findViewById(R.id.btn_play);
+
+            ((View) mReviewCancelButton).setVisibility(View.VISIBLE);
+
+            ((View) mReviewDoneButton).setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    onReviewDoneClicked(v);
+                }
+            });
+            ((View) mReviewCancelButton).setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    onReviewCancelClicked(v);
+                }
+            });
+
+            ((View) mReviewPlayButton).setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    onReviewPlayClicked(v);
+                }
+            });
+
+
+            // Not grayed out upon disabled, to make the follow-up fade-out
+            // effect look smooth. Note that the review done button in tablet
+            // layout is not a TwoStateImageView.
+            if (mReviewDoneButton instanceof TwoStateImageView) {
+                ((TwoStateImageView) mReviewDoneButton).enableFilter(false);
+            }
+        }
+    }
+
+    private void initializeMiscControls() {
+        mPreviewFrameLayout = (PreviewFrameLayout) mRootView.findViewById(R.id.frame);
+        mPreviewFrameLayout.setOnLayoutChangeListener(mActivity);
+        mReviewImage = (ImageView) mRootView.findViewById(R.id.review_image);
+
+        mShutterButton = mActivity.getShutterButton();
+        mShutterButton.setImageResource(R.drawable.btn_new_shutter_video);
+        mShutterButton.setOnShutterButtonListener(this);
+        mShutterButton.requestFocus();
+
+        // 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()) {
+            mShutterButton.setEnabled(false);
+        }
+
+        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);
+
+        mBgLearningMessageRotater = (RotateLayout) mRootView.findViewById(R.id.bg_replace_message);
+        mBgLearningMessageFrame = mRootView.findViewById(R.id.bg_replace_message_frame);
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        setDisplayOrientation();
+
+        // Change layout in response to configuration change
+        LayoutInflater inflater = mActivity.getLayoutInflater();
+        ((ViewGroup) mRootView).removeAllViews();
+        inflater.inflate(R.layout.video_module, (ViewGroup) mRootView);
+
+        // from onCreate()
+        initializeControlByIntent();
+        initializeOverlay();
+        initializeSurfaceView();
+        initializeMiscControls();
+        showTimeLapseUI(mCaptureTimeLapse);
+        initializeVideoSnapshot();
+        resizeForPreviewAspectRatio();
+
+        // from onResume()
+        showVideoSnapshotUI(false);
+        initializeZoom();
+        onFullScreenChanged(mActivity.isInCameraApp());
+        updateOnScreenIndicators();
+    }
+
+    @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 (mActivity.mCameraDevice == null) return;
+
+            boolean recordLocation = RecordLocationPreference.get(
+                    mPreferences, mContentResolver);
+            mLocationManager.recordLocation(recordLocation);
+
+            // Check if the current effects selection has changed
+            if (updateEffectSelection()) return;
+
+            readVideoPreferences();
+            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();
+            }
+            updateOnScreenIndicators();
+        }
+    }
+
+    private void updateOnScreenIndicators() {
+        updateFlashOnScreenIndicator(mParameters.getFlashMode());
+    }
+
+    private 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);
+            }
+        }
+    }
+
+    private void switchCamera() {
+        if (mPaused) return;
+
+        Log.d(TAG, "Start to switch camera.");
+        mCameraId = mPendingSwitchCameraId;
+        mPendingSwitchCameraId = -1;
+        mVideoControl.setCameraId(mCameraId);
+
+        closeCamera();
+
+        // 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
+        initializeZoom();
+        setOrientationIndicator(0, false);
+
+        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);
+        }
+        updateOnScreenIndicators();
+    }
+
+    // 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();
+        showTimeLapseUI(mCaptureTimeLapse);
+        Size size = mParameters.getPreviewSize();
+        if (size.width != mDesiredPreviewWidth
+                || size.height != mDesiredPreviewHeight) {
+            resizeForPreviewAspectRatio();
+        }
+        // Start up preview again
+        startPreview();
+    }
+
+    private void showTimeLapseUI(boolean enable) {
+        if (mTimeLapseLabel != null) {
+            mTimeLapseLabel.setVisibility(enable ? View.VISIBLE : View.GONE);
+        }
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent m) {
+        if (mSwitchingCamera) return true;
+        if (mPopup == null && mGestures != null && mRenderOverlay != null) {
+            return mGestures.dispatchTouch(m);
+        } else if (mPopup != null) {
+            return mActivity.superDispatchTouchEvent(m);
+        }
+        return false;
+    }
+
+    private class ZoomChangeListener implements ZoomRenderer.OnZoomChangedListener {
+        @Override
+        public void onZoomValueChanged(int value) {
+            // Not useful to change zoom value when the activity is paused.
+            if (mPaused) return;
+            mZoomValue = value;
+            // Set zoom parameters asynchronously
+            mParameters.setZoom(mZoomValue);
+            mActivity.mCameraDevice.setParametersAsync(mParameters);
+            Parameters p = mActivity.mCameraDevice.getParameters();
+            mZoomRenderer.setZoomValue(mZoomRatios.get(p.getZoom()));
+        }
+
+        @Override
+        public void onZoomStart() {
+        }
+        @Override
+        public void onZoomEnd() {
+        }
+    }
+
+    private void initializeZoom() {
+        if (!mParameters.isZoomSupported()) return;
+        mZoomMax = mParameters.getMaxZoom();
+        mZoomRatios = mParameters.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(mParameters.getZoom());
+        mZoomRenderer.setZoomValue(mZoomRatios.get(mParameters.getZoom()));
+        mZoomRenderer.setOnZoomChangeListener(new ZoomChangeListener());
+    }
+
+    private void initializeVideoSnapshot() {
+        if (Util.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) {
+            mActivity.setSingleTapUpListener(mPreviewFrameLayout);
+            // 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);
+            }
+        } else {
+            mActivity.setSingleTapUpListener(null);
+        }
+    }
+
+    void showVideoSnapshotUI(boolean enabled) {
+        if (Util.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) {
+            if (ApiHelper.HAS_SURFACE_TEXTURE && enabled) {
+                ((CameraScreenNail) mActivity.mCameraScreenNail).animateCapture(mDisplayRotation);
+            } else {
+                mPreviewFrameLayout.showBorder(enabled);
+            }
+            mShutterButton.setEnabled(!enabled);
+        }
+    }
+
+    // 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;
+        }
+
+        if (mPaused || mSnapshotInProgress || effectsActive()) {
+            return;
+        }
+
+        if (!mMediaRecorderRecording) {
+            // check for dismissing popup
+            if (mPopup != null) {
+                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);
+        mActivity.mCameraDevice.setParameters(mParameters);
+
+        Log.v(TAG, "Video snapshot start");
+        mActivity.mCameraDevice.takePicture(null, null, null, new JpegPictureCallback(loc));
+        showVideoSnapshotUI(true);
+        mSnapshotInProgress = true;
+    }
+
+    @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 (!mActivity.mShowCameraAppView) {
+            if (mParameters.getFlashMode().equals(Parameters.FLASH_MODE_OFF)) {
+                mRestoreFlash = false;
+                return;
+            }
+            mRestoreFlash = true;
+            setCameraParameters();
+        } else if (mRestoreFlash) {
+            mRestoreFlash = false;
+            setCameraParameters();
+        }
+    }
+
+    private void setShowMenu(boolean show) {
+        if (mOnScreenIndicators != null) {
+            mOnScreenIndicators.setVisibility(show ? View.VISIBLE : View.GONE);
+        }
+        if (mMenu != null) {
+            mMenu.setVisibility(show ? View.VISIBLE : View.GONE);
+        }
+    }
+
+    @Override
+    public void onFullScreenChanged(boolean full) {
+        if (mGestures != null) {
+            mGestures.setEnabled(full);
+        }
+        if (mPopup != null) {
+            dismissPopup(false, full);
+        }
+        if (mRenderOverlay != null) {
+            // this can not happen in capture mode
+            mRenderOverlay.setVisibility(full ? View.VISIBLE : View.GONE);
+        }
+        setShowMenu(full);
+        if (mBlocker != null) {
+            // this can not happen in capture mode
+            mBlocker.setVisibility(full ? View.VISIBLE : View.GONE);
+        }
+        if (ApiHelper.HAS_SURFACE_TEXTURE) {
+            if (mActivity.mCameraScreenNail != null) {
+                ((CameraScreenNail) mActivity.mCameraScreenNail).setFullScreen(full);
+            }
+            return;
+        }
+        if (full) {
+            mPreviewSurfaceView.expand();
+        } else {
+            mPreviewSurfaceView.shrink();
+        }
+    }
+
+    private final class JpegPictureCallback implements PictureCallback {
+        Location mLocation;
+
+        public JpegPictureCallback(Location loc) {
+            mLocation = loc;
+        }
+
+        @Override
+        public void onPictureTaken(byte [] jpegData, android.hardware.Camera 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);
+        int orientation = Exif.getOrientation(data);
+        Size s = mParameters.getPictureSize();
+        Uri uri = Storage.addImage(mContentResolver, title, dateTaken, loc, orientation, data,
+                s.width, s.height);
+        if (uri != null) {
+            Util.broadcastNewPicture(mActivity, uri);
+        }
+    }
+
+    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();
+    }
+
+    private void clearVideoNamer() {
+        if (mVideoNamer != null) {
+            mVideoNamer.finish();
+            mVideoNamer = null;
+        }
+    }
+
+    private static class VideoNamer extends Thread {
+        private boolean mRequestPending;
+        private ContentResolver mResolver;
+        private ContentValues mValues;
+        private boolean mStop;
+        private Uri mUri;
+
+        // Runs in main thread
+        public VideoNamer() {
+            start();
+        }
+
+        // Runs in main thread
+        public synchronized void prepareUri(
+                ContentResolver resolver, ContentValues values) {
+            mRequestPending = true;
+            mResolver = resolver;
+            mValues = new ContentValues(values);
+            notifyAll();
+        }
+
+        // Runs in main thread
+        public synchronized Uri getUri() {
+            // wait until the request is done.
+            while (mRequestPending) {
+                try {
+                    wait();
+                } catch (InterruptedException ex) {
+                    // ignore.
+                }
+            }
+            Uri uri = mUri;
+            mUri = null;
+            return uri;
+        }
+
+        // Runs in namer thread
+        @Override
+        public synchronized void run() {
+            while (true) {
+                if (mStop) break;
+                if (!mRequestPending) {
+                    try {
+                        wait();
+                    } catch (InterruptedException ex) {
+                        // ignore.
+                    }
+                    continue;
+                }
+                cleanOldUri();
+                generateUri();
+                mRequestPending = false;
+                notifyAll();
+            }
+            cleanOldUri();
+        }
+
+        // Runs in main thread
+        public synchronized void finish() {
+            mStop = true;
+            notifyAll();
+        }
+
+        // Runs in namer thread
+        private void generateUri() {
+            Uri videoTable = Uri.parse("content://media/external/video/media");
+            mUri = mResolver.insert(videoTable, mValues);
+        }
+
+        // Runs in namer thread
+        private void cleanOldUri() {
+            if (mUri == null) return;
+            mResolver.delete(mUri, null, null);
+            mUri = null;
+        }
+    }
+
+    private class SurfaceViewCallback implements SurfaceHolder.Callback {
+        public SurfaceViewCallback() {}
+
+        @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");
+            mSurfaceViewReady = true;
+            if (mPaused) return;
+            if (!ApiHelper.HAS_SURFACE_TEXTURE) {
+                mActivity.mCameraDevice.setPreviewDisplayAsync(mPreviewSurfaceView.getHolder());
+                if (!mPreviewing) {
+                    startPreview();
+                }
+            }
+        }
+
+        @Override
+        public void surfaceDestroyed(SurfaceHolder holder) {
+            Log.v(TAG, "Surface destroyed");
+            mSurfaceViewReady = false;
+            if (mPaused) return;
+            if (!ApiHelper.HAS_SURFACE_TEXTURE) {
+                stopVideoRecording();
+                stopPreview();
+            }
+        }
+    }
+
+    @Override
+    public boolean updateStorageHintOnResume() {
+        return true;
+    }
+
+    // required by OnPreferenceChangedListener
+    @Override
+    public void onCameraPickerClicked(int cameraId) {
+        if (mPaused || mPendingSwitchCameraId != -1) return;
+
+        mPendingSwitchCameraId = cameraId;
+        if (ApiHelper.HAS_SURFACE_TEXTURE) {
+            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.
+            ((CameraScreenNail) mActivity.mCameraScreenNail).copyTexture();
+            // Disable all camera controls.
+            mSwitchingCamera = true;
+        } else {
+            switchCamera();
+        }
+    }
+
+    @Override
+    public boolean needsSwitcher() {
+        return !mIsVideoCaptureIntent;
+    }
+
+    @Override
+    public void onPieOpened(int centerX, int centerY) {
+        mActivity.cancelActivityTouchHandling();
+        mActivity.setSwipingEnabled(false);
+    }
+
+    @Override
+    public void onPieClosed() {
+        mActivity.setSwipingEnabled(true);
+    }
+
+    public void showPopup(AbstractSettingPopup popup) {
+        mActivity.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) {
+        if (fullScreen) {
+            mActivity.showUI();
+            mBlocker.setVisibility(View.VISIBLE);
+        }
+        setShowMenu(fullScreen);
+        if (mPopup != null) {
+            ((FrameLayout) mRootView).removeView(mPopup);
+            mPopup = null;
+        }
+        mVideoControl.popupDismissed(topLevelPopupOnly);
+    }
+
+    @Override
+    public void onShowSwitcherPopup() {
+        if (mPieRenderer.showsItems()) {
+            mPieRenderer.hide();
+        }
+    }
+}
diff --git a/src/com/android/camera/drawable/TextDrawable.java b/src/com/android/camera/drawable/TextDrawable.java
new file mode 100644
index 0000000..2e86364
--- /dev/null
+++ b/src/com/android/camera/drawable/TextDrawable.java
@@ -0,0 +1,84 @@
+/*
+ * 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.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;
+
+    public TextDrawable(Resources res, CharSequence text) {
+        mText = text;
+        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mPaint.setColor(DEFAULT_COLOR);
+        mPaint.setTextAlign(Align.CENTER);
+        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);
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        Rect bounds = getBounds();
+        canvas.drawText(mText, 0, mText.length(),
+                bounds.centerX(), bounds.centerY(), mPaint);
+    }
+
+    @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 0000000..49df77b
--- /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.camera.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/CameraSwitcher.java b/src/com/android/camera/ui/CameraSwitcher.java
new file mode 100644
index 0000000..7b9fb64
--- /dev/null
+++ b/src/com/android/camera/ui/CameraSwitcher.java
@@ -0,0 +1,293 @@
+/*
+ * 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.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.LinearLayout;
+
+import com.android.camera.R;
+import com.android.gallery3d.common.ApiHelper;
+
+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 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);
+    }
+
+    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)) {
+            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;
+        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) {
+                    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_pan:
+                    item.setContentDescription(getContext().getResources().getString(
+                            R.string.accessibility_switch_to_panorama));
+                    break;
+                case R.drawable.ic_switch_photosphere:
+                    item.setContentDescription(getContext().getResources().getString(
+                            R.string.accessibility_switch_to_new_panorama));
+                    break;
+                default:
+                    break;
+            }
+            content.addView(item, new LinearLayout.LayoutParams(mItemSize, mItemSize));
+        }
+    }
+
+    public boolean showsPopup() {
+        return mShowingPopup;
+    }
+
+    public boolean isInsidePopup(MotionEvent evt) {
+        if (!showsPopup()) return false;
+        return evt.getX() >= mPopup.getLeft()
+                && evt.getX() < mPopup.getRight()
+                && evt.getY() >= mPopup.getTop()
+                && evt.getY() < mPopup.getBottom();
+    }
+
+    private void hidePopup() {
+        mShowingPopup = false;
+        setVisibility(View.VISIBLE);
+        if (mPopup != null && !animateHidePopup()) {
+            mPopup.setVisibility(View.INVISIBLE);
+        }
+        mParent.setOnTouchListener(null);
+    }
+
+    private void showSwitcher() {
+        mShowingPopup = true;
+        if (mPopup == null) {
+            initPopup();
+        }
+        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 updateInitialTranslations() {
+        if (getResources().getConfiguration().orientation
+                == Configuration.ORIENTATION_PORTRAIT) {
+            mTranslationX = -getWidth() / 2;
+            mTranslationY = getHeight();
+        } else {
+            mTranslationX = getWidth();
+            mTranslationY = getHeight() / 2;
+        }
+    }
+    private void popupAnimationSetup() {
+        if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) {
+            return;
+        }
+        updateInitialTranslations();
+        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.setVisibility(View.INVISIBLE);
+                    }
+                }
+            };
+        }
+        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);
+                    }
+                }
+            };
+        }
+        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 0000000..4e77504
--- /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 0000000..ade25c3
--- /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.camera.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/EffectSettingPopup.java b/src/com/android/camera/ui/EffectSettingPopup.java
new file mode 100644
index 0000000..628d815
--- /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.camera.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 0000000..13cf58f
--- /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 0000000..9e6f982
--- /dev/null
+++ b/src/com/android/camera/ui/FaceView.java
@@ -0,0 +1,217 @@
+/*
+ * 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.CameraActivity;
+import com.android.camera.CameraScreenNail;
+import com.android.camera.R;
+import com.android.camera.Util;
+import com.android.gallery3d.common.ApiHelper;
+
+@TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+public class FaceView extends View implements FocusIndicator, Rotatable {
+    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 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));
+    }
+
+    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)) {
+            final CameraScreenNail sn = ((CameraActivity) getContext()).getCameraScreenNail();
+            int rw = sn.getUncroppedRenderWidth();
+            int rh = sn.getUncroppedRenderHeight();
+            // 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/FocusIndicator.java b/src/com/android/camera/ui/FocusIndicator.java
new file mode 100644
index 0000000..e060570
--- /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 0000000..5d9cc38
--- /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.camera.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 0000000..4f88f27
--- /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.camera.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 0000000..2fe8934
--- /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.camera.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 0000000..ef4eb6a
--- /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 0000000..6261d34
--- /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 0000000..6e118fc
--- /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 0000000..c0411c9
--- /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.camera.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 0000000..ab1baba
--- /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.camera.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 0000000..566f5c7
--- /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 0000000..417e219
--- /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 0000000..677e5ac
--- /dev/null
+++ b/src/com/android/camera/ui/PieItem.java
@@ -0,0 +1,203 @@
+/*
+ * 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.BitmapDrawable;
+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 float mCenter;
+    private float start;
+    private float sweep;
+    private float animate;
+    private int inner;
+    private int outer;
+    private boolean mSelected;
+    private boolean mEnabled;
+    private List<PieItem> mItems;
+    private Path mPath;
+    private OnClickListener mOnClickListener;
+    private float mAlpha;
+
+    // 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;
+        setAlpha(1f);
+        mEnabled = true;
+        setAnimationAngle(getAnimationAngle());
+        start = -1;
+        mCenter = -1;
+    }
+
+    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 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 setAnimationAngle(float a) {
+        animate = a;
+    }
+
+    public float getAnimationAngle() {
+        return animate;
+    }
+
+    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 setGeometry(float st, float sw, int inside, int outside) {
+        start = st;
+        sweep = sw;
+        inner = inside;
+        outer = outside;
+    }
+
+    public void setFixedSlice(float center, float sweep) {
+        mCenter = center;
+        this.sweep = sweep;
+    }
+
+    public float getCenter() {
+        return mCenter;
+    }
+
+    public float getStart() {
+        return start;
+    }
+
+    public float getStartAngle() {
+        return start + animate;
+    }
+
+    public float getSweep() {
+        return sweep;
+    }
+
+    public int getInnerRadius() {
+        return inner;
+    }
+
+    public int getOuterRadius() {
+        return outer;
+    }
+
+    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/PieRenderer.java b/src/com/android/camera/ui/PieRenderer.java
new file mode 100644
index 0000000..b592508
--- /dev/null
+++ b/src/com/android/camera/ui/PieRenderer.java
@@ -0,0 +1,825 @@
+/*
+ * 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.AnimatorListenerAdapter;
+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.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.Transformation;
+
+import com.android.camera.R;
+import com.android.gallery3d.common.ApiHelper;
+
+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 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;
+
+    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 int MSG_OPEN = 0;
+    private static final int MSG_CLOSE = 1;
+    private static final float PIE_SWEEP = (float)(Math.PI * 2 / 3);
+    // geometry
+    private Point mCenter;
+    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> mItems;
+
+    private PieItem mOpenItem;
+
+    private Paint mSelectedPaint;
+    private Paint mSubPaint;
+
+    // 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 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 LinearAnimation mXFade;
+    private LinearAnimation mFadeIn;
+    private volatile boolean mFocusCancelled;
+
+    private Handler mHandler = new Handler() {
+        public void handleMessage(Message msg) {
+            switch(msg.what) {
+            case MSG_OPEN:
+                if (mListener != null) {
+                    mListener.onPieOpened(mCenter.x, mCenter.y);
+                }
+                break;
+            case MSG_CLOSE:
+                if (mListener != null) {
+                    mListener.onPieClosed();
+                }
+                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);
+        mItems = new ArrayList<PieItem>();
+        Resources res = ctx.getResources();
+        mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start);
+        mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset);
+        mRadiusInc =  (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment);
+        mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset);
+        mCenter = new Point(0,0);
+        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();
+    }
+
+    public boolean showsItems() {
+        return mTapMode;
+    }
+
+    public void addItem(PieItem item) {
+        // add the item to the pie itself
+        mItems.add(item);
+    }
+
+    public void removeItem(PieItem item) {
+        mItems.remove(item);
+    }
+
+    public void clearItems() {
+        mItems.clear();
+    }
+
+    public void showInCenter() {
+        if ((mState == STATE_PIE) && isVisible()) {
+            mTapMode = false;
+            show(false);
+        } else {
+            if (mState != STATE_IDLE) {
+                cancelFocus();
+            }
+            mState = STATE_PIE;
+            setCenter(mCenterX, mCenterY);
+            mTapMode = true;
+            show(true);
+        }
+    }
+
+    public void hide() {
+        show(false);
+    }
+
+    /**
+     * guaranteed has center set
+     * @param show
+     */
+    private void show(boolean show) {
+        if (show) {
+            mState = STATE_PIE;
+            // ensure clean state
+            mCurrentItem = null;
+            mOpenItem = null;
+            for (PieItem item : mItems) {
+                item.setSelected(false);
+            }
+            layoutPie();
+            fadeIn();
+        } else {
+            mState = STATE_IDLE;
+            mTapMode = false;
+            if (mXFade != null) {
+                mXFade.cancel();
+            }
+        }
+        setVisible(show);
+        mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE);
+    }
+
+    private void fadeIn() {
+        mFadeIn = new LinearAnimation(0, 1);
+        mFadeIn.setDuration(PIE_FADE_IN_DURATION);
+        mFadeIn.setAnimationListener(new AnimationListener() {
+            @Override
+            public void onAnimationStart(Animation animation) {
+            }
+
+            @Override
+            public void onAnimationEnd(Animation animation) {
+                mFadeIn = null;
+            }
+
+            @Override
+            public void onAnimationRepeat(Animation animation) {
+            }
+        });
+        mFadeIn.startNow();
+        mOverlay.startAnimation(mFadeIn);
+    }
+
+    public void setCenter(int x, int y) {
+        mCenter.x = x;
+        mCenter.y = y;
+        // when using the pie menu, align the focus ring
+        alignFocus(x, y);
+    }
+
+    private void layoutPie() {
+        int rgap = 2;
+        int inner = mRadius + rgap;
+        int outer = mRadius + mRadiusInc - rgap;
+        int gap = 1;
+        layoutItems(mItems, (float) (Math.PI / 2), inner, outer, gap);
+    }
+
+    private void layoutItems(List<PieItem> items, float centerAngle, int inner,
+            int outer, int gap) {
+        float emptyangle = PIE_SWEEP / 16;
+        float sweep = (float) (PIE_SWEEP - 2 * emptyangle) / items.size();
+        float angle = centerAngle - PIE_SWEEP / 2 + emptyangle + sweep / 2;
+        // check if we have custom geometry
+        // first item we find triggers custom sweep for all
+        // this allows us to re-use the path
+        for (PieItem item : items) {
+            if (item.getCenter() >= 0) {
+                sweep = item.getSweep();
+                break;
+            }
+        }
+        Path path = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap,
+                outer, inner, mCenter);
+        for (PieItem item : items) {
+            // shared between items
+            item.setPath(path);
+            if (item.getCenter() >= 0) {
+                angle = item.getCenter();
+            }
+            int w = item.getIntrinsicWidth();
+            int h = item.getIntrinsicHeight();
+            // move views to outer border
+            int r = inner + (outer - inner) * 2 / 3;
+            int x = (int) (r * Math.cos(angle));
+            int y = mCenter.y - (int) (r * Math.sin(angle)) - h / 2;
+            x = mCenter.x + x - w / 2;
+            item.setBounds(x, y, x + w, y + h);
+            float itemstart = angle - sweep / 2;
+            item.setGeometry(itemstart, sweep, inner, outer);
+            if (item.hasItems()) {
+                layoutItems(item.getItems(), angle, inner,
+                        outer + mRadiusInc / 2, gap);
+            }
+            angle += sweep;
+        }
+    }
+
+    private Path makeSlice(float start, float end, int outer, int inner, Point center) {
+        RectF bb =
+                new RectF(center.x - outer, center.y - outer, center.x + outer,
+                        center.y + outer);
+        RectF bbi =
+                new RectF(center.x - inner, center.y - inner, center.x + inner,
+                        center.y + inner);
+        Path path = new Path();
+        path.arcTo(bb, start, end - start, true);
+        path.arcTo(bbi, end, start - end);
+        path.close();
+        return path;
+    }
+
+    /**
+     * 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() {
+        if (ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) {
+            mOverlay.animate().alpha(0).setListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    deselect();
+                    show(false);
+                    mOverlay.setAlpha(1);
+                    super.onAnimationEnd(animation);
+                }
+            }).setDuration(PIE_SELECT_FADE_DURATION);
+        } else {
+            deselect();
+            show(false);
+        }
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        float alpha = 1;
+        if (mXFade != null) {
+            alpha = mXFade.getValue();
+        } else if (mFadeIn != null) {
+            alpha = mFadeIn.getValue();
+        }
+        int state = canvas.save();
+        if (mFadeIn != null) {
+            float sf = 0.9f + alpha * 0.1f;
+            canvas.scale(sf, sf, mCenter.x, mCenter.y);
+        }
+        drawFocus(canvas);
+        if (mState == STATE_FINISHING) {
+            canvas.restoreToCount(state);
+            return;
+        }
+        if ((mOpenItem == null) || (mXFade != null)) {
+            // draw base menu
+            for (PieItem item : mItems) {
+                drawItem(canvas, item, alpha);
+            }
+        }
+        if (mOpenItem != null) {
+            for (PieItem inner : mOpenItem.getItems()) {
+                drawItem(canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1);
+            }
+        }
+        canvas.restoreToCount(state);
+    }
+
+    private void drawItem(Canvas canvas, PieItem item, float alpha) {
+        if (mState == STATE_PIE) {
+            if (item.getPath() != null) {
+                if (item.isSelected()) {
+                    Paint p = mSelectedPaint;
+                    int state = canvas.save();
+                    float r = getDegrees(item.getStartAngle());
+                    canvas.rotate(r, mCenter.x, mCenter.y);
+                    canvas.drawPath(item.getPath(), p);
+                    canvas.restoreToCount(state);
+                }
+                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();
+        PointF polar = getPolar(x, y, !(mTapMode));
+        if (MotionEvent.ACTION_DOWN == action) {
+            mDown.x = (int) evt.getX();
+            mDown.y = (int) evt.getY();
+            mOpening = false;
+            if (mTapMode) {
+                PieItem item = findItem(polar);
+                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(polar);
+                    if (item != null && mOpening) {
+                        mOpening = false;
+                        return true;
+                    }
+                }
+                if (item == null) {
+                    mTapMode = false;
+                    show(false);
+                } else if (!mOpening
+                        && !item.hasItems()) {
+                    item.performClick();
+                    startFadeOut();
+                    mTapMode = false;
+                }
+                return true;
+            }
+        } else if (MotionEvent.ACTION_CANCEL == action) {
+            if (isVisible() || mTapMode) {
+                show(false);
+            }
+            deselect();
+            return false;
+        } else if (MotionEvent.ACTION_MOVE == action) {
+            if (polar.y < mRadius) {
+                if (mOpenItem != null) {
+                    mOpenItem = null;
+                } else {
+                    deselect();
+                }
+                return false;
+            }
+            PieItem item = findItem(polar);
+            boolean moved = hasMoved(evt);
+            if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) {
+                // only select if we didn't just open or have moved past slop
+                mOpening = false;
+                if (moved) {
+                    // switch back to swipe mode
+                    mTapMode = false;
+                }
+                onEnter(item);
+            }
+        }
+        return false;
+    }
+
+    private boolean hasMoved(MotionEvent e) {
+        return mTouchSlopSquared < (e.getX() - mDown.x) * (e.getX() - mDown.x)
+                + (e.getY() - mDown.y) * (e.getY() - mDown.y);
+    }
+
+    /**
+     * 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;
+            if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) {
+                openCurrentItem();
+            }
+        } else {
+            mCurrentItem = null;
+        }
+    }
+
+    private void deselect() {
+        if (mCurrentItem != null) {
+            mCurrentItem.setSelected(false);
+        }
+        if (mOpenItem != null) {
+            mOpenItem = null;
+        }
+        mCurrentItem = null;
+    }
+
+    private void openCurrentItem() {
+        if ((mCurrentItem != null) && mCurrentItem.hasItems()) {
+            mCurrentItem.setSelected(false);
+            mOpenItem = mCurrentItem;
+            mOpening = true;
+            mXFade = new LinearAnimation(1, 0);
+            mXFade.setDuration(PIE_XFADE_DURATION);
+            mXFade.setAnimationListener(new AnimationListener() {
+                @Override
+                public void onAnimationStart(Animation animation) {
+                }
+
+                @Override
+                public void onAnimationEnd(Animation animation) {
+                    mXFade = null;
+                }
+
+                @Override
+                public void onAnimationRepeat(Animation animation) {
+                }
+            });
+            mXFade.startNow();
+            mOverlay.startAnimation(mXFade);
+        }
+    }
+
+    private PointF getPolar(float x, float y, boolean useOffset) {
+        PointF res = new PointF();
+        // get angle and radius from x/y
+        res.x = (float) Math.PI / 2;
+        x = x - mCenter.x;
+        y = mCenter.y - y;
+        res.y = (float) Math.sqrt(x * x + y * y);
+        if (x != 0) {
+            res.x = (float) Math.atan2(y,  x);
+            if (res.x < 0) {
+                res.x = (float) (2 * Math.PI + res.x);
+            }
+        }
+        res.y = res.y + (useOffset ? mTouchOffset : 0);
+        return res;
+    }
+
+    /**
+     * @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 = (mOpenItem != null) ? mOpenItem.getItems() : mItems;
+        for (PieItem item : items) {
+            if (inside(polar, item)) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    private boolean inside(PointF polar, PieItem item) {
+        return (item.getInnerRadius() < polar.y)
+                && (item.getStartAngle() < polar.x)
+                && (item.getStartAngle() + item.getSweep() > polar.x)
+                && (!mTapMode || (item.getOuterRadius() > polar.y));
+    }
+
+    @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());
+    }
+
+    @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;
+        setCircle(mFocusX, mFocusY);
+        if (isVisible() && mState == STATE_PIE) {
+            setCenter(mCenterX, mCenterY);
+            layoutPie();
+        }
+    }
+
+    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.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);
+        }
+    }
+
+
+    private class LinearAnimation extends Animation {
+        private float mFrom;
+        private float mTo;
+        private float mValue;
+
+        public LinearAnimation(float from, float to) {
+            setFillAfter(true);
+            setInterpolator(new LinearInterpolator());
+            mFrom = from;
+            mTo = to;
+        }
+
+        public float getValue() {
+            return mValue;
+        }
+
+        @Override
+        protected void applyTransformation(float interpolatedTime, Transformation t) {
+            mValue = (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 0000000..0dcf34f
--- /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 0000000..9a428e2
--- /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 0000000..ba25915
--- /dev/null
+++ b/src/com/android/camera/ui/RenderOverlay.java
@@ -0,0 +1,165 @@
+/*
+ * 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 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;
+
+    // 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 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) {
+        return false;
+    }
+
+    public boolean directDispatchTouch(MotionEvent m, Renderer target) {
+        mRenderView.setTouchTarget(target);
+        boolean res = super.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 onTouchEvent(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 0000000..6d428b8
--- /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/RotateImageView.java b/src/com/android/camera/ui/RotateImageView.java
new file mode 100644
index 0000000..05e1a7c
--- /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 0000000..86f5c81
--- /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 0000000..f73c033
--- /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.R;
+import com.android.camera.Util;
+
+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 0000000..5b1ab4c
--- /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.camera.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 0000000..b79663b
--- /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.camera.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/TimerSettingPopup.java b/src/com/android/camera/ui/TimerSettingPopup.java
new file mode 100644
index 0000000..06d7e4e
--- /dev/null
+++ b/src/com/android/camera/ui/TimerSettingPopup.java
@@ -0,0 +1,153 @@
+/*
+ * 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.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.ListPreference;
+import com.android.camera.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 TimerSettingPopup extends AbstractSettingPopup {
+    private static final String TAG = "TimerSettingPopup";
+    private NumberPicker mNumberSpinner;
+    private Switch mTimerSwitch;
+    private String[] mDurations;
+    private ListPreference 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 TimerSettingPopup(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void initialize(ListPreference preference) {
+        mPreference = preference;
+
+        // Set title.
+        mTitle.setText(mPreference.getTitle());
+
+        // Duration
+        CharSequence[] entries = mPreference.getEntryValues();
+        mDurations = new String[entries.length - 1];
+        Locale locale = getResources().getConfiguration().locale;
+        for (int i = 1; i < entries.length; i++)
+            mDurations[i-1] = 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);
+
+        mTimerSwitch = (Switch) findViewById(R.id.timer_setting_switch);
+        mHelpText = (TextView) findViewById(R.id.set_timer_help_text);
+        mConfirmButton = (Button) findViewById(R.id.timer_set_button);
+        mTimePicker = findViewById(R.id.time_duration_picker);
+
+        // Disable focus on the spinners to prevent keyboard from coming up
+        mNumberSpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS);
+
+        mTimerSwitch.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
+            mTimerSwitch.setChecked(false);
+            setTimeSelectionEnabled(false);
+        } else {
+            mTimerSwitch.setChecked(true);
+            setTimeSelectionEnabled(true);
+            mNumberSpinner.setValue(index - 1);
+        }
+    }
+
+    @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 (mTimerSwitch.isChecked()) {
+            int newId = 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 0000000..cd5b27f
--- /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 0000000..10c5e80
--- /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.camera.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();
+        }
+    }
+
+}