Merge "Combine code for bitmap decoding and add byte array decoding." into gb-ub-photos-carlsbad
diff --git a/src/com/android/camera/NewCameraActivity.java b/src/com/android/camera/NewCameraActivity.java
new file mode 100644
index 0000000..8ce5ce4
--- /dev/null
+++ b/src/com/android/camera/NewCameraActivity.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.provider.Settings;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.OrientationEventListener;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+
+import com.android.camera.data.CameraDataAdapter;
+import com.android.camera.ui.CameraSwitcher.CameraSwitchListener;
+import com.android.camera.ui.FilmStripView;
+import com.android.camera.ui.NewCameraRootView;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.util.LightCycleHelper;
+
+public class NewCameraActivity extends Activity
+    implements 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;
+    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 CameraDataAdapter mDataAdapter;
+    private int mCurrentModuleIndex;
+    private NewCameraModule mCurrentModule;
+    private View mRootView;
+    private FilmStripView mFilmStripView;
+    private int mResultCodeForTesting;
+    private Intent mResultDataForTesting;
+    private OnScreenHint mStorageHint;
+    private long mStorageSpace = Storage.LOW_STORAGE_THRESHOLD;
+    private PhotoModule mController;
+    private boolean mAutoRotateScreen;
+    private boolean mSecureCamera;
+    private int mLastRawOrientation;
+    private MyOrientationEventListener mOrientationListener;
+    private class MyOrientationEventListener
+        extends OrientationEventListener {
+        public MyOrientationEventListener(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onOrientationChanged(int orientation) {
+            // We keep the last known orientation. So if the user first orient
+            // the camera then point the camera to floor or sky, we still have
+            // the correct orientation.
+            if (orientation == ORIENTATION_UNKNOWN) return;
+            mLastRawOrientation = orientation;
+            mCurrentModule.onOrientationChanged(orientation);
+        }
+    }
+    private MediaSaveService mMediaSaveService;
+    private ServiceConnection mConnection = new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName className, IBinder b) {
+                mMediaSaveService = ((MediaSaveService.LocalBinder) b).getService();
+                mCurrentModule.onMediaSaveServiceConnected(mMediaSaveService);
+            }
+            @Override
+            public void onServiceDisconnected(ComponentName className) {
+                mMediaSaveService = null;
+            }};
+
+    public MediaSaveService getMediaSaveService() {
+        return mMediaSaveService;
+    }
+
+    private void bindMediaSaveService() {
+        Intent intent = new Intent(this, MediaSaveService.class);
+        startService(intent);  // start service before binding it so the
+                               // service won't be killed if we unbind it.
+        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+    }
+
+    private void unbindMediaSaveService() {
+        mMediaSaveService.setListener(null);
+        unbindService(mConnection);
+    }
+
+    @Override
+    public void onCreate(Bundle state) {
+        super.onCreate(state);
+        setContentView(R.layout.camera_filmstrip);
+        if (ApiHelper.HAS_ROTATION_ANIMATION) {
+            setRotationAnimation();
+        }
+        // Check if this is in the secure camera mode.
+        Intent intent = getIntent();
+        String action = intent.getAction();
+        if (INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE.equals(action)) {
+            mSecureCamera = true;
+            // Use a new album when this is started from the lock screen.
+        //TODO:    sSecureAlbumId++;
+        } else if (ACTION_IMAGE_CAPTURE_SECURE.equals(action)) {
+            mSecureCamera = true;
+        } else {
+            mSecureCamera = intent.getBooleanExtra(SECURE_CAMERA_EXTRA, false);
+        }
+        /*TODO: if (mSecureCamera) {
+            IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
+            registerReceiver(mScreenOffReceiver, filter);
+            if (sScreenOffReceiver == null) {
+                sScreenOffReceiver = new ScreenOffReceiver();
+                getApplicationContext().registerReceiver(sScreenOffReceiver, filter);
+            }
+        }*/
+        LayoutInflater inflater = getLayoutInflater();
+        View rootLayout = inflater.inflate(R.layout.camera, null, false);
+        mRootView = rootLayout.findViewById(R.id.camera_app_root);
+        mDataAdapter = new CameraDataAdapter(
+                new ColorDrawable(getResources().getColor(R.color.photo_placeholder)));
+        mFilmStripView = (FilmStripView) findViewById(R.id.filmstrip_view);
+        // Set up the camera preview first so the preview shows up ASAP.
+        mDataAdapter.setCameraPreviewInfo(rootLayout,
+                FilmStripView.ImageData.SIZE_FULL, FilmStripView.ImageData.SIZE_FULL);
+        mFilmStripView.setDataAdapter(mDataAdapter);
+        mFilmStripView.setListener(new FilmStripView.Listener() {
+            @Override
+            public void onDataPromoted(int dataID) {
+                mDataAdapter.removeData(dataID);
+            }
+
+            @Override
+            public void onDataDemoted(int dataID) {
+                mDataAdapter.removeData(dataID);
+            }
+
+        });
+        mCurrentModule = new NewPhotoModule();
+        mCurrentModule.init(this, mRootView);
+        mOrientationListener = new MyOrientationEventListener(this);
+        bindMediaSaveService();
+    }
+
+    private void setRotationAnimation() {
+        int rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE;
+        rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE;
+        Window win = getWindow();
+        WindowManager.LayoutParams winParams = win.getAttributes();
+        winParams.rotationAnimation = rotationAnimation;
+        win.setAttributes(winParams);
+    }
+
+    @Override
+    public void onUserInteraction() {
+        super.onUserInteraction();
+        mCurrentModule.onUserInteraction();
+    }
+
+    @Override
+    public void onPause() {
+        mOrientationListener.disable();
+        mCurrentModule.onPauseBeforeSuper();
+        super.onPause();
+        mCurrentModule.onPauseAfterSuper();
+    }
+
+    @Override
+    public void onResume() {
+        if (Settings.System.getInt(getContentResolver(),
+                Settings.System.ACCELEROMETER_ROTATION, 0) == 0) {// auto-rotate off
+            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+            mAutoRotateScreen = false;
+        } else {
+            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
+            mAutoRotateScreen = true;
+        }
+        mOrientationListener.enable();
+        mCurrentModule.onResumeBeforeSuper();
+        super.onResume();
+        mCurrentModule.onResumeAfterSuper();
+
+        // The loading is done in background and will update the filmstrip later.
+        mDataAdapter.requestLoad(getContentResolver());
+    }
+
+    @Override
+    public void onDestroy() {
+        unbindMediaSaveService();
+        super.onDestroy();
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration config) {
+        super.onConfigurationChanged(config);
+        mCurrentModule.onConfigurationChanged(config);
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent m) {
+        //if (mFilmStripView.isInCameraFullscreen()) {
+        //    return mCurrentModule.dispatchTouchEvent(m);
+        //}
+        return mFilmStripView.dispatchTouchEvent(m);
+    }
+    public boolean isAutoRotateScreen() {
+        return mAutoRotateScreen;
+    }
+
+    protected void updateStorageSpace() {
+        mStorageSpace = Storage.getAvailableSpace();
+    }
+
+    protected long getStorageSpace() {
+        return mStorageSpace;
+    }
+
+    protected void updateStorageSpaceAndHint() {
+        updateStorageSpace();
+        updateStorageHint(mStorageSpace);
+    }
+
+    protected void updateStorageHint() {
+        updateStorageHint(mStorageSpace);
+    }
+
+    protected boolean updateStorageHintOnResume() {
+        return true;
+    }
+
+    protected void updateStorageHint(long storageSpace) {
+        String message = null;
+        if (storageSpace == Storage.UNAVAILABLE) {
+            message = getString(R.string.no_storage);
+        } else if (storageSpace == Storage.PREPARING) {
+            message = getString(R.string.preparing_sd);
+        } else if (storageSpace == Storage.UNKNOWN_SIZE) {
+            message = getString(R.string.access_sd_fail);
+        } else if (storageSpace <= Storage.LOW_STORAGE_THRESHOLD) {
+            message = getString(R.string.spaceIsLow_content);
+        }
+
+        if (message != null) {
+            if (mStorageHint == null) {
+                mStorageHint = OnScreenHint.makeText(this, message);
+            } else {
+                mStorageHint.setText(message);
+            }
+            mStorageHint.show();
+        } else if (mStorageHint != null) {
+            mStorageHint.cancel();
+            mStorageHint = null;
+        }
+    }
+
+    protected void setResultEx(int resultCode) {
+        mResultCodeForTesting = resultCode;
+        setResult(resultCode);
+    }
+
+    protected void setResultEx(int resultCode, Intent data) {
+        mResultCodeForTesting = resultCode;
+        mResultDataForTesting = data;
+        setResult(resultCode, data);
+    }
+
+    public int getResultCode() {
+        return mResultCodeForTesting;
+    }
+
+    public Intent getResultData() {
+        return mResultDataForTesting;
+    }
+
+    public boolean isSecureCamera() {
+        return mSecureCamera;
+    }
+
+    @Override
+    public void onCameraSelected(int i) {
+        if (mCurrentModuleIndex == i) return;
+
+        CameraHolder.instance().keep();
+        closeModule(mCurrentModule);
+        mCurrentModuleIndex = i;
+        switch (i) {
+            case VIDEO_MODULE_INDEX:
+                mCurrentModule = new NewVideoModule();
+                break;
+            case PHOTO_MODULE_INDEX:
+                mCurrentModule = new NewPhotoModule();
+                break;
+            /* TODO:
+            case LIGHTCYCLE_MODULE_INDEX:
+                mCurrentModule = LightCycleHelper.createPanoramaModule();
+                break; */
+           default:
+               break;
+        }
+
+        openModule(mCurrentModule);
+        mCurrentModule.onOrientationChanged(mLastRawOrientation);
+        if (mMediaSaveService != null) {
+            mCurrentModule.onMediaSaveServiceConnected(mMediaSaveService);
+        }
+    }
+
+    private void openModule(NewCameraModule module) {
+        module.init(this, mRootView);
+        module.onResumeBeforeSuper();
+        module.onResumeAfterSuper();
+    }
+
+    private void closeModule(NewCameraModule module) {
+        module.onPauseBeforeSuper();
+        module.onPauseAfterSuper();
+        ((ViewGroup) mRootView).removeAllViews();
+    }
+
+    @Override
+    public void onShowSwitcherPopup() {
+    }
+}
diff --git a/src/com/android/camera/NewCameraModule.java b/src/com/android/camera/NewCameraModule.java
new file mode 100644
index 0000000..061cc6c
--- /dev/null
+++ b/src/com/android/camera/NewCameraModule.java
@@ -0,0 +1,76 @@
+/*
+ * 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.Intent;
+import android.content.res.Configuration;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+public interface NewCameraModule {
+
+    public void init(NewCameraActivity activity, View frame);
+
+    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 needsSwitcher();
+
+    public boolean needsPieMenu();
+
+    public void onOrientationChanged(int orientation);
+
+    public void onShowSwitcherPopup();
+
+    public void onMediaSaveServiceConnected(MediaSaveService s);
+}
diff --git a/src/com/android/camera/NewPhotoMenu.java b/src/com/android/camera/NewPhotoMenu.java
new file mode 100644
index 0000000..f324033
--- /dev/null
+++ b/src/com/android/camera/NewPhotoMenu.java
@@ -0,0 +1,208 @@
+/*
+ * 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 java.util.Locale;
+
+import android.content.res.Resources;
+import android.hardware.Camera.Parameters;
+
+import com.android.camera.ui.AbstractSettingPopup;
+import com.android.camera.ui.CountdownTimerPopup;
+import com.android.camera.ui.ListPrefSettingPopup;
+import com.android.camera.ui.PieItem;
+import com.android.camera.ui.PieItem.OnClickListener;
+import com.android.camera.ui.PieRenderer;
+import com.android.gallery3d.R;
+
+public class NewPhotoMenu extends PieController
+        implements CountdownTimerPopup.Listener,
+        ListPrefSettingPopup.Listener {
+    private static String TAG = "CAM_photomenu";
+
+    private static final int POS_HDR = 0;
+    private static final int POS_EXP = 1;
+    private static final int POS_MORE = 2;
+    private static final int POS_FLASH = 3;
+    private static final int POS_SWITCH = 4;
+    private static final int POS_WB = 1;
+    private static final int POS_SET = 2;
+
+    private final String mSettingOff;
+
+    private NewPhotoUI mUI;
+    private AbstractSettingPopup mPopup;
+    private NewCameraActivity mActivity;
+
+    public NewPhotoMenu(NewCameraActivity activity, NewPhotoUI ui, PieRenderer pie) {
+        super(activity, pie);
+        mUI = ui;
+        mSettingOff = activity.getString(R.string.setting_off_value);
+        mActivity = activity;
+    }
+
+    public void initialize(PreferenceGroup group) {
+        super.initialize(group);
+        mPopup = null;
+        PieItem item = null;
+        final Resources res = mActivity.getResources();
+        Locale locale = res.getConfiguration().locale;
+        // the order is from left to right in the menu
+
+        // hdr
+        if (group.findPreference(CameraSettings.KEY_CAMERA_HDR) != null) {
+            item = makeSwitchItem(CameraSettings.KEY_CAMERA_HDR, true);
+            mRenderer.addItem(item);
+        }
+        // exposure compensation
+        if (group.findPreference(CameraSettings.KEY_EXPOSURE) != null) {
+            item = makeItem(CameraSettings.KEY_EXPOSURE);
+            item.setLabel(res.getString(R.string.pref_exposure_label));
+            mRenderer.addItem(item);
+        }
+        // more settings
+        PieItem more = makeItem(R.drawable.ic_settings_holo_light);
+        more.setLabel(res.getString(R.string.camera_menu_more_label));
+        mRenderer.addItem(more);
+        // flash
+        if (group.findPreference(CameraSettings.KEY_FLASH_MODE) != null) {
+            item = makeItem(CameraSettings.KEY_FLASH_MODE);
+            item.setLabel(res.getString(R.string.pref_camera_flashmode_label));
+            mRenderer.addItem(item);
+        }
+        // camera switcher
+        if (group.findPreference(CameraSettings.KEY_CAMERA_ID) != null) {
+            item = makeSwitchItem(CameraSettings.KEY_CAMERA_ID, false);
+            final PieItem fitem = item;
+            item.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(PieItem item) {
+                    // Find the index of next camera.
+                    ListPreference pref = mPreferenceGroup
+                            .findPreference(CameraSettings.KEY_CAMERA_ID);
+                    if (pref != null) {
+                        int index = pref.findIndexOfValue(pref.getValue());
+                        CharSequence[] values = pref.getEntryValues();
+                        index = (index + 1) % values.length;
+                        pref.setValueIndex(index);
+                        mListener.onCameraPickerClicked(index);
+                    }
+                    updateItem(fitem, CameraSettings.KEY_CAMERA_ID);
+                }
+            });
+            mRenderer.addItem(item);
+        }
+        // location
+        if (group.findPreference(CameraSettings.KEY_RECORD_LOCATION) != null) {
+            item = makeSwitchItem(CameraSettings.KEY_RECORD_LOCATION, true);
+            more.addItem(item);
+            if (mActivity.isSecureCamera()) {
+                // Prevent location preference from getting changed in secure camera mode
+                item.setEnabled(false);
+            }
+        }
+        // countdown timer
+        final ListPreference ctpref = group.findPreference(CameraSettings.KEY_TIMER);
+        final ListPreference beeppref = group.findPreference(CameraSettings.KEY_TIMER_SOUND_EFFECTS);
+        item = makeItem(R.drawable.ic_timer);
+        item.setLabel(res.getString(R.string.pref_camera_timer_title).toUpperCase(locale));
+        item.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(PieItem item) {
+                CountdownTimerPopup timerPopup = (CountdownTimerPopup) mActivity.getLayoutInflater().inflate(
+                        R.layout.countdown_setting_popup, null, false);
+                timerPopup.initialize(ctpref, beeppref);
+                timerPopup.setSettingChangedListener(NewPhotoMenu.this);
+                mUI.dismissPopup();
+                mPopup = timerPopup;
+                mUI.showPopup(mPopup);
+            }
+        });
+        more.addItem(item);
+        // image size
+        item = makeItem(R.drawable.ic_imagesize);
+        final ListPreference sizePref = group.findPreference(CameraSettings.KEY_PICTURE_SIZE);
+        item.setLabel(res.getString(R.string.pref_camera_picturesize_title).toUpperCase(locale));
+        item.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(PieItem item) {
+                ListPrefSettingPopup popup = (ListPrefSettingPopup) mActivity.getLayoutInflater().inflate(
+                        R.layout.list_pref_setting_popup, null, false);
+                popup.initialize(sizePref);
+                popup.setSettingChangedListener(NewPhotoMenu.this);
+                mUI.dismissPopup();
+                mPopup = popup;
+                mUI.showPopup(mPopup);
+            }
+        });
+        more.addItem(item);
+        // white balance
+        if (group.findPreference(CameraSettings.KEY_WHITE_BALANCE) != null) {
+            item = makeItem(CameraSettings.KEY_WHITE_BALANCE);
+            item.setLabel(res.getString(R.string.pref_camera_whitebalance_label));
+            more.addItem(item);
+        }
+        // scene mode
+        if (group.findPreference(CameraSettings.KEY_SCENE_MODE) != null) {
+            IconListPreference pref = (IconListPreference) group.findPreference(
+                    CameraSettings.KEY_SCENE_MODE);
+            pref.setUseSingleIcon(true);
+            item = makeItem(CameraSettings.KEY_SCENE_MODE);
+            more.addItem(item);
+        }
+    }
+
+    @Override
+    // Hit when an item in a popup gets selected
+    public void onListPrefChanged(ListPreference pref) {
+        if (mPopup != null) {
+            mUI.dismissPopup();
+        }
+        onSettingChanged(pref);
+    }
+
+    public void popupDismissed() {
+        if (mPopup != null) {
+            mPopup = null;
+        }
+    }
+
+    // Return true if the preference has the specified key but not the value.
+    private static boolean notSame(ListPreference pref, String key, String value) {
+        return (key.equals(pref.getKey()) && !value.equals(pref.getValue()));
+    }
+
+    private void setPreference(String key, String value) {
+        ListPreference pref = mPreferenceGroup.findPreference(key);
+        if (pref != null && !value.equals(pref.getValue())) {
+            pref.setValue(value);
+            reloadPreferences();
+        }
+    }
+
+    @Override
+    public void onSettingChanged(ListPreference pref) {
+        // Reset the scene mode if HDR is set to on. Reset HDR if scene mode is
+        // set to non-auto.
+        if (notSame(pref, CameraSettings.KEY_CAMERA_HDR, mSettingOff)) {
+            setPreference(CameraSettings.KEY_SCENE_MODE, Parameters.SCENE_MODE_AUTO);
+        } else if (notSame(pref, CameraSettings.KEY_SCENE_MODE, Parameters.SCENE_MODE_AUTO)) {
+            setPreference(CameraSettings.KEY_CAMERA_HDR, mSettingOff);
+        }
+        super.onSettingChanged(pref);
+    }
+}
diff --git a/src/com/android/camera/NewPhotoModule.java b/src/com/android/camera/NewPhotoModule.java
new file mode 100644
index 0000000..e045d5d
--- /dev/null
+++ b/src/com/android/camera/NewPhotoModule.java
@@ -0,0 +1,2005 @@
+/*
+ * 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.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+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.Parameters;
+import android.hardware.Camera.PictureCallback;
+import android.hardware.Camera.Size;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.location.Location;
+import android.media.CameraProfile;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.SystemClock;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.OrientationEventListener;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.android.camera.CameraManager.CameraProxy;
+import com.android.camera.ui.CountDownView.OnCountDownFinishedListener;
+import com.android.camera.ui.PopupManager;
+import com.android.camera.ui.RotateTextToast;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.exif.ExifTag;
+import com.android.gallery3d.exif.Rational;
+import com.android.gallery3d.filtershow.crop.CropExtras;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.util.UsageStatistics;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Formatter;
+import java.util.List;
+
+public class NewPhotoModule
+    implements NewCameraModule,
+    PhotoController,
+    FocusOverlayManager.Listener,
+    CameraPreference.OnPreferenceChangedListener,
+    ShutterButton.OnShutterButtonListener,
+    MediaSaveService.Listener,
+    OnCountDownFinishedListener,
+    SensorEventListener {
+
+    private static final String TAG = "CAM_PhotoModule";
+
+    // We number the request code from 1000 to avoid collision with Gallery.
+    private static final int REQUEST_CROP = 1000;
+
+    private static final int SETUP_PREVIEW = 1;
+    private static final int FIRST_TIME_INIT = 2;
+    private static final int CLEAR_SCREEN_DELAY = 3;
+    private static final int SET_CAMERA_PARAMETERS_WHEN_IDLE = 4;
+    private static final int CHECK_DISPLAY_ROTATION = 5;
+    private static final int SHOW_TAP_TO_FOCUS_TOAST = 6;
+    private static final int SWITCH_CAMERA = 7;
+    private static final int SWITCH_CAMERA_START_ANIMATION = 8;
+    private static final int CAMERA_OPEN_DONE = 9;
+    private static final int START_PREVIEW_DONE = 10;
+    private static final int OPEN_CAMERA_FAIL = 11;
+    private static final int CAMERA_DISABLED = 12;
+
+    // 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 NewCameraActivity mActivity;
+    private CameraProxy mCameraDevice;
+    private int mCameraId;
+    private Parameters mParameters;
+    private boolean mPaused;
+
+    private NewPhotoUI mUI;
+
+    // -1 means camera is not switching.
+    protected int mPendingSwitchCameraId = -1;
+    private boolean mOpenCameraFail;
+    private boolean mCameraDisabled;
+
+    // When setCameraParametersWhenIdle() is called, we accumulate the subsets
+    // needed to be updated in mUpdateSet.
+    private int mUpdateSet;
+
+    private static final int SCREEN_DELAY = 2 * 60 * 1000;
+
+    private int mZoomValue;  // The current zoom value.
+
+    private Parameters mInitialParams;
+    private boolean mFocusAreaSupported;
+    private boolean mMeteringAreaSupported;
+    private boolean mAeLockSupported;
+    private boolean mAwbLockSupported;
+    private boolean mContinousFocusSupported;
+
+    // The degrees of the device rotated clockwise from its natural orientation.
+    private int mOrientation = OrientationEventListener.ORIENTATION_UNKNOWN;
+    private ComboPreferences mPreferences;
+
+    private static final String sTempCropFilename = "crop-temp";
+
+    private ContentProviderClient mMediaProviderClient;
+    private boolean mFaceDetectionStarted = false;
+
+    // mCropValue and mSaveUri are used only if isImageCaptureIntent() is true.
+    private String mCropValue;
+    private Uri mSaveUri;
+
+    // We use a queue to generated names of the images to be used later
+    // when the image is ready to be saved.
+    private NamedImages mNamedImages;
+
+    private Runnable mDoSnapRunnable = new Runnable() {
+        @Override
+        public void run() {
+            onShutterButtonClick();
+        }
+    };
+
+    private final StringBuilder mBuilder = new StringBuilder();
+    private final Formatter mFormatter = new Formatter(mBuilder);
+    private final Object[] mFormatterArgs = new Object[1];
+
+    /**
+     * An unpublished intent flag requesting to return as soon as capturing
+     * is completed.
+     *
+     * TODO: consider publishing by moving into MediaStore.
+     */
+    private static final String EXTRA_QUICK_CAPTURE =
+            "android.intent.extra.quickCapture";
+
+    // The display rotation in degrees. This is only valid when mCameraState is
+    // not PREVIEW_STOPPED.
+    private int mDisplayRotation;
+    // The value for android.hardware.Camera.setDisplayOrientation.
+    private int mCameraDisplayOrientation;
+    // The value for UI components like indicators.
+    private int mDisplayOrientation;
+    // The value for android.hardware.Camera.Parameters.setRotation.
+    private int mJpegRotation;
+    private boolean mFirstTimeInitialized;
+    private boolean mIsImageCaptureIntent;
+
+    private int mCameraState = PREVIEW_STOPPED;
+    private boolean mSnapshotOnIdle = false;
+
+    private ContentResolver mContentResolver;
+
+    private LocationManager mLocationManager;
+
+    private final 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 String mSceneMode;
+
+    private final Handler mHandler = new MainHandler();
+    private PreferenceGroup mPreferenceGroup;
+
+    private boolean mQuickCapture;
+    private SensorManager mSensorManager;
+    private float[] mGData = new float[3];
+    private float[] mMData = new float[3];
+    private float[] mR = new float[16];
+    private int mHeading = -1;
+
+    CameraStartUpThread mCameraStartUpThread;
+    ConditionVariable mStartPreviewPrerequisiteReady = new ConditionVariable();
+
+    private MediaSaveService.OnMediaSavedListener mOnMediaSavedListener =
+            new MediaSaveService.OnMediaSavedListener() {
+                @Override
+                public void onMediaSaved(Uri uri) {
+                    if (uri != null) {
+                        // TODO: Commenting out the line below for now. need to get it working
+                        // mActivity.addSecureAlbumItemIfNeeded(false, uri);
+                        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: {
+                   // TODO: Need to revisit
+                   // ((CameraScreenNail) mActivity.mCameraScreenNail).animateSwitchCamera();
+                    break;
+                }
+
+                case CAMERA_OPEN_DONE: {
+                    onCameraOpened();
+                    break;
+                }
+
+                case START_PREVIEW_DONE: {
+                    onPreviewStarted();
+                    break;
+                }
+
+                case OPEN_CAMERA_FAIL: {
+                    mCameraStartUpThread = null;
+                    mOpenCameraFail = true;
+                    Util.showErrorAndFinish(mActivity,
+                            R.string.cannot_connect_camera);
+                    break;
+                }
+
+                case CAMERA_DISABLED: {
+                    mCameraStartUpThread = null;
+                    mCameraDisabled = true;
+                    Util.showErrorAndFinish(mActivity,
+                            R.string.camera_disabled);
+                    break;
+                }
+            }
+        }
+    }
+
+    @Override
+    public void init(NewCameraActivity activity, View parent) {
+        mActivity = activity;
+        mUI = new NewPhotoUI(activity, this, parent);
+        mPreferences = new ComboPreferences(mActivity);
+        CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal());
+        mCameraId = getPreferredCameraId(mPreferences);
+
+        mContentResolver = mActivity.getContentResolver();
+
+        // To reduce startup time, open the camera and start the preview in
+        // another thread.
+        mCameraStartUpThread = new CameraStartUpThread();
+        mCameraStartUpThread.start();
+
+        // Surface texture is from camera screen nail and startPreview needs it.
+        // This must be done before startPreview.
+        mIsImageCaptureIntent = isImageCaptureIntent();
+
+        mPreferences.setLocalId(mActivity, mCameraId);
+        CameraSettings.upgradeLocalPreferences(mPreferences.getLocal());
+        // we need to reset exposure for the preview
+        resetExposureCompensation();
+        // Starting the preview needs preferences, camera screen nail, and
+        // focus area indicator.
+        mStartPreviewPrerequisiteReady.open();
+
+        initializeControlByIntent();
+        mQuickCapture = mActivity.getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false);
+        mLocationManager = new LocationManager(mActivity, mUI);
+        mSensorManager = (SensorManager)(mActivity.getSystemService(Context.SENSOR_SERVICE));
+    }
+
+    private void initializeControlByIntent() {
+        mUI.initializeControlByIntent();
+        if (mIsImageCaptureIntent) {
+            setupCaptureParams();
+        }
+    }
+
+    private void onPreviewStarted() {
+        mCameraStartUpThread = null;
+        setCameraState(IDLE);
+        startFaceDetection();
+        locationFirstRun();
+    }
+
+    // Prompt the user to pick to record location for the very first run of
+    // camera only
+    private void locationFirstRun() {
+        if (RecordLocationPreference.isSet(mPreferences)) {
+            return;
+        }
+        if (mActivity.isSecureCamera()) return;
+        // Check if the back camera exists
+        int backCameraId = CameraHolder.instance().getBackCameraId();
+        if (backCameraId == -1) {
+            // If there is no back camera, do not show the prompt.
+            return;
+        }
+
+        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 onCameraOpened() {
+        View root = mUI.getRootView();
+        // These depend on camera parameters.
+
+        int width = root.getWidth();
+        int height = root.getHeight();
+        mFocusManager.setPreviewSize(width, height);
+        openCameraCommon();
+    }
+
+    private void switchCamera() {
+        if (mPaused) return;
+
+        Log.v(TAG, "Start to switch camera. id=" + mPendingSwitchCameraId);
+        mCameraId = mPendingSwitchCameraId;
+        mPendingSwitchCameraId = -1;
+        setCameraId(mCameraId);
+
+        // from onPause
+        closeCamera();
+        mUI.collapseCameraControls();
+        mUI.clearFaces();
+        if (mFocusManager != null) mFocusManager.removeMessages();
+
+        // Restart the camera and initialize the UI. From onCreate.
+        mPreferences.setLocalId(mActivity, mCameraId);
+        CameraSettings.upgradeLocalPreferences(mPreferences.getLocal());
+        try {
+            mCameraDevice = Util.openCamera(mActivity, mCameraId);
+            mParameters = mCameraDevice.getParameters();
+        } catch (CameraHardwareException e) {
+            Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera);
+            return;
+        } catch (CameraDisabledException e) {
+            Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
+            return;
+        }
+        initializeCapabilities();
+        CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
+        boolean mirror = (info.facing == CameraInfo.CAMERA_FACING_FRONT);
+        mFocusManager.setMirror(mirror);
+        mFocusManager.setParameters(mInitialParams);
+        setupPreview();
+
+        openCameraCommon();
+
+        if (ApiHelper.HAS_SURFACE_TEXTURE) {
+            // Start switch camera animation. Post a message because
+            // onFrameAvailable from the old camera may already exist.
+            mHandler.sendEmptyMessage(SWITCH_CAMERA_START_ANIMATION);
+        }
+    }
+
+    protected void setCameraId(int cameraId) {
+        ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID);
+        pref.setValue("" + cameraId);
+    }
+
+    // either open a new camera or switch cameras
+    private void openCameraCommon() {
+        loadCameraPreferences();
+
+        mUI.onCameraOpened(mPreferenceGroup, mPreferences, mParameters, this);
+        updateSceneMode();
+        showTapToFocusToastIfNeeded();
+
+
+    }
+
+    public void onScreenSizeChanged(int width, int height, int previewWidth, int previewHeight) {
+        if (mFocusManager != null) mFocusManager.setPreviewSize(width, height);
+    }
+
+    private void resetExposureCompensation() {
+        String value = mPreferences.getString(CameraSettings.KEY_EXPOSURE,
+                CameraSettings.EXPOSURE_DEFAULT_VALUE);
+        if (!CameraSettings.EXPOSURE_DEFAULT_VALUE.equals(value)) {
+            Editor editor = mPreferences.edit();
+            editor.putString(CameraSettings.KEY_EXPOSURE, "0");
+            editor.apply();
+        }
+    }
+
+    private void keepMediaProviderInstance() {
+        // We want to keep a reference to MediaProvider in camera's lifecycle.
+        // TODO: Utilize mMediaProviderClient instance to replace
+        // ContentResolver calls.
+        if (mMediaProviderClient == null) {
+            mMediaProviderClient = mContentResolver
+                    .acquireContentProviderClient(MediaStore.AUTHORITY);
+        }
+    }
+
+    // Snapshots can only be taken after this is called. It should be called
+    // once only. We could have done these things in onCreate() but we want to
+    // make preview screen appear as soon as possible.
+    private void initializeFirstTime() {
+        if (mFirstTimeInitialized) return;
+
+        // Initialize location service.
+        boolean recordLocation = RecordLocationPreference.get(
+                mPreferences, mContentResolver);
+        mLocationManager.recordLocation(recordLocation);
+
+        keepMediaProviderInstance();
+
+        mUI.initializeFirstTime();
+        MediaSaveService s = mActivity.getMediaSaveService();
+        // We set the listener only when both service and shutterbutton
+        // are initialized.
+        if (s != null) {
+            s.setListener(this);
+        }
+
+        mNamedImages = new NamedImages();
+
+        mFirstTimeInitialized = true;
+        addIdleHandler();
+
+        mActivity.updateStorageSpaceAndHint();
+    }
+
+    // If the activity is paused and resumed, this method will be called in
+    // onResume.
+    private void initializeSecondTime() {
+        // Start location update if needed.
+        boolean recordLocation = RecordLocationPreference.get(
+                mPreferences, mContentResolver);
+        mLocationManager.recordLocation(recordLocation);
+        MediaSaveService s = mActivity.getMediaSaveService();
+        if (s != null) {
+            s.setListener(this);
+        }
+        mNamedImages = new NamedImages();
+        mUI.initializeSecondTime(mParameters);
+        keepMediaProviderInstance();
+    }
+
+    @Override
+    public void onSurfaceCreated(SurfaceHolder holder) {
+        // Do not access the camera if camera start up thread is not finished.
+        if (mCameraDevice == null || mCameraStartUpThread != null)
+            return;
+
+        mCameraDevice.setPreviewDisplayAsync(holder);
+        // This happens when onConfigurationChanged arrives, surface has been
+        // destroyed, and there is no onFullScreenChanged.
+        if (mCameraState == PREVIEW_STOPPED) {
+            setupPreview();
+        }
+    }
+
+    private void showTapToFocusToastIfNeeded() {
+        // Show the tap to focus toast if this is the first start.
+        if (mFocusAreaSupported &&
+                mPreferences.getBoolean(CameraSettings.KEY_CAMERA_FIRST_USE_HINT_SHOWN, true)) {
+            // Delay the toast for one second to wait for orientation.
+            mHandler.sendEmptyMessageDelayed(SHOW_TAP_TO_FOCUS_TOAST, 1000);
+        }
+    }
+
+    private void addIdleHandler() {
+        MessageQueue queue = Looper.myQueue();
+        queue.addIdleHandler(new MessageQueue.IdleHandler() {
+            @Override
+            public boolean queueIdle() {
+                Storage.ensureOSXCompatible();
+                return false;
+            }
+        });
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    @Override
+    public void startFaceDetection() {
+        if (!ApiHelper.HAS_FACE_DETECTION) return;
+        if (mFaceDetectionStarted) return;
+        if (mParameters.getMaxNumDetectedFaces() > 0) {
+            mFaceDetectionStarted = true;
+            CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
+            mUI.onStartFaceDetection(mDisplayOrientation,
+                    (info.facing == CameraInfo.CAMERA_FACING_FRONT));
+            mCameraDevice.setFaceDetectionListener(mUI);
+            mCameraDevice.startFaceDetection();
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    @Override
+    public void stopFaceDetection() {
+        if (!ApiHelper.HAS_FACE_DETECTION) return;
+        if (!mFaceDetectionStarted) return;
+        if (mParameters.getMaxNumDetectedFaces() > 0) {
+            mFaceDetectionStarted = false;
+            mCameraDevice.setFaceDetectionListener(null);
+            mCameraDevice.stopFaceDetection();
+            mUI.clearFaces();
+        }
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent m) {
+        if (mCameraState == SWITCHING_CAMERA) return true;
+        return mUI.dispatchTouchEvent(m);
+    }
+
+    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) {
+                mUI.showSwitcher();
+                //TODO: 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");
+
+             /*TODO:
+            // Only animate when in full screen capture mode
+            // i.e. If monkey/a user swipes to the gallery during picture taking,
+            // don't show animation
+            if (ApiHelper.HAS_SURFACE_TEXTURE && !mIsImageCaptureIntent
+                    && mActivity.mShowCameraAppView) {
+                // Finish capture animation
+                ((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();
+                ExifInterface exif = Exif.getExif(jpegData);
+                int orientation = Exif.getOrientation(exif);
+                int width, height;
+                if ((mJpegRotation + orientation) % 180 == 0) {
+                    width = s.width;
+                    height = s.height;
+                } else {
+                    width = s.height;
+                    height = s.width;
+                }
+                String title = mNamedImages.getTitle();
+                long date = mNamedImages.getDate();
+                if (title == null) {
+                    Log.e(TAG, "Unbalanced name/data pair");
+                } else {
+                    if (date == -1) date = mCaptureStartTime;
+                    if (mHeading >= 0) {
+                        // heading direction has been updated by the sensor.
+                        ExifTag directionRefTag = exif.buildTag(
+                                ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
+                                ExifInterface.GpsTrackRef.MAGNETIC_DIRECTION);
+                        ExifTag directionTag = exif.buildTag(
+                                ExifInterface.TAG_GPS_IMG_DIRECTION,
+                                new Rational(mHeading, 1));
+                        exif.setTag(directionRefTag);
+                        exif.setTag(directionTag);
+                    }
+                    mActivity.getMediaSaveService().addImage(
+                            jpegData, title, date, mLocation, width, height,
+                            orientation, exif, mOnMediaSavedListener, mContentResolver);
+                }
+            } else {
+                mJpegImageData = jpegData;
+                if (!mQuickCapture) {
+                    mUI.showPostCaptureAlert();
+                } else {
+                    onCaptureDone();
+                }
+            }
+
+            // Check this in advance of each shot so we don't add to shutter
+            // latency. It's true that someone else could write to the SD card in
+            // the mean time and fill it, but that could have happened between the
+            // shutter press and saving the JPEG too.
+            mActivity.updateStorageSpaceAndHint();
+
+            long now = System.currentTimeMillis();
+            mJpegCallbackFinishTime = now - mJpegPictureCallbackTime;
+            Log.v(TAG, "mJpegCallbackFinishTime = "
+                    + mJpegCallbackFinishTime + "ms");
+            mJpegPictureCallbackTime = 0;
+        }
+    }
+
+    private final class AutoFocusCallback
+            implements 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, mUI.isShutterPressed());
+        }
+    }
+
+    @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 PhotoController.PREVIEW_STOPPED:
+        case PhotoController.SNAPSHOT_IN_PROGRESS:
+        case PhotoController.FOCUSING:
+        case PhotoController.SWITCHING_CAMERA:
+            mUI.enableGestures(false);
+            break;
+        case PhotoController.IDLE:
+            mUI.enableGestures(true);
+            break;
+        }
+    }
+
+    private void animateFlash() {
+        /* //TODO:
+        // Only animate when in full screen capture mode
+        // i.e. If monkey/a user swipes to the gallery during picture taking,
+        // don't show animation
+        if (ApiHelper.HAS_SURFACE_TEXTURE && !mIsImageCaptureIntent
+                && mActivity.mShowCameraAppView) {
+            // 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
+                || mActivity.getMediaSaveService().isQueueFull()) {
+            return false;
+        }
+        mCaptureStartTime = System.currentTimeMillis();
+        mPostViewPictureCallbackTime = 0;
+        mJpegImageData = null;
+
+        final boolean animateBefore = (mSceneMode == Util.SCENE_MODE_HDR);
+
+        if (animateBefore) {
+            animateFlash();
+        }
+
+        // Set rotation and gps data.
+        int orientation = (360 - mDisplayRotation) % 360;
+        // We need to be consistent with the framework orientation (i.e. the
+        // orientation of the UI.) when the auto-rotate screen setting is on.
+        if (mActivity.isAutoRotateScreen()) {
+            orientation = (360 - mDisplayRotation) % 360;
+        } else {
+            orientation = mOrientation;
+        }
+        mJpegRotation = Util.getJpegRotation(mCameraId, orientation);
+        mParameters.setRotation(mJpegRotation);
+        Location loc = mLocationManager.getCurrentLocation();
+        Util.setGpsParameters(mParameters, loc);
+        mCameraDevice.setParameters(mParameters);
+
+        mCameraDevice.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 updateSceneMode() {
+        // If scene mode is set, we cannot set flash mode, white balance, and
+        // focus mode, instead, we read it from driver
+        if (!Parameters.SCENE_MODE_AUTO.equals(mSceneMode)) {
+            overrideCameraSettings(mParameters.getFlashMode(),
+                    mParameters.getWhiteBalance(), mParameters.getFocusMode());
+        } else {
+            overrideCameraSettings(null, null, null);
+        }
+    }
+
+    private void overrideCameraSettings(final String flashMode,
+            final String whiteBalance, final String focusMode) {
+        mUI.overrideSettings(
+                CameraSettings.KEY_FLASH_MODE, flashMode,
+                CameraSettings.KEY_WHITE_BALANCE, whiteBalance,
+                CameraSettings.KEY_FOCUS_MODE, focusMode);
+    }
+
+    private void loadCameraPreferences() {
+        CameraSettings settings = new CameraSettings(mActivity, mInitialParams,
+                mCameraId, CameraHolder.instance().getCameraInfo());
+        mPreferenceGroup = settings.getPreferenceGroup(R.xml.camera_preferences);
+    }
+
+    @Override
+    public void onOrientationChanged(int orientation) {
+        // We keep the last known orientation. So if the user first orient
+        // the camera then point the camera to floor or sky, we still have
+        // the correct orientation.
+        if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) return;
+        mOrientation = Util.roundOrientation(orientation, mOrientation);
+
+        // Show the toast after getting the first orientation changed.
+        if (mHandler.hasMessages(SHOW_TAP_TO_FOCUS_TOAST)) {
+            mHandler.removeMessages(SHOW_TAP_TO_FOCUS_TOAST);
+            showTapToFocusToast();
+        }
+    }
+
+    @Override
+    public void onStop() {
+        if (mMediaProviderClient != null) {
+            mMediaProviderClient.release();
+            mMediaProviderClient = null;
+        }
+    }
+
+    @Override
+    public void onCaptureCancelled() {
+        mActivity.setResultEx(Activity.RESULT_CANCELED, new Intent());
+        mActivity.finish();
+    }
+
+    @Override
+    public void onCaptureRetake() {
+        if (mPaused)
+            return;
+        mUI.hidePostCaptureAlert();
+        setupPreview();
+    }
+
+    @Override
+    public void onCaptureDone() {
+        if (mPaused) {
+            return;
+        }
+
+        byte[] data = mJpegImageData;
+
+        if (mCropValue == null) {
+            // First handle the no crop case -- just return the value.  If the
+            // caller specifies a "save uri" then write the data to its
+            // stream. Otherwise, pass back a scaled down version of the bitmap
+            // directly in the extras.
+            if (mSaveUri != null) {
+                OutputStream outputStream = null;
+                try {
+                    outputStream = mContentResolver.openOutputStream(mSaveUri);
+                    outputStream.write(data);
+                    outputStream.close();
+
+                    mActivity.setResultEx(Activity.RESULT_OK);
+                    mActivity.finish();
+                } catch (IOException ex) {
+                    // ignore exception
+                } finally {
+                    Util.closeSilently(outputStream);
+                }
+            } else {
+                ExifInterface exif = Exif.getExif(data);
+                int orientation = Exif.getOrientation(exif);
+                Bitmap bitmap = Util.makeBitmap(data, 50 * 1024);
+                bitmap = Util.rotate(bitmap, orientation);
+                mActivity.setResultEx(Activity.RESULT_OK,
+                        new Intent("inline-data").putExtra("data", bitmap));
+                mActivity.finish();
+            }
+        } else {
+            // Save the image to a temp file and invoke the cropper
+            Uri tempUri = null;
+            FileOutputStream tempStream = null;
+            try {
+                File path = mActivity.getFileStreamPath(sTempCropFilename);
+                path.delete();
+                tempStream = mActivity.openFileOutput(sTempCropFilename, 0);
+                tempStream.write(data);
+                tempStream.close();
+                tempUri = Uri.fromFile(path);
+            } catch (FileNotFoundException ex) {
+                mActivity.setResultEx(Activity.RESULT_CANCELED);
+                mActivity.finish();
+                return;
+            } catch (IOException ex) {
+                mActivity.setResultEx(Activity.RESULT_CANCELED);
+                mActivity.finish();
+                return;
+            } finally {
+                Util.closeSilently(tempStream);
+            }
+
+            Bundle newExtras = new Bundle();
+            if (mCropValue.equals("circle")) {
+                newExtras.putString("circleCrop", "true");
+            }
+            if (mSaveUri != null) {
+                newExtras.putParcelable(MediaStore.EXTRA_OUTPUT, mSaveUri);
+            } else {
+                newExtras.putBoolean(CropExtras.KEY_RETURN_DATA, true);
+            }
+            if (mActivity.isSecureCamera()) {
+                newExtras.putBoolean(CropExtras.KEY_SHOW_WHEN_LOCKED, true);
+            }
+
+            Intent cropIntent = new Intent(FilterShowActivity.CROP_ACTION);
+
+            cropIntent.setData(tempUri);
+            cropIntent.putExtras(newExtras);
+
+            mActivity.startActivityForResult(cropIntent, REQUEST_CROP);
+        }
+    }
+
+    @Override
+    public void onShutterButtonFocus(boolean pressed) {
+        if (mPaused || mUI.collapseCameraControls()
+                || (mCameraState == SNAPSHOT_IN_PROGRESS)
+                || (mCameraState == PREVIEW_STOPPED)) return;
+
+        // Do not do focus if there is not enough storage.
+        if (pressed && !canTakePicture()) return;
+
+        if (pressed) {
+            if (mSceneMode == Util.SCENE_MODE_HDR) {
+                mUI.hideSwitcher();
+                //TODO: mActivity.setSwipingEnabled(false);
+            }
+            mFocusManager.onShutterDown();
+        } else {
+            // for countdown mode, we need to postpone the shutter release
+            // i.e. lock the focus during countdown.
+            if (!mUI.isCountingDown()) {
+                mFocusManager.onShutterUp();
+            }
+        }
+    }
+
+    @Override
+    public void onShutterButtonClick() {
+        if (mPaused || mUI.collapseCameraControls()
+                || (mCameraState == SWITCHING_CAMERA)
+                || (mCameraState == PREVIEW_STOPPED)) return;
+
+        // Do not take the picture if there is not enough storage.
+        if (mActivity.getStorageSpace() <= Storage.LOW_STORAGE_THRESHOLD) {
+            Log.i(TAG, "Not enough space or storage not ready. remaining="
+                    + mActivity.getStorageSpace());
+            return;
+        }
+        Log.v(TAG, "onShutterButtonClick: mCameraState=" + mCameraState);
+
+        // If the user wants to do a snapshot while the previous one is still
+        // in progress, remember the fact and do it after we finish the previous
+        // one and re-start the preview. Snapshot in progress also includes the
+        // state that autofocus is focusing and a picture will be taken when
+        // focus callback arrives.
+        if ((mFocusManager.isFocusingSnapOnFinish() || mCameraState == SNAPSHOT_IN_PROGRESS)
+                && !mIsImageCaptureIntent) {
+            mSnapshotOnIdle = true;
+            return;
+        }
+
+        String timer = mPreferences.getString(
+                CameraSettings.KEY_TIMER,
+                mActivity.getString(R.string.pref_camera_timer_default));
+        boolean playSound = mPreferences.getString(CameraSettings.KEY_TIMER_SOUND_EFFECTS,
+                mActivity.getString(R.string.pref_camera_timer_sound_default))
+                .equals(mActivity.getString(R.string.setting_on_value));
+
+        int seconds = Integer.parseInt(timer);
+        // When shutter button is pressed, check whether the previous countdown is
+        // finished. If not, cancel the previous countdown and start a new one.
+        if (mUI.isCountingDown()) {
+            mUI.cancelCountDown();
+        }
+        if (seconds > 0) {
+            mUI.startCountDown(seconds, playSound);
+        } else {
+           mSnapshotOnIdle = false;
+           mFocusManager.doSnap();
+        }
+    }
+
+    @Override
+    public void installIntentFilter() {
+    }
+
+    @Override
+    public boolean updateStorageHintOnResume() {
+        return mFirstTimeInitialized;
+    }
+
+    @Override
+    public void updateCameraAppView() {
+    }
+
+    @Override
+    public void onResumeBeforeSuper() {
+        mPaused = false;
+    }
+
+    @Override
+    public void onResumeAfterSuper() {
+        if (mOpenCameraFail || mCameraDisabled) return;
+
+        mJpegPictureCallbackTime = 0;
+        mZoomValue = 0;
+        // Start the preview if it is not started.
+        if (mCameraState == PREVIEW_STOPPED && mCameraStartUpThread == null) {
+            resetExposureCompensation();
+            mCameraStartUpThread = new CameraStartUpThread();
+            mCameraStartUpThread.start();
+        }
+
+        // If first time initialization is not finished, put it in the
+        // message queue.
+        if (!mFirstTimeInitialized) {
+            mHandler.sendEmptyMessage(FIRST_TIME_INIT);
+        } else {
+            initializeSecondTime();
+        }
+        keepScreenOnAwhile();
+
+        // Dismiss open menu if exists.
+        PopupManager.getInstance(mActivity).notifyShowPopup(null);
+        UsageStatistics.onContentViewChanged(
+                UsageStatistics.COMPONENT_CAMERA, "PhotoModule");
+
+        Sensor gsensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+        if (gsensor != null) {
+            mSensorManager.registerListener(this, gsensor, SensorManager.SENSOR_DELAY_NORMAL);
+        }
+
+        Sensor msensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
+        if (msensor != null) {
+            mSensorManager.registerListener(this, msensor, SensorManager.SENSOR_DELAY_NORMAL);
+        }
+    }
+
+    void waitCameraStartUpThread() {
+        try {
+            if (mCameraStartUpThread != null) {
+                mCameraStartUpThread.cancel();
+                mCameraStartUpThread.join();
+                mCameraStartUpThread = null;
+                setCameraState(IDLE);
+            }
+        } catch (InterruptedException e) {
+            // ignore
+        }
+    }
+
+    @Override
+    public void onPauseBeforeSuper() {
+        mPaused = true;
+        Sensor gsensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+        if (gsensor != null) {
+            mSensorManager.unregisterListener(this, gsensor);
+        }
+
+        Sensor msensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
+        if (msensor != null) {
+            mSensorManager.unregisterListener(this, msensor);
+        }
+    }
+
+    @Override
+    public void onPauseAfterSuper() {
+        // Wait the camera start up thread to finish.
+        waitCameraStartUpThread();
+
+        // When camera is started from secure lock screen for the first time
+        // after screen on, the activity gets onCreate->onResume->onPause->onResume.
+        // To reduce the latency, keep the camera for a short time so it does
+        // not need to be opened again.
+        if (mCameraDevice != null && mActivity.isSecureCamera()
+                && 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();
+
+        mNamedImages = null;
+
+        if (mLocationManager != null) mLocationManager.recordLocation(false);
+
+        // If we are in an image capture intent and has taken
+        // a picture, we just clear it in onPause.
+        mJpegImageData = null;
+
+        // Remove the messages in the event queue.
+        mHandler.removeMessages(SETUP_PREVIEW);
+        mHandler.removeMessages(FIRST_TIME_INIT);
+        mHandler.removeMessages(CHECK_DISPLAY_ROTATION);
+        mHandler.removeMessages(SWITCH_CAMERA);
+        mHandler.removeMessages(SWITCH_CAMERA_START_ANIMATION);
+        mHandler.removeMessages(CAMERA_OPEN_DONE);
+        mHandler.removeMessages(START_PREVIEW_DONE);
+        mHandler.removeMessages(OPEN_CAMERA_FAIL);
+        mHandler.removeMessages(CAMERA_DISABLED);
+
+        closeCamera();
+
+        resetScreenOn();
+        mUI.onPause();
+
+        mPendingSwitchCameraId = -1;
+        if (mFocusManager != null) mFocusManager.removeMessages();
+        MediaSaveService s = mActivity.getMediaSaveService();
+        if (s != null) {
+            s.setListener(null);
+        }
+    }
+
+    /**
+     * The focus manager is the first UI related element to get initialized,
+     * and it requires the RenderOverlay, so initialize it here
+     */
+    private void initializeFocusManager() {
+        // Create FocusManager object. startPreview needs it.
+        // if mFocusManager not null, reuse it
+        // otherwise create a new instance
+        if (mFocusManager != null) {
+            mFocusManager.removeMessages();
+        } else {
+            CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
+            boolean mirror = (info.facing == CameraInfo.CAMERA_FACING_FRONT);
+            String[] defaultFocusModes = mActivity.getResources().getStringArray(
+                    R.array.pref_camera_focusmode_default_array);
+            mFocusManager = new FocusOverlayManager(mPreferences, defaultFocusModes,
+                    mInitialParams, this, mirror,
+                    mActivity.getMainLooper(), mUI);
+        }
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        Log.v(TAG, "onConfigurationChanged");
+        setDisplayOrientation();
+    }
+
+    @Override
+    public void 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 (mUI.removeTopLevelPopup()) return;
+
+        // Check if metering area or focus area is supported.
+        if (!mFocusAreaSupported && !mMeteringAreaSupported) return;
+        mFocusManager.onSingleTapUp(x, y);
+    }
+
+    @Override
+    public boolean onBackPressed() {
+        return mUI.onBackPressed();
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        switch (keyCode) {
+        case KeyEvent.KEYCODE_VOLUME_UP:
+        case KeyEvent.KEYCODE_VOLUME_DOWN:
+        case KeyEvent.KEYCODE_FOCUS:
+            if (/*TODO: mActivity.isInCameraApp() &&*/ mFirstTimeInitialized) {
+                if (event.getRepeatCount() == 0) {
+                    onShutterButtonFocus(true);
+                }
+                return true;
+            }
+            return false;
+        case KeyEvent.KEYCODE_CAMERA:
+            if (mFirstTimeInitialized && event.getRepeatCount() == 0) {
+                onShutterButtonClick();
+            }
+            return true;
+        case KeyEvent.KEYCODE_DPAD_CENTER:
+            // If we get a dpad center event without any focused view, move
+            // the focus to the shutter button and press it.
+            if (mFirstTimeInitialized && event.getRepeatCount() == 0) {
+                // Start auto-focus immediately to reduce shutter lag. After
+                // the shutter button gets the focus, onShutterButtonFocus()
+                // will be called again but it is fine.
+                if (mUI.removeTopLevelPopup()) return true;
+                onShutterButtonFocus(true);
+                mUI.pressShutterButton();
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        switch (keyCode) {
+        case KeyEvent.KEYCODE_VOLUME_UP:
+        case KeyEvent.KEYCODE_VOLUME_DOWN:
+            if (/*mActivity.isInCameraApp() && */ mFirstTimeInitialized) {
+                onShutterButtonClick();
+                return true;
+            }
+            return false;
+        case KeyEvent.KEYCODE_FOCUS:
+            if (mFirstTimeInitialized) {
+                onShutterButtonFocus(false);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    private void closeCamera() {
+        if (mCameraDevice != null) {
+            mCameraDevice.setZoomChangeListener(null);
+            if(ApiHelper.HAS_FACE_DETECTION) {
+                mCameraDevice.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 = mDisplayOrientation;
+        mUI.setDisplayOrientation(mDisplayOrientation);
+        if (mFocusManager != null) {
+            mFocusManager.setDisplayOrientation(mDisplayOrientation);
+        }
+        // Change the camera display orientation
+        if (mCameraDevice != null) {
+            mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation);
+        }
+    }
+
+    // Only called by UI thread.
+    private void setupPreview() {
+        mFocusManager.resetTouchFocus();
+        startPreview();
+        setCameraState(IDLE);
+        startFaceDetection();
+    }
+
+    // This can be called by UI Thread or CameraStartUpThread. So this should
+    // not modify the views.
+    private void startPreview() {
+        mCameraDevice.setErrorCallback(mErrorCallback);
+
+        // ICS camera frameworks has a bug. Face detection state is not cleared
+        // after taking a picture. Stop the preview to work around it. The bug
+        // was fixed in JB.
+        if (mCameraState != PREVIEW_STOPPED) stopPreview();
+
+        setDisplayOrientation();
+
+        if (!mSnapshotOnIdle) {
+            // If the focus mode is continuous autofocus, call cancelAutoFocus to
+            // resume it because it may have been paused by autoFocus call.
+            if (Util.FOCUS_MODE_CONTINUOUS_PICTURE.equals(mFocusManager.getFocusMode())) {
+                mCameraDevice.cancelAutoFocus();
+            }
+            mFocusManager.setAeAwbLock(false); // Unlock AE and AWB.
+        }
+        setCameraParameters(UPDATE_PARAM_ALL);
+        // Let UI set its expected aspect ratio
+        mUI.setPreviewSize(mParameters.getPreviewSize());
+        Object st = mUI.getSurfaceTexture();
+        if (st != null) {
+           mCameraDevice.setPreviewTextureAsync((SurfaceTexture) st);
+        }
+
+        Log.v(TAG, "startPreview");
+        mCameraDevice.startPreviewAsync();
+        mFocusManager.onPreviewStarted();
+
+        if (mSnapshotOnIdle) {
+            mHandler.post(mDoSnapRunnable);
+        }
+    }
+
+    @Override
+    public void stopPreview() {
+        if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
+            Log.v(TAG, "stopPreview");
+            mCameraDevice.stopPreview();
+            mFaceDetectionStarted = false;
+        }
+        setCameraState(PREVIEW_STOPPED);
+        if (mFocusManager != null) mFocusManager.onPreviewStopped();
+    }
+
+    @SuppressWarnings("deprecation")
+    private void updateCameraParametersInitialize() {
+        // Reset preview frame rate to the maximum because it may be lowered by
+        // video camera application.
+        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);
+            updateSceneMode();
+            mUpdateSet = 0;
+        } else {
+            if (!mHandler.hasMessages(SET_CAMERA_PARAMETERS_WHEN_IDLE)) {
+                mHandler.sendEmptyMessageDelayed(
+                        SET_CAMERA_PARAMETERS_WHEN_IDLE, 1000);
+            }
+        }
+    }
+
+    public boolean isCameraIdle() {
+        return (mCameraState == IDLE) ||
+                (mCameraState == PREVIEW_STOPPED) ||
+                ((mFocusManager != null) && mFocusManager.isFocusCompleted()
+                        && (mCameraState != SWITCHING_CAMERA));
+    }
+
+    public boolean isImageCaptureIntent() {
+        String action = mActivity.getIntent().getAction();
+        return (MediaStore.ACTION_IMAGE_CAPTURE.equals(action)
+                || 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");
+        }
+    }
+
+    @Override
+    public void onSharedPreferenceChanged() {
+        // ignore the events after "onPause()"
+        if (mPaused) return;
+
+        boolean recordLocation = RecordLocationPreference.get(
+                mPreferences, mContentResolver);
+        mLocationManager.recordLocation(recordLocation);
+
+        setCameraParametersWhenIdle(UPDATE_PARAM_PREFERENCE);
+        mUI.updateOnScreenIndicators(mParameters, mPreferenceGroup, mPreferences);
+    }
+
+    @Override
+    public void onCameraPickerClicked(int cameraId) {
+        if (mPaused || mPendingSwitchCameraId != -1) return;
+
+        mPendingSwitchCameraId = cameraId;
+
+        Log.v(TAG, "Start to switch camera. cameraId=" + cameraId);
+        // We need to keep a preview frame for the animation before
+        // releasing the camera. This will trigger onPreviewTextureCopied.
+        //TODO: Need to animate the camera switch
+        switchCamera();
+    }
+
+    // Preview texture has been copied. Now camera can be released and the
+    // animation can be started.
+    @Override
+    public void onPreviewTextureCopied() {
+        mHandler.sendEmptyMessage(SWITCH_CAMERA);
+    }
+
+    @Override
+    public void onCaptureTextureCopied() {
+    }
+
+    @Override
+    public void onUserInteraction() {
+      if (!mActivity.isFinishing()) keepScreenOnAwhile();
+    }
+
+    private void resetScreenOn() {
+        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+        mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+    }
+
+    private void keepScreenOnAwhile() {
+        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+        mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+        mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY);
+    }
+
+    @Override
+    public void onOverriddenPreferencesClicked() {
+        if (mPaused) return;
+        mUI.showPreferencesToast();
+    }
+
+    private void showTapToFocusToast() {
+        // TODO: Use a toast?
+        new RotateTextToast(mActivity, R.string.tap_to_focus, 0).show();
+        // Clear the preference.
+        Editor editor = mPreferences.edit();
+        editor.putBoolean(CameraSettings.KEY_CAMERA_FIRST_USE_HINT_SHOWN, false);
+        editor.apply();
+    }
+
+    private void initializeCapabilities() {
+        mInitialParams = mCameraDevice.getParameters();
+        mFocusAreaSupported = Util.isFocusAreaSupported(mInitialParams);
+        mMeteringAreaSupported = Util.isMeteringAreaSupported(mInitialParams);
+        mAeLockSupported = Util.isAutoExposureLockSupported(mInitialParams);
+        mAwbLockSupported = Util.isAutoWhiteBalanceLockSupported(mInitialParams);
+        mContinousFocusSupported = mInitialParams.getSupportedFocusModes().contains(
+                Util.FOCUS_MODE_CONTINUOUS_PICTURE);
+    }
+
+    @Override
+    public void onCountDownFinished() {
+        mSnapshotOnIdle = false;
+        mFocusManager.doSnap();
+        mFocusManager.onShutterUp();
+    }
+
+    @Override
+    public boolean needsSwitcher() {
+        return !mIsImageCaptureIntent;
+    }
+
+    @Override
+    public boolean needsPieMenu() {
+        return true;
+    }
+
+    @Override
+    public void onShowSwitcherPopup() {
+        mUI.onShowSwitcherPopup();
+    }
+
+    @Override
+    public int onZoomChanged(int index) {
+        // Not useful to change zoom value when the activity is paused.
+        if (mPaused) return index;
+        mZoomValue = index;
+        if (mParameters == null || mCameraDevice == null) return index;
+        // Set zoom parameters asynchronously
+        mParameters.setZoom(mZoomValue);
+        mCameraDevice.setParameters(mParameters);
+        Parameters p = mCameraDevice.getParameters();
+        if (p != null) return p.getZoom();
+        return index;
+    }
+
+    @Override
+    public int getCameraState() {
+        return mCameraState;
+    }
+
+    @Override
+    public void onQueueStatus(boolean full) {
+        mUI.enableShutter(!full);
+    }
+
+    @Override
+    public void onMediaSaveServiceConnected(MediaSaveService s) {
+        // We set the listener only when both service and shutterbutton
+        // are initialized.
+        if (mFirstTimeInitialized) {
+            s.setListener(this);
+        }
+    }
+
+    @Override
+    public void onAccuracyChanged(Sensor sensor, int accuracy) {
+    }
+
+    @Override
+    public void onSensorChanged(SensorEvent event) {
+        int type = event.sensor.getType();
+        float[] data;
+        if (type == Sensor.TYPE_ACCELEROMETER) {
+            data = mGData;
+        } else if (type == Sensor.TYPE_MAGNETIC_FIELD) {
+            data = mMData;
+        } else {
+            // we should not be here.
+            return;
+        }
+        for (int i = 0; i < 3 ; i++) {
+            data[i] = event.values[i];
+        }
+        float[] orientation = new float[3];
+        SensorManager.getRotationMatrix(mR, null, mGData, mMData);
+        SensorManager.getOrientation(mR, orientation);
+        mHeading = (int) (orientation[0] * 180f / Math.PI) % 360;
+        if (mHeading < 0) {
+            mHeading += 360;
+        }
+    }
+/* Below is no longer needed, except to get rid of compile error
+ * TODO: Remove these
+ */
+
+    // TODO: Delete this function after old camera code is removed
+    @Override
+    public void onRestorePreferencesClicked() {}
+
+    @Override
+    public void onFullScreenChanged(boolean full) {
+        /* //TODO:
+        mUI.onFullScreenChanged(full);
+        if (ApiHelper.HAS_SURFACE_TEXTURE) {
+            if (mActivity.mCameraScreenNail != null) {
+                ((CameraScreenNail) mActivity.mCameraScreenNail).setFullScreen(full);
+            }
+            return;
+        } */
+    }
+
+}
diff --git a/src/com/android/camera/NewPhotoUI.java b/src/com/android/camera/NewPhotoUI.java
new file mode 100644
index 0000000..1a14cf5
--- /dev/null
+++ b/src/com/android/camera/NewPhotoUI.java
@@ -0,0 +1,806 @@
+/*
+ * 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.graphics.Matrix;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.hardware.Camera.Face;
+import android.hardware.Camera.FaceDetectionListener;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.Size;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.TextureView;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLayoutChangeListener;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.ImageView;
+import android.widget.Toast;
+
+import com.android.camera.CameraPreference.OnPreferenceChangedListener;
+import com.android.camera.FocusOverlayManager.FocusUI;
+import com.android.camera.ui.AbstractSettingPopup;
+import com.android.camera.ui.CameraSwitcher.CameraSwitchListener;
+import com.android.camera.ui.CountDownView;
+import com.android.camera.ui.CountDownView.OnCountDownFinishedListener;
+import com.android.camera.ui.CameraSwitcher;
+import com.android.camera.ui.FaceView;
+import com.android.camera.ui.FocusIndicator;
+import com.android.camera.ui.PieRenderer;
+import com.android.camera.ui.PieRenderer.PieListener;
+import com.android.camera.ui.RenderOverlay;
+import com.android.camera.ui.ZoomRenderer;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.io.IOException;
+import java.util.List;
+
+public class NewPhotoUI implements PieListener,
+    NewPreviewGestures.SingleTapListener,
+    NewPreviewGestures.CancelEventListener,
+    FocusUI, TextureView.SurfaceTextureListener,
+    LocationManager.Listener,
+    FaceDetectionListener,
+    NewPreviewGestures.SwipeListener {
+
+    private static final String TAG = "CAM_UI";
+    private static final int UPDATE_TRANSFORM_MATRIX = 1;
+    private NewCameraActivity mActivity;
+    private PhotoController mController;
+    private NewPreviewGestures mGestures;
+
+    private View mRootView;
+    private Object mSurfaceTexture;
+
+    private AbstractSettingPopup mPopup;
+    private ShutterButton mShutterButton;
+    private CountDownView mCountDownView;
+
+    private FaceView mFaceView;
+    private RenderOverlay mRenderOverlay;
+    private View mReviewCancelButton;
+    private View mReviewDoneButton;
+    private View mReviewRetakeButton;
+
+    private View mMenuButton;
+    private View mBlocker;
+    private NewPhotoMenu mMenu;
+    private CameraSwitcher mSwitcher;
+    private View mCameraControls;
+
+    // Small indicators which show the camera settings in the viewfinder.
+    private OnScreenIndicators mOnScreenIndicators;
+
+    private PieRenderer mPieRenderer;
+    private ZoomRenderer mZoomRenderer;
+    private Toast mNotSelectableToast;
+
+    private int mZoomMax;
+    private List<Integer> mZoomRatios;
+
+    private int mPreviewWidth = 0;
+    private int mPreviewHeight = 0;
+    private float mSurfaceTextureUncroppedWidth;
+    private float mSurfaceTextureUncroppedHeight;
+
+    private SurfaceTextureSizeChangedListener mSurfaceTextureSizeListener;
+    private TextureView mTextureView;
+    private Matrix mMatrix = null;
+    private float mAspectRatio = 4f / 3f;
+    private final Object mLock = new Object();
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case UPDATE_TRANSFORM_MATRIX:
+                    setTransformMatrix(mPreviewWidth, mPreviewHeight);
+                    break;
+                default:
+                    break;
+            }
+        }
+    };
+
+    public interface SurfaceTextureSizeChangedListener {
+        public void onSurfaceTextureSizeChanged(int uncroppedWidth, int uncroppedHeight);
+    }
+
+    private OnLayoutChangeListener mLayoutListener = new OnLayoutChangeListener() {
+        @Override
+        public void onLayoutChange(View v, int left, int top, int right,
+                int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+            int width = right - left;
+            int height = bottom - top;
+            // Full-screen screennail
+            int w = width;
+            int h = height;
+            if (Util.getDisplayRotation(mActivity) % 180 != 0) {
+                w = height;
+                h = width;
+            }
+            if (mPreviewWidth != width || mPreviewHeight != height) {
+                mPreviewWidth = width;
+                mPreviewHeight = height;
+                onScreenSizeChanged(width, height, w, h);
+                mController.onScreenSizeChanged(width, height, w, h);
+            }
+        }
+    };
+
+    public NewPhotoUI(NewCameraActivity activity, PhotoController controller, View parent) {
+        mActivity = activity;
+        mController = controller;
+        mRootView = parent;
+
+        mActivity.getLayoutInflater().inflate(R.layout.new_photo_module,
+                (ViewGroup) mRootView, true);
+        mRenderOverlay = (RenderOverlay) mRootView.findViewById(R.id.render_overlay);
+        // display the view
+        mTextureView = (TextureView) mRootView.findViewById(R.id.preview_content);
+        mTextureView.setSurfaceTextureListener(this);
+        mTextureView.addOnLayoutChangeListener(mLayoutListener);
+        initIndicators();
+
+        mShutterButton = (ShutterButton) mRootView.findViewById(R.id.shutter_button);
+        mSwitcher = (CameraSwitcher) mRootView.findViewById(R.id.camera_switcher);
+        mSwitcher.setCurrentIndex(0);
+        mSwitcher.setSwitchListener((CameraSwitchListener) mActivity);
+        mMenuButton = mRootView.findViewById(R.id.menu);
+        if (ApiHelper.HAS_FACE_DETECTION) {
+            ViewStub faceViewStub = (ViewStub) mRootView
+                    .findViewById(R.id.face_view_stub);
+            if (faceViewStub != null) {
+                faceViewStub.inflate();
+                mFaceView = (FaceView) mRootView.findViewById(R.id.face_view);
+                setSurfaceTextureSizeChangedListener(
+                        (SurfaceTextureSizeChangedListener) mFaceView);
+            }
+        }
+        mCameraControls = mRootView.findViewById(R.id.camera_controls);
+    }
+
+    public void onScreenSizeChanged(int width, int height, int previewWidth, int previewHeight) {
+        setTransformMatrix(width, height);
+    }
+
+    public void setSurfaceTextureSizeChangedListener(SurfaceTextureSizeChangedListener listener) {
+        mSurfaceTextureSizeListener = listener;
+    }
+
+    public void setPreviewSize(Size size) {
+        int width = size.width;
+        int height = size.height;
+        if (width == 0 || height == 0) {
+            Log.w(TAG, "Preview size should not be 0.");
+            return;
+        }
+        if (width > height) {
+            mAspectRatio = (float) width / height;
+        } else {
+            mAspectRatio = (float) height / width;
+        }
+        mHandler.sendEmptyMessage(UPDATE_TRANSFORM_MATRIX);
+    }
+
+    private void setTransformMatrix(int width, int height) {
+        mMatrix = mTextureView.getTransform(mMatrix);
+        int orientation = Util.getDisplayRotation(mActivity);
+        float scaleX = 1f, scaleY = 1f;
+        float scaledTextureWidth, scaledTextureHeight;
+        if (width > height) {
+            scaledTextureWidth = Math.max(width,
+                    (int) (height * mAspectRatio));
+            scaledTextureHeight = Math.max(height,
+                    (int)(width / mAspectRatio));
+        } else {
+            scaledTextureWidth = Math.max(width,
+                    (int) (height / mAspectRatio));
+            scaledTextureHeight = Math.max(height,
+                    (int) (width * mAspectRatio));
+        }
+
+        if (mSurfaceTextureUncroppedWidth != scaledTextureWidth ||
+                mSurfaceTextureUncroppedHeight != scaledTextureHeight) {
+            mSurfaceTextureUncroppedWidth = scaledTextureWidth;
+            mSurfaceTextureUncroppedHeight = scaledTextureHeight;
+            if (mSurfaceTextureSizeListener != null) {
+                mSurfaceTextureSizeListener.onSurfaceTextureSizeChanged(
+                        (int) mSurfaceTextureUncroppedWidth, (int) mSurfaceTextureUncroppedHeight);
+            }
+        }
+        scaleX = scaledTextureWidth / width;
+        scaleY = scaledTextureHeight / height;
+        mMatrix.setScale(scaleX, scaleY, (float) width / 2, (float) height / 2);
+        mTextureView.setTransform(mMatrix);
+    }
+
+    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+        synchronized (mLock) {
+            mSurfaceTexture = surface;
+            mLock.notifyAll();
+        }
+    }
+
+    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+        // Ignored, Camera does all the work for us
+    }
+
+    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+        mSurfaceTexture = null;
+        mController.stopPreview();
+        Log.w(TAG, "surfaceTexture is destroyed");
+        return true;
+    }
+
+    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+        // Invoked every time there's a new Camera preview frame
+    }
+
+    public View getRootView() {
+        return mRootView;
+    }
+
+    private void initIndicators() {
+        mOnScreenIndicators = new OnScreenIndicators(mActivity,
+                mRootView.findViewById(R.id.on_screen_indicators));
+    }
+
+    public void onCameraOpened(PreferenceGroup prefGroup, ComboPreferences prefs,
+            Camera.Parameters params, OnPreferenceChangedListener listener) {
+        if (mPieRenderer == null) {
+            mPieRenderer = new PieRenderer(mActivity);
+            mPieRenderer.setPieListener(this);
+        }
+
+        if (mMenu == null) {
+            mMenu = new NewPhotoMenu(mActivity, this, mPieRenderer);
+            mMenu.setListener(listener);
+        }
+        mMenu.initialize(prefGroup);
+
+        if (mZoomRenderer == null) {
+            mZoomRenderer = new ZoomRenderer(mActivity);
+        }
+        mRenderOverlay.addRenderer(mPieRenderer);
+        mRenderOverlay.addRenderer(mZoomRenderer);
+
+        if (mGestures == null) {
+            // this will handle gesture disambiguation and dispatching
+            mGestures = new NewPreviewGestures(mActivity, this, mZoomRenderer, mPieRenderer,
+                    this);
+            mGestures.setCancelEventListener(this);
+        }
+        mGestures.clearTouchReceivers();
+        mGestures.setRenderOverlay(mRenderOverlay);
+        mGestures.addTouchReceiver(mMenuButton);
+        mGestures.addTouchReceiver(mBlocker);
+        // make sure to add touch targets for image capture
+        if (mController.isImageCaptureIntent()) {
+            if (mReviewCancelButton != null) {
+                mGestures.addTouchReceiver(mReviewCancelButton);
+            }
+            if (mReviewDoneButton != null) {
+                mGestures.addTouchReceiver(mReviewDoneButton);
+            }
+        }
+        mRenderOverlay.requestLayout();
+
+        initializeZoom(params);
+        updateOnScreenIndicators(params, prefGroup, prefs);
+    }
+
+    private void openMenu() {
+        if (mPieRenderer != null) {
+            // If autofocus is not finished, cancel autofocus so that the
+            // subsequent touch can be handled by PreviewGestures
+            if (mController.getCameraState() == PhotoController.FOCUSING) {
+                    mController.cancelAutoFocus();
+            }
+            mPieRenderer.showInCenter();
+        }
+    }
+
+    public void initializeControlByIntent() {
+        mBlocker = mRootView.findViewById(R.id.blocker);
+        mMenuButton = mRootView.findViewById(R.id.menu);
+        mMenuButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                openMenu();
+            }
+        });
+        if (mController.isImageCaptureIntent()) {
+            hideSwitcher();
+            ViewGroup cameraControls = (ViewGroup) mRootView.findViewById(R.id.camera_controls);
+            mActivity.getLayoutInflater().inflate(R.layout.review_module_control, cameraControls);
+
+            mReviewDoneButton = mRootView.findViewById(R.id.btn_done);
+            mReviewCancelButton = mRootView.findViewById(R.id.btn_cancel);
+            mReviewRetakeButton = mRootView.findViewById(R.id.btn_retake);
+            mReviewCancelButton.setVisibility(View.VISIBLE);
+
+            mReviewDoneButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mController.onCaptureDone();
+                }
+            });
+            mReviewCancelButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mController.onCaptureCancelled();
+                }
+            });
+
+            mReviewRetakeButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mController.onCaptureRetake();
+                }
+            });
+        }
+    }
+
+    public void hideUI() {
+        mCameraControls.setVisibility(View.INVISIBLE);
+        hideSwitcher();
+        mShutterButton.setVisibility(View.GONE);
+    }
+
+    public void showUI() {
+        mCameraControls.setVisibility(View.VISIBLE);
+        showSwitcher();
+        mShutterButton.setVisibility(View.VISIBLE);
+    }
+
+    public void hideSwitcher() {
+        mSwitcher.closePopup();
+        mSwitcher.setVisibility(View.INVISIBLE);
+    }
+
+    public void showSwitcher() {
+        mSwitcher.setVisibility(View.VISIBLE);
+    }
+
+    // called from onResume but only the first time
+    public  void initializeFirstTime() {
+        // Initialize shutter button.
+        mShutterButton.setImageResource(R.drawable.btn_new_shutter);
+        mShutterButton.setOnShutterButtonListener(mController);
+        mShutterButton.setVisibility(View.VISIBLE);
+    }
+
+    // called from onResume every other time
+    public void initializeSecondTime(Camera.Parameters params) {
+        initializeZoom(params);
+        if (mController.isImageCaptureIntent()) {
+            hidePostCaptureAlert();
+        }
+        if (mMenu != null) {
+            mMenu.reloadPreferences();
+        }
+    }
+
+    public void initializeZoom(Camera.Parameters params) {
+        if ((params == null) || !params.isZoomSupported()
+                || (mZoomRenderer == null)) return;
+        mZoomMax = params.getMaxZoom();
+        mZoomRatios = params.getZoomRatios();
+        // Currently we use immediate zoom for fast zooming to get better UX and
+        // there is no plan to take advantage of the smooth zoom.
+        if (mZoomRenderer != null) {
+            mZoomRenderer.setZoomMax(mZoomMax);
+            mZoomRenderer.setZoom(params.getZoom());
+            mZoomRenderer.setZoomValue(mZoomRatios.get(params.getZoom()));
+            mZoomRenderer.setOnZoomChangeListener(new ZoomChangeListener());
+        }
+    }
+
+    public void showGpsOnScreenIndicator(boolean hasSignal) { }
+
+    public void hideGpsOnScreenIndicator() { }
+
+    public void overrideSettings(final String ... keyvalues) {
+        mMenu.overrideSettings(keyvalues);
+    }
+
+    public void updateOnScreenIndicators(Camera.Parameters params,
+            PreferenceGroup group, ComboPreferences prefs) {
+        if (params == null) return;
+        mOnScreenIndicators.updateSceneOnScreenIndicator(params.getSceneMode());
+        mOnScreenIndicators.updateExposureOnScreenIndicator(params,
+                CameraSettings.readExposure(prefs));
+        mOnScreenIndicators.updateFlashOnScreenIndicator(params.getFlashMode());
+        int wbIndex = 2;
+        ListPreference pref = group.findPreference(CameraSettings.KEY_WHITE_BALANCE);
+        if (pref != null) {
+            wbIndex = pref.getCurrentIndex();
+        }
+        mOnScreenIndicators.updateWBIndicator(wbIndex);
+        boolean location = RecordLocationPreference.get(
+                prefs, mActivity.getContentResolver());
+        mOnScreenIndicators.updateLocationIndicator(location);
+    }
+
+    public void setCameraState(int state) {
+    }
+
+    // Gestures and touch events
+
+    public boolean dispatchTouchEvent(MotionEvent m) {
+        if (mPopup != null || mSwitcher.showsPopup()) {
+            boolean handled = mRootView.dispatchTouchEvent(m);
+            if (!handled && mPopup != null) {
+                dismissPopup();
+            }
+            return handled;
+        } else if (mGestures != null && mRenderOverlay != null) {
+            if (mGestures.dispatchTouch(m)) {
+                return true;
+            } else {
+                return mRootView.dispatchTouchEvent(m);
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public void onTouchEventCancelled(MotionEvent cancelEvent) {
+        mRootView.dispatchTouchEvent(cancelEvent);
+    }
+
+    public void enableGestures(boolean enable) {
+        if (mGestures != null) {
+            mGestures.setEnabled(enable);
+        }
+    }
+
+    // forward from preview gestures to controller
+    @Override
+    public void onSingleTapUp(View view, int x, int y) {
+        mController.onSingleTapUp(view, x, y);
+    }
+
+    public boolean onBackPressed() {
+        if (mPieRenderer != null && mPieRenderer.showsItems()) {
+            mPieRenderer.hide();
+            return true;
+        }
+        // In image capture mode, back button should:
+        // 1) if there is any popup, dismiss them, 2) otherwise, get out of
+        // image capture
+        if (mController.isImageCaptureIntent()) {
+            if (!removeTopLevelPopup()) {
+                // no popup to dismiss, cancel image capture
+                mController.onCaptureCancelled();
+            }
+            return true;
+        } else if (!mController.isCameraIdle()) {
+            // ignore backs while we're taking a picture
+            return true;
+        } else {
+            return removeTopLevelPopup();
+        }
+    }
+
+    public void onFullScreenChanged(boolean full) {
+        if (mFaceView != null) {
+            mFaceView.setBlockDraw(!full);
+        }
+        if (mPopup != null) {
+            dismissPopup(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();
+    }
+
+    public boolean removeTopLevelPopup() {
+        // Remove the top level popup or dialog box and return true if there's any
+        if (mPopup != null) {
+            dismissPopup();
+            return true;
+        }
+        return false;
+    }
+
+    public void showPopup(AbstractSettingPopup popup) {
+        hideUI();
+        mBlocker.setVisibility(View.INVISIBLE);
+        setShowMenu(false);
+        mPopup = popup;
+        mPopup.setVisibility(View.VISIBLE);
+        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
+                LayoutParams.WRAP_CONTENT);
+        lp.gravity = Gravity.CENTER;
+        ((FrameLayout) mRootView).addView(mPopup, lp);
+    }
+
+    public void dismissPopup() {
+        dismissPopup(true);
+    }
+
+    private void dismissPopup(boolean fullScreen) {
+        if (fullScreen) {
+            showUI();
+            mBlocker.setVisibility(View.VISIBLE);
+        }
+        setShowMenu(fullScreen);
+        if (mPopup != null) {
+            ((FrameLayout) mRootView).removeView(mPopup);
+            mPopup = null;
+        }
+        mMenu.popupDismissed();
+    }
+
+    public void onShowSwitcherPopup() {
+        if (mPieRenderer != null && mPieRenderer.showsItems()) {
+            mPieRenderer.hide();
+        }
+    }
+
+    private void setShowMenu(boolean show) {
+        if (mOnScreenIndicators != null) {
+            mOnScreenIndicators.setVisibility(show ? View.VISIBLE : View.GONE);
+        }
+        if (mMenuButton != null) {
+            mMenuButton.setVisibility(show ? View.VISIBLE : View.GONE);
+        }
+    }
+
+    public boolean collapseCameraControls() {
+        // Remove all the popups/dialog boxes
+        boolean ret = false;
+        if (mPopup != null) {
+            dismissPopup();
+            ret = true;
+        }
+        return ret;
+    }
+
+    protected void showPostCaptureAlert() {
+        mOnScreenIndicators.setVisibility(View.GONE);
+        mMenuButton.setVisibility(View.GONE);
+        Util.fadeIn(mReviewDoneButton);
+        mShutterButton.setVisibility(View.INVISIBLE);
+        Util.fadeIn(mReviewRetakeButton);
+    }
+
+    protected void hidePostCaptureAlert() {
+        mOnScreenIndicators.setVisibility(View.VISIBLE);
+        mMenuButton.setVisibility(View.VISIBLE);
+        Util.fadeOut(mReviewDoneButton);
+        mShutterButton.setVisibility(View.VISIBLE);
+        Util.fadeOut(mReviewRetakeButton);
+    }
+
+    public void setDisplayOrientation(int orientation) {
+        if (mFaceView != null) {
+            mFaceView.setDisplayOrientation(orientation);
+        }
+    }
+
+    // shutter button handling
+
+    public boolean isShutterPressed() {
+        return mShutterButton.isPressed();
+    }
+
+    public void enableShutter(boolean enabled) {
+        if (mShutterButton != null) {
+            mShutterButton.setEnabled(enabled);
+        }
+    }
+
+    public void pressShutterButton() {
+        if (mShutterButton.isInTouchMode()) {
+            mShutterButton.requestFocusFromTouch();
+        } else {
+            mShutterButton.requestFocus();
+        }
+        mShutterButton.setPressed(true);
+    }
+
+    private class ZoomChangeListener implements ZoomRenderer.OnZoomChangedListener {
+        @Override
+        public void onZoomValueChanged(int index) {
+            int newZoom = mController.onZoomChanged(index);
+            if (mZoomRenderer != null) {
+                mZoomRenderer.setZoomValue(mZoomRatios.get(newZoom));
+            }
+        }
+
+        @Override
+        public void onZoomStart() {
+            if (mPieRenderer != null) {
+                mPieRenderer.setBlockFocus(true);
+            }
+        }
+
+        @Override
+        public void onZoomEnd() {
+            if (mPieRenderer != null) {
+                mPieRenderer.setBlockFocus(false);
+            }
+        }
+    }
+
+    @Override
+    public void onPieOpened(int centerX, int centerY) {
+      //TODO:   mActivity.cancelActivityTouchHandling();
+      //TODO:    mActivity.setSwipingEnabled(false);
+        if (mFaceView != null) {
+            mFaceView.setBlockDraw(true);
+        }
+    }
+
+    @Override
+    public void onPieClosed() {
+      //TODO:     mActivity.setSwipingEnabled(true);
+        if (mFaceView != null) {
+            mFaceView.setBlockDraw(false);
+        }
+    }
+
+    public Object getSurfaceTexture() {
+        synchronized (mLock) {
+            if (mSurfaceTexture == null) {
+                try {
+                    mLock.wait();
+                } catch (InterruptedException e) {
+                    Log.w(TAG, "Unexpected interruption when waiting to get surface texture");
+                }
+            }
+        }
+        return mSurfaceTexture;
+    }
+
+    // Countdown timer
+
+    private void initializeCountDown() {
+        mActivity.getLayoutInflater().inflate(R.layout.count_down_to_capture,
+                (ViewGroup) mRootView, true);
+        mCountDownView = (CountDownView) (mRootView.findViewById(R.id.count_down_to_capture));
+        mCountDownView.setCountDownFinishedListener((OnCountDownFinishedListener) mController);
+    }
+
+    public boolean isCountingDown() {
+        return mCountDownView != null && mCountDownView.isCountingDown();
+    }
+
+    public void cancelCountDown() {
+        if (mCountDownView == null) return;
+        mCountDownView.cancelCountDown();
+    }
+
+    public void startCountDown(int sec, boolean playSound) {
+        if (mCountDownView == null) initializeCountDown();
+        mCountDownView.startCountDown(sec, playSound);
+    }
+
+    public void showPreferencesToast() {
+        if (mNotSelectableToast == null) {
+            String str = mActivity.getResources().getString(R.string.not_selectable_in_scene_mode);
+            mNotSelectableToast = Toast.makeText(mActivity, str, Toast.LENGTH_SHORT);
+        }
+        mNotSelectableToast.show();
+    }
+
+    public void onPause() {
+        cancelCountDown();
+
+        // Clear UI.
+        collapseCameraControls();
+        if (mFaceView != null) mFaceView.clear();
+
+        mPreviewWidth = 0;
+        mPreviewHeight = 0;
+    }
+
+    // focus UI implementation
+
+    private FocusIndicator getFocusIndicator() {
+        return (mFaceView != null && mFaceView.faceExists()) ? mFaceView : mPieRenderer;
+    }
+
+    @Override
+    public boolean hasFaces() {
+        return (mFaceView != null && mFaceView.faceExists());
+    }
+
+    public void clearFaces() {
+        if (mFaceView != null) mFaceView.clear();
+    }
+
+    @Override
+    public void clearFocus() {
+        FocusIndicator indicator = getFocusIndicator();
+        if (indicator != null) indicator.clear();
+    }
+
+    @Override
+    public void setFocusPosition(int x, int y) {
+        mPieRenderer.setFocus(x, y);
+    }
+
+    @Override
+    public void onFocusStarted() {
+        getFocusIndicator().showStart();
+    }
+
+    @Override
+    public void onFocusSucceeded(boolean timeout) {
+        getFocusIndicator().showSuccess(timeout);
+    }
+
+    @Override
+    public void onFocusFailed(boolean timeout) {
+        getFocusIndicator().showFail(timeout);
+    }
+
+    @Override
+    public void pauseFaceDetection() {
+        if (mFaceView != null) mFaceView.pause();
+    }
+
+    @Override
+    public void resumeFaceDetection() {
+        if (mFaceView != null) mFaceView.resume();
+    }
+
+    public void onStartFaceDetection(int orientation, boolean mirror) {
+        mFaceView.clear();
+        mFaceView.setVisibility(View.VISIBLE);
+        mFaceView.setDisplayOrientation(orientation);
+        mFaceView.setMirror(mirror);
+        mFaceView.resume();
+    }
+
+    @Override
+    public void onFaceDetection(Face[] faces, android.hardware.Camera camera) {
+        mFaceView.setFaces(faces);
+    }
+
+    @Override
+    public void onSwipe(int direction) {
+        if (direction == PreviewGestures.DIR_UP) {
+            openMenu();
+        }
+    }
+}
diff --git a/src/com/android/camera/NewPreviewGestures.java b/src/com/android/camera/NewPreviewGestures.java
new file mode 100644
index 0000000..2718e55
--- /dev/null
+++ b/src/com/android/camera/NewPreviewGestures.java
@@ -0,0 +1,358 @@
+package com.android.camera;
+
+/*
+ * 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.
+ */
+
+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.PreviewGestures.SwipeListener;
+import com.android.camera.ui.PieRenderer;
+import com.android.camera.ui.RenderOverlay;
+import com.android.camera.ui.ZoomRenderer;
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class NewPreviewGestures
+        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 static final int MODE_SWIPE = 5;
+
+    public static final int DIR_UP = 0;
+    public static final int DIR_DOWN = 1;
+    public static final int DIR_LEFT = 2;
+    public static final int DIR_RIGHT = 3;
+
+    private NewCameraActivity mActivity;
+    private SingleTapListener mTapListener;
+    private CancelEventListener mCancelEventListener;
+    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 SwipeListener mSwipeListener;
+
+    private Handler mHandler = new Handler() {
+        public void handleMessage(Message msg) {
+            if (msg.what == MSG_PIE) {
+                mMode = MODE_PIE;
+                openPie();
+                cancelActivityTouchHandling(mDown);
+            }
+        }
+    };
+
+    public interface SingleTapListener {
+        public void onSingleTapUp(View v, int x, int y);
+    }
+
+    public interface CancelEventListener {
+        public void onTouchEventCancelled(MotionEvent cancelEvent);
+    }
+
+    interface SwipeListener {
+        public void onSwipe(int direction);
+    }
+
+    public NewPreviewGestures(NewCameraActivity ctx, SingleTapListener tapListener,
+            ZoomRenderer zoom, PieRenderer pie, SwipeListener swipe) {
+        mActivity = ctx;
+        mTapListener = tapListener;
+        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];
+        mSwipeListener = swipe;
+    }
+
+    public void setCancelEventListener(CancelEventListener listener) {
+        mCancelEventListener = listener;
+    }
+
+    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 false;
+        }
+        mCurrent = m;
+        if (MotionEvent.ACTION_DOWN == m.getActionMasked()) {
+            if (checkReceivers(m)) {
+                mMode = MODE_MODULE;
+                return false;
+            } 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 false;
+            }
+        } else if (mMode == MODE_NONE) {
+            return false;
+        } else if (mMode == MODE_SWIPE) {
+            if (MotionEvent.ACTION_UP == m.getActionMasked()) {
+                mSwipeListener.onSwipe(getSwipeDirection(m));
+            }
+            return true;
+        } 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 false;
+        } 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) {
+                    mTapListener.onSingleTapUp(null,
+                            (int) mDown.getX() - mOverlay.getWindowPositionX(),
+                            (int) mDown.getY() - mOverlay.getWindowPositionY());
+                    return true;
+                } else {
+                    return false;
+                }
+            } 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();
+                    int dir = getSwipeDirection(m);
+                    if (dir == DIR_LEFT) {
+                        mMode = MODE_MODULE;
+                        return false;
+                    } else {
+                        cancelActivityTouchHandling(m);
+                        mMode = MODE_NONE;
+                    }
+                }
+            }
+            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 int getSwipeDirection(MotionEvent m) {
+        float dx = 0;
+        float dy = 0;
+        switch (mOrientation) {
+        case 0:
+            dx = m.getX() - mDown.getX();
+            dy = m.getY() - mDown.getY();
+            break;
+        case 90:
+            dx = - (m.getY() - mDown.getY());
+            dy = m.getX() - mDown.getX();
+            break;
+        case 180:
+            dx = -(m.getX() - mDown.getX());
+            dy = m.getY() - mDown.getY();
+            break;
+        case 270:
+            dx = m.getY() - mDown.getY();
+            dy = m.getX() - mDown.getX();
+            break;
+        }
+        if (dx < 0 && (Math.abs(dy) / -dx < 2)) return DIR_LEFT;
+        if (dx > 0 && (Math.abs(dy) / dx < 2)) return DIR_RIGHT;
+        if (dy > 0) return DIR_DOWN;
+        return DIR_UP;
+    }
+
+    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) {
+        if (mCancelEventListener != null) {
+            mCancelEventListener.onTouchEventCancelled(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/NewVideoMenu.java b/src/com/android/camera/NewVideoMenu.java
new file mode 100644
index 0000000..1038628
--- /dev/null
+++ b/src/com/android/camera/NewVideoMenu.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.LayoutInflater;
+
+import com.android.camera.ui.AbstractSettingPopup;
+import com.android.camera.ui.ListPrefSettingPopup;
+import com.android.camera.ui.MoreSettingPopup;
+import com.android.camera.ui.PieItem;
+import com.android.camera.ui.PieItem.OnClickListener;
+import com.android.camera.ui.PieRenderer;
+import com.android.camera.ui.TimeIntervalPopup;
+import com.android.gallery3d.R;
+
+public class NewVideoMenu extends PieController
+        implements MoreSettingPopup.Listener,
+        ListPrefSettingPopup.Listener,
+        TimeIntervalPopup.Listener {
+
+    private static String TAG = "CAM_VideoMenu";
+    private static final int POS_WB = 1;
+    private static final int POS_SET = 2;
+    private static final int POS_FLASH = 3;
+    private static final int POS_SWITCH = 4;
+
+    private NewVideoUI mUI;
+    private String[] mOtherKeys;
+    private AbstractSettingPopup mPopup;
+
+    private static final int POPUP_NONE = 0;
+    private static final int POPUP_FIRST_LEVEL = 1;
+    private static final int POPUP_SECOND_LEVEL = 2;
+    private int mPopupStatus;
+    private NewCameraActivity mActivity;
+
+    public NewVideoMenu(NewCameraActivity activity, NewVideoUI ui, PieRenderer pie) {
+        super(activity, pie);
+        mUI = ui;
+        mActivity = activity;
+    }
+
+
+    public void initialize(PreferenceGroup group) {
+        super.initialize(group);
+        mPopup = null;
+        mPopupStatus = POPUP_NONE;
+        PieItem item = null;
+        // white balance
+        if (group.findPreference(CameraSettings.KEY_WHITE_BALANCE) != null) {
+            item = makeItem(CameraSettings.KEY_WHITE_BALANCE);
+            mRenderer.addItem(item);
+        }
+        // settings popup
+        mOtherKeys = new String[] {
+                CameraSettings.KEY_VIDEO_EFFECT,
+                CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL,
+                CameraSettings.KEY_VIDEO_QUALITY,
+                CameraSettings.KEY_RECORD_LOCATION
+        };
+        item = makeItem(R.drawable.ic_settings_holo_light);
+        item.setLabel(mActivity.getResources().getString(R.string.camera_menu_settings_label));
+        item.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(PieItem item) {
+                if (mPopup == null || mPopupStatus != POPUP_FIRST_LEVEL) {
+                    initializePopup();
+                    mPopupStatus = POPUP_FIRST_LEVEL;
+                }
+                mUI.showPopup(mPopup);
+            }
+        });
+        mRenderer.addItem(item);
+        // camera switcher
+        if (group.findPreference(CameraSettings.KEY_CAMERA_ID) != null) {
+            item = makeItem(R.drawable.ic_switch_back);
+            IconListPreference lpref = (IconListPreference) group.findPreference(
+                    CameraSettings.KEY_CAMERA_ID);
+            item.setLabel(lpref.getLabel());
+            item.setImageResource(mActivity,
+                    ((IconListPreference) lpref).getIconIds()
+                    [lpref.findIndexOfValue(lpref.getValue())]);
+
+            final PieItem fitem = item;
+            item.setOnClickListener(new OnClickListener() {
+
+                @Override
+                public void onClick(PieItem item) {
+                    // Find the index of next camera.
+                    ListPreference pref =
+                            mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID);
+                    if (pref != null) {
+                        int index = pref.findIndexOfValue(pref.getValue());
+                        CharSequence[] values = pref.getEntryValues();
+                        index = (index + 1) % values.length;
+                        int newCameraId = Integer.parseInt((String) values[index]);
+                        fitem.setImageResource(mActivity,
+                                ((IconListPreference) pref).getIconIds()[index]);
+                        fitem.setLabel(pref.getLabel());
+                        mListener.onCameraPickerClicked(newCameraId);
+                    }
+                }
+            });
+            mRenderer.addItem(item);
+        }
+        // flash
+        if (group.findPreference(CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE) != null) {
+            item = makeItem(CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE);
+            mRenderer.addItem(item);
+        }
+    }
+
+    @Override
+    public void reloadPreferences() {
+        super.reloadPreferences();
+        if (mPopup != null) {
+            mPopup.reloadPreference();
+        }
+    }
+
+    @Override
+    public void overrideSettings(final String ... keyvalues) {
+        super.overrideSettings(keyvalues);
+        if (mPopup == null || mPopupStatus != POPUP_FIRST_LEVEL) {
+            mPopupStatus = POPUP_FIRST_LEVEL;
+            initializePopup();
+        }
+        ((MoreSettingPopup) mPopup).overrideSettings(keyvalues);
+    }
+
+    @Override
+    // Hit when an item in the second-level popup gets selected
+    public void onListPrefChanged(ListPreference pref) {
+        if (mPopup != null) {
+            if (mPopupStatus == POPUP_SECOND_LEVEL) {
+                mUI.dismissPopup(true);
+            }
+        }
+        super.onSettingChanged(pref);
+    }
+
+    protected void initializePopup() {
+        LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+
+        MoreSettingPopup popup = (MoreSettingPopup) inflater.inflate(
+                R.layout.more_setting_popup, null, false);
+        popup.setSettingChangedListener(this);
+        popup.initialize(mPreferenceGroup, mOtherKeys);
+        if (mActivity.isSecureCamera()) {
+            // Prevent location preference from getting changed in secure camera mode
+            popup.setPreferenceEnabled(CameraSettings.KEY_RECORD_LOCATION, false);
+        }
+        mPopup = popup;
+    }
+
+    public void popupDismissed(boolean topPopupOnly) {
+        // if the 2nd level popup gets dismissed
+        if (mPopupStatus == POPUP_SECOND_LEVEL) {
+            initializePopup();
+            mPopupStatus = POPUP_FIRST_LEVEL;
+            if (topPopupOnly) mUI.showPopup(mPopup);
+        }
+    }
+
+    @Override
+    // Hit when an item in the first-level popup gets selected, then bring up
+    // the second-level popup
+    public void onPreferenceClicked(ListPreference pref) {
+        if (mPopupStatus != POPUP_FIRST_LEVEL) return;
+
+        LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+
+        if (CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL.equals(pref.getKey())) {
+            TimeIntervalPopup timeInterval = (TimeIntervalPopup) inflater.inflate(
+                    R.layout.time_interval_popup, null, false);
+            timeInterval.initialize((IconListPreference) pref);
+            timeInterval.setSettingChangedListener(this);
+            mUI.dismissPopup(true);
+            mPopup = timeInterval;
+        } else {
+            ListPrefSettingPopup basic = (ListPrefSettingPopup) inflater.inflate(
+                    R.layout.list_pref_setting_popup, null, false);
+            basic.initialize(pref);
+            basic.setSettingChangedListener(this);
+            mUI.dismissPopup(true);
+            mPopup = basic;
+        }
+        mUI.showPopup(mPopup);
+        mPopupStatus = POPUP_SECOND_LEVEL;
+    }
+}
diff --git a/src/com/android/camera/NewVideoModule.java b/src/com/android/camera/NewVideoModule.java
new file mode 100644
index 0000000..54e3373
--- /dev/null
+++ b/src/com/android/camera/NewVideoModule.java
@@ -0,0 +1,2352 @@
+/*
+ * 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.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.KeyEvent;
+import android.view.MotionEvent;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import com.android.camera.CameraManager.CameraProxy;
+import com.android.camera.ui.PopupManager;
+import com.android.camera.ui.RotateTextToast;
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.OrientationManager;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.util.AccessibilityUtils;
+import com.android.gallery3d.util.UsageStatistics;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+
+public class NewVideoModule implements NewCameraModule,
+    VideoController,
+    CameraPreference.OnPreferenceChangedListener,
+    ShutterButton.OnShutterButtonListener,
+    MediaRecorder.OnErrorListener,
+    MediaRecorder.OnInfoListener,
+    EffectsRecorder.EffectsListener {
+
+    private static final String TAG = "CAM_VideoModule";
+
+    // We number the request code from 1000 to avoid collision with Gallery.
+    private static final int REQUEST_EFFECT_BACKDROPPER = 1000;
+
+    private static final int CHECK_DISPLAY_ROTATION = 3;
+    private static final int CLEAR_SCREEN_DELAY = 4;
+    private static final int UPDATE_RECORD_TIME = 5;
+    private static final int ENABLE_SHUTTER_BUTTON = 6;
+    private static final int SHOW_TAP_TO_SNAPSHOT_TOAST = 7;
+    private static final int SWITCH_CAMERA = 8;
+    private static final int SWITCH_CAMERA_START_ANIMATION = 9;
+    private static final int HIDE_SURFACE_VIEW = 10;
+
+    private static final int 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 NewCameraActivity mActivity;
+    private boolean mPaused;
+    private int mCameraId;
+    private Parameters mParameters;
+
+    private Boolean mCameraOpened = false;
+    private boolean mIsInReviewMode;
+    private boolean mSnapshotInProgress = false;
+
+    private static final String EFFECT_BG_FROM_GALLERY = "gallery";
+
+    private final CameraErrorCallback mErrorCallback = new CameraErrorCallback();
+
+    private ComboPreferences mPreferences;
+    private PreferenceGroup mPreferenceGroup;
+
+    private boolean mIsVideoCaptureIntent;
+    private boolean mQuickCapture;
+
+    private MediaRecorder mMediaRecorder;
+    private EffectsRecorder mEffectsRecorder;
+    private boolean mEffectsDisplayResult;
+
+    private int mEffectType = EffectsRecorder.EFFECT_NONE;
+    private Object mEffectParameter = null;
+    private String mEffectUriFromGallery = null;
+    private String mPrefVideoEffectDefault;
+    private boolean mResetEffect = true;
+
+    private boolean mSwitchingCamera;
+    private boolean mMediaRecorderRecording = false;
+    private long mRecordingStartTime;
+    private boolean mRecordingTimeCountsDown = false;
+    private long mOnResumeTime;
+    // The video file that the hardware camera is about to record into
+    // (or is recording into.)
+    private String mVideoFilename;
+    private ParcelFileDescriptor mVideoFileDescriptor;
+
+    // The video file that has already been recorded, and that is being
+    // examined by the user.
+    private String mCurrentVideoFilename;
+    private Uri mCurrentVideoUri;
+    private ContentValues mCurrentVideoValues;
+
+    private CamcorderProfile mProfile;
+
+    // The video duration limit. 0 menas no limit.
+    private int mMaxVideoDurationInMs;
+
+    // Time Lapse parameters.
+    private boolean mCaptureTimeLapse = false;
+    // Default 0. If it is larger than 0, the camcorder is in time lapse mode.
+    private int mTimeBetweenTimeLapseFrameCaptureMs = 0;
+
+    boolean mPreviewing = false; // True if preview is started.
+    // The display rotation in degrees. This is only valid when mPreviewing is
+    // true.
+    private int mDisplayRotation;
+    private int mCameraDisplayOrientation;
+
+    private int mDesiredPreviewWidth;
+    private int mDesiredPreviewHeight;
+    private ContentResolver mContentResolver;
+
+    private LocationManager mLocationManager;
+    private OrientationManager mOrientationManager;
+
+    private VideoNamer mVideoNamer;
+    private Surface mSurface;
+    private int mPendingSwitchCameraId;
+    private boolean mOpenCameraFail;
+    private boolean mCameraDisabled;
+    private final Handler mHandler = new MainHandler();
+    private NewVideoUI mUI;
+    private CameraProxy mCameraDevice;
+
+    // The degrees of the device rotated clockwise from its natural orientation.
+    private int mOrientation = OrientationEventListener.ORIENTATION_UNKNOWN;
+
+    private int mZoomValue;  // The current zoom value.
+
+    private boolean mRestoreFlash;  // This is used to check if we need to restore the flash
+                                    // status when going back from gallery.
+
+    private MediaSaveService.OnMediaSavedListener mOnMediaSavedListener =
+            new MediaSaveService.OnMediaSavedListener() {
+                @Override
+                public void onMediaSaved(Uri uri) {
+                    if (uri != null) {
+                        Util.broadcastNewPicture(mActivity, uri);
+                    }
+                }
+            };
+
+
+    protected class CameraOpenThread extends Thread {
+        @Override
+        public void run() {
+            openCamera();
+        }
+    }
+
+    private void openCamera() {
+        try {
+            synchronized(mCameraOpened) {
+                if (!mCameraOpened) {
+                    mCameraDevice = Util.openCamera(mActivity, mCameraId);
+                    mCameraOpened = true;
+                }
+            }
+            mParameters = mCameraDevice.getParameters();
+        } catch (CameraHardwareException e) {
+            mOpenCameraFail = true;
+        } catch (CameraDisabledException e) {
+            mCameraDisabled = true;
+        }
+    }
+
+    // This Handler is used to post message back onto the main thread of the
+    // application
+    private class MainHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+
+                case ENABLE_SHUTTER_BUTTON:
+                    mUI.enableShutter(true);
+                    break;
+
+                case CLEAR_SCREEN_DELAY: {
+                    mActivity.getWindow().clearFlags(
+                            WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+                    break;
+                }
+
+                case UPDATE_RECORD_TIME: {
+                    updateRecordingTime();
+                    break;
+                }
+
+                case CHECK_DISPLAY_ROTATION: {
+                    // Restart the preview if display rotation has changed.
+                    // Sometimes this happens when the device is held upside
+                    // down and camera app is opened. Rotation animation will
+                    // take some time and the rotation value we have got may be
+                    // wrong. Framework does not have a callback for this now.
+                    if ((Util.getDisplayRotation(mActivity) != mDisplayRotation)
+                            && !mMediaRecorderRecording && !mSwitchingCamera) {
+                        startPreview();
+                    }
+                    if (SystemClock.uptimeMillis() - mOnResumeTime < 5000) {
+                        mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100);
+                    }
+                    break;
+                }
+
+                case SHOW_TAP_TO_SNAPSHOT_TOAST: {
+                    showTapToSnapshotToast();
+                    break;
+                }
+
+                case SWITCH_CAMERA: {
+                    switchCamera();
+                    break;
+                }
+
+                case SWITCH_CAMERA_START_ANIMATION: {
+                    //TODO:
+                    //((CameraScreenNail) mActivity.mCameraScreenNail).animateSwitchCamera();
+
+                    // Enable all camera controls.
+                    mSwitchingCamera = false;
+                    break;
+                }
+
+                default:
+                    Log.v(TAG, "Unhandled message: " + msg.what);
+                    break;
+            }
+        }
+    }
+
+    private BroadcastReceiver mReceiver = null;
+
+    private class MyBroadcastReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
+                stopVideoRecording();
+            } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) {
+                Toast.makeText(mActivity,
+                        mActivity.getResources().getString(R.string.wait), Toast.LENGTH_LONG).show();
+            }
+        }
+    }
+
+    private String createName(long dateTaken) {
+        Date date = new Date(dateTaken);
+        SimpleDateFormat dateFormat = new SimpleDateFormat(
+                mActivity.getString(R.string.video_file_name_format));
+
+        return dateFormat.format(date);
+    }
+
+    private int getPreferredCameraId(ComboPreferences preferences) {
+        int intentCameraId = Util.getCameraFacingIntentExtras(mActivity);
+        if (intentCameraId != -1) {
+            // Testing purpose. Launch a specific camera through the intent
+            // extras.
+            return intentCameraId;
+        } else {
+            return CameraSettings.readPreferredCameraId(preferences);
+        }
+    }
+
+    private void initializeSurfaceView() {
+        if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {  // API level < 16
+            mUI.initializeSurfaceView();
+        }
+    }
+
+    @Override
+    public void init(NewCameraActivity activity, View root) {
+        mActivity = activity;
+        mUI = new NewVideoUI(activity, this, root);
+        mPreferences = new ComboPreferences(mActivity);
+        CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal());
+        mCameraId = getPreferredCameraId(mPreferences);
+
+        mPreferences.setLocalId(mActivity, mCameraId);
+        CameraSettings.upgradeLocalPreferences(mPreferences.getLocal());
+
+        mPrefVideoEffectDefault = mActivity.getString(R.string.pref_video_effect_default);
+        resetEffect();
+        mOrientationManager = new OrientationManager(mActivity);
+
+        /*
+         * To reduce startup time, we start the preview in another thread.
+         * We make sure the preview is started at the end of onCreate.
+         */
+        CameraOpenThread cameraOpenThread = new CameraOpenThread();
+        cameraOpenThread.start();
+
+        mContentResolver = mActivity.getContentResolver();
+
+        // Surface texture is from camera screen nail and startPreview needs it.
+        // This must be done before startPreview.
+        mIsVideoCaptureIntent = isVideoCaptureIntent();
+        initializeSurfaceView();
+
+        // Make sure camera device is opened.
+        try {
+            cameraOpenThread.join();
+            if (mOpenCameraFail) {
+                Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera);
+                return;
+            } else if (mCameraDisabled) {
+                Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
+                return;
+            }
+        } catch (InterruptedException ex) {
+            // ignore
+        }
+
+        readVideoPreferences();
+        mUI.setPrefChangedListener(this);
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                startPreview();
+            }
+        }).start();
+
+        mQuickCapture = mActivity.getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false);
+        mLocationManager = new LocationManager(mActivity, null);
+
+        mUI.setOrientationIndicator(0, false);
+        setDisplayOrientation();
+
+        mUI.showTimeLapseUI(mCaptureTimeLapse);
+        initializeVideoSnapshot();
+        resizeForPreviewAspectRatio();
+
+        initializeVideoControl();
+        mPendingSwitchCameraId = -1;
+        mUI.updateOnScreenIndicators(mParameters);
+
+        // Disable the shutter button if effects are ON since it might take
+        // a little more time for the effects preview to be ready. We do not
+        // want to allow recording before that happens. The shutter button
+        // will be enabled when we get the message from effectsrecorder that
+        // the preview is running. This becomes critical when the camera is
+        // swapped.
+        if (effectsActive()) {
+            mUI.enableShutter(false);
+        }
+    }
+
+    // SingleTapListener
+    // Preview area is touched. Take a picture.
+    @Override
+    public void onSingleTapUp(View view, int x, int y) {
+        if (mMediaRecorderRecording && effectsActive()) {
+            new RotateTextToast(mActivity, R.string.disable_video_snapshot_hint,
+                    mOrientation).show();
+            return;
+        }
+
+        MediaSaveService s = mActivity.getMediaSaveService();
+        if (mPaused || mSnapshotInProgress || effectsActive() || s == null || s.isQueueFull()) {
+            return;
+        }
+
+        if (!mMediaRecorderRecording) {
+            // check for dismissing popup
+            mUI.dismissPopup(true);
+            return;
+        }
+
+        // Set rotation and gps data.
+        int rotation = Util.getJpegRotation(mCameraId, mOrientation);
+        mParameters.setRotation(rotation);
+        Location loc = mLocationManager.getCurrentLocation();
+        Util.setGpsParameters(mParameters, loc);
+        mCameraDevice.setParameters(mParameters);
+
+        Log.v(TAG, "Video snapshot start");
+        mCameraDevice.takePicture(null, null, null, new JpegPictureCallback(loc));
+        showVideoSnapshotUI(true);
+        mSnapshotInProgress = true;
+        UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
+                UsageStatistics.ACTION_CAPTURE_DONE, "VideoSnapshot");
+    }
+
+    @Override
+    public void onStop() {}
+
+    private void loadCameraPreferences() {
+        CameraSettings settings = new CameraSettings(mActivity, mParameters,
+                mCameraId, CameraHolder.instance().getCameraInfo());
+        // Remove the video quality preference setting when the quality is given in the intent.
+        mPreferenceGroup = filterPreferenceScreenByIntent(
+                settings.getPreferenceGroup(R.xml.video_preferences));
+    }
+
+    private void initializeVideoControl() {
+        loadCameraPreferences();
+        mUI.initializePopup(mPreferenceGroup);
+        if (effectsActive()) {
+            mUI.overrideSettings(
+                    CameraSettings.KEY_VIDEO_QUALITY,
+                    Integer.toString(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 startPlayVideoActivity() {
+        Intent intent = new Intent(Intent.ACTION_VIEW);
+        intent.setDataAndType(mCurrentVideoUri, convertOutputFormatToMimeType(mProfile.fileFormat));
+        try {
+            mActivity.startActivity(intent);
+        } catch (ActivityNotFoundException ex) {
+            Log.e(TAG, "Couldn't view video " + mCurrentVideoUri, ex);
+        }
+    }
+
+    @OnClickAttr
+    public void onReviewPlayClicked(View v) {
+        startPlayVideoActivity();
+    }
+
+    @OnClickAttr
+    public void onReviewDoneClicked(View v) {
+        mIsInReviewMode = false;
+        doReturnToCaller(true);
+    }
+
+    @OnClickAttr
+    public void onReviewCancelClicked(View v) {
+        mIsInReviewMode = false;
+        stopVideoRecording();
+        doReturnToCaller(false);
+    }
+
+    @Override
+    public boolean isInReviewMode() {
+        return mIsInReviewMode;
+    }
+
+    private void onStopVideoRecording() {
+        mEffectsDisplayResult = true;
+        boolean recordFail = stopVideoRecording();
+        if (mIsVideoCaptureIntent) {
+            if (!effectsActive()) {
+                if (mQuickCapture) {
+                    doReturnToCaller(!recordFail);
+                } else if (!recordFail) {
+                    showCaptureResult();
+                }
+            }
+        } else if (!recordFail){
+            // Start capture animation.
+            if (!mPaused && ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
+                // The capture animation is disabled on ICS because we use SurfaceView
+                // for preview during recording. When the recording is done, we switch
+                // back to use SurfaceTexture for preview and we need to stop then start
+                // the preview. This will cause the preview flicker since the preview
+                // will not be continuous for a short period of time.
+                // TODO: need to get the capture animation to work 
+                // ((CameraScreenNail) mActivity.mCameraScreenNail).animateCapture(mDisplayRotation);
+            }
+        }
+    }
+
+    public void onProtectiveCurtainClick(View v) {
+        // Consume clicks
+    }
+
+    @Override
+    public void onShutterButtonClick() {
+        if (mUI.collapseCameraControls() || mSwitchingCamera) return;
+
+        boolean stop = mMediaRecorderRecording;
+
+        if (stop) {
+            onStopVideoRecording();
+        } else {
+            startVideoRecording();
+        }
+        mUI.enableShutter(false);
+
+        // Keep the shutter button disabled when in video capture intent
+        // mode and recording is stopped. It'll be re-enabled when
+        // re-take button is clicked.
+        if (!(mIsVideoCaptureIntent && stop)) {
+            mHandler.sendEmptyMessageDelayed(
+                    ENABLE_SHUTTER_BUTTON, SHUTTER_BUTTON_TIMEOUT);
+        }
+    }
+
+    @Override
+    public void onShutterButtonFocus(boolean pressed) {
+        mUI.setShutterPressed(pressed);
+    }
+
+    private void readVideoPreferences() {
+        // The preference stores values from ListPreference and is thus string type for all values.
+        // We need to convert it to int manually.
+        String 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 = mCameraDevice.getParameters();
+        if (ApiHelper.HAS_GET_SUPPORTED_VIDEO_SIZE) {
+            if (mParameters.getSupportedVideoSizes() == null || effectsActive()) {
+                mDesiredPreviewWidth = mProfile.videoFrameWidth;
+                mDesiredPreviewHeight = mProfile.videoFrameHeight;
+            } else {  // Driver supports separates outputs for preview and video.
+                List<Size> sizes = mParameters.getSupportedPreviewSizes();
+                Size preferred = mParameters.getPreferredPreviewSizeForVideo();
+                int product = preferred.width * preferred.height;
+                Iterator<Size> it = sizes.iterator();
+                // Remove the preview sizes that are not preferred.
+                while (it.hasNext()) {
+                    Size size = it.next();
+                    if (size.width * size.height > product) {
+                        it.remove();
+                    }
+                }
+                Size optimalSize = Util.getOptimalPreviewSize(mActivity, sizes,
+                        (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight);
+                mDesiredPreviewWidth = optimalSize.width;
+                mDesiredPreviewHeight = optimalSize.height;
+            }
+        } else {
+            mDesiredPreviewWidth = mProfile.videoFrameWidth;
+            mDesiredPreviewHeight = mProfile.videoFrameHeight;
+        }
+        mUI.setPreviewSize(mDesiredPreviewWidth, mDesiredPreviewHeight);
+        Log.v(TAG, "mDesiredPreviewWidth=" + mDesiredPreviewWidth +
+                ". mDesiredPreviewHeight=" + mDesiredPreviewHeight);
+    }
+
+    private void resizeForPreviewAspectRatio() {
+        mUI.setAspectRatio(
+                (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight);
+    }
+
+    @Override
+    public void installIntentFilter() {
+        // install an intent filter to receive SD card related events.
+        IntentFilter intentFilter =
+                new IntentFilter(Intent.ACTION_MEDIA_EJECT);
+        intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
+        intentFilter.addDataScheme("file");
+        mReceiver = new MyBroadcastReceiver();
+        mActivity.registerReceiver(mReceiver, intentFilter);
+    }
+
+    @Override
+    public void onResumeBeforeSuper() {
+        mPaused = false;
+    }
+
+    @Override
+    public void onResumeAfterSuper() {
+        if (mOpenCameraFail || mCameraDisabled)
+            return;
+        mUI.enableShutter(false);
+        mZoomValue = 0;
+
+        showVideoSnapshotUI(false);
+
+        if (!mPreviewing) {
+            resetEffect();
+            openCamera();
+            if (mOpenCameraFail) {
+                Util.showErrorAndFinish(mActivity,
+                        R.string.cannot_connect_camera);
+                return;
+            } else if (mCameraDisabled) {
+                Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
+                return;
+            }
+            readVideoPreferences();
+            resizeForPreviewAspectRatio();
+            new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    startPreview();
+                }
+            }).start();
+        } else {
+            // preview already started
+            mUI.enableShutter(true);
+        }
+
+        // Initializing it here after the preview is started.
+        mUI.initializeZoom(mParameters);
+
+        keepScreenOnAwhile();
+
+        // Initialize location service.
+        boolean recordLocation = RecordLocationPreference.get(mPreferences,
+                mContentResolver);
+        mLocationManager.recordLocation(recordLocation);
+
+        if (mPreviewing) {
+            mOnResumeTime = SystemClock.uptimeMillis();
+            mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100);
+        }
+        // Dismiss open menu if exists.
+        PopupManager.getInstance(mActivity).notifyShowPopup(null);
+
+        mVideoNamer = new VideoNamer();
+        UsageStatistics.onContentViewChanged(
+                UsageStatistics.COMPONENT_CAMERA, "VideoModule");
+    }
+
+    private void setDisplayOrientation() {
+        mDisplayRotation = Util.getDisplayRotation(mActivity);
+        mCameraDisplayOrientation = Util.getDisplayOrientation(mDisplayRotation, mCameraId);
+        // Change the camera display orientation
+        if (mCameraDevice != null) {
+            mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation);
+        }
+    }
+
+    @Override
+    public int onZoomChanged(int index) {
+        // Not useful to change zoom value when the activity is paused.
+        if (mPaused) return index;
+        mZoomValue = index;
+        if (mParameters == null || mCameraDevice == null) return index;
+        // Set zoom parameters asynchronously
+        mParameters.setZoom(mZoomValue);
+        mCameraDevice.setParameters(mParameters);
+        Parameters p = mCameraDevice.getParameters();
+        if (p != null) return p.getZoom();
+        return index;
+    }
+    private void startPreview() {
+        Log.v(TAG, "startPreview");
+
+        mCameraDevice.setErrorCallback(mErrorCallback);
+        if (mPreviewing == true) {
+            stopPreview();
+            if (effectsActive() && mEffectsRecorder != null) {
+                mEffectsRecorder.release();
+                mEffectsRecorder = null;
+            }
+        }
+
+        setDisplayOrientation();
+        mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation);
+        setCameraParameters();
+
+        try {
+            if (!effectsActive()) {
+                SurfaceTexture surfaceTexture = mUI.getSurfaceTexture();
+                if (surfaceTexture == null) {
+                    return; // The texture has been destroyed (pause, etc)
+                }
+                mCameraDevice.setPreviewTextureAsync(surfaceTexture);
+                mCameraDevice.startPreviewAsync();
+                mPreviewing = true;
+                onPreviewStarted();
+            } else {
+                initializeEffectsPreview();
+                mEffectsRecorder.startPreview();
+                mPreviewing = true;
+                onPreviewStarted();
+            }
+        } catch (Throwable ex) {
+            closeCamera();
+            throw new RuntimeException("startPreview failed", ex);
+        } finally {
+            mActivity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    if (mOpenCameraFail) {
+                        Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera);
+                    } else if (mCameraDisabled) {
+                        Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
+                    }
+                }
+            });
+        }
+
+    }
+
+    private void onPreviewStarted() {
+        mUI.enableShutter(true);
+    }
+
+    @Override
+    public void stopPreview() {
+        if (!mPreviewing) return;
+        mCameraDevice.stopPreview();
+        mPreviewing = false;
+    }
+
+    // Closing the effects out. Will shut down the effects graph.
+    private void closeEffects() {
+        Log.v(TAG, "Closing effects");
+        mEffectType = EffectsRecorder.EFFECT_NONE;
+        if (mEffectsRecorder == null) {
+            Log.d(TAG, "Effects are already closed. Nothing to do");
+            return;
+        }
+        // This call can handle the case where the camera is already released
+        // after the recording has been stopped.
+        mEffectsRecorder.release();
+        mEffectsRecorder = null;
+    }
+
+    // By default, we want to close the effects as well with the camera.
+    private void closeCamera() {
+        closeCamera(true);
+    }
+
+    // In certain cases, when the effects are active, we may want to shutdown
+    // only the camera related parts, and handle closing the effects in the
+    // effectsUpdate callback.
+    // For example, in onPause, we want to make the camera available to
+    // outside world immediately, however, want to wait till the effects
+    // callback to shut down the effects. In such a case, we just disconnect
+    // the effects from the camera by calling disconnectCamera. That way
+    // the effects can handle that when shutting down.
+    //
+    // @param closeEffectsAlso - indicates whether we want to close the
+    // effects also along with the camera.
+    private void closeCamera(boolean closeEffectsAlso) {
+        Log.v(TAG, "closeCamera");
+        if (mCameraDevice == null) {
+            Log.d(TAG, "already stopped.");
+            return;
+        }
+
+        if (mEffectsRecorder != null) {
+            // Disconnect the camera from effects so that camera is ready to
+            // be released to the outside world.
+            mEffectsRecorder.disconnectCamera();
+        }
+        if (closeEffectsAlso) closeEffects();
+        mCameraDevice.setZoomChangeListener(null);
+        mCameraDevice.setErrorCallback(null);
+        synchronized(mCameraOpened) {
+            if (mCameraOpened) {
+                CameraHolder.instance().release();
+            }
+            mCameraOpened = false;
+        }
+        mCameraDevice = null;
+        mPreviewing = false;
+        mSnapshotInProgress = false;
+    }
+
+    private void releasePreviewResources() {
+        if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
+            mUI.hideSurfaceView();
+        }
+    }
+
+    @Override
+    public void onPauseBeforeSuper() {
+        mPaused = true;
+
+        if (mMediaRecorderRecording) {
+            // Camera will be released in onStopVideoRecording.
+            onStopVideoRecording();
+        } else {
+            closeCamera();
+            if (!effectsActive()) releaseMediaRecorder();
+        }
+        if (effectsActive()) {
+            // If the effects are active, make sure we tell the graph that the
+            // surfacetexture is not valid anymore. Disconnect the graph from
+            // the display. This should be done before releasing the surface
+            // texture.
+            mEffectsRecorder.disconnectDisplay();
+        } else {
+            // Close the file descriptor and clear the video namer only if the
+            // effects are not active. If effects are active, we need to wait
+            // till we get the callback from the Effects that the graph is done
+            // recording. That also needs a change in the stopVideoRecording()
+            // call to not call closeCamera if the effects are active, because
+            // that will close down the effects are well, thus making this if
+            // condition invalid.
+            closeVideoFileDescriptor();
+            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 (mUI.hidePieRenderer()) {
+            return true;
+        } else {
+            return mUI.removeTopLevelPopup();
+        }
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        // Do not handle any key if the activity is paused.
+        if (mPaused) {
+            return true;
+        }
+
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_CAMERA:
+                if (event.getRepeatCount() == 0) {
+                    mUI.clickShutter();
+                    return true;
+                }
+                break;
+            case KeyEvent.KEYCODE_DPAD_CENTER:
+                if (event.getRepeatCount() == 0) {
+                    mUI.clickShutter();
+                    return true;
+                }
+                break;
+            case KeyEvent.KEYCODE_MENU:
+                if (mMediaRecorderRecording) return true;
+                break;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_CAMERA:
+                mUI.pressShutter(false);
+                return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean isVideoCaptureIntent() {
+        String action = mActivity.getIntent().getAction();
+        return (MediaStore.ACTION_VIDEO_CAPTURE.equals(action));
+    }
+
+    private void doReturnToCaller(boolean valid) {
+        Intent resultIntent = new Intent();
+        int resultCode;
+        if (valid) {
+            resultCode = Activity.RESULT_OK;
+            resultIntent.setData(mCurrentVideoUri);
+        } else {
+            resultCode = Activity.RESULT_CANCELED;
+        }
+        mActivity.setResultEx(resultCode, resultIntent);
+        mActivity.finish();
+    }
+
+    private void cleanupEmptyFile() {
+        if (mVideoFilename != null) {
+            File f = new File(mVideoFilename);
+            if (f.length() == 0 && f.delete()) {
+                Log.v(TAG, "Empty video file deleted: " + mVideoFilename);
+                mVideoFilename = null;
+            }
+        }
+    }
+
+    private void setupMediaRecorderPreviewDisplay() {
+        // Nothing to do here if using SurfaceTexture.
+        if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
+            // We stop the preview here before unlocking the device because we
+            // need to change the SurfaceTexture to SurfaceView for preview.
+            stopPreview();
+            mCameraDevice.setPreviewDisplayAsync(mUI.getSurfaceHolder());
+            // The orientation for SurfaceTexture is different from that for
+            // SurfaceView. For SurfaceTexture we don't need to consider the
+            // display rotation. Just consider the sensor's orientation and we
+            // will set the orientation correctly when showing the texture.
+            // Gallery will handle the orientation for the preview. For
+            // SurfaceView we will have to take everything into account so the
+            // display rotation is considered.
+            mCameraDevice.setDisplayOrientation(
+                    Util.getDisplayOrientation(mDisplayRotation, mCameraId));
+            mCameraDevice.startPreviewAsync();
+            mPreviewing = true;
+            mMediaRecorder.setPreviewDisplay(mUI.getSurfaceHolder().getSurface());
+        }
+    }
+
+    // Prepares media recorder.
+    private void initializeRecorder() {
+        Log.v(TAG, "initializeRecorder");
+        // If the mCameraDevice is null, then this activity is going to finish
+        if (mCameraDevice == null) return;
+
+        if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
+            // Set the SurfaceView to visible so the surface gets created.
+            // surfaceCreated() is called immediately when the visibility is
+            // changed to visible. Thus, mSurfaceViewReady should become true
+            // right after calling setVisibility().
+            mUI.showSurfaceView();
+        }
+
+        Intent intent = mActivity.getIntent();
+        Bundle myExtras = intent.getExtras();
+
+        long requestedSizeLimit = 0;
+        closeVideoFileDescriptor();
+        if (mIsVideoCaptureIntent && myExtras != null) {
+            Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT);
+            if (saveUri != null) {
+                try {
+                    mVideoFileDescriptor =
+                            mContentResolver.openFileDescriptor(saveUri, "rw");
+                    mCurrentVideoUri = saveUri;
+                } catch (java.io.FileNotFoundException ex) {
+                    // invalid uri
+                    Log.e(TAG, ex.toString());
+                }
+            }
+            requestedSizeLimit = myExtras.getLong(MediaStore.EXTRA_SIZE_LIMIT);
+        }
+        mMediaRecorder = new MediaRecorder();
+
+        setupMediaRecorderPreviewDisplay();
+        // Unlock the camera object before passing it to media recorder.
+        mCameraDevice.unlock();
+        mCameraDevice.waitDone();
+        mMediaRecorder.setCamera(mCameraDevice.getCamera());
+        if (!mCaptureTimeLapse) {
+            mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
+        }
+        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
+        mMediaRecorder.setProfile(mProfile);
+        mMediaRecorder.setMaxDuration(mMaxVideoDurationInMs);
+        if (mCaptureTimeLapse) {
+            double fps = 1000 / (double) mTimeBetweenTimeLapseFrameCaptureMs;
+            setCaptureRate(mMediaRecorder, fps);
+        }
+
+        setRecordLocation();
+
+        // Set output file.
+        // Try Uri in the intent first. If it doesn't exist, use our own
+        // instead.
+        if (mVideoFileDescriptor != null) {
+            mMediaRecorder.setOutputFile(mVideoFileDescriptor.getFileDescriptor());
+        } else {
+            generateVideoFilename(mProfile.fileFormat);
+            mMediaRecorder.setOutputFile(mVideoFilename);
+        }
+
+        // Set maximum file size.
+        long maxFileSize = mActivity.getStorageSpace() - Storage.LOW_STORAGE_THRESHOLD;
+        if (requestedSizeLimit > 0 && requestedSizeLimit < maxFileSize) {
+            maxFileSize = requestedSizeLimit;
+        }
+
+        try {
+            mMediaRecorder.setMaxFileSize(maxFileSize);
+        } catch (RuntimeException exception) {
+            // We are going to ignore failure of setMaxFileSize here, as
+            // a) The composer selected may simply not support it, or
+            // b) The underlying media framework may not handle 64-bit range
+            // on the size restriction.
+        }
+
+        // See android.hardware.Camera.Parameters.setRotation for
+        // documentation.
+        // Note that mOrientation here is the device orientation, which is the opposite of
+        // what activity.getWindowManager().getDefaultDisplay().getRotation() would return,
+        // which is the orientation the graphics need to rotate in order to render correctly.
+        int rotation = 0;
+        if (mOrientation != OrientationEventListener.ORIENTATION_UNKNOWN) {
+            CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
+            if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
+                rotation = (info.orientation - mOrientation + 360) % 360;
+            } else {  // back-facing camera
+                rotation = (info.orientation + mOrientation) % 360;
+            }
+        }
+        mMediaRecorder.setOrientationHint(rotation);
+
+        try {
+            mMediaRecorder.prepare();
+        } catch (IOException e) {
+            Log.e(TAG, "prepare failed for " + mVideoFilename, e);
+            releaseMediaRecorder();
+            throw new RuntimeException(e);
+        }
+
+        mMediaRecorder.setOnErrorListener(this);
+        mMediaRecorder.setOnInfoListener(this);
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    private static void setCaptureRate(MediaRecorder recorder, double fps) {
+        recorder.setCaptureRate(fps);
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    private void setRecordLocation() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+            Location loc = mLocationManager.getCurrentLocation();
+            if (loc != null) {
+                mMediaRecorder.setLocation((float) loc.getLatitude(),
+                        (float) loc.getLongitude());
+            }
+        }
+    }
+
+    private void initializeEffectsPreview() {
+        Log.v(TAG, "initializeEffectsPreview");
+        // If the mCameraDevice is null, then this activity is going to finish
+        if (mCameraDevice == null) return;
+
+        boolean inLandscape = (mActivity.getResources().getConfiguration().orientation
+                == Configuration.ORIENTATION_LANDSCAPE);
+
+        CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
+
+        mEffectsDisplayResult = false;
+        mEffectsRecorder = new EffectsRecorder(mActivity);
+
+        // TODO: Confirm none of the following need to go to initializeEffectsRecording()
+        // and none of these change even when the preview is not refreshed.
+        mEffectsRecorder.setCameraDisplayOrientation(mCameraDisplayOrientation);
+        mEffectsRecorder.setCamera(mCameraDevice);
+        mEffectsRecorder.setCameraFacing(info.facing);
+        mEffectsRecorder.setProfile(mProfile);
+        mEffectsRecorder.setEffectsListener(this);
+        mEffectsRecorder.setOnInfoListener(this);
+        mEffectsRecorder.setOnErrorListener(this);
+
+        // The input of effects recorder is affected by
+        // android.hardware.Camera.setDisplayOrientation. Its value only
+        // compensates the camera orientation (no Display.getRotation). So the
+        // orientation hint here should only consider sensor orientation.
+        int orientation = 0;
+        if (mOrientation != OrientationEventListener.ORIENTATION_UNKNOWN) {
+            orientation = mOrientation;
+        }
+        mEffectsRecorder.setOrientationHint(orientation);
+
+        mEffectsRecorder.setPreviewSurfaceTexture(mUI.getSurfaceTexture(),
+            mUI.getPreviewWidth(), mUI.getPreviewHeight());
+
+        if (mEffectType == EffectsRecorder.EFFECT_BACKDROPPER &&
+                ((String) mEffectParameter).equals(EFFECT_BG_FROM_GALLERY)) {
+            mEffectsRecorder.setEffect(mEffectType, mEffectUriFromGallery);
+        } else {
+            mEffectsRecorder.setEffect(mEffectType, mEffectParameter);
+        }
+    }
+
+    private void initializeEffectsRecording() {
+        Log.v(TAG, "initializeEffectsRecording");
+
+        Intent intent = mActivity.getIntent();
+        Bundle myExtras = intent.getExtras();
+
+        long requestedSizeLimit = 0;
+        closeVideoFileDescriptor();
+        if (mIsVideoCaptureIntent && myExtras != null) {
+            Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT);
+            if (saveUri != null) {
+                try {
+                    mVideoFileDescriptor =
+                            mContentResolver.openFileDescriptor(saveUri, "rw");
+                    mCurrentVideoUri = saveUri;
+                } catch (java.io.FileNotFoundException ex) {
+                    // invalid uri
+                    Log.e(TAG, ex.toString());
+                }
+            }
+            requestedSizeLimit = myExtras.getLong(MediaStore.EXTRA_SIZE_LIMIT);
+        }
+
+        mEffectsRecorder.setProfile(mProfile);
+        // important to set the capture rate to zero if not timelapsed, since the
+        // effectsrecorder object does not get created again for each recording
+        // session
+        if (mCaptureTimeLapse) {
+            mEffectsRecorder.setCaptureRate((1000 / (double) mTimeBetweenTimeLapseFrameCaptureMs));
+        } else {
+            mEffectsRecorder.setCaptureRate(0);
+        }
+
+        // Set output file
+        if (mVideoFileDescriptor != null) {
+            mEffectsRecorder.setOutputFile(mVideoFileDescriptor.getFileDescriptor());
+        } else {
+            generateVideoFilename(mProfile.fileFormat);
+            mEffectsRecorder.setOutputFile(mVideoFilename);
+        }
+
+        // Set maximum file size.
+        long maxFileSize = mActivity.getStorageSpace() - Storage.LOW_STORAGE_THRESHOLD;
+        if (requestedSizeLimit > 0 && requestedSizeLimit < maxFileSize) {
+            maxFileSize = requestedSizeLimit;
+        }
+        mEffectsRecorder.setMaxFileSize(maxFileSize);
+        mEffectsRecorder.setMaxDuration(mMaxVideoDurationInMs);
+    }
+
+
+    private void releaseMediaRecorder() {
+        Log.v(TAG, "Releasing media recorder.");
+        if (mMediaRecorder != null) {
+            cleanupEmptyFile();
+            mMediaRecorder.reset();
+            mMediaRecorder.release();
+            mMediaRecorder = null;
+        }
+        mVideoFilename = null;
+    }
+
+    private void releaseEffectsRecorder() {
+        Log.v(TAG, "Releasing effects recorder.");
+        if (mEffectsRecorder != null) {
+            cleanupEmptyFile();
+            mEffectsRecorder.release();
+            mEffectsRecorder = null;
+        }
+        mEffectType = EffectsRecorder.EFFECT_NONE;
+        mVideoFilename = null;
+    }
+
+    private void generateVideoFilename(int outputFileFormat) {
+        long dateTaken = System.currentTimeMillis();
+        String title = createName(dateTaken);
+        // Used when emailing.
+        String filename = title + convertOutputFormatToFileExt(outputFileFormat);
+        String mime = convertOutputFormatToMimeType(outputFileFormat);
+        String path = Storage.DIRECTORY + '/' + filename;
+        String tmpPath = path + ".tmp";
+        mCurrentVideoValues = new ContentValues(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();
+                //TODO: 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");
+        // TODO: 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.
+                mCameraDevice.lock();
+                return;
+            }
+        }
+
+        // Make sure the video recording has started before announcing
+        // this in accessibility.
+        AccessibilityUtils.makeAnnouncement(mUI.getShutterButton(),
+                mActivity.getString(R.string.video_recording_started));
+
+        // The parameters might have been altered by MediaRecorder already.
+        // We need to force mCameraDevice to refresh before getting it.
+        mCameraDevice.refreshParameters();
+        // The parameters may have been changed by MediaRecorder upon starting
+        // recording. We need to alter the parameters if we support camcorder
+        // zoom. To reduce latency when setting the parameters during zoom, we
+        // update mParameters here once.
+        if (ApiHelper.HAS_ZOOM_WHEN_RECORDING) {
+            mParameters = mCameraDevice.getParameters();
+        }
+
+        mUI.enableCameraControls(false);
+
+        mMediaRecorderRecording = true;
+        mOrientationManager.lockOrientation();
+        mRecordingStartTime = SystemClock.uptimeMillis();
+        mUI.showRecordingUI(true, mParameters.isZoomSupported());
+
+        updateRecordingTime();
+        keepScreenOn();
+        UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
+                UsageStatistics.ACTION_CAPTURE_START, "Video");
+    }
+
+    private void showCaptureResult() {
+        mIsInReviewMode = true;
+        Bitmap bitmap = null;
+        if (mVideoFileDescriptor != null) {
+            bitmap = Thumbnail.createVideoThumbnailBitmap(mVideoFileDescriptor.getFileDescriptor(),
+                    mDesiredPreviewWidth);
+        } else if (mCurrentVideoFilename != null) {
+            bitmap = Thumbnail.createVideoThumbnailBitmap(mCurrentVideoFilename,
+                    mDesiredPreviewWidth);
+        }
+        if (bitmap != null) {
+            // MetadataRetriever already rotates the thumbnail. We should rotate
+            // it to match the UI orientation (and mirror if it is front-facing camera).
+            CameraInfo[] info = CameraHolder.instance().getCameraInfo();
+            boolean mirror = (info[mCameraId].facing == CameraInfo.CAMERA_FACING_FRONT);
+            bitmap = Util.rotateAndMirror(bitmap, 0, mirror);
+            mUI.showReviewImage(bitmap);
+        }
+
+        mUI.showReviewControls();
+        mUI.enableCameraControls(false);
+        mUI.showTimeLapseUI(false);
+    }
+
+    private void hideAlert() {
+        mUI.enableCameraControls(true);
+        mUI.hideReviewUI();
+        if (mCaptureTimeLapse) {
+            mUI.showTimeLapseUI(true);
+        }
+    }
+
+    private boolean stopVideoRecording() {
+        Log.v(TAG, "stopVideoRecording");
+        //TODO: mUI.setSwipingEnabled(true);
+        mUI.showSwitcher();
+
+        boolean fail = false;
+        if (mMediaRecorderRecording) {
+            boolean shouldAddToMediaStoreNow = false;
+
+            try {
+                if (effectsActive()) {
+                    // This is asynchronous, so we can't add to media store now because thumbnail
+                    // may not be ready. In such case 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(mUI.getShutterButton(),
+                        mActivity.getString(R.string.video_recording_stopped));
+            } catch (RuntimeException e) {
+                Log.e(TAG, "stop fail",  e);
+                if (mVideoFilename != null) deleteVideoFile(mVideoFilename);
+                fail = true;
+            }
+            mMediaRecorderRecording = false;
+            mOrientationManager.unlockOrientation();
+
+            // If the activity is paused, this means activity is interrupted
+            // during recording. Release the camera as soon as possible because
+            // face unlock or other applications may need to use the camera.
+            // However, if the effects are active, then we can only release the
+            // camera and cannot release the effects recorder since that will
+            // stop the graph. It is possible to separate out the Camera release
+            // part and the effects release part. However, the effects recorder
+            // does hold on to the camera, hence, it needs to be "disconnected"
+            // from the camera in the closeCamera call.
+            if (mPaused) {
+                // Closing only the camera part if effects active. Effects will
+                // be closed in the callback from effects.
+                boolean closeEffects = !effectsActive();
+                closeCamera(closeEffects);
+            }
+
+            mUI.showRecordingUI(false, mParameters.isZoomSupported());
+            if (!mIsVideoCaptureIntent) {
+                mUI.enableCameraControls(true);
+            }
+            // The orientation was fixed during video recording. Now make it
+            // reflect the device orientation as video recording is stopped.
+            mUI.setOrientationIndicator(0, true);
+            keepScreenOnAwhile();
+            if (shouldAddToMediaStoreNow) {
+                if (addVideoToMediaStore()) fail = true;
+            }
+        }
+        // always release media recorder if no effects running
+        if (!effectsActive()) {
+            releaseMediaRecorder();
+            if (!mPaused) {
+                mCameraDevice.lock();
+                mCameraDevice.waitDone();
+                if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
+                    stopPreview();
+                    mUI.hideSurfaceView();
+                    // Switch back to use SurfaceTexture for preview.
+                    startPreview();
+                }
+            }
+        }
+        // Update the parameters here because the parameters might have been altered
+        // by MediaRecorder.
+        if (!mPaused) mParameters = mCameraDevice.getParameters();
+        UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
+                fail ? UsageStatistics.ACTION_CAPTURE_FAIL :
+                    UsageStatistics.ACTION_CAPTURE_DONE, "Video",
+                    SystemClock.uptimeMillis() - mRecordingStartTime);
+        return fail;
+    }
+
+    private void resetScreenOn() {
+        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+        mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+    }
+
+    private void keepScreenOnAwhile() {
+        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+        mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+        mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY);
+    }
+
+    private void keepScreenOn() {
+        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+        mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+    }
+
+    private static String millisecondToTimeString(long milliSeconds, boolean displayCentiSeconds) {
+        long seconds = milliSeconds / 1000; // round down to compute seconds
+        long minutes = seconds / 60;
+        long hours = minutes / 60;
+        long remainderMinutes = minutes - (hours * 60);
+        long remainderSeconds = seconds - (minutes * 60);
+
+        StringBuilder timeStringBuilder = new StringBuilder();
+
+        // Hours
+        if (hours > 0) {
+            if (hours < 10) {
+                timeStringBuilder.append('0');
+            }
+            timeStringBuilder.append(hours);
+
+            timeStringBuilder.append(':');
+        }
+
+        // Minutes
+        if (remainderMinutes < 10) {
+            timeStringBuilder.append('0');
+        }
+        timeStringBuilder.append(remainderMinutes);
+        timeStringBuilder.append(':');
+
+        // Seconds
+        if (remainderSeconds < 10) {
+            timeStringBuilder.append('0');
+        }
+        timeStringBuilder.append(remainderSeconds);
+
+        // Centi seconds
+        if (displayCentiSeconds) {
+            timeStringBuilder.append('.');
+            long remainderCentiSeconds = (milliSeconds - seconds * 1000) / 10;
+            if (remainderCentiSeconds < 10) {
+                timeStringBuilder.append('0');
+            }
+            timeStringBuilder.append(remainderCentiSeconds);
+        }
+
+        return timeStringBuilder.toString();
+    }
+
+    private long getTimeLapseVideoLength(long deltaMs) {
+        // For better approximation calculate fractional number of frames captured.
+        // This will update the video time at a higher resolution.
+        double numberOfFrames = (double) deltaMs / mTimeBetweenTimeLapseFrameCaptureMs;
+        return (long) (numberOfFrames / mProfile.videoFrameRate * 1000);
+    }
+
+    private void updateRecordingTime() {
+        if (!mMediaRecorderRecording) {
+            return;
+        }
+        long now = SystemClock.uptimeMillis();
+        long delta = now - mRecordingStartTime;
+
+        // Starting a minute before reaching the max duration
+        // limit, we'll countdown the remaining time instead.
+        boolean countdownRemainingTime = (mMaxVideoDurationInMs != 0
+                && delta >= mMaxVideoDurationInMs - 60000);
+
+        long deltaAdjusted = delta;
+        if (countdownRemainingTime) {
+            deltaAdjusted = Math.max(0, mMaxVideoDurationInMs - deltaAdjusted) + 999;
+        }
+        String text;
+
+        long targetNextUpdateDelay;
+        if (!mCaptureTimeLapse) {
+            text = millisecondToTimeString(deltaAdjusted, false);
+            targetNextUpdateDelay = 1000;
+        } else {
+            // The length of time lapse video is different from the length
+            // of the actual wall clock time elapsed. Display the video length
+            // only in format hh:mm:ss.dd, where dd are the centi seconds.
+            text = millisecondToTimeString(getTimeLapseVideoLength(delta), true);
+            targetNextUpdateDelay = mTimeBetweenTimeLapseFrameCaptureMs;
+        }
+
+        mUI.setRecordingTime(text);
+
+        if (mRecordingTimeCountsDown != countdownRemainingTime) {
+            // Avoid setting the color on every update, do it only
+            // when it needs changing.
+            mRecordingTimeCountsDown = countdownRemainingTime;
+
+            int color = mActivity.getResources().getColor(countdownRemainingTime
+                    ? R.color.recording_time_remaining_text
+                    : R.color.recording_time_elapsed_text);
+
+            mUI.setRecordingTimeTextColor(color);
+        }
+
+        long actualNextUpdateDelay = targetNextUpdateDelay - (delta % targetNextUpdateDelay);
+        mHandler.sendEmptyMessageDelayed(
+                UPDATE_RECORD_TIME, actualNextUpdateDelay);
+    }
+
+    private static boolean isSupported(String value, List<String> supported) {
+        return supported == null ? false : supported.indexOf(value) >= 0;
+    }
+
+    @SuppressWarnings("deprecation")
+    private void setCameraParameters() {
+        mParameters.setPreviewSize(mDesiredPreviewWidth, mDesiredPreviewHeight);
+        mParameters.setPreviewFrameRate(mProfile.videoFrameRate);
+
+        // Set flash mode.
+        String flashMode;
+        if (mUI.isVisible()) {
+            flashMode = mPreferences.getString(
+                    CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE,
+                    mActivity.getString(R.string.pref_camera_video_flashmode_default));
+        } else {
+            flashMode = Parameters.FLASH_MODE_OFF;
+        }
+        List<String> supportedFlash = mParameters.getSupportedFlashModes();
+        if (isSupported(flashMode, supportedFlash)) {
+            mParameters.setFlashMode(flashMode);
+        } else {
+            flashMode = mParameters.getFlashMode();
+            if (flashMode == null) {
+                flashMode = mActivity.getString(
+                        R.string.pref_camera_flashmode_no_flash);
+            }
+        }
+
+        // Set white balance parameter.
+        String whiteBalance = mPreferences.getString(
+                CameraSettings.KEY_WHITE_BALANCE,
+                mActivity.getString(R.string.pref_camera_whitebalance_default));
+        if (isSupported(whiteBalance,
+                mParameters.getSupportedWhiteBalance())) {
+            mParameters.setWhiteBalance(whiteBalance);
+        } else {
+            whiteBalance = mParameters.getWhiteBalance();
+            if (whiteBalance == null) {
+                whiteBalance = Parameters.WHITE_BALANCE_AUTO;
+            }
+        }
+
+        // Set zoom.
+        if (mParameters.isZoomSupported()) {
+            mParameters.setZoom(mZoomValue);
+        }
+
+        // Set continuous autofocus.
+        List<String> supportedFocus = mParameters.getSupportedFocusModes();
+        if (isSupported(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, supportedFocus)) {
+            mParameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
+        }
+
+        mParameters.set(Util.RECORDING_HINT, Util.TRUE);
+
+        // Enable video stabilization. Convenience methods not available in API
+        // level <= 14
+        String vstabSupported = mParameters.get("video-stabilization-supported");
+        if ("true".equals(vstabSupported)) {
+            mParameters.set("video-stabilization", "true");
+        }
+
+        // Set picture size.
+        // The logic here is different from the logic in still-mode camera.
+        // There we determine the preview size based on the picture size, but
+        // here we determine the picture size based on the preview size.
+        List<Size> supported = mParameters.getSupportedPictureSizes();
+        Size optimalSize = Util.getOptimalVideoSnapshotPictureSize(supported,
+                (double) mDesiredPreviewWidth / mDesiredPreviewHeight);
+        Size original = mParameters.getPictureSize();
+        if (!original.equals(optimalSize)) {
+            mParameters.setPictureSize(optimalSize.width, optimalSize.height);
+        }
+        Log.v(TAG, "Video snapshot size is " + optimalSize.width + "x" +
+                optimalSize.height);
+
+        // Set JPEG quality.
+        int jpegQuality = CameraProfile.getJpegEncodingQualityParameter(mCameraId,
+                CameraProfile.QUALITY_HIGH);
+        mParameters.setJpegQuality(jpegQuality);
+
+        mCameraDevice.setParameters(mParameters);
+        // Keep preview size up to date.
+        mParameters = mCameraDevice.getParameters();
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        switch (requestCode) {
+            case REQUEST_EFFECT_BACKDROPPER:
+                if (resultCode == Activity.RESULT_OK) {
+                    // onActivityResult() runs before onResume(), so this parameter will be
+                    // seen by startPreview from onResume()
+                    mEffectUriFromGallery = data.getData().toString();
+                    Log.v(TAG, "Received URI from gallery: " + mEffectUriFromGallery);
+                    mResetEffect = false;
+                } else {
+                    mEffectUriFromGallery = null;
+                    Log.w(TAG, "No URI from gallery");
+                    mResetEffect = true;
+                }
+                break;
+        }
+    }
+
+    @Override
+    public void onEffectsUpdate(int effectId, int effectMsg) {
+        Log.v(TAG, "onEffectsUpdate. Effect Message = " + effectMsg);
+        if (effectMsg == EffectsRecorder.EFFECT_MSG_EFFECTS_STOPPED) {
+            // Effects have shut down. Hide learning message if any,
+            // and restart regular preview.
+            checkQualityAndStartPreview();
+        } else if (effectMsg == EffectsRecorder.EFFECT_MSG_RECORDING_DONE) {
+            // This follows the codepath from onStopVideoRecording.
+            if (mEffectsDisplayResult && !addVideoToMediaStore()) {
+                if (mIsVideoCaptureIntent) {
+                    if (mQuickCapture) {
+                        doReturnToCaller(true);
+                    } else {
+                        showCaptureResult();
+                    }
+                }
+            }
+            mEffectsDisplayResult = false;
+            // In onPause, these were not called if the effects were active. We
+            // had to wait till the effects recording is complete to do this.
+            if (mPaused) {
+                closeVideoFileDescriptor();
+                clearVideoNamer();
+            }
+        } else if (effectMsg == EffectsRecorder.EFFECT_MSG_PREVIEW_RUNNING) {
+            // Enable the shutter button once the preview is complete.
+            mUI.enableShutter(true);
+        }
+        // In onPause, this was not called if the effects were active. We had to
+        // wait till the effects completed to do this.
+        if (mPaused) {
+            Log.v(TAG, "OnEffectsUpdate: closing effects if activity paused");
+            closeEffects();
+        }
+    }
+
+    public void onCancelBgTraining(View v) {
+        // Write default effect out to shared prefs
+        writeDefaultEffectToPrefs();
+        // Tell VideoCamer to re-init based on new shared pref values.
+        onSharedPreferenceChanged();
+    }
+
+    @Override
+    public synchronized void onEffectsError(Exception exception, String fileName) {
+        // TODO: Eventually we may want to show the user an error dialog, and then restart the
+        // camera and encoder gracefully. For now, we just delete the file and bail out.
+        if (fileName != null && new File(fileName).exists()) {
+            deleteVideoFile(fileName);
+        }
+        try {
+            if (Class.forName("android.filterpacks.videosink.MediaRecorderStopException")
+                    .isInstance(exception)) {
+                Log.w(TAG, "Problem recoding video file. Removing incomplete file.");
+                return;
+            }
+        } catch (ClassNotFoundException ex) {
+            Log.w(TAG, ex);
+        }
+        throw new RuntimeException("Error during recording!", exception);
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        Log.v(TAG, "onConfigurationChanged");
+        setDisplayOrientation();
+    }
+
+    @Override
+    public void onOverriddenPreferencesClicked() {
+    }
+
+    @Override
+    // TODO: Delete this after old camera code is removed
+    public void onRestorePreferencesClicked() {
+    }
+
+    private boolean effectsActive() {
+        return (mEffectType != EffectsRecorder.EFFECT_NONE);
+    }
+
+    @Override
+    public void onSharedPreferenceChanged() {
+        // ignore the events after "onPause()" or preview has not started yet
+        if (mPaused) return;
+        synchronized (mPreferences) {
+            // If mCameraDevice is not ready then we can set the parameter in
+            // startPreview().
+            if (mCameraDevice == null) return;
+
+            boolean recordLocation = RecordLocationPreference.get(
+                    mPreferences, mContentResolver);
+            mLocationManager.recordLocation(recordLocation);
+
+            // Check if the current effects selection has changed
+            if (updateEffectSelection()) return;
+
+            readVideoPreferences();
+            mUI.showTimeLapseUI(mCaptureTimeLapse);
+            // We need to restart the preview if preview size is changed.
+            Size size = mParameters.getPreviewSize();
+            if (size.width != mDesiredPreviewWidth
+                    || size.height != mDesiredPreviewHeight) {
+                if (!effectsActive()) {
+                    stopPreview();
+                } else {
+                    mEffectsRecorder.release();
+                    mEffectsRecorder = null;
+                }
+                resizeForPreviewAspectRatio();
+                startPreview(); // Parameters will be set in startPreview().
+            } else {
+                setCameraParameters();
+            }
+            mUI.updateOnScreenIndicators(mParameters);
+        }
+    }
+
+    protected void setCameraId(int cameraId) {
+        ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID);
+        pref.setValue("" + cameraId);
+    }
+
+    private void switchCamera() {
+        if (mPaused) return;
+
+        Log.d(TAG, "Start to switch camera.");
+        mCameraId = mPendingSwitchCameraId;
+        mPendingSwitchCameraId = -1;
+        setCameraId(mCameraId);
+
+        closeCamera();
+        mUI.collapseCameraControls();
+        // Restart the camera and initialize the UI. From onCreate.
+        mPreferences.setLocalId(mActivity, mCameraId);
+        CameraSettings.upgradeLocalPreferences(mPreferences.getLocal());
+        openCamera();
+        readVideoPreferences();
+        startPreview();
+        initializeVideoSnapshot();
+        resizeForPreviewAspectRatio();
+        initializeVideoControl();
+
+        // From onResume
+        mUI.initializeZoom(mParameters);
+        mUI.setOrientationIndicator(0, false);
+
+        // Start switch camera animation. Post a message because
+        // onFrameAvailable from the old camera may already exist.
+        mHandler.sendEmptyMessage(SWITCH_CAMERA_START_ANIMATION);
+        mUI.updateOnScreenIndicators(mParameters);
+    }
+
+    // Preview texture has been copied. Now camera can be released and the
+    // animation can be started.
+    @Override
+    public void onPreviewTextureCopied() {
+        mHandler.sendEmptyMessage(SWITCH_CAMERA);
+    }
+
+    @Override
+    public void onCaptureTextureCopied() {
+    }
+
+    private boolean updateEffectSelection() {
+        int previousEffectType = mEffectType;
+        Object previousEffectParameter = mEffectParameter;
+        mEffectType = CameraSettings.readEffectType(mPreferences);
+        mEffectParameter = CameraSettings.readEffectParameter(mPreferences);
+
+        if (mEffectType == previousEffectType) {
+            if (mEffectType == EffectsRecorder.EFFECT_NONE) return false;
+            if (mEffectParameter.equals(previousEffectParameter)) return false;
+        }
+        Log.v(TAG, "New effect selection: " + mPreferences.getString(
+                CameraSettings.KEY_VIDEO_EFFECT, "none"));
+
+        if (mEffectType == EffectsRecorder.EFFECT_NONE) {
+            // Stop effects and return to normal preview
+            mEffectsRecorder.stopPreview();
+            mPreviewing = false;
+            return true;
+        }
+        if (mEffectType == EffectsRecorder.EFFECT_BACKDROPPER &&
+            ((String) mEffectParameter).equals(EFFECT_BG_FROM_GALLERY)) {
+            // Request video from gallery to use for background
+            Intent i = new Intent(Intent.ACTION_PICK);
+            i.setDataAndType(Video.Media.EXTERNAL_CONTENT_URI,
+                             "video/*");
+            i.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
+            mActivity.startActivityForResult(i, REQUEST_EFFECT_BACKDROPPER);
+            return true;
+        }
+        if (previousEffectType == EffectsRecorder.EFFECT_NONE) {
+            // Stop regular preview and start effects.
+            stopPreview();
+            checkQualityAndStartPreview();
+        } else {
+            // Switch currently running effect
+            mEffectsRecorder.setEffect(mEffectType, mEffectParameter);
+        }
+        return true;
+    }
+
+    // Verifies that the current preview view size is correct before starting
+    // preview. If not, resets the surface texture and resizes the view.
+    private void checkQualityAndStartPreview() {
+        readVideoPreferences();
+        mUI.showTimeLapseUI(mCaptureTimeLapse);
+        Size size = mParameters.getPreviewSize();
+        if (size.width != mDesiredPreviewWidth
+                || size.height != mDesiredPreviewHeight) {
+            resizeForPreviewAspectRatio();
+        }
+        // Start up preview again
+        startPreview();
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent m) {
+        if (mSwitchingCamera) return true;
+        return mUI.dispatchTouchEvent(m);
+    }
+
+    private void initializeVideoSnapshot() {
+        if (mParameters == null) return;
+        if (Util.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) {
+            // Show the tap to focus toast if this is the first start.
+            if (mPreferences.getBoolean(
+                        CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, true)) {
+                // Delay the toast for one second to wait for orientation.
+                mHandler.sendEmptyMessageDelayed(SHOW_TAP_TO_SNAPSHOT_TOAST, 1000);
+            }
+        }
+    }
+
+    void showVideoSnapshotUI(boolean enabled) {
+        if (mParameters == null) return;
+        if (Util.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) {
+            if (enabled) {
+             // TODO: ((CameraScreenNail) mActivity.mCameraScreenNail).animateCapture(mDisplayRotation);
+            } else {
+                mUI.showPreviewBorder(enabled);
+            }
+            mUI.enableShutter(!enabled);
+        }
+    }
+
+    @Override
+    public void updateCameraAppView() {
+        if (!mPreviewing || mParameters.getFlashMode() == null) return;
+
+        // When going to and back from gallery, we need to turn off/on the flash.
+        if (!mUI.isVisible()) {
+            if (mParameters.getFlashMode().equals(Parameters.FLASH_MODE_OFF)) {
+                mRestoreFlash = false;
+                return;
+            }
+            mRestoreFlash = true;
+            setCameraParameters();
+        } else if (mRestoreFlash) {
+            mRestoreFlash = false;
+            setCameraParameters();
+        }
+    }
+
+    @Override
+    public void onFullScreenChanged(boolean full) {
+        mUI.onFullScreenChanged(full);
+    }
+
+    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);
+        ExifInterface exif = Exif.getExif(data);
+        int orientation = Exif.getOrientation(exif);
+        Size s = mParameters.getPictureSize();
+        mActivity.getMediaSaveService().addImage(
+                data, title, dateTaken, loc, s.width, s.height, orientation,
+                exif, mOnMediaSavedListener, mContentResolver);
+    }
+
+    private boolean resetEffect() {
+        if (mResetEffect) {
+            String value = mPreferences.getString(CameraSettings.KEY_VIDEO_EFFECT,
+                    mPrefVideoEffectDefault);
+            if (!mPrefVideoEffectDefault.equals(value)) {
+                writeDefaultEffectToPrefs();
+                return true;
+            }
+        }
+        mResetEffect = true;
+        return false;
+    }
+
+    private String convertOutputFormatToMimeType(int outputFileFormat) {
+        if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) {
+            return "video/mp4";
+        }
+        return "video/3gpp";
+    }
+
+    private String convertOutputFormatToFileExt(int outputFileFormat) {
+        if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) {
+            return ".mp4";
+        }
+        return ".3gp";
+    }
+
+    private void closeVideoFileDescriptor() {
+        if (mVideoFileDescriptor != null) {
+            try {
+                mVideoFileDescriptor.close();
+            } catch (IOException e) {
+                Log.e(TAG, "Fail to close fd", e);
+            }
+            mVideoFileDescriptor = null;
+        }
+    }
+
+    private void showTapToSnapshotToast() {
+        new RotateTextToast(mActivity, R.string.video_snapshot_hint, 0)
+                .show();
+        // Clear the preference.
+        Editor editor = mPreferences.edit();
+        editor.putBoolean(CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, false);
+        editor.apply();
+    }
+
+    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;
+        }
+    }
+
+    @Override
+    public boolean updateStorageHintOnResume() {
+        return true;
+    }
+
+    // required by OnPreferenceChangedListener
+    @Override
+    public void onCameraPickerClicked(int cameraId) {
+        if (mPaused || mPendingSwitchCameraId != -1) return;
+
+        mPendingSwitchCameraId = cameraId;
+        Log.d(TAG, "Start to copy texture.");
+        // We need to keep a preview frame for the animation before
+        // releasing the camera. This will trigger onPreviewTextureCopied.
+        // TODO: ((CameraScreenNail) mActivity.mCameraScreenNail).copyTexture();
+        // Disable all camera controls.
+        mSwitchingCamera = true;
+
+    }
+
+    @Override
+    public boolean needsSwitcher() {
+        return !mIsVideoCaptureIntent;
+    }
+
+    @Override
+    public boolean needsPieMenu() {
+        return true;
+    }
+
+    @Override
+    public void onShowSwitcherPopup() {
+        mUI.onShowSwitcherPopup();
+    }
+
+    @Override
+    public void onMediaSaveServiceConnected(MediaSaveService s) {
+        // do nothing.
+    }
+}
diff --git a/src/com/android/camera/NewVideoUI.java b/src/com/android/camera/NewVideoUI.java
new file mode 100644
index 0000000..a14dae3
--- /dev/null
+++ b/src/com/android/camera/NewVideoUI.java
@@ -0,0 +1,724 @@
+/*
+ * 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.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.Size;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.TextureView.SurfaceTextureListener;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLayoutChangeListener;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.camera.CameraPreference.OnPreferenceChangedListener;
+import com.android.camera.ui.AbstractSettingPopup;
+import com.android.camera.ui.CameraSwitcher;
+import com.android.camera.ui.PieRenderer;
+import com.android.camera.ui.RenderOverlay;
+import com.android.camera.ui.RotateLayout;
+import com.android.camera.ui.ZoomRenderer;
+import com.android.camera.ui.CameraSwitcher.CameraSwitchListener;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.List;
+
+public class NewVideoUI implements PieRenderer.PieListener,
+        NewPreviewGestures.SingleTapListener,
+        NewPreviewGestures.SwipeListener, SurfaceTextureListener,
+        SurfaceHolder.Callback {
+    private final static String TAG = "CAM_VideoUI";
+    private static final int UPDATE_TRANSFORM_MATRIX = 1;
+    // module fields
+    private NewCameraActivity mActivity;
+    private View mRootView;
+    private TextureView mTextureView;
+    // An review image having same size as preview. It is displayed when
+    // recording is stopped in capture intent.
+    private ImageView mReviewImage;
+    private View mReviewCancelButton;
+    private View mReviewDoneButton;
+    private View mReviewPlayButton;
+    private ShutterButton mShutterButton;
+    private CameraSwitcher mSwitcher;
+    private TextView mRecordingTimeView;
+    private LinearLayout mLabelsLinearLayout;
+    private View mTimeLapseLabel;
+    private RenderOverlay mRenderOverlay;
+    private PieRenderer mPieRenderer;
+    private NewVideoMenu mVideoMenu;
+    private View mCameraControls;
+    private AbstractSettingPopup mPopup;
+    private ZoomRenderer mZoomRenderer;
+    private NewPreviewGestures mGestures;
+    private View mMenuButton;
+    private View mBlocker;
+    private View mOnScreenIndicators;
+    private ImageView mFlashIndicator;
+    private RotateLayout mRecordingTimeRect;
+    private final Object mLock = new Object();
+    private SurfaceTexture mSurfaceTexture;
+    private VideoController mController;
+    private int mZoomMax;
+    private List<Integer> mZoomRatios;
+
+    private SurfaceView mSurfaceView = null;
+    private int mPreviewWidth = 0;
+    private int mPreviewHeight = 0;
+    private float mSurfaceTextureUncroppedWidth;
+    private float mSurfaceTextureUncroppedHeight;
+    private float mAspectRatio = 4f / 3f;
+    private Matrix mMatrix = null;
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case UPDATE_TRANSFORM_MATRIX:
+                    setTransformMatrix(mPreviewWidth, mPreviewHeight);
+                    break;
+                default:
+                    break;
+            }
+        }
+    };
+    private OnLayoutChangeListener mLayoutListener = new OnLayoutChangeListener() {
+        @Override
+        public void onLayoutChange(View v, int left, int top, int right,
+                int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+            int width = right - left;
+            int height = bottom - top;
+            // Full-screen screennail
+            int w = width;
+            int h = height;
+            if (Util.getDisplayRotation(mActivity) % 180 != 0) {
+                w = height;
+                h = width;
+            }
+            if (mPreviewWidth != width || mPreviewHeight != height) {
+                mPreviewWidth = width;
+                mPreviewHeight = height;
+                onScreenSizeChanged(width, height, w, h);
+            }
+        }
+    };
+
+    public NewVideoUI(NewCameraActivity activity, VideoController controller, View parent) {
+        mActivity = activity;
+        mController = controller;
+        mRootView = parent;
+        mActivity.getLayoutInflater().inflate(R.layout.new_video_module, (ViewGroup) mRootView, true);
+        mTextureView = (TextureView) mRootView.findViewById(R.id.preview_content);
+        mTextureView.setSurfaceTextureListener(this);
+        mRootView.addOnLayoutChangeListener(mLayoutListener);
+        mShutterButton = (ShutterButton) mRootView.findViewById(R.id.shutter_button);
+        mSwitcher = (CameraSwitcher) mRootView.findViewById(R.id.camera_switcher);
+        mSwitcher.setCurrentIndex(1);
+        mSwitcher.setSwitchListener((CameraSwitchListener) mActivity);
+        initializeMiscControls();
+        initializeControlByIntent();
+        initializeOverlay();
+    }
+
+
+    public void initializeSurfaceView() {
+        mSurfaceView = new SurfaceView(mActivity);
+        ((ViewGroup) mRootView).addView(mSurfaceView, 0);
+        mSurfaceView.getHolder().addCallback(this);
+    }
+
+    private void initializeControlByIntent() {
+        mBlocker = mActivity.findViewById(R.id.blocker);
+        mMenuButton = mActivity.findViewById(R.id.menu);
+        mMenuButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (mPieRenderer != null) {
+                    mPieRenderer.showInCenter();
+                }
+            }
+        });
+
+        mCameraControls = mActivity.findViewById(R.id.camera_controls);
+        mOnScreenIndicators = mActivity.findViewById(R.id.on_screen_indicators);
+        mFlashIndicator = (ImageView) mActivity.findViewById(R.id.menu_flash_indicator);
+        if (mController.isVideoCaptureIntent()) {
+            hideSwitcher();
+            mActivity.getLayoutInflater().inflate(R.layout.review_module_control, (ViewGroup) mCameraControls);
+            // Cannot use RotateImageView for "done" and "cancel" button because
+            // the tablet layout uses RotateLayout, which cannot be cast to
+            // RotateImageView.
+            mReviewDoneButton = mActivity.findViewById(R.id.btn_done);
+            mReviewCancelButton = mActivity.findViewById(R.id.btn_cancel);
+            mReviewPlayButton = mActivity.findViewById(R.id.btn_play);
+            mReviewCancelButton.setVisibility(View.VISIBLE);
+            mReviewDoneButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mController.onReviewDoneClicked(v);
+                }
+            });
+            mReviewCancelButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mController.onReviewCancelClicked(v);
+                }
+            });
+            mReviewPlayButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mController.onReviewPlayClicked(v);
+                }
+            });
+        }
+    }
+
+    public void setPreviewSize(int width, int height) {
+        if (width == 0 || height == 0) {
+            Log.w(TAG, "Preview size should not be 0.");
+            return;
+        }
+        if (width > height) {
+            mAspectRatio = (float) width / height;
+        } else {
+            mAspectRatio = (float) height / width;
+        }
+        mHandler.sendEmptyMessage(UPDATE_TRANSFORM_MATRIX);
+    }
+
+    public int getPreviewWidth() {
+        return mPreviewWidth;
+    }
+
+    public int getPreviewHeight() {
+        return mPreviewHeight;
+    }
+
+    public void onScreenSizeChanged(int width, int height, int previewWidth, int previewHeight) {
+        setTransformMatrix(width, height);
+    }
+
+    private void setTransformMatrix(int width, int height) {
+        mMatrix = mTextureView.getTransform(mMatrix);
+        int orientation = Util.getDisplayRotation(mActivity);
+        float scaleX = 1f, scaleY = 1f;
+        float scaledTextureWidth, scaledTextureHeight;
+        if (width > height) {
+            scaledTextureWidth = Math.max(width,
+                    (int) (height * mAspectRatio));
+            scaledTextureHeight = Math.max(height,
+                    (int)(width / mAspectRatio));
+        } else {
+            scaledTextureWidth = Math.max(width,
+                    (int) (height / mAspectRatio));
+            scaledTextureHeight = Math.max(height,
+                    (int) (width * mAspectRatio));
+        }
+
+        if (mSurfaceTextureUncroppedWidth != scaledTextureWidth ||
+                mSurfaceTextureUncroppedHeight != scaledTextureHeight) {
+            mSurfaceTextureUncroppedWidth = scaledTextureWidth;
+            mSurfaceTextureUncroppedHeight = scaledTextureHeight;
+        }
+        scaleX = scaledTextureWidth / width;
+        scaleY = scaledTextureHeight / height;
+        mMatrix.setScale(scaleX, scaleY, (float) width / 2, (float) height / 2);
+        mTextureView.setTransform(mMatrix);
+
+        if (mSurfaceView != null && mSurfaceView.getVisibility() == View.VISIBLE) {
+            LayoutParams lp = (LayoutParams) mSurfaceView.getLayoutParams();
+            lp.width = (int) mSurfaceTextureUncroppedWidth;
+            lp.height = (int) mSurfaceTextureUncroppedHeight;
+            lp.gravity = Gravity.CENTER;
+            mSurfaceView.requestLayout();
+        }
+    }
+
+    public void hideUI() {
+        mCameraControls.setVisibility(View.INVISIBLE);
+        hideSwitcher();
+        mShutterButton.setVisibility(View.GONE);
+    }
+
+    public void showUI() {
+        mCameraControls.setVisibility(View.VISIBLE);
+        showSwitcher();
+        mShutterButton.setVisibility(View.VISIBLE);
+    }
+
+    public void hideSwitcher() {
+        mSwitcher.closePopup();
+        mSwitcher.setVisibility(View.INVISIBLE);
+    }
+
+    public void showSwitcher() {
+        mSwitcher.setVisibility(View.VISIBLE);
+    }
+
+    public boolean collapseCameraControls() {
+        boolean ret = false;
+        if (mPopup != null) {
+            dismissPopup(false);
+            ret = true;
+        }
+        return ret;
+    }
+
+    public boolean removeTopLevelPopup() {
+        if (mPopup != null) {
+            dismissPopup(true);
+            return true;
+        }
+        return false;
+    }
+
+    public void enableCameraControls(boolean enable) {
+        if (mGestures != null) {
+            mGestures.setZoomOnly(!enable);
+        }
+        if (mPieRenderer != null && mPieRenderer.showsItems()) {
+            mPieRenderer.hide();
+        }
+    }
+
+    public void overrideSettings(final String... keyvalues) {
+        mVideoMenu.overrideSettings(keyvalues);
+    }
+
+    public void setOrientationIndicator(int orientation, boolean animation) {
+        if (mGestures != null) {
+            mGestures.setOrientation(orientation);
+        }
+        // We change the orientation of the linearlayout only for phone UI
+        // because when in portrait the width is not enough.
+        if (mLabelsLinearLayout != null) {
+            if (((orientation / 90) & 1) == 0) {
+                mLabelsLinearLayout.setOrientation(LinearLayout.VERTICAL);
+            } else {
+                mLabelsLinearLayout.setOrientation(LinearLayout.HORIZONTAL);
+            }
+        }
+        mRecordingTimeRect.setOrientation(0, animation);
+    }
+
+    public SurfaceHolder getSurfaceHolder() {
+        return mSurfaceView.getHolder();
+    }
+
+    public void hideSurfaceView() {
+        mSurfaceView.setVisibility(View.GONE);
+        mTextureView.setVisibility(View.VISIBLE);
+        setTransformMatrix(mPreviewWidth, mPreviewHeight);
+    }
+
+    public void showSurfaceView() {
+        mSurfaceView.setVisibility(View.VISIBLE);
+        mTextureView.setVisibility(View.GONE);
+        setTransformMatrix(mPreviewWidth, mPreviewHeight);
+    }
+
+    private void initializeOverlay() {
+        mRenderOverlay = (RenderOverlay) mRootView.findViewById(R.id.render_overlay);
+        if (mPieRenderer == null) {
+            mPieRenderer = new PieRenderer(mActivity);
+            mVideoMenu = new NewVideoMenu(mActivity, this, mPieRenderer);
+            mPieRenderer.setPieListener(this);
+        }
+        mRenderOverlay.addRenderer(mPieRenderer);
+        if (mZoomRenderer == null) {
+            mZoomRenderer = new ZoomRenderer(mActivity);
+        }
+        mRenderOverlay.addRenderer(mZoomRenderer);
+        if (mGestures == null) {
+            mGestures = new NewPreviewGestures(mActivity, this, mZoomRenderer, mPieRenderer, this);
+        }
+        mGestures.setRenderOverlay(mRenderOverlay);
+        mGestures.clearTouchReceivers();
+        mGestures.addTouchReceiver(mMenuButton);
+        mGestures.addTouchReceiver(mBlocker);
+        if (mController.isVideoCaptureIntent()) {
+            if (mReviewCancelButton != null) {
+                mGestures.addTouchReceiver(mReviewCancelButton);
+            }
+            if (mReviewDoneButton != null) {
+                mGestures.addTouchReceiver(mReviewDoneButton);
+            }
+            if (mReviewPlayButton != null) {
+                mGestures.addTouchReceiver(mReviewPlayButton);
+            }
+        }
+    }
+
+    public void setPrefChangedListener(OnPreferenceChangedListener listener) {
+        mVideoMenu.setListener(listener);
+    }
+
+    private void initializeMiscControls() {
+        mReviewImage = (ImageView) mRootView.findViewById(R.id.review_image);
+        mShutterButton.setImageResource(R.drawable.btn_new_shutter_video);
+        mShutterButton.setOnShutterButtonListener(mController);
+        mShutterButton.setVisibility(View.VISIBLE);
+        mShutterButton.requestFocus();
+        mShutterButton.enableTouch(true);
+        mRecordingTimeView = (TextView) mRootView.findViewById(R.id.recording_time);
+        mRecordingTimeRect = (RotateLayout) mRootView.findViewById(R.id.recording_time_rect);
+        mTimeLapseLabel = mRootView.findViewById(R.id.time_lapse_label);
+        // The R.id.labels can only be found in phone layout.
+        // That is, mLabelsLinearLayout should be null in tablet layout.
+        mLabelsLinearLayout = (LinearLayout) mRootView.findViewById(R.id.labels);
+    }
+
+    public void updateOnScreenIndicators(Parameters param) {
+        if (param == null) return;
+        String value = param.getFlashMode();
+        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);
+            }
+        }
+    }
+
+    public void setAspectRatio(double ratio) {
+      //  mPreviewFrameLayout.setAspectRatio(ratio);
+    }
+
+    public void showTimeLapseUI(boolean enable) {
+        if (mTimeLapseLabel != null) {
+            mTimeLapseLabel.setVisibility(enable ? View.VISIBLE : View.GONE);
+        }
+    }
+
+    private void openMenu() {
+        if (mPieRenderer != null) {
+            mPieRenderer.showInCenter();
+        }
+    }
+
+    public void showPopup(AbstractSettingPopup popup) {
+        hideUI();
+        mBlocker.setVisibility(View.INVISIBLE);
+        setShowMenu(false);
+        mPopup = popup;
+        mPopup.setVisibility(View.VISIBLE);
+        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
+                LayoutParams.WRAP_CONTENT);
+        lp.gravity = Gravity.CENTER;
+        ((FrameLayout) mRootView).addView(mPopup, lp);
+    }
+
+    public void dismissPopup(boolean topLevelOnly) {
+        dismissPopup(topLevelOnly, true);
+    }
+
+    public void dismissPopup(boolean topLevelPopupOnly, boolean fullScreen) {
+        if (fullScreen) {
+            showUI();
+            mBlocker.setVisibility(View.VISIBLE);
+        }
+        setShowMenu(fullScreen);
+        if (mPopup != null) {
+            ((FrameLayout) mRootView).removeView(mPopup);
+            mPopup = null;
+        }
+        mVideoMenu.popupDismissed(topLevelPopupOnly);
+    }
+
+    public void onShowSwitcherPopup() {
+        hidePieRenderer();
+    }
+
+    public boolean hidePieRenderer() {
+        if (mPieRenderer != null && mPieRenderer.showsItems()) {
+            mPieRenderer.hide();
+            return true;
+        }
+        return false;
+    }
+
+    // disable preview gestures after shutter is pressed
+    public void setShutterPressed(boolean pressed) {
+        if (mGestures == null) return;
+        mGestures.setEnabled(!pressed);
+    }
+
+    public void enableShutter(boolean enable) {
+        if (mShutterButton != null) {
+            mShutterButton.setEnabled(enable);
+        }
+    }
+
+    // PieListener
+    @Override
+    public void onPieOpened(int centerX, int centerY) {
+        // TODO: mActivity.cancelActivityTouchHandling();
+        // mActivity.setSwipingEnabled(false);
+    }
+
+    @Override
+    public void onPieClosed() {
+        // TODO: mActivity.setSwipingEnabled(true);
+    }
+
+    public void showPreviewBorder(boolean enable) {
+       // TODO: mPreviewFrameLayout.showBorder(enable);
+    }
+
+    // SingleTapListener
+    // Preview area is touched. Take a picture.
+    @Override
+    public void onSingleTapUp(View view, int x, int y) {
+        mController.onSingleTapUp(view, x, y);
+    }
+
+    public void showRecordingUI(boolean recording, boolean zoomSupported) {
+        mMenuButton.setVisibility(recording ? View.GONE : View.VISIBLE);
+        mOnScreenIndicators.setVisibility(recording ? View.GONE : View.VISIBLE);
+        if (recording) {
+            mShutterButton.setImageResource(R.drawable.btn_shutter_video_recording);
+            hideSwitcher();
+            mRecordingTimeView.setText("");
+            mRecordingTimeView.setVisibility(View.VISIBLE);
+            // The camera is not allowed to be accessed in older api levels during
+            // recording. It is therefore necessary to hide the zoom UI on older
+            // platforms.
+            // See the documentation of android.media.MediaRecorder.start() for
+            // further explanation.
+            if (!ApiHelper.HAS_ZOOM_WHEN_RECORDING && zoomSupported) {
+                // TODO: disable zoom UI here.
+            }
+        } else {
+            mShutterButton.setImageResource(R.drawable.btn_new_shutter_video);
+            showSwitcher();
+            mRecordingTimeView.setVisibility(View.GONE);
+            if (!ApiHelper.HAS_ZOOM_WHEN_RECORDING && zoomSupported) {
+                // TODO: enable zoom UI here.
+            }
+        }
+    }
+
+    public void showReviewImage(Bitmap bitmap) {
+        mReviewImage.setImageBitmap(bitmap);
+        mReviewImage.setVisibility(View.VISIBLE);
+    }
+
+    public void showReviewControls() {
+        Util.fadeOut(mShutterButton);
+        Util.fadeIn(mReviewDoneButton);
+        Util.fadeIn(mReviewPlayButton);
+        mReviewImage.setVisibility(View.VISIBLE);
+        mMenuButton.setVisibility(View.GONE);
+        mOnScreenIndicators.setVisibility(View.GONE);
+    }
+
+    public void hideReviewUI() {
+        mReviewImage.setVisibility(View.GONE);
+        mShutterButton.setEnabled(true);
+        mMenuButton.setVisibility(View.VISIBLE);
+        mOnScreenIndicators.setVisibility(View.VISIBLE);
+        Util.fadeOut(mReviewDoneButton);
+        Util.fadeOut(mReviewPlayButton);
+        Util.fadeIn(mShutterButton);
+    }
+
+    private void setShowMenu(boolean show) {
+        if (mOnScreenIndicators != null) {
+            mOnScreenIndicators.setVisibility(show ? View.VISIBLE : View.GONE);
+        }
+        if (mMenuButton != null) {
+            mMenuButton.setVisibility(show ? View.VISIBLE : View.GONE);
+        }
+    }
+
+    public void 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);
+        }
+    }
+
+    public void initializePopup(PreferenceGroup pref) {
+        mVideoMenu.initialize(pref);
+    }
+
+    public void initializeZoom(Parameters param) {
+        if (param == null || !param.isZoomSupported()) return;
+        mZoomMax = param.getMaxZoom();
+        mZoomRatios = param.getZoomRatios();
+        // Currently we use immediate zoom for fast zooming to get better UX and
+        // there is no plan to take advantage of the smooth zoom.
+        mZoomRenderer.setZoomMax(mZoomMax);
+        mZoomRenderer.setZoom(param.getZoom());
+        mZoomRenderer.setZoomValue(mZoomRatios.get(param.getZoom()));
+        mZoomRenderer.setOnZoomChangeListener(new ZoomChangeListener());
+    }
+
+    public void clickShutter() {
+        mShutterButton.performClick();
+    }
+
+    public void pressShutter(boolean pressed) {
+        mShutterButton.setPressed(pressed);
+    }
+
+    public View getShutterButton() {
+        return mShutterButton;
+    }
+
+    // Gestures and touch events
+
+    public boolean dispatchTouchEvent(MotionEvent m) {
+        if (mPopup != null || mSwitcher.showsPopup()) {
+            boolean handled = mRootView.dispatchTouchEvent(m);
+            if (!handled && mPopup != null) {
+                dismissPopup(false);
+            }
+            return handled;
+        } else if (mGestures != null && mRenderOverlay != null) {
+            if (mGestures.dispatchTouch(m)) {
+                return true;
+            } else {
+                return mRootView.dispatchTouchEvent(m);
+            }
+        }
+        return true;
+    }
+    public void setRecordingTime(String text) {
+        mRecordingTimeView.setText(text);
+    }
+
+    public void setRecordingTimeTextColor(int color) {
+        mRecordingTimeView.setTextColor(color);
+    }
+
+    public boolean isVisible() {
+        return mTextureView.getVisibility() == View.VISIBLE;
+    }
+
+    private class ZoomChangeListener implements ZoomRenderer.OnZoomChangedListener {
+        @Override
+        public void onZoomValueChanged(int index) {
+            int newZoom = mController.onZoomChanged(index);
+            if (mZoomRenderer != null) {
+                mZoomRenderer.setZoomValue(mZoomRatios.get(newZoom));
+            }
+        }
+
+        @Override
+        public void onZoomStart() {
+        }
+
+        @Override
+        public void onZoomEnd() {
+        }
+    }
+
+    @Override
+    public void onSwipe(int direction) {
+        if (direction == PreviewGestures.DIR_UP) {
+            openMenu();
+        }
+    }
+
+    public SurfaceTexture getSurfaceTexture() {
+        synchronized (mLock) {
+            if (mSurfaceTexture == null) {
+                try {
+                    mLock.wait();
+                } catch (InterruptedException e) {
+                    Log.w(TAG, "Unexpected interruption when waiting to get surface texture");
+                }
+            }
+        }
+        return mSurfaceTexture;
+    }
+
+    // SurfaceTexture callbacks
+    @Override
+    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+        synchronized (mLock) {
+            mSurfaceTexture = surface;
+            mLock.notifyAll();
+        }
+    }
+
+    @Override
+    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+        mSurfaceTexture = null;
+        mController.stopPreview();
+        Log.d(TAG, "surfaceTexture is destroyed");
+        return true;
+    }
+
+    @Override
+    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+    }
+
+    @Override
+    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+    }
+
+    // SurfaceHolder callbacks
+    @Override
+    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+        Log.v(TAG, "Surface changed. width=" + width + ". height=" + height);
+    }
+
+    @Override
+    public void surfaceCreated(SurfaceHolder holder) {
+        Log.v(TAG, "Surface created");
+    }
+
+    @Override
+    public void surfaceDestroyed(SurfaceHolder holder) {
+        Log.v(TAG, "Surface destroyed");
+        mController.stopPreview();
+    }
+}
diff --git a/src/com/android/camera/data/CameraDataAdapter.java b/src/com/android/camera/data/CameraDataAdapter.java
index 1fb9465..2bce1b4 100644
--- a/src/com/android/camera/data/CameraDataAdapter.java
+++ b/src/com/android/camera/data/CameraDataAdapter.java
@@ -117,6 +117,12 @@
         }
     }
 
+    public void removeData(int dataID) {
+        if (dataID >= mImages.size()) return;
+        LocalData d = mImages.remove(dataID);
+        mListener.onDataRemoved(dataID, d);
+    }
+
     private LocalData buildCameraImageData(int width, int height) {
         LocalData d = new CameraPreviewData(width, height);
         return d;
diff --git a/src/com/android/camera/data/LocalData.java b/src/com/android/camera/data/LocalData.java
index 1fda9eb..1f60160 100644
--- a/src/com/android/camera/data/LocalData.java
+++ b/src/com/android/camera/data/LocalData.java
@@ -208,6 +208,10 @@
             ImageColumns.HEIGHT,        // 8, int
         };
 
+        private static final int mSupportedAction =
+                FilmStripView.ImageData.ACTION_DEMOTE
+                | FilmStripView.ImageData.ACTION_PROMOTE;
+
         // 32K buffer.
         private static final byte[] DECODE_TEMP_STORAGE = new byte[32 * 1024];
 
@@ -263,6 +267,11 @@
         }
 
         @Override
+        public boolean isActionSupported(int action) {
+            return ((action & mSupportedAction) != 0);
+        }
+
+        @Override
         protected BitmapLoadTask getBitmapLoadTask(
                 ImageView v, int decodeWidth, int decodeHeight) {
             return new PhotoBitmapLoadTask(v, decodeWidth, decodeHeight);
@@ -327,6 +336,11 @@
         public static final int COL_WIDTH = 6;
         public static final int COL_HEIGHT = 7;
 
+        private static final int mSupportedActions =
+                FilmStripView.ImageData.ACTION_DEMOTE
+                | FilmStripView.ImageData.ACTION_PROMOTE
+                | FilmStripView.ImageData.ACTION_PLAY;
+
         static final String QUERY_ORDER = VideoColumns.DATE_TAKEN + " DESC, "
                 + VideoColumns._ID + " DESC";
         static final String[] QUERY_PROJECTION = {
@@ -382,6 +396,11 @@
         }
 
         @Override
+        public boolean isActionSupported(int action) {
+            return ((action & mSupportedActions) != 0);
+        }
+
+        @Override
         protected BitmapLoadTask getBitmapLoadTask(
                 ImageView v, int decodeWidth, int decodeHeight) {
             return new VideoBitmapLoadTask(v);
diff --git a/src/com/android/camera/ui/FaceView.java b/src/com/android/camera/ui/FaceView.java
index f4dd823..2415049 100644
--- a/src/com/android/camera/ui/FaceView.java
+++ b/src/com/android/camera/ui/FaceView.java
@@ -33,13 +33,15 @@
 
 import com.android.camera.CameraActivity;
 import com.android.camera.CameraScreenNail;
+import com.android.camera.NewPhotoUI;
 import com.android.camera.Util;
 import com.android.gallery3d.R;
 import com.android.gallery3d.common.ApiHelper;
 
 @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
 public class FaceView extends View
-    implements FocusIndicator, Rotatable {
+    implements FocusIndicator, Rotatable,
+    NewPhotoUI.SurfaceTextureSizeChangedListener {
     private static final String TAG = "CAM FaceView";
     private final boolean LOGV = false;
     // The value for android.hardware.Camera.setDisplayOrientation.
@@ -95,6 +97,12 @@
         mPaint.setStrokeWidth(res.getDimension(R.dimen.face_circle_stroke));
     }
 
+    @Override
+    public void onSurfaceTextureSizeChanged(int uncroppedWidth, int uncroppedHeight) {
+        mUncroppedWidth = uncroppedWidth;
+        mUncroppedHeight = uncroppedHeight;
+    }
+
     public void setFaces(Face[] faces) {
         if (LOGV) Log.v(TAG, "Num of faces=" + faces.length);
         if (mPause) return;
diff --git a/src/com/android/camera/ui/FilmStripGestureRecognizer.java b/src/com/android/camera/ui/FilmStripGestureRecognizer.java
index f0e2534..f870b58 100644
--- a/src/com/android/camera/ui/FilmStripGestureRecognizer.java
+++ b/src/com/android/camera/ui/FilmStripGestureRecognizer.java
@@ -35,6 +35,7 @@
         boolean onScaleBegin(float focusX, float focusY);
         boolean onScale(float focusX, float focusY, float scale);
         boolean onDown(float x, float y);
+        boolean onUp(float x, float y);
         void onScaleEnd();
     }
 
@@ -50,8 +51,12 @@
                 context, new MyScaleListener());
     }
 
-    public boolean onTouchEvent(MotionEvent event) {
-        return mGestureDetector.onTouchEvent(event) || mScaleDetector.onTouchEvent(event);
+    public void onTouchEvent(MotionEvent event) {
+        mGestureDetector.onTouchEvent(event);
+        mScaleDetector.onTouchEvent(event);
+        if (event.getAction() == MotionEvent.ACTION_UP) {
+            mListener.onUp(event.getX(), event.getY());
+        }
     }
 
     private class MyGestureListener
diff --git a/src/com/android/camera/ui/FilmStripView.java b/src/com/android/camera/ui/FilmStripView.java
index c836c91..6c90576 100644
--- a/src/com/android/camera/ui/FilmStripView.java
+++ b/src/com/android/camera/ui/FilmStripView.java
@@ -26,13 +26,14 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.animation.DecelerateInterpolator;
+import android.view.animation.LinearInterpolator;
 import android.widget.Scroller;
 
 public class FilmStripView extends ViewGroup {
     private static final String TAG = FilmStripView.class.getSimpleName();
     private static final int BUFFER_SIZE = 5;
     // Horizontal padding of children.
-    private static final int H_PADDING = 50;
+    private static final int H_PADDING = 100;
     // Duration to go back to the first.
     private static final int DURATION_BACK_ANIM = 500;
     private static final int DURATION_SCROLL_TO_FILMSTRIP = 350;
@@ -48,9 +49,12 @@
     private int mCurrentInfo;
     private float mScale;
     private GeometryAnimator mGeometryAnimator;
+    private LinearInterpolator mLinearInterpolator;
     private int mCenterPosition = -1;
     private ViewInfo[] mViewInfo = new ViewInfo[BUFFER_SIZE];
 
+    private Listener mListener;
+
     // This is used to resolve the misalignment problem when the device
     // orientation is changed. If the current item is in fullscreen, it might
     // be shifted because mCenterPosition is not adjusted with the orientation.
@@ -69,7 +73,8 @@
         // | and &.
         public static final int ACTION_NONE = 0;
         public static final int ACTION_PROMOTE = 1;
-        public static final int ACTION_DEMOTE = 2;
+        public static final int ACTION_DEMOTE = (1 << 1);
+        public static final int ACTION_PLAY = (1 << 2);
 
         // SIZE_FULL means disgard the width or height when deciding the view size
         // of this ImageData, just use full screen size.
@@ -102,8 +107,8 @@
             // Only some of the data is changed. The listener should check
             // if any thing needs to be updated.
             public void onDataUpdated(UpdateReporter reporter);
-            public void onDataInserted(int dataID);
-            public void onDataRemoved(int dataID);
+            public void onDataInserted(int dataID, ImageData data);
+            public void onDataRemoved(int dataID, ImageData data);
         }
 
         public int getTotalNumber();
@@ -114,13 +119,17 @@
         public void setListener(Listener listener);
     }
 
+    public interface Listener {
+        public void onDataPromoted(int dataID);
+        public void onDataDemoted(int dataID);
+    }
+
     // A helper class to tract and calculate the view coordination.
     private static class ViewInfo {
         private int mDataID;
         // the position of the left of the view in the whole filmstrip.
         private int mLeftPosition;
         private View mView;
-        private float mOffsetY;
 
         public ViewInfo(int id, View v) {
             v.setPivotX(0f);
@@ -128,13 +137,16 @@
             mDataID = id;
             mView = v;
             mLeftPosition = -1;
-            mOffsetY = 0;
         }
 
         public int getID() {
             return mDataID;
         }
 
+        public void setID(int id) {
+            mDataID = id;
+        }
+
         public void setLeftPosition(int pos) {
             mLeftPosition = pos;
         }
@@ -143,12 +155,24 @@
             return mLeftPosition;
         }
 
-        public float getOffsetY() {
-            return mOffsetY;
+        public float getTranslationY(float scale) {
+            return mView.getTranslationY() / scale;
         }
 
-        public void setOffsetY(float offset) {
-            mOffsetY = offset;
+        public float getTranslationX(float scale) {
+            return mView.getTranslationX();
+        }
+
+        public void setTranslationY(float transY, float scale) {
+            mView.setTranslationY(transY * scale);
+        }
+
+        public void setTranslationX(float transX, float scale) {
+            mView.setTranslationX(transX * scale);
+        }
+
+        public void translateXBy(float transX, float scale) {
+            mView.setTranslationX(mView.getTranslationX() + transX * scale);
         }
 
         public int getCenterX() {
@@ -168,8 +192,7 @@
             // drawArea is where to layout in.
             // refCenter is the absolute horizontal position of the center of drawArea.
             int left = (int) (drawArea.centerX() + (mLeftPosition - refCenter) * scale);
-            int top = (int) (drawArea.centerY() - (mView.getMeasuredHeight() / 2) * scale
-                    + mOffsetY);
+            int top = (int) (drawArea.centerY() - (mView.getMeasuredHeight() / 2) * scale);
             layoutAt(left, top);
             mView.setScaleX(scale);
             mView.setScaleY(scale);
@@ -202,10 +225,15 @@
         mContext = context;
         mScale = 1.0f;
         mGeometryAnimator = new GeometryAnimator(context);
+        mLinearInterpolator = new LinearInterpolator();
         mGestureRecognizer =
                 new FilmStripGestureRecognizer(context, new MyGestureReceiver());
     }
 
+    public void setListener(Listener l) {
+        mListener = l;
+    }
+
     public float getScale() {
         return mScale;
     }
@@ -251,8 +279,12 @@
 
             int imageWidth = mDataAdapter.getImageData(info.getID()).getWidth();
             int imageHeight = mDataAdapter.getImageData(info.getID()).getHeight();
-            if (imageWidth == ImageData.SIZE_FULL) imageWidth = boundWidth;
-            if (imageHeight == ImageData.SIZE_FULL) imageHeight = boundHeight;
+            if (imageWidth == ImageData.SIZE_FULL) {
+                imageWidth = boundWidth;
+            }
+            if (imageHeight == ImageData.SIZE_FULL) {
+                imageHeight = boundHeight;
+            }
 
             int scaledWidth = boundWidth;
             int scaledHeight = boundHeight;
@@ -262,7 +294,6 @@
             } else {
                 scaledWidth = imageWidth * scaledHeight / imageHeight;
             }
-            scaledWidth += H_PADDING * 2;
             mViewInfo[i].getView().measure(
                     View.MeasureSpec.makeMeasureSpec(scaledWidth, wMode)
                     , View.MeasureSpec.makeMeasureSpec(scaledHeight, hMode));
@@ -302,7 +333,6 @@
         data.prepare();
         View v = mDataAdapter.getView(mContext, dataID);
         if (v == null) return null;
-        v.setPadding(H_PADDING, 0, H_PADDING, 0);
         ViewInfo info = new ViewInfo(dataID, v);
         addView(info.getView());
         return info;
@@ -389,7 +419,7 @@
             if (curr != null) {
                 ViewInfo next = mViewInfo[infoID + 1];
                 curr.setLeftPosition(
-                        next.getLeftPosition() - curr.getView().getMeasuredWidth());
+                        next.getLeftPosition() - curr.getView().getMeasuredWidth() - H_PADDING);
                 curr.layoutIn(mDrawArea, mCenterPosition, mScale);
             }
         }
@@ -400,7 +430,7 @@
             if (curr != null) {
                 ViewInfo prev = mViewInfo[infoID - 1];
                 curr.setLeftPosition(
-                        prev.getLeftPosition() + prev.getView().getMeasuredWidth());
+                        prev.getLeftPosition() + prev.getView().getMeasuredWidth() + H_PADDING);
                 curr.layoutIn(mDrawArea, mCenterPosition, mScale);
             }
         }
@@ -430,6 +460,107 @@
         }
     }
 
+    private void slideViewBack(View v) {
+        v.animate()
+                .translationX(0)
+                .alpha(1f)
+                .setDuration(DURATION_GEOMETRY_ADJUST)
+                .start();
+    }
+
+    private void updateRemoval(int removedInfo, final ImageData data) {
+        final View removedView = mViewInfo[removedInfo].getView();
+        final int offsetX = (int) (removedView.getMeasuredWidth() + H_PADDING);
+
+        for (int i = removedInfo + 1; i < BUFFER_SIZE; i++) {
+            if (mViewInfo[i] != null) {
+                mViewInfo[i].setID(mViewInfo[i].getID() - 1);
+                mViewInfo[i].setLeftPosition(mViewInfo[i].getLeftPosition() - offsetX);
+            }
+        }
+
+        if (removedInfo >= mCurrentInfo
+                && mViewInfo[removedInfo].getID() < mDataAdapter.getTotalNumber()) {
+            // fill the removed info by left shift when the current one or anyone on the
+            // right is removed, and there's more data on the right available.
+            for (int i = removedInfo; i < BUFFER_SIZE - 1; i++) {
+                mViewInfo[i] = mViewInfo[i + 1];
+            }
+
+            // pull data out from the DataAdapter for the last one.
+            int curr = BUFFER_SIZE - 1;
+            int prev = curr - 1;
+            if (mViewInfo[prev] != null) {
+                mViewInfo[curr] = buildInfoFromData(mViewInfo[prev].getID() + 1);
+            }
+
+
+            for (int i = removedInfo; i < BUFFER_SIZE - 1; i++) {
+                if (mViewInfo[i] != null) {
+                    mViewInfo[i].setTranslationX(offsetX, mScale);
+                }
+            }
+
+            // The end of the filmstrip might have been changed.
+            // The mCenterPosition might be out of the bound.
+            ViewInfo currInfo = mViewInfo[mCurrentInfo];
+            if (currInfo.getID() == mDataAdapter.getTotalNumber() - 1
+                    && mCenterPosition > currInfo.getCenterX()) {
+                int adjustDiff = currInfo.getCenterX() - mCenterPosition;
+                mCenterPosition = currInfo.getCenterX();
+                for (int i = 0; i < BUFFER_SIZE; i++) {
+                    if (mViewInfo[i] != null) {
+                        mViewInfo[i].translateXBy(adjustDiff, mScale);
+                    }
+                }
+            }
+        } else {
+            // fill the removed place by right shift
+            mCenterPosition -= offsetX;
+
+            for (int i = removedInfo; i > 0; i--) {
+                mViewInfo[i] = mViewInfo[i - 1];
+                if (mViewInfo[i] != null) {
+                    mViewInfo[i].setTranslationX(-offsetX, mScale);
+                }
+            }
+
+            // pull data out from the DataAdapter for the first one.
+            int curr = 0;
+            int next = curr + 1;
+            if (mViewInfo[next] != null) {
+                mViewInfo[curr] = buildInfoFromData(mViewInfo[next].getID() - 1);
+            }
+        }
+
+        // Now, slide every one back.
+        for (int i = 0; i < BUFFER_SIZE; i++) {
+            if (mViewInfo[i] != null
+                    && mViewInfo[i].getTranslationX(mScale) != 0f) {
+                slideViewBack(mViewInfo[i].getView());
+            }
+        }
+
+        int transY = getHeight() / 8;
+        if (removedView.getTranslationY() < 0) {
+            transY = -transY;
+        }
+        removedView.animate()
+                .alpha(0f)
+                .translationYBy(transY)
+                .setInterpolator(mLinearInterpolator)
+                .setDuration(DURATION_GEOMETRY_ADJUST)
+                .withEndAction(new Runnable() {
+                    @Override
+                    public void run() {
+                        removeView(removedView);
+                        data.recycle();
+                    }
+                })
+                .start();
+        layoutChildren();
+    }
+
     public void setDataAdapter(DataAdapter adapter) {
         mDataAdapter = adapter;
         mDataAdapter.suggestDecodeSize(getMeasuredWidth(), getMeasuredHeight());
@@ -445,11 +576,18 @@
             }
 
             @Override
-            public void onDataInserted(int dataID) {
+            public void onDataInserted(int dataID, ImageData data) {
             }
 
             @Override
-            public void onDataRemoved(int dataID) {
+            public void onDataRemoved(int dataID, ImageData data) {
+                int removedInfo = 0;
+                for (; removedInfo < BUFFER_SIZE; removedInfo++) {
+                    if (mViewInfo[removedInfo] != null
+                            && mViewInfo[removedInfo].getID() == dataID) break;
+                }
+                if (removedInfo == BUFFER_SIZE) return;
+                updateRemoval(removedInfo, data);
             }
         });
     }
@@ -507,7 +645,9 @@
                 }
             } else {
                 ViewInfo next = mViewInfo[i + 1];
-                if (next != null) mViewInfo[i] = buildInfoFromData(next.getID() - 1);
+                if (next != null) {
+                    mViewInfo[i] = buildInfoFromData(next.getID() - 1);
+                }
             }
         }
 
@@ -521,7 +661,9 @@
                 }
             } else {
                 ViewInfo prev = mViewInfo[i - 1];
-                if (prev != null) mViewInfo[i] = buildInfoFromData(prev.getID() + 1);
+                if (prev != null) {
+                    mViewInfo[i] = buildInfoFromData(prev.getID() + 1);
+                }
             }
         }
     }
@@ -554,6 +696,18 @@
         layoutChildren();
     }
 
+    private void promoteData(int infoID, int dataID) {
+        if (mListener != null) {
+            mListener.onDataPromoted(dataID);
+        }
+    }
+
+    private void demoteData(int infoID, int dataID) {
+        if (mListener != null) {
+            mListener.onDataDemoted(dataID);
+        }
+    }
+
     // GeometryAnimator controls all the geometry animations. It passively
     // tells the geometry information on demand.
     private class GeometryAnimator implements
@@ -731,32 +885,65 @@
         }
 
         @Override
-        public boolean onScroll(float x, float y, float dx, float dy) {
-            int deltaX = (int) (dx / mScale);
-            if (deltaX > 0 && isInCameraFullscreen()) {
-                mGeometryAnimator.unlockPosition();
-                mGeometryAnimator.scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST, false);
-            }
-
-            mCenterPosition += deltaX;
-
-            // Vertical part. Promote or demote.
-            int scaledDeltaY = (int) (dy / mScale);
-
+        public boolean onUp(float x, float y) {
+            float halfH = getHeight() / 2;
             for (int i = 0; i < BUFFER_SIZE; i++) {
                 if (mViewInfo[i] == null) continue;
-                Rect hitRect = new Rect();
-                View v = mViewInfo[i].getView();
-                v.getHitRect(hitRect);
-                if (hitRect.contains((int) x, (int) y)) {
-                    ImageData data = mDataAdapter.getImageData(mViewInfo[i].getID());
-                    if ((data.isActionSupported(ImageData.ACTION_DEMOTE) && dy > 0)
-                            || (data.isActionSupported(ImageData.ACTION_PROMOTE) && dy < 0)) {
-                        mViewInfo[i].setOffsetY(mViewInfo[i].getOffsetY() - dy);
-                    }
-                    break;
+                float transY = mViewInfo[i].getTranslationY(mScale);
+                if (transY == 0) continue;
+                int id = mViewInfo[i].getID();
+
+                if (mDataAdapter.getImageData(id)
+                        .isActionSupported(ImageData.ACTION_DEMOTE)
+                        && transY > halfH) {
+                    demoteData(i, id);
+                } else if (mDataAdapter.getImageData(id)
+                        .isActionSupported(ImageData.ACTION_PROMOTE)
+                        && transY < -halfH) {
+                    promoteData(i, id);
+                } else {
+                    // put the view back.
+                    mViewInfo[i].getView().animate()
+                            .translationY(0f)
+                            .alpha(1f)
+                            .setDuration(DURATION_GEOMETRY_ADJUST)
+                            .start();
                 }
             }
+            return false;
+        }
+
+        @Override
+        public boolean onScroll(float x, float y, float dx, float dy) {
+            if (Math.abs(dx) > Math.abs(dy)) {
+                int deltaX = (int) (dx / mScale);
+                if (deltaX > 0 && isInCameraFullscreen()) {
+                    mGeometryAnimator.unlockPosition();
+                    mGeometryAnimator.scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST, false);
+                }
+                mCenterPosition += deltaX;
+            } else {
+                // Vertical part. Promote or demote.
+                //int scaledDeltaY = (int) (dy * mScale);
+                int hit = 0;
+                Rect hitRect = new Rect();
+                for (; hit < BUFFER_SIZE; hit++) {
+                    if (mViewInfo[hit] == null) continue;
+                    mViewInfo[hit].getView().getHitRect(hitRect);
+                    if (hitRect.contains((int) x, (int) y)) break;
+                }
+                if (hit == BUFFER_SIZE) return false;
+
+                ImageData data = mDataAdapter.getImageData(mViewInfo[hit].getID());
+                float transY = mViewInfo[hit].getTranslationY(mScale) - dy / mScale;
+                if (!data.isActionSupported(ImageData.ACTION_DEMOTE) && transY > 0f) {
+                    transY = 0f;
+                }
+                if (!data.isActionSupported(ImageData.ACTION_PROMOTE) && transY < 0f) {
+                    transY = 0f;
+                }
+                mViewInfo[hit].setTranslationY(transY, mScale);
+            }
 
             layoutChildren();
             return true;
@@ -764,21 +951,25 @@
 
         @Override
         public boolean onFling(float velocityX, float velocityY) {
-            float scaledVelocityX = velocityX / mScale;
-            if (isInCameraFullscreen() && scaledVelocityX < 0) {
-                mGeometryAnimator.unlockPosition();
-                mGeometryAnimator.scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST, false);
+            if (Math.abs(velocityX) > Math.abs(velocityY)) {
+                float scaledVelocityX = velocityX / mScale;
+                if (isInCameraFullscreen() && scaledVelocityX < 0) {
+                    mGeometryAnimator.unlockPosition();
+                    mGeometryAnimator.scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST, false);
+                }
+                ViewInfo info = mViewInfo[mCurrentInfo];
+                int w = getWidth();
+                if (info == null) return true;
+                mGeometryAnimator.fling((int) -scaledVelocityX,
+                        // estimation of possible length on the left
+                        info.getLeftPosition() - info.getID() * w * 2,
+                        // estimation of possible length on the right
+                        info.getLeftPosition()
+                        + (mDataAdapter.getTotalNumber() - info.getID()) * w * 2);
+                layoutChildren();
+            } else {
+                // ignore horizontal fling.
             }
-            ViewInfo info = mViewInfo[mCurrentInfo];
-            int w = getWidth();
-            if (info == null) return true;
-            mGeometryAnimator.fling((int) -scaledVelocityX,
-                    // estimation of possible length on the left
-                    info.getLeftPosition() - info.getID() * w * 2,
-                    // estimation of possible length on the right
-                    info.getLeftPosition()
-                    + (mDataAdapter.getTotalNumber() - info.getID()) * w * 2);
-            layoutChildren();
             return true;
         }
 
@@ -793,10 +984,14 @@
         public boolean onScale(float focusX, float focusY, float scale) {
             if (isInCameraFullscreen()) return false;
 
-            mScaleTrend = mScaleTrend * 0.5f + scale * 0.5f;
+            mScaleTrend = mScaleTrend * 0.3f + scale * 0.7f;
             mScale *= scale;
-            if (mScale <= FILM_STRIP_SCALE) mScale = FILM_STRIP_SCALE;
-            if (mScale >= MAX_SCALE) mScale = MAX_SCALE;
+            if (mScale <= FILM_STRIP_SCALE) {
+                mScale = FILM_STRIP_SCALE;
+            }
+            if (mScale >= MAX_SCALE) {
+                mScale = MAX_SCALE;
+            }
             layoutChildren();
             return true;
         }
diff --git a/src/com/android/camera/ui/NewCameraRootView.java b/src/com/android/camera/ui/NewCameraRootView.java
new file mode 100644
index 0000000..a50e41a
--- /dev/null
+++ b/src/com/android/camera/ui/NewCameraRootView.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.ui;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Debug;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.camera.Util;
+import com.android.gallery3d.R;
+
+public class NewCameraRootView extends FrameLayout {
+
+    private int mTopMargin = 0;
+    private int mBottomMargin = 0;
+    private int mLeftMargin = 0;
+    private int mRightMargin = 0;
+    private Rect mCurrentInsets;
+    private int mOffset = 0;
+    public NewCameraRootView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+    }
+
+    @Override
+    protected boolean fitSystemWindows(Rect insets) {
+        super.fitSystemWindows(insets);
+        mCurrentInsets = insets;
+        // insets include status bar, navigation bar, etc
+        // In this case, we are only concerned with the size of nav bar
+        if (mOffset > 0) return true;
+
+        if (insets.bottom > 0) {
+            mOffset = insets.bottom;
+        } else if (insets.right > 0) {
+            mOffset = insets.right;
+        }
+        return true;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int rotation = Util.getDisplayRotation((Activity) getContext());
+        // all the layout code assumes camera device orientation to be portrait
+        // adjust rotation for landscape
+        int orientation = getResources().getConfiguration().orientation;
+        int camOrientation = (rotation % 180 == 0) ? Configuration.ORIENTATION_PORTRAIT
+                : Configuration.ORIENTATION_LANDSCAPE;
+        if (camOrientation != orientation) {
+            rotation = (rotation + 90) % 360;
+        }
+        // calculate margins
+        mLeftMargin = 0;
+        mRightMargin = 0;
+        mBottomMargin = 0;
+        mTopMargin = 0;
+        switch (rotation) {
+            case 0:
+                mBottomMargin += mOffset;
+                break;
+            case 90:
+                mRightMargin += mOffset;
+                break;
+            case 180:
+                mTopMargin += mOffset;
+                break;
+            case 270:
+                mLeftMargin += mOffset;
+                break;
+        }
+        if (mCurrentInsets != null) {
+            if (mCurrentInsets.right > 0) {
+                // navigation bar on the right
+                mRightMargin = mRightMargin > 0 ? mRightMargin : mCurrentInsets.right;
+            } else {
+                // navigation bar on the bottom
+                mBottomMargin = mBottomMargin > 0 ? mBottomMargin : mCurrentInsets.bottom;
+            }
+        }
+        // make sure all the children are resized
+        super.onMeasure(widthMeasureSpec - mLeftMargin - mRightMargin,
+                heightMeasureSpec - mTopMargin - mBottomMargin);
+        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    @Override
+    public void onLayout(boolean changed, int l, int t, int r, int b) {
+        r -= l;
+        b -= t;
+        l = 0;
+        t = 0;
+        int orientation = getResources().getConfiguration().orientation;
+        // Lay out children
+        for (int i = 0; i < getChildCount(); i++) {
+            View v = getChildAt(i);
+            if (v instanceof CameraControls) {
+                // Lay out camera controls to center on the short side of the screen
+                // so that they stay in place during rotation
+                int width = v.getMeasuredWidth();
+                int height = v.getMeasuredHeight();
+                if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+                    int left = (l + r - width) / 2;
+                    v.layout(left, t + mTopMargin, left + width, b - mBottomMargin);
+                } else {
+                    int top = (t + b - height) / 2;
+                    v.layout(l + mLeftMargin, top, r - mRightMargin, top + height);
+                }
+            } else {
+                v.layout(l + mLeftMargin, t + mTopMargin, r - mRightMargin, b - mBottomMargin);
+            }
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/FilterShowActivity.java b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
index 4b65378..27b5fb9 100644
--- a/src/com/android/gallery3d/filtershow/FilterShowActivity.java
+++ b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
@@ -443,6 +443,9 @@
     }
 
     public void showRepresentation(FilterRepresentation representation) {
+        if (representation == null) {
+            return;
+        }
         useFilterRepresentation(representation);
 
         // show representation
@@ -832,7 +835,8 @@
         loadXML();
         loadMainPanel();
 
-        if (!mShowingTinyPlanet) {
+        // mLoadBitmapTask==null implies you have looked at the intent
+        if (!mShowingTinyPlanet && (mLoadBitmapTask == null)) {
             mCategoryFiltersAdapter.removeTinyPlanet();
         }
         final View loading = findViewById(R.id.loading);
diff --git a/src/com/android/gallery3d/filtershow/HistoryAdapter.java b/src/com/android/gallery3d/filtershow/HistoryAdapter.java
index 1d47160..79460be 100644
--- a/src/com/android/gallery3d/filtershow/HistoryAdapter.java
+++ b/src/com/android/gallery3d/filtershow/HistoryAdapter.java
@@ -133,21 +133,8 @@
     }
 
     public void addHistoryItem(ImagePreset preset) {
-        if (canAddHistoryItem(preset)) {
-            insert(preset, 0);
-            updateMenuItems();
-        }
-    }
-
-    public boolean canAddHistoryItem(ImagePreset preset) {
-        if (getCount() > 0 && getCurrent().same(preset)) {
-            // we may still want to insert if the previous
-            // history element isn't the same
-            if (getLast().historyName().equalsIgnoreCase(preset.historyName())) {
-                return false;
-            }
-        }
-        return true;
+        insert(preset, 0);
+        updateMenuItems();
     }
 
     @Override
@@ -164,9 +151,6 @@
             }
             mCurrentPresetPosition = position;
             this.notifyDataSetChanged();
-            if (!canAddHistoryItem(preset)) {
-                return;
-            }
         }
         super.insert(preset, position);
         mCurrentPresetPosition = position;
diff --git a/src/com/android/gallery3d/filtershow/category/CategoryAdapter.java b/src/com/android/gallery3d/filtershow/category/CategoryAdapter.java
index e310b2f..0a65cd9 100644
--- a/src/com/android/gallery3d/filtershow/category/CategoryAdapter.java
+++ b/src/com/android/gallery3d/filtershow/category/CategoryAdapter.java
@@ -21,24 +21,25 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.AbsListView;
 import android.widget.ArrayAdapter;
-import android.widget.LinearLayout;
 import android.widget.ListView;
+
 import com.android.gallery3d.R;
 import com.android.gallery3d.filtershow.filters.FilterRepresentation;
-import com.android.gallery3d.filtershow.filters.FilterTinyPlanetRepresentation;
-import com.android.gallery3d.filtershow.filters.ImageFilter;
 import com.android.gallery3d.filtershow.filters.ImageFilterTinyPlanet;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.presets.ImagePreset;
 import com.android.gallery3d.filtershow.ui.FilterIconButton;
 
 public class CategoryAdapter extends ArrayAdapter<Action> {
 
     private static final String LOGTAG = "CategoryAdapter";
     private int mItemHeight = 200;
-    private ListView mContainer;
+    private View mContainer;
     private int mItemWidth = ListView.LayoutParams.MATCH_PARENT;
     private boolean mUseFilterIconButton = false;
+    private int mSelectedPosition;
+    int mCategory;
 
     public CategoryAdapter(Context context, int textViewResourceId) {
         super(context, textViewResourceId);
@@ -62,6 +63,22 @@
         action.setAdapter(this);
     }
 
+    public void initializeSelection(int category) {
+        mCategory = category;
+        if (category == MainPanel.LOOKS || category == MainPanel.BORDERS) {
+            ImagePreset preset = MasterImage.getImage().getPreset();
+            if (preset != null) {
+                for (int i = 0; i < getCount(); i++) {
+                    if (preset.historyName().equals(getItem(i).getRepresentation().getName())) {
+                        mSelectedPosition = i;
+                    }
+                }
+            }
+        } else {
+            mSelectedPosition = -1;
+        }
+    }
+
     @Override
     public View getView(int position, View convertView, ViewGroup parent) {
         if (mUseFilterIconButton) {
@@ -73,27 +90,55 @@
             FilterIconButton view = (FilterIconButton) convertView;
             Action action = getItem(position);
             view.setAction(action);
-            view.setup(action.getName(), null);
+            view.setup(action.getName(), null, this);
             view.setLayoutParams(
                     new ListView.LayoutParams(mItemWidth, mItemHeight));
+            view.setTag(position);
+            if (mCategory == MainPanel.LOOKS || mCategory == MainPanel.BORDERS) {
+                view.setBackground(null);
+            }
             return view;
         }
         if (convertView == null) {
             convertView = new CategoryView(getContext());
         }
         CategoryView view = (CategoryView) convertView;
-        view.setAction(getItem(position));
+        view.setAction(getItem(position), this);
         view.setLayoutParams(
                 new ListView.LayoutParams(mItemWidth, mItemHeight));
+        view.setTag(position);
         return view;
     }
 
-    public void setContainer(ListView container) {
-        mContainer = container;
+    public void setSelected(View v) {
+        int old = mSelectedPosition;
+        mSelectedPosition = (Integer) v.getTag();
+        if (old != -1) {
+            invalidateView(old);
+        }
+        invalidateView(mSelectedPosition);
     }
 
-    public ListView getContainer() {
-        return mContainer;
+    public boolean isSelected(View v) {
+        return (Integer) v.getTag() == mSelectedPosition;
+    }
+
+    private void invalidateView(int position) {
+        View child = null;
+        if (mContainer instanceof ListView) {
+            ListView lv = (ListView) mContainer;
+            child = lv.getChildAt(position - lv.getFirstVisiblePosition());
+        } else {
+            CategoryTrack ct = (CategoryTrack) mContainer;
+            child = ct.getChildAt(position);
+        }
+        if (child != null) {
+            child.invalidate();
+        }
+    }
+
+    public void setContainer(View container) {
+        mContainer = container;
     }
 
     public void imageLoaded() {
diff --git a/src/com/android/gallery3d/filtershow/category/CategoryPanel.java b/src/com/android/gallery3d/filtershow/category/CategoryPanel.java
index 9ddfcab..abae80f 100644
--- a/src/com/android/gallery3d/filtershow/category/CategoryPanel.java
+++ b/src/com/android/gallery3d/filtershow/category/CategoryPanel.java
@@ -50,18 +50,22 @@
         switch (adapter) {
             case MainPanel.LOOKS: {
                 mAdapter = activity.getCategoryLooksAdapter();
+                mAdapter.initializeSelection(MainPanel.LOOKS);
                 break;
             }
             case MainPanel.BORDERS: {
                 mAdapter = activity.getCategoryBordersAdapter();
+                mAdapter.initializeSelection(MainPanel.BORDERS);
                 break;
             }
             case MainPanel.GEOMETRY: {
                 mAdapter = activity.getCategoryGeometryAdapter();
+                mAdapter.initializeSelection(MainPanel.GEOMETRY);
                 break;
             }
             case MainPanel.FILTERS: {
                 mAdapter = activity.getCategoryFiltersAdapter();
+                mAdapter.initializeSelection(MainPanel.FILTERS);
                 break;
             }
         }
@@ -90,6 +94,7 @@
             CategoryTrack panel = (CategoryTrack) panelView;
             mAdapter.setUseFilterIconButton(true);
             panel.setAdapter(mAdapter);
+            mAdapter.setContainer(panel);
         } else {
             ListView panel = (ListView) main.findViewById(R.id.listItems);
             panel.setAdapter(mAdapter);
diff --git a/src/com/android/gallery3d/filtershow/category/CategoryView.java b/src/com/android/gallery3d/filtershow/category/CategoryView.java
index c101f98..84a973b 100644
--- a/src/com/android/gallery3d/filtershow/category/CategoryView.java
+++ b/src/com/android/gallery3d/filtershow/category/CategoryView.java
@@ -29,6 +29,7 @@
 import com.android.gallery3d.R;
 import com.android.gallery3d.filtershow.FilterShowActivity;
 import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.ui.SelectionRenderer;
 
 public class CategoryView extends View implements View.OnClickListener {
 
@@ -40,6 +41,9 @@
     private static int sTextSize = 32;
     private int mTextColor;
     private int mBackgroundColor;
+    private Paint mSelectPaint;
+    CategoryAdapter mAdapter;
+    private int mSelectionStroke;
 
     public static void setTextSize(int size) {
         sTextSize = size;
@@ -55,6 +59,10 @@
         Resources res = getResources();
         mBackgroundColor = res.getColor(R.color.filtershow_categoryview_background);
         mTextColor = res.getColor(R.color.filtershow_categoryview_text);
+        mSelectionStroke = res.getDimensionPixelSize(R.dimen.thumbnail_margin);
+        mSelectPaint = new Paint();
+        mSelectPaint.setStyle(Paint.Style.FILL);
+        mSelectPaint.setColor(res.getColor(R.color.filtershow_category_selection));
     }
 
     public void drawText(Canvas canvas, String text) {
@@ -71,6 +79,7 @@
         canvas.drawText(text, x, y, mPaint);
     }
 
+    @Override
     public void onDraw(Canvas canvas) {
         canvas.drawColor(mBackgroundColor);
         if (mAction != null) {
@@ -81,6 +90,10 @@
             } else {
                 Bitmap bitmap = mAction.getImage();
                 canvas.drawBitmap(bitmap, 0, 0, mPaint);
+                if (mAdapter.isSelected(this)) {
+                    SelectionRenderer.drawSelection(canvas, 0, 0, bitmap.getWidth(),
+                            bitmap.getHeight(), mSelectionStroke, mSelectPaint);
+                }
             }
             mPaint.setColor(mBackgroundColor);
             mPaint.setStyle(Paint.Style.STROKE);
@@ -93,8 +106,9 @@
         }
     }
 
-    public void setAction(Action action) {
+    public void setAction(Action action, CategoryAdapter adapter) {
         mAction = action;
+        mAdapter = adapter;
         invalidate();
     }
 
@@ -106,5 +120,6 @@
     public void onClick(View view) {
         FilterShowActivity activity = (FilterShowActivity) getContext();
         activity.showRepresentation(mAction.getRepresentation());
+        mAdapter.setSelected(this);
     }
 }
diff --git a/src/com/android/gallery3d/filtershow/category/MainPanel.java b/src/com/android/gallery3d/filtershow/category/MainPanel.java
index 7cadbc3..9a64ffb 100644
--- a/src/com/android/gallery3d/filtershow/category/MainPanel.java
+++ b/src/com/android/gallery3d/filtershow/category/MainPanel.java
@@ -24,6 +24,7 @@
 import android.view.ViewGroup;
 import android.widget.ImageButton;
 import android.widget.LinearLayout;
+
 import com.android.gallery3d.R;
 import com.android.gallery3d.filtershow.FilterShowActivity;
 import com.android.gallery3d.filtershow.state.StatePanel;
diff --git a/src/com/android/gallery3d/filtershow/crop/CropActivity.java b/src/com/android/gallery3d/filtershow/crop/CropActivity.java
index fcbf7bb..d349d5d 100644
--- a/src/com/android/gallery3d/filtershow/crop/CropActivity.java
+++ b/src/com/android/gallery3d/filtershow/crop/CropActivity.java
@@ -43,6 +43,7 @@
 import android.widget.Toast;
 
 import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -363,6 +364,20 @@
         Intent mResultIntent = null;
         int mRotation = 0;
 
+        // Helper to setup input stream
+        private void regenerateInputStream() {
+            if (mInUri == null) {
+                Log.w(LOGTAG, "cannot read original file, no input URI given");
+            } else {
+                Utils.closeSilently(mInStream);
+                try {
+                    mInStream = getContentResolver().openInputStream(mInUri);
+                } catch (FileNotFoundException e) {
+                    Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
+                }
+            }
+        }
+
         public BitmapIOTask(Uri sourceUri, Uri destUri, String outputFormat, int flags,
                 RectF cropBounds, RectF photoBounds, RectF originalBitmapBounds, int rotation,
                 int outputX, int outputY) {
@@ -395,15 +410,7 @@
             }
 
             if ((flags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0) {
-                if (mInUri == null) {
-                    Log.w(LOGTAG, "cannot read original file, no input URI given");
-                } else {
-                    try {
-                        mInStream = getContentResolver().openInputStream(mInUri);
-                    } catch (FileNotFoundException e) {
-                        Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
-                    }
-                }
+                regenerateInputStream();
             }
         }
 
@@ -451,17 +458,6 @@
 
             // Do the large cropped bitmap and/or set the wallpaper
             if ((mFlags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0 && mInStream != null) {
-                BitmapRegionDecoder decoder = null;
-                try {
-                    decoder = BitmapRegionDecoder.newInstance(mInStream, true);
-                } catch (IOException e) {
-                    Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
-                }
-                if (decoder == null) {
-                    failure = true;
-                    return false;
-                }
-
                 // Find crop bounds (scaled to original image size)
                 RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig);
                 if (trueCrop == null) {
@@ -477,14 +473,40 @@
                     failure = true;
                     return false;
                 }
-                // Do region decoding to get crop bitmap
-                BitmapFactory.Options options = new BitmapFactory.Options();
-                options.inMutable = true;
-                Bitmap crop = decoder.decodeRegion(roundedTrueCrop, options);
-                decoder.recycle();
+
+                // Attempt to open a region decoder
+                BitmapRegionDecoder decoder = null;
+                try {
+                    decoder = BitmapRegionDecoder.newInstance(mInStream, true);
+                } catch (IOException e) {
+                    Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
+                }
+
+                Bitmap crop = null;
+                if (decoder != null) {
+                    // Do region decoding to get crop bitmap
+                    BitmapFactory.Options options = new BitmapFactory.Options();
+                    options.inMutable = true;
+                    crop = decoder.decodeRegion(roundedTrueCrop, options);
+                    decoder.recycle();
+                }
 
                 if (crop == null) {
-                    Log.w(LOGTAG, "cannot region decode file: " + mInUri.toString());
+                    // BitmapRegionDecoder has failed, try to crop in-memory
+                    regenerateInputStream();
+                    Bitmap fullSize = null;
+                    if (mInStream != null) {
+                        fullSize = BitmapFactory.decodeStream(mInStream);
+                    }
+                    if (fullSize != null) {
+                        crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left,
+                                roundedTrueCrop.top, roundedTrueCrop.width(),
+                                roundedTrueCrop.height());
+                    }
+                }
+
+                if (crop == null) {
+                    Log.w(LOGTAG, "cannot decode file: " + mInUri.toString());
                     failure = true;
                     return false;
                 }
@@ -578,6 +600,8 @@
 
         @Override
         protected void onPostExecute(Boolean result) {
+            Utils.closeSilently(mOutStream);
+            Utils.closeSilently(mInStream);
             doneBitmapIO(result.booleanValue(), mResultIntent);
         }
 
diff --git a/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java
index 7e086b0..ab8567f 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java
@@ -35,7 +35,7 @@
 
     private static final String LOGTAG = "MasterImage";
     private boolean DEBUG  = false;
-
+    private static final boolean DISABLEZOOM = true;
     private static MasterImage sMasterImage = null;
     private static int sIconSeedSize = 128;
     private static float sHistoryPreviewSize = 128.0f;
@@ -451,6 +451,9 @@
     }
 
     public void setScaleFactor(float scaleFactor) {
+        if (DISABLEZOOM) {
+            return;
+        }
         if (scaleFactor == mScaleFactor) {
             return;
         }
@@ -463,6 +466,11 @@
     }
 
     public void setTranslation(Point translation) {
+        if (DISABLEZOOM) {
+            mTranslation.x = 0;
+            mTranslation.y = 0;
+            return;
+        }
         mTranslation.x = translation.x;
         mTranslation.y = translation.y;
         needsUpdatePartialPreview();
@@ -473,6 +481,9 @@
     }
 
     public void setOriginalTranslation(Point originalTranslation) {
+        if (DISABLEZOOM) {
+            return;
+        }
         mOriginalTranslation.x = originalTranslation.x;
         mOriginalTranslation.y = originalTranslation.y;
     }
@@ -492,6 +503,9 @@
     }
 
     public float getMaxScaleFactor() {
+        if (DISABLEZOOM) {
+            return 1;
+        }
         return mMaxScaleFactor;
     }
 
diff --git a/src/com/android/gallery3d/filtershow/ui/FilterIconButton.java b/src/com/android/gallery3d/filtershow/ui/FilterIconButton.java
index c3c8769..96126c5 100644
--- a/src/com/android/gallery3d/filtershow/ui/FilterIconButton.java
+++ b/src/com/android/gallery3d/filtershow/ui/FilterIconButton.java
@@ -17,18 +17,25 @@
 package com.android.gallery3d.filtershow.ui;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
 import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.view.View;
 import android.widget.LinearLayout;
 
+import com.android.gallery3d.R;
 import com.android.gallery3d.filtershow.FilterShowActivity;
 import com.android.gallery3d.filtershow.cache.RenderingRequest;
 import com.android.gallery3d.filtershow.cache.RenderingRequestCaller;
 import com.android.gallery3d.filtershow.category.Action;
+import com.android.gallery3d.filtershow.category.CategoryAdapter;
 import com.android.gallery3d.filtershow.filters.FilterRepresentation;
 import com.android.gallery3d.filtershow.imageshow.GeometryListener;
 import com.android.gallery3d.filtershow.imageshow.GeometryMetadata;
@@ -45,6 +52,9 @@
     private FilterRepresentation mFilterRepresentation = null;
     private Bitmap mIconBitmap = null;
     private Action mAction;
+    private Paint mSelectPaint;
+    private int mSelectStroke;
+    private CategoryAdapter mAdapter;
     public FilterIconButton(Context context) {
         super(context);
     }
@@ -57,11 +67,17 @@
         super(context, attrs, defStyle);
     }
 
-    public void setup(String text, LinearLayout parent) {
+    public void setup(String text, LinearLayout parent, CategoryAdapter adapter) {
+        mAdapter = adapter;
         setText(text);
         setContentDescription(text);
         super.setOnClickListener(this);
+        Resources res = getContext().getResources();
         MasterImage.getImage().addGeometryListener(this);
+        mSelectStroke = res.getDimensionPixelSize(R.dimen.thumbnail_margin);
+        mSelectPaint = new Paint();
+        mSelectPaint.setStyle(Paint.Style.FILL);
+        mSelectPaint.setColor(res.getColor(R.color.filtershow_category_selection));
         invalidate();
     }
 
@@ -69,6 +85,7 @@
     public void onClick(View v) {
         FilterShowActivity activity = (FilterShowActivity) getContext();
         activity.showRepresentation(mFilterRepresentation);
+        mAdapter.setSelected(v);
     }
 
     public FilterRepresentation getFilterRepresentation() {
@@ -113,6 +130,20 @@
         } else {
             super.onDraw(canvas);
         }
+        if (mAdapter.isSelected(this)) {
+            Drawable iconDrawable = getCompoundDrawables()[1];
+            if (iconDrawable != null) {
+                canvas.save();
+                int padding = getCompoundDrawablePadding();
+                canvas.translate(getScrollX() + padding + getPaddingLeft() - mSelectStroke - 1,
+                        getScrollY() + padding + getPaddingTop() - mSelectStroke - 1);
+                Rect r = iconDrawable.getBounds();
+                SelectionRenderer.drawSelection(canvas, r.left, r.top,
+                        r.right + 2 * mSelectStroke + 2, r.bottom + 2 * mSelectStroke + 2,
+                        mSelectStroke, mSelectPaint);
+                canvas.restore();
+            }
+        }
     }
 
     @Override
diff --git a/src/com/android/gallery3d/filtershow/ui/SelectionRenderer.java b/src/com/android/gallery3d/filtershow/ui/SelectionRenderer.java
new file mode 100644
index 0000000..1b108bd
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/ui/SelectionRenderer.java
@@ -0,0 +1,32 @@
+/*
+ * 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.gallery3d.filtershow.ui;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+public class SelectionRenderer {
+
+    public static void drawSelection(Canvas canvas, int left, int top, int right, int bottom,
+            int stroke, Paint paint) {
+        canvas.drawRect(left, top, right, top + stroke, paint);
+        canvas.drawRect(left, bottom - stroke, right, bottom, paint);
+        canvas.drawRect(left, top, left + stroke, bottom, paint);
+        canvas.drawRect(right - stroke, top, right, bottom, paint);
+    }
+
+}
diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java
index 5c8ac1c..7afa203 100644
--- a/src/com/android/gallery3d/ui/PhotoView.java
+++ b/src/com/android/gallery3d/ui/PhotoView.java
@@ -1423,6 +1423,11 @@
 
     @Override
     protected void render(GLCanvas canvas) {
+        if (mFirst) {
+            // Make sure the fields are properly initialized before checking
+            // whether isCamera()
+            mPictures.get(0).reload();
+        }
         // Check if the camera preview occupies the full screen.
         boolean full = !mFilmMode && mPictures.get(0).isCamera()
                 && mPositionController.isCenter()
diff --git a/src/com/android/photos/data/GalleryBitmapPool.java b/src/com/android/photos/data/GalleryBitmapPool.java
index a5a17ed..7eb9794 100644
--- a/src/com/android/photos/data/GalleryBitmapPool.java
+++ b/src/com/android/photos/data/GalleryBitmapPool.java
@@ -23,9 +23,25 @@
 
 import com.android.photos.data.SparseArrayBitmapPool.Node;
 
+/**
+ * Pool allowing the efficient reuse of bitmaps in order to avoid long
+ * garbage collection pauses.
+ */
 public class GalleryBitmapPool {
 
     private static final int CAPACITY_BYTES = 20971520;
+
+    // We found that Gallery uses bitmaps that are either square (for example,
+    // tiles of large images or square thumbnails), match one of the common
+    // photo aspect ratios (4x3, 3x2, or 16x9), or, less commonly, are of some
+    // other aspect ratio. Taking advantage of this information, we use 3
+    // SparseArrayBitmapPool instances to back the GalleryBitmapPool, which affords
+    // O(1) lookups for square bitmaps, and average-case - but *not* asymptotically -
+    // O(1) lookups for common photo aspect ratios and other miscellaneous aspect
+    // ratios. Beware of the pathological case where there are many bitmaps added
+    // to the pool with different non-square aspect ratios but the same width, as
+    // performance will degrade and the average case lookup will approach
+    // O(# of different aspect ratios).
     private static final int POOL_INDEX_NONE = -1;
     private static final int POOL_INDEX_SQUARE = 0;
     private static final int POOL_INDEX_PHOTO = 1;
@@ -84,11 +100,20 @@
         return POOL_INDEX_MISC;
     }
 
+    /**
+     * @return Capacity of the pool in bytes.
+     */
     public synchronized int getCapacity() {
         return mCapacityBytes;
     }
 
-    public synchronized int getSize() {
+    /**
+     * @return Approximate total size in bytes of the bitmaps stored in the pool.
+     */
+    public int getSize() {
+        // Note that this only returns an approximate size, since multiple threads
+        // might be getting and putting Bitmaps from the pool and we lock at the
+        // sub-pool level to avoid unnecessary blocking.
         int total = 0;
         for (SparseArrayBitmapPool p : mPools) {
             total += p.getSize();
@@ -96,6 +121,9 @@
         return total;
     }
 
+    /**
+     * @return Bitmap from the pool with the desired height/width or null if none available.
+     */
     public Bitmap get(int width, int height) {
         SparseArrayBitmapPool pool = getPoolForDimensions(width, height);
         if (pool == null) {
@@ -105,6 +133,10 @@
         }
     }
 
+    /**
+     * Adds the given bitmap to the pool.
+     * @return Whether the bitmap was added to the pool.
+     */
     public boolean put(Bitmap b) {
         if (b == null || b.getConfig() != Bitmap.Config.ARGB_8888) {
             return false;
@@ -118,6 +150,9 @@
         }
     }
 
+    /**
+     * Empty the pool, recycling all the bitmaps currently in it.
+     */
     public void clear() {
         for (SparseArrayBitmapPool p : mPools) {
             p.clear();
diff --git a/src/com/android/photos/data/SparseArrayBitmapPool.java b/src/com/android/photos/data/SparseArrayBitmapPool.java
index 1ef9e9f..95e1026 100644
--- a/src/com/android/photos/data/SparseArrayBitmapPool.java
+++ b/src/com/android/photos/data/SparseArrayBitmapPool.java
@@ -20,10 +20,15 @@
 import android.util.SparseArray;
 
 import android.util.Pools.Pool;
+import android.util.Pools.SimplePool;
 
+/**
+ * Bitmap pool backed by a sparse array indexing linked lists of bitmaps
+ * sharing the same width. Performance will degrade if using this to store
+ * many bitmaps with the same width but many different heights.
+ */
 public class SparseArrayBitmapPool {
 
-    private static final int BITMAPS_TO_KEEP_AFTER_UNNEEDED_HINT = 4;
     private int mCapacityBytes;
     private SparseArray<Node> mStore = new SparseArray<Node>();
     private int mSizeBytes = 0;
@@ -34,53 +39,80 @@
 
     protected static class Node {
         Bitmap bitmap;
+
+        // Each node is part of two doubly linked lists:
+        // - A pool-level list (accessed by mPoolNodesHead and mPoolNodesTail)
+        //   that is used for FIFO eviction of nodes when the pool gets full.
+        // - A bucket-level list for each index of the sparse array, so that
+        //   each index can store more than one item.
         Node prevInBucket;
         Node nextInBucket;
         Node nextInPool;
         Node prevInPool;
     }
 
+    /**
+     * @param capacityBytes Maximum capacity of the pool in bytes.
+     * @param nodePool Shared pool to use for recycling linked list nodes, or null.
+     */
     public SparseArrayBitmapPool(int capacityBytes, Pool<Node> nodePool) {
         mCapacityBytes = capacityBytes;
-        mNodePool = nodePool;
+        if (nodePool == null) {
+            mNodePool = new SimplePool<Node>(32);
+        } else {
+            mNodePool = nodePool;
+        }
     }
 
+    /**
+     * Set the maximum capacity of the pool, and if necessary trim it down to size.
+     */
     public synchronized void setCapacity(int capacityBytes) {
         mCapacityBytes = capacityBytes;
+
+        // No-op unless current size exceeds the new capacity.
         freeUpCapacity(0);
     }
 
     private void freeUpCapacity(int bytesNeeded) {
         int targetSize = mCapacityBytes - bytesNeeded;
+        // Repeatedly remove the oldest node until we have freed up at least bytesNeeded.
         while (mPoolNodesTail != null && mSizeBytes > targetSize) {
             unlinkAndRecycleNode(mPoolNodesTail, true);
         }
     }
 
     private void unlinkAndRecycleNode(Node n, boolean recycleBitmap) {
-        // Remove the node from its spot in its bucket
+        // Unlink the node from its sparse array bucket list.
         if (n.prevInBucket != null) {
+            // This wasn't the head, update the previous node.
             n.prevInBucket.nextInBucket = n.nextInBucket;
         } else {
+            // This was the head of the bucket, replace it with the next node.
             mStore.put(n.bitmap.getWidth(), n.nextInBucket);
         }
         if (n.nextInBucket != null) {
+            // This wasn't the tail, update the next node.
             n.nextInBucket.prevInBucket = n.prevInBucket;
         }
 
-        // Remove the node from its spot in the list of pool nodes
+        // Unlink the node from the pool-wide list.
         if (n.prevInPool != null) {
+            // This wasn't the head, update the previous node.
             n.prevInPool.nextInPool = n.nextInPool;
         } else {
+            // This was the head of the pool-wide list, update the head pointer.
             mPoolNodesHead = n.nextInPool;
         }
         if (n.nextInPool != null) {
+            // This wasn't the tail, update the next node.
             n.nextInPool.prevInPool = n.prevInPool;
         } else {
+            // This was the tail, update the tail pointer.
             mPoolNodesTail = n.prevInPool;
         }
 
-        // Recycle the node
+        // Recycle the node.
         n.nextInBucket = null;
         n.nextInPool = null;
         n.prevInBucket = null;
@@ -91,16 +123,29 @@
         mNodePool.release(n);
     }
 
+    /**
+     * @return Capacity of the pool in bytes.
+     */
     public synchronized int getCapacity() {
         return mCapacityBytes;
     }
 
+    /**
+     * @return Total size in bytes of the bitmaps stored in the pool.
+     */
     public synchronized int getSize() {
         return mSizeBytes;
     }
 
+    /**
+     * @return Bitmap from the pool with the desired height/width or null if none available.
+     */
     public synchronized Bitmap get(int width, int height) {
         Node cur = mStore.get(width);
+
+        // Traverse the list corresponding to the width bucket in the
+        // sparse array, and unlink and return the first bitmap that
+        // also has the correct height.
         while (cur != null) {
             if (cur.bitmap.getHeight() == height) {
                 Bitmap b = cur.bitmap;
@@ -112,28 +157,43 @@
         return null;
     }
 
+    /**
+     * Adds the given bitmap to the pool.
+     * @return Whether the bitmap was added to the pool.
+     */
     public synchronized boolean put(Bitmap b) {
         if (b == null) {
             return false;
         }
+
+        // Ensure there is enough room to contain the new bitmap.
         int bytes = b.getByteCount();
         freeUpCapacity(bytes);
+
         Node newNode = mNodePool.acquire();
         if (newNode == null) {
             newNode = new Node();
         }
         newNode.bitmap = b;
+
+        // We append to the head, and freeUpCapacity clears from the tail,
+        // resulting in FIFO eviction.
         newNode.prevInBucket = null;
         newNode.prevInPool = null;
         newNode.nextInPool = mPoolNodesHead;
         mPoolNodesHead = newNode;
+
+        // Insert the node into its appropriate bucket based on width.
         int key = b.getWidth();
         newNode.nextInBucket = mStore.get(key);
         if (newNode.nextInBucket != null) {
+            // The bucket already had nodes, update the old head.
             newNode.nextInBucket.prevInBucket = newNode;
         }
         mStore.put(key, newNode);
+
         if (newNode.nextInPool == null) {
+            // This is the only node in the list, update the tail pointer.
             mPoolNodesTail = newNode;
         } else {
             newNode.nextInPool.prevInPool = newNode;
@@ -142,7 +202,11 @@
         return true;
     }
 
+    /**
+     * Empty the pool, recycling all the bitmaps currently in it.
+     */
     public synchronized void clear() {
+        // Clearing is equivalent to ensuring all the capacity is available.
         freeUpCapacity(mCapacityBytes);
     }
 }