am be4f113f: Fix editor on ICS

* commit 'be4f113fa1fea32e8d8865a52491b9f25760eaba':
  Fix editor on ICS
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
new file mode 100644
index 0000000..2bce1b4
--- /dev/null
+++ b/src/com/android/camera/data/CameraDataAdapter.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.data;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Video;
+import android.util.Log;
+import android.view.View;
+
+import com.android.camera.Storage;
+import com.android.camera.ui.FilmStripView;
+import com.android.camera.ui.FilmStripView.ImageData;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A FilmStrip.DataProvider that provide data in the camera folder.
+ *
+ * The given view for camera preview won't be added until the preview info
+ * has been set by setCameraPreviewInfo(int, int).
+ */
+public class CameraDataAdapter implements FilmStripView.DataAdapter {
+    private static final String TAG = CameraDataAdapter.class.getSimpleName();
+
+    private static final int DEFAULT_DECODE_SIZE = 3000;
+    private static final String[] CAMERA_PATH = { Storage.DIRECTORY + "%" };
+
+    private List<LocalData> mImages;
+
+    private Listener mListener;
+    private View mCameraPreviewView;
+    private Drawable mPlaceHolder;
+
+    private int mSuggestedWidth = DEFAULT_DECODE_SIZE;
+    private int mSuggestedHeight = DEFAULT_DECODE_SIZE;
+
+    public CameraDataAdapter(Drawable placeHolder) {
+        mPlaceHolder = placeHolder;
+    }
+
+    public void setCameraPreviewInfo(View cameraPreview, int width, int height) {
+        mCameraPreviewView = cameraPreview;
+        addOrReplaceCameraData(buildCameraImageData(width, height));
+    }
+
+    public void requestLoad(ContentResolver resolver) {
+        QueryTask qtask = new QueryTask();
+        qtask.execute(resolver);
+    }
+
+    @Override
+    public int getTotalNumber() {
+        if (mImages == null) {
+            return 0;
+        }
+        return mImages.size();
+    }
+
+    @Override
+    public ImageData getImageData(int id) {
+        if (mImages == null || id >= mImages.size() || id < 0) {
+            return null;
+        }
+        return mImages.get(id);
+    }
+
+    @Override
+    public void suggestDecodeSize(int w, int h) {
+        if (w <= 0 || h <= 0) {
+            mSuggestedWidth  = mSuggestedHeight = DEFAULT_DECODE_SIZE;
+        } else {
+            mSuggestedWidth = (w < DEFAULT_DECODE_SIZE ? w : DEFAULT_DECODE_SIZE);
+            mSuggestedHeight = (h < DEFAULT_DECODE_SIZE ? h : DEFAULT_DECODE_SIZE);
+        }
+    }
+
+    @Override
+    public View getView(Context c, int dataID) {
+        if (mImages == null) {
+            return null;
+        }
+        if (dataID >= mImages.size() || dataID < 0) {
+            return null;
+        }
+
+        return mImages.get(dataID).getView(
+                c, mSuggestedWidth, mSuggestedHeight, mPlaceHolder);
+    }
+
+    @Override
+    public void setListener(Listener listener) {
+        mListener = listener;
+        if (mImages != null) {
+            mListener.onDataLoaded();
+        }
+    }
+
+    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;
+    }
+
+    private void addOrReplaceCameraData(LocalData data) {
+        if (mImages == null) {
+            mImages = new ArrayList<LocalData>();
+        }
+        if (mImages.size() == 0) {
+            // No data at all.
+            mImages.add(0, data);
+            if (mListener != null) {
+                mListener.onDataLoaded();
+            }
+            return;
+        }
+
+        LocalData first = mImages.get(0);
+        if (first.getType() == ImageData.TYPE_CAMERA_PREVIEW) {
+            // Replace the old camera data.
+            mImages.set(0, data);
+            if (mListener != null) {
+                mListener.onDataUpdated(new UpdateReporter() {
+                    @Override
+                    public boolean isDataRemoved(int id) {
+                        return false;
+                    }
+
+                    @Override
+                    public boolean isDataUpdated(int id) {
+                        if (id == 0) {
+                            return true;
+                        }
+                        return false;
+                    }
+                });
+            }
+        } else {
+            // Add a new camera data.
+            mImages.add(0, data);
+            if (mListener != null) {
+                mListener.onDataLoaded();
+            }
+        }
+    }
+
+    private class QueryTask extends AsyncTask<ContentResolver, Void, List<LocalData>> {
+        @Override
+        protected List<LocalData> doInBackground(ContentResolver... resolver) {
+            List<LocalData> l = new ArrayList<LocalData>();
+            // Photos
+            Cursor c = resolver[0].query(
+                    Images.Media.EXTERNAL_CONTENT_URI,
+                    LocalData.Photo.QUERY_PROJECTION,
+                    MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH,
+                    LocalData.Photo.QUERY_ORDER);
+            if (c != null && c.moveToFirst()) {
+                // build up the list.
+                while (true) {
+                    LocalData data = LocalData.Photo.buildFromCursor(c);
+                    if (data != null) {
+                        l.add(data);
+                    } else {
+                        Log.e(TAG, "Error loading data:"
+                                + c.getString(LocalData.Photo.COL_DATA));
+                    }
+                    if (c.isLast()) {
+                        break;
+                    }
+                    c.moveToNext();
+                }
+            }
+            if (c != null) {
+                c.close();
+            }
+
+            c = resolver[0].query(
+                    Video.Media.EXTERNAL_CONTENT_URI,
+                    LocalData.Video.QUERY_PROJECTION,
+                    MediaStore.Video.Media.DATA + " like ? ", CAMERA_PATH,
+                    LocalData.Video.QUERY_ORDER);
+            if (c != null && c.moveToFirst()) {
+                // build up the list.
+                c.moveToFirst();
+                while (true) {
+                    LocalData data = LocalData.Video.buildFromCursor(c);
+                    if (data != null) {
+                        l.add(data);
+                        Log.v(TAG, "video data added:" + data);
+                    } else {
+                        Log.e(TAG, "Error loading data:"
+                                + c.getString(LocalData.Video.COL_DATA));
+                    }
+                    if (!c.isLast()) {
+                        c.moveToNext();
+                    } else {
+                        break;
+                    }
+                }
+            }
+            if (c != null) {
+                c.close();
+            }
+
+            if (l.size() == 0) return null;
+
+            Collections.sort(l, new LocalData.NewestFirstComparator());
+            return l;
+        }
+
+        @Override
+        protected void onPostExecute(List<LocalData> l) {
+            boolean changed = (l != mImages);
+            LocalData cameraData = null;
+            if (mImages != null && mImages.size() > 0) {
+                cameraData = mImages.get(0);
+                if (cameraData.getType() != ImageData.TYPE_CAMERA_PREVIEW) {
+                    cameraData = null;
+                }
+            }
+
+            mImages = l;
+            if (cameraData != null) {
+                // camera view exists, so we make sure at least 1 data is in the list.
+                if (mImages == null) {
+                    mImages = new ArrayList<LocalData>();
+                }
+                mImages.add(0, cameraData);
+                if (mListener != null) {
+                    // Only the camera data is not changed, everything else is changed.
+                    mListener.onDataUpdated(new UpdateReporter() {
+                        @Override
+                        public boolean isDataRemoved(int id) {
+                            return false;
+                        }
+
+                        @Override
+                        public boolean isDataUpdated(int id) {
+                            if (id == 0) return false;
+                            return true;
+                        }
+                    });
+                }
+            } else {
+                // both might be null.
+                if (changed) {
+                    mListener.onDataLoaded();
+                }
+            }
+        }
+    }
+
+    private class CameraPreviewData implements LocalData {
+        private int width;
+        private int height;
+
+        CameraPreviewData(int w, int h) {
+            width = w;
+            height = h;
+        }
+
+        @Override
+        public long getDateTaken() {
+            // This value is used for sorting.
+            return -1;
+        }
+
+        @Override
+        public long getDateModified() {
+            // This value might be used for sorting.
+            return -1;
+        }
+
+        @Override
+        public String getTitle() {
+            return "";
+        }
+
+        @Override
+        public int getWidth() {
+            return width;
+        }
+
+        @Override
+        public int getHeight() {
+            return height;
+        }
+
+        @Override
+        public int getType() {
+            return ImageData.TYPE_CAMERA_PREVIEW;
+        }
+
+        @Override
+        public boolean isActionSupported(int action) {
+            return false;
+        }
+
+        @Override
+        public View getView(Context c, int width, int height, Drawable placeHolder) {
+            return mCameraPreviewView;
+        }
+
+        @Override
+        public void prepare() {
+            // do nothing.
+        }
+
+        @Override
+        public void recycle() {
+            // do nothing.
+        }
+    }
+
+}
diff --git a/src/com/android/camera/data/LocalData.java b/src/com/android/camera/data/LocalData.java
new file mode 100644
index 0000000..1f60160
--- /dev/null
+++ b/src/com/android/camera/data/LocalData.java
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.data;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.graphics.drawable.Drawable;
+import android.media.MediaMetadataRetriever;
+import android.os.AsyncTask;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.Video;
+import android.provider.MediaStore.Video.VideoColumns;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.camera.ui.FilmStripView;
+
+import java.util.Comparator;
+import java.util.Date;
+
+/* An abstract interface that represents the local media data. Also implements
+ * Comparable interface so we can sort in DataAdapter.
+ */
+abstract interface LocalData extends FilmStripView.ImageData {
+    static final String TAG = "LocalData";
+
+    abstract View getView(Context c, int width, int height, Drawable placeHolder);
+    abstract long getDateTaken();
+    abstract long getDateModified();
+    abstract String getTitle();
+
+    static class NewestFirstComparator implements Comparator<LocalData> {
+
+        private static int compare(long v1, long v2) {
+            if (v1 == -1) {
+                if (v2 == -1) return 0;
+                return -1;
+            }
+            if (v2 == -1) return 0;
+
+            return ((v1 > v2) ? 1 : ((v1 < v2) ? -1 : 0));
+        }
+
+        @Override
+        public int compare(LocalData d1, LocalData d2) {
+            int cmp = compare(d1.getDateTaken(), d2.getDateTaken());
+            if (cmp == 0) {
+                cmp = compare(d1.getDateModified(), d2.getDateModified());
+            }
+            if (cmp == 0) {
+                cmp = d1.getTitle().compareTo(d2.getTitle());
+            }
+            return cmp;
+        }
+    }
+
+    /*
+     * A base class for all the local media files. The bitmap is loaded in background
+     * thread. Subclasses should implement their own background loading thread by
+     * subclassing BitmapLoadTask and overriding doInBackground() to return a bitmap.
+     */
+    abstract static class LocalMediaData implements LocalData {
+        protected long id;
+        protected String title;
+        protected String mimeType;
+        protected long dateTaken;
+        protected long dateModified;
+        protected String path;
+        // width and height should be adjusted according to orientation.
+        protected int width;
+        protected int height;
+
+        // true if this data has a corresponding visible view.
+        protected Boolean mUsing = false;
+
+        @Override
+        public long getDateTaken() {
+            return dateTaken;
+        }
+
+        @Override
+        public long getDateModified() {
+            return dateModified;
+        }
+
+        @Override
+        public String getTitle() {
+            return new String(title);
+        }
+
+        @Override
+        public int getWidth() {
+            return width;
+        }
+
+        @Override
+        public int getHeight() {
+            return height;
+        }
+
+        @Override
+        public boolean isActionSupported(int action) {
+            return false;
+        }
+
+        @Override
+        public View getView(Context c,
+                int decodeWidth, int decodeHeight, Drawable placeHolder) {
+            ImageView v = new ImageView(c);
+            v.setImageDrawable(placeHolder);
+
+            v.setScaleType(ImageView.ScaleType.FIT_XY);
+            BitmapLoadTask task = getBitmapLoadTask(v, decodeWidth, decodeHeight);
+            task.execute();
+            return v;
+        }
+
+        @Override
+        public void prepare() {
+            synchronized (mUsing) {
+                mUsing = true;
+            }
+        }
+
+        @Override
+        public void recycle() {
+            synchronized (mUsing) {
+                mUsing = false;
+            }
+        }
+
+        protected boolean isUsing() {
+            synchronized (mUsing) {
+                return mUsing;
+            }
+        }
+
+        @Override
+        public abstract int getType();
+
+        protected abstract BitmapLoadTask getBitmapLoadTask(
+                ImageView v, int decodeWidth, int decodeHeight);
+
+        /*
+         * An AsyncTask class that loads the bitmap in the background thread.
+         * Sub-classes should implement their own "protected Bitmap doInBackground(Void... )"
+         */
+        protected abstract class BitmapLoadTask extends AsyncTask<Void, Void, Bitmap> {
+            protected ImageView mView;
+
+            protected BitmapLoadTask(ImageView v) {
+                mView = v;
+            }
+
+            @Override
+            protected void onPostExecute(Bitmap bitmap) {
+                if (!isUsing()) return;
+                if (bitmap == null) {
+                    Log.e(TAG, "Failed decoding bitmap for file:" + path);
+                    return;
+                }
+                mView.setScaleType(ImageView.ScaleType.FIT_XY);
+                mView.setImageBitmap(bitmap);
+            }
+        }
+    }
+
+    static class Photo extends LocalMediaData {
+        public static final int COL_ID = 0;
+        public static final int COL_TITLE = 1;
+        public static final int COL_MIME_TYPE = 2;
+        public static final int COL_DATE_TAKEN = 3;
+        public static final int COL_DATE_MODIFIED = 4;
+        public static final int COL_DATA = 5;
+        public static final int COL_ORIENTATION = 6;
+        public static final int COL_WIDTH = 7;
+        public static final int COL_HEIGHT = 8;
+
+        static final String QUERY_ORDER = ImageColumns.DATE_TAKEN + " DESC, "
+                + ImageColumns._ID + " DESC";
+        static final String[] QUERY_PROJECTION = {
+            ImageColumns._ID,           // 0, int
+            ImageColumns.TITLE,         // 1, string
+            ImageColumns.MIME_TYPE,     // 2, string
+            ImageColumns.DATE_TAKEN,    // 3, int
+            ImageColumns.DATE_MODIFIED, // 4, int
+            ImageColumns.DATA,          // 5, string
+            ImageColumns.ORIENTATION,   // 6, int, 0, 90, 180, 270
+            ImageColumns.WIDTH,         // 7, int
+            ImageColumns.HEIGHT,        // 8, int
+        };
+
+        private static final int mSupportedAction =
+                FilmStripView.ImageData.ACTION_DEMOTE
+                | FilmStripView.ImageData.ACTION_PROMOTE;
+
+        // 32K buffer.
+        private static final byte[] DECODE_TEMP_STORAGE = new byte[32 * 1024];
+
+        // from MediaStore, can only be 0, 90, 180, 270;
+        public int orientation;
+
+        static Photo buildFromCursor(Cursor c) {
+            Photo d = new Photo();
+            d.id = c.getLong(COL_ID);
+            d.title = c.getString(COL_TITLE);
+            d.mimeType = c.getString(COL_MIME_TYPE);
+            d.dateTaken = c.getLong(COL_DATE_TAKEN);
+            d.dateModified = c.getLong(COL_DATE_MODIFIED);
+            d.path = c.getString(COL_DATA);
+            d.orientation = c.getInt(COL_ORIENTATION);
+            d.width = c.getInt(COL_WIDTH);
+            d.height = c.getInt(COL_HEIGHT);
+            if (d.width <= 0 || d.height <= 0) {
+                Log.v(TAG, "warning! zero dimension for "
+                        + d.path + ":" + d.width + "x" + d.height);
+                BitmapFactory.Options opts = decodeDimension(d.path);
+                if (opts != null) {
+                    d.width = opts.outWidth;
+                    d.height = opts.outHeight;
+                } else {
+                    Log.v(TAG, "warning! dimension decode failed for " + d.path);
+                    Bitmap b = BitmapFactory.decodeFile(d.path);
+                    if (b == null) {
+                        return null;
+                    }
+                    d.width = b.getWidth();
+                    d.height = b.getHeight();
+                }
+            }
+            if (d.orientation == 90 || d.orientation == 270) {
+                int b = d.width;
+                d.width = d.height;
+                d.height = b;
+            }
+            return d;
+        }
+
+        @Override
+        public String toString() {
+            return "Photo:" + ",data=" + path + ",mimeType=" + mimeType
+                    + "," + width + "x" + height + ",orientation=" + orientation
+                    + ",date=" + new Date(dateTaken);
+        }
+
+        @Override
+        public int getType() {
+            return TYPE_PHOTO;
+        }
+
+        @Override
+        public boolean isActionSupported(int action) {
+            return ((action & mSupportedAction) != 0);
+        }
+
+        @Override
+        protected BitmapLoadTask getBitmapLoadTask(
+                ImageView v, int decodeWidth, int decodeHeight) {
+            return new PhotoBitmapLoadTask(v, decodeWidth, decodeHeight);
+        }
+
+        private static BitmapFactory.Options decodeDimension(String path) {
+            BitmapFactory.Options opts = new BitmapFactory.Options();
+            opts.inJustDecodeBounds = true;
+            Bitmap b = BitmapFactory.decodeFile(path, opts);
+            if (b == null)  {
+                return null;
+            }
+            return opts;
+        }
+
+        private final class PhotoBitmapLoadTask extends BitmapLoadTask {
+            private int mDecodeWidth;
+            private int mDecodeHeight;
+
+            public PhotoBitmapLoadTask(ImageView v, int decodeWidth, int decodeHeight) {
+                super(v);
+                mDecodeWidth = decodeWidth;
+                mDecodeHeight = decodeHeight;
+            }
+
+            @Override
+            protected Bitmap doInBackground(Void... v) {
+                BitmapFactory.Options opts = null;
+                Bitmap b;
+                int sample = 1;
+                while (mDecodeWidth * sample < width
+                        || mDecodeHeight * sample < height) {
+                    sample *= 2;
+                }
+                opts = new BitmapFactory.Options();
+                opts.inSampleSize = sample;
+                opts.inTempStorage = DECODE_TEMP_STORAGE;
+                if (isCancelled() || !isUsing()) {
+                    return null;
+                }
+                b = BitmapFactory.decodeFile(path, opts);
+                if (orientation != 0) {
+                    if (isCancelled() || !isUsing()) {
+                        return null;
+                    }
+                    Matrix m = new Matrix();
+                    m.setRotate((float) orientation);
+                    b = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, false);
+                }
+                return b;
+            }
+        }
+    }
+
+    static class Video extends LocalMediaData {
+        public static final int COL_ID = 0;
+        public static final int COL_TITLE = 1;
+        public static final int COL_MIME_TYPE = 2;
+        public static final int COL_DATE_TAKEN = 3;
+        public static final int COL_DATE_MODIFIED = 4;
+        public static final int COL_DATA = 5;
+        public static final int COL_WIDTH = 6;
+        public static final int COL_HEIGHT = 7;
+
+        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 = {
+            VideoColumns._ID,           // 0, int
+            VideoColumns.TITLE,         // 1, string
+            VideoColumns.MIME_TYPE,     // 2, string
+            VideoColumns.DATE_TAKEN,    // 3, int
+            VideoColumns.DATE_MODIFIED, // 4, int
+            VideoColumns.DATA,          // 5, string
+            VideoColumns.WIDTH,         // 6, int
+            VideoColumns.HEIGHT,        // 7, int
+            VideoColumns.RESOLUTION
+        };
+
+        static Video buildFromCursor(Cursor c) {
+            Video d = new Video();
+            d.id = c.getLong(COL_ID);
+            d.title = c.getString(COL_TITLE);
+            d.mimeType = c.getString(COL_MIME_TYPE);
+            d.dateTaken = c.getLong(COL_DATE_TAKEN);
+            d.dateModified = c.getLong(COL_DATE_MODIFIED);
+            d.path = c.getString(COL_DATA);
+            d.width = c.getInt(COL_WIDTH);
+            d.height = c.getInt(COL_HEIGHT);
+            MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+            retriever.setDataSource(d.path);
+            String rotation = retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
+            if (d.width == 0 || d.height == 0) {
+                d.width = Integer.parseInt(retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
+                d.height = Integer.parseInt(retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
+            }
+            retriever.release();
+            if (rotation.equals("90") || rotation.equals("270")) {
+                int b = d.width;
+                d.width = d.height;
+                d.height = b;
+            }
+            return d;
+        }
+
+        @Override
+        public String toString() {
+            return "Video:" + ",data=" + path + ",mimeType=" + mimeType
+                    + "," + width + "x" + height + ",date=" + new Date(dateTaken);
+        }
+
+        @Override
+        public int getType() {
+            return TYPE_PHOTO;
+        }
+
+        @Override
+        public boolean isActionSupported(int action) {
+            return ((action & mSupportedActions) != 0);
+        }
+
+        @Override
+        protected BitmapLoadTask getBitmapLoadTask(
+                ImageView v, int decodeWidth, int decodeHeight) {
+            return new VideoBitmapLoadTask(v);
+        }
+
+        private final class VideoBitmapLoadTask extends BitmapLoadTask {
+
+            public VideoBitmapLoadTask(ImageView v) {
+                super(v);
+            }
+
+            @Override
+            protected Bitmap doInBackground(Void... v) {
+                if (isCancelled() || !isUsing()) {
+                    return null;
+                }
+                android.media.MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+                retriever.setDataSource(path);
+                byte[] data = retriever.getEmbeddedPicture();
+                Bitmap bitmap = null;
+                if (isCancelled() || !isUsing()) {
+                    retriever.release();
+                    return null;
+                }
+                if (data != null) {
+                    bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
+                }
+                if (bitmap == null) {
+                    bitmap = (Bitmap) retriever.getFrameAtTime();
+                }
+                retriever.release();
+                return bitmap;
+            }
+        }
+    }
+}
+
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
new file mode 100644
index 0000000..f870b58
--- /dev/null
+++ b/src/com/android/camera/ui/FilmStripGestureRecognizer.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.ui;
+
+import android.content.Context;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+
+// This class aggregates three gesture detectors: GestureDetector,
+// ScaleGestureDetector.
+public class FilmStripGestureRecognizer {
+    @SuppressWarnings("unused")
+    private static final String TAG = "FilmStripGestureRecognizer";
+
+    public interface Listener {
+        boolean onSingleTapUp(float x, float y);
+        boolean onDoubleTap(float x, float y);
+        boolean onScroll(float x, float y, float dx, float dy);
+        boolean onFling(float velocityX, float velocityY);
+        boolean onScaleBegin(float focusX, float focusY);
+        boolean onScale(float focusX, float focusY, float scale);
+        boolean onDown(float x, float y);
+        boolean onUp(float x, float y);
+        void onScaleEnd();
+    }
+
+    private final GestureDetector mGestureDetector;
+    private final ScaleGestureDetector mScaleDetector;
+    private final Listener mListener;
+
+    public FilmStripGestureRecognizer(Context context, Listener listener) {
+        mListener = listener;
+        mGestureDetector = new GestureDetector(context, new MyGestureListener(),
+                null, true /* ignoreMultitouch */);
+        mScaleDetector = new ScaleGestureDetector(
+                context, new MyScaleListener());
+    }
+
+    public void onTouchEvent(MotionEvent event) {
+        mGestureDetector.onTouchEvent(event);
+        mScaleDetector.onTouchEvent(event);
+        if (event.getAction() == MotionEvent.ACTION_UP) {
+            mListener.onUp(event.getX(), event.getY());
+        }
+    }
+
+    private class MyGestureListener
+                extends GestureDetector.SimpleOnGestureListener {
+        @Override
+        public boolean onSingleTapUp(MotionEvent e) {
+            return mListener.onSingleTapUp(e.getX(), e.getY());
+        }
+
+        @Override
+        public boolean onDoubleTap(MotionEvent e) {
+            return mListener.onDoubleTap(e.getX(), e.getY());
+        }
+
+        @Override
+        public boolean onScroll(
+                MotionEvent e1, MotionEvent e2, float dx, float dy) {
+            return mListener.onScroll(e2.getX(), e2.getY(), dx, dy);
+        }
+
+        @Override
+        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+                float velocityY) {
+            return mListener.onFling(velocityX, velocityY);
+        }
+
+        @Override
+        public boolean onDown(MotionEvent e) {
+            mListener.onDown(e.getX(), e.getY());
+            return super.onDown(e);
+        }
+    }
+
+    private class MyScaleListener
+            extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+        @Override
+        public boolean onScaleBegin(ScaleGestureDetector detector) {
+            return mListener.onScaleBegin(
+                    detector.getFocusX(), detector.getFocusY());
+        }
+
+        @Override
+        public boolean onScale(ScaleGestureDetector detector) {
+            return mListener.onScale(detector.getFocusX(),
+                    detector.getFocusY(), detector.getScaleFactor());
+        }
+
+        @Override
+        public void onScaleEnd(ScaleGestureDetector detector) {
+            mListener.onScaleEnd();
+        }
+    }
+}
diff --git a/src/com/android/camera/ui/FilmStripView.java b/src/com/android/camera/ui/FilmStripView.java
new file mode 100644
index 0000000..b2e36e4
--- /dev/null
+++ b/src/com/android/camera/ui/FilmStripView.java
@@ -0,0 +1,1072 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.ui;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+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 = 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;
+    private static final int DURATION_GEOMETRY_ADJUST = 200;
+    private static final float FILM_STRIP_SCALE = 0.6f;
+    private static final float MAX_SCALE = 1f;
+
+    private Context mContext;
+    private FilmStripGestureRecognizer mGestureRecognizer;
+    private DataAdapter mDataAdapter;
+    private final Rect mDrawArea = new Rect();
+
+    private final int mCurrentInfo = (BUFFER_SIZE - 1) / 2;
+    private float mScale;
+    private GeometryAnimator mGeometryAnimator;
+    private LinearInterpolator mLinearInterpolator;
+    private int mCenterX = -1;
+    private ViewInfo[] mViewInfo = new ViewInfo[BUFFER_SIZE];
+
+    private Listener mListener;
+
+    private View mCameraView;
+    private ImageData mCameraData;
+
+    // This is used to resolve the misalignment problem when the device
+    // orientation is changed. If the current item is in fullscreen, it might
+    // be shifted because mCenterX is not adjusted with the orientation.
+    // Set this to true when onSizeChanged is called to make sure we adjust
+    // mCenterX accordingly.
+    private boolean mAnchorPending;
+
+    public interface ImageData {
+        public static final int TYPE_NONE = 0;
+        public static final int TYPE_CAMERA_PREVIEW = 1;
+        public static final int TYPE_PHOTO = 2;
+        public static final int TYPE_VIDEO = 3;
+        public static final int TYPE_PHOTOSPHERE = 4;
+
+        // The actions are defined bit-wise so we can use bit operations like
+        // | and &.
+        public static final int ACTION_NONE = 0;
+        public static final int ACTION_PROMOTE = 1;
+        public static final int ACTION_DEMOTE = (1 << 1);
+        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.
+        public static final int SIZE_FULL = -2;
+
+        // The values returned by getWidth() and getHeight() will be used for layout.
+        public int getWidth();
+        public int getHeight();
+        public int getType();
+        public boolean isActionSupported(int action);
+
+        // prepare() should be called first time before using it.
+        public void prepare();
+
+        // recycle() should be called before we nullify the reference to this
+        // data.
+        public void recycle();
+    }
+
+    public interface DataAdapter {
+        public interface UpdateReporter {
+            public boolean isDataRemoved(int id);
+            public boolean isDataUpdated(int id);
+        }
+
+        public interface Listener {
+            // Called when the whole data loading is done. No any assumption
+            // on previous data.
+            public void onDataLoaded();
+            // Only some of the data is changed. The listener should check
+            // if any thing needs to be updated.
+            public void onDataUpdated(UpdateReporter reporter);
+            public void onDataInserted(int dataID, ImageData data);
+            public void onDataRemoved(int dataID, ImageData data);
+        }
+
+        public int getTotalNumber();
+        public View getView(Context context, int id);
+        public ImageData getImageData(int id);
+        public void suggestDecodeSize(int w, int h);
+
+        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;
+
+        public ViewInfo(int id, View v) {
+            v.setPivotX(0f);
+            v.setPivotY(0f);
+            mDataID = id;
+            mView = v;
+            mLeftPosition = -1;
+        }
+
+        public int getID() {
+            return mDataID;
+        }
+
+        public void setID(int id) {
+            mDataID = id;
+        }
+
+        public void setLeftPosition(int pos) {
+            mLeftPosition = pos;
+        }
+
+        public int getLeftPosition() {
+            return mLeftPosition;
+        }
+
+        public float getTranslationY(float scale) {
+            return mView.getTranslationY() / scale;
+        }
+
+        public float getTranslationX(float scale) {
+            return mView.getTranslationX();
+        }
+
+        public void setTranslationY(float transY, float scale) {
+            mView.setTranslationY(transY * scale);
+        }
+
+        public void setTranslationX(float transX, float scale) {
+            mView.setTranslationX(transX * scale);
+        }
+
+        public void translateXBy(float transX, float scale) {
+            mView.setTranslationX(mView.getTranslationX() + transX * scale);
+        }
+
+        public int getCenterX() {
+            return mLeftPosition + mView.getWidth() / 2;
+        }
+
+        public int getMeasuredCenterX(float scale) {
+            return mLeftPosition + (int) (mView.getMeasuredWidth() * scale / 2);
+        }
+
+        public View getView() {
+            return mView;
+        }
+
+        private void layoutAt(int left, int top) {
+            mView.layout(left, top, left + mView.getMeasuredWidth(),
+                    top + mView.getMeasuredHeight());
+        }
+
+        public void layoutIn(Rect drawArea, int refCenter, float scale) {
+            // drawArea is where to layout in.
+            // refCenter is the absolute horizontal position of the center of drawArea.
+            int left = (int) (drawArea.centerX() + (mLeftPosition - refCenter) * scale);
+            int top = (int) (drawArea.centerY() - (mView.getMeasuredHeight() / 2) * scale);
+            layoutAt(left, top);
+            mView.setScaleX(scale);
+            mView.setScaleY(scale);
+        }
+    }
+
+    public FilmStripView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public FilmStripView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public FilmStripView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context);
+    }
+
+    private void init(Context context) {
+        // This is for positioning camera controller at the same place in
+        // different orientations.
+        setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
+
+        setWillNotDraw(false);
+        mContext = context;
+        mScale = 1.0f;
+        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;
+    }
+
+    public boolean isAnchoredTo(int id) {
+        if (mViewInfo[mCurrentInfo].getID() == id
+                && mViewInfo[mCurrentInfo].getCenterX() == mCenterX) {
+            return true;
+        }
+        return false;
+    }
+
+    public int getCurrentType() {
+        if (mDataAdapter == null) return ImageData.TYPE_NONE;
+        ViewInfo curr = mViewInfo[mCurrentInfo];
+        if (curr == null) return ImageData.TYPE_NONE;
+        return mDataAdapter.getImageData(curr.getID()).getType();
+    }
+
+    @Override
+    public void onDraw(Canvas c) {
+        if (mGeometryAnimator.hasNewGeometry()) {
+            layoutChildren();
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        int boundWidth = MeasureSpec.getSize(widthMeasureSpec);
+        int boundHeight = MeasureSpec.getSize(heightMeasureSpec);
+        if (mDataAdapter != null) {
+            mDataAdapter.suggestDecodeSize(boundWidth / 2, boundHeight / 2);
+        }
+
+        int wMode = View.MeasureSpec.EXACTLY;
+        int hMode = View.MeasureSpec.EXACTLY;
+
+        for (int i = 0; i < mViewInfo.length; i++) {
+            ViewInfo info = mViewInfo[i];
+            if (mViewInfo[i] == null) continue;
+
+            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;
+            }
+
+            int scaledWidth = boundWidth;
+            int scaledHeight = boundHeight;
+
+            if (imageWidth * scaledHeight > scaledWidth * imageHeight) {
+                scaledHeight = imageHeight * scaledWidth / imageWidth;
+            } else {
+                scaledWidth = imageWidth * scaledHeight / imageHeight;
+            }
+            mViewInfo[i].getView().measure(
+                    View.MeasureSpec.makeMeasureSpec(scaledWidth, wMode)
+                    , View.MeasureSpec.makeMeasureSpec(scaledHeight, hMode));
+        }
+        setMeasuredDimension(boundWidth, boundHeight);
+    }
+
+    private int findTheNearestView(int pointX) {
+
+        int nearest = 0;
+        // find the first non-null ViewInfo.
+        for (; nearest < BUFFER_SIZE
+                && (mViewInfo[nearest] == null || mViewInfo[nearest].getLeftPosition() == -1);
+                nearest++);
+        // no existing available ViewInfo
+        if (nearest == BUFFER_SIZE) return -1;
+        int min = Math.abs(pointX - mViewInfo[nearest].getCenterX());
+
+        for (int infoID = nearest + 1;
+                infoID < BUFFER_SIZE && mViewInfo[infoID] != null; infoID++) {
+            // not measured yet.
+            if  (mViewInfo[infoID].getLeftPosition() == -1) continue;
+
+            int c = mViewInfo[infoID].getCenterX();
+            int dist = Math.abs(pointX - c);
+            if (dist < min) {
+                min = dist;
+                nearest = infoID;
+            }
+        }
+        return nearest;
+    }
+
+    private ViewInfo buildInfoFromData(int dataID) {
+        ImageData data = mDataAdapter.getImageData(dataID);
+        if (data == null) return null;
+        data.prepare();
+        View v = mDataAdapter.getView(mContext, dataID);
+        if (v == null) return null;
+        ViewInfo info = new ViewInfo(dataID, v);
+        v = info.getView();
+        if (v != mCameraView) {
+            addView(info.getView());
+        } else {
+            v.setVisibility(View.VISIBLE);
+        }
+        return info;
+    }
+
+    private void removeInfo(int infoID) {
+        if (infoID >= mViewInfo.length || mViewInfo[infoID] == null) return;
+
+        ImageData data = mDataAdapter.getImageData(mViewInfo[infoID].getID());
+        checkForRemoval(data, mViewInfo[infoID].getView());
+        mViewInfo[infoID] = null;
+    }
+
+    // We try to keep the one closest to the center of the screen at position mCurrentInfo.
+    private void stepIfNeeded() {
+        int nearest = findTheNearestView(mCenterX);
+        // no change made.
+        if (nearest == -1 || nearest == mCurrentInfo) return;
+
+        int adjust = nearest - mCurrentInfo;
+        if (adjust > 0) {
+            for (int k = 0; k < adjust; k++) {
+                removeInfo(k);
+            }
+            for (int k = 0; k + adjust < BUFFER_SIZE; k++) {
+                mViewInfo[k] = mViewInfo[k + adjust];
+            }
+            for (int k = BUFFER_SIZE - adjust; k < BUFFER_SIZE; k++) {
+                mViewInfo[k] = null;
+                if (mViewInfo[k - 1] != null) {
+                        mViewInfo[k] = buildInfoFromData(mViewInfo[k - 1].getID() + 1);
+                }
+            }
+        } else {
+            for (int k = BUFFER_SIZE - 1; k >= BUFFER_SIZE + adjust; k--) {
+                removeInfo(k);
+            }
+            for (int k = BUFFER_SIZE - 1; k + adjust >= 0; k--) {
+                mViewInfo[k] = mViewInfo[k + adjust];
+            }
+            for (int k = -1 - adjust; k >= 0; k--) {
+                mViewInfo[k] = null;
+                if (mViewInfo[k + 1] != null) {
+                        mViewInfo[k] = buildInfoFromData(mViewInfo[k + 1].getID() - 1);
+                }
+            }
+        }
+    }
+
+    // Don't go beyond the bound.
+    private void adjustCenterX() {
+        ViewInfo curr = mViewInfo[mCurrentInfo];
+        if (curr == null) return;
+
+        if (curr.getID() == 0 && mCenterX < curr.getCenterX()) {
+            mCenterX = curr.getCenterX();
+            if (mGeometryAnimator.isScrolling()) {
+                mGeometryAnimator.stopScroll();
+            }
+            if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW
+                    && !mGeometryAnimator.isScalling()
+                    && mScale != MAX_SCALE) {
+                mGeometryAnimator.scaleTo(MAX_SCALE, DURATION_GEOMETRY_ADJUST, false);
+            }
+        }
+        if (curr.getID() == mDataAdapter.getTotalNumber() - 1
+                && mCenterX > curr.getCenterX()) {
+            mCenterX = curr.getCenterX();
+            if (!mGeometryAnimator.isScrolling()) {
+                mGeometryAnimator.stopScroll();
+            }
+        }
+    }
+
+    private void layoutChildren() {
+        if (mAnchorPending) {
+            mCenterX = mViewInfo[mCurrentInfo].getCenterX();
+            mAnchorPending = false;
+        }
+
+        if (mGeometryAnimator.hasNewGeometry()) {
+            mCenterX = mGeometryAnimator.getNewPosition();
+            mScale = mGeometryAnimator.getNewScale();
+        }
+
+        adjustCenterX();
+
+        mViewInfo[mCurrentInfo].layoutIn(mDrawArea, mCenterX, mScale);
+
+        // images on the left
+        for (int infoID = mCurrentInfo - 1; infoID >= 0; infoID--) {
+            ViewInfo curr = mViewInfo[infoID];
+            if (curr != null) {
+                ViewInfo next = mViewInfo[infoID + 1];
+                curr.setLeftPosition(
+                        next.getLeftPosition() - curr.getView().getMeasuredWidth() - H_PADDING);
+                curr.layoutIn(mDrawArea, mCenterX, mScale);
+            }
+        }
+
+        // images on the right
+        for (int infoID = mCurrentInfo + 1; infoID < BUFFER_SIZE; infoID++) {
+            ViewInfo curr = mViewInfo[infoID];
+            if (curr != null) {
+                ViewInfo prev = mViewInfo[infoID - 1];
+                curr.setLeftPosition(
+                        prev.getLeftPosition() + prev.getView().getMeasuredWidth() + H_PADDING);
+                curr.layoutIn(mDrawArea, mCenterX, mScale);
+            }
+        }
+
+        stepIfNeeded();
+        invalidate();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        if (mViewInfo[mCurrentInfo] == null) return;
+
+        mDrawArea.left = l;
+        mDrawArea.top = t;
+        mDrawArea.right = r;
+        mDrawArea.bottom = b;
+
+        layoutChildren();
+    }
+
+    // Keeps the view in the view hierarchy if it's camera preview.
+    // Remove from the hierarchy otherwise.
+    private void checkForRemoval(ImageData data, View v) {
+        if (data.getType() != ImageData.TYPE_CAMERA_PREVIEW) {
+            removeView(v);
+            data.recycle();
+        } else {
+            v.setVisibility(View.INVISIBLE);
+            if (mCameraView != null && mCameraView != v) {
+                removeView(mCameraView);
+                mCameraData = null;
+            }
+            mCameraView = v;
+            mCameraData = data;
+        }
+    }
+
+    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 mCenterX might be out of the bound.
+            ViewInfo currInfo = mViewInfo[mCurrentInfo];
+            if (currInfo.getID() == mDataAdapter.getTotalNumber() - 1
+                    && mCenterX > currInfo.getCenterX()) {
+                int adjustDiff = currInfo.getCenterX() - mCenterX;
+                mCenterX = currInfo.getCenterX();
+                for (int i = 0; i < BUFFER_SIZE; i++) {
+                    if (mViewInfo[i] != null) {
+                        mViewInfo[i].translateXBy(adjustDiff, mScale);
+                    }
+                }
+            }
+        } else {
+            // fill the removed place by right shift
+            mCenterX -= offsetX;
+
+            for (int i = removedInfo; i > 0; i--) {
+                mViewInfo[i] = mViewInfo[i - 1];
+                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() {
+                        checkForRemoval(data, removedView);
+                    }
+                })
+                .start();
+        layoutChildren();
+    }
+
+    public void setDataAdapter(DataAdapter adapter) {
+        mDataAdapter = adapter;
+        mDataAdapter.suggestDecodeSize(getMeasuredWidth(), getMeasuredHeight());
+        mDataAdapter.setListener(new DataAdapter.Listener() {
+            @Override
+            public void onDataLoaded() {
+                reload();
+            }
+
+            @Override
+            public void onDataUpdated(DataAdapter.UpdateReporter reporter) {
+                update(reporter);
+            }
+
+            @Override
+            public void onDataInserted(int dataID, ImageData data) {
+            }
+
+            @Override
+            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);
+            }
+        });
+    }
+
+    public boolean isInCameraFullscreen() {
+        return (isAnchoredTo(0) && mScale == 1f
+                && getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (isInCameraFullscreen()) return false;
+        return true;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        mGestureRecognizer.onTouchEvent(ev);
+        return true;
+    }
+
+    private void updateViewInfo(int infoID) {
+        ViewInfo info = mViewInfo[infoID];
+        removeView(info.getView());
+        mViewInfo[infoID] = buildInfoFromData(info.getID());
+    }
+
+    // Some of the data is changed.
+    private void update(DataAdapter.UpdateReporter reporter) {
+        // No data yet.
+        if (mViewInfo[mCurrentInfo] == null) {
+            reload();
+            return;
+        }
+
+        // Check the current one.
+        ViewInfo curr = mViewInfo[mCurrentInfo];
+        int dataID = curr.getID();
+        if (reporter.isDataRemoved(dataID)) {
+            mCenterX = -1;
+            reload();
+            return;
+        }
+        if (reporter.isDataUpdated(dataID)) {
+            updateViewInfo(mCurrentInfo);
+        }
+
+        // Check left
+        for (int i = mCurrentInfo - 1; i >= 0; i--) {
+            curr = mViewInfo[i];
+            if (curr != null) {
+                dataID = curr.getID();
+                if (reporter.isDataRemoved(dataID) || reporter.isDataUpdated(dataID)) {
+                    updateViewInfo(i);
+                }
+            } else {
+                ViewInfo next = mViewInfo[i + 1];
+                if (next != null) {
+                    mViewInfo[i] = buildInfoFromData(next.getID() - 1);
+                }
+            }
+        }
+
+        // Check right
+        for (int i = mCurrentInfo + 1; i < BUFFER_SIZE; i++) {
+            curr = mViewInfo[i];
+            if (curr != null) {
+                dataID = curr.getID();
+                if (reporter.isDataRemoved(dataID) || reporter.isDataUpdated(dataID)) {
+                    updateViewInfo(i);
+                }
+            } else {
+                ViewInfo prev = mViewInfo[i - 1];
+                if (prev != null) {
+                    mViewInfo[i] = buildInfoFromData(prev.getID() + 1);
+                }
+            }
+        }
+    }
+
+    // The whole data might be totally different. Flush all and load from the start.
+    private void reload() {
+        removeAllViews();
+        int dataNumber = mDataAdapter.getTotalNumber();
+        if (dataNumber == 0) return;
+
+        mViewInfo[mCurrentInfo] = buildInfoFromData(0);
+        mViewInfo[mCurrentInfo].setLeftPosition(0);
+        if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW) {
+            // we are in camera mode by default.
+            mGeometryAnimator.lockPositionAtViewInfo(mCurrentInfo);
+        }
+        for (int i = 1; mCurrentInfo + i < BUFFER_SIZE || mCurrentInfo - i >= 0; i++) {
+            int infoID = mCurrentInfo + i;
+            if (infoID < BUFFER_SIZE && mViewInfo[infoID - 1] != null) {
+                mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID - 1].getID() + 1);
+            }
+            infoID = mCurrentInfo - i;
+            if (infoID >= 0 && mViewInfo[infoID + 1] != null) {
+                mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID + 1].getID() - 1);
+            }
+        }
+        layoutChildren();
+    }
+
+    private void promoteData(int infoID, int dataID) {
+        if (mListener != null) {
+            mListener.onDataPromoted(dataID);
+        }
+    }
+
+    private void demoteData(int infoID, int dataID) {
+        if (mListener != null) {
+            mListener.onDataDemoted(dataID);
+        }
+    }
+
+    private void swipeHorizontally(float velocityX) {
+        float scaledVelocityX = velocityX / mScale;
+        if (isInCameraFullscreen() && scaledVelocityX < 0) {
+            mGeometryAnimator.unlockPosition();
+            mGeometryAnimator.scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST, false);
+        }
+        ViewInfo info = mViewInfo[mCurrentInfo];
+        if (info == null) return;
+        int w = getWidth();
+        mGeometryAnimator.fling((int) -scaledVelocityX,
+                // Estimation of possible length on the left. To ensure the
+                // velocity doesn't become too slow eventually, we add a huge number
+                // to the estimated maximum.
+                info.getLeftPosition() - (info.getID() + 100) * (w + H_PADDING),
+                // Estimation of possible length on the right. Likewise, exaggerate
+                // the possible maximum too.
+                info.getLeftPosition()
+                + (mDataAdapter.getTotalNumber() - info.getID() + 100)
+                * (w + H_PADDING));
+        layoutChildren();
+    }
+
+    // GeometryAnimator controls all the geometry animations. It passively
+    // tells the geometry information on demand.
+    private class GeometryAnimator implements
+            ValueAnimator.AnimatorUpdateListener,
+            Animator.AnimatorListener {
+
+        private ValueAnimator mScaleAnimator;
+        private boolean mHasNewScale;
+        private float mNewScale;
+
+        private Scroller mScroller;
+        private boolean mHasNewPosition;
+        private DecelerateInterpolator mDecelerateInterpolator;
+
+        private boolean mCanStopScroll;
+        private boolean mCanStopScale;
+
+        private boolean mIsPositionLocked;
+        private int mLockedViewInfo;
+
+        GeometryAnimator(Context context) {
+            mScroller = new Scroller(context);
+            mHasNewPosition = false;
+            mScaleAnimator = new ValueAnimator();
+            mScaleAnimator.addUpdateListener(GeometryAnimator.this);
+            mScaleAnimator.addListener(GeometryAnimator.this);
+            mDecelerateInterpolator = new DecelerateInterpolator();
+            mCanStopScroll = true;
+            mCanStopScale = true;
+            mHasNewScale = false;
+        }
+
+        boolean isScrolling() {
+            return !mScroller.isFinished();
+        }
+
+        boolean isScalling() {
+            return mScaleAnimator.isRunning();
+        }
+
+        boolean isFinished() {
+            return (!isScrolling() && !isScalling());
+        }
+
+        boolean hasNewGeometry() {
+            mHasNewPosition = mScroller.computeScrollOffset();
+            if (!mHasNewPosition) {
+                mCanStopScroll = true;
+            }
+            // If the position is locked, then we always return true to force
+            // the position value to use the locked value.
+            return (mHasNewPosition || mHasNewScale || mIsPositionLocked);
+        }
+
+        // Always call hasNewGeometry() before getting the new scale value.
+        float getNewScale() {
+            if (!mHasNewScale) return mScale;
+            mHasNewScale = false;
+            return mNewScale;
+        }
+
+        // Always call hasNewGeometry() before getting the new position value.
+        int getNewPosition() {
+            if (mIsPositionLocked) {
+                if (mViewInfo[mLockedViewInfo] == null) return mCenterX;
+                return mViewInfo[mLockedViewInfo].getCenterX();
+            }
+            if (!mHasNewPosition) return mCenterX;
+            return mScroller.getCurrX();
+        }
+
+        void lockPositionAtViewInfo(int infoID) {
+            mIsPositionLocked = true;
+            mLockedViewInfo = infoID;
+        }
+
+        void unlockPosition() {
+            if (mIsPositionLocked) {
+                // only when the position is previously locked we set the current
+                // position to make it consistent.
+                if (mViewInfo[mLockedViewInfo] != null) {
+                    mCenterX = mViewInfo[mLockedViewInfo].getCenterX();
+                }
+                mIsPositionLocked = false;
+            }
+        }
+
+        void fling(int velocityX, int minX, int maxX) {
+            if (!stopScroll() || mIsPositionLocked) return;
+            mScroller.fling(mCenterX, 0, velocityX, 0, minX, maxX, 0, 0);
+        }
+
+        boolean stopScroll() {
+            if (!mCanStopScroll) return false;
+            mScroller.forceFinished(true);
+            mHasNewPosition = false;
+            return true;
+        }
+
+        boolean stopScale() {
+            if (!mCanStopScale) return false;
+            mScaleAnimator.cancel();
+            mHasNewScale = false;
+            return true;
+        }
+
+        void stop() {
+            stopScroll();
+            stopScale();
+        }
+
+        void scrollTo(int position, int duration, boolean interruptible) {
+            if (!stopScroll() || mIsPositionLocked) return;
+            mCanStopScroll = interruptible;
+            stopScroll();
+            mScroller.startScroll(mCenterX, 0, position - mCenterX,
+                    0, duration);
+        }
+
+        void scrollTo(int position, int duration) {
+            scrollTo(position, duration, true);
+        }
+
+        void scaleTo(float scale, int duration, boolean interruptible) {
+            if (!stopScale()) return;
+            mCanStopScale = interruptible;
+            mScaleAnimator.setDuration(duration);
+            mScaleAnimator.setFloatValues(mScale, scale);
+            mScaleAnimator.setInterpolator(mDecelerateInterpolator);
+            mScaleAnimator.start();
+            mHasNewScale = true;
+        }
+
+        void scaleTo(float scale, int duration) {
+            scaleTo(scale, duration, true);
+        }
+
+        @Override
+        public void onAnimationUpdate(ValueAnimator animation) {
+            mHasNewScale = true;
+            mNewScale = (Float) animation.getAnimatedValue();
+            layoutChildren();
+        }
+
+        @Override
+        public void onAnimationStart(Animator anim) {
+        }
+
+        @Override
+        public void onAnimationEnd(Animator anim) {
+            ViewInfo info = mViewInfo[mCurrentInfo];
+            if (info != null && mCenterX == info.getCenterX()) {
+                if (mScale == 1f) {
+                    lockPositionAtViewInfo(mCurrentInfo);
+                } else if (mScale == FILM_STRIP_SCALE) {
+                    unlockPosition();
+                }
+            }
+            mCanStopScale = true;
+        }
+
+        @Override
+        public void onAnimationCancel(Animator anim) {
+        }
+
+        @Override
+        public void onAnimationRepeat(Animator anim) {
+        }
+    }
+
+    private class MyGestureReceiver implements FilmStripGestureRecognizer.Listener {
+        // Indicating the current trend of scaling is up (>1) or down (<1).
+        private float mScaleTrend;
+
+        @Override
+        public boolean onSingleTapUp(float x, float y) {
+            return false;
+        }
+
+        @Override
+        public boolean onDoubleTap(float x, float y) {
+            return false;
+        }
+
+        @Override
+        public boolean onDown(float x, float y) {
+            mGeometryAnimator.stop();
+            return true;
+        }
+
+        @Override
+        public boolean onUp(float x, float y) {
+            float halfH = getHeight() / 2;
+            for (int i = 0; i < BUFFER_SIZE; i++) {
+                if (mViewInfo[i] == null) continue;
+                float transY = mViewInfo[i].getTranslationY(mScale);
+                if (transY == 0) continue;
+                int id = mViewInfo[i].getID();
+
+                if (mDataAdapter.getImageData(id)
+                        .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);
+                }
+                mCenterX += 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;
+        }
+
+        @Override
+        public boolean onFling(float velocityX, float velocityY) {
+            if (Math.abs(velocityX) > Math.abs(velocityY)) {
+                swipeHorizontally(velocityX);
+            } else {
+                // ignore horizontal fling.
+            }
+            return true;
+        }
+
+        @Override
+        public boolean onScaleBegin(float focusX, float focusY) {
+            if (isInCameraFullscreen()) return false;
+            mScaleTrend = 1f;
+            return true;
+        }
+
+        @Override
+        public boolean onScale(float focusX, float focusY, float scale) {
+            if (isInCameraFullscreen()) return false;
+
+            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;
+            }
+            layoutChildren();
+            return true;
+        }
+
+        @Override
+        public void onScaleEnd() {
+            if (mScaleTrend >= 1f) {
+                if (mScale != 1f) {
+                    mGeometryAnimator.scaleTo(1f, DURATION_GEOMETRY_ADJUST, false);
+                }
+
+                if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW) {
+                    if (isAnchoredTo(0)) {
+                        mGeometryAnimator.lockPositionAtViewInfo(mCurrentInfo);
+                    } else {
+                        mGeometryAnimator.scrollTo(
+                                mViewInfo[mCurrentInfo].getCenterX(),
+                                DURATION_GEOMETRY_ADJUST, false);
+                    }
+                }
+            } else {
+                // Scale down to film strip mode.
+                if (mScale == FILM_STRIP_SCALE) {
+                    mGeometryAnimator.unlockPosition();
+                    return;
+                }
+                mGeometryAnimator.scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST, false);
+            }
+        }
+    }
+}
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/app/CommonControllerOverlay.java b/src/com/android/gallery3d/app/CommonControllerOverlay.java
index a4f5807..9adb4e7 100644
--- a/src/com/android/gallery3d/app/CommonControllerOverlay.java
+++ b/src/com/android/gallery3d/app/CommonControllerOverlay.java
@@ -66,6 +66,10 @@
 
     protected boolean mCanReplay = true;
 
+    public void setSeekable(boolean canSeek) {
+        mTimeBar.setSeekable(canSeek);
+    }
+
     public CommonControllerOverlay(Context context) {
         super(context);
 
diff --git a/src/com/android/gallery3d/app/MoviePlayer.java b/src/com/android/gallery3d/app/MoviePlayer.java
index 00e4cd6..ce91834 100644
--- a/src/com/android/gallery3d/app/MoviePlayer.java
+++ b/src/com/android/gallery3d/app/MoviePlayer.java
@@ -25,7 +25,6 @@
 import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.graphics.Color;
 import android.media.AudioManager;
 import android.media.MediaPlayer;
 import android.net.Uri;
@@ -135,6 +134,17 @@
                 return true;
             }
         });
+        mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+            @Override
+            public void onPrepared(MediaPlayer player) {
+                if (!mVideoView.canSeekForward() || !mVideoView.canSeekBackward()) {
+                    mController.setSeekable(false);
+                } else {
+                    mController.setSeekable(true);
+                }
+                setProgress();
+            }
+        });
 
         // The SurfaceView is transparent before drawing the first frame.
         // This makes the UI flashing when open a video. (black -> old screen
diff --git a/src/com/android/gallery3d/app/TimeBar.java b/src/com/android/gallery3d/app/TimeBar.java
index 402dfcf..246346a 100644
--- a/src/com/android/gallery3d/app/TimeBar.java
+++ b/src/com/android/gallery3d/app/TimeBar.java
@@ -259,4 +259,8 @@
         }
     }
 
+    public void setSeekable(boolean canSeek) {
+        mShowScrubber = canSeek;
+    }
+
 }
diff --git a/src/com/android/gallery3d/filtershow/FilterShowActivity.java b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
index e5ddbf5..27b5fb9 100644
--- a/src/com/android/gallery3d/filtershow/FilterShowActivity.java
+++ b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
@@ -69,6 +69,8 @@
 import com.android.gallery3d.filtershow.state.StateAdapter;
 import com.android.gallery3d.filtershow.tools.BitmapTask;
 import com.android.gallery3d.filtershow.tools.SaveCopyTask;
+import com.android.gallery3d.filtershow.tools.XmpPresets;
+import com.android.gallery3d.filtershow.tools.XmpPresets.XMresults;
 import com.android.gallery3d.filtershow.ui.FramedTextButton;
 import com.android.gallery3d.filtershow.ui.Spline;
 import com.android.gallery3d.util.GalleryUtils;
@@ -119,6 +121,9 @@
     private LoadBitmapTask mLoadBitmapTask;
     private boolean mLoading = true;
 
+    private Uri mOriginalImageUri = null;
+    private ImagePreset mOriginalPreset = null;
+
     private CategoryAdapter mCategoryLooksAdapter = null;
     private CategoryAdapter mCategoryBordersAdapter = null;
     private CategoryAdapter mCategoryGeometryAdapter = null;
@@ -147,6 +152,7 @@
 
         setDefaultPreset();
 
+        extractXMPData();
         processIntent();
     }
 
@@ -285,9 +291,12 @@
         }
 
         mAction = intent.getAction();
-
-        if (intent.getData() != null) {
-            startLoadBitmap(intent.getData());
+        Uri srcUri = intent.getData();
+        if (mOriginalImageUri != null) {
+            srcUri = mOriginalImageUri;
+        }
+        if (srcUri != null) {
+            startLoadBitmap(srcUri);
         } else {
             pickImage();
         }
@@ -364,8 +373,9 @@
 
         for (int i = 0; i < borders.size(); i++) {
             FilterRepresentation filter = borders.elementAt(i);
+            filter.setScrName(getString(R.string.borders));
             if (i == 0) {
-                filter.setName(getString(R.string.none));
+                filter.setScrName(getString(R.string.none));
             }
         }
 
@@ -519,6 +529,11 @@
             mCategoryFiltersAdapter.imageLoaded();
             mLoadBitmapTask = null;
 
+            if (mOriginalPreset != null) {
+                MasterImage.getImage().setPreset(mOriginalPreset, true);
+                mOriginalPreset = null;
+            }
+
             if (mAction == TINY_PLANET_ACTION) {
                 showRepresentation(mCategoryFiltersAdapter.getTinyPlanet());
             }
@@ -1053,4 +1068,13 @@
         System.loadLibrary("jni_filtershow_filters");
     }
 
+    private void extractXMPData() {
+        XMresults res = XmpPresets.extractXMPData(
+                getBaseContext(), mMasterImage, getIntent().getData());
+        if (res == null)
+            return;
+
+        mOriginalImageUri = res.originalimage;
+        mOriginalPreset = res.preset;
+    }
 }
diff --git a/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java b/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java
index 8760c4a..1ea40f2 100644
--- a/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java
+++ b/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java
@@ -29,8 +29,9 @@
 import com.android.gallery3d.filtershow.imageshow.MasterImage;
 import com.android.gallery3d.filtershow.presets.FilterEnvironment;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
+import com.android.gallery3d.filtershow.presets.PipelineInterface;
 
-public class CachingPipeline {
+public class CachingPipeline implements PipelineInterface {
     private static final String LOGTAG = "CachingPipeline";
     private boolean DEBUG = false;
 
@@ -65,22 +66,10 @@
         mName = name;
     }
 
-    public static synchronized Resources getResources() {
-        return sResources;
-    }
-
-    public static synchronized void setResources(Resources resources) {
-        sResources = resources;
-    }
-
     public static synchronized RenderScript getRenderScriptContext() {
         return sRS;
     }
 
-    public static synchronized void setRenderScriptContext(RenderScript RS) {
-        sRS = RS;
-    }
-
     public static synchronized void createRenderscriptContext(Activity context) {
         if (sRS != null) {
             Log.w(LOGTAG, "A prior RS context exists when calling setRenderScriptContext");
@@ -128,6 +117,10 @@
         }
     }
 
+    public Resources getResources() {
+        return sRS.getApplicationContext().getResources();
+    }
+
     private synchronized void destroyPixelAllocations() {
         if (DEBUG) {
             Log.v(LOGTAG, "destroyPixelAllocations in " + getName());
@@ -167,14 +160,14 @@
     }
 
     private void setupEnvironment(ImagePreset preset, boolean highResPreview) {
-        mEnvironment.setCachingPipeline(this);
+        mEnvironment.setPipeline(this);
         mEnvironment.setFiltersManager(mFiltersManager);
         if (highResPreview) {
             mEnvironment.setScaleFactor(mHighResPreviewScaleFactor);
         } else {
             mEnvironment.setScaleFactor(mPreviewScaleFactor);
         }
-        mEnvironment.setQuality(ImagePreset.QUALITY_PREVIEW);
+        mEnvironment.setQuality(FilterEnvironment.QUALITY_PREVIEW);
         mEnvironment.setImagePreset(preset);
         mEnvironment.setStop(false);
     }
@@ -293,11 +286,11 @@
                     || request.getType() == RenderingRequest.STYLE_ICON_RENDERING) {
 
                 if (request.getType() == RenderingRequest.ICON_RENDERING) {
-                    mEnvironment.setQuality(ImagePreset.QUALITY_ICON);
+                    mEnvironment.setQuality(FilterEnvironment.QUALITY_ICON);
                 } else  if (request.getType() == RenderingRequest.STYLE_ICON_RENDERING) {
                     mEnvironment.setQuality(ImagePreset.STYLE_ICON);
                 } else {
-                    mEnvironment.setQuality(ImagePreset.QUALITY_PREVIEW);
+                    mEnvironment.setQuality(FilterEnvironment.QUALITY_PREVIEW);
                 }
 
                 Bitmap bmp = preset.apply(bitmap, mEnvironment);
@@ -317,8 +310,11 @@
             setupEnvironment(preset, false);
             mFiltersManager.freeFilterResources(preset);
             preset.applyFilters(-1, -1, in, out, mEnvironment);
-            // TODO: we should render the border onto a different bitmap instead
-            preset.applyBorder(in, out, mEnvironment);
+            boolean copyOut = false;
+            if (preset.nbFilters() > 0) {
+                copyOut = true;
+            }
+            preset.applyBorder(in, out, copyOut, mEnvironment);
         }
     }
 
@@ -328,7 +324,7 @@
                 return bitmap;
             }
             setupEnvironment(preset, false);
-            mEnvironment.setQuality(ImagePreset.QUALITY_FINAL);
+            mEnvironment.setQuality(FilterEnvironment.QUALITY_FINAL);
             mEnvironment.setScaleFactor(1.0f);
             mFiltersManager.freeFilterResources(preset);
             bitmap = preset.applyGeometry(bitmap, mEnvironment);
@@ -345,7 +341,7 @@
         }
         mGeometry.useRepresentation(preset.getGeometry());
         return mGeometry.apply(bitmap, mPreviewScaleFactor,
-                ImagePreset.QUALITY_PREVIEW);
+                FilterEnvironment.QUALITY_PREVIEW);
     }
 
     public synchronized void compute(TripleBufferBitmap buffer, ImagePreset preset, int type) {
@@ -458,4 +454,7 @@
         return mName;
     }
 
+    public RenderScript getRSContext() {
+        return CachingPipeline.getRenderScriptContext();
+    }
 }
diff --git a/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java b/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java
index 072edd7..25169c2 100644
--- a/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java
+++ b/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java
@@ -109,5 +109,4 @@
     public void setFilterView(FilterView editor) {
         mEditor = editor;
     }
-
 }
diff --git a/src/com/android/gallery3d/filtershow/controller/BasicSlider.java b/src/com/android/gallery3d/filtershow/controller/BasicSlider.java
index df5b6ae..9d8278d 100644
--- a/src/com/android/gallery3d/filtershow/controller/BasicSlider.java
+++ b/src/com/android/gallery3d/filtershow/controller/BasicSlider.java
@@ -84,5 +84,4 @@
         mSeekBar.setMax(mParameter.getMaximum() - mParameter.getMinimum());
         mSeekBar.setProgress(mParameter.getValue() - mParameter.getMinimum());
     }
-
 }
diff --git a/src/com/android/gallery3d/filtershow/crop/CropMath.java b/src/com/android/gallery3d/filtershow/crop/CropMath.java
index 849ac60..671554f 100644
--- a/src/com/android/gallery3d/filtershow/crop/CropMath.java
+++ b/src/com/android/gallery3d/filtershow/crop/CropMath.java
@@ -196,14 +196,13 @@
         float finalH = origH;
         if (origA < a) {
             finalH = origW / a;
+            r.top = r.centerY() - finalH / 2;
+            r.bottom = r.top + finalH;
         } else {
             finalW = origH * a;
+            r.left = r.centerX() - finalW / 2;
+            r.right = r.left + finalW;
         }
-        float centX = r.centerX();
-        float centY = r.centerY();
-        float hw = finalW / 2;
-        float hh = finalH / 2;
-        r.set(centX - hw, centY - hh, centX + hw, centY + hh);
     }
 
     /**
diff --git a/src/com/android/gallery3d/filtershow/data/FilterStackDBHelper.java b/src/com/android/gallery3d/filtershow/data/FilterStackDBHelper.java
new file mode 100644
index 0000000..e18d310
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/data/FilterStackDBHelper.java
@@ -0,0 +1,101 @@
+/*
+ * 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.data;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+public class FilterStackDBHelper extends SQLiteOpenHelper {
+
+    public static final int DATABASE_VERSION = 1;
+    public static final String DATABASE_NAME = "filterstacks.db";
+    private static final String SQL_CREATE_TABLE = "CREATE TABLE ";
+
+    public static interface FilterStack {
+        /** The row uid */
+        public static final String _ID = "_id";
+        /** The table name */
+        public static final String TABLE = "filterstack";
+        /** The stack name */
+        public static final String STACK_ID = "stack_id";
+        /** A serialized stack of filters. */
+        public static final String FILTER_STACK= "stack";
+    }
+
+    private static final String[][] CREATE_FILTER_STACK = {
+            { FilterStack._ID, "INTEGER PRIMARY KEY AUTOINCREMENT" },
+            { FilterStack.STACK_ID, "TEXT" },
+            { FilterStack.FILTER_STACK, "BLOB" },
+    };
+
+    public FilterStackDBHelper(Context context, String name, int version) {
+        super(context, name, null, version);
+    }
+
+    public FilterStackDBHelper(Context context, String name) {
+        this(context, name, DATABASE_VERSION);
+    }
+
+    public FilterStackDBHelper(Context context) {
+        this(context, DATABASE_NAME);
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        createTable(db, FilterStack.TABLE, CREATE_FILTER_STACK);
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        dropTable(db, FilterStack.TABLE);
+        onCreate(db);
+    }
+
+    protected static void createTable(SQLiteDatabase db, String table, String[][] columns) {
+        StringBuilder create = new StringBuilder(SQL_CREATE_TABLE);
+        create.append(table).append('(');
+        boolean first = true;
+        for (String[] column : columns) {
+            if (!first) {
+                create.append(',');
+            }
+            first = false;
+            for (String val : column) {
+                create.append(val).append(' ');
+            }
+        }
+        create.append(')');
+        db.beginTransaction();
+        try {
+            db.execSQL(create.toString());
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    protected static void dropTable(SQLiteDatabase db, String table) {
+        db.beginTransaction();
+        try {
+            db.execSQL("drop table if exists " + table);
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/data/FilterStackSource.java b/src/com/android/gallery3d/filtershow/data/FilterStackSource.java
new file mode 100644
index 0000000..4e34377
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/data/FilterStackSource.java
@@ -0,0 +1,143 @@
+/*
+ * 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.data;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.gallery3d.filtershow.data.FilterStackDBHelper.FilterStack;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FilterStackSource {
+    private static final String LOGTAG = "FilterStackSource";
+
+    private SQLiteDatabase database = null;;
+    private final FilterStackDBHelper dbHelper;
+
+    public FilterStackSource(Context context) {
+        dbHelper = new FilterStackDBHelper(context);
+    }
+
+    public void open() {
+        try {
+            database = dbHelper.getWritableDatabase();
+        } catch (SQLiteException e) {
+            Log.w(LOGTAG, "could not open database", e);
+        }
+    }
+
+    public void close() {
+        database = null;
+        dbHelper.close();
+    }
+
+    public boolean insertStack(String stackName, byte[] stackBlob) {
+        boolean ret = true;
+        ContentValues val = new ContentValues();
+        val.put(FilterStack.STACK_ID, stackName);
+        val.put(FilterStack.FILTER_STACK, stackBlob);
+        database.beginTransaction();
+        try {
+            ret = (-1 != database.insert(FilterStack.TABLE, null, val));
+            database.setTransactionSuccessful();
+        } finally {
+            database.endTransaction();
+        }
+        return ret;
+    }
+
+    public boolean removeStack(String stackName) {
+        boolean ret = true;
+        database.beginTransaction();
+        try {
+            ret = (0 != database.delete(FilterStack.TABLE, FilterStack.STACK_ID + " = ?",
+                    new String[] { stackName}));
+            database.setTransactionSuccessful();
+        } finally {
+            database.endTransaction();
+        }
+        return ret;
+    }
+
+    public void removeAllStacks() {
+        database.beginTransaction();
+        try {
+            database.delete(FilterStack.TABLE, null, null);
+            database.setTransactionSuccessful();
+        } finally {
+            database.endTransaction();
+        }
+    }
+
+    public byte[] getStack(String stackName) {
+        byte[] ret = null;
+        Cursor c = null;
+        database.beginTransaction();
+        try {
+            c = database.query(FilterStack.TABLE,
+                    new String[] { FilterStack.FILTER_STACK },
+                    FilterStack.STACK_ID + " = ?",
+                    new String[] { stackName }, null, null, null, null);
+            if (c != null && c.moveToFirst() && !c.isNull(0)) {
+                ret = c.getBlob(0);
+            }
+            database.setTransactionSuccessful();
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+            database.endTransaction();
+        }
+        return ret;
+    }
+
+    public List<Pair<String, byte[]>> getAllStacks() {
+        List<Pair<String, byte[]>> ret = new ArrayList<Pair<String, byte[]>>();
+        Cursor c = null;
+        database.beginTransaction();
+        try {
+            c = database.query(FilterStack.TABLE,
+                    new String[] { FilterStack.STACK_ID, FilterStack.FILTER_STACK },
+                    null, null, null, null, null, null);
+            if (c != null) {
+                boolean loopCheck = c.moveToFirst();
+                while (loopCheck) {
+                    String name = (c.isNull(0)) ?  null : c.getString(0);
+                    byte[] b = (c.isNull(1)) ? null : c.getBlob(1);
+                    ret.add(new Pair<String, byte[]>(name, b));
+                    loopCheck = c.moveToNext();
+                }
+            }
+            database.setTransactionSuccessful();
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+            database.endTransaction();
+        }
+        if (ret.size() <= 0) {
+            return null;
+        }
+        return ret;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
index 9927a0a..1c7294c 100644
--- a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
+++ b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
@@ -17,6 +17,8 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.util.Log;
+
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
@@ -24,11 +26,14 @@
 import java.util.HashMap;
 import java.util.Vector;
 
-public abstract class BaseFiltersManager {
+public abstract class BaseFiltersManager implements FiltersManagerInterface {
     protected HashMap<Class, ImageFilter> mFilters = null;
+    protected HashMap<String, FilterRepresentation> mRepresentationLookup = null;
+    private static final String LOGTAG = "BaseFiltersManager";
 
     protected void init() {
         mFilters = new HashMap<Class, ImageFilter>();
+        mRepresentationLookup = new HashMap<String, FilterRepresentation>();
         Vector<Class> filters = new Vector<Class>();
         addFilterClasses(filters);
         for (Class filterClass : filters) {
@@ -36,6 +41,12 @@
                 Object filterInstance = filterClass.newInstance();
                 if (filterInstance instanceof ImageFilter) {
                     mFilters.put(filterClass, (ImageFilter) filterInstance);
+
+                    FilterRepresentation rep = 
+                    		((ImageFilter) filterInstance).getDefaultRepresentation();
+                    if (rep != null) {
+                        addRepresentation(rep);
+                    }
                 }
             } catch (InstantiationException e) {
                 e.printStackTrace();
@@ -45,6 +56,20 @@
         }
     }
 
+    public void addRepresentation(FilterRepresentation rep) {
+        mRepresentationLookup.put(rep.getSerializationName(), rep);
+    }
+
+    public FilterRepresentation createFilterFromName(String name) {
+        try {
+            return mRepresentationLookup.get(name).clone();
+        } catch (Exception e) {
+            Log.v(LOGTAG, "unable to generate a filter representation for \"" + name + "\"");
+            e.printStackTrace();
+        }
+        return null;
+    }
+
     public ImageFilter getFilter(Class c) {
         return mFilters.get(c);
     }
@@ -53,10 +78,6 @@
         return mFilters.get(representation.getFilterClass());
     }
 
-    public void addFilter(Class filterClass, ImageFilter filter) {
-        mFilters.put(filterClass, filter);
-    }
-
     public FilterRepresentation getRepresentation(Class c) {
         ImageFilter filter = mFilters.get(c);
         if (filter != null) {
@@ -89,7 +110,7 @@
 
     protected void addFilterClasses(Vector<Class> filters) {
         filters.add(ImageFilterTinyPlanet.class);
-        //filters.add(ImageFilterRedEye.class);
+        filters.add(ImageFilterRedEye.class);
         filters.add(ImageFilterWBalance.class);
         filters.add(ImageFilterExposure.class);
         filters.add(ImageFilterVignette.class);
@@ -99,7 +120,7 @@
         filters.add(ImageFilterVibrance.class);
         filters.add(ImageFilterSharpen.class);
         filters.add(ImageFilterCurves.class);
-        // filters.add(ImageFilterDraw.class);
+        filters.add(ImageFilterDraw.class);
         filters.add(ImageFilterHue.class);
         filters.add(ImageFilterSaturated.class);
         filters.add(ImageFilterBwFilter.class);
@@ -168,8 +189,8 @@
     }
 
     public void addTools(Vector<FilterRepresentation> representations) {
-        //representations.add(getRepresentation(ImageFilterRedEye.class));
-        // representations.add(getRepresentation(ImageFilterDraw.class));
+        representations.add(getRepresentation(ImageFilterRedEye.class));
+        representations.add(getRepresentation(ImageFilterDraw.class));
     }
 
     public void setFilterResources(Resources resources) {
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java
index 4d0651e..368e5c0 100644
--- a/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java
+++ b/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java
@@ -31,6 +31,8 @@
     private int mMaximum;
     private int mDefaultValue;
     private int mPreviewValue;
+    public static final String SERIAL_NAME = "Name";
+    public static final String SERIAL_VALUE = "Value";
     private boolean mLogVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
 
     public FilterBasicRepresentation(String name, int minimum, int value, int maximum) {
@@ -171,4 +173,23 @@
     public void copyFrom(Parameter src) {
         useParametersFrom((FilterBasicRepresentation) src);
     }
+
+    @Override
+    public String[][] serializeRepresentation() {
+        String[][] ret = {
+                {SERIAL_NAME  , getName() },
+                {SERIAL_VALUE , Integer.toString(mValue)}};
+        return ret;
+    }
+
+    @Override
+    public void deSerializeRepresentation(String[][] rep) {
+        super.deSerializeRepresentation(rep);
+        for (int i = 0; i < rep.length; i++) {
+            if (SERIAL_VALUE.equals(rep[i][0])) {
+                mValue = Integer.parseInt(rep[i][1]);
+                break;
+            }
+        }
+    }
 }
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java
index cbcae4b..a32068a 100644
--- a/src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java
+++ b/src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java
@@ -15,6 +15,7 @@
 
     public FilterCurvesRepresentation() {
         super("Curves");
+        setSerializationName("CURVES");
         setFilterClass(ImageFilterCurves.class);
         setTextId(R.string.curvesRGB);
         setButtonId(R.id.curvesButtonRGB);
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java
index dc59b0c..9b144b9 100644
--- a/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java
+++ b/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java
@@ -49,6 +49,7 @@
 
     public FilterDrawRepresentation() {
         super("Draw");
+        setSerializationName("DRAW");
         setFilterClass(ImageFilterDraw.class);
         setPriority(FilterRepresentation.TYPE_VIGNETTE);
         setTextId(R.string.imageDraw);
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java
index 6e2e7ea..1ceffb4 100644
--- a/src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java
+++ b/src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java
@@ -21,7 +21,8 @@
 import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
 
 public class FilterFxRepresentation extends FilterRepresentation {
-    private static final String LOGTAG = "FilterFxRepresentation";
+   private static final String SERIALIZATION_NAME = "LUT3D";
+   private static final String LOGTAG = "FilterFxRepresentation";
     // TODO: When implementing serialization, we should find a unique way of
     // specifying bitmaps / names (the resource IDs being random)
     private int mBitmapResource = 0;
@@ -29,6 +30,8 @@
 
     public FilterFxRepresentation(String name, int bitmapResource, int nameResource) {
         super(name);
+        setSerializationName(SERIALIZATION_NAME);
+
         mBitmapResource = bitmapResource;
         mNameResource = nameResource;
         setFilterClass(ImageFilterFx.class);
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java
index 3f823ea..8a87841 100644
--- a/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java
+++ b/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java
@@ -28,6 +28,7 @@
 
     public FilterRedEyeRepresentation() {
         super("RedEye",R.string.redeye,EditorRedEye.ID);
+        setSerializationName("REDEYE");
         setFilterClass(ImageFilterRedEye.class);
         setOverlayId(R.drawable.photoeditor_effect_redeye);
         setOverlayOnly(true);
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java
index 82012b9..91bf676 100644
--- a/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java
+++ b/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java
@@ -16,7 +16,7 @@
 
 package com.android.gallery3d.filtershow.filters;
 
-import com.android.gallery3d.app.Log;
+import android.util.Log;
 import com.android.gallery3d.filtershow.editors.BasicEditor;
 
 public class FilterRepresentation implements Cloneable {
@@ -34,7 +34,7 @@
     private boolean mShowEditingControls = true;
     private boolean mShowParameterValue = true;
     private boolean mShowUtilityPanel = true;
-
+    private String mSerializationName;
     public static final byte TYPE_BORDER = 1;
     public static final byte TYPE_FX = 2;
     public static final byte TYPE_WBALANCE = 3;
@@ -63,6 +63,8 @@
         representation.setShowEditingControls(showEditingControls());
         representation.setShowParameterValue(showParameterValue());
         representation.setShowUtilityPanel(showUtilityPanel());
+        representation.mSerializationName = mSerializationName;
+
         representation.mTempRepresentation =
                 mTempRepresentation != null ? mTempRepresentation.clone() : null;
         if (DEBUG) {
@@ -96,6 +98,10 @@
         return mName;
     }
 
+    public void setScrName(String name) {
+        mName = name;
+    }
+
     public void setName(String name) {
         mName = name;
     }
@@ -104,6 +110,14 @@
         return mName;
     }
 
+    public void setSerializationName(String sname) {
+        mSerializationName = sname;
+    }
+
+    public String getSerializationName() {
+        return mSerializationName;
+    }
+
     public void setPriority(int priority) {
         mPriority = priority;
     }
@@ -241,4 +255,17 @@
         return "";
     }
 
+    public String[][] serializeRepresentation() {
+        String[][] ret = { { "Name" , getName() }};
+        return ret;
+    }
+
+    public void deSerializeRepresentation(String[][] rep) {
+        for (int i = 0; i < rep.length; i++) {
+            if ("Name".equals(rep[i][0])) {
+                mName = rep[i][0];
+                break;
+            }
+        }
+    }
 }
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java
index ac5e046..48c8b38 100644
--- a/src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java
+++ b/src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java
@@ -20,11 +20,14 @@
 import com.android.gallery3d.filtershow.editors.EditorTinyPlanet;
 
 public class FilterTinyPlanetRepresentation extends FilterBasicRepresentation {
+    private static final String SERIALIZATION_NAME = "TINYPLANET";
     private static final String LOGTAG = "FilterTinyPlanetRepresentation";
+    private static final String SERIAL_ANGLE = "Angle";
     private float mAngle = 0;
 
     public FilterTinyPlanetRepresentation() {
         super("TinyPlanet", 0, 50, 100);
+        setSerializationName(SERIALIZATION_NAME);
         setShowParameterValue(true);
         setFilterClass(ImageFilterTinyPlanet.class);
         setPriority(FilterRepresentation.TYPE_TINYPLANET);
@@ -71,4 +74,25 @@
         // TinyPlanet always has an effect
         return false;
     }
+
+    @Override
+    public String[][] serializeRepresentation() {
+        String[][] ret = {
+                {SERIAL_NAME  , getName() },
+                {SERIAL_VALUE , Integer.toString(getValue())},
+                {SERIAL_ANGLE , Float.toString(mAngle)}};
+        return ret;
+    }
+
+    @Override
+    public void deSerializeRepresentation(String[][] rep) {
+        super.deSerializeRepresentation(rep);
+        for (int i = 0; i < rep.length; i++) {
+            if (SERIAL_VALUE.equals(rep[i][0])) {
+                setValue(Integer.parseInt(rep[i][1]));
+            } else if (SERIAL_ANGLE.equals(rep[i][0])) {
+                setAngle(Float.parseFloat(rep[i][1]));
+            }
+        }
+    }
 }
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java
index eef54ef..9827088 100644
--- a/src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java
+++ b/src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java
@@ -29,6 +29,7 @@
 
     public FilterVignetteRepresentation() {
         super("Vignette", -100, 50, 100);
+        setSerializationName("VIGNETTE");
         setShowParameterValue(true);
         setPriority(FilterRepresentation.TYPE_VIGNETTE);
         setTextId(R.string.vignette);
@@ -111,4 +112,44 @@
     public boolean isNil() {
         return getValue() == 0;
     }
+
+    private static final String[] sParams = {
+            "Name", "value", "mCenterX", "mCenterY", "mRadiusX",
+            "mRadiusY"
+    };
+
+    @Override
+    public String[][] serializeRepresentation() {
+        String[][] ret = {
+                { sParams[0], getName() },
+                { sParams[1], Integer.toString(getValue()) },
+                { sParams[2], Float.toString(mCenterX) },
+                { sParams[3], Float.toString(mCenterY) },
+                { sParams[4], Float.toString(mRadiusX) },
+                { sParams[5], Float.toString(mRadiusY) }
+        };
+        return ret;
+    }
+
+    @Override
+    public void deSerializeRepresentation(String[][] rep) {
+        super.deSerializeRepresentation(rep);
+        for (int i = 0; i < rep.length; i++) {
+            String key = rep[i][0];
+            String value = rep[i][1];
+            if (sParams[0].equals(key)) {
+                setName(value);
+            } else if (sParams[1].equals(key)) {
+               setValue(Integer.parseInt(value));
+            } else if (sParams[2].equals(key)) {
+                mCenterX = Float.parseFloat(value);
+            } else if (sParams[3].equals(key)) {
+                mCenterY = Float.parseFloat(value);
+            } else if (sParams[4].equals(key)) {
+                mRadiusX = Float.parseFloat(value);
+            } else if (sParams[5].equals(key)) {
+                mRadiusY = Float.parseFloat(value);
+            }
+        }
+    }
 }
diff --git a/src/com/android/gallery3d/filtershow/filters/FiltersManagerInterface.java b/src/com/android/gallery3d/filtershow/filters/FiltersManagerInterface.java
new file mode 100644
index 0000000..710128f
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FiltersManagerInterface.java
@@ -0,0 +1,21 @@
+/*
+ * 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.filters;
+
+public interface FiltersManagerInterface {
+   ImageFilter getFilterForRepresentation(FilterRepresentation representation);
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilter.java b/src/com/android/gallery3d/filtershow/filters/ImageFilter.java
index 96ab84f..b80fc7f 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilter.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilter.java
@@ -16,12 +16,12 @@
 
 package com.android.gallery3d.filtershow.filters;
 
+import android.app.Activity;
 import android.graphics.Bitmap;
 import android.graphics.Matrix;
 import android.support.v8.renderscript.Allocation;
 import android.widget.Toast;
 
-import com.android.gallery3d.filtershow.FilterShowActivity;
 import com.android.gallery3d.filtershow.imageshow.GeometryMetadata;
 import com.android.gallery3d.filtershow.presets.FilterEnvironment;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
@@ -35,9 +35,9 @@
     // TODO: Temporary, for dogfood note memory issues with toasts for better
     // feedback. Remove this when filters actually work in low memory
     // situations.
-    private static FilterShowActivity sActivity = null;
+    private static Activity sActivity = null;
 
-    public static void setActivityForMemoryToasts(FilterShowActivity activity) {
+    public static void setActivityForMemoryToasts(Activity activity) {
         sActivity = activity;
     }
 
@@ -76,10 +76,6 @@
         return bitmap;
     }
 
-    public ImagePreset getImagePreset() {
-        return getEnvironment().getImagePreset();
-    }
-
     public abstract void useRepresentation(FilterRepresentation representation);
 
     native protected void nativeApplyGradientFilter(Bitmap bitmap, int w, int h,
@@ -90,10 +86,11 @@
     }
 
     protected Matrix getOriginalToScreenMatrix(int w, int h) {
-        GeometryMetadata geo = getImagePreset().mGeoData;
+        ImagePreset preset = getEnvironment().getImagePreset();
+        GeometryMetadata geo = getEnvironment().getImagePreset().mGeoData;
         Matrix originalToScreen = geo.getOriginalToScreen(true,
-                getImagePreset().getImageLoader().getOriginalBounds().width(),
-                getImagePreset().getImageLoader().getOriginalBounds().height(),
+                preset.getImageLoader().getOriginalBounds().width(),
+                preset.getImageLoader().getOriginalBounds().height(),
                 w, h);
         return originalToScreen;
     }
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java
index a4626cd..64c48df 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java
@@ -23,6 +23,7 @@
 
 
 public class ImageFilterBwFilter extends SimpleImageFilter {
+    private static final String SERIALIZATION_NAME = "BWFILTER";
 
     public ImageFilterBwFilter() {
         mName = "BW Filter";
@@ -31,6 +32,8 @@
     public FilterRepresentation getDefaultRepresentation() {
         FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation();
         representation.setName("BW Filter");
+        representation.setSerializationName(SERIALIZATION_NAME);
+
         representation.setFilterClass(ImageFilterBwFilter.class);
         representation.setMaximum(180);
         representation.setMinimum(-180);
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java
index 2097f0d..c8b41c2 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java
@@ -21,6 +21,7 @@
 import android.graphics.Bitmap;
 
 public class ImageFilterContrast extends SimpleImageFilter {
+    private static final String SERIALIZATION_NAME = "CONTRAST";
 
     public ImageFilterContrast() {
         mName = "Contrast";
@@ -30,6 +31,8 @@
         FilterBasicRepresentation representation =
                 (FilterBasicRepresentation) super.getDefaultRepresentation();
         representation.setName("Contrast");
+        representation.setSerializationName(SERIALIZATION_NAME);
+
         representation.setFilterClass(ImageFilterContrast.class);
         representation.setTextId(R.string.contrast);
         representation.setButtonId(R.id.contrastButton);
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterDownsample.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterDownsample.java
index 0b02fc4..ea2ff35 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterDownsample.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterDownsample.java
@@ -24,6 +24,7 @@
 import com.android.gallery3d.filtershow.cache.ImageLoader;
 
 public class ImageFilterDownsample extends SimpleImageFilter {
+    private static final String SERIALIZATION_NAME = "DOWNSAMPLE";
     private static final int ICON_DOWNSAMPLE_FRACTION = 8;
     private ImageLoader mImageLoader;
 
@@ -35,6 +36,8 @@
     public FilterRepresentation getDefaultRepresentation() {
         FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation();
         representation.setName("Downsample");
+        representation.setSerializationName(SERIALIZATION_NAME);
+
         representation.setFilterClass(ImageFilterDownsample.class);
         representation.setMaximum(100);
         representation.setMinimum(1);
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java
index 1fd9071..812ab02 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java
@@ -31,6 +31,7 @@
 import com.android.gallery3d.R;
 import com.android.gallery3d.filtershow.filters.FilterDrawRepresentation.StrokeData;
 import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.presets.FilterEnvironment;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
 
 import java.util.Vector;
@@ -204,7 +205,7 @@
 
     public void drawData(Canvas canvas, Matrix originalRotateToScreen, int quality) {
         Paint paint = new Paint();
-        if (quality == ImagePreset.QUALITY_FINAL) {
+        if (quality == FilterEnvironment.QUALITY_FINAL) {
             paint.setAntiAlias(true);
         }
         paint.setStyle(Style.STROKE);
@@ -214,7 +215,7 @@
         if (mParameters.getDrawing().isEmpty() && mParameters.getCurrentDrawing() == null) {
             return;
         }
-        if (quality == ImagePreset.QUALITY_FINAL) {
+        if (quality == FilterEnvironment.QUALITY_FINAL) {
             for (FilterDrawRepresentation.StrokeData strokeData : mParameters.getDrawing()) {
                 paint(strokeData, canvas, originalRotateToScreen, quality);
             }
@@ -248,17 +249,17 @@
         int n = v.size();
 
         for (int i = mCachedStrokes; i < n; i++) {
-            paint(v.get(i), drawCache, originalRotateToScreen, ImagePreset.QUALITY_PREVIEW);
+            paint(v.get(i), drawCache, originalRotateToScreen, FilterEnvironment.QUALITY_PREVIEW);
         }
         mCachedStrokes = n;
     }
 
     public void draw(Canvas canvas, Matrix originalRotateToScreen) {
         for (FilterDrawRepresentation.StrokeData strokeData : mParameters.getDrawing()) {
-            paint(strokeData, canvas, originalRotateToScreen, ImagePreset.QUALITY_PREVIEW);
+            paint(strokeData, canvas, originalRotateToScreen, FilterEnvironment.QUALITY_PREVIEW);
         }
         mDrawingsTypes[mCurrentStyle].paint(
-                null, canvas, originalRotateToScreen, ImagePreset.QUALITY_PREVIEW);
+                null, canvas, originalRotateToScreen, FilterEnvironment.QUALITY_PREVIEW);
     }
 
     @Override
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java
index 46a9a29..82de2b7 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java
@@ -21,7 +21,7 @@
 import com.android.gallery3d.R;
 
 public class ImageFilterEdge extends SimpleImageFilter {
-
+    private static final String SERIALIZATION_NAME = "EDGE";
     public ImageFilterEdge() {
         mName = "Edge";
     }
@@ -29,6 +29,7 @@
     public FilterRepresentation getDefaultRepresentation() {
         FilterRepresentation representation = super.getDefaultRepresentation();
         representation.setName("Edge");
+        representation.setSerializationName(SERIALIZATION_NAME);
         representation.setFilterClass(ImageFilterEdge.class);
         representation.setTextId(R.string.edge);
         representation.setButtonId(R.id.edgeButton);
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java
index b0b0b2d..6fdcd24 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java
@@ -21,7 +21,7 @@
 import android.graphics.Bitmap;
 
 public class ImageFilterExposure extends SimpleImageFilter {
-
+    private static final String SERIALIZATION_NAME = "EXPOSURE";
     public ImageFilterExposure() {
         mName = "Exposure";
     }
@@ -30,6 +30,7 @@
         FilterBasicRepresentation representation =
                 (FilterBasicRepresentation) super.getDefaultRepresentation();
         representation.setName("Exposure");
+        representation.setSerializationName(SERIALIZATION_NAME);
         representation.setFilterClass(ImageFilterExposure.class);
         representation.setTextId(R.string.exposure);
         representation.setButtonId(R.id.exposureButton);
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java
index 68e8a7c..51c6612 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java
@@ -37,6 +37,11 @@
         mFxBitmap = null;
     }
 
+    @Override
+    public FilterRepresentation getDefaultRepresentation() {
+        return null;
+    }
+
     public void useRepresentation(FilterRepresentation representation) {
         FilterFxRepresentation parameters = (FilterFxRepresentation) representation;
         mParameters = parameters;
@@ -87,4 +92,5 @@
     public void setResources(Resources resources) {
         mResources = resources;
     }
+
 }
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java
index 0022a9e..0725dd1 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java
@@ -21,6 +21,7 @@
 import com.android.gallery3d.R;
 
 public class ImageFilterHighlights extends SimpleImageFilter {
+    private static final String SERIALIZATION_NAME = "HIGHLIGHTS";
     private static final String LOGTAG = "ImageFilterVignette";
 
     public ImageFilterHighlights() {
@@ -33,7 +34,8 @@
     public FilterRepresentation getDefaultRepresentation() {
         FilterBasicRepresentation representation =
                 (FilterBasicRepresentation) super.getDefaultRepresentation();
-        representation.setName("Shadows");
+        representation.setName("Highlights");
+        representation.setSerializationName(SERIALIZATION_NAME);
         representation.setFilterClass(ImageFilterHighlights.class);
         representation.setTextId(R.string.highlight_recovery);
         representation.setButtonId(R.id.highlightRecoveryButton);
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java
index b1f9f73..7e6f685 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java
@@ -22,6 +22,7 @@
 import android.graphics.Bitmap;
 
 public class ImageFilterHue extends SimpleImageFilter {
+    private static final String SERIALIZATION_NAME = "HUE";
     private ColorSpaceMatrix cmatrix = null;
 
     public ImageFilterHue() {
@@ -33,6 +34,7 @@
         FilterBasicRepresentation representation =
                 (FilterBasicRepresentation) super.getDefaultRepresentation();
         representation.setName("Hue");
+        representation.setSerializationName(SERIALIZATION_NAME);
         representation.setFilterClass(ImageFilterHue.class);
         representation.setMinimum(-180);
         representation.setMaximum(180);
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java
index 29e6d16..9381381 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java
@@ -22,6 +22,7 @@
 import com.android.gallery3d.R;
 
 public class ImageFilterKMeans extends SimpleImageFilter {
+    private static final String SERIALIZATION_NAME = "KMEANS";
     private int mSeed = 0;
 
     public ImageFilterKMeans() {
@@ -36,6 +37,7 @@
     public FilterRepresentation getDefaultRepresentation() {
         FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation();
         representation.setName("KMeans");
+        representation.setSerializationName(SERIALIZATION_NAME);
         representation.setFilterClass(ImageFilterKMeans.class);
         representation.setMaximum(20);
         representation.setMinimum(2);
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterNegative.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterNegative.java
index c256686..0747190 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterNegative.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterNegative.java
@@ -6,13 +6,14 @@
 import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
 
 public class ImageFilterNegative extends ImageFilter {
-
+    private static final String SERIALIZATION_NAME = "NEGATIVE";
     public ImageFilterNegative() {
         mName = "Negative";
     }
 
     public FilterRepresentation getDefaultRepresentation() {
         FilterRepresentation representation = new FilterDirectRepresentation("Negative");
+        representation.setSerializationName(SERIALIZATION_NAME);
         representation.setFilterClass(ImageFilterNegative.class);
         representation.setTextId(R.string.negative);
         representation.setButtonId(R.id.negativeButton);
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java
index cfbb560..69d18f8 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java
@@ -22,13 +22,14 @@
 import android.util.Log;
 import android.content.res.Resources;
 import com.android.gallery3d.R;
-import com.android.gallery3d.filtershow.cache.CachingPipeline;
+import com.android.gallery3d.filtershow.presets.PipelineInterface;
 
 public abstract class ImageFilterRS extends ImageFilter {
     private static final String LOGTAG = "ImageFilterRS";
     private boolean DEBUG = false;
     private int mLastInputWidth = 0;
     private int mLastInputHeight = 0;
+    private long mLastTimeCalled;
 
     public static boolean PERF_LOGGING = false;
 
@@ -51,26 +52,36 @@
     }
 
     protected RenderScript getRenderScriptContext() {
-        return CachingPipeline.getRenderScriptContext();
+        PipelineInterface pipeline = getEnvironment().getPipeline();
+        return pipeline.getRSContext();
     }
 
     protected Allocation getInPixelsAllocation() {
-        CachingPipeline pipeline = getEnvironment().getCachingPipeline();
+        PipelineInterface pipeline = getEnvironment().getPipeline();
         return pipeline.getInPixelsAllocation();
     }
 
     protected Allocation getOutPixelsAllocation() {
-        CachingPipeline pipeline = getEnvironment().getCachingPipeline();
+        PipelineInterface pipeline = getEnvironment().getPipeline();
         return pipeline.getOutPixelsAllocation();
     }
 
     @Override
     public void apply(Allocation in, Allocation out) {
         long startOverAll = System.nanoTime();
+        if (PERF_LOGGING) {
+            long delay = (startOverAll - mLastTimeCalled) / 1000;
+            String msg = String.format("%s; image size %dx%d; ", getName(),
+                    in.getType().getX(), in.getType().getY());
+            msg += String.format("called after %.2f ms (%.2f FPS); ",
+                    delay / 1000.f, 1000000.f / delay);
+            Log.i(LOGTAG, msg);
+        }
+        mLastTimeCalled = startOverAll;
         long startFilter = 0;
         long endFilter = 0;
         if (!mResourcesLoaded) {
-            CachingPipeline pipeline = getEnvironment().getCachingPipeline();
+            PipelineInterface pipeline = getEnvironment().getPipeline();
             createFilter(pipeline.getResources(), getEnvironment().getScaleFactor(),
                     getEnvironment().getQuality(), in);
             mResourcesLoaded = true;
@@ -102,7 +113,7 @@
             return bitmap;
         }
         try {
-            CachingPipeline pipeline = getEnvironment().getCachingPipeline();
+            PipelineInterface pipeline = getEnvironment().getPipeline();
             if (DEBUG) {
                 Log.v(LOGTAG, "apply filter " + getName() + " in pipeline " + pipeline.getName());
             }
@@ -137,18 +148,16 @@
             displayLowMemoryToast();
             Log.e(LOGTAG, "not enough memory for filter " + getName(), e);
         }
-
         return bitmap;
     }
 
-    protected static Allocation convertBitmap(Bitmap bitmap) {
-        return Allocation.createFromBitmap(CachingPipeline.getRenderScriptContext(), bitmap,
+    protected static Allocation convertBitmap(RenderScript RS, Bitmap bitmap) {
+        return Allocation.createFromBitmap(RS, bitmap,
                 Allocation.MipmapControl.MIPMAP_NONE,
                 Allocation.USAGE_SCRIPT | Allocation.USAGE_GRAPHICS_TEXTURE);
     }
 
-    private static Allocation convertRGBAtoA(Bitmap bitmap) {
-        RenderScript RS = CachingPipeline.getRenderScriptContext();
+    private static Allocation convertRGBAtoA(RenderScript RS, Bitmap bitmap) {
         if (RS != mRScache || mGreyConvert == null) {
             mGreyConvert = new ScriptC_grey(RS, RS.getApplicationContext().getResources(),
                                             R.raw.grey);
@@ -157,7 +166,7 @@
 
         Type.Builder tb_a8 = new Type.Builder(RS, Element.A_8(RS));
 
-        Allocation bitmapTemp = convertBitmap(bitmap);
+        Allocation bitmapTemp = convertBitmap(RS, bitmap);
         if (bitmapTemp.getType().getElement().isCompatible(Element.A_8(RS))) {
             return bitmapTemp;
         }
@@ -173,20 +182,20 @@
     }
 
     public Allocation loadScaledResourceAlpha(int resource, int inSampleSize) {
-        Resources res = CachingPipeline.getResources();
+        Resources res = getEnvironment().getPipeline().getResources();
         final BitmapFactory.Options options = new BitmapFactory.Options();
         options.inPreferredConfig = Bitmap.Config.ALPHA_8;
         options.inSampleSize      = inSampleSize;
         Bitmap bitmap = BitmapFactory.decodeResource(
                 res,
                 resource, options);
-        Allocation ret = convertRGBAtoA(bitmap);
+        Allocation ret = convertRGBAtoA(getRenderScriptContext(), bitmap);
         bitmap.recycle();
         return ret;
     }
 
     public Allocation loadScaledResourceAlpha(int resource, int w, int h, int inSampleSize) {
-        Resources res = CachingPipeline.getResources();
+        Resources res = getEnvironment().getPipeline().getResources();
         final BitmapFactory.Options options = new BitmapFactory.Options();
         options.inPreferredConfig = Bitmap.Config.ALPHA_8;
         options.inSampleSize      = inSampleSize;
@@ -194,7 +203,7 @@
                 res,
                 resource, options);
         Bitmap resizeBitmap = Bitmap.createScaledBitmap(bitmap, w, h, true);
-        Allocation ret = convertRGBAtoA(resizeBitmap);
+        Allocation ret = convertRGBAtoA(getRenderScriptContext(), resizeBitmap);
         resizeBitmap.recycle();
         bitmap.recycle();
         return ret;
@@ -205,13 +214,13 @@
     }
 
     public Allocation loadResource(int resource) {
-        Resources res = CachingPipeline.getResources();
+        Resources res = getEnvironment().getPipeline().getResources();
         final BitmapFactory.Options options = new BitmapFactory.Options();
         options.inPreferredConfig = Bitmap.Config.ARGB_8888;
         Bitmap bitmap = BitmapFactory.decodeResource(
                 res,
                 resource, options);
-        Allocation ret = convertBitmap(bitmap);
+        Allocation ret = convertBitmap(getRenderScriptContext(), bitmap);
         bitmap.recycle();
         return ret;
     }
@@ -232,7 +241,7 @@
     /**
      * RS Script objects (and all other RS objects) should be cleared here
      */
-    abstract protected void resetScripts();
+    public abstract void resetScripts();
 
     /**
      * Scripts values should be bound here
@@ -244,6 +253,8 @@
             return;
         }
         resetAllocations();
+        mLastInputWidth = 0;
+        mLastInputHeight = 0;
         setResourcesLoaded(false);
     }
 }
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java
index 0febe49..adc74c5 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java
@@ -21,7 +21,7 @@
 import android.graphics.Bitmap;
 
 public class ImageFilterSaturated extends SimpleImageFilter {
-
+    private static final String SERIALIZATION_NAME = "SATURATED";
     public ImageFilterSaturated() {
         mName = "Saturated";
     }
@@ -31,6 +31,7 @@
         FilterBasicRepresentation representation =
                 (FilterBasicRepresentation) super.getDefaultRepresentation();
         representation.setName("Saturated");
+        representation.setSerializationName(SERIALIZATION_NAME);
         representation.setFilterClass(ImageFilterSaturated.class);
         representation.setTextId(R.string.saturation);
         representation.setButtonId(R.id.saturationButton);
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java
index fd67ee8..845290b 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java
@@ -21,7 +21,7 @@
 import android.graphics.Bitmap;
 
 public class ImageFilterShadows extends SimpleImageFilter {
-
+    private static final String SERIALIZATION_NAME = "SHADOWS";
     public ImageFilterShadows() {
         mName = "Shadows";
 
@@ -31,6 +31,7 @@
         FilterBasicRepresentation representation =
                 (FilterBasicRepresentation) super.getDefaultRepresentation();
         representation.setName("Shadows");
+        representation.setSerializationName(SERIALIZATION_NAME);
         representation.setFilterClass(ImageFilterShadows.class);
         representation.setTextId(R.string.shadow_recovery);
         representation.setButtonId(R.id.shadowRecoveryButton);
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java
index 76ae475..1dc2c05 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java
@@ -19,7 +19,7 @@
 import com.android.gallery3d.R;
 
 public class ImageFilterSharpen extends ImageFilterRS {
-
+    private static final String SERIALIZATION_NAME = "SHARPEN";
     private static final String LOGTAG = "ImageFilterSharpen";
     private ScriptC_convolve3x3 mScript;
 
@@ -31,6 +31,7 @@
 
     public FilterRepresentation getDefaultRepresentation() {
         FilterRepresentation representation = new FilterBasicRepresentation("Sharpen", 0, 0, 100);
+        representation.setSerializationName(SERIALIZATION_NAME);
         representation.setShowParameterValue(true);
         representation.setFilterClass(ImageFilterSharpen.class);
         representation.setTextId(R.string.sharpness);
@@ -52,7 +53,7 @@
     }
 
     @Override
-    protected void resetScripts() {
+    public void resetScripts() {
         if (mScript != null) {
             mScript.destroy();
             mScript = null;
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java
index 37d5739..f265c4d 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java
@@ -76,7 +76,7 @@
         int w = bitmapIn.getWidth();
         int h = bitmapIn.getHeight();
         int outputSize = (int) (w / 2f);
-        ImagePreset preset = getImagePreset();
+        ImagePreset preset = getEnvironment().getImagePreset();
         Bitmap mBitmapOut = null;
         if (preset != null) {
             XMPMeta xmp = preset.getImageLoader().getXmpObject();
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java
index ea315d3..900fd90 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java
@@ -21,7 +21,7 @@
 import android.graphics.Bitmap;
 
 public class ImageFilterVibrance extends SimpleImageFilter {
-
+    private static final String SERIALIZATION_NAME = "VIBRANCE";
     public ImageFilterVibrance() {
         mName = "Vibrance";
     }
@@ -30,6 +30,7 @@
         FilterBasicRepresentation representation =
                 (FilterBasicRepresentation) super.getDefaultRepresentation();
         representation.setName("Vibrance");
+        representation.setSerializationName(SERIALIZATION_NAME);
         representation.setFilterClass(ImageFilterVibrance.class);
         representation.setTextId(R.string.vibrance);
         representation.setButtonId(R.id.vibranceButton);
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java
index e06f544..cfe1350 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java
@@ -22,7 +22,7 @@
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import com.android.gallery3d.R;
-import com.android.gallery3d.filtershow.presets.ImagePreset;
+import com.android.gallery3d.filtershow.presets.FilterEnvironment;
 
 public class ImageFilterVignette extends SimpleImageFilter {
     private static final String LOGTAG = "ImageFilterVignette";
@@ -57,9 +57,9 @@
 
     @Override
     public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
-        if (SIMPLE_ICONS && ImagePreset.QUALITY_ICON == quality) {
+        if (SIMPLE_ICONS && FilterEnvironment.QUALITY_ICON == quality) {
             if (mOverlayBitmap == null) {
-                Resources res = getEnvironment().getCachingPipeline().getResources();
+                Resources res = getEnvironment().getPipeline().getResources();
                 mOverlayBitmap = IconUtilities.getFXBitmap(res,
                         R.drawable.filtershow_icon_vignette);
             }
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java
index c4c293a..84a14c9 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java
@@ -22,6 +22,7 @@
 import android.graphics.Bitmap;
 
 public class ImageFilterWBalance extends ImageFilter {
+    private static final String SERIALIZATION_NAME = "WBALANCE";
     private static final String TAG = "ImageFilterWBalance";
 
     public ImageFilterWBalance() {
@@ -30,6 +31,7 @@
 
     public FilterRepresentation getDefaultRepresentation() {
         FilterRepresentation representation = new FilterDirectRepresentation("WBalance");
+        representation.setSerializationName(SERIALIZATION_NAME);
         representation.setFilterClass(ImageFilterWBalance.class);
         representation.setPriority(FilterRepresentation.TYPE_WBALANCE);
         representation.setTextId(R.string.wbalance);
diff --git a/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java b/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java
index e5820a8..77dbd5e 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java
@@ -20,6 +20,7 @@
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.util.Log;
 
 import com.android.gallery3d.filtershow.cache.ImageLoader;
 import com.android.gallery3d.filtershow.crop.CropExtras;
@@ -30,7 +31,14 @@
 import com.android.gallery3d.filtershow.filters.FilterRepresentation;
 import com.android.gallery3d.filtershow.filters.ImageFilterGeometry;
 
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+
 public class GeometryMetadata extends FilterRepresentation {
+    private static final String SERIALIZATION_NAME = "GEOM";
     private static final String LOGTAG = "GeometryMetadata";
     private float mScaleFactor = 1.0f;
     private float mRotation = 0;
@@ -40,7 +48,26 @@
     private FLIP mFlip = FLIP.NONE;
 
     public enum FLIP {
-        NONE, VERTICAL, HORIZONTAL, BOTH
+        NONE("N"), VERTICAL("V"), HORIZONTAL("H"), BOTH("B");
+        String mValue;
+
+        FLIP(String name) {
+            mValue = name;
+        }
+
+        public static FLIP parse(String name){
+            switch (name.charAt(0)) {
+                case 'N':
+                    return NONE;
+                case 'V':
+                    return VERTICAL;
+                case 'H':
+                    return HORIZONTAL;
+                case 'B':
+                    return BOTH;
+            };
+            return NONE;
+        }
     }
 
     // Output format data from intent extras
@@ -64,6 +91,7 @@
 
     public GeometryMetadata() {
         super("GeometryMetadata");
+        setSerializationName(SERIALIZATION_NAME);
         setFilterClass(ImageFilterGeometry.class);
         setEditorId(EditorCrop.ID);
         setTextId(0);
@@ -492,4 +520,87 @@
         representation.useParametersFrom(this);
         return representation;
     }
+
+    private static final String[] sParams = {
+            "Name", "ScaleFactor", "Rotation", "StraightenRotation", "CropBoundsLeft",
+            "CropBoundsTop", "CropBoundsRight", "CropBoundsBottom", "PhotoBoundsLeft",
+            "PhotoBoundsTop", "PhotoBoundsRight", "PhotoBoundsBottom", "Flip"
+    };
+
+    @Override
+    public String[][] serializeRepresentation() {
+        String[][] ret = {
+                { "Name", getName() },
+                { "ScaleFactor", Float.toString(mScaleFactor) },
+                { "Rotation", Float.toString(mRotation) },
+                { "StraightenRotation", Float.toString(mStraightenRotation) },
+                { "CropBoundsLeft", Float.toString(mCropBounds.left) },
+                { "CropBoundsTop", Float.toString(mCropBounds.top) },
+                { "CropBoundsRight", Float.toString(mCropBounds.right) },
+                { "CropBoundsBottom", Float.toString(mCropBounds.bottom) },
+                { "PhotoBoundsLeft", Float.toString(mPhotoBounds.left) },
+                { "PhotoBoundsTop", Float.toString(mPhotoBounds.top) },
+                { "PhotoBoundsRight", Float.toString(mPhotoBounds.right) },
+                { "PhotoBoundsBottom", Float.toString(mPhotoBounds.bottom) },
+                { "Flip", mFlip.mValue } };
+        return ret;
+    }
+
+    @Override
+    public void deSerializeRepresentation(String[][] rep) {
+        HashMap<String, Integer> map = new HashMap<String, Integer>();
+        for (int i = 0; i < sParams.length; i++) {
+            map.put(sParams[i], i);
+        }
+        for (int i = 0; i < rep.length; i++) {
+            String key = rep[i][0];
+            String value = rep[i][1];
+
+            switch (map.get(key)) {
+                case -1: // Unknown
+                    break;
+                case 0:
+                    if (!getName().equals(value)) {
+                        throw new IllegalArgumentException("Not a "+getName());
+                    }
+                    break;
+                case 1: // "ScaleFactor", Float
+                    mScaleFactor = Float.parseFloat(value);
+                    break;
+                case 2: // "Rotation", Float
+                    mRotation = Float.parseFloat(value);
+                    break;
+                case 3: // "StraightenRotation", Float
+                    mStraightenRotation = Float.parseFloat(value);
+                    break;
+                case 4: // "mCropBoundsLeft", Float
+                    mCropBounds.left = Float.parseFloat(value);
+                    break;
+                case 5: // "mCropBoundsTop", Float
+                    mCropBounds.top = Float.parseFloat(value);
+                    break;
+                case 6: // "mCropBoundsRight", Float
+                    mCropBounds.right = Float.parseFloat(value);
+                    break;
+                case 7: // "mCropBoundsBottom", Float
+                    mCropBounds.bottom = Float.parseFloat(value);
+                    break;
+                case 8: // "mPhotoBoundsLeft", Float
+                    mPhotoBounds.left = Float.parseFloat(value);
+                    break;
+                case 9: // "mPhotoBoundsTop", Float
+                    mPhotoBounds.top = Float.parseFloat(value);
+                    break;
+                case 10: // "mPhotoBoundsRight", Float
+                    mPhotoBounds.right = Float.parseFloat(value);
+                    break;
+                case 11: // "mPhotoBoundsBottom", Float
+                    mPhotoBounds.bottom = Float.parseFloat(value);
+                    break;
+                case 12: // "Flip", enum
+                    mFlip = FLIP.parse(value);
+                    break;
+            }
+        }
+    }
 }
diff --git a/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java
index a0b59c0..ab8567f 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java
@@ -20,6 +20,7 @@
 import android.os.Handler;
 import android.os.Message;
 
+import android.util.Log;
 import com.android.gallery3d.filtershow.FilterShowActivity;
 import com.android.gallery3d.filtershow.HistoryAdapter;
 import com.android.gallery3d.filtershow.cache.*;
@@ -139,6 +140,7 @@
     }
 
     public synchronized void setPreset(ImagePreset preset, boolean addToHistory) {
+        preset.showFilters();
         mPreset = preset;
         mPreset.setImageLoader(mLoader);
         setGeometry();
diff --git a/src/com/android/gallery3d/filtershow/presets/FilterEnvironment.java b/src/com/android/gallery3d/filtershow/presets/FilterEnvironment.java
index c454c1a..47f8dfc 100644
--- a/src/com/android/gallery3d/filtershow/presets/FilterEnvironment.java
+++ b/src/com/android/gallery3d/filtershow/presets/FilterEnvironment.java
@@ -18,11 +18,9 @@
 
 import android.graphics.Bitmap;
 import android.support.v8.renderscript.Allocation;
-import android.util.Log;
 
-import com.android.gallery3d.filtershow.cache.CachingPipeline;
 import com.android.gallery3d.filtershow.filters.FilterRepresentation;
-import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.filters.FiltersManagerInterface;
 import com.android.gallery3d.filtershow.filters.ImageFilter;
 
 import java.lang.ref.WeakReference;
@@ -33,10 +31,14 @@
     private ImagePreset mImagePreset;
     private float mScaleFactor;
     private int mQuality;
-    private FiltersManager mFiltersManager;
-    private CachingPipeline mCachingPipeline;
+    private FiltersManagerInterface mFiltersManager;
+    private PipelineInterface mPipeline;
     private volatile boolean mStop = false;
 
+    public static final int QUALITY_ICON = 0;
+    public static final int QUALITY_PREVIEW = 1;
+    public static final int QUALITY_FINAL = 2;
+
     public synchronized boolean needsStop() {
         return mStop;
     }
@@ -98,11 +100,11 @@
         return mQuality;
     }
 
-    public void setFiltersManager(FiltersManager filtersManager) {
+    public void setFiltersManager(FiltersManagerInterface filtersManager) {
         mFiltersManager = filtersManager;
     }
 
-    public FiltersManager getFiltersManager() {
+    public FiltersManagerInterface getFiltersManager() {
         return mFiltersManager;
     }
 
@@ -126,12 +128,12 @@
         return ret;
     }
 
-    public CachingPipeline getCachingPipeline() {
-        return mCachingPipeline;
+    public PipelineInterface getPipeline() {
+        return mPipeline;
     }
 
-    public void setCachingPipeline(CachingPipeline cachingPipeline) {
-        mCachingPipeline = cachingPipeline;
+    public void setPipeline(PipelineInterface cachingPipeline) {
+        mPipeline = cachingPipeline;
     }
 
 }
diff --git a/src/com/android/gallery3d/filtershow/presets/ImagePreset.java b/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
index 2a7e601..8476695 100644
--- a/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
+++ b/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
@@ -18,12 +18,19 @@
 
 import android.graphics.Bitmap;
 import android.graphics.Rect;
+import android.net.Uri;
 import android.support.v8.renderscript.Allocation;
+import android.util.JsonReader;
+import android.util.JsonWriter;
 import android.util.Log;
 
+import com.adobe.xmp.XMPException;
+import com.adobe.xmp.XMPMeta;
+import com.adobe.xmp.options.PropertyOptions;
 import com.android.gallery3d.filtershow.cache.CachingPipeline;
 import com.android.gallery3d.filtershow.cache.ImageLoader;
 import com.android.gallery3d.filtershow.filters.BaseFiltersManager;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
 import com.android.gallery3d.filtershow.filters.FilterRepresentation;
 import com.android.gallery3d.filtershow.filters.ImageFilter;
 import com.android.gallery3d.filtershow.imageshow.GeometryMetadata;
@@ -32,6 +39,10 @@
 import com.android.gallery3d.filtershow.state.StateAdapter;
 import com.android.gallery3d.util.UsageStatistics;
 
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
 import java.util.Vector;
 
 public class ImagePreset {
@@ -39,10 +50,8 @@
     private static final String LOGTAG = "ImagePreset";
 
     private FilterRepresentation mBorder = null;
-    public static final int QUALITY_ICON = 0;
-    public static final int QUALITY_PREVIEW = 1;
-    public static final int QUALITY_FINAL = 2;
     public static final int STYLE_ICON = 3;
+    public static final String PRESET_NAME = "PresetName";
 
     private ImageLoader mImageLoader = null;
 
@@ -210,11 +219,11 @@
         }
         for (FilterRepresentation representation : mFilters) {
             if (representation.getPriority() == FilterRepresentation.TYPE_VIGNETTE
-                && !representation.isNil()) {
+                    && !representation.isNil()) {
                 return false;
             }
             if (representation.getPriority() == FilterRepresentation.TYPE_TINYPLANET
-                && !representation.isNil()) {
+                    && !representation.isNil()) {
                 return false;
             }
         }
@@ -460,7 +469,7 @@
         if (mBorder != null && mDoApplyGeometry) {
             mBorder.synchronizeRepresentation();
             bitmap = environment.applyRepresentation(mBorder, bitmap);
-            if (environment.getQuality() == QUALITY_FINAL) {
+            if (environment.getQuality() == FilterEnvironment.QUALITY_FINAL) {
                 UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
                         "SaveBorder", mBorder.getName(), 1);
             }
@@ -468,6 +477,10 @@
         return bitmap;
     }
 
+    public int nbFilters() {
+        return mFilters.size();
+    }
+
     public Bitmap applyFilters(Bitmap bitmap, int from, int to, FilterEnvironment environment) {
         if (mDoApplyFilters) {
             if (from < 0) {
@@ -483,7 +496,7 @@
                     representation.synchronizeRepresentation();
                 }
                 bitmap = environment.applyRepresentation(representation, bitmap);
-                if (environment.getQuality() == QUALITY_FINAL) {
+                if (environment.getQuality() == FilterEnvironment.QUALITY_FINAL) {
                     UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
                             "SaveFilter", representation.getName(), 1);
                 }
@@ -496,17 +509,23 @@
         return bitmap;
     }
 
-    public void applyBorder(Allocation in, Allocation out, FilterEnvironment environment) {
+    public void applyBorder(Allocation in, Allocation out,
+                            boolean copyOut, FilterEnvironment environment) {
         if (mBorder != null && mDoApplyGeometry) {
             mBorder.synchronizeRepresentation();
             // TODO: should keep the bitmap around
-            Allocation bitmapIn = Allocation.createTyped(CachingPipeline.getRenderScriptContext(), in.getType());
-            bitmapIn.copyFrom(out);
+            Allocation bitmapIn = in;
+            if (copyOut) {
+                bitmapIn = Allocation.createTyped(
+                        CachingPipeline.getRenderScriptContext(), in.getType());
+                bitmapIn.copyFrom(out);
+            }
             environment.applyRepresentation(mBorder, bitmapIn, out);
         }
     }
 
-    public void applyFilters(int from, int to, Allocation in, Allocation out, FilterEnvironment environment) {
+    public void applyFilters(int from, int to, Allocation in, Allocation out,
+                             FilterEnvironment environment) {
         if (mDoApplyFilters) {
             if (from < 0) {
                 from = 0;
@@ -605,4 +624,109 @@
         return usedFilters;
     }
 
+    public String getJsonString(String name) {
+        StringWriter swriter = new StringWriter();
+        try {
+            JsonWriter writer = new JsonWriter(swriter);
+            writeJson(writer, name);
+            writer.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return swriter.toString();
+    }
+
+    public void writeJson(JsonWriter writer, String name) {
+        int numFilters =  mFilters.size();
+        try {
+            writer.beginObject();
+            writer.name(PRESET_NAME).value(name);
+            writer.name(mGeoData.getSerializationName());
+            writer.beginObject();
+            {
+                String[][] rep = mGeoData.serializeRepresentation();
+                for (int i = 0; i < rep.length; i++) {
+                    writer.name(rep[i][0]);
+                    writer.value(rep[i][1]);
+                }
+            }
+            writer.endObject();
+
+            for (int i = 0; i < numFilters; i++) {
+                FilterRepresentation filter = mFilters.get(i);
+                String sname = filter.getSerializationName();
+                writer.name(sname);
+                writer.beginObject();
+                {
+                    String[][] rep = filter.serializeRepresentation();
+                    for (int k = 0; k < rep.length; k++) {
+                        writer.name(rep[k][0]);
+                        writer.value(rep[k][1]);
+                    }
+                }
+                writer.endObject();
+            }
+            writer.endObject();
+
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public boolean readJsonFromString(String filterString) {
+        StringReader sreader = new StringReader(filterString);
+        try {
+            JsonReader reader = new JsonReader(sreader);
+            boolean ok = readJson(reader);
+            if (!ok) {
+                return false;
+            }
+            reader.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return true;
+    }
+
+    public boolean readJson(JsonReader sreader) throws IOException {
+        sreader.beginObject();
+        sreader.nextName();
+        mName = sreader.nextString();
+
+        while (sreader.hasNext()) {
+            String name = sreader.nextName();
+
+            if (mGeoData.getSerializationName().equals(name)) {
+                mGeoData.deSerializeRepresentation(read(sreader));
+            } else {
+                FilterRepresentation filter = creatFilterFromName(name);
+                if (filter == null)
+                    return false;
+                filter.deSerializeRepresentation(read(sreader));
+                addFilter(filter);
+            }
+        }
+        sreader.endObject();
+        return true;
+    }
+
+    FilterRepresentation creatFilterFromName(String name) {
+        FiltersManager filtersManager = FiltersManager.getManager();
+        return filtersManager.createFilterFromName(name);
+    }
+
+    String[][] read(JsonReader reader) throws IOException {
+        ArrayList <String[]> al = new ArrayList<String[]>();
+
+        reader.beginObject();
+
+        while (reader.hasNext()) {
+            String[]kv = { reader.nextName(),reader.nextString()};
+            al.add(kv);
+
+        }
+        reader.endObject();
+        return al.toArray(new String[al.size()][]);
+    }
 }
diff --git a/src/com/android/gallery3d/filtershow/presets/PipelineInterface.java b/src/com/android/gallery3d/filtershow/presets/PipelineInterface.java
new file mode 100644
index 0000000..05f0a1a
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/presets/PipelineInterface.java
@@ -0,0 +1,31 @@
+/*
+ * 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.presets;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.support.v8.renderscript.Allocation;
+import android.support.v8.renderscript.RenderScript;
+
+public interface PipelineInterface {
+    public String getName();
+    public Resources getResources();
+    public Allocation getInPixelsAllocation();
+    public Allocation getOutPixelsAllocation();
+    public boolean prepareRenderscriptAllocations(Bitmap bitmap);
+    public RenderScript getRSContext();
+}
diff --git a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java b/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java
index c5851c4..b5de692 100644
--- a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java
+++ b/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java
@@ -206,6 +206,8 @@
                     uri = insertContent(context, sourceUri, this.destinationFile, saveFileName,
                             time);
                 }
+                XmpPresets.writeFilterXMP(context, sourceUri, this.destinationFile, preset);
+
                 noBitmap = false;
             } catch (java.lang.OutOfMemoryError e) {
                 // Try 5 times before failing for good.
@@ -219,6 +221,7 @@
         return uri;
     }
 
+
     @Override
     protected void onPostExecute(Uri result) {
         if (callback != null) {
diff --git a/src/com/android/gallery3d/filtershow/tools/XmpPresets.java b/src/com/android/gallery3d/filtershow/tools/XmpPresets.java
new file mode 100644
index 0000000..be75f02
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/tools/XmpPresets.java
@@ -0,0 +1,133 @@
+/*
+ * 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.tools;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+import com.adobe.xmp.XMPException;
+import com.adobe.xmp.XMPMeta;
+import com.adobe.xmp.XMPMetaFactory;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.presets.ImagePreset;
+import com.android.gallery3d.util.XmpUtilHelper;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+public class XmpPresets {
+    public static final String
+            XMP_GOOGLE_FILTER_NAMESPACE = "http://ns.google.com/photos/1.0/filter/";
+    public static final String XMP_GOOGLE_FILTER_PREFIX = "AFltr";
+    public static final String XMP_SRC_FILE_URI = "SourceFileUri";
+    public static final String XMP_FILTERSTACK = "filterstack";
+    private static final String LOGTAG = "XmpPresets";
+
+    public static class XMresults {
+        public String presetString;
+        public ImagePreset preset;
+        public Uri originalimage;
+    }
+
+    static {
+        try {
+            XMPMetaFactory.getSchemaRegistry().registerNamespace(
+                    XMP_GOOGLE_FILTER_NAMESPACE, XMP_GOOGLE_FILTER_PREFIX);
+        } catch (XMPException e) {
+            Log.e(LOGTAG, "Register XMP name space failed", e);
+        }
+    }
+
+    public static void writeFilterXMP(
+            Context context, Uri srcUri, File dstFile, ImagePreset preset) {
+        InputStream is = null;
+        XMPMeta xmpMeta = null;
+        try {
+            is = context.getContentResolver().openInputStream(srcUri);
+            xmpMeta = XmpUtilHelper.extractXMPMeta(is);
+        } catch (FileNotFoundException e) {
+
+        } finally {
+            Utils.closeSilently(is);
+        }
+
+        if (xmpMeta == null) {
+            xmpMeta = XMPMetaFactory.create();
+        }
+        try {
+            xmpMeta.setProperty(XMP_GOOGLE_FILTER_NAMESPACE,
+                    XMP_SRC_FILE_URI, srcUri.toString());
+            xmpMeta.setProperty(XMP_GOOGLE_FILTER_NAMESPACE,
+                    XMP_FILTERSTACK, preset.getJsonString(context.getString(R.string.saved)));
+        } catch (XMPException e) {
+            Log.v(LOGTAG, "Write XMP meta to file failed:" + dstFile.getAbsolutePath());
+            return;
+        }
+
+        if (!XmpUtilHelper.writeXMPMeta(dstFile.getAbsolutePath(), xmpMeta)) {
+            Log.v(LOGTAG, "Write XMP meta to file failed:" + dstFile.getAbsolutePath());
+        }
+    }
+
+    public static XMresults extractXMPData(
+            Context context, MasterImage mMasterImage, Uri uriToEdit) {
+        XMresults ret = new XMresults();
+
+        InputStream is = null;
+        XMPMeta xmpMeta = null;
+        try {
+            is = context.getContentResolver().openInputStream(uriToEdit);
+            xmpMeta = XmpUtilHelper.extractXMPMeta(is);
+        } catch (FileNotFoundException e) {
+        } finally {
+            Utils.closeSilently(is);
+        }
+
+        if (xmpMeta == null) {
+            return null;
+        }
+
+        try {
+            String strSrcUri = xmpMeta.getPropertyString(XMP_GOOGLE_FILTER_NAMESPACE,
+                    XMP_SRC_FILE_URI);
+
+            if (strSrcUri != null) {
+                String filterString = xmpMeta.getPropertyString(XMP_GOOGLE_FILTER_NAMESPACE,
+                        XMP_FILTERSTACK);
+
+                Uri srcUri = Uri.parse(strSrcUri);
+                ret.originalimage = srcUri;
+
+                ret.preset = new ImagePreset(mMasterImage.getPreset());
+                ret.presetString = filterString;
+                boolean ok = ret.preset.readJsonFromString(filterString);
+                if (!ok) {
+                    return null;
+                }
+                return ret;
+            }
+        } catch (XMPException e) {
+            e.printStackTrace();
+        }
+
+        return null;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/MenuExecutor.java b/src/com/android/gallery3d/ui/MenuExecutor.java
index 8f4854e..29def05 100644
--- a/src/com/android/gallery3d/ui/MenuExecutor.java
+++ b/src/com/android/gallery3d/ui/MenuExecutor.java
@@ -190,7 +190,7 @@
         setMenuItemVisible(menu, R.id.action_setas, supportSetAs);
         setMenuItemVisible(menu, R.id.action_show_on_map, supportShowOnMap);
         setMenuItemVisible(menu, R.id.action_edit, supportEdit);
-        // setMenuItemVisible(menu, R.id.action_simple_edit, supportEdit);
+        setMenuItemVisible(menu, R.id.action_simple_edit, supportEdit);
         setMenuItemVisible(menu, R.id.action_details, supportInfo);
     }
 
diff --git a/src/com/android/gallery3d/util/SaveVideoFileUtils.java b/src/com/android/gallery3d/util/SaveVideoFileUtils.java
index c281dd3..e2c5f51 100644
--- a/src/com/android/gallery3d/util/SaveVideoFileUtils.java
+++ b/src/com/android/gallery3d/util/SaveVideoFileUtils.java
@@ -19,6 +19,7 @@
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.database.Cursor;
+import android.media.MediaMetadataRetriever;
 import android.net.Uri;
 import android.os.Environment;
 import android.provider.MediaStore.Video;
@@ -95,7 +96,7 @@
             ContentResolver contentResolver, Uri uri ) {
         long nowInMs = System.currentTimeMillis();
         long nowInSec = nowInMs / 1000;
-        final ContentValues values = new ContentValues(12);
+        final ContentValues values = new ContentValues(13);
         values.put(Video.Media.TITLE, mDstFileInfo.mFileName);
         values.put(Video.Media.DISPLAY_NAME, mDstFileInfo.mFile.getName());
         values.put(Video.Media.MIME_TYPE, "video/mp4");
@@ -104,6 +105,8 @@
         values.put(Video.Media.DATE_ADDED, nowInSec);
         values.put(Video.Media.DATA, mDstFileInfo.mFile.getAbsolutePath());
         values.put(Video.Media.SIZE, mDstFileInfo.mFile.length());
+        int durationMs = retriveVideoDurationMs(mDstFileInfo.mFile.getPath());
+        values.put(Video.Media.DURATION, durationMs);
         // Copy the data taken and location info from src.
         String[] projection = new String[] {
                 VideoColumns.DATE_TAKEN,
@@ -138,4 +141,18 @@
         return contentResolver.insert(Video.Media.EXTERNAL_CONTENT_URI, values);
     }
 
+    public static int retriveVideoDurationMs(String path) {
+        int durationMs = 0;
+        // Calculate the duration of the destination file.
+        MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+        retriever.setDataSource(path);
+        String duration = retriever.extractMetadata(
+                MediaMetadataRetriever.METADATA_KEY_DURATION);
+        if (duration != null) {
+            durationMs = Integer.parseInt(duration);
+        }
+        retriever.release();
+        return durationMs;
+    }
+
 }
diff --git a/src/com/android/photos/data/BitmapDecoder.java b/src/com/android/photos/data/BitmapDecoder.java
index a0ab410..3e5a0f7 100644
--- a/src/com/android/photos/data/BitmapDecoder.java
+++ b/src/com/android/photos/data/BitmapDecoder.java
@@ -18,6 +18,7 @@
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.Config;
 import android.graphics.BitmapFactory;
+import android.graphics.BitmapFactory.Options;
 import android.util.Log;
 import android.util.Pools.Pool;
 import android.util.Pools.SynchronizedPool;
@@ -41,60 +42,103 @@
     private static final String TAG = BitmapDecoder.class.getSimpleName();
     private static final int POOL_SIZE = 4;
     private static final int TEMP_STORAGE_SIZE_BYTES = 16 * 1024;
-    private static final int HEADER_MAX_SIZE = 16 * 1024;
+    private static final int HEADER_MAX_SIZE = 128 * 1024;
 
     private static final Pool<BitmapFactory.Options> sOptions =
             new SynchronizedPool<BitmapFactory.Options>(POOL_SIZE);
 
+    private interface Decoder<T> {
+        Bitmap decode(T input, BitmapFactory.Options options);
+
+        boolean decodeBounds(T input, BitmapFactory.Options options);
+    }
+
+    private static abstract class OnlyDecode<T> implements Decoder<T> {
+        @Override
+        public boolean decodeBounds(T input, BitmapFactory.Options options) {
+            decode(input, options);
+            return true;
+        }
+    }
+
+    private static final Decoder<InputStream> sStreamDecoder = new Decoder<InputStream>() {
+        @Override
+        public Bitmap decode(InputStream is, Options options) {
+            return BitmapFactory.decodeStream(is, null, options);
+        }
+
+        @Override
+        public boolean decodeBounds(InputStream is, Options options) {
+            is.mark(HEADER_MAX_SIZE);
+            BitmapFactory.decodeStream(is, null, options);
+            try {
+                is.reset();
+                return true;
+            } catch (IOException e) {
+                Log.e(TAG, "Could not decode stream to bitmap", e);
+                return false;
+            }
+        }
+    };
+
+    private static final Decoder<String> sFileDecoder = new OnlyDecode<String>() {
+        @Override
+        public Bitmap decode(String filePath, Options options) {
+            return BitmapFactory.decodeFile(filePath, options);
+        }
+    };
+
+    private static final Decoder<byte[]> sByteArrayDecoder = new OnlyDecode<byte[]>() {
+        @Override
+        public Bitmap decode(byte[] data, Options options) {
+            return BitmapFactory.decodeByteArray(data, 0, data.length, options);
+        }
+    };
+
+    private static <T> Bitmap delegateDecode(Decoder<T> decoder, T input) {
+        BitmapFactory.Options options = getOptions();
+        try {
+            options.inJustDecodeBounds = true;
+            if (!decoder.decodeBounds(input, options)) {
+                return null;
+            }
+            options.inJustDecodeBounds = false;
+            GalleryBitmapPool pool = GalleryBitmapPool.getInstance();
+            Bitmap reuseBitmap = pool.get(options.outWidth, options.outHeight);
+            options.inBitmap = reuseBitmap;
+            Bitmap decodedBitmap = decoder.decode(input, options);
+            if (reuseBitmap != null && decodedBitmap != reuseBitmap) {
+                pool.put(reuseBitmap);
+            }
+            return decodedBitmap;
+        } finally {
+            options.inBitmap = null;
+            options.inJustDecodeBounds = false;
+            sOptions.release(options);
+        }
+    }
+
     public static Bitmap decode(InputStream in) {
-        BitmapFactory.Options opts = getOptions();
         try {
             if (!in.markSupported()) {
                 in = new BufferedInputStream(in);
             }
-            opts.inJustDecodeBounds = true;
-            in.mark(HEADER_MAX_SIZE);
-            BitmapFactory.decodeStream(in, null, opts);
-            in.reset();
-            opts.inJustDecodeBounds = false;
-            GalleryBitmapPool pool = GalleryBitmapPool.getInstance();
-            Bitmap reuseBitmap = pool.get(opts.outWidth, opts.outHeight);
-            opts.inBitmap = reuseBitmap;
-            Bitmap decodedBitmap = BitmapFactory.decodeStream(in, null, opts);
-            if (reuseBitmap != null && decodedBitmap != reuseBitmap) {
-                pool.put(reuseBitmap);
-            }
-            return decodedBitmap;
-        } catch (IOException e) {
-            Log.e(TAG, "Could not decode stream to bitmap", e);
-            return null;
+            return delegateDecode(sStreamDecoder, in);
         } finally {
             Utils.closeSilently(in);
-            release(opts);
         }
     }
 
-    public static Bitmap decode(File in) {
-        return decodeFile(in.toString());
+    public static Bitmap decode(File file) {
+        return decodeFile(file.getPath());
     }
 
-    public static Bitmap decodeFile(String in) {
-        BitmapFactory.Options opts = getOptions();
-        try {
-            opts.inJustDecodeBounds = true;
-            BitmapFactory.decodeFile(in, opts);
-            opts.inJustDecodeBounds = false;
-            GalleryBitmapPool pool = GalleryBitmapPool.getInstance();
-            Bitmap reuseBitmap = pool.get(opts.outWidth, opts.outHeight);
-            opts.inBitmap = reuseBitmap;
-            Bitmap decodedBitmap = BitmapFactory.decodeFile(in, opts);
-            if (reuseBitmap != null && decodedBitmap != reuseBitmap) {
-                pool.put(reuseBitmap);
-            }
-            return decodedBitmap;
-        } finally {
-            release(opts);
-        }
+    public static Bitmap decodeFile(String path) {
+        return delegateDecode(sFileDecoder, path);
+    }
+
+    public static Bitmap decodeByteArray(byte[] data) {
+        return delegateDecode(sByteArrayDecoder, data);
     }
 
     public static void put(Bitmap bitmap) {
@@ -113,10 +157,4 @@
 
         return opts;
     }
-
-    private static void release(BitmapFactory.Options opts) {
-        opts.inBitmap = null;
-        opts.inJustDecodeBounds = false;
-        sOptions.release(opts);
-    }
 }
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);
     }
 }