Merge "Usage statistics for photo and video capture" into gb-ub-photos-bryce
diff --git a/src/com/android/camera/CameraManager.java b/src/com/android/camera/CameraManager.java
index e89c2eb..be82ea6 100644
--- a/src/com/android/camera/CameraManager.java
+++ b/src/com/android/camera/CameraManager.java
@@ -29,7 +29,6 @@
 import android.hardware.Camera.PictureCallback;
 import android.hardware.Camera.PreviewCallback;
 import android.hardware.Camera.ShutterCallback;
-import android.os.ConditionVariable;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
@@ -40,7 +39,6 @@
 import com.android.gallery3d.common.ApiHelper;
 
 import java.io.IOException;
-import java.util.concurrent.SynchronousQueue;
 
 public class CameraManager {
     private static final String TAG = "CameraManager";
@@ -48,10 +46,7 @@
 
     private Parameters mParameters;
     private boolean mParametersIsDirty;
-    private SynchronousQueue<Parameters> mParametersQueue =
-            new SynchronousQueue<Parameters>();
-    private SynchronousQueue<IOExceptionHolder> mReconnectExceptionQueue =
-            new SynchronousQueue<IOExceptionHolder>();
+    private IOException mReconnectIOException;
 
     private static final int RELEASE = 1;
     private static final int RECONNECT = 2;
@@ -148,16 +143,11 @@
                         return;
 
                     case RECONNECT:
-                        IOExceptionHolder holder = new IOExceptionHolder();
-                        holder.ex = null;
+                        mReconnectIOException = null;
                         try {
                             mCamera.reconnect();
                         } catch (IOException ex) {
-                            holder.ex = ex;
-                        }
-                        try {
-                            mReconnectExceptionQueue.put(holder);
-                        } catch (InterruptedException ex) {
+                            mReconnectIOException = ex;
                         }
                         return;
 
@@ -240,11 +230,7 @@
                         return;
 
                     case GET_PARAMETERS:
-                        try {
-                            mParametersQueue.put(mCamera.getParameters());
-                        } catch (InterruptedException e) {
-                            throw new RuntimeException(e);
-                        }
+                        mParameters = mCamera.getParameters();
                         return;
 
                     case SET_PARAMETERS_ASYNC:
@@ -303,14 +289,6 @@
     }
 
     public class CameraProxy {
-        private ConditionVariable mWaitDoneLock = new ConditionVariable();
-        private Runnable mUnlockRunnable = new Runnable() {
-            @Override
-            public void run() {
-                mWaitDoneLock.open();
-            }
-        };
-
 
         private CameraProxy() {
             Assert(mCamera != null);
@@ -329,13 +307,9 @@
 
         public void reconnect() throws IOException {
             mCameraHandler.sendEmptyMessage(RECONNECT);
-            IOExceptionHolder holder = null;
-            try {
-                holder = mReconnectExceptionQueue.take();
-            } catch (InterruptedException ex) {
-            }
-            if (holder != null && holder.ex != null) {
-                throw holder.ex;
+            waitDone();
+            if (mReconnectIOException != null) {
+                throw mReconnectIOException;
             }
         }
 
@@ -446,12 +420,20 @@
         }
 
         public void setParameters(Parameters params) {
-            // TODO: check if this synchronous version is necessary
+            if (params == null) {
+                Log.v(TAG, "null parameters in setParameters()");
+                return;
+            }
             mParametersIsDirty = true;
             mCameraHandler.obtainMessage(SET_PARAMETERS, params).sendToTarget();
         }
 
         public void setParametersAsync(Parameters params) {
+            // TODO: remove this.
+            if (params == null) {
+                Log.v(TAG, "null parameters in setParameters()");
+                return;
+            }
             mParametersIsDirty = true;
             mCameraHandler.removeMessages(SET_PARAMETERS_ASYNC);
             mCameraHandler.obtainMessage(SET_PARAMETERS_ASYNC, params).sendToTarget();
@@ -460,11 +442,7 @@
         public Parameters getParameters() {
             if (mParametersIsDirty || mParameters == null) {
                 mCameraHandler.sendEmptyMessage(GET_PARAMETERS);
-                try {
-                    mParameters = mParametersQueue.take();
-                    mParametersIsDirty = false;
-                } catch (InterruptedException ex) {
-                }
+                if (waitDone()) mParametersIsDirty = false;
             }
             return mParameters;
         }
@@ -474,10 +452,28 @@
                     ENABLE_SHUTTER_SOUND, (enable ? 1 : 0), 0).sendToTarget();
         }
 
-        public void waitDone() {
-            mWaitDoneLock.close();
-            mCameraHandler.post(mUnlockRunnable);
-            mWaitDoneLock.block();
+        // return false if cancelled.
+        public boolean waitDone() {
+            final Object waitDoneLock = new Object();
+            final Runnable unlockRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    synchronized (waitDoneLock) {
+                        waitDoneLock.notifyAll();
+                    }
+                }
+            };
+
+            synchronized (waitDoneLock) {
+                mCameraHandler.post(unlockRunnable);
+                try {
+                    waitDoneLock.wait();
+                } catch (InterruptedException ex) {
+                    Log.v(TAG, "waitDone interrupted");
+                    return false;
+                }
+            }
+            return true;
         }
     }
 }
diff --git a/src/com/android/camera/ComboPreferences.java b/src/com/android/camera/ComboPreferences.java
index c561e6b..e17e47a 100644
--- a/src/com/android/camera/ComboPreferences.java
+++ b/src/com/android/camera/ComboPreferences.java
@@ -22,6 +22,8 @@
 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
 import android.preference.PreferenceManager;
 
+import com.android.gallery3d.util.UsageStatistics;
+
 import java.util.Map;
 import java.util.Set;
 import java.util.WeakHashMap;
@@ -205,9 +207,7 @@
 
     @Override
     public boolean contains(String key) {
-        if (mPrefLocal.contains(key)) return true;
-        if (mPrefGlobal.contains(key)) return true;
-        return false;
+        return mPrefLocal.contains(key) || mPrefGlobal.contains(key);
     }
 
     private class MyEditor implements Editor {
@@ -330,5 +330,6 @@
             listener.onSharedPreferenceChanged(this, key);
         }
         BackupManager.dataChanged(mPackageName);
+        UsageStatistics.onEvent("CameraSettingsChange", null, key);
     }
 }
diff --git a/src/com/android/camera/PhotoMenu.java b/src/com/android/camera/PhotoMenu.java
index 2535a0c..280ad44 100644
--- a/src/com/android/camera/PhotoMenu.java
+++ b/src/com/android/camera/PhotoMenu.java
@@ -34,7 +34,6 @@
         TimerSettingPopup.Listener,
         ListPrefSettingPopup.Listener {
     private static String TAG = "CAM_photomenu";
-    private static float FLOAT_PI_DIVIDED_BY_TWO = (float) Math.PI / 2;
     private final String mSettingOff;
 
     private PhotoUI mUI;
@@ -54,13 +53,20 @@
         super.initialize(group);
         mPopup = null;
         mSecondPopup = null;
-        float sweep = FLOAT_PI_DIVIDED_BY_TWO / 2;
-        addItem(CameraSettings.KEY_FLASH_MODE, FLOAT_PI_DIVIDED_BY_TWO - sweep, sweep);
-        addItem(CameraSettings.KEY_EXPOSURE, 3 * FLOAT_PI_DIVIDED_BY_TWO - sweep, sweep);
-        addItem(CameraSettings.KEY_WHITE_BALANCE, 3 * FLOAT_PI_DIVIDED_BY_TWO + sweep, sweep);
+        float sweep = (float) (SWEEP * Math.PI);
+        PieItem item = null;
+        // flash
+        if (group.findPreference(CameraSettings.KEY_FLASH_MODE) != null) {
+            item = makeItem(CameraSettings.KEY_FLASH_MODE, CENTER - sweep, sweep);
+            mRenderer.addItem(item);
+        }
+        // exposure compensation
+        item = makeItem(CameraSettings.KEY_EXPOSURE, CENTER + sweep, sweep);
+        mRenderer.addItem(item);
+        // camera switcher
         if (group.findPreference(CameraSettings.KEY_CAMERA_ID) != null) {
-            PieItem item = makeItem(R.drawable.ic_switch_photo_facing_holo_light);
-            item.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO + sweep, sweep);
+            item = makeItem(R.drawable.ic_switch_photo_facing_holo_light);
+            item.setFixedSlice(CENTER - 2 * sweep, sweep);
             item.setOnClickListener(new OnClickListener() {
                 @Override
                 public void onClick(PieItem item) {
@@ -79,10 +85,11 @@
             });
             mRenderer.addItem(item);
         }
+        // hdr
         if (group.findPreference(CameraSettings.KEY_CAMERA_HDR) != null) {
-            PieItem hdr = makeItem(R.drawable.ic_hdr);
-            hdr.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO, sweep);
-            hdr.setOnClickListener(new OnClickListener() {
+            item = makeItem(R.drawable.ic_hdr);
+            item.setFixedSlice(CENTER + 2 * sweep, sweep);
+            item.setOnClickListener(new OnClickListener() {
                 @Override
                 public void onClick(PieItem item) {
                     // Find the index of next camera.
@@ -96,8 +103,18 @@
                     }
                 }
             });
-            mRenderer.addItem(hdr);
+            mRenderer.addItem(item);
         }
+
+        // more settings
+        PieItem more = makeItem(R.drawable.ic_settings_holo_light);
+        more.setFixedSlice(CENTER, sweep);
+        mRenderer.addItem(more);
+        // white balance
+        item = makeItem(CameraSettings.KEY_WHITE_BALANCE,
+                CENTER + sweep, sweep);
+        more.addItem(item);
+        // settings popup
         mOtherKeys = new String[] {
                 CameraSettings.KEY_SCENE_MODE,
                 CameraSettings.KEY_RECORD_LOCATION,
@@ -106,8 +123,8 @@
                 CameraSettings.KEY_TIMER,
                 CameraSettings.KEY_TIMER_SOUND_EFFECTS,
                 };
-        PieItem item = makeItem(R.drawable.ic_settings_holo_light);
-        item.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO * 3, sweep);
+        item = makeItem(R.drawable.ic_settings_holo_light);
+        item.setFixedSlice(CENTER, sweep);
         item.setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(PieItem item) {
@@ -117,7 +134,7 @@
                 mUI.showPopup(mPopup);
             }
         });
-        mRenderer.addItem(item);
+        more.addItem(item);
     }
 
     @Override
diff --git a/src/com/android/camera/PhotoUI.java b/src/com/android/camera/PhotoUI.java
index de0468b..1af870a 100644
--- a/src/com/android/camera/PhotoUI.java
+++ b/src/com/android/camera/PhotoUI.java
@@ -56,7 +56,8 @@
     PreviewGestures.SingleTapListener,
     FocusUI,
     LocationManager.Listener,
-    FaceDetectionListener {
+    FaceDetectionListener,
+    PreviewGestures.SwipeListener {
 
     private static final String TAG = "CAM_UI";
 
@@ -175,7 +176,8 @@
         }
         if (mGestures == null) {
             // this will handle gesture disambiguation and dispatching
-            mGestures = new PreviewGestures(mActivity, this, mZoomRenderer, mPieRenderer);
+            mGestures = new PreviewGestures(mActivity, this, mZoomRenderer, mPieRenderer,
+                    this);
         }
         mGestures.clearTouchReceivers();
         mGestures.setRenderOverlay(mRenderOverlay);
@@ -196,20 +198,24 @@
         updateOnScreenIndicators(params, 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 = mActivity.findViewById(R.id.blocker);
         mMenuButton = mActivity.findViewById(R.id.menu);
         mMenuButton.setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(View v) {
-                if (mPieRenderer != null) {
-                    // If autofocus is not finished, cancel autofocus so that the
-                    // subsequent touch can be handled by PreviewGestures
-                    if (mController.getCameraState() == PhotoController.FOCUSING) {
-                            mController.cancelAutoFocus();
-                    }
-                    mPieRenderer.showInCenter();
-                }
+                openMenu();
             }
         });
         if (mController.isImageCaptureIntent()) {
@@ -723,4 +729,11 @@
         mFaceView.setFaces(faces);
     }
 
+    @Override
+    public void onSwipe(int direction) {
+        if (direction == PreviewGestures.DIR_UP) {
+            openMenu();
+        }
+    }
+
 }
diff --git a/src/com/android/camera/PieController.java b/src/com/android/camera/PieController.java
index 8202fca..2145fd8 100644
--- a/src/com/android/camera/PieController.java
+++ b/src/com/android/camera/PieController.java
@@ -37,6 +37,10 @@
     protected static final int MODE_PHOTO = 0;
     protected static final int MODE_VIDEO = 1;
 
+    protected static float CENTER = (float) Math.PI / 2;
+    protected static final float SWEEP = 0.06f;
+
+
     protected CameraActivity mActivity;
     protected PreferenceGroup mPreferenceGroup;
     protected OnPreferenceChangedListener mListener;
@@ -84,10 +88,10 @@
         return new PieItem(drawable, 0);
     }
 
-    public void addItem(String prefKey, float center, float sweep) {
+    public PieItem makeItem(String prefKey, float center, float sweep) {
         final IconListPreference pref =
                 (IconListPreference) mPreferenceGroup.findPreference(prefKey);
-        if (pref == null) return;
+        if (pref == null) return null;
         int[] iconIds = pref.getLargeIconIds();
         int resid = -1;
         if (!pref.getUseSingleIcon() && iconIds != null) {
@@ -101,7 +105,6 @@
         PieItem item = makeItem(resid);
         // use center and sweep to determine layout
         item.setFixedSlice(center, sweep);
-        mRenderer.addItem(item);
         mPreferences.add(pref);
         mPreferenceMap.put(pref, item);
         int nOfEntries = pref.getEntries().length;
@@ -113,6 +116,7 @@
                 } else {
                     inner = makeItem(pref.getEntries()[i]);
                 }
+                layoutInner(inner, i, nOfEntries);
                 item.addItem(inner);
                 final int index = i;
                 inner.setOnClickListener(new OnClickListener() {
@@ -125,6 +129,23 @@
                 });
             }
         }
+        return item;
+    }
+
+    public PieItem makeDialItem(ListPreference pref, int iconId, float center, float sweep) {
+        PieItem item = makeItem(iconId);
+        return item;
+    }
+
+    protected void layoutInner(PieItem item, int ix, int n) {
+        float sweep = (float) (SWEEP * Math.PI);//FLOAT_PI_DIVIDED_BY_TWO / Math.max(n, 5);
+        float start = CENTER + (n - 1) * (sweep / 2f);
+        item.setFixedSlice(start - ix * sweep, sweep);
+    }
+
+    public void addItem(String prefKey, float center, float sweep) {
+        PieItem item = makeItem(prefKey, center, sweep);
+        mRenderer.addItem(item);
     }
 
     public void setPreferenceGroup(PreferenceGroup group) {
diff --git a/src/com/android/camera/PreviewGestures.java b/src/com/android/camera/PreviewGestures.java
index f6b6228..351da0a 100644
--- a/src/com/android/camera/PreviewGestures.java
+++ b/src/com/android/camera/PreviewGestures.java
@@ -18,7 +18,6 @@
 
 import android.os.Handler;
 import android.os.Message;
-import android.util.Log;
 import android.view.MotionEvent;
 import android.view.ScaleGestureDetector;
 import android.view.View;
@@ -44,6 +43,12 @@
     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 CameraActivity mActivity;
     private SingleTapListener mTapListener;
@@ -61,6 +66,7 @@
     private boolean mZoomOnly;
     private int mOrientation;
     private int[] mLocation;
+    private SwipeListener mSwipeListener;
 
     private Handler mHandler = new Handler() {
         public void handleMessage(Message msg) {
@@ -76,8 +82,12 @@
         public void onSingleTapUp(View v, int x, int y);
     }
 
+    interface SwipeListener {
+        public void onSwipe(int direction);
+    }
+
     public PreviewGestures(CameraActivity ctx, SingleTapListener tapListener,
-            ZoomRenderer zoom, PieRenderer pie) {
+            ZoomRenderer zoom, PieRenderer pie, SwipeListener swipe) {
         mActivity = ctx;
         mTapListener = tapListener;
         mPie = pie;
@@ -88,6 +98,7 @@
         mTapTimeout = ViewConfiguration.getTapTimeout();
         mEnabled = true;
         mLocation = new int[2];
+        mSwipeListener = swipe;
     }
 
     public void setRenderOverlay(RenderOverlay overlay) {
@@ -149,6 +160,11 @@
             }
         } 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));
@@ -215,18 +231,13 @@
                         || Math.abs(m.getY() - mDown.getY()) > mSlop) {
                     // moved too far and no timeout yet, no focus or pie
                     cancelPie();
-                    if (isSwipe(m, true)) {
+                    int dir = getSwipeDirection(m);
+                    if (dir == DIR_LEFT) {
                         mMode = MODE_MODULE;
                         return mActivity.superDispatchTouchEvent(m);
                     } else {
                         cancelActivityTouchHandling(m);
-                        if (isSwipe(m , false)) {
-                            mMode = MODE_NONE;
-                        } else if (!mZoomOnly) {
-                            mMode = MODE_PIE;
-                            openPie();
-                            sendToPie(m);
-                        }
+                        mMode = MODE_NONE;
                     }
                 }
             }
@@ -246,32 +257,31 @@
     }
 
     // left tests for finger moving right to left
-    private boolean isSwipe(MotionEvent m, boolean left) {
+    private int getSwipeDirection(MotionEvent m) {
         float dx = 0;
         float dy = 0;
         switch (mOrientation) {
         case 0:
             dx = m.getX() - mDown.getX();
-            dy = Math.abs(m.getY() - mDown.getY());
+            dy = m.getY() - mDown.getY();
             break;
         case 90:
             dx = - (m.getY() - mDown.getY());
-            dy = Math.abs(m.getX() - mDown.getX());
+            dy = m.getX() - mDown.getX();
             break;
         case 180:
             dx = -(m.getX() - mDown.getX());
-            dy = Math.abs(m.getY() - mDown.getY());
+            dy = m.getY() - mDown.getY();
             break;
         case 270:
             dx = m.getY() - mDown.getY();
-            dy = Math.abs(m.getX() - mDown.getX());
+            dy = m.getX() - mDown.getX();
             break;
         }
-        if (left) {
-            return (dx < 0 && dy / -dx < 0.6f);
-        } else {
-            return (dx > 0 && dy / dx < 0.6f);
-        }
+        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) {
diff --git a/src/com/android/camera/VideoMenu.java b/src/com/android/camera/VideoMenu.java
index aa3a807..0f987aa 100644
--- a/src/com/android/camera/VideoMenu.java
+++ b/src/com/android/camera/VideoMenu.java
@@ -34,7 +34,6 @@
         TimeIntervalPopup.Listener {
 
     private static String TAG = "CAM_VideoMenu";
-    private static float FLOAT_PI_DIVIDED_BY_TWO = (float) Math.PI / 2;
 
     private VideoUI mUI;
     private String[] mOtherKeys;
@@ -54,12 +53,13 @@
         super.initialize(group);
         mPopup = null;
         mPopupStatus = POPUP_NONE;
-        float sweep = FLOAT_PI_DIVIDED_BY_TWO / 2;
+        float sweep = (float)(SWEEP * Math.PI);
 
-        addItem(CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE, FLOAT_PI_DIVIDED_BY_TWO - sweep, sweep);
-        addItem(CameraSettings.KEY_WHITE_BALANCE, 3 * FLOAT_PI_DIVIDED_BY_TWO + sweep, sweep);
+        addItem(CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE, CENTER - sweep, sweep);
+        addItem(CameraSettings.KEY_WHITE_BALANCE, CENTER + sweep, sweep);
+        // camera switcher
         PieItem item = makeItem(R.drawable.ic_switch_video_facing_holo_light);
-        item.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO + sweep, sweep);
+        item.setFixedSlice(CENTER - 2 * sweep, sweep);
         item.setOnClickListener(new OnClickListener() {
 
             @Override
@@ -76,15 +76,15 @@
             }
         });
         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.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO * 3, sweep);
+        item.setFixedSlice(CENTER, sweep);
         item.setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(PieItem item) {
diff --git a/src/com/android/camera/VideoUI.java b/src/com/android/camera/VideoUI.java
index 0eac49d..bb615e9 100644
--- a/src/com/android/camera/VideoUI.java
+++ b/src/com/android/camera/VideoUI.java
@@ -44,7 +44,8 @@
 import java.util.List;
 
 public class VideoUI implements SurfaceHolder.Callback, PieRenderer.PieListener,
-        PreviewGestures.SingleTapListener {
+        PreviewGestures.SingleTapListener,
+        PreviewGestures.SwipeListener {
     private final static String TAG = "CAM_VideoUI";
     // module fields
     private CameraActivity mActivity;
@@ -210,7 +211,7 @@
         }
         mRenderOverlay.addRenderer(mZoomRenderer);
         if (mGestures == null) {
-            mGestures = new PreviewGestures(mActivity, this, mZoomRenderer, mPieRenderer);
+            mGestures = new PreviewGestures(mActivity, this, mZoomRenderer, mPieRenderer, this);
         }
         mGestures.setRenderOverlay(mRenderOverlay);
         mGestures.clearTouchReceivers();
@@ -279,6 +280,12 @@
         }
     }
 
+    private void openMenu() {
+        if (mPieRenderer != null) {
+            mPieRenderer.showInCenter();
+        }
+    }
+
     public void showPopup(AbstractSettingPopup popup) {
         mActivity.hideUI();
         mBlocker.setVisibility(View.INVISIBLE);
@@ -513,4 +520,12 @@
         public void onZoomEnd() {
         }
     }
+
+    @Override
+    public void onSwipe(int direction) {
+        if (direction == PreviewGestures.DIR_UP) {
+            openMenu();
+        }
+    }
+
 }
diff --git a/src/com/android/camera/ui/PieItem.java b/src/com/android/camera/ui/PieItem.java
index 677e5ac..bbfa1dc 100644
--- a/src/com/android/camera/ui/PieItem.java
+++ b/src/com/android/camera/ui/PieItem.java
@@ -57,7 +57,9 @@
     public PieItem(Drawable drawable, int level) {
         mDrawable = drawable;
         this.level = level;
-        setAlpha(1f);
+        if (drawable != null) {
+            setAlpha(1f);
+        }
         mEnabled = true;
         setAnimationAngle(getAnimationAngle());
         start = -1;
@@ -79,6 +81,10 @@
         mItems.add(item);
     }
 
+    public void clearItems() {
+        mItems = null;
+    }
+
     public void setPath(Path p) {
         mPath = p;
     }
diff --git a/src/com/android/camera/ui/PieRenderer.java b/src/com/android/camera/ui/PieRenderer.java
index b60d9f6..dffa47e 100644
--- a/src/com/android/camera/ui/PieRenderer.java
+++ b/src/com/android/camera/ui/PieRenderer.java
@@ -27,6 +27,7 @@
 import android.graphics.RectF;
 import android.os.Handler;
 import android.os.Message;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 import android.view.animation.Animation;
@@ -54,6 +55,8 @@
     private static final int STATE_FINISHING = 2;
     private static final int STATE_PIE = 8;
 
+    private static final float MATH_PI_2 = (float)(Math.PI / 2);
+
     private Runnable mDisappear = new Disappear();
     private Animation.AnimationListener mEndAction = new EndAction();
     private static final int SCALING_UP_TIME = 600;
@@ -66,26 +69,26 @@
     private static final long PIE_FADE_IN_DURATION = 200;
     private static final long PIE_XFADE_DURATION = 200;
     private static final long PIE_SELECT_FADE_DURATION = 300;
+    private static final long PIE_OPEN_SUB_DELAY = 400;
 
     private static final int MSG_OPEN = 0;
     private static final int MSG_CLOSE = 1;
-    private static final float PIE_SWEEP = (float)(Math.PI * 2 / 3);
+    private static final int MSG_OPENSUBMENU = 2;
+    private static final float PIE_SWEEP = (float)(Math.PI / 2);
     // geometry
-    private Point mCenter;
+    private Point mSliceCenter;
     private int mRadius;
-    private int mRadiusInc;
 
     // the detection if touch is inside a slice is offset
     // inbounds by this amount to allow the selection to show before the
     // finger covers it
     private int mTouchOffset;
 
-    private List<PieItem> mItems;
-
-    private PieItem mOpenItem;
+    private List<PieItem> mOpen;
 
     private Paint mSelectedPaint;
     private Paint mSubPaint;
+    private Paint mMenuArcPaint;
 
     // touch handling
     private PieItem mCurrentItem;
@@ -98,6 +101,11 @@
     private int mFocusY;
     private int mCenterX;
     private int mCenterY;
+    private int mPieCenterX;
+    private int mPieCenterY;
+    private int mIconRadius;
+    private int mArcRadius;
+    private int mArcOffset;
 
     private int mDialAngle;
     private RectF mCircle;
@@ -124,7 +132,7 @@
             switch(msg.what) {
             case MSG_OPEN:
                 if (mListener != null) {
-                    mListener.onPieOpened(mCenter.x, mCenter.y);
+                    mListener.onPieOpened(mPieCenterX, mPieCenterY);
                 }
                 break;
             case MSG_CLOSE:
@@ -132,7 +140,11 @@
                     mListener.onPieClosed();
                 }
                 break;
+            case MSG_OPENSUBMENU:
+                onEnterOpen();
+                break;
             }
+
         }
     };
 
@@ -153,13 +165,13 @@
 
     private void init(Context ctx) {
         setVisible(false);
-        mItems = new ArrayList<PieItem>();
+        mOpen = new ArrayList<PieItem>();
+        mOpen.add(new PieItem(null, 0));
         Resources res = ctx.getResources();
         mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start);
         mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset);
-        mRadiusInc =  (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment);
         mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset);
-        mCenter = new Point(0,0);
+        mSliceCenter = new Point(0,0);
         mSelectedPaint = new Paint();
         mSelectedPaint.setColor(Color.argb(255, 51, 181, 229));
         mSelectedPaint.setAntiAlias(true);
@@ -184,6 +196,18 @@
         mTouchSlopSquared = ViewConfiguration.get(ctx).getScaledTouchSlop();
         mTouchSlopSquared = mTouchSlopSquared * mTouchSlopSquared;
         mDown = new Point();
+        mMenuArcPaint = new Paint();
+        mMenuArcPaint.setAntiAlias(true);
+        mMenuArcPaint.setColor(Color.argb(140, 255, 255, 255));
+        mMenuArcPaint.setStrokeWidth(10);
+        mMenuArcPaint.setStyle(Paint.Style.STROKE);
+        mIconRadius = res.getDimensionPixelSize(R.dimen.pie_item_radius);
+        mArcRadius = res.getDimensionPixelSize(R.dimen.pie_arc_radius);
+        mArcOffset = res.getDimensionPixelSize(R.dimen.pie_arc_offset);
+    }
+
+    private PieItem getRoot() {
+        return mOpen.get(0);
     }
 
     public boolean showsItems() {
@@ -192,15 +216,11 @@
 
     public void addItem(PieItem item) {
         // add the item to the pie itself
-        mItems.add(item);
-    }
-
-    public void removeItem(PieItem item) {
-        mItems.remove(item);
+        getRoot().addItem(item);
     }
 
     public void clearItems() {
-        mItems.clear();
+        getRoot().clearItems();
     }
 
     public void showInCenter() {
@@ -212,7 +232,8 @@
                 cancelFocus();
             }
             mState = STATE_PIE;
-            setCenter(mCenterX, mCenterY);
+            resetPieCenter();
+            setCenter(mPieCenterX, mPieCenterY);
             mTapMode = true;
             show(true);
         }
@@ -231,10 +252,16 @@
             mState = STATE_PIE;
             // ensure clean state
             mCurrentItem = null;
-            mOpenItem = null;
-            for (PieItem item : mItems) {
-                item.setSelected(false);
+            PieItem root = getRoot();
+            for (PieItem openItem : mOpen) {
+                if (openItem.hasItems()) {
+                    for (PieItem item : openItem.getItems()) {
+                        item.setSelected(false);
+                    }
+                }
             }
+            mOpen.clear();
+            mOpen.add(root);
             layoutPie();
             fadeIn();
         } else {
@@ -270,22 +297,41 @@
     }
 
     public void setCenter(int x, int y) {
-        mCenter.x = x;
-        mCenter.y = y;
-        // when using the pie menu, align the focus ring
-        alignFocus(x, y);
+        mPieCenterX = x;
+        mPieCenterY = y;
+        mSliceCenter.x = x;
+        mSliceCenter.y = y - mArcOffset + mIconRadius;
+    }
+
+    @Override
+    public void layout(int l, int t, int r, int b) {
+        super.layout(l, t, r, b);
+        mCenterX = (r - l) / 2;
+        mCenterY = (b - t) / 2;
+
+        mFocusX = mCenterX;
+        mFocusY = mCenterY;
+        resetPieCenter();
+        setCircle(mFocusX, mFocusY);
+        if (isVisible() && mState == STATE_PIE) {
+            setCenter(mPieCenterX, mPieCenterY);
+            layoutPie();
+        }
+    }
+
+    private void resetPieCenter() {
+        mPieCenterX = mCenterX;
+        mPieCenterY = mCenterY + mCenterY / 3;
     }
 
     private void layoutPie() {
-        int rgap = 2;
-        int inner = mRadius + rgap;
-        int outer = mRadius + mRadiusInc - rgap;
+        int inner = mIconRadius;
+        int outer = inner + mTouchOffset;
         int gap = 1;
-        layoutItems(mItems, (float) (Math.PI / 2), inner, outer, gap);
+        layoutItems(0, getRoot().getItems(), (float) (Math.PI / 2), inner, outer, gap);
     }
 
-    private void layoutItems(List<PieItem> items, float centerAngle, int inner,
-            int outer, int gap) {
+    private void layoutItems(int level, List<PieItem> items, float centerAngle, int inner, int outer, int gap) {
         float emptyangle = PIE_SWEEP / 16;
         float sweep = (float) (PIE_SWEEP - 2 * emptyangle) / items.size();
         float angle = centerAngle - PIE_SWEEP / 2 + emptyangle + sweep / 2;
@@ -298,8 +344,10 @@
                 break;
             }
         }
+        Point p = new Point(mSliceCenter);
+        p.y -= level * mTouchOffset;
         Path path = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap,
-                outer, inner, mCenter);
+                outer, inner, p);
         for (PieItem item : items) {
             // shared between items
             item.setPath(path);
@@ -311,14 +359,14 @@
             // move views to outer border
             int r = inner + (outer - inner) * 2 / 3;
             int x = (int) (r * Math.cos(angle));
-            int y = mCenter.y - (int) (r * Math.sin(angle)) - h / 2;
-            x = mCenter.x + x - w / 2;
+            int y = mSliceCenter.y - (level * mTouchOffset) - (int) (r * Math.sin(angle)) - h / 2;
+            x = mSliceCenter.x + x - w / 2;
             item.setBounds(x, y, x + w, y + h);
             float itemstart = angle - sweep / 2;
             item.setGeometry(itemstart, sweep, inner, outer);
             if (item.hasItems()) {
-                layoutItems(item.getItems(), angle, inner,
-                        outer + mRadiusInc / 2, gap);
+                layoutItems(level + 1, item.getItems(), MATH_PI_2, inner,
+                        outer, gap);
             }
             angle += sweep;
         }
@@ -378,6 +426,31 @@
         mOverlay.startAnimation(mFadeOut);
     }
 
+    // root does not count
+    private boolean hasOpenItem() {
+        return mOpen.size() > 1;
+    }
+
+    // pop an item of the open item stack
+    private PieItem closeOpenItem() {
+        PieItem item = getOpenItem();
+        mOpen.remove(mOpen.size() -1);
+        return item;
+    }
+
+    private PieItem getOpenItem() {
+        return mOpen.get(mOpen.size() - 1);
+    }
+
+    // return the children either the root or parent of the current open item
+    private PieItem getParent() {
+        return mOpen.get(Math.max(0, mOpen.size() - 2));
+    }
+
+    private int getLevel() {
+        return mOpen.size() - 1;
+    }
+
     @Override
     public void onDraw(Canvas canvas) {
         float alpha = 1;
@@ -391,39 +464,55 @@
         int state = canvas.save();
         if (mFadeIn != null) {
             float sf = 0.9f + alpha * 0.1f;
-            canvas.scale(sf, sf, mCenter.x, mCenter.y);
+            canvas.scale(sf, sf, mPieCenterX, mPieCenterY);
         }
-        drawFocus(canvas);
+        if (mState != STATE_PIE) {
+            drawFocus(canvas);
+        }
         if (mState == STATE_FINISHING) {
             canvas.restoreToCount(state);
             return;
         }
-        if ((mOpenItem == null) || (mXFade != null)) {
+        if (!hasOpenItem() || (mXFade != null)) {
             // draw base menu
-            for (PieItem item : mItems) {
-                drawItem(canvas, item, alpha);
+            drawArc(canvas, getLevel());
+            for (PieItem item : getParent().getItems()) {
+                drawItem(Math.max(0, mOpen.size() - 2), canvas, item, alpha);
             }
         }
-        if (mOpenItem != null) {
-            for (PieItem inner : mOpenItem.getItems()) {
+        if (hasOpenItem()) {
+            int level = getLevel();
+            drawArc(canvas, level);
+            for (PieItem inner : getOpenItem().getItems()) {
                 if (mFadeOut != null) {
-                    drawItem(canvas, inner, alpha);
+                    drawItem(level, canvas, inner, alpha);
                 } else {
-                    drawItem(canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1);
+                    drawItem(level, canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1);
                 }
             }
         }
         canvas.restoreToCount(state);
     }
 
-    private void drawItem(Canvas canvas, PieItem item, float alpha) {
+    private void drawArc(Canvas canvas, int level) {
+        // arc
+        if (mState == STATE_PIE) {
+            int nr = mArcRadius;
+            int cy = mPieCenterY - mArcOffset + mArcRadius  - level * mTouchOffset;
+            canvas.drawArc(new RectF(mPieCenterX - nr, cy - mArcRadius,
+                    mPieCenterX + nr, cy + mArcRadius),
+                    252, 36, false, mMenuArcPaint);
+        }
+    }
+    private void drawItem(int level, Canvas canvas, PieItem item, float alpha) {
         if (mState == STATE_PIE) {
             if (item.getPath() != null) {
+                int y = mSliceCenter.y - level * mTouchOffset;
                 if (item.isSelected()) {
                     Paint p = mSelectedPaint;
                     int state = canvas.save();
                     float r = getDegrees(item.getStartAngle());
-                    canvas.rotate(r, mCenter.x, mCenter.y);
+                    canvas.rotate(r, mSliceCenter.x, y);
                     if (mFadeOut != null) {
                         p.setAlpha((int)(255 * alpha));
                     }
@@ -448,7 +537,7 @@
         float x = evt.getX();
         float y = evt.getY();
         int action = evt.getActionMasked();
-        PointF polar = getPolar(x, y, !(mTapMode));
+        PointF polar = getPolar(x, y, !mTapMode);
         if (MotionEvent.ACTION_DOWN == action) {
             mDown.x = (int) evt.getX();
             mDown.y = (int) evt.getY();
@@ -469,7 +558,7 @@
                 PieItem item = mCurrentItem;
                 if (mTapMode) {
                     item = findItem(polar);
-                    if (item != null && mOpening) {
+                    if (mOpening) {
                         mOpening = false;
                         return true;
                     }
@@ -477,10 +566,11 @@
                 if (item == null) {
                     mTapMode = false;
                     show(false);
-                } else if (!mOpening
-                        && !item.hasItems()) {
-                    startFadeOut(item);
-                    mTapMode = false;
+                } else if (!mOpening && !item.hasItems()) {
+                        startFadeOut(item);
+                        mTapMode = false;
+                } else {
+                    mTapMode = true;
                 }
                 return true;
             }
@@ -489,11 +579,17 @@
                 show(false);
             }
             deselect();
+            mHandler.removeMessages(MSG_OPENSUBMENU);
             return false;
         } else if (MotionEvent.ACTION_MOVE == action) {
-            if (polar.y < mRadius) {
-                if (mOpenItem != null) {
-                    mOpenItem = null;
+            if (pulledToCenter(polar)) {
+                mHandler.removeMessages(MSG_OPENSUBMENU);
+                if (hasOpenItem()) {
+                    if (mCurrentItem != null) {
+                        mCurrentItem.setSelected(false);
+                    }
+                    closeOpenItem();
+                    mCurrentItem = null;
                 } else {
                     deselect();
                 }
@@ -502,23 +598,63 @@
             PieItem item = findItem(polar);
             boolean moved = hasMoved(evt);
             if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) {
+                mHandler.removeMessages(MSG_OPENSUBMENU);
                 // only select if we didn't just open or have moved past slop
-                mOpening = false;
                 if (moved) {
                     // switch back to swipe mode
                     mTapMode = false;
                 }
-                onEnter(item);
+                onEnterSelect(item);
+                mHandler.sendEmptyMessageDelayed(MSG_OPENSUBMENU, PIE_OPEN_SUB_DELAY);
             }
         }
         return false;
     }
 
+    private boolean pulledToCenter(PointF polarCoords) {
+        return polarCoords.y < mIconRadius - mArcOffset;
+    }
+
+    private PointF getPolar(float x, float y, boolean useOffset) {
+        PointF res = new PointF();
+        // get angle and radius from x/y
+        res.x = (float) Math.PI / 2;
+        x = x - mSliceCenter.x;
+        y = mSliceCenter.y - getLevel() * mTouchOffset - y;
+        res.y = (float) Math.sqrt(x * x + y * y);
+        if (x != 0) {
+            res.x = (float) Math.atan2(y,  x);
+            if (res.x < 0) {
+                res.x = (float) (2 * Math.PI + res.x);
+            }
+        }
+        res.y = res.y + (useOffset ? mTouchOffset : 0);
+        return res;
+    }
+
     private boolean hasMoved(MotionEvent e) {
         return mTouchSlopSquared < (e.getX() - mDown.x) * (e.getX() - mDown.x)
                 + (e.getY() - mDown.y) * (e.getY() - mDown.y);
     }
 
+    private void onEnterSelect(PieItem item) {
+        if (mCurrentItem != null) {
+            mCurrentItem.setSelected(false);
+        }
+        if (item != null && item.isEnabled()) {
+            item.setSelected(true);
+            mCurrentItem = item;
+        } else {
+            mCurrentItem = null;
+        }
+    }
+
+    private void onEnterOpen() {
+        if ((mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) {
+            openCurrentItem();
+        }
+    }
+
     /**
      * enter a slice for a view
      * updates model only
@@ -531,7 +667,7 @@
         if (item != null && item.isEnabled()) {
             item.setSelected(true);
             mCurrentItem = item;
-            if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) {
+            if ((mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) {
                 openCurrentItem();
             }
         } else {
@@ -543,22 +679,24 @@
         if (mCurrentItem != null) {
             mCurrentItem.setSelected(false);
         }
-        if (mOpenItem != null) {
-            mOpenItem = null;
+        if (hasOpenItem()) {
+            PieItem item = closeOpenItem();
+            onEnter(item);
+        } else {
+            mCurrentItem = null;
         }
-        mCurrentItem = null;
     }
 
     private void openCurrentItem() {
         if ((mCurrentItem != null) && mCurrentItem.hasItems()) {
-            mCurrentItem.setSelected(false);
-            mOpenItem = mCurrentItem;
+            mOpen.add(mCurrentItem);
             mOpening = true;
             if (mFadeIn != null) {
                 mFadeIn.cancel();
             }
             mXFade = new LinearAnimation(1, 0);
             mXFade.setDuration(PIE_XFADE_DURATION);
+            final PieItem ci = mCurrentItem;
             mXFade.setAnimationListener(new AnimationListener() {
                 @Override
                 public void onAnimationStart(Animation animation) {
@@ -567,6 +705,8 @@
                 @Override
                 public void onAnimationEnd(Animation animation) {
                     mXFade = null;
+                    ci.setSelected(false);
+                    mOpening = false;
                 }
 
                 @Override
@@ -578,30 +718,13 @@
         }
     }
 
-    private PointF getPolar(float x, float y, boolean useOffset) {
-        PointF res = new PointF();
-        // get angle and radius from x/y
-        res.x = (float) Math.PI / 2;
-        x = x - mCenter.x;
-        y = mCenter.y - y;
-        res.y = (float) Math.sqrt(x * x + y * y);
-        if (x != 0) {
-            res.x = (float) Math.atan2(y,  x);
-            if (res.x < 0) {
-                res.x = (float) (2 * Math.PI + res.x);
-            }
-        }
-        res.y = res.y + (useOffset ? mTouchOffset : 0);
-        return res;
-    }
-
     /**
      * @param polar x: angle, y: dist
      * @return the item at angle/dist or null
      */
     private PieItem findItem(PointF polar) {
         // find the matching item:
-        List<PieItem> items = (mOpenItem != null) ? mOpenItem.getItems() : mItems;
+        List<PieItem> items = getOpenItem().getItems();
         for (PieItem item : items) {
             if (inside(polar, item)) {
                 return item;
@@ -656,20 +779,6 @@
         return (int)(-60 + 120 * Math.random());
     }
 
-    @Override
-    public void layout(int l, int t, int r, int b) {
-        super.layout(l, t, r, b);
-        mCenterX = (r - l) / 2;
-        mCenterY = (b - t) / 2;
-        mFocusX = mCenterX;
-        mFocusY = mCenterY;
-        setCircle(mFocusX, mFocusY);
-        if (isVisible() && mState == STATE_PIE) {
-            setCenter(mCenterX, mCenterY);
-            layoutPie();
-        }
-    }
-
     private void setCircle(int cx, int cy) {
         mCircle.set(cx - mCircleSize, cy - mCircleSize,
                 cx + mCircleSize, cy + mCircleSize);
@@ -849,7 +958,6 @@
         }
     }
 
-
     private class LinearAnimation extends Animation {
         private float mFrom;
         private float mTo;
diff --git a/src/com/android/gallery3d/filtershow/FilterShowActivity.java b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
index 8c5d7a4..dbf26e4 100644
--- a/src/com/android/gallery3d/filtershow/FilterShowActivity.java
+++ b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
@@ -402,7 +402,15 @@
     private void setupBorders() {
         LinearLayout list = (LinearLayout) findViewById(R.id.listBorders);
         Vector<FilterRepresentation> borders = new Vector<FilterRepresentation>();
+        ImageButton borderButton = (ImageButton) findViewById(R.id.borderButton);
+
+        // The "no border" implementation
         borders.add(new FilterImageBorderRepresentation(0));
+
+        // Google-build borders
+        FiltersManager.getManager().addBorders(borders);
+
+        // Regular borders
         borders.add(new FilterImageBorderRepresentation(R.drawable.filtershow_border_4x5));
         borders.add(new FilterImageBorderRepresentation(R.drawable.filtershow_border_brush));
         borders.add(new FilterImageBorderRepresentation(R.drawable.filtershow_border_grunge));
@@ -424,7 +432,6 @@
             if (i == 0) {
                 filter.setName(getString(R.string.none));
             }
-            ImageButton borderButton = (ImageButton) findViewById(R.id.borderButton);
             FilterIconButton b = setupFilterRepresentationButton(filter, list, borderButton);
             if (i == 0) {
                 mNullBorderFilter = b;
@@ -998,17 +1005,12 @@
                         saveImage();
                     }
                 });
-                builder.setNeutralButton(R.string.exit, new DialogInterface.OnClickListener() {
+                builder.setNegativeButton(R.string.exit, new DialogInterface.OnClickListener() {
                     public void onClick(DialogInterface dialog, int id) {
                         done();
                     }
                 });
-                builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
-                    public void onClick(DialogInterface dialog, int id) {
-                    }
-                });
-
-                AlertDialog dialog = builder.show();
+                builder.show();
             }
         }
     }
diff --git a/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java b/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java
index ecfdaba..8c312a9 100644
--- a/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java
+++ b/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java
@@ -23,6 +23,7 @@
 import android.support.v8.renderscript.RenderScript;
 import android.util.Log;
 import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.filters.ImageFilterGeometry;
 import com.android.gallery3d.filtershow.filters.ImageFilterRS;
 import com.android.gallery3d.filtershow.imageshow.GeometryMetadata;
 import com.android.gallery3d.filtershow.imageshow.MasterImage;
@@ -57,6 +58,8 @@
     private volatile float mHighResPreviewScaleFactor = 1.0f;
     private volatile String mName = "";
 
+    private ImageFilterGeometry mGeometry = null;
+
     public CachingPipeline(FiltersManager filtersManager, String name) {
         mFiltersManager = filtersManager;
         mName = name;
@@ -302,17 +305,15 @@
         }
     }
 
-    public synchronized Bitmap renderGeometryIcon(Bitmap bitmap, ImagePreset preset) {
-        synchronized (CachingPipeline.class) {
-            if (getRenderScriptContext() == null) {
-                return bitmap;
-            }
-            setupEnvironment(preset, false);
-            mEnvironment.setQuality(ImagePreset.QUALITY_PREVIEW);
-            mFiltersManager.freeFilterResources(preset);
-            bitmap = preset.applyGeometry(bitmap, mEnvironment);
-            return bitmap;
+    public Bitmap renderGeometryIcon(Bitmap bitmap, ImagePreset preset) {
+        // Called by RenderRequest on the main thread
+        // TODO: change this -- we should reuse a pool of bitmaps instead...
+        if (mGeometry == null) {
+            mGeometry = new ImageFilterGeometry();
         }
+        mGeometry.useRepresentation(preset.getGeometry());
+        return mGeometry.apply(bitmap, mPreviewScaleFactor,
+                ImagePreset.QUALITY_PREVIEW);
     }
 
     public synchronized void compute(TripleBufferBitmap buffer, ImagePreset preset, int type) {
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorFlip.java b/src/com/android/gallery3d/filtershow/editors/EditorFlip.java
index c996dcb..de6240c 100644
--- a/src/com/android/gallery3d/filtershow/editors/EditorFlip.java
+++ b/src/com/android/gallery3d/filtershow/editors/EditorFlip.java
@@ -17,13 +17,18 @@
 package com.android.gallery3d.filtershow.editors;
 
 import android.content.Context;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
 import android.widget.FrameLayout;
+import android.widget.LinearLayout;
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.filtershow.imageshow.ImageFlip;
 import com.android.gallery3d.filtershow.imageshow.MasterImage;
 
 public class EditorFlip extends Editor implements EditorInfo {
+    public static final String LOGTAG = "EditorFlip";
     public static final int ID = R.id.editorFlip;
     ImageFlip mImageFlip;
 
@@ -44,6 +49,18 @@
     }
 
     @Override
+    public void openUtilityPanel(final LinearLayout accessoryViewList) {
+        final Button button = (Button) accessoryViewList.findViewById(R.id.applyEffect);
+        button.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View arg0) {
+                mImageFlip.flip();
+                mImageFlip.saveAndSetPreset();
+            }
+        });
+    }
+
+    @Override
     public int getTextId() {
         return R.string.mirror;
     }
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorRotate.java b/src/com/android/gallery3d/filtershow/editors/EditorRotate.java
index e495558..a637c08 100644
--- a/src/com/android/gallery3d/filtershow/editors/EditorRotate.java
+++ b/src/com/android/gallery3d/filtershow/editors/EditorRotate.java
@@ -17,13 +17,18 @@
 package com.android.gallery3d.filtershow.editors;
 
 import android.content.Context;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
 import android.widget.FrameLayout;
+import android.widget.LinearLayout;
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.filtershow.imageshow.ImageRotate;
 import com.android.gallery3d.filtershow.imageshow.MasterImage;
 
 public class EditorRotate extends Editor implements EditorInfo {
+    public static final String LOGTAG = "EditorRotate";
     public static final int ID = R.id.editorRotate;
     ImageRotate mImageRotate;
 
@@ -44,6 +49,19 @@
     }
 
     @Override
+    public void openUtilityPanel(final LinearLayout accessoryViewList) {
+        final Button button = (Button) accessoryViewList.findViewById(R.id.applyEffect);
+        button.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View arg0) {
+                mImageRotate.rotate();
+                button.setText(mContext.getString(getTextId()) + " " + mImageRotate.getLocalValue());
+                mImageRotate.saveAndSetPreset();
+            }
+        });
+    }
+
+    @Override
     public int getTextId() {
         return R.string.rotate;
     }
diff --git a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
index b5c4de0..232de64 100644
--- a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
+++ b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
@@ -108,6 +108,10 @@
         filters.add(ImageFilterGeometry.class);
     }
 
+    public void addBorders(Vector<FilterRepresentation> representations) {
+        // Override
+    }
+
     public void addLooks(Vector<FilterRepresentation> representations) {
         // Override
     }
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageFlip.java b/src/com/android/gallery3d/filtershow/imageshow/ImageFlip.java
index 70637a3..15197b0 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageFlip.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageFlip.java
@@ -58,6 +58,22 @@
         return (rot / 90) % 2 != 0;
     }
 
+    public void flip() {
+        FLIP flip = getLocalFlip();
+        boolean next = true;
+        // Picks next flip in order from enum FLIP (wrapping)
+        for (FLIP f : FLIP.values()) {
+            if (next) {
+                mNextFlip = f;
+                next = false;
+            }
+            if (f.equals(flip)) {
+                next = true;
+            }
+        }
+        setLocalFlip(mNextFlip);
+    }
+
     @Override
     protected void setActionMove(float x, float y) {
         super.setActionMove(x, y);
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java b/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java
index c4b9aa2..ab8023e 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java
@@ -54,6 +54,13 @@
         mAngle = (mBaseAngle - angle) % 360;
     }
 
+    public void rotate() {
+        mAngle += 90;
+        mAngle = snappedAngle(mAngle);
+        mAngle %= 360;
+        setLocalRotation(mAngle);
+    }
+
     @Override
     protected void setActionDown(float x, float y) {
         super.setActionDown(x, y);
@@ -76,7 +83,7 @@
     }
 
     @Override
-    protected int getLocalValue() {
+    public int getLocalValue() {
         return constrainedRotation(snappedAngle(getLocalRotation()));
     }
 
diff --git a/src/com/android/gallery3d/filtershow/presets/ImagePreset.java b/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
index b2dd6b7..a35a18a 100644
--- a/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
+++ b/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
@@ -117,15 +117,24 @@
         if (filterRepresentation == null) {
             return null;
         }
-        int position = getPositionForRepresentation(filterRepresentation);
-        if (position == -1) {
-            return null;
-        }
         FilterRepresentation representation = null;
-        try {
-            representation = mFilters.elementAt(position).clone();
-        } catch (CloneNotSupportedException e) {
-            e.printStackTrace();
+        if ((mBorder != null)
+                && (mBorder.getFilterClass() == filterRepresentation.getFilterClass())) {
+            // TODO: instead of special casing for border, we should correctly implements "filters priority set"
+            representation = mBorder;
+        } else {
+            int position = getPositionForRepresentation(filterRepresentation);
+            if (position == -1) {
+                return null;
+            }
+            representation = mFilters.elementAt(position);
+        }
+        if (representation != null) {
+            try {
+                representation = representation.clone();
+            } catch (CloneNotSupportedException e) {
+                e.printStackTrace();
+            }
         }
         return representation;
     }
@@ -138,9 +147,17 @@
             if (representation instanceof GeometryMetadata) {
                 setGeometry((GeometryMetadata) representation);
             } else {
-                int position = getPositionForRepresentation(representation);
-                FilterRepresentation old = mFilters.elementAt(position);
-                old.updateTempParametersFrom(representation);
+                if ((mBorder != null)
+                        && (mBorder.getFilterClass() == representation.getFilterClass())) {
+                    mBorder.updateTempParametersFrom(representation);
+                } else {
+                    int position = getPositionForRepresentation(representation);
+                    if (position == -1) {
+                        return;
+                    }
+                    FilterRepresentation old = mFilters.elementAt(position);
+                    old.updateTempParametersFrom(representation);
+                }
             }
         }
         MasterImage.getImage().invalidatePreview();
diff --git a/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java b/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java
index 5e1fb34..30868c2 100644
--- a/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java
+++ b/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java
@@ -38,16 +38,28 @@
 
     public static Bitmap getThumbnail(MtpDevice device, MtpObjectInfo info) {
         byte[] imageBytes = device.getThumbnail(info.getObjectHandle());
-        if (imageBytes == null) return null;
+        if (imageBytes == null) {
+            return null;
+        }
         BitmapFactory.Options o = new BitmapFactory.Options();
         o.inJustDecodeBounds = true;
         BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
-        if (o.outWidth == 0 || o.outHeight == 0) return null;
+        if (o.outWidth == 0 || o.outHeight == 0) {
+            return null;
+        }
         o.inBitmap = GalleryBitmapPool.getInstance().get(o.outWidth, o.outHeight);
         o.inMutable = true;
         o.inJustDecodeBounds = false;
         o.inSampleSize = 1;
-        return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
+        try {
+            return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
+        } catch (IllegalArgumentException e) {
+            // BitmapFactory throws an exception rather than returning null
+            // when image decoding fails and an existing bitmap was supplied
+            // for recycling, even if the failure was not caused by the use
+            // of that bitmap.
+            return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
+        }
     }
 
     public static BitmapWithMetadata getFullsize(MtpDevice device, MtpObjectInfo info) {
@@ -56,7 +68,9 @@
 
     public static BitmapWithMetadata getFullsize(MtpDevice device, MtpObjectInfo info, int maxSide) {
         byte[] imageBytes = device.getObject(info.getObjectHandle(), info.getCompressedSize());
-        if (imageBytes == null) return null;
+        if (imageBytes == null) {
+            return null;
+        }
         Bitmap created;
         if (maxSide > 0) {
             BitmapFactory.Options o = new BitmapFactory.Options();
@@ -76,7 +90,9 @@
         } else {
             created = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
         }
-        if (created == null) return null;
+        if (created == null) {
+            return null;
+        }
 
         return new BitmapWithMetadata(created, Exif.getOrientation(imageBytes));
     }
diff --git a/src/com/android/photos/data/GalleryBitmapPool.java b/src/com/android/photos/data/GalleryBitmapPool.java
index 4c3279f..a5a17ed 100644
--- a/src/com/android/photos/data/GalleryBitmapPool.java
+++ b/src/com/android/photos/data/GalleryBitmapPool.java
@@ -106,7 +106,7 @@
     }
 
     public boolean put(Bitmap b) {
-        if (b == null) {
+        if (b == null || b.getConfig() != Bitmap.Config.ARGB_8888) {
             return false;
         }
         SparseArrayBitmapPool pool = getPoolForDimensions(b.getWidth(), b.getHeight());