Merge "Make original exif test pass with the exiftool ground truth" into gb-ub-photos-bryce
diff --git a/gallerycommon/src/com/android/gallery3d/common/Utils.java b/gallerycommon/src/com/android/gallery3d/common/Utils.java
index f5a2667..3a68745 100644
--- a/gallerycommon/src/com/android/gallery3d/common/Utils.java
+++ b/gallerycommon/src/com/android/gallery3d/common/Utils.java
@@ -76,7 +76,7 @@
     // Throws IllegalArgumentException if the input is <= 0 or
     // the answer overflows.
     public static int nextPowerOf2(int n) {
-        if (n <= 0 || n > (1 << 30)) throw new IllegalArgumentException();
+        if (n <= 0 || n > (1 << 30)) throw new IllegalArgumentException("n is invalid: " + n);
         n -= 1;
         n |= n >> 16;
         n |= n >> 8;
diff --git a/src/com/android/gallery3d/anim/StateTransitionAnimation.java b/src/com/android/gallery3d/anim/StateTransitionAnimation.java
new file mode 100644
index 0000000..d4d59d3
--- /dev/null
+++ b/src/com/android/gallery3d/anim/StateTransitionAnimation.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.anim;
+
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.gallery3d.ui.GLCanvas;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.RawTexture;
+import com.android.gallery3d.ui.TiledScreenNail;
+
+public class StateTransitionAnimation extends Animation {
+
+    public static class Spec {
+        public static final Spec OUTGOING;
+        public static final Spec INCOMING;
+
+        public int duration = 330;
+        public float backgroundAlphaFrom = 0;
+        public float backgroundAlphaTo = 0;
+        public float backgroundScaleFrom = 0;
+        public float backgroundScaleTo = 0;
+        public float contentAlphaFrom = 1;
+        public float contentAlphaTo = 1;
+        public float contentScaleFrom = 1;
+        public float contentScaleTo = 1;
+        public float overlayAlphaFrom = 0;
+        public float overlayAlphaTo = 0;
+        public float overlayScaleFrom = 0;
+        public float overlayScaleTo = 0;
+        public Interpolator interpolator;
+
+        static {
+            OUTGOING = new Spec();
+            OUTGOING.backgroundAlphaFrom = 1f;
+            OUTGOING.backgroundAlphaTo = 0f;
+            OUTGOING.backgroundScaleFrom = 1f;
+            OUTGOING.backgroundScaleTo = 0f;
+            OUTGOING.contentAlphaFrom = 0.9f;
+            OUTGOING.contentAlphaTo = 1f;
+            OUTGOING.contentScaleFrom = 3f;
+            OUTGOING.contentScaleTo = 1f;
+            OUTGOING.interpolator = new DecelerateInterpolator();
+
+            INCOMING = new Spec();
+            INCOMING.overlayAlphaFrom = 1f;
+            INCOMING.overlayAlphaTo = 0f;
+            INCOMING.overlayScaleFrom = 1f;
+            INCOMING.overlayScaleTo = 3f;
+            INCOMING.contentAlphaFrom = 0f;
+            INCOMING.contentAlphaTo = 1f;
+            INCOMING.contentScaleFrom = 0.25f;
+            INCOMING.contentScaleTo = 1f;
+            INCOMING.interpolator = new DecelerateInterpolator();
+        }
+    }
+
+    private final Spec mTransitionSpec;
+    private float mCurrentContentScale;
+    private float mCurrentContentAlpha;
+    private float mCurrentBackgroundScale;
+    private float mCurrentBackgroundAlpha;
+    private float mCurrentOverlayScale;
+    private float mCurrentOverlayAlpha;
+    private RawTexture mOldScreenTexture;
+
+    public StateTransitionAnimation(Spec spec, RawTexture oldScreen) {
+        mTransitionSpec = spec != null ? spec : Spec.OUTGOING;
+        setDuration(mTransitionSpec.duration);
+        setInterpolator(mTransitionSpec.interpolator);
+        mOldScreenTexture = oldScreen;
+        if (mOldScreenTexture != null) {
+            TiledScreenNail.disableDrawPlaceholder();
+        }
+    }
+
+    @Override
+    public boolean calculate(long currentTimeMillis) {
+        boolean retval = super.calculate(currentTimeMillis);
+        if (mOldScreenTexture != null && !isActive()) {
+            mOldScreenTexture.recycle();
+            mOldScreenTexture = null;
+            TiledScreenNail.enableDrawPlaceholder();
+        }
+        return retval;
+    }
+
+    @Override
+    protected void onCalculate(float progress) {
+        mCurrentContentScale = mTransitionSpec.contentScaleFrom
+                + (mTransitionSpec.contentScaleTo - mTransitionSpec.contentScaleFrom) * progress;
+        mCurrentContentAlpha = mTransitionSpec.contentAlphaFrom
+                + (mTransitionSpec.contentAlphaTo - mTransitionSpec.contentAlphaFrom) * progress;
+        mCurrentBackgroundAlpha = mTransitionSpec.backgroundAlphaFrom
+                + (mTransitionSpec.backgroundAlphaTo - mTransitionSpec.backgroundAlphaFrom)
+                * progress;
+        mCurrentBackgroundScale = mTransitionSpec.backgroundScaleFrom
+                + (mTransitionSpec.backgroundScaleTo - mTransitionSpec.backgroundScaleFrom)
+                * progress;
+        mCurrentOverlayScale = mTransitionSpec.overlayScaleFrom
+                + (mTransitionSpec.overlayScaleTo - mTransitionSpec.overlayScaleFrom) * progress;
+        mCurrentOverlayAlpha = mTransitionSpec.overlayAlphaFrom
+                + (mTransitionSpec.overlayAlphaTo - mTransitionSpec.overlayAlphaFrom) * progress;
+    }
+
+    private void applyOldTexture(GLView view, GLCanvas canvas, float alpha, float scale, boolean clear) {
+        if (mOldScreenTexture == null)
+            return;
+        if (clear) canvas.clearBuffer(view.getBackgroundColor());
+        canvas.save();
+        canvas.setAlpha(alpha);
+        int xOffset = view.getWidth() / 2;
+        int yOffset = view.getHeight() / 2;
+        canvas.translate(xOffset, yOffset);
+        canvas.scale(scale, scale, 1);
+        mOldScreenTexture.draw(canvas, -xOffset, -yOffset);
+        canvas.restore();
+    }
+
+    public void applyBackground(GLView view, GLCanvas canvas) {
+        if (mCurrentBackgroundAlpha > 0f) {
+            applyOldTexture(view, canvas, mCurrentBackgroundAlpha, mCurrentBackgroundScale, true);
+        }
+    }
+
+    public void applyContentTransform(GLView view, GLCanvas canvas) {
+        int xOffset = view.getWidth() / 2;
+        int yOffset = view.getHeight() / 2;
+        canvas.translate(xOffset, yOffset);
+        canvas.scale(mCurrentContentScale, mCurrentContentScale, 1);
+        canvas.translate(-xOffset, -yOffset);
+        canvas.setAlpha(mCurrentContentAlpha);
+    }
+
+    public void applyOverlay(GLView view, GLCanvas canvas) {
+        if (mCurrentOverlayAlpha > 0f) {
+            applyOldTexture(view, canvas, mCurrentOverlayAlpha, mCurrentOverlayScale, false);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/app/AbstractGalleryActivity.java b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
index cb3aa9d..76649ca 100644
--- a/src/com/android/gallery3d/app/AbstractGalleryActivity.java
+++ b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
@@ -41,6 +41,7 @@
 import com.android.gallery3d.ui.GLRoot;
 import com.android.gallery3d.ui.GLRootView;
 import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaViewHelper;
 
 public class AbstractGalleryActivity extends Activity implements GalleryContext {
     @SuppressWarnings("unused")
@@ -51,6 +52,7 @@
     private OrientationManager mOrientationManager;
     private TransitionStore mTransitionStore = new TransitionStore();
     private boolean mDisableToggleStatusBar;
+    private PanoramaViewHelper mPanoramaViewHelper;
 
     private AlertDialog mAlertDialog = null;
     private BroadcastReceiver mMountReceiver = new BroadcastReceiver() {
@@ -67,6 +69,8 @@
         mOrientationManager = new OrientationManager(this);
         toggleStatusBarByOrientation();
         getWindow().setBackgroundDrawable(null);
+        mPanoramaViewHelper = new PanoramaViewHelper(this);
+        mPanoramaViewHelper.onCreate();
     }
 
     @Override
@@ -168,6 +172,7 @@
             mAlertDialog = builder.show();
             registerReceiver(mMountReceiver, mMountFilter);
         }
+        mPanoramaViewHelper.onStart();
     }
 
     @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
@@ -184,6 +189,7 @@
             mAlertDialog.dismiss();
             mAlertDialog = null;
         }
+        mPanoramaViewHelper.onStop();
     }
 
     @Override
@@ -293,4 +299,8 @@
     public TransitionStore getTransitionStore() {
         return mTransitionStore;
     }
+
+    public PanoramaViewHelper getPanoramaViewHelper() {
+        return mPanoramaViewHelper;
+    }
 }
diff --git a/src/com/android/gallery3d/app/ActivityState.java b/src/com/android/gallery3d/app/ActivityState.java
index 75327e4..ff0b32c 100644
--- a/src/com/android/gallery3d/app/ActivityState.java
+++ b/src/com/android/gallery3d/app/ActivityState.java
@@ -35,7 +35,10 @@
 import android.view.WindowManager;
 
 import com.android.gallery3d.R;
+import com.android.gallery3d.anim.StateTransitionAnimation;
 import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.PreparePageFadeoutTexture;
+import com.android.gallery3d.ui.RawTexture;
 import com.android.gallery3d.util.GalleryUtils;
 
 abstract public class ActivityState {
@@ -66,11 +69,24 @@
     private boolean mPlugged = false;
     boolean mIsFinishing = false;
 
+    private static final String KEY_TRANSITION_IN = "transition-in";
+
+    public static enum StateTransition { None, Outgoing, Incoming };
+    private StateTransition mNextTransition = StateTransition.None;
+    private StateTransitionAnimation mIntroAnimation;
+    private GLView mContentPane;
+
     protected ActivityState() {
     }
 
     protected void setContentPane(GLView content) {
-        mActivity.getGLRoot().setContentPane(content);
+        mContentPane = content;
+        if (mNextTransition != StateTransition.None) {
+            mContentPane.setIntroAnimation(mIntroAnimation);
+            mIntroAnimation = null;
+        }
+        mContentPane.setBackgroundColor(getBackgroundColor());
+        mActivity.getGLRoot().setContentPane(mContentPane);
     }
 
     void initialize(AbstractGalleryActivity activity, Bundle data) {
@@ -157,10 +173,26 @@
         win.setAttributes(params);
     }
 
+    protected void transitionOnNextPause(Class<? extends ActivityState> outgoing,
+            Class<? extends ActivityState> incoming, StateTransition hint) {
+        if (outgoing == PhotoPage.class && incoming == AlbumPage.class) {
+            mNextTransition = StateTransition.Outgoing;
+        } else if (outgoing == AlbumPage.class && incoming == PhotoPage.class) {
+            mNextTransition = StateTransition.Incoming;
+        } else {
+            mNextTransition = hint;
+        }
+    }
+
     protected void onPause() {
         if (0 != (mFlags & FLAG_SCREEN_ON_WHEN_PLUGGED)) {
             ((Activity) mActivity).unregisterReceiver(mPowerIntentReceiver);
         }
+        if (mNextTransition != StateTransition.None) {
+            mActivity.getTransitionStore().put(KEY_TRANSITION_IN, mNextTransition);
+            PreparePageFadeoutTexture.prepareFadeOutTexture(mActivity, mContentPane);
+            mNextTransition = StateTransition.None;
+        }
     }
 
     // should only be called by StateManager
@@ -214,6 +246,16 @@
 
     // a subclass of ActivityState should override the method to resume itself
     protected void onResume() {
+        RawTexture fade = mActivity.getTransitionStore().get(
+                PreparePageFadeoutTexture.KEY_FADE_TEXTURE);
+        mNextTransition = mActivity.getTransitionStore().get(
+                KEY_TRANSITION_IN, StateTransition.None);
+        if (mNextTransition != StateTransition.None) {
+            mIntroAnimation = new StateTransitionAnimation(
+                    (mNextTransition == StateTransition.Incoming) ?
+                            StateTransitionAnimation.Spec.INCOMING :
+                            StateTransitionAnimation.Spec.OUTGOING, fade);
+        }
     }
 
     protected boolean onCreateActionBar(Menu menu) {
diff --git a/src/com/android/gallery3d/app/AlbumPage.java b/src/com/android/gallery3d/app/AlbumPage.java
index c956bc9..4cf73a8 100644
--- a/src/com/android/gallery3d/app/AlbumPage.java
+++ b/src/com/android/gallery3d/app/AlbumPage.java
@@ -47,7 +47,6 @@
 import com.android.gallery3d.ui.GLRoot;
 import com.android.gallery3d.ui.GLView;
 import com.android.gallery3d.ui.PhotoFallbackEffect;
-import com.android.gallery3d.ui.PreparePageFadeoutTexture;
 import com.android.gallery3d.ui.RelativePosition;
 import com.android.gallery3d.ui.SelectionManager;
 import com.android.gallery3d.ui.SlotView;
@@ -139,11 +138,6 @@
         private final float mMatrix[] = new float[16];
 
         @Override
-        protected void renderBackground(GLCanvas view) {
-            view.clearBuffer(getBackgroundColor());
-        }
-
-        @Override
         protected void onLayout(
                 boolean changed, int left, int top, int right, int bottom) {
 
@@ -254,8 +248,6 @@
         } else {
             // Render transition in pressed state
             mAlbumView.setPressedIndex(slotIndex);
-            PreparePageFadeoutTexture.prepareFadeOutTexture(mActivity, mRootPane);
-            mAlbumView.setPressedIndex(-1);
 
             pickPhoto(slotIndex);
         }
@@ -300,8 +292,12 @@
             data.putBoolean(PhotoPage.KEY_START_IN_FILMSTRIP,
                     startInFilmstrip);
             data.putBoolean(PhotoPage.KEY_IN_CAMERA_ROLL, mMediaSet.isCameraRoll());
-            mActivity.getStateManager().startStateForResult(
-                    PhotoPage.class, REQUEST_PHOTO, data);
+            if (startInFilmstrip) {
+                mActivity.getStateManager().switchState(this, PhotoPage.class, data);
+            } else {
+                mActivity.getStateManager().startStateForResult(
+                            PhotoPage.class, REQUEST_PHOTO, data);
+            }
         }
     }
 
@@ -320,8 +316,9 @@
             activity.startActivity(intent);
             activity.finish();
         } else {
-            activity.setResult(Activity.RESULT_OK,
-                    new Intent(null, item.getContentUri()));
+            Intent intent = new Intent(null, item.getContentUri())
+                .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+            activity.setResult(Activity.RESULT_OK, intent);
             activity.finish();
         }
     }
@@ -373,15 +370,6 @@
         mLaunchedFromPhotoPage =
                 mActivity.getStateManager().hasStateClass(PhotoPage.class);
         mInCameraApp = data.getBoolean(PhotoPage.KEY_APP_BRIDGE, false);
-
-        // Don't show animation if it is restored or switched from filmstrip
-        if (!mLaunchedFromPhotoPage && restoreState == null && data != null) {
-            int[] center = data.getIntArray(KEY_SET_CENTER);
-            if (center != null) {
-                mOpenCenter.setAbsolutePosition(center[0], center[1]);
-                mSlotView.startScatteringAnimation(mOpenCenter);
-            }
-        }
     }
 
     @Override
@@ -411,6 +399,7 @@
         mAlbumDataAdapter.resume();
 
         mAlbumView.resume();
+        mAlbumView.setPressedIndex(-1);
         mActionModeHandler.resume();
         if (!mInitialSynced) {
             setLoadingBit(BIT_LOADING_SYNC);
@@ -561,7 +550,6 @@
         if (mAlbumDataAdapter == null || !mAlbumDataAdapter.isActive(slotIndex)) return;
         MediaItem item = mAlbumDataAdapter.get(slotIndex);
         if (item == null) return;
-        PreparePageFadeoutTexture.prepareFadeOutTexture(mActivity, mRootPane);
         TransitionStore transitions = mActivity.getTransitionStore();
         transitions.put(PhotoPage.KEY_INDEX_HINT, slotIndex);
         transitions.put(PhotoPage.KEY_OPEN_ANIMATION_RECT,
diff --git a/src/com/android/gallery3d/app/AlbumSetPage.java b/src/com/android/gallery3d/app/AlbumSetPage.java
index 1719b89..49ab683 100644
--- a/src/com/android/gallery3d/app/AlbumSetPage.java
+++ b/src/com/android/gallery3d/app/AlbumSetPage.java
@@ -52,7 +52,6 @@
 import com.android.gallery3d.ui.GLCanvas;
 import com.android.gallery3d.ui.GLRoot;
 import com.android.gallery3d.ui.GLView;
-import com.android.gallery3d.ui.PreparePageFadeoutTexture;
 import com.android.gallery3d.ui.SelectionManager;
 import com.android.gallery3d.ui.SlotView;
 import com.android.gallery3d.ui.SynchronizedHandler;
@@ -130,11 +129,6 @@
         private final float mMatrix[] = new float[16];
 
         @Override
-        protected void renderBackground(GLCanvas view) {
-            view.clearBuffer(getBackgroundColor());
-        }
-
-        @Override
         protected void onLayout(
                 boolean changed, int left, int top, int right, int bottom) {
             mEyePosition.resetPosition();
@@ -273,7 +267,6 @@
                     & MediaObject.SUPPORT_IMPORT) != 0) {
                 data.putBoolean(AlbumPage.KEY_AUTO_SELECT_ALL, true);
             } else if (!mGetContent && albumShouldOpenInFilmstrip(targetSet)) {
-                PreparePageFadeoutTexture.prepareFadeOutTexture(mActivity, mRootPane);
                 data.putParcelable(PhotoPage.KEY_OPEN_ANIMATION_RECT,
                         mSlotView.getSlotRect(slotIndex, mRootPane));
                 data.putInt(PhotoPage.KEY_INDEX_HINT, 0);
@@ -365,6 +358,7 @@
     }
 
     private boolean setupCameraButton() {
+        if (!GalleryUtils.isCameraAvailable(mActivity)) return false;
         RelativeLayout galleryRoot = (RelativeLayout) ((Activity) mActivity)
                 .findViewById(R.id.gallery_root);
         if (galleryRoot == null) return false;
diff --git a/src/com/android/gallery3d/app/CropImage.java b/src/com/android/gallery3d/app/CropImage.java
index 2b74504..89ca63d 100644
--- a/src/com/android/gallery3d/app/CropImage.java
+++ b/src/com/android/gallery3d/app/CropImage.java
@@ -61,6 +61,7 @@
 import com.android.gallery3d.exif.ExifReader;
 import com.android.gallery3d.exif.ExifTag;
 import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.ui.BitmapScreenNail;
 import com.android.gallery3d.ui.BitmapTileProvider;
 import com.android.gallery3d.ui.CropView;
 import com.android.gallery3d.ui.GLRoot;
@@ -151,6 +152,7 @@
     private BitmapRegionDecoder mRegionDecoder;
     private Bitmap mBitmapInIntent;
     private boolean mUseRegionDecoder = false;
+    private BitmapScreenNail mBitmapScreenNail;
 
     private ProgressDialog mProgressDialog;
     private Future<BitmapRegionDecoder> mLoadTask;
@@ -813,8 +815,14 @@
                 BitmapUtils.UNCONSTRAINED, BACKUP_PIXEL_COUNT);
         mBitmap = regionDecoder.decodeRegion(
                 new Rect(0, 0, width, height), options);
-        mCropView.setDataModel(new TileImageViewAdapter(
-                mBitmap, regionDecoder), mMediaItem.getFullImageRotation());
+
+        mBitmapScreenNail = new BitmapScreenNail(mBitmap);
+
+        TileImageViewAdapter adapter = new TileImageViewAdapter();
+        adapter.setScreenNail(mBitmapScreenNail, width, height);
+        adapter.setRegionDecoder(regionDecoder);
+
+        mCropView.setDataModel(adapter, mMediaItem.getFullImageRotation());
         if (mDoFaceDetection) {
             mCropView.detectFaces(mBitmap);
         } else {
@@ -976,6 +984,15 @@
         }
     }
 
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (mBitmapScreenNail != null) {
+            mBitmapScreenNail.recycle();
+            mBitmapScreenNail = null;
+        }
+    }
+
     private void dismissProgressDialogIfShown() {
         if (mProgressDialog != null) {
             mProgressDialog.dismiss();
diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java
index 66f2874..20f15be 100644
--- a/src/com/android/gallery3d/app/PhotoDataAdapter.java
+++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java
@@ -30,11 +30,12 @@
 import com.android.gallery3d.data.MediaObject;
 import com.android.gallery3d.data.MediaSet;
 import com.android.gallery3d.data.Path;
-import com.android.gallery3d.ui.BitmapScreenNail;
 import com.android.gallery3d.ui.PhotoView;
 import com.android.gallery3d.ui.ScreenNail;
 import com.android.gallery3d.ui.SynchronizedHandler;
 import com.android.gallery3d.ui.TileImageViewAdapter;
+import com.android.gallery3d.ui.TiledScreenNail;
+import com.android.gallery3d.ui.TiledTexture;
 import com.android.gallery3d.util.Future;
 import com.android.gallery3d.util.FutureListener;
 import com.android.gallery3d.util.MediaSetUtils;
@@ -59,8 +60,8 @@
     private static final int MSG_RUN_OBJECT = 3;
     private static final int MSG_UPDATE_IMAGE_REQUESTS = 4;
 
-    private static final int MIN_LOAD_COUNT = 8;
-    private static final int DATA_CACHE_SIZE = 32;
+    private static final int MIN_LOAD_COUNT = 16;
+    private static final int DATA_CACHE_SIZE = 256;
     private static final int SCREEN_NAIL_MAX = PhotoView.SCREEN_NAIL_MAX;
     private static final int IMAGE_CACHE_SIZE = 2 * SCREEN_NAIL_MAX + 1;
 
@@ -162,6 +163,7 @@
     private DataListener mDataListener;
 
     private final SourceListener mSourceListener = new SourceListener();
+    private final TiledTexture.Uploader mUploader;
 
     // The path of the current viewing item will be stored in mItemPath.
     // If mItemPath is not null, mCurrentIndex is only a hint for where we
@@ -183,6 +185,8 @@
 
         Arrays.fill(mChanges, MediaObject.INVALID_DATA_VERSION);
 
+        mUploader = new TiledTexture.Uploader(activity.getGLRoot());
+
         mMainHandler = new SynchronizedHandler(activity.getGLRoot()) {
             @SuppressWarnings("unchecked")
             @Override
@@ -301,8 +305,8 @@
         entry.screenNailTask = null;
 
         // Combine the ScreenNails if we already have a BitmapScreenNail
-        if (entry.screenNail instanceof BitmapScreenNail) {
-            BitmapScreenNail original = (BitmapScreenNail) entry.screenNail;
+        if (entry.screenNail instanceof TiledScreenNail) {
+            TiledScreenNail original = (TiledScreenNail) entry.screenNail;
             screenNail = original.combine(screenNail);
         }
 
@@ -321,6 +325,7 @@
             }
         }
         updateImageRequests();
+        updateScreenNailUploadQueue();
     }
 
     private void updateFullImage(Path path, Future<BitmapRegionDecoder> future) {
@@ -345,6 +350,8 @@
     @Override
     public void resume() {
         mIsActive = true;
+        TiledTexture.prepareResources();
+
         mSource.addContentListener(mSourceListener);
         updateImageCache();
         updateImageRequests();
@@ -371,6 +378,9 @@
         }
         mImageCache.clear();
         mTileProvider.clear();
+
+        mUploader.clear();
+        TiledTexture.freeResources();
     }
 
     private MediaItem getItem(int index) {
@@ -402,6 +412,32 @@
         fireDataChange();
     }
 
+    private void uploadScreenNail(int offset) {
+        int index = mCurrentIndex + offset;
+        if (index < mActiveStart || index >= mActiveEnd) return;
+
+        MediaItem item = getItem(index);
+        if (item == null) return;
+
+        ImageEntry e = mImageCache.get(item.getPath());
+        if (e == null) return;
+
+        ScreenNail s = e.screenNail;
+        if (s instanceof TiledScreenNail) {
+            TiledTexture t = ((TiledScreenNail) s).getTexture();
+            if (t != null && !t.isReady()) mUploader.addTexture(t);
+        }
+    }
+
+    private void updateScreenNailUploadQueue() {
+        mUploader.clear();
+        uploadScreenNail(0);
+        for (int i = 1; i < IMAGE_CACHE_SIZE; ++i) {
+            uploadScreenNail(i);
+            uploadScreenNail(-i);
+        }
+    }
+
     @Override
     public void moveTo(int index) {
         updateCurrentIndex(index);
@@ -680,7 +716,7 @@
                 bitmap = BitmapUtils.rotateBitmap(bitmap,
                     mItem.getRotation() - mItem.getFullImageRotation(), true);
             }
-            return bitmap == null ? null : new BitmapScreenNail(bitmap);
+            return bitmap == null ? null : new TiledScreenNail(bitmap);
         }
     }
 
@@ -729,7 +765,7 @@
     private ScreenNail newPlaceholderScreenNail(MediaItem item) {
         int width = item.getWidth();
         int height = item.getHeight();
-        return new BitmapScreenNail(width, height);
+        return new TiledScreenNail(width, height);
     }
 
     // Returns the task if we started the task or the task is already started.
@@ -791,8 +827,8 @@
                 if (entry.requestedScreenNail != item.getDataVersion()) {
                     // This ScreenNail is outdated, we want to update it if it's
                     // still a placeholder.
-                    if (entry.screenNail instanceof BitmapScreenNail) {
-                        BitmapScreenNail s = (BitmapScreenNail) entry.screenNail;
+                    if (entry.screenNail instanceof TiledScreenNail) {
+                        TiledScreenNail s = (TiledScreenNail) entry.screenNail;
                         s.updatePlaceholderSize(
                                 item.getWidth(), item.getHeight());
                     }
@@ -810,6 +846,8 @@
             if (entry.screenNailTask != null) entry.screenNailTask.cancel();
             if (entry.screenNail != null) entry.screenNail.recycle();
         }
+
+        updateScreenNailUploadQueue();
     }
 
     private class FullImageListener
diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java
index db7f3d9..ecb4832 100644
--- a/src/com/android/gallery3d/app/PhotoPage.java
+++ b/src/com/android/gallery3d/app/PhotoPage.java
@@ -34,12 +34,10 @@
 import android.os.SystemClock;
 import android.view.Menu;
 import android.view.MenuItem;
-import android.view.animation.AccelerateInterpolator;
 import android.widget.RelativeLayout;
 import android.widget.Toast;
 
 import com.android.gallery3d.R;
-import com.android.gallery3d.anim.FloatAnimation;
 import com.android.gallery3d.common.ApiHelper;
 import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.data.ComboAlbum;
@@ -59,9 +57,9 @@
 import com.android.gallery3d.data.SnailAlbum;
 import com.android.gallery3d.data.SnailItem;
 import com.android.gallery3d.data.SnailSource;
+import com.android.gallery3d.filtershow.FilterShowActivity;
 import com.android.gallery3d.picasasource.PicasaSource;
 import com.android.gallery3d.ui.AnimationTime;
-import com.android.gallery3d.ui.BitmapScreenNail;
 import com.android.gallery3d.ui.DetailsHelper;
 import com.android.gallery3d.ui.DetailsHelper.CloseListener;
 import com.android.gallery3d.ui.DetailsHelper.DetailsSource;
@@ -73,12 +71,10 @@
 import com.android.gallery3d.ui.MenuExecutor;
 import com.android.gallery3d.ui.PhotoFallbackEffect;
 import com.android.gallery3d.ui.PhotoView;
-import com.android.gallery3d.ui.PreparePageFadeoutTexture;
-import com.android.gallery3d.ui.RawTexture;
 import com.android.gallery3d.ui.SelectionManager;
 import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.ui.TiledScreenNail;
 import com.android.gallery3d.util.GalleryUtils;
-import com.android.gallery3d.util.LightCycleHelper;
 
 public class PhotoPage extends ActivityState implements
         PhotoView.Listener, OrientationManager.Listener, AppBridge.Server,
@@ -94,8 +90,9 @@
     private static final int MSG_ON_CAMERA_CENTER = 9;
     private static final int MSG_ON_PICTURE_CENTER = 10;
     private static final int MSG_REFRESH_IMAGE = 11;
-    private static final int MSG_UPDATE_DEFERRED = 12;
+    private static final int MSG_UPDATE_PHOTO_UI = 12;
     private static final int MSG_UPDATE_PROGRESS = 13;
+    private static final int MSG_UPDATE_DEFERRED = 14;
 
     private static final int HIDE_BARS_TIMEOUT = 3500;
     private static final int UNFREEZE_GLROOT_TIMEOUT = 250;
@@ -165,7 +162,6 @@
     private boolean mTreatBackAsUp;
     private boolean mStartInFilmstrip;
     private boolean mInCameraRoll;
-    private boolean mStartedFromAlbumPage;
     private boolean mRecenterCameraOnResume = true;
 
     private long mCameraSwitchCutoff = 0;
@@ -176,9 +172,7 @@
     private boolean mDeferredUpdateWaiting = false;
     private long mDeferUpdateUntil = Long.MAX_VALUE;
 
-    private RawTexture mFadeOutTexture;
     private Rect mOpenAnimationRect;
-    public static final int ANIM_TIME_OPENING = 300;
 
     // The item that is deleted (but it can still be undeleted before commiting)
     private Path mDeletePath;
@@ -193,8 +187,15 @@
     private SupportedOperationsListener mSupportedOperationsListener =
         new SupportedOperationsListener() {
             @Override
-            public void onChange(int operations) {
-                mHandler.sendEmptyMessage(MSG_REFRESH_IMAGE);
+            public void onChange(MediaObject item, int operations) {
+                if (item == mCurrentPhoto) {
+                    if (mPhotoView.getFilmMode()
+                            && SystemClock.uptimeMillis() < mDeferUpdateUntil) {
+                        requestDeferredUpdate();
+                    } else {
+                        mHandler.sendEmptyMessage(MSG_UPDATE_PHOTO_UI);
+                    }
+                }
             }
         };
 
@@ -213,13 +214,6 @@
         }
     }
 
-    private static class BackgroundFadeOut extends FloatAnimation {
-        public BackgroundFadeOut() {
-            super(1f, 0f, ANIM_TIME_OPENING);
-            setInterpolator(new AccelerateInterpolator(2f));
-        }
-    }
-
     private class UpdateProgressListener implements StitchingChangeListener {
 
         @Override
@@ -246,8 +240,6 @@
         }
     };
 
-    private final FloatAnimation mBackgroundFade = new BackgroundFadeOut();
-
     @Override
     protected int getBackgroundColorId() {
         return R.color.photo_background;
@@ -255,28 +247,6 @@
 
     private final GLView mRootPane = new GLView() {
         @Override
-        protected void renderBackground(GLCanvas view) {
-            if (mFadeOutTexture != null) {
-                if (mBackgroundFade.calculate(AnimationTime.get())) invalidate();
-                if (!mBackgroundFade.isActive()) {
-                    mFadeOutTexture = null;
-                    mOpenAnimationRect = null;
-                    BitmapScreenNail.enableDrawPlaceholder();
-                } else {
-                    float fadeAlpha = mBackgroundFade.get();
-                    if (fadeAlpha < 1f) {
-                        view.clearBuffer(getBackgroundColor());
-                        view.setAlpha(fadeAlpha);
-                    }
-                    mFadeOutTexture.draw(view, 0, 0);
-                    view.setAlpha(1f - fadeAlpha);
-                    return;
-                }
-            }
-            view.clearBuffer(getBackgroundColor());
-        }
-
-        @Override
         protected void onLayout(
                 boolean changed, int left, int top, int right, int bottom) {
             mPhotoView.layout(0, 0, right - left, bottom - top);
@@ -366,6 +336,12 @@
                         break;
                     }
                     case MSG_REFRESH_IMAGE: {
+                        final MediaItem photo = mCurrentPhoto;
+                        mCurrentPhoto = null;
+                        updateCurrentPhoto(photo);
+                        break;
+                    }
+                    case MSG_UPDATE_PHOTO_UI: {
                         updateUIForCurrentPhoto();
                         break;
                     }
@@ -388,9 +364,6 @@
         mTreatBackAsUp = data.getBoolean(KEY_TREAT_BACK_AS_UP, false);
         mStartInFilmstrip = data.getBoolean(KEY_START_IN_FILMSTRIP, false);
         mInCameraRoll = data.getBoolean(KEY_IN_CAMERA_ROLL, false);
-        mStartedFromAlbumPage =
-                data.getInt(KEY_ALBUMPAGE_TRANSITION,
-                        MSG_ALBUMPAGE_NONE) == MSG_ALBUMPAGE_STARTED;
         mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0);
         if (mSetPathString != null) {
             mShowSpinner = true;
@@ -548,10 +521,14 @@
                 mProgressBar = new PhotoPageProgressBar(mActivity, galleryRoot);
                 mProgressListener = new UpdateProgressListener();
                 progressManager.addChangeListener(mProgressListener);
+                if (mSecureAlbum != null) {
+                    progressManager.addChangeListener(mSecureAlbum);
+                }
             }
         }
     }
 
+    @Override
     public void onPictureCenter(boolean isCamera) {
         mPhotoView.setWantPictureCenterCallbacks(false);
         mHandler.removeMessages(MSG_ON_CAMERA_CENTER);
@@ -559,10 +536,12 @@
         mHandler.sendEmptyMessage(isCamera ? MSG_ON_CAMERA_CENTER : MSG_ON_PICTURE_CENTER);
     }
 
+    @Override
     public boolean canDisplayBottomControls() {
         return mIsActive && !mPhotoView.getFilmMode();
     }
 
+    @Override
     public boolean canDisplayBottomControl(int control) {
         if (mCurrentPhoto == null) return false;
         switch(control) {
@@ -580,6 +559,7 @@
         }
     }
 
+    @Override
     public void onBottomControlClicked(int control) {
         switch(control) {
             case R.id.photopage_bottom_control_edit:
@@ -587,7 +567,8 @@
                 return;
             case R.id.photopage_bottom_control_panorama:
                 mRecenterCameraOnResume = false;
-                LightCycleHelper.viewPanorama(mActivity, mCurrentPhoto.getContentUri());
+                mActivity.getPanoramaViewHelper()
+                        .showPanorama(mCurrentPhoto.getContentUri());
                 return;
             default:
                 return;
@@ -617,11 +598,10 @@
     private Intent createShareIntent(Path path) {
         DataManager manager = mActivity.getDataManager();
         int type = manager.getMediaType(path);
-        Intent intent = new Intent(Intent.ACTION_SEND);
-        intent.setType(MenuExecutor.getMimeType(type));
-        Uri uri = manager.getContentUri(path);
-        intent.putExtra(Intent.EXTRA_STREAM, uri);
-        return intent;
+        return new Intent(Intent.ACTION_SEND)
+                .setType(MenuExecutor.getMimeType(type))
+                .putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path))
+                .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
     }
 
     private Intent createSharePanoramaIntent(Path path) {
@@ -630,11 +610,10 @@
         if ((supported & MediaObject.SUPPORT_PANORAMA360) == 0) {
             return null;
         }
-        Intent intent = new Intent(Intent.ACTION_SEND);
-        intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360);
-        Uri uri = manager.getContentUri(path);
-        intent.putExtra(Intent.EXTRA_STREAM, uri);
-        return intent;
+        return new Intent(Intent.ACTION_SEND)
+                .setType(GalleryUtils.MIME_TYPE_PANORAMA360)
+                .putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path))
+                .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
     }
 
     private void launchPhotoEditor() {
@@ -953,11 +932,10 @@
     };
 
     private void switchToGrid() {
-        if (mStartedFromAlbumPage) {
+        if (mActivity.getStateManager().hasStateClass(AlbumPage.class)) {
             onUpPressed();
         } else {
             if (mOriginalSetPathString == null) return;
-            preparePhotoFallbackView();
             Bundle data = new Bundle(getData());
             data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString);
             data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
@@ -976,7 +954,11 @@
             mActivity.getTransitionStore().put(KEY_RETURN_INDEX_HINT,
                     mAppBridge != null ? mCurrentIndex - 1 : mCurrentIndex);
 
-            mActivity.getStateManager().startState(AlbumPage.class, data);
+            if (mInCameraRoll && mAppBridge != null) {
+                mActivity.getStateManager().startState(AlbumPage.class, data);
+            } else {
+                mActivity.getStateManager().switchState(this, AlbumPage.class, data);
+            }
         }
     }
 
@@ -1014,9 +996,10 @@
             }
             case R.id.action_crop: {
                 Activity activity = mActivity;
-                Intent intent = new Intent(CropImage.CROP_ACTION);
-                intent.setClass(activity, CropImage.class);
-                intent.setData(manager.getContentUri(path));
+                Intent intent = new Intent(FilterShowActivity.CROP_ACTION);
+                intent.setClass(activity, FilterShowActivity.class);
+                intent.setDataAndType(manager.getContentUri(path), current.getMimeType())
+                    .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                 activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current)
                         ? REQUEST_CROP_PICASA
                         : REQUEST_CROP);
@@ -1197,6 +1180,16 @@
         Path path = mApplication.getDataManager()
                 .findPathByUri(intent.getData(), intent.getType());
         if (path != null) {
+            Path albumPath = mApplication.getDataManager().getDefaultSetOf(path);
+            if (!albumPath.equalsIgnoreCase(mOriginalSetPathString)) {
+                // If the edited image is stored in a different album, we need
+                // to start a new activity state to show the new image
+                Bundle data = new Bundle(getData());
+                data.putString(KEY_MEDIA_SET_PATH, albumPath.toString());
+                data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, path.toString());
+                mActivity.getStateManager().startState(PhotoPage.class, data);
+                return;
+            }
             mModel.setCurrentPhoto(path, mCurrentIndex);
         }
     }
@@ -1287,7 +1280,6 @@
         // Hide the detail dialog on exit
         if (mShowDetails) hideDetails();
         if (mModel != null) {
-            if (isFinishing()) preparePhotoFallbackView();
             mModel.pause();
         }
         mPhotoView.pause();
@@ -1351,18 +1343,6 @@
         } else if (albumPageTransition == MSG_ALBUMPAGE_PICKED) {
             mPhotoView.setFilmMode(false);
         }
-
-        mFadeOutTexture = transitions.get(PreparePageFadeoutTexture.KEY_FADE_TEXTURE);
-        if (mFadeOutTexture != null) {
-            mBackgroundFade.start();
-            BitmapScreenNail.disableDrawPlaceholder();
-            mOpenAnimationRect =
-                    albumPageTransition == MSG_ALBUMPAGE_NONE ?
-                    (Rect) mData.getParcelable(KEY_OPEN_ANIMATION_RECT) :
-                    (Rect) transitions.get(KEY_OPEN_ANIMATION_RECT);
-            mPhotoView.setOpenAnimationRect(mOpenAnimationRect);
-            mBackgroundFade.start();
-        }
     }
 
     @Override
diff --git a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
index 2f6f16f..00f2fe7 100644
--- a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
+++ b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
@@ -27,6 +27,7 @@
 import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.data.MediaItem;
 import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.BitmapScreenNail;
 import com.android.gallery3d.ui.PhotoView;
 import com.android.gallery3d.ui.ScreenNail;
 import com.android.gallery3d.ui.SynchronizedHandler;
@@ -50,6 +51,7 @@
     private PhotoView mPhotoView;
     private ThreadPool mThreadPool;
     private int mLoadingState = LOADING_INIT;
+    private BitmapScreenNail mBitmapScreenNail;
 
     public SinglePhotoDataAdapter(
             AbstractGalleryActivity activity, PhotoView view, MediaItem item) {
@@ -113,6 +115,11 @@
         return false;
     }
 
+    private void setScreenNail(Bitmap bitmap, int width, int height) {
+        mBitmapScreenNail = new BitmapScreenNail(bitmap);
+        setScreenNail(mBitmapScreenNail, width, height);
+    }
+
     private void onDecodeLargeComplete(ImageBundle bundle) {
         try {
             setScreenNail(bundle.backupImage,
@@ -162,6 +169,10 @@
         if (task.get() == null) {
             mTask = null;
         }
+        if (mBitmapScreenNail != null) {
+            mBitmapScreenNail.recycle();
+            mBitmapScreenNail = null;
+        }
     }
 
     @Override
diff --git a/src/com/android/gallery3d/app/StateManager.java b/src/com/android/gallery3d/app/StateManager.java
index c041b0e..fefb2c8 100644
--- a/src/com/android/gallery3d/app/StateManager.java
+++ b/src/com/android/gallery3d/app/StateManager.java
@@ -57,6 +57,8 @@
         }
         if (!mStack.isEmpty()) {
             ActivityState top = getTopState();
+            top.transitionOnNextPause(top.getClass(), klass,
+                    ActivityState.StateTransition.Incoming);
             if (mIsResumed) top.onPause();
         }
         state.initialize(mActivity, data);
@@ -81,6 +83,8 @@
 
         if (!mStack.isEmpty()) {
             ActivityState as = getTopState();
+            as.transitionOnNextPause(as.getClass(), klass,
+                    ActivityState.StateTransition.Incoming);
             as.mReceivedResults = state.mResult;
             if (mIsResumed) as.onPause();
         } else {
@@ -186,15 +190,18 @@
         // Remove the top state.
         mStack.pop();
         state.mIsFinishing = true;
-        if (mIsResumed && fireOnPause) state.onPause();
+        ActivityState top = !mStack.isEmpty() ? mStack.peek().activityState : null;
+        if (mIsResumed && fireOnPause) {
+            if (top != null) {
+                state.transitionOnNextPause(state.getClass(), top.getClass(),
+                        ActivityState.StateTransition.Outgoing);
+            }
+            state.onPause();
+        }
         mActivity.getGLRoot().setContentPane(null);
         state.onDestroy();
 
-        if (!mStack.isEmpty()) {
-            // Restore the immediately previous state
-            ActivityState top = mStack.peek().activityState;
-            if (mIsResumed) top.resume();
-        }
+        if (top != null && mIsResumed) top.resume();
     }
 
     public void switchState(ActivityState oldState,
@@ -207,6 +214,10 @@
         }
         // Remove the top state.
         mStack.pop();
+        if (!data.containsKey(PhotoPage.KEY_APP_BRIDGE)) {
+            // Do not do the fade out stuff when we are switching camera modes
+            oldState.transitionOnNextPause(oldState.getClass(), klass, ActivityState.StateTransition.Incoming);
+        }
         if (mIsResumed) oldState.onPause();
         oldState.onDestroy();
 
diff --git a/src/com/android/gallery3d/app/TrimVideo.java b/src/com/android/gallery3d/app/TrimVideo.java
index 09a2abd..01fe462 100644
--- a/src/com/android/gallery3d/app/TrimVideo.java
+++ b/src/com/android/gallery3d/app/TrimVideo.java
@@ -268,9 +268,8 @@
             return;
         }
         if (Math.abs(mVideoView.getDuration() - delta) < 100) {
-            Toast.makeText(getApplicationContext(),
-                getString(R.string.trim_too_long),
-                Toast.LENGTH_SHORT).show();
+            // If no change has been made, go back
+            onBackPressed();
             return;
         }
         // Use the default save directory if the source directory cannot be
diff --git a/src/com/android/gallery3d/data/DataManager.java b/src/com/android/gallery3d/data/DataManager.java
index 3d2c0c2..408a24b 100644
--- a/src/com/android/gallery3d/data/DataManager.java
+++ b/src/com/android/gallery3d/data/DataManager.java
@@ -85,9 +85,6 @@
 
     private static final String TOP_LOCAL_VIDEO_SET_PATH = "/local/video";
 
-    private static final String ACTION_DELETE_PICTURE =
-            "com.android.gallery3d.action.DELETE_PICTURE";
-
     public static final Comparator<MediaItem> sDateTakenComparator =
             new DateTakenComparator();
 
@@ -335,15 +332,6 @@
         }
     }
 
-    // Sends a local broadcast if a local image or video is deleted. This is
-    // used to update the thumbnail shown in the camera app.
-    public void broadcastLocalDeletion() {
-        LocalBroadcastManager manager = LocalBroadcastManager.getInstance(
-                mApplication.getAndroidContext());
-        Intent intent = new Intent(ACTION_DELETE_PICTURE);
-        manager.sendBroadcast(intent);
-    }
-
     private static class NotifyBroker extends ContentObserver {
         private WeakHashMap<ChangeNotifier, Object> mNotifiers =
                 new WeakHashMap<ChangeNotifier, Object>();
diff --git a/src/com/android/gallery3d/data/EmptyAlbumImage.java b/src/com/android/gallery3d/data/EmptyAlbumImage.java
index dbbc01a..6f8c37c 100644
--- a/src/com/android/gallery3d/data/EmptyAlbumImage.java
+++ b/src/com/android/gallery3d/data/EmptyAlbumImage.java
@@ -24,7 +24,7 @@
     private static final String TAG = "EmptyAlbumImage";
 
     public EmptyAlbumImage(Path path, GalleryApp application) {
-        super(path, application, R.drawable.ic_menu_revert_holo_dark);
+        super(path, application, R.drawable.placeholder_empty);
     }
 
     @Override
diff --git a/src/com/android/gallery3d/data/LocalAlbum.java b/src/com/android/gallery3d/data/LocalAlbum.java
index 4682e77..e05aac0 100644
--- a/src/com/android/gallery3d/data/LocalAlbum.java
+++ b/src/com/android/gallery3d/data/LocalAlbum.java
@@ -267,7 +267,6 @@
         GalleryUtils.assertNotInRenderThread();
         mResolver.delete(mBaseUri, mWhereClause,
                 new String[]{String.valueOf(mBucketId)});
-        mApplication.getDataManager().broadcastLocalDeletion();
     }
 
     @Override
@@ -275,7 +274,7 @@
         return true;
     }
 
-    private static String getLocalizedName(Resources res, int bucketId,
+    public static String getLocalizedName(Resources res, int bucketId,
             String name) {
         if (bucketId == MediaSetUtils.CAMERA_BUCKET_ID) {
             return res.getString(R.string.folder_camera);
@@ -285,6 +284,8 @@
             return res.getString(R.string.folder_imported);
         } else if (bucketId == MediaSetUtils.SNAPSHOT_BUCKET_ID) {
             return res.getString(R.string.folder_screenshot);
+        } else if (bucketId == MediaSetUtils.EDITED_ONLINE_PHOTOS_BUCKET_ID) {
+            return res.getString(R.string.folder_edited_online_photos);
         } else {
             return name;
         }
diff --git a/src/com/android/gallery3d/data/LocalImage.java b/src/com/android/gallery3d/data/LocalImage.java
index dba6b68..61961d8 100644
--- a/src/com/android/gallery3d/data/LocalImage.java
+++ b/src/com/android/gallery3d/data/LocalImage.java
@@ -288,8 +288,6 @@
     @Override
     public void setSupportedOperationsListener(SupportedOperationsListener l) {
         synchronized (mLock) {
-            if (mPanoramaMetadataInitialized) return; // no more updates
-
             if (l == null) {
                 if (mGetPanoMetadataTask != null) {
                     mGetPanoMetadataTask.cancel();
@@ -308,7 +306,7 @@
                             mPanoramaMetadata = future.get();
                             mPanoramaMetadataInitialized = true;
                             if (mListener != null) {
-                                mListener.onChange(getSupportedOperations());
+                                mListener.onChange(LocalImage.this, getSupportedOperations());
                             }
                         }
                         });
@@ -324,7 +322,6 @@
         Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
         mApplication.getContentResolver().delete(baseUri, "_id=?",
                 new String[]{String.valueOf(id)});
-        mApplication.getDataManager().broadcastLocalDeletion();
     }
 
     private static String getExifOrientation(int orientation) {
diff --git a/src/com/android/gallery3d/data/LocalVideo.java b/src/com/android/gallery3d/data/LocalVideo.java
index 5b6ee4b..c876d81 100644
--- a/src/com/android/gallery3d/data/LocalVideo.java
+++ b/src/com/android/gallery3d/data/LocalVideo.java
@@ -187,7 +187,6 @@
         Uri baseUri = Video.Media.EXTERNAL_CONTENT_URI;
         mApplication.getContentResolver().delete(baseUri, "_id=?",
                 new String[]{String.valueOf(id)});
-        mApplication.getDataManager().broadcastLocalDeletion();
     }
 
     @Override
diff --git a/src/com/android/gallery3d/data/MediaObject.java b/src/com/android/gallery3d/data/MediaObject.java
index 382a5c7..14cd524 100644
--- a/src/com/android/gallery3d/data/MediaObject.java
+++ b/src/com/android/gallery3d/data/MediaObject.java
@@ -50,7 +50,7 @@
     public static final int SUPPORT_ALL = 0xffffffff;
 
     public static interface SupportedOperationsListener {
-        public void onChange(int operations);
+        public void onChange(MediaObject item, int operations);
     }
 
     // These are the bits returned from getMediaType():
@@ -100,7 +100,7 @@
     }
 
     public int getSupportedOperations(boolean getAll) {
-        return 0;
+        return getSupportedOperations();
     }
 
     public void setSupportedOperationsListener(SupportedOperationsListener l) {
diff --git a/src/com/android/gallery3d/data/Path.java b/src/com/android/gallery3d/data/Path.java
index a4840bd..fcae65e 100644
--- a/src/com/android/gallery3d/data/Path.java
+++ b/src/com/android/gallery3d/data/Path.java
@@ -79,6 +79,7 @@
     }
 
     @Override
+    // TODO: toString() should be more efficient, will fix it later
     public String toString() {
         synchronized (Path.class) {
             StringBuilder sb = new StringBuilder();
@@ -91,6 +92,11 @@
         }
     }
 
+    public boolean equalsIgnoreCase (String p) {
+        String path = toString();
+        return path.equalsIgnoreCase(p);
+    }
+
     public static Path fromString(String s) {
         synchronized (Path.class) {
             String[] segments = split(s);
diff --git a/src/com/android/gallery3d/data/SecureAlbum.java b/src/com/android/gallery3d/data/SecureAlbum.java
index c666bdc..0a8c5a8 100644
--- a/src/com/android/gallery3d/data/SecureAlbum.java
+++ b/src/com/android/gallery3d/data/SecureAlbum.java
@@ -24,12 +24,13 @@
 import android.provider.MediaStore.Video;
 
 import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.app.StitchingChangeListener;
 import com.android.gallery3d.util.MediaSetUtils;
 
 import java.util.ArrayList;
 
 // This class lists all media items added by the client.
-public class SecureAlbum extends MediaSet {
+public class SecureAlbum extends MediaSet implements StitchingChangeListener {
     @SuppressWarnings("unused")
     private static final String TAG = "SecureAlbum";
     private static final String[] PROJECTION = {MediaColumns._ID};
@@ -183,4 +184,18 @@
     public boolean isLeafAlbum() {
         return true;
     }
+
+    @Override
+    public void onStitchingQueued(Uri uri) {
+        int id = Integer.parseInt(uri.getLastPathSegment());
+        addMediaItem(false, id);
+    }
+
+    @Override
+    public void onStitchingResult(Uri uri) {
+    }
+
+    @Override
+    public void onStitchingProgress(Uri uri, final int progress) {
+    }
 }
diff --git a/src/com/android/gallery3d/data/UriImage.java b/src/com/android/gallery3d/data/UriImage.java
index 5fab667..aaa36a9 100644
--- a/src/com/android/gallery3d/data/UriImage.java
+++ b/src/com/android/gallery3d/data/UriImage.java
@@ -255,8 +255,6 @@
     @Override
     public void setSupportedOperationsListener(SupportedOperationsListener l) {
         synchronized (mLock) {
-            if (mPanoramaMetadataInitialized) return; // no more updates
-
             if (l != null) {
                 if (mGetPanoMetadataTask != null) {
                     mGetPanoMetadataTask.cancel();
@@ -275,7 +273,7 @@
                             mPanoramaMetadata = future.get();
                             mPanoramaMetadataInitialized = true;
                             if (mListener != null) {
-                                mListener.onChange(getSupportedOperations());
+                                mListener.onChange(UriImage.this, getSupportedOperations());
                             }
                         }
                         });
diff --git a/src/com/android/gallery3d/filtershow/FilterShowActivity.java b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
index f2b817c..fa9277e 100644
--- a/src/com/android/gallery3d/filtershow/FilterShowActivity.java
+++ b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
@@ -6,6 +6,7 @@
 import android.app.Activity;
 import android.app.ProgressDialog;
 import android.content.ContentValues;
+import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
@@ -31,8 +32,10 @@
 import android.widget.SeekBar;
 import android.widget.ShareActionProvider;
 import android.widget.ShareActionProvider.OnShareTargetSelectedListener;
+import android.widget.Toast;
 
 import com.android.gallery3d.R;
+import com.android.gallery3d.data.LocalAlbum;
 import com.android.gallery3d.filtershow.cache.ImageLoader;
 import com.android.gallery3d.filtershow.filters.ImageFilter;
 import com.android.gallery3d.filtershow.filters.ImageFilterBorder;
@@ -60,7 +63,10 @@
 import com.android.gallery3d.filtershow.presets.ImagePreset;
 import com.android.gallery3d.filtershow.provider.SharedImageProvider;
 import com.android.gallery3d.filtershow.tools.SaveCopyTask;
+import com.android.gallery3d.filtershow.ui.ImageButtonTitle;
 import com.android.gallery3d.filtershow.ui.ImageCurves;
+import com.android.gallery3d.filtershow.ui.Spline;
+import com.android.gallery3d.util.GalleryUtils;
 
 import java.io.File;
 import java.lang.ref.WeakReference;
@@ -70,6 +76,7 @@
 public class FilterShowActivity extends Activity implements OnItemClickListener,
         OnShareTargetSelectedListener {
 
+    public static final String CROP_ACTION = "com.android.camera.action.CROP";
     private final PanelController mPanelController = new PanelController();
     private ImageLoader mImageLoader = null;
     private ImageShow mImageShow = null;
@@ -77,7 +84,7 @@
     private ImageBorder mImageBorders = null;
     private ImageStraighten mImageStraighten = null;
     private ImageZoom mImageZoom = null;
-    private final ImageCrop mImageCrop = null;
+    private ImageCrop mImageCrop = null;
     private ImageRotate mImageRotate = null;
     private ImageFlip mImageFlip = null;
 
@@ -96,6 +103,7 @@
     private static final int SELECT_PICTURE = 1;
     private static final String LOGTAG = "FilterShowActivity";
     protected static final boolean ANIMATE_PANELS = true;
+    private static int mImageBorderSize = 40;
 
     private boolean mShowingHistoryPanel = false;
     private boolean mShowingImageStatePanel = false;
@@ -110,6 +118,7 @@
     private boolean mSharingImage = false;
 
     private WeakReference<ProgressDialog> mSavingProgressDialog;
+    private static final int SEEK_BAR_MAX = 600;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -117,6 +126,20 @@
 
         ImageFilterRS.setRenderScriptContext(this);
 
+        ImageShow.setDefaultBackgroundColor(getResources().getColor(R.color.background_screen));
+        // TODO: get those values from XML.
+        ImageShow.setTextSize((int) getPixelsFromDip(12));
+        ImageShow.setTextPadding((int) getPixelsFromDip(10));
+        ImageButtonTitle.setTextSize((int) getPixelsFromDip(12));
+        ImageButtonTitle.setTextPadding((int) getPixelsFromDip(10));
+        ImageSmallFilter.setMargin((int) getPixelsFromDip(3));
+        ImageSmallFilter.setTextMargin((int) getPixelsFromDip(4));
+        mImageBorderSize = (int) getPixelsFromDip(20);
+        Drawable curveHandle = getResources().getDrawable(R.drawable.camera_crop_holo);
+        int curveHandleSize = (int) getResources().getDimension(R.dimen.crop_indicator_size);
+        Spline.setCurveHandle(curveHandle, curveHandleSize);
+        Spline.setCurveWidth((int) getPixelsFromDip(3));
+
         setContentView(R.layout.filtershow_activity);
         ActionBar actionBar = getActionBar();
         actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
@@ -129,7 +152,7 @@
             }
         });
 
-        mImageLoader = new ImageLoader(getApplicationContext());
+        mImageLoader = new ImageLoader(this, getApplicationContext());
 
         LinearLayout listFilters = (LinearLayout) findViewById(R.id.listFilters);
         LinearLayout listBorders = (LinearLayout) findViewById(R.id.listBorders);
@@ -140,8 +163,7 @@
         mImageBorders = (ImageBorder) findViewById(R.id.imageBorder);
         mImageStraighten = (ImageStraighten) findViewById(R.id.imageStraighten);
         mImageZoom = (ImageZoom) findViewById(R.id.imageZoom);
-        // TODO: implement crop
-        // mImageCrop = (ImageCrop) findViewById(R.id.imageCrop);
+        mImageCrop = (ImageCrop) findViewById(R.id.imageCrop);
         mImageRotate = (ImageRotate) findViewById(R.id.imageRotate);
         mImageFlip = (ImageFlip) findViewById(R.id.imageFlip);
 
@@ -150,8 +172,7 @@
         mImageViews.add(mImageBorders);
         mImageViews.add(mImageStraighten);
         mImageViews.add(mImageZoom);
-        // TODO: implement crop
-        // mImageViews.add(mImageCrop);
+        mImageViews.add(mImageCrop);
         mImageViews.add(mImageRotate);
         mImageViews.add(mImageFlip);
 
@@ -180,9 +201,8 @@
         mImageStraighten.setMaster(mImageShow);
         mImageZoom.setImageLoader(mImageLoader);
         mImageZoom.setMaster(mImageShow);
-        // TODO: implement crop
-        // mImageCrop.setImageLoader(mImageLoader);
-        // mImageCrop.setMaster(mImageShow);
+        mImageCrop.setImageLoader(mImageLoader);
+        mImageCrop.setMaster(mImageShow);
         mImageRotate.setImageLoader(mImageLoader);
         mImageRotate.setMaster(mImageShow);
         mImageFlip.setImageLoader(mImageLoader);
@@ -192,8 +212,7 @@
         mPanelController.addImageView(findViewById(R.id.imageCurves));
         mPanelController.addImageView(findViewById(R.id.imageBorder));
         mPanelController.addImageView(findViewById(R.id.imageStraighten));
-        // TODO: implement crop
-        // mPanelController.addImageView(findViewById(R.id.imageCrop));
+        mPanelController.addImageView(findViewById(R.id.imageCrop));
         mPanelController.addImageView(findViewById(R.id.imageRotate));
         mPanelController.addImageView(findViewById(R.id.imageFlip));
         mPanelController.addImageView(findViewById(R.id.imageZoom));
@@ -203,14 +222,13 @@
 
         mPanelController.addPanel(mGeometryButton, mListGeometry, 2);
         mPanelController.addComponent(mGeometryButton, findViewById(R.id.straightenButton));
-        // TODO: implement crop
-//        mPanelController.addComponent(mGeometryButton, findViewById(R.id.cropButton));
+        mPanelController.addComponent(mGeometryButton, findViewById(R.id.cropButton));
         mPanelController.addComponent(mGeometryButton, findViewById(R.id.rotateButton));
         mPanelController.addComponent(mGeometryButton, findViewById(R.id.flipButton));
 
         mPanelController.addPanel(mColorsButton, mListColors, 3);
 
-        int []recastIDs = {
+        int[] recastIDs = {
                 R.id.vignetteButton,
                 R.id.vibranceButton,
                 R.id.contrastButton,
@@ -220,7 +238,7 @@
                 R.id.exposureButton,
                 R.id.shadowRecoveryButton
         };
-        ImageFilter []filters = {
+        ImageFilter[] filters = {
                 new ImageFilterVignette(),
                 new ImageFilterVibrance(),
                 new ImageFilterContrast(),
@@ -231,7 +249,6 @@
                 new ImageFilterShadows()
         };
 
-
         for (int i = 0; i < filters.length; i++) {
 
             ImageSmallFilter fView = new ImageSmallFilter(this);
@@ -240,51 +257,51 @@
             listColors.removeView(v);
 
             filters[i].setParameter(100);
+            if(v instanceof ImageButtonTitle)
+                filters[i].setName(((ImageButtonTitle) v).getText());
             fView.setImageFilter(filters[i]);
             fView.setController(this);
             fView.setImageLoader(mImageLoader);
             fView.setId(recastIDs[i]);
-
             mPanelController.addComponent(mColorsButton, fView);
-            listColors.addView(fView,pos);
+            listColors.addView(fView, pos);
         }
 
-        int []overlayIDs = {
+        int[] overlayIDs = {
                 R.id.sharpenButton,
                 R.id.curvesButtonRGB
         };
-        int []overlayBitmaps = {
+        int[] overlayBitmaps = {
                 R.drawable.filtershow_button_colors_sharpen,
                 R.drawable.filtershow_button_colors_curve
         };
         int []overlayNames = {
-                R.string.sharpen,
+                R.string.sharpness,
                 R.string.curvesRGB
         };
 
-        for (int i = 0; i < overlayIDs.length; i++)  {
+        for (int i = 0; i < overlayIDs.length; i++) {
             ImageWithIcon fView = new ImageWithIcon(this);
             View v = listColors.findViewById(overlayIDs[i]);
             int pos = listColors.indexOfChild(v);
             listColors.removeView(v);
-            final int sid =overlayNames[i];
-            ImageFilterExposure efilter = new ImageFilterExposure(){
+            final int sid = overlayNames[i];
+            ImageFilterExposure efilter = new ImageFilterExposure() {
                 {
                     mName = getString(sid);
                 }
             };
             efilter.setParameter(-300);
             Bitmap bitmap = BitmapFactory.decodeResource(getResources(),
-                    overlayBitmaps[i] );
+                    overlayBitmaps[i]);
 
             fView.setIcon(bitmap);
             fView.setImageFilter(efilter);
             fView.setController(this);
             fView.setImageLoader(mImageLoader);
             fView.setId(overlayIDs[i]);
-
             mPanelController.addComponent(mColorsButton, fView);
-            listColors.addView(fView,pos);
+            listColors.addView(fView, pos);
         }
 
         mPanelController.addComponent(mColorsButton, findViewById(R.id.curvesButtonRGB));
@@ -298,7 +315,8 @@
         mPanelController.addComponent(mColorsButton, findViewById(R.id.shadowRecoveryButton));
 
         mPanelController.addView(findViewById(R.id.applyEffect));
-
+        mPanelController.addView(findViewById(R.id.pickCurvesChannel));
+        mPanelController.addView(findViewById(R.id.aspect));
         findViewById(R.id.resetOperationsButton).setOnClickListener(
                 createOnClickResetOperationsButton());
 
@@ -313,35 +331,42 @@
         fillListBorders(listBorders);
 
         SeekBar seekBar = (SeekBar) findViewById(R.id.filterSeekBar);
-        seekBar.setMax(200);
+        seekBar.setMax(SEEK_BAR_MAX);
+
         mImageShow.setSeekBar(seekBar);
         mImageZoom.setSeekBar(seekBar);
         mPanelController.setRowPanel(findViewById(R.id.secondRowPanel));
         mPanelController.setUtilityPanel(this, findViewById(R.id.filterButtonsList),
-                findViewById(R.id.applyEffect));
+                findViewById(R.id.applyEffect), findViewById(R.id.aspect));
         mPanelController.setMasterImage(mImageShow);
         mPanelController.setCurrentPanel(mFxButton);
         Intent intent = getIntent();
         String data = intent.getDataString();
         if (data != null) {
             Uri uri = Uri.parse(data);
-            mImageLoader.loadBitmap(uri,getScreenImageSize());
+            mImageLoader.loadBitmap(uri, getScreenImageSize());
         } else {
             pickImage();
         }
+
+        String action = intent.getAction();
+        if (action == CROP_ACTION){
+            mPanelController.showComponent(findViewById(R.id.cropButton));
+        }
+
     }
 
-    private int getScreenImageSize(){
-        DisplayMetrics metrics = new  DisplayMetrics();
+    private int getScreenImageSize() {
+        DisplayMetrics metrics = new DisplayMetrics();
         Display display = getWindowManager().getDefaultDisplay();
-        Point size = new  Point();
+        Point size = new Point();
         display.getSize(size);
         display.getMetrics(metrics);
         int msize = Math.min(size.x, size.y);
-        return  (133*msize)/metrics.densityDpi;
+        return (133 * msize) / metrics.densityDpi;
     }
 
-    private void showSavingProgress() {
+    private void showSavingProgress(String albumName) {
         ProgressDialog progress;
         if (mSavingProgressDialog != null) {
             progress = mSavingProgressDialog.get();
@@ -351,7 +376,13 @@
             }
         }
         // TODO: Allow cancellation of the saving process
-        progress = ProgressDialog.show(this, "", getString(R.string.saving_image), true, false);
+        String progressText;
+        if (albumName == null) {
+            progressText = getString(R.string.saving_image);
+        } else {
+            progressText = getString(R.string.filtershow_saving_image, albumName);
+        }
+        progress = ProgressDialog.show(this, "", progressText, true, false);
         mSavingProgressDialog = new WeakReference<ProgressDialog>(progress);
     }
 
@@ -389,7 +420,7 @@
         mSharingImage = true;
 
         // Process and save the image in the background.
-        showSavingProgress();
+        showSavingProgress(null);
         mImageShow.saveImage(this, mSharedOutputFile);
         return true;
     }
@@ -493,27 +524,27 @@
         int p = 0;
 
         int[] drawid = {
-                R.drawable.filtershow_fx_0000_vintage,
-                R.drawable.filtershow_fx_0001_instant,
-                R.drawable.filtershow_fx_0002_bleach,
-                R.drawable.filtershow_fx_0003_blue_crush,
-                R.drawable.filtershow_fx_0004_bw_contrast,
                 R.drawable.filtershow_fx_0005_punch,
-                R.drawable.filtershow_fx_0006_x_process,
+                R.drawable.filtershow_fx_0000_vintage,
+                R.drawable.filtershow_fx_0004_bw_contrast,
+                R.drawable.filtershow_fx_0002_bleach,
+                R.drawable.filtershow_fx_0001_instant,
                 R.drawable.filtershow_fx_0007_washout,
-                R.drawable.filtershow_fx_0008_washout_color
+                R.drawable.filtershow_fx_0003_blue_crush,
+                R.drawable.filtershow_fx_0008_washout_color,
+                R.drawable.filtershow_fx_0006_x_process
         };
 
         int[] fxNameid = {
-                R.string.ffx_vintage,
-                R.string.ffx_instant,
-                R.string.ffx_bleach,
-                R.string.ffx_blue_crush,
-                R.string.ffx_bw_contrast,
                 R.string.ffx_punch,
-                R.string.ffx_x_process,
+                R.string.ffx_vintage,
+                R.string.ffx_bw_contrast,
+                R.string.ffx_bleach,
+                R.string.ffx_instant,
                 R.string.ffx_washout,
+                R.string.ffx_blue_crush,
                 R.string.ffx_washout_color,
+                R.string.ffx_x_process
         };
 
         ImagePreset preset = new ImagePreset(); // empty
@@ -522,14 +553,12 @@
         filter.setSelected(true);
         mCurrentImageSmallFilter = filter;
 
-        filter.setPreviousImageSmallFilter(null);
-        preset.setIsFx(true);
-        filter.setImagePreset(preset);
+        filter.setImageFilter(new ImageFilterFx(null,getString(R.string.none)));
 
         filter.setController(this);
         filter.setImageLoader(mImageLoader);
         listFilters.addView(filter);
-        ImageSmallFilter   previousFilter = filter;
+        ImageSmallFilter previousFilter = filter;
 
         BitmapFactory.Options o = new BitmapFactory.Options();
         o.inScaled = false;
@@ -541,8 +570,6 @@
 
         for (int i = 0; i < p; i++) {
             filter = new ImageSmallFilter(this);
-
-            filter.setPreviousImageSmallFilter(previousFilter);
             filter.setImageFilter(fxArray[i]);
             filter.setController(this);
             filter.setImageLoader(mImageLoader);
@@ -565,15 +592,16 @@
         borders[p++] = new ImageFilterBorder(npd1);
         Drawable npd2 = getResources().getDrawable(R.drawable.filtershow_border_brush);
         borders[p++] = new ImageFilterBorder(npd2);
-        borders[p++] = new ImageFilterParametricBorder(Color.BLACK, 100, 0);
-        borders[p++] = new ImageFilterParametricBorder(Color.BLACK, 100, 100);
-        borders[p++] = new ImageFilterParametricBorder(Color.WHITE, 100, 0);
-        borders[p++] = new ImageFilterParametricBorder(Color.WHITE, 100, 100);
+        borders[p++] = new ImageFilterParametricBorder(Color.BLACK, mImageBorderSize, 0);
+        borders[p++] = new ImageFilterParametricBorder(Color.BLACK, mImageBorderSize,
+                mImageBorderSize);
+        borders[p++] = new ImageFilterParametricBorder(Color.WHITE, mImageBorderSize, 0);
+        borders[p++] = new ImageFilterParametricBorder(Color.WHITE, mImageBorderSize,
+                mImageBorderSize);
 
         ImageSmallFilter previousFilter = null;
         for (int i = 0; i < p; i++) {
             ImageSmallBorder filter = new ImageSmallBorder(this);
-            filter.setPreviousImageSmallFilter(previousFilter);
             filter.setImageFilter(borders[i]);
             filter.setController(this);
             filter.setBorder(true);
@@ -626,7 +654,6 @@
         }
     }
 
-
     // //////////////////////////////////////////////////////////////////////////////
     // imageState panel...
 
@@ -699,6 +726,7 @@
         adapter.reset();
         ImagePreset original = new ImagePreset(adapter.getItem(0));
         mImageShow.setImagePreset(original);
+        mPanelController.resetParameters();
         invalidateViews();
     }
 
@@ -712,6 +740,20 @@
         };
     }
 
+    @Override
+    public void onBackPressed() {
+        if (mPanelController.onBackPressed()) {
+            saveImage();
+        }
+    }
+
+    public void cannotLoadImage() {
+        CharSequence text = getString(R.string.cannot_load_image);
+        Toast toast = Toast.makeText(this, text, Toast.LENGTH_SHORT);
+        toast.show();
+        finish();
+    }
+
     // //////////////////////////////////////////////////////////////////////////////
 
     public float getPixelsFromDip(float value) {
@@ -783,14 +825,22 @@
         if (resultCode == RESULT_OK) {
             if (requestCode == SELECT_PICTURE) {
                 Uri selectedImageUri = data.getData();
-                mImageLoader.loadBitmap(selectedImageUri,getScreenImageSize());
+                mImageLoader.loadBitmap(selectedImageUri, getScreenImageSize());
             }
         }
     }
 
     public void saveImage() {
-        showSavingProgress();
-        mImageShow.saveImage(this, null);
+        if (mImageShow.hasModifications()) {
+            // Get the name of the album, to which the image will be saved
+            File saveDir = SaveCopyTask.getFinalSaveDirectory(this, mImageLoader.getUri());
+            int bucketId = GalleryUtils.getBucketId(saveDir.getPath());
+            String albumName = LocalAlbum.getLocalizedName(getResources(), bucketId, null);
+            showSavingProgress(albumName);
+            mImageShow.saveImage(this, null);
+        } else {
+            finish();
+        }
     }
 
     static {
diff --git a/src/com/android/gallery3d/filtershow/PanelController.java b/src/com/android/gallery3d/filtershow/PanelController.java
index f13cdd4..2e8dd23 100644
--- a/src/com/android/gallery3d/filtershow/PanelController.java
+++ b/src/com/android/gallery3d/filtershow/PanelController.java
@@ -20,9 +20,11 @@
 import com.android.gallery3d.filtershow.filters.ImageFilterVibrance;
 import com.android.gallery3d.filtershow.filters.ImageFilterVignette;
 import com.android.gallery3d.filtershow.filters.ImageFilterWBalance;
+import com.android.gallery3d.filtershow.imageshow.ImageCrop;
 import com.android.gallery3d.filtershow.imageshow.ImageShow;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
 import com.android.gallery3d.filtershow.ui.ImageCurves;
+import com.android.gallery3d.filtershow.ui.ImageButtonTitle;
 
 import java.util.HashMap;
 import java.util.Vector;
@@ -113,17 +115,95 @@
         private String mEffectName = null;
         private int mParameterValue = 0;
         private boolean mShowParameterValue = false;
+        private View mAspectButton = null;
+        private int mCurrentAspectButton = 0;
+        private static final int NUMBER_OF_ASPECT_BUTTONS = 6;
+        private static final int ASPECT_NONE = 0;
+        private static final int ASPECT_1TO1 = 1;
+        private static final int ASPECT_5TO7 = 2;
+        private static final int ASPECT_4TO6 = 3;
+        private static final int ASPECT_16TO9 = 4;
+        private static final int ASPECT_ORIG = 5;
 
-        public UtilityPanel(Context context, View view, View textView) {
+        public UtilityPanel(Context context, View view, View textView, View button) {
             mContext = context;
             mView = view;
             mTextView = (TextView) textView;
+            mAspectButton = button;
         }
 
         public boolean selected() {
             return mSelected;
         }
 
+        public void nextAspectButton() {
+            if (mAspectButton instanceof ImageButtonTitle
+                    && mCurrentImage instanceof ImageCrop) {
+                switch (mCurrentAspectButton) {
+                    case ASPECT_NONE:
+                        ((ImageButtonTitle) mAspectButton).setText(mContext
+                                .getString(R.string.aspect)
+                                + " "
+                                + mContext.getString(R.string.aspect1to1_effect));
+                        ((ImageCrop) mCurrentImage).apply(1, 1);
+                        break;
+                    case ASPECT_1TO1:
+                        ((ImageButtonTitle) mAspectButton).setText(mContext
+                                .getString(R.string.aspect)
+                                + " "
+                                + mContext.getString(R.string.aspect5to7_effect));
+                        ((ImageCrop) mCurrentImage).apply(7, 5);
+                        break;
+                    case ASPECT_5TO7:
+                        ((ImageButtonTitle) mAspectButton).setText(mContext
+                                .getString(R.string.aspect)
+                                + " "
+                                + mContext.getString(R.string.aspect4to6_effect));
+                        ((ImageCrop) mCurrentImage).apply(6, 4);
+                        break;
+                    case ASPECT_4TO6:
+                        ((ImageButtonTitle) mAspectButton).setText(mContext
+                                .getString(R.string.aspect)
+                                + " "
+                                + mContext.getString(R.string.aspect9to16_effect));
+                        ((ImageCrop) mCurrentImage).apply(16, 9);
+                        break;
+                    case ASPECT_16TO9:
+                        ((ImageButtonTitle) mAspectButton).setText(mContext
+                                .getString(R.string.aspect)
+                                + " "
+                                + mContext.getString(R.string.aspectOriginal_effect));
+                        ((ImageCrop) mCurrentImage).applyOriginal();
+                        break;
+                    case ASPECT_ORIG:
+                        ((ImageButtonTitle) mAspectButton).setText(mContext
+                                .getString(R.string.aspect)
+                                + " "
+                                + mContext.getString(R.string.aspectNone_effect));
+                        ((ImageCrop) mCurrentImage).applyClear();
+                        break;
+                    default:
+                        ((ImageButtonTitle) mAspectButton).setText(mContext
+                                .getString(R.string.aspect)
+                                + " "
+                                + mContext.getString(R.string.aspect1to1_effect));
+                        ((ImageCrop) mCurrentImage).applyClear();
+                        break;
+                }
+                mCurrentAspectButton = (mCurrentAspectButton + 1) % NUMBER_OF_ASPECT_BUTTONS;
+            }
+        }
+
+        public void showAspectButtons() {
+            if (mAspectButton != null)
+                mAspectButton.setVisibility(View.VISIBLE);
+        }
+
+        public void hideAspectButtons() {
+            if (mAspectButton != null)
+                mAspectButton.setVisibility(View.GONE);
+        }
+
         public void onNewValue(int value) {
             mParameterValue = value;
             updateText();
@@ -131,12 +211,12 @@
 
         public void setEffectName(String effectName) {
             mEffectName = effectName;
-            showParameter(true);
-            updateText();
+            setShowParameter(true);
         }
 
-        public void showParameter(boolean s) {
+        public void setShowParameter(boolean s) {
             mShowParameterValue = s;
+            updateText();
         }
 
         public void updateText() {
@@ -229,12 +309,26 @@
         imageShow.setPanelController(this);
     }
 
+    public void resetParameters() {
+        mCurrentImage.resetParameter();
+        showPanel(mCurrentPanel);
+        mCurrentImage.select();
+    }
+
+    public boolean onBackPressed() {
+        if (mUtilityPanel == null || !mUtilityPanel.selected()) {
+            return true;
+        }
+        resetParameters();
+        return false;
+    }
+
     public void onNewValue(int value) {
         mUtilityPanel.onNewValue(value);
     }
 
     public void showParameter(boolean s) {
-        mUtilityPanel.showParameter(s);
+        mUtilityPanel.setShowParameter(s);
     }
 
     public void setCurrentPanel(View panel) {
@@ -245,8 +339,9 @@
         mRowPanel = rowPanel;
     }
 
-    public void setUtilityPanel(Context context, View utilityPanel, View textView) {
-        mUtilityPanel = new UtilityPanel(context, utilityPanel, textView);
+    public void setUtilityPanel(Context context, View utilityPanel, View textView,
+            View button) {
+        mUtilityPanel = new UtilityPanel(context, utilityPanel, textView, button);
     }
 
     public void setMasterImage(ImageShow imageShow) {
@@ -335,34 +430,45 @@
     public void ensureFilter(String name) {
         ImagePreset preset = getImagePreset();
         ImageFilter filter = preset.getFilter(name);
-        if (filter == null && name.equalsIgnoreCase("Vignette")) {
+        if (filter == null
+                && name.equalsIgnoreCase(mCurrentImage.getContext().getString(R.string.vignette))) {
             filter = setImagePreset(new ImageFilterVignette(), name);
         }
-        if (filter == null && name.equalsIgnoreCase("Sharpen")) {
+        if (filter == null
+                && name.equalsIgnoreCase(mCurrentImage.getContext().getString(R.string.sharpness))) {
             filter = setImagePreset(new ImageFilterSharpen(), name);
         }
-        if (filter == null && name.equalsIgnoreCase("Contrast")) {
+        if (filter == null
+                && name.equalsIgnoreCase(mCurrentImage.getContext().getString(R.string.contrast))) {
             filter = setImagePreset(new ImageFilterContrast(), name);
         }
-        if (filter == null && name.equalsIgnoreCase("Saturated")) {
+        if (filter == null
+                && name.equalsIgnoreCase(mCurrentImage.getContext().getString(R.string.saturation))) {
             filter = setImagePreset(new ImageFilterSaturated(), name);
         }
-        if (filter == null && name.equalsIgnoreCase("Hue")) {
+        if (filter == null
+                && name.equalsIgnoreCase(mCurrentImage.getContext().getString(R.string.hue))) {
             filter = setImagePreset(new ImageFilterHue(), name);
         }
-        if (filter == null && name.equalsIgnoreCase("Exposure")) {
+        if (filter == null
+                && name.equalsIgnoreCase(mCurrentImage.getContext().getString(R.string.exposure))) {
             filter = setImagePreset(new ImageFilterExposure(), name);
         }
-        if (filter == null && name.equalsIgnoreCase("Vibrance")) {
+        if (filter == null
+                && name.equalsIgnoreCase(mCurrentImage.getContext().getString(R.string.vibrance))) {
             filter = setImagePreset(new ImageFilterVibrance(), name);
         }
-        if (filter == null && name.equalsIgnoreCase("Shadows")) {
+        if (filter == null
+                && name.equalsIgnoreCase(mCurrentImage.getContext().getString(
+                        R.string.shadow_recovery))) {
             filter = setImagePreset(new ImageFilterShadows(), name);
         }
-        if (filter == null && name.equalsIgnoreCase("Redeye")) {
+        if (filter == null
+                && name.equalsIgnoreCase(mCurrentImage.getContext().getString(R.string.redeye))) {
             filter = setImagePreset(new ImageFilterRedEye(), name);
         }
-        if (filter == null && name.equalsIgnoreCase("WBalance")) {
+        if (filter == null
+                && name.equalsIgnoreCase(mCurrentImage.getContext().getString(R.string.wbalance))) {
             filter = setImagePreset(new ImageFilterWBalance(), name);
         }
         mMasterImage.setCurrentFilter(filter);
@@ -379,10 +485,16 @@
             }
         }
 
+        if (view.getId() == R.id.pickCurvesChannel) {
+            ImageCurves curves = (ImageCurves) showImageView(R.id.imageCurves);
+            curves.nextChannel();
+            return;
+        }
+
         if (mCurrentImage != null) {
             mCurrentImage.unselect();
         }
-
+        mUtilityPanel.hideAspectButtons();
         switch (view.getId()) {
             case R.id.straightenButton: {
                 mCurrentImage = showImageView(R.id.imageStraighten);
@@ -390,16 +502,14 @@
                 mUtilityPanel.setEffectName(ename);
                 break;
             }
-            /*
-            // TODO: implement crop
             case R.id.cropButton: {
                 mCurrentImage = showImageView(R.id.imageCrop);
                 String ename = mCurrentImage.getContext().getString(R.string.crop);
                 mUtilityPanel.setEffectName(ename);
-                mUtilityPanel.showParameter(false);
+                mUtilityPanel.setShowParameter(false);
+                mUtilityPanel.showAspectButtons();
                 break;
             }
-            */
             case R.id.rotateButton: {
                 mCurrentImage = showImageView(R.id.imageRotate);
                 String ename = mCurrentImage.getContext().getString(R.string.rotate);
@@ -408,90 +518,94 @@
             }
             case R.id.flipButton: {
                 mCurrentImage = showImageView(R.id.imageFlip);
-                String ename = mCurrentImage.getContext().getString(R.string.flip);
+                String ename = mCurrentImage.getContext().getString(R.string.mirror);
                 mUtilityPanel.setEffectName(ename);
-                mUtilityPanel.showParameter(false);
+                mUtilityPanel.setShowParameter(false);
                 break;
             }
             case R.id.vignetteButton: {
                 mCurrentImage = showImageView(R.id.imageShow).setShowControls(true);
                 String ename = mCurrentImage.getContext().getString(R.string.vignette);
                 mUtilityPanel.setEffectName(ename);
-                ensureFilter("Vignette");
+                ensureFilter(ename);
                 break;
             }
             case R.id.curvesButtonRGB: {
                 ImageCurves curves = (ImageCurves) showImageView(R.id.imageCurves);
-                String ename = mCurrentImage.getContext().getString(R.string.curvesRGB);
+                String ename = curves.getContext().getString(R.string.curvesRGB);
                 mUtilityPanel.setEffectName(ename);
-                curves.setUseRed(true);
-                curves.setUseGreen(true);
-                curves.setUseBlue(true);
+                mUtilityPanel.setShowParameter(false);
                 curves.reloadCurve();
                 mCurrentImage = curves;
                 break;
             }
             case R.id.sharpenButton: {
                 mCurrentImage = showImageView(R.id.imageZoom).setShowControls(true);
-                String ename = mCurrentImage.getContext().getString(R.string.sharpen);
+                String ename = mCurrentImage.getContext().getString(R.string.sharpness);
                 mUtilityPanel.setEffectName(ename);
-                ensureFilter("Sharpen");
+                ensureFilter(ename);
                 break;
             }
             case R.id.contrastButton: {
                 mCurrentImage = showImageView(R.id.imageShow).setShowControls(true);
                 String ename = mCurrentImage.getContext().getString(R.string.contrast);
                 mUtilityPanel.setEffectName(ename);
-                ensureFilter("Contrast");
+                ensureFilter(ename);
                 break;
             }
             case R.id.saturationButton: {
                 mCurrentImage = showImageView(R.id.imageShow).setShowControls(true);
                 String ename = mCurrentImage.getContext().getString(R.string.saturation);
                 mUtilityPanel.setEffectName(ename);
-                ensureFilter("Saturated");
+                ensureFilter(ename);
                 break;
             }
             case R.id.wbalanceButton: {
                 mCurrentImage = showImageView(R.id.imageShow).setShowControls(false);
                 String ename = mCurrentImage.getContext().getString(R.string.wbalance);
                 mUtilityPanel.setEffectName(ename);
-                ensureFilter("WBalance");
+                mUtilityPanel.setShowParameter(false);
+                ensureFilter(ename);
                 break;
             }
             case R.id.hueButton: {
                 mCurrentImage = showImageView(R.id.imageShow).setShowControls(true);
                 String ename = mCurrentImage.getContext().getString(R.string.hue);
                 mUtilityPanel.setEffectName(ename);
-                ensureFilter("Hue");
+                ensureFilter(ename);
                 break;
             }
             case R.id.exposureButton: {
                 mCurrentImage = showImageView(R.id.imageShow).setShowControls(true);
                 String ename = mCurrentImage.getContext().getString(R.string.exposure);
                 mUtilityPanel.setEffectName(ename);
-                ensureFilter("Exposure");
+                ensureFilter(ename);
                 break;
             }
             case R.id.vibranceButton: {
                 mCurrentImage = showImageView(R.id.imageShow).setShowControls(true);
                 String ename = mCurrentImage.getContext().getString(R.string.vibrance);
                 mUtilityPanel.setEffectName(ename);
-                ensureFilter("Vibrance");
+                ensureFilter(ename);
                 break;
             }
             case R.id.shadowRecoveryButton: {
                 mCurrentImage = showImageView(R.id.imageShow).setShowControls(true);
                 String ename = mCurrentImage.getContext().getString(R.string.shadow_recovery);
                 mUtilityPanel.setEffectName(ename);
-                ensureFilter("Shadows");
+                ensureFilter(ename);
                 break;
             }
             case R.id.redEyeButton: {
                 mCurrentImage = showImageView(R.id.imageShow).setShowControls(true);
                 String ename = mCurrentImage.getContext().getString(R.string.redeye);
                 mUtilityPanel.setEffectName(ename);
-                ensureFilter("Redeye");
+                ensureFilter(ename);
+                break;
+            }
+            case R.id.aspect: {
+                mUtilityPanel.nextAspectButton();
+                mUtilityPanel.showAspectButtons();
                 break;
             }
             case R.id.applyEffect: {
diff --git a/src/com/android/gallery3d/filtershow/cache/DirectPresetCache.java b/src/com/android/gallery3d/filtershow/cache/DirectPresetCache.java
index 67bd49b..25d1db4 100644
--- a/src/com/android/gallery3d/filtershow/cache/DirectPresetCache.java
+++ b/src/com/android/gallery3d/filtershow/cache/DirectPresetCache.java
@@ -62,7 +62,7 @@
     private CachedPreset getCachedPreset(ImagePreset preset) {
         for (int i = 0; i < mCache.size(); i++) {
             CachedPreset cache = mCache.elementAt(i);
-            if (cache.mPreset == preset && !cache.mBusy) {
+            if (cache.mPreset == preset) {
                 return cache;
             }
         }
@@ -73,7 +73,7 @@
     public Bitmap get(ImagePreset preset) {
         // Log.v(LOGTAG, "get preset " + preset.name() + " : " + preset);
         CachedPreset cache = getCachedPreset(preset);
-        if (cache != null) {
+        if (cache != null && !cache.mBusy) {
             return cache.mBitmap;
         }
         // Log.v(LOGTAG, "didn't find preset " + preset.name() + " : " + preset
@@ -138,18 +138,21 @@
     public void prepare(ImagePreset preset) {
         // Log.v(LOGTAG, "prepare preset " + preset.name() + " : " + preset);
         CachedPreset cache = getCachedPreset(preset);
-        if (cache == null) {
-            if (mCache.size() < mCacheSize) {
-                cache = new CachedPreset();
-                mCache.add(cache);
-            } else {
-                cache = getOldestCachedPreset();
+        if (cache == null || (cache.mBitmap == null && !cache.mBusy)) {
+            if (cache == null) {
+                if (mCache.size() < mCacheSize) {
+                    cache = new CachedPreset();
+                    mCache.add(cache);
+                } else {
+                    cache = getOldestCachedPreset();
+                }
             }
             if (cache != null) {
                 cache.mPreset = preset;
+                willCompute(cache);
             }
         }
-        willCompute(cache);
+
     }
 
 }
diff --git a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
index e00a1b7..6beaed6 100644
--- a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
+++ b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
@@ -48,14 +48,25 @@
 
     private int mOrientation = 0;
     private HistoryAdapter mAdapter = null;
-    private static int PORTRAIT_ORIENTATION = 6;
+
+    private FilterShowActivity mActivity = null;
+
+    private static final int ORI_NORMAL     = ExifInterface.ORIENTATION_NORMAL;
+    private static final int ORI_ROTATE_90  = ExifInterface.ORIENTATION_ROTATE_90;
+    private static final int ORI_ROTATE_180 = ExifInterface.ORIENTATION_ROTATE_180;
+    private static final int ORI_ROTATE_270 = ExifInterface.ORIENTATION_ROTATE_270;
+    private static final int ORI_FLIP_HOR   = ExifInterface.ORIENTATION_FLIP_HORIZONTAL;
+    private static final int ORI_FLIP_VERT  = ExifInterface.ORIENTATION_FLIP_VERTICAL;
+    private static final int ORI_TRANSPOSE  = ExifInterface.ORIENTATION_TRANSPOSE;
+    private static final int ORI_TRANSVERSE = ExifInterface.ORIENTATION_TRANSVERSE;
 
     private Context mContext = null;
     private Uri mUri = null;
 
     private Rect mOriginalBounds = null;
 
-    public ImageLoader(Context context) {
+    public ImageLoader(FilterShowActivity activity, Context context) {
+        mActivity = activity;
         mContext = context;
         mCache = new DelayedPresetCache(this, 30);
         mHiresCache = new DelayedPresetCache(this, 2);
@@ -64,8 +75,11 @@
     public void loadBitmap(Uri uri,int size) {
         mUri = uri;
         mOrientation = getOrientation(uri);
-
         mOriginalBitmapSmall = loadScaledBitmap(uri, 160);
+        if (mOriginalBitmapSmall == null) {
+            // Couldn't read the bitmap, let's exit
+            mActivity.cannotLoadImage();
+        }
         mOriginalBitmapLarge = loadScaledBitmap(uri, size);
         updateBitmaps();
     }
@@ -90,7 +104,20 @@
                         MediaStore.Images.ImageColumns.ORIENTATION
                     },
                     null, null, null);
-            return cursor.moveToNext() ? cursor.getInt(0) : -1;
+            if (cursor.moveToNext()){
+              int ori =   cursor.getInt(0);
+
+              switch (ori){
+                  case 0:   return ORI_NORMAL;
+                  case 90:  return ORI_ROTATE_90;
+                  case 270: return ORI_ROTATE_270;
+                  case 180: return ORI_ROTATE_180;
+                  default:
+                      return -1;
+              }
+            } else{
+                return -1;
+            }
         } catch (SQLiteException e){
             return ExifInterface.ORIENTATION_UNDEFINED;
         } finally {
@@ -111,18 +138,56 @@
     }
 
     private void updateBitmaps() {
+        if (mOrientation > 1) {
+            mOriginalBitmapSmall = rotateToPortrait(mOriginalBitmapSmall,mOrientation);
+            mOriginalBitmapLarge = rotateToPortrait(mOriginalBitmapLarge,mOrientation);
+        }
         mCache.setOriginalBitmap(mOriginalBitmapSmall);
         mHiresCache.setOriginalBitmap(mOriginalBitmapLarge);
-        if (mOrientation == PORTRAIT_ORIENTATION) {
-            mOriginalBitmapSmall = rotateToPortrait(mOriginalBitmapSmall);
-            mOriginalBitmapLarge = rotateToPortrait(mOriginalBitmapLarge);
-        }
         warnListeners();
     }
 
-    private Bitmap rotateToPortrait(Bitmap bitmap) {
-        Matrix matrix = new Matrix();
-        matrix.postRotate(90);
+    private Bitmap rotateToPortrait(Bitmap bitmap,int ori) {
+           Matrix matrix = new Matrix();
+           int w = bitmap.getWidth();
+           int h = bitmap.getHeight();
+           if (ori == ORI_ROTATE_90 ||
+                   ori == ORI_ROTATE_270 ||
+                   ori == ORI_TRANSPOSE||
+                   ori == ORI_TRANSVERSE) {
+               int tmp = w;
+               w = h;
+               h = tmp;
+           }
+           switch(ori){
+               case ORI_ROTATE_90:
+                   matrix.setRotate(90,w/2f,h/2f);
+                   break;
+               case ORI_ROTATE_180:
+                   matrix.setRotate(180,w/2f,h/2f);
+                   break;
+               case ORI_ROTATE_270:
+                   matrix.setRotate(270,w/2f,h/2f);
+                   break;
+               case ORI_FLIP_HOR:
+                   matrix.preScale(-1, 1);
+                   break;
+              case ORI_FLIP_VERT:
+                   matrix.preScale(1, -1);
+                   break;
+               case ORI_TRANSPOSE:
+                   matrix.setRotate(90,w/2f,h/2f);
+                   matrix.preScale(1, -1);
+                   break;
+               case ORI_TRANSVERSE:
+                   matrix.setRotate(270,w/2f,h/2f);
+                   matrix.preScale(1, -1);
+                   break;
+               case ORI_NORMAL:
+               default:
+                   return bitmap;
+            }
+
         return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
                 bitmap.getHeight(), matrix, true);
     }
@@ -295,6 +360,7 @@
             mFullOriginalBitmap = BitmapFactory.decodeStream(is, null, options);
             // TODO: on <3.x we need a copy of the bitmap (inMutable doesn't
             // exist)
+            mFullOriginalBitmap = rotateToPortrait(mFullOriginalBitmap,mOrientation);
             mSaveCopy = mFullOriginalBitmap;
             preset.setIsHighQuality(true);
             preset.setScaleFactor(1.0f);
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilter.java b/src/com/android/gallery3d/filtershow/filters/ImageFilter.java
index 6d0c020..78a8351 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilter.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilter.java
@@ -5,6 +5,9 @@
 
 public class ImageFilter implements Cloneable {
 
+    protected int mMaxParameter = 100;
+    protected int mMinParameter = -100;
+    protected int mDefaultParameter = 0;
     protected int mParameter = 0;
     protected String mName = "Original";
     private final String LOGTAG = "ImageFilter";
@@ -29,9 +32,19 @@
         filter.setName(getName());
         filter.setParameter(getParameter());
         filter.setFilterType(filterType);
+        filter.mMaxParameter = mMaxParameter;
+        filter.mMinParameter = mMinParameter;
+        filter.mDefaultParameter = mDefaultParameter;
         return filter;
     }
 
+    public boolean isNil() {
+        if (mParameter == mDefaultParameter) {
+            return true;
+        }
+        return false;
+    }
+
     public void setName(String name) {
         mName = name;
     }
@@ -53,6 +66,30 @@
         mParameter = value;
     }
 
+    /**
+     * The maximum allowed value (inclusive)
+     * @return maximum value allowed as input to this filter
+     */
+    public int getMaxParameter(){
+        return mMaxParameter;
+    }
+
+    /**
+     * The minimum allowed value (inclusive)
+     * @return minimum value allowed as input to this filter
+     */
+    public int getMinParameter(){
+        return mMinParameter;
+    }
+
+    /**
+     * Returns the default value returned by this filter.
+     * @return default value
+     */
+    public int getDefaultParameter(){
+        return mDefaultParameter;
+    }
+
     public boolean same(ImageFilter filter) {
         if (!filter.getName().equalsIgnoreCase(getName())) {
             return false;
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterBorder.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterBorder.java
index dd7d17c..4291fe4 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterBorder.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterBorder.java
@@ -23,6 +23,14 @@
     }
 
     @Override
+    public boolean isNil() {
+        if (mNinePatch == null) {
+            return  true;
+        }
+        return false;
+    }
+
+    @Override
     public boolean same(ImageFilter filter) {
         boolean isBorderFilter = super.same(filter);
         if (!isBorderFilter) {
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterCurves.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterCurves.java
index 01b280b..0f05a1c 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterCurves.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterCurves.java
@@ -9,12 +9,7 @@
 
     private static final String LOGTAG = "ImageFilterCurves";
 
-    private final float[] mCurve = new float[256];
-
-    private boolean mUseRed = true;
-    private boolean mUseGreen = true;
-    private boolean mUseBlue = true;
-    private Spline mSpline = null;
+    private final Spline[] mSplines = new Spline[4];
 
     public ImageFilterCurves() {
         mName = "Curves";
@@ -23,29 +18,22 @@
     @Override
     public ImageFilter clone() throws CloneNotSupportedException {
         ImageFilterCurves filter = (ImageFilterCurves) super.clone();
-        filter.setCurve(mCurve);
-        filter.setSpline(new Spline(mSpline));
+        for (int i = 0; i < 4; i++) {
+            if (mSplines[i] != null) {
+                filter.setSpline(new Spline(mSplines[i]), i);
+            }
+        }
         return filter;
     }
 
-    public void setUseRed(boolean value) {
-        mUseRed = value;
-    }
-
-    public void setUseGreen(boolean value) {
-        mUseGreen = value;
-    }
-
-    public void setUseBlue(boolean value) {
-        mUseBlue = value;
-    }
-
-    public void setCurve(float[] curve) {
-        for (int i = 0; i < curve.length; i++) {
-            if (i < 256) {
-                mCurve[i] = curve[i];
+    @Override
+    public boolean isNil() {
+        for (int i = 0; i < 4; i++) {
+            if (mSplines[i] != null && !mSplines[i].isOriginal()) {
+                return false;
             }
         }
+        return true;
     }
 
     @Override
@@ -55,36 +43,48 @@
             return false;
         }
         ImageFilterCurves curve = (ImageFilterCurves) filter;
-        for (int i = 0; i < 256; i++) {
-            if (curve.mCurve[i] != mCurve[i]) {
+        for (int i = 0; i < 4; i++) {
+            if (mSplines[i] != curve.mSplines[i]) {
                 return false;
             }
         }
         return true;
     }
 
-    public void populateArray(int[] array) {
+    public void populateArray(int[] array, int curveIndex) {
+        Spline spline = mSplines[curveIndex];
+        if (spline == null) {
+            return;
+        }
+        float[] curve = spline.getAppliedCurve();
         for (int i = 0; i < 256; i++) {
-            array[i] = (int) (mCurve[i]);
+            array[i] = (int) (curve[i] * 255);
         }
     }
 
     @Override
     public Bitmap apply(Bitmap bitmap, float scaleFactor, boolean highQuality) {
+        if (!mSplines[Spline.RGB].isOriginal()) {
+            int[] rgbGradient = new int[256];
+            populateArray(rgbGradient, Spline.RGB);
+            nativeApplyGradientFilter(bitmap, bitmap.getWidth(), bitmap.getHeight(),
+                    rgbGradient, rgbGradient, rgbGradient);
+        }
+
         int[] redGradient = null;
-        if (mUseRed) {
+        if (!mSplines[Spline.RED].isOriginal()) {
             redGradient = new int[256];
-            populateArray(redGradient);
+            populateArray(redGradient, Spline.RED);
         }
         int[] greenGradient = null;
-        if (mUseGreen) {
+        if (!mSplines[Spline.GREEN].isOriginal()) {
             greenGradient = new int[256];
-            populateArray(greenGradient);
+            populateArray(greenGradient, Spline.GREEN);
         }
         int[] blueGradient = null;
-        if (mUseBlue) {
+        if (!mSplines[Spline.BLUE].isOriginal()) {
             blueGradient = new int[256];
-            populateArray(blueGradient);
+            populateArray(blueGradient, Spline.BLUE);
         }
 
         nativeApplyGradientFilter(bitmap, bitmap.getWidth(), bitmap.getHeight(),
@@ -92,11 +92,11 @@
         return bitmap;
     }
 
-    public void setSpline(Spline spline) {
-        mSpline = spline;
+    public void setSpline(Spline spline, int splineIndex) {
+        mSplines[splineIndex] = spline;
     }
 
-    public Spline getSpline() {
-        return mSpline;
+    public Spline getSpline(int splineIndex) {
+        return mSplines[splineIndex];
     }
 }
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java
index 1575b18..3adfbef 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java
@@ -2,17 +2,10 @@
 package com.android.gallery3d.filtershow.filters;
 
 import android.graphics.Bitmap;
-import android.graphics.drawable.Drawable;
-import android.util.Log;
-
-import com.android.gallery3d.R;
-
-import java.util.Arrays;
 
 public class ImageFilterFx extends ImageFilter {
     private static final String TAG = "ImageFilterFx";
     Bitmap fxBitmap;
-
     public ImageFilterFx(Bitmap fxBitmap,String name) {
         setFilterType(TYPE_FX);
         mName = name;
@@ -26,9 +19,20 @@
         return filter;
     }
 
+    @Override
+    public boolean isNil() {
+        if (fxBitmap != null) {
+            return false;
+        }
+        return true;
+    }
+
     native protected void nativeApplyFilter(Bitmap bitmap, int w, int h,Bitmap  fxBitmap, int fxw, int fxh);
 
+    @Override
     public Bitmap apply(Bitmap bitmap, float scaleFactor, boolean highQuality) {
+        if (fxBitmap==null)
+            return bitmap;
 
         int w = bitmap.getWidth();
         int h = bitmap.getHeight();
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterGeometry.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterGeometry.java
index 08c09fb..3d48b7e 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterGeometry.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterGeometry.java
@@ -20,6 +20,8 @@
 import android.graphics.Canvas;
 import android.graphics.Matrix;
 import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
 
 import com.android.gallery3d.filtershow.imageshow.GeometryMetadata;
 
@@ -61,23 +63,19 @@
     native protected void nativeApplyFilterStraighten(Bitmap src, int srcWidth, int srcHeight,
             Bitmap dst, int dstWidth, int dstHeight, float straightenAngle);
 
-    public Matrix buildMatrix(Bitmap bitmap, boolean rotated) {
-        Matrix drawMatrix = new Matrix();
-        float dx = bitmap.getWidth() / 2.0f;
-        float dy = bitmap.getHeight() / 2.0f;
-
-        Matrix flipper = mGeometry.getFlipMatrix(bitmap.getWidth(), bitmap.getHeight());
-        drawMatrix.postConcat(flipper);
-        drawMatrix.postTranslate(-dx, -dy);
-        drawMatrix.postScale(1.0f / mGeometry.getScaleFactor(), 1.0f / mGeometry.getScaleFactor());
-        float angle = (mGeometry.getRotation() + mGeometry.getStraightenRotation());
-        drawMatrix.postRotate(angle);
-        if (rotated) {
-            drawMatrix.postTranslate(dy, dx);
-        } else {
-            drawMatrix.postTranslate(dx, dy);
+    public Matrix buildMatrix(RectF r) {
+        float dx = r.width()/2;
+        float dy = r.height()/2;
+        if(mGeometry.hasSwitchedWidthHeight()){
+            float temp = dx;
+            dx = dy;
+            dy = temp;
         }
-        return drawMatrix;
+        float w = r.left * 2 + r.width();
+        float h = r.top * 2 + r.height();
+        Matrix m = mGeometry.buildGeometryMatrix(w, h, 1f, dx, dy, false);
+
+        return m;
     }
 
     @Override
@@ -85,18 +83,18 @@
         // TODO: implement bilinear or bicubic here... for now, just use
         // canvas to do a simple implementation...
         // TODO: and be more memory efficient! (do it in native?)
-
+        Rect cropBounds = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+        RectF crop = mGeometry.getCropBounds(bitmap);
+        if(crop.width() > 0 && crop.height() > 0)
+            crop.roundOut(cropBounds);
         Bitmap temp = null;
-        float rotation = mGeometry.getRotation();
-        boolean rotated = false;
-        if (rotation == 0 || rotation % 180 == 0) {
-            temp = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), mConfig);
+        if (mGeometry.hasSwitchedWidthHeight()) {
+            temp = Bitmap.createBitmap(cropBounds.height(), cropBounds.width(), mConfig);
         } else {
-            temp = Bitmap.createBitmap(bitmap.getHeight(), bitmap.getWidth(), mConfig);
-            rotated = true;
+            temp = Bitmap.createBitmap(cropBounds.width(), cropBounds.height(), mConfig);
         }
 
-        Matrix drawMatrix = buildMatrix(bitmap, rotated);
+        Matrix drawMatrix = buildMatrix(crop);
         Canvas canvas = new Canvas(temp);
         canvas.drawBitmap(bitmap, drawMatrix, new Paint());
         return temp;
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java
index 6f6f9e8..e5e52ec 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java
@@ -9,6 +9,8 @@
     public ImageFilterHue() {
         mName = "Hue";
         cmatrix = new ColorSpaceMatrix();
+        mMaxParameter = 180;
+        mMinParameter = -180;
     }
 
     @Override
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterParametricBorder.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterParametricBorder.java
index 3649d28..66dad78 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterParametricBorder.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterParametricBorder.java
@@ -31,6 +31,11 @@
     }
 
     @Override
+    public boolean isNil() {
+        return false;
+    }
+
+    @Override
     public boolean same(ImageFilter filter) {
         boolean isBorderFilter = super.same(filter);
         if (!isBorderFilter) {
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java
index 9d9c7e5..5e613eb 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java
@@ -18,7 +18,6 @@
     public Bitmap apply(Bitmap bitmap, float scaleFactor, boolean highQuality) {
         int w = bitmap.getWidth();
         int h = bitmap.getHeight();
-        Log.v(TAG,"White Balance Call");
         nativeApplyFilter(bitmap, w, h, -1,-1);
         return bitmap;
     }
diff --git a/src/com/android/gallery3d/filtershow/imageshow/GeometryMath.java b/src/com/android/gallery3d/filtershow/imageshow/GeometryMath.java
new file mode 100644
index 0000000..95d174f
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/GeometryMath.java
@@ -0,0 +1,54 @@
+package com.android.gallery3d.filtershow.imageshow;
+
+public class GeometryMath {
+    protected static float clamp(float i, float low, float high) {
+        return Math.max(Math.min(i, high), low);
+    }
+
+    protected static float[] shortestVectorFromPointToLine(float[] point, float[] l1, float[] l2) {
+        float x1 = l1[0];
+        float x2 = l2[0];
+        float y1 = l1[1];
+        float y2 = l2[1];
+        float xdelt = x2 - x1;
+        float ydelt = y2 - y1;
+        if (xdelt == 0 && ydelt == 0)
+            return null;
+        float u = ((point[0] - x1) * xdelt + (point[1] - y1) * ydelt)
+                / (xdelt * xdelt + ydelt * ydelt);
+        float[] ret = {
+                (x1 + u * (x2 - x1)), (y1 + u * (y2 - y1))
+        };
+        return ret;
+    }
+
+    //A . B
+    protected static float dotProduct(float[] a, float[] b){
+        return a[0] * b[0] + a[1] * b[1];
+    }
+
+    protected static float[] normalize(float[] a){
+        float length = (float) Math.sqrt(a[0] * a[0] + a[1] * a[1]);
+        float[] b = { a[0] / length, a[1] / length };
+        return b;
+    }
+
+    //A onto B
+    protected static float scalarProjection(float[] a, float[] b){
+        float length = (float) Math.sqrt(b[0] * b[0] + b[1] * b[1]);
+        return dotProduct(a, b) / length;
+    }
+
+    protected static float[] getVectorFromPoints(float [] point1, float [] point2){
+        float [] p = { point2[0] - point1[0], point2[1] - point1[1] };
+        return p;
+    }
+
+    protected static float[] getUnitVectorFromPoints(float [] point1, float [] point2){
+        float [] p = { point2[0] - point1[0], point2[1] - point1[1] };
+        float length = (float) Math.sqrt(p[0] * p[0] + p[1] * p[1]);
+        p[0] = p[0] / length;
+        p[1] = p[1] / length;
+        return p;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java b/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java
index 0eb2e22..164b6f9 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java
@@ -22,26 +22,17 @@
 
 import com.android.gallery3d.filtershow.filters.ImageFilterGeometry;
 
-/**
- * This class holds metadata about an image's geometry. Specifically: rotation,
- * scaling, cropping, and image boundaries. It maintains the invariant that the
- * cropping boundaries are within or equal to the image boundaries (before
- * rotation) WHEN mSafe is true.
- */
-
 public class GeometryMetadata {
     // Applied in order: rotate, crop, scale.
     // Do not scale saved image (presumably?).
     private static final ImageFilterGeometry mImageFilter = new ImageFilterGeometry();
+    private static final String LOGTAG = "GeometryMetadata";
     private float mScaleFactor = 1.0f;
     private float mRotation = 0;
     private float mStraightenRotation = 0;
     private final RectF mCropBounds = new RectF();
     private final RectF mPhotoBounds = new RectF();
     private FLIP mFlip = FLIP.NONE;
-    private boolean mSafe = false;
-
-    private Matrix mMatrix = new Matrix();
 
     private RectF mBounds = new RectF();
 
@@ -56,13 +47,31 @@
         set(g);
     }
 
-    public Bitmap apply(Bitmap original, float scaleFactor, boolean highQuality){
+    public boolean hasModifications() {
+        if (mScaleFactor != 1.0f) {
+            return true;
+        }
+        if (mRotation != 0) {
+            return true;
+        }
+        if (mStraightenRotation != 0) {
+            return true;
+        }
+        if (!mCropBounds.equals(mPhotoBounds)) {
+            return true;
+        }
+        return false;
+    }
+
+    public Bitmap apply(Bitmap original, float scaleFactor, boolean highQuality) {
+        if (!hasModifications()) {
+            return original;
+        }
         mImageFilter.setGeometryMetadata(this);
         Bitmap m = mImageFilter.apply(original, scaleFactor, highQuality);
         return m;
     }
 
-    // Safe as long as invariant holds.
     public void set(GeometryMetadata g) {
         mScaleFactor = g.mScaleFactor;
         mRotation = g.mRotation;
@@ -70,8 +79,6 @@
         mCropBounds.set(g.mCropBounds);
         mPhotoBounds.set(g.mPhotoBounds);
         mFlip = g.mFlip;
-        mSafe = g.mSafe;
-        mMatrix = g.mMatrix;
         mBounds = g.mBounds;
     }
 
@@ -87,10 +94,19 @@
         return mStraightenRotation;
     }
 
-    public RectF getCropBounds() {
+    public RectF getPreviewCropBounds() {
         return new RectF(mCropBounds);
     }
 
+    public RectF getCropBounds(Bitmap bitmap) {
+        float scale = 1.0f;
+        if (mPhotoBounds.width() > 0) {
+            scale = bitmap.getWidth() / mPhotoBounds.width();
+        }
+        return new RectF(mCropBounds.left * scale, mCropBounds.top * scale,
+                mCropBounds.right * scale, mCropBounds.bottom * scale);
+    }
+
     public FLIP getFlipType() {
         return mFlip;
     }
@@ -99,10 +115,6 @@
         return new RectF(mPhotoBounds);
     }
 
-    public boolean safe() {
-        return mSafe;
-    }
-
     public void setScaleFactor(float scale) {
         mScaleFactor = scale;
     }
@@ -119,41 +131,12 @@
         mStraightenRotation = straighten;
     }
 
-    /**
-     * Sets crop bounds to be the intersection of mPhotoBounds and the new crop
-     * bounds. If there was no intersection, returns false and does not set crop
-     * bounds
-     */
-    public boolean safeSetCropBounds(RectF newCropBounds) {
-        if (mCropBounds.setIntersect(newCropBounds, mPhotoBounds)) {
-            mSafe = true;
-            return true;
-        }
-        return false;
-    }
-
     public void setCropBounds(RectF newCropBounds) {
         mCropBounds.set(newCropBounds);
-        mSafe = false;
-    }
-
-    /**
-     * Sets mPhotoBounds to be the new photo bounds and sets mCropBounds to be
-     * the intersection of the new photo bounds and the old crop bounds. Sets
-     * the crop bounds to mPhotoBounds if there is no intersection.
-     */
-
-    public void safeSetPhotoBounds(RectF newPhotoBounds) {
-        mPhotoBounds.set(newPhotoBounds);
-        if (!mCropBounds.intersect(mPhotoBounds)) {
-            mCropBounds.set(mPhotoBounds);
-        }
-        mSafe = true;
     }
 
     public void setPhotoBounds(RectF newPhotoBounds) {
         mPhotoBounds.set(newPhotoBounds);
-        mSafe = false;
     }
 
     public boolean cropFitsInPhoto(RectF cropBounds) {
@@ -171,7 +154,7 @@
         return (mScaleFactor == d.mScaleFactor &&
                 mRotation == d.mRotation &&
                 mStraightenRotation == d.mStraightenRotation &&
-                mFlip == d.mFlip && mSafe == d.mSafe &&
+                mFlip == d.mFlip &&
                 mCropBounds.equals(d.mCropBounds) && mPhotoBounds.equals(d.mPhotoBounds));
     }
 
@@ -184,15 +167,13 @@
         result = 31 * result + mFlip.hashCode();
         result = 31 * result + mCropBounds.hashCode();
         result = 31 * result + mPhotoBounds.hashCode();
-        result = 31 * result + (mSafe ? 1 : 0);
         return result;
     }
 
     @Override
     public String toString() {
         return getClass().getName() + "[" + "scale=" + mScaleFactor
-                + ",rotation=" + mRotation + ",flip=" + mFlip + ",safe="
-                + (mSafe ? "true" : "false") + ",straighten="
+                + ",rotation=" + mRotation + ",flip=" + mFlip + ",straighten="
                 + mStraightenRotation + ",cropRect=" + mCropBounds.toShortString()
                 + ",photoRect=" + mPhotoBounds.toShortString() + "]";
     }
@@ -228,7 +209,34 @@
         }
     }
 
-    public Matrix getMatrix() {
-        return mMatrix;
+    public boolean hasSwitchedWidthHeight() {
+        return (((int) (mRotation / 90)) % 2) != 0;
+    }
+
+    public Matrix buildGeometryMatrix(float width, float height, float scaling, float dx, float dy,
+            float rotation) {
+        float dx0 = width / 2;
+        float dy0 = height / 2;
+        Matrix m = getFlipMatrix(width, height);
+        m.postTranslate(-dx0, -dy0);
+        m.postRotate(rotation);
+        m.postScale(scaling, scaling);
+        m.postTranslate(dx, dy);
+        return m;
+    }
+
+    public Matrix buildGeometryMatrix(float width, float height, float scaling, float dx, float dy,
+            boolean onlyRotate) {
+        float rot = mRotation;
+        if (!onlyRotate) {
+            rot += mStraightenRotation;
+        }
+        return buildGeometryMatrix(width, height, scaling, dx, dy, rot);
+    }
+
+    public Matrix buildGeometryUIMatrix(float scaling, float dx, float dy) {
+        float w = mPhotoBounds.width();
+        float h = mPhotoBounds.height();
+        return buildGeometryMatrix(w, h, scaling, dx, dy, false);
     }
 }
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java b/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java
index 90d36e9..e34e3a2 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java
@@ -22,12 +22,10 @@
 import android.graphics.Canvas;
 import android.graphics.Matrix;
 import android.graphics.Paint;
-import android.graphics.Path;
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.util.Log;
-
 import com.android.gallery3d.R;
 
 public class ImageCrop extends ImageGeometry {
@@ -38,21 +36,22 @@
     private static final int MOVE_BOTTOM = 8;
     private static final int MOVE_BLOCK = 16;
 
+    //Corners
+    private static final int TOP_LEFT = MOVE_TOP | MOVE_LEFT;
+    private static final int TOP_RIGHT = MOVE_TOP | MOVE_RIGHT;
+    private static final int BOTTOM_RIGHT = MOVE_BOTTOM | MOVE_RIGHT;
+    private static final int BOTTOM_LEFT = MOVE_BOTTOM | MOVE_LEFT;
+
     private static final float MIN_CROP_WIDTH_HEIGHT = 0.1f;
     private static final int TOUCH_TOLERANCE = 30;
-    private static final int SHADOW_ALPHA = 160;
 
-    private final float mAspectWidth = 4;
-    private final float mAspectHeight = 3;
-    private final boolean mFixAspectRatio = false; // not working yet
+    private boolean mFirstDraw = true;
+    private float mAspectWidth = 1;
+    private float mAspectHeight = 1;
+    private boolean mFixAspectRatio = false;
 
     private final Paint borderPaint;
 
-    private float mCropOffsetX = 0;
-    private float mCropOffsetY = 0;
-    private float mPrevOffsetX = 0;
-    private float mPrevOffsetY = 0;
-
     private int movingEdges;
     private final Drawable cropIndicator;
     private final int indicatorSize;
@@ -85,23 +84,133 @@
         borderPaint.setStrokeWidth(2f);
     }
 
+    private boolean switchCropBounds(int moving_corner, RectF dst) {
+        RectF crop = getCropBoundsDisplayed();
+        float dx1 = 0;
+        float dy1 = 0;
+        float dx2 = 0;
+        float dy2 = 0;
+        if ((moving_corner & MOVE_RIGHT) != 0) {
+            dx1 = mCurrentX - crop.right;
+        } else if ((moving_corner & MOVE_LEFT) != 0) {
+            dx1 = mCurrentX - crop.left;
+        }
+        if ((moving_corner & MOVE_BOTTOM) != 0) {
+            dy1 = mCurrentY - crop.bottom;
+        } else if ((moving_corner & MOVE_TOP) != 0) {
+            dy1 = mCurrentY - crop.top;
+        }
+        RectF newCrop = null;
+        //Fix opposite corner in place and move sides
+        if (moving_corner == BOTTOM_RIGHT) {
+            newCrop = new RectF(crop.left, crop.top, crop.left + crop.height(), crop.top
+                    + crop.width());
+        } else if (moving_corner == BOTTOM_LEFT) {
+            newCrop = new RectF(crop.right - crop.height(), crop.top, crop.right, crop.top
+                    + crop.width());
+        } else if (moving_corner == TOP_LEFT) {
+            newCrop = new RectF(crop.right - crop.height(), crop.bottom - crop.width(),
+                    crop.right, crop.bottom);
+        } else if (moving_corner == TOP_RIGHT) {
+            newCrop = new RectF(crop.left, crop.bottom - crop.width(), crop.left
+                    + crop.height(), crop.bottom);
+        }
+        if ((moving_corner & MOVE_RIGHT) != 0) {
+            dx2 = mCurrentX - newCrop.right;
+        } else if ((moving_corner & MOVE_LEFT) != 0) {
+            dx2 = mCurrentX - newCrop.left;
+        }
+        if ((moving_corner & MOVE_BOTTOM) != 0) {
+            dy2 = mCurrentY - newCrop.bottom;
+        } else if ((moving_corner & MOVE_TOP) != 0) {
+            dy2 = mCurrentY - newCrop.top;
+        }
+        if (Math.sqrt(dx1*dx1 + dy1*dy1) > Math.sqrt(dx2*dx2 + dy2*dy2)){
+             Matrix m = getCropBoundDisplayMatrix();
+             Matrix m0 = new Matrix();
+             if (!m.invert(m0)){
+                 if (LOGV)
+                     Log.v(LOGTAG, "FAILED TO INVERT CROP MATRIX");
+                 return false;
+             }
+             if (!m0.mapRect(newCrop)){
+                 if (LOGV)
+                     Log.v(LOGTAG, "FAILED TO MAP RECTANGLE TO RECTANGLE");
+                 return false;
+             }
+             float temp = mAspectWidth;
+             mAspectWidth = mAspectHeight;
+             mAspectHeight = temp;
+             dst.set(newCrop);
+             return true;
+        }
+        return false;
+    }
+
+    public void apply(float w, float h){
+        mFixAspectRatio = true;
+        mAspectWidth = w;
+        mAspectHeight = h;
+        setLocalCropBounds(getUntranslatedStraightenCropBounds(getLocalPhotoBounds(),
+                getLocalStraighten()));
+        cropSetup();
+        saveAndSetPreset();
+        invalidate();
+    }
+
+    public void applyOriginal() {
+        mFixAspectRatio = true;
+        RectF photobounds = getLocalPhotoBounds();
+        float w = photobounds.width();
+        float h = photobounds.height();
+        float scale = Math.min(w, h);
+        mAspectWidth = w / scale;
+        mAspectHeight = h / scale;
+        setLocalCropBounds(getUntranslatedStraightenCropBounds(photobounds,
+                getLocalStraighten()));
+        cropSetup();
+        saveAndSetPreset();
+        invalidate();
+    }
+
+    public void applyClear() {
+        mFixAspectRatio = false;
+        setLocalCropBounds(getUntranslatedStraightenCropBounds(getLocalPhotoBounds(),
+                getLocalStraighten()));
+        cropSetup();
+        saveAndSetPreset();
+        invalidate();
+    }
+
     private float getScaledMinWidthHeight() {
-        RectF disp = getLocalDisplayBounds();
+        RectF disp = new RectF(0, 0, getWidth(), getHeight());
         float scaled = Math.min(disp.width(), disp.height()) * MIN_CROP_WIDTH_HEIGHT
-                / getLocalScale();
+                / computeScale(getWidth(), getHeight());
         return scaled;
     }
 
-    protected static Matrix getCropRotationMatrix(float rotation, RectF localImage) {
-        Matrix m = new Matrix();
-        m.setRotate(rotation, localImage.centerX(), localImage.centerY());
+    protected Matrix getCropRotationMatrix(float rotation, RectF localImage) {
+        Matrix m = getLocalGeoFlipMatrix(localImage.width(), localImage.height());
+        m.postRotate(rotation, localImage.centerX(), localImage.centerY());
         if (!m.rectStaysRect()) {
             return null;
         }
         return m;
     }
 
-    @Override
+    protected Matrix getCropBoundDisplayMatrix(){
+        Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds());
+        if (m == null) {
+            if (LOGV)
+                Log.v(LOGTAG, "FAILED TO MAP CROP BOUNDS TO RECTANGLE");
+            m = new Matrix();
+        }
+        float zoom = computeScale(getWidth(), getHeight());
+        m.postTranslate(mXOffset, mYOffset);
+        m.postScale(zoom, zoom, mCenterX, mCenterY);
+        return m;
+    }
+
     protected RectF getCropBoundsDisplayed() {
         RectF bounds = getLocalCropBounds();
         RectF crop = new RectF(bounds);
@@ -115,7 +224,7 @@
             m.mapRect(crop);
         }
         m = new Matrix();
-        float zoom = getLocalScale();
+        float zoom = computeScale(getWidth(), getHeight());
         m.setScale(zoom, zoom, mCenterX, mCenterY);
         m.preTranslate(mXOffset, mYOffset);
         m.mapRect(crop);
@@ -137,6 +246,44 @@
         return crop;
     }
 
+    private RectF getUnrotatedCropBounds(RectF cropBounds) {
+        Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds());
+
+        if (m == null) {
+            if (LOGV)
+                Log.v(LOGTAG, "FAILED TO GET ROTATION MATRIX");
+            return null;
+        }
+        Matrix m0 = new Matrix();
+        if (!m.invert(m0)) {
+            if (LOGV)
+                Log.v(LOGTAG, "FAILED TO INVERT ROTATION MATRIX");
+            return null;
+        }
+        RectF crop = new RectF(cropBounds);
+        if (!m0.mapRect(crop)) {
+            if (LOGV)
+                Log.v(LOGTAG, "FAILED TO UNROTATE CROPPING BOUNDS");
+            return null;
+        }
+        return crop;
+    }
+
+    private RectF getRotatedStraightenBounds() {
+        RectF straightenBounds = getUntranslatedStraightenCropBounds(getLocalPhotoBounds(),
+                getLocalStraighten());
+        Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds());
+
+        if (m == null) {
+            if (LOGV)
+                Log.v(LOGTAG, "FAILED TO MAP STRAIGHTEN BOUNDS TO RECTANGLE");
+            return null;
+        } else {
+            m.mapRect(straightenBounds);
+        }
+        return straightenBounds;
+    }
+
     /**
      * Sets cropped bounds; modifies the bounds if it's smaller than the allowed
      * dimensions.
@@ -145,16 +292,33 @@
         // Avoid cropping smaller than minimum width or height.
         RectF cbounds = new RectF(bounds);
         float minWidthHeight = getScaledMinWidthHeight();
+        float aw = mAspectWidth;
+        float ah = mAspectHeight;
+        if (mFixAspectRatio) {
+            minWidthHeight /= aw * ah;
+            int r = (int) (getLocalRotation() / 90);
+            if (r % 2 != 0) {
+                float temp = aw;
+                aw = ah;
+                ah = temp;
+            }
+        }
 
         float newWidth = cbounds.width();
         float newHeight = cbounds.height();
-        if (newWidth < minWidthHeight) {
-            newWidth = minWidthHeight;
+        if (mFixAspectRatio) {
+            if (newWidth < (minWidthHeight * aw) || newHeight < (minWidthHeight * ah)) {
+                newWidth = minWidthHeight * aw;
+                newHeight = minWidthHeight * ah;
+            }
+        } else {
+            if (newWidth < minWidthHeight) {
+                newWidth = minWidthHeight;
+            }
+            if (newHeight < minWidthHeight) {
+                newHeight = minWidthHeight;
+            }
         }
-        if (newHeight < minWidthHeight) {
-            newHeight = minWidthHeight;
-        }
-
         RectF pbounds = getLocalPhotoBounds();
         if (pbounds.width() < minWidthHeight) {
             newWidth = pbounds.width();
@@ -164,18 +328,14 @@
         }
 
         cbounds.set(cbounds.left, cbounds.top, cbounds.left + newWidth, cbounds.top + newHeight);
-        RectF snappedCrop = findCropBoundForRotatedImg(cbounds, pbounds, getLocalStraighten(),
-                mCenterX - mXOffset, mCenterY - mYOffset);
-
-        RectF straightenBounds = getUntranslatedStraightenCropBounds(getLocalPhotoBounds(), getLocalStraighten());
-        snappedCrop.intersect(straightenBounds);
-
+        RectF straightenBounds = getUntranslatedStraightenCropBounds(getLocalPhotoBounds(),
+                getLocalStraighten());
+        cbounds.intersect(straightenBounds);
 
         if (mFixAspectRatio) {
-            // TODO: add aspect ratio stuff
-            fixAspectRatio(snappedCrop, mAspectWidth, mAspectHeight);
+            fixAspectRatio(cbounds, aw, ah);
         }
-        setLocalCropBounds(snappedCrop);
+        setLocalCropBounds(cbounds);
         invalidate();
     }
 
@@ -202,35 +362,150 @@
         else if (bottom <= TOUCH_TOLERANCE) {
             movingEdges |= MOVE_BOTTOM;
         }
+        // Check inside block.
+        if (cropped.contains(x, y) && (movingEdges == 0)) {
+            movingEdges = MOVE_BLOCK;
+        }
+        if (mFixAspectRatio && (movingEdges != MOVE_BLOCK)) {
+            movingEdges = fixEdgeToCorner(movingEdges);
+        }
         invalidate();
     }
 
+    private int fixEdgeToCorner(int moving_edges){
+        if (moving_edges == MOVE_LEFT) {
+            moving_edges |= MOVE_TOP;
+        }
+        if (moving_edges == MOVE_TOP) {
+            moving_edges |= MOVE_LEFT;
+        }
+        if (moving_edges == MOVE_RIGHT) {
+            moving_edges |= MOVE_BOTTOM;
+        }
+        if (moving_edges == MOVE_BOTTOM) {
+            moving_edges |= MOVE_RIGHT;
+        }
+        return moving_edges;
+    }
+
+    private RectF fixedCornerResize(RectF r, int moving_corner, float dx, float dy){
+        RectF newCrop = null;
+        //Fix opposite corner in place and move sides
+        if (moving_corner == BOTTOM_RIGHT) {
+            newCrop = new RectF(r.left, r.top, r.left + r.width() + dx, r.top + r.height()
+                    + dy);
+        } else if (moving_corner == BOTTOM_LEFT) {
+            newCrop = new RectF(r.right - r.width() + dx, r.top, r.right, r.top + r.height()
+                    + dy);
+        } else if (moving_corner == TOP_LEFT) {
+            newCrop = new RectF(r.right - r.width() + dx, r.bottom - r.height() + dy,
+                    r.right, r.bottom);
+        } else if (moving_corner == TOP_RIGHT) {
+            newCrop = new RectF(r.left, r.bottom - r.height() + dy, r.left
+                    + r.width() + dx, r.bottom);
+        }
+        return newCrop;
+    }
+
     private void moveEdges(float dX, float dY) {
         RectF cropped = getRotatedCropBounds();
         float minWidthHeight = getScaledMinWidthHeight();
-        float scale = getLocalScale();
+        float scale = computeScale(getWidth(), getHeight());
         float deltaX = dX / scale;
         float deltaY = dY / scale;
-        if (movingEdges == MOVE_BLOCK) {
-            // TODO
-        } else {
-            if ((movingEdges & MOVE_LEFT) != 0) {
-                cropped.left = Math.min(cropped.left + deltaX, cropped.right - minWidthHeight);
-                fixRectAspectW(cropped);
+        int select = movingEdges;
+        if (mFixAspectRatio && (select != MOVE_BLOCK)) {
+            if (select == MOVE_LEFT) {
+                select |= MOVE_TOP;
             }
-            if ((movingEdges & MOVE_TOP) != 0) {
-                cropped.top = Math.min(cropped.top + deltaY, cropped.bottom - minWidthHeight);
-                fixRectAspectH(cropped);
+            if (select == MOVE_TOP) {
+                select |= MOVE_LEFT;
             }
-            if ((movingEdges & MOVE_RIGHT) != 0) {
-                cropped.right = Math.max(cropped.right + deltaX, cropped.left + minWidthHeight);
-                fixRectAspectW(cropped);
+            if (select == MOVE_RIGHT) {
+                select |= MOVE_BOTTOM;
             }
-            if ((movingEdges & MOVE_BOTTOM) != 0) {
-                cropped.bottom = Math.max(cropped.bottom + deltaY, cropped.top + minWidthHeight);
-                fixRectAspectH(cropped);
+            if (select == MOVE_BOTTOM) {
+                select |= MOVE_RIGHT;
+            }
+            RectF blank = new RectF();
+            if(switchCropBounds(select, blank)){
+                setCropBounds(blank);
+                return;
             }
         }
+
+        if (select == MOVE_BLOCK) {
+            RectF straight = getRotatedStraightenBounds();
+            // Move the whole cropped bounds within the photo display bounds.
+            deltaX = (deltaX > 0) ? Math.min(straight.right - cropped.right, deltaX)
+                    : Math.max(straight.left - cropped.left, deltaX);
+            deltaY = (deltaY > 0) ? Math.min(straight.bottom - cropped.bottom, deltaY)
+                    : Math.max(straight.top - cropped.top, deltaY);
+            cropped.offset(deltaX, deltaY);
+        } else {
+            float dx = 0;
+            float dy = 0;
+
+            if ((select & MOVE_LEFT) != 0) {
+                dx = Math.min(cropped.left + deltaX, cropped.right - minWidthHeight) - cropped.left;
+            }
+            if ((select & MOVE_TOP) != 0) {
+                dy = Math.min(cropped.top + deltaY, cropped.bottom - minWidthHeight) - cropped.top;
+            }
+            if ((select & MOVE_RIGHT) != 0) {
+                dx = Math.max(cropped.right + deltaX, cropped.left + minWidthHeight)
+                        - cropped.right;
+            }
+            if ((select & MOVE_BOTTOM) != 0) {
+                dy = Math.max(cropped.bottom + deltaY, cropped.top + minWidthHeight)
+                        - cropped.bottom;
+            }
+
+            if (mFixAspectRatio) {
+                RectF crop = getCropBoundsDisplayed();
+                float [] l1 = {crop.left, crop.bottom};
+                float [] l2 = {crop.right, crop.top};
+                if(movingEdges == TOP_LEFT || movingEdges == BOTTOM_RIGHT){
+                    l1[1] = crop.top;
+                    l2[1] = crop.bottom;
+                }
+                float[] b = { l1[0] - l2[0], l1[1] - l2[1] };
+                float[] disp = {dx, dy};
+                float[] bUnit = GeometryMath.normalize(b);
+                float sp = GeometryMath.scalarProjection(disp, bUnit);
+                dx = sp * bUnit[0];
+                dy = sp * bUnit[1];
+                RectF newCrop = fixedCornerResize(crop, select, dx * scale, dy * scale);
+                Matrix m = getCropBoundDisplayMatrix();
+                Matrix m0 = new Matrix();
+                if (!m.invert(m0)){
+                    if (LOGV)
+                        Log.v(LOGTAG, "FAILED TO INVERT CROP MATRIX");
+                    return;
+                }
+                if (!m0.mapRect(newCrop)){
+                    if (LOGV)
+                        Log.v(LOGTAG, "FAILED TO MAP RECTANGLE TO RECTANGLE");
+                    return;
+                }
+                setCropBounds(newCrop);
+                return;
+            } else {
+                if ((select & MOVE_LEFT) != 0) {
+                    cropped.left += dx;
+                }
+                if ((select & MOVE_TOP) != 0) {
+                    cropped.top += dy;
+                }
+                if ((select & MOVE_RIGHT) != 0) {
+                    cropped.right += dx;
+                }
+                if ((select & MOVE_BOTTOM) != 0) {
+                    cropped.bottom += dy;
+                }
+            }
+        }
+        movingEdges = select;
         Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds());
         Matrix m0 = new Matrix();
         if (!m.invert(m0)) {
@@ -244,24 +519,6 @@
         setCropBounds(cropped);
     }
 
-    private void fixRectAspectH(RectF cropped) {
-        if (mFixAspectRatio) {
-            float half = getNewWidthForHeightAspect(cropped.height(), mAspectWidth, mAspectHeight) / 2;
-            float mid = (cropped.right - cropped.left) / 2;
-            cropped.left = mid - half;
-            cropped.right = mid + half;
-        }
-    }
-
-    private void fixRectAspectW(RectF cropped) {
-        if (mFixAspectRatio) {
-            float half = getNewHeightForWidthAspect(cropped.width(), mAspectWidth, mAspectHeight) / 2;
-            float mid = (cropped.bottom - cropped.top) / 2;
-            cropped.top = mid - half;
-            cropped.bottom = mid + half;
-        }
-    }
-
     private void drawIndicator(Canvas canvas, Drawable indicator, float centerX, float centerY) {
         int left = (int) centerX - indicatorSize / 2;
         int top = (int) centerY - indicatorSize / 2;
@@ -273,81 +530,111 @@
     protected void setActionDown(float x, float y) {
         super.setActionDown(x, y);
         detectMovingEdges(x, y);
-        if (movingEdges == 0) {
-            mPrevOffsetX = mCropOffsetX;
-            mPrevOffsetY = mCropOffsetY;
-        }
+    }
+
+    @Override
+    protected void setActionUp() {
+        super.setActionUp();
+        movingEdges = 0;
     }
 
     @Override
     protected void setActionMove(float x, float y) {
-        if (movingEdges != 0) {
+        if (movingEdges != 0){
             moveEdges(x - mCurrentX, y - mCurrentY);
-        } else {
-            float dx = x - mTouchCenterX;
-            float dy = y - mTouchCenterY;
-            mCropOffsetX = dx + mPrevOffsetX;
-            mCropOffsetY = dy + mPrevOffsetY;
         }
         super.setActionMove(x, y);
     }
 
-    @Override
-    protected void gainedVisibility() {
-        setCropBounds(getLocalCropBounds());
-        super.gainedVisibility();
+    private void cropSetup() {
+        if (mFixAspectRatio) {
+            RectF cb = getRotatedCropBounds();
+            fixAspectRatio(cb, mAspectWidth, mAspectHeight);
+            RectF cb0 = getUnrotatedCropBounds(cb);
+            setCropBounds(cb0);
+        } else {
+            setCropBounds(getLocalCropBounds());
+        }
     }
 
-    protected RectF drawCrop(Canvas canvas, Paint p, RectF cropBounds, float scale,
-            float rotation, float centerX, float centerY, float offsetX, float offsetY) {
-        RectF crop = new RectF(cropBounds);
-        Matrix m = new Matrix();
-        m.preTranslate(offsetX, offsetY);
-        m.mapRect(crop);
+    @Override
+    protected void gainedVisibility() {
+        cropSetup();
+        mFirstDraw = true;
+    }
 
-        m.setRotate(rotation, centerX, centerY);
-        if (!m.rectStaysRect()) {
-            float[] corners = getCornersFromRect(crop);
-            m.mapPoints(corners);
-            drawClosedPath(canvas, p, corners);
-        } else {
-            RectF crop2 = new RectF(crop);
-            m.mapRect(crop2);
-            Path path = new Path();
-            path.addRect(crop2, Path.Direction.CCW);
-            canvas.drawPath(path, p);
-        }
-        return crop;
+    @Override
+    public void resetParameter() {
+        super.resetParameter();
+        cropSetup();
+    }
+
+    @Override
+    protected void lostVisibility() {
     }
 
     @Override
     protected void drawShape(Canvas canvas, Bitmap image) {
+        // TODO: move style to xml
         gPaint.setAntiAlias(true);
         gPaint.setFilterBitmap(true);
         gPaint.setDither(true);
         gPaint.setARGB(255, 255, 255, 255);
-        drawTransformedBitmap(canvas, image, gPaint, false);
 
-        float scale = getLocalScale();
+        if (mFirstDraw) {
+            cropSetup();
+            mFirstDraw = false;
+        }
         float rotation = getLocalRotation();
+        drawTransformedBitmap(canvas, image, gPaint, true);
 
-        RectF scaledCrop = drawCrop(canvas, gPaint, getLocalCropBounds(), scale,
-                rotation, mCenterX, mCenterY, mXOffset,
-                mYOffset);
-
-        boolean notMoving = movingEdges == 0;
-        if (((movingEdges & MOVE_TOP) != 0) || notMoving) {
+        gPaint.setARGB(255, 125, 255, 128);
+        gPaint.setStrokeWidth(3);
+        gPaint.setStyle(Paint.Style.STROKE);
+        drawStraighten(canvas, gPaint);
+        RectF scaledCrop = unrotatedCropBounds();
+        int decoded_moving = decoder(movingEdges, rotation);
+        canvas.save();
+        canvas.rotate(rotation, mCenterX, mCenterY);
+        boolean notMoving = decoded_moving == 0;
+        if (((decoded_moving & MOVE_TOP) != 0) || notMoving) {
             drawIndicator(canvas, cropIndicator, scaledCrop.centerX(), scaledCrop.top);
         }
-        if (((movingEdges & MOVE_BOTTOM) != 0) || notMoving) {
+        if (((decoded_moving & MOVE_BOTTOM) != 0) || notMoving) {
             drawIndicator(canvas, cropIndicator, scaledCrop.centerX(), scaledCrop.bottom);
         }
-        if (((movingEdges & MOVE_LEFT) != 0) || notMoving) {
+        if (((decoded_moving & MOVE_LEFT) != 0) || notMoving) {
             drawIndicator(canvas, cropIndicator, scaledCrop.left, scaledCrop.centerY());
         }
-        if (((movingEdges & MOVE_RIGHT) != 0) || notMoving) {
+        if (((decoded_moving & MOVE_RIGHT) != 0) || notMoving) {
             drawIndicator(canvas, cropIndicator, scaledCrop.right, scaledCrop.centerY());
         }
+        canvas.restore();
     }
 
+    private int bitCycleLeft(int x, int times, int d) {
+        int mask = (1 << d) - 1;
+        int mout = x & mask;
+        times %= d;
+        int hi = mout >> (d - times);
+        int low = (mout << times) & mask;
+        int ret = x & ~mask;
+        ret |= low;
+        ret |= hi;
+        return ret;
+    }
+
+    protected int decoder(int movingEdges, float rotation) {
+        int rot = constrainedRotation(rotation);
+        switch (rot) {
+            case 90:
+                return bitCycleLeft(movingEdges, 3, 4);
+            case 180:
+                return bitCycleLeft(movingEdges, 2, 4);
+            case 270:
+                return bitCycleLeft(movingEdges, 1, 4);
+            default:
+                return movingEdges;
+        }
+    }
 }
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageFlip.java b/src/com/android/gallery3d/filtershow/imageshow/ImageFlip.java
index 3408405..00b9aed 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageFlip.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageFlip.java
@@ -45,6 +45,11 @@
         super.setActionDown(x, y);
     }
 
+    boolean hasRotated90(){
+        int rot = constrainedRotation(getLocalRotation());
+        return ((int) (rot / 90)) % 2 != 0;
+    }
+
     @Override
     protected void setActionMove(float x, float y) {
         super.setActionMove(x, y);
@@ -52,6 +57,11 @@
         float diffx = mTouchCenterX - x;
         float diffy = mTouchCenterY - y;
         float flick = getScaledMinFlick();
+        if(hasRotated90()){
+            float temp = diffx;
+            diffx = diffy;
+            diffy = temp;
+        }
         if (Math.abs(diffx) >= flick) {
             // flick moving left/right
             FLIP flip = getLocalFlip();
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageGeometry.java b/src/com/android/gallery3d/filtershow/imageshow/ImageGeometry.java
index 98f892e..d5a7ada 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageGeometry.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageGeometry.java
@@ -26,12 +26,12 @@
 import android.graphics.Path;
 import android.graphics.RectF;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 
 import com.android.gallery3d.filtershow.imageshow.GeometryMetadata.FLIP;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
+import com.android.gallery3d.filtershow.imageshow.GeometryMath;
 
 public abstract class ImageGeometry extends ImageSlave {
     private boolean mVisibilityGained = false;
@@ -48,10 +48,8 @@
     protected float mTouchCenterX;
     protected float mTouchCenterY;
 
-    private Matrix mLocalMatrix = null;
-
     // Local geometry data
-    private GeometryMetadata mLocalGeoMetadata = null;
+    private GeometryMetadata mLocalGeometry = null;
     private RectF mLocalDisplayBounds = null;
     protected float mXOffset = 0;
     protected float mYOffset = 0;
@@ -77,9 +75,38 @@
         calculateLocalScalingFactorAndOffset();
     }
 
-    private float computeScale(float width, float height) {
-        float imageWidth = mLocalGeoMetadata.getPhotoBounds().width();
-        float imageHeight = mLocalGeoMetadata.getPhotoBounds().height();
+    protected static float angleFor(float dx, float dy) {
+        return (float) (Math.atan2(dx, dy) * 180 / Math.PI);
+    }
+
+    protected static int snappedAngle(float angle) {
+        float remainder = angle % 90;
+        int current = (int) (angle / 90); // truncates
+        if (remainder < -45) {
+            --current;
+        } else if (remainder > 45) {
+            ++current;
+        }
+        return current * 90;
+    }
+
+    protected float getCurrentTouchAngle(){
+        if (mCurrentX == mTouchCenterX && mCurrentY == mTouchCenterY) {
+            return 0;
+        }
+        float dX1 = mTouchCenterX - mCenterX;
+        float dY1 = mTouchCenterY - mCenterY;
+        float dX2 = mCurrentX - mCenterX;
+        float dY2 = mCurrentY - mCenterY;
+
+        float angleA = angleFor(dX1, dY1);
+        float angleB = angleFor(dX2, dY2);
+        return (angleB - angleA) % 360;
+    }
+
+    protected float computeScale(float width, float height) {
+        float imageWidth = mLocalGeometry.getPhotoBounds().width();
+        float imageHeight = mLocalGeometry.getPhotoBounds().height();
         float zoom = width / imageWidth;
         if (imageHeight > imageWidth) {
             zoom = height / imageHeight;
@@ -88,9 +115,9 @@
     }
 
     private void calculateLocalScalingFactorAndOffset() {
-        if (mLocalGeoMetadata == null || mLocalDisplayBounds == null)
+        if (mLocalGeometry == null || mLocalDisplayBounds == null)
             return;
-        RectF imageBounds = mLocalGeoMetadata.getPhotoBounds();
+        RectF imageBounds = mLocalGeometry.getPhotoBounds();
         float imageWidth = imageBounds.width();
         float imageHeight = imageBounds.height();
         float displayWidth = mLocalDisplayBounds.width();
@@ -100,9 +127,7 @@
         mCenterY = displayHeight / 2;
         mYOffset = (displayHeight - imageHeight) / 2.0f;
         mXOffset = (displayWidth - imageWidth) / 2.0f;
-
-        float zoom = computeScale(mLocalDisplayBounds.width(), mLocalDisplayBounds.height());
-        mLocalGeoMetadata.setScaleFactor(zoom);
+        updateScale();
     }
 
     @Override
@@ -118,21 +143,16 @@
 
     // Overwrites local with master
     protected void syncLocalToMasterGeometry() {
-        mLocalGeoMetadata = getMaster().getGeometry();
+        mLocalGeometry = getMaster().getGeometry();
         calculateLocalScalingFactorAndOffset();
-        mLocalMatrix = mLocalGeoMetadata.getMatrix();
-    }
-
-    public Matrix getLocalMatrix() {
-        return mLocalMatrix;
     }
 
     protected RectF getLocalPhotoBounds() {
-        return mLocalGeoMetadata.getPhotoBounds();
+        return mLocalGeometry.getPhotoBounds();
     }
 
     protected RectF getLocalCropBounds() {
-        return mLocalGeoMetadata.getCropBounds();
+        return mLocalGeometry.getPreviewCropBounds();
     }
 
     protected RectF getLocalDisplayBounds() {
@@ -140,59 +160,62 @@
     }
 
     protected float getLocalScale() {
-        return mLocalGeoMetadata.getScaleFactor();
+        return mLocalGeometry.getScaleFactor();
     }
 
     protected float getLocalRotation() {
-        return mLocalGeoMetadata.getRotation();
+        return mLocalGeometry.getRotation();
     }
 
     protected float getLocalStraighten() {
-        return mLocalGeoMetadata.getStraightenRotation();
+        return mLocalGeometry.getStraightenRotation();
     }
 
     protected void setLocalScale(float s) {
-        mLocalGeoMetadata.setScaleFactor(s);
+        mLocalGeometry.setScaleFactor(s);
     }
 
-    protected void updateMatrix() {
-        RectF bounds = getUntranslatedStraightenCropBounds(mLocalGeoMetadata.getPhotoBounds(),
+    protected void updateScale() {
+        RectF bounds = getUntranslatedStraightenCropBounds(mLocalGeometry.getPhotoBounds(),
                 getLocalStraighten());
         float zoom = computeScale(bounds.width(), bounds.height());
         setLocalScale(zoom);
-        float w = mLocalGeoMetadata.getPhotoBounds().width();
-        float h = mLocalGeoMetadata.getPhotoBounds().height();
-        float ratio = h / w;
-        float rcenterx = 0.5f;
-        float rcentery = 0.5f * ratio;
-        Matrix flipper = mLocalGeoMetadata.getFlipMatrix(1.0f, ratio);
-        mLocalMatrix.reset();
-        mLocalMatrix.postConcat(flipper);
-        mLocalMatrix.postRotate(getTotalLocalRotation(), rcenterx, rcentery);
-        invalidate();
     }
 
     protected void setLocalRotation(float r) {
-        mLocalGeoMetadata.setRotation(r);
-        updateMatrix();
+        mLocalGeometry.setRotation(r);
+        updateScale();
+    }
+
+    /**
+     * Constrains rotation to be in [0, 90, 180, 270].
+     */
+    protected int constrainedRotation(float rotation) {
+        int r = (int) ((rotation % 360) / 90);
+        r = (r < 0) ? (r + 4) : r;
+        return r * 90;
+    }
+
+    protected Matrix getLocalGeoFlipMatrix(float width, float height) {
+        return mLocalGeometry.getFlipMatrix(width, height);
     }
 
     protected void setLocalStraighten(float r) {
-        mLocalGeoMetadata.setStraightenRotation(r);
-        updateMatrix();
+        mLocalGeometry.setStraightenRotation(r);
+        updateScale();
     }
 
     protected void setLocalCropBounds(RectF c) {
-        mLocalGeoMetadata.setCropBounds(c);
+        mLocalGeometry.setCropBounds(c);
+        updateScale();
     }
 
     protected FLIP getLocalFlip() {
-        return mLocalGeoMetadata.getFlipType();
+        return mLocalGeometry.getFlipType();
     }
 
     protected void setLocalFlip(FLIP flip) {
-        mLocalGeoMetadata.setFlipType(flip);
-        updateMatrix();
+        mLocalGeometry.setFlipType(flip);
     }
 
     protected float getTotalLocalRotation() {
@@ -208,64 +231,26 @@
     protected static float[] getCornersFromRect(RectF r) {
         // Order is:
         // 0------->1
-        // ^ |
-        // | v
+        // ^        |
+        // |        v
         // 3<-------2
         float[] corners = {
                 r.left, r.top, // 0
                 r.right, r.top, // 1
                 r.right, r.bottom,// 2
-                r.left, r.bottom
-                // 3
+                r.left, r.bottom // 3
         };
         return corners;
     }
 
-    // Returns maximal rectangular crop bound that still fits within
-    // the image bound after the image has been rotated.
-    protected static RectF findCropBoundForRotatedImg(RectF cropBound,
-            RectF imageBound,
-            float rotation,
-            float centerX,
-            float centerY) {
-        Matrix m = new Matrix();
-        float[] cropEdges = getCornersFromRect(cropBound);
-        m.setRotate(rotation, centerX, centerY);
-        Matrix m0 = new Matrix();
-        if (!m.invert(m0))
-            return null;
-        m0.mapPoints(cropEdges);
-        getEdgePoints(imageBound, cropEdges);
-        m.mapPoints(cropEdges);
-        return trapToRect(cropEdges);
-    }
-
     // If edge point [x, y] in array [x0, y0, x1, y1, ...] is outside of the
     // image bound rectangle, clamps it to the edge of the rectangle.
     protected static void getEdgePoints(RectF imageBound, float[] array) {
         if (array.length < 2)
             return;
         for (int x = 0; x < array.length; x += 2) {
-            array[x] = clamp(array[x], imageBound.left, imageBound.right);
-            array[x + 1] = clamp(array[x + 1], imageBound.top, imageBound.bottom);
-        }
-    }
-
-    protected static RectF trapToRect(float[] array) {
-        float dx0 = array[4] - array[0];
-        float dy0 = array[5] - array[1];
-        float dx1 = array[6] - array[2];
-        float dy1 = array[7] - array[3];
-        float l0 = dx0 * dx0 + dy0 * dy0;
-        float l1 = dx1 * dx1 + dy1 * dy1;
-        if (l0 > l1) {
-            RectF n = new RectF(array[2], array[3], array[6], array[7]);
-            n.sort();
-            return n;
-        } else {
-            RectF n = new RectF(array[0], array[1], array[4], array[5]);
-            n.sort();
-            return n;
+            array[x] = GeometryMath.clamp(array[x], imageBound.left, imageBound.right);
+            array[x + 1] = GeometryMath.clamp(array[x + 1], imageBound.top, imageBound.bottom);
         }
     }
 
@@ -280,26 +265,14 @@
         return crop;
     }
 
-    protected static float[] shortestVectorFromPointToLine(float[] point, float[] l1, float[] l2) {
-        float x1 = l1[0];
-        float x2 = l2[0];
-        float y1 = l1[1];
-        float y2 = l2[1];
-        float xdelt = x2 - x1;
-        float ydelt = y2 - y1;
-        if (xdelt == 0 && ydelt == 0)
-            return null;
-        float u = ((point[0] - x1) * xdelt + (point[1] - y1) * ydelt)
-                / (xdelt * xdelt + ydelt * ydelt);
-        float[] ret = {
-                (x1 + u * (x2 - x1)), (y1 + u * (y2 - y1))
-        };
-        return ret;
-    }
-
     protected static void fixAspectRatio(RectF r, float w, float h) {
         float scale = Math.min(r.width() / w, r.height() / h);
-        r.set(r.left, r.top, scale * w, scale * h);
+        float centX = r.centerX();
+        float centY = r.centerY();
+        float hw = scale * w / 2;
+        float hh = scale * h / 2;
+        r.set(centX - hw, centY - hh, centX + hw, centY + hh);
+
     }
 
     protected static float getNewHeightForWidthAspect(float width, float w, float h) {
@@ -310,22 +283,17 @@
         return height * w / h;
     }
 
-    protected void logMasterGeo() {
-        Log.v(LOGTAG, getMaster().getGeometry().toString());
-    }
-
     @Override
     protected void onVisibilityChanged(View changedView, int visibility) {
         super.onVisibilityChanged(changedView, visibility);
         if (visibility == View.VISIBLE) {
             mVisibilityGained = true;
             syncLocalToMasterGeometry();
+            updateScale();
             gainedVisibility();
-            logMasterGeo();
         } else {
             if (mVisibilityGained == true && mHasDrawn == true) {
                 lostVisibility();
-                logMasterGeo();
             }
             mVisibilityGained = false;
             mHasDrawn = false;
@@ -334,7 +302,6 @@
 
     protected void gainedVisibility() {
         // TODO: Override this stub.
-        updateMatrix();
     }
 
     protected void lostVisibility() {
@@ -356,8 +323,6 @@
             case (MotionEvent.ACTION_UP):
                 setActionUp();
                 saveAndSetPreset();
-                Log.v(LOGTAG, "up action");
-                logMasterGeo();
                 break;
             case (MotionEvent.ACTION_MOVE):
                 setActionMove(event.getX(), event.getY());
@@ -405,17 +370,12 @@
 
     protected void saveAndSetPreset() {
         ImagePreset copy = new ImagePreset(getImagePreset());
-        copy.setGeometry(mLocalGeoMetadata);
+        copy.setGeometry(mLocalGeometry);
         copy.setHistoryName("Geometry");
         copy.setIsFx(false);
         setImagePreset(copy);
     }
 
-    //
-    protected static float clamp(float i, float low, float high) {
-        return Math.max(Math.min(i, high), low);
-    }
-
     public static RectF getUntranslatedStraightenCropBounds(RectF imageRect, float straightenAngle) {
         float deg = straightenAngle;
         if (deg < 0) {
@@ -440,102 +400,125 @@
         return new RectF(left, top, right, bottom);
     }
 
+    protected Matrix getGeoMatrix(RectF r, boolean onlyRotate) {
+        float scale = computeScale(getWidth(), getHeight());
+        float yoff = getHeight() / 2;
+        float xoff = getWidth() / 2;
+        float w = r.left * 2 + r.width();
+        float h = r.top * 2 + r.height();
+        return mLocalGeometry.buildGeometryMatrix(w, h, scale, xoff, yoff, onlyRotate);
+    }
+
+    protected void drawImageBitmap(Canvas canvas, Bitmap bitmap, Paint paint, Matrix m) {
+        canvas.save();
+        canvas.drawBitmap(bitmap, m, paint);
+        canvas.restore();
+    }
+
+    protected void drawImageBitmap(Canvas canvas, Bitmap bitmap, Paint paint) {
+        float scale = computeScale(getWidth(), getHeight());
+        float yoff = getHeight() / 2;
+        float xoff = getWidth() / 2;
+        Matrix m = mLocalGeometry.buildGeometryUIMatrix(scale, xoff, yoff);
+        drawImageBitmap(canvas, bitmap, paint, m);
+    }
+
+    protected RectF straightenBounds() {
+        RectF bounds = getUntranslatedStraightenCropBounds(getLocalPhotoBounds(),
+                getLocalStraighten());
+        Matrix m = getGeoMatrix(bounds, true);
+        m.mapRect(bounds);
+        return bounds;
+    }
+
+    protected void drawStraighten(Canvas canvas, Paint paint) {
+        RectF bounds = straightenBounds();
+        canvas.save();
+        canvas.drawRect(bounds, paint);
+        canvas.restore();
+    }
+
+    protected RectF unrotatedCropBounds() {
+        RectF bounds = getLocalCropBounds();
+        RectF pbounds = getLocalPhotoBounds();
+        float scale = computeScale(getWidth(), getHeight());
+        float yoff = getHeight() / 2;
+        float xoff = getWidth() / 2;
+        Matrix m = mLocalGeometry.buildGeometryMatrix(pbounds.width(), pbounds.height(), scale, xoff, yoff, 0);
+        m.mapRect(bounds);
+        return bounds;
+    }
+
+    protected RectF cropBounds() {
+        RectF bounds = getLocalCropBounds();
+        Matrix m = getGeoMatrix(getLocalPhotoBounds(), true);
+        m.mapRect(bounds);
+        return bounds;
+    }
+
+    // Fails for non-90 degree
+    protected void drawCrop(Canvas canvas, Paint paint) {
+        RectF bounds = cropBounds();
+        canvas.save();
+        canvas.drawRect(bounds, paint);
+        canvas.restore();
+    }
+
+    protected void drawCropSafe(Canvas canvas, Paint paint) {
+        Matrix m = getGeoMatrix(getLocalPhotoBounds(), true);
+        RectF crop = getLocalCropBounds();
+        if (!m.rectStaysRect()) {
+            float[] corners = getCornersFromRect(crop);
+            m.mapPoints(corners);
+            drawClosedPath(canvas, paint, corners);
+        } else {
+            m.mapRect(crop);
+            Path path = new Path();
+            path.addRect(crop, Path.Direction.CCW);
+            canvas.drawPath(path, paint);
+        }
+    }
+
+    protected void drawTransformedBitmap(Canvas canvas, Bitmap bitmap, Paint paint, boolean clip) {
+        paint.setARGB(255, 0, 0, 0);
+        drawImageBitmap(canvas, bitmap, paint);
+        paint.setColor(Color.WHITE);
+        paint.setStyle(Style.STROKE);
+        paint.setStrokeWidth(2);
+        drawCropSafe(canvas, paint);
+        paint.setARGB(128, 0, 0, 0);
+        paint.setStyle(Paint.Style.FILL);
+        drawShadows(canvas, paint, unrotatedCropBounds());
+    }
+
+    protected void drawShadows(Canvas canvas, Paint p, RectF innerBounds) {
+        RectF display = new RectF(0, 0, getWidth(), getHeight());
+        drawShadows(canvas, p, innerBounds, display, getLocalRotation(), getWidth() / 2,
+                getHeight() / 2);
+    }
+
     protected static void drawShadows(Canvas canvas, Paint p, RectF innerBounds, RectF outerBounds,
             float rotation, float centerX, float centerY) {
         canvas.save();
         canvas.rotate(rotation, centerX, centerY);
-        float dWidth = outerBounds.width();
-        float dHeight = outerBounds.height();
-        canvas.drawRect(0, 0, dWidth, innerBounds.top, p);
-        canvas.drawRect(0, innerBounds.bottom, dWidth, dHeight, p);
-        canvas.drawRect(0, innerBounds.top, innerBounds.left, innerBounds.bottom,
+
+        float x = (outerBounds.left - outerBounds.right);
+        float y = (outerBounds.top - outerBounds.bottom);
+        float longest = (float) Math.sqrt(x * x + y * y) / 2;
+        float minX = centerX - longest;
+        float maxX = centerX + longest;
+        float minY = centerY - longest;
+        float maxY = centerY + longest;
+        canvas.drawRect(minX, minY, innerBounds.right, innerBounds.top, p);
+        canvas.drawRect(minX, innerBounds.top, innerBounds.left, maxY, p);
+        canvas.drawRect(innerBounds.left, innerBounds.bottom, maxX, maxY,
                 p);
-        canvas.drawRect(innerBounds.right, innerBounds.top, dWidth,
+        canvas.drawRect(innerBounds.right, minY, maxX,
                 innerBounds.bottom, p);
         canvas.rotate(-rotation, centerX, centerY);
         canvas.restore();
     }
 
-    public Matrix computeBoundsMatrix(Bitmap bitmap) {
-        Matrix boundsMatrix = new Matrix();
-        boundsMatrix.setTranslate((getWidth() - bitmap.getWidth()) / 2.0f,
-                (getHeight() - bitmap.getHeight()) / 2.0f);
-        boundsMatrix.postRotate(getLocalRotation(), getWidth() / 2.0f, getHeight() / 2.0f);
-        return boundsMatrix;
-    }
-
-    public RectF cropBounds(Bitmap bitmap) {
-        Matrix boundsMatrix = computeBoundsMatrix(bitmap);
-        RectF bounds = getUntranslatedStraightenCropBounds(getLocalPhotoBounds(),
-                getLocalStraighten());
-        RectF transformedBounds = new RectF(bounds);
-        boundsMatrix.mapRect(transformedBounds);
-        return transformedBounds;
-    }
-
-    protected void drawTransformedBitmap(Canvas canvas, Bitmap bitmap, Paint paint, boolean clip) {
-        Matrix boundsMatrix = computeBoundsMatrix(bitmap);
-        RectF bounds = getUntranslatedStraightenCropBounds(getLocalPhotoBounds(),
-                getLocalStraighten());
-        RectF transformedBounds = new RectF(bounds);
-        boundsMatrix.mapRect(transformedBounds);
-
-        canvas.save();
-        Matrix matrix = getLocalMatrix();
-        canvas.translate((getWidth() - bitmap.getWidth()) / 2.0f,
-                (getHeight() - bitmap.getHeight()) / 2.0f);
-        paint.setARGB(255, 0, 0, 0);
-        Matrix drawMatrix = new Matrix();
-        float w = bitmap.getWidth();
-        drawMatrix.preScale(1.0f/w, 1.0f/w);
-        drawMatrix.postConcat(matrix);
-        drawMatrix.postScale(w, w);
-        canvas.drawBitmap(bitmap, drawMatrix, paint);
-        canvas.restore();
-
-        canvas.save();
-        canvas.setMatrix(boundsMatrix);
-        paint.setColor(Color.WHITE);
-        paint.setStyle(Style.STROKE);
-        paint.setStrokeWidth(2);
-        canvas.drawRect(bounds, paint);
-        canvas.restore();
-
-        if (!clip) { // we display the rest of the bitmap grayed-out
-            drawShadows(canvas, transformedBounds, new RectF(0, 0, getWidth(), getHeight()), paint);
-        }
-    }
-
-    protected RectF getCropBoundsDisplayed() {
-        return getCropBoundsDisplayed(getLocalCropBounds());
-    }
-
-    protected RectF getCropBoundsDisplayed(RectF bounds) {
-        RectF crop = new RectF(bounds);
-        Matrix m = new Matrix();
-        float zoom = getLocalScale();
-        m.setScale(zoom, zoom, mCenterX, mCenterY);
-        m.preTranslate(mXOffset, mYOffset);
-        m.mapRect(crop);
-        return crop;
-    }
-
-    protected void drawShadows(Canvas canvas, RectF innerBounds, RectF outerBounds, Paint p) {
-        float dWidth = outerBounds.width();
-        float dHeight = outerBounds.height();
-
-        // TODO: move style to xml
-        p.setARGB(128, 0, 0, 0);
-        p.setStyle(Paint.Style.FILL);
-
-        canvas.drawRect(0, 0, dWidth, innerBounds.top, p);
-        canvas.drawRect(0, innerBounds.bottom, dWidth, dHeight, p);
-        canvas.drawRect(0, innerBounds.top, innerBounds.left, innerBounds.bottom,
-                p);
-        canvas.drawRect(innerBounds.right, innerBounds.top, dWidth,
-                innerBounds.bottom, p);
-    }
-
     @Override
     public void onDraw(Canvas canvas) {
         if (getDirtyGeometryFlag()) {
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java b/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java
index 089e117..775aa71 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java
@@ -23,7 +23,6 @@
 import android.util.AttributeSet;
 
 public class ImageRotate extends ImageGeometry {
-    private static final float MATH_PI = (float) Math.PI;
 
     private float mBaseAngle = 0;
     private float mAngle = 0;
@@ -39,35 +38,10 @@
         super(context);
     }
 
-    private float angleFor(float dx, float dy) {
-        return (float) (Math.atan2(dx, dy) * 180 / MATH_PI);
-    }
-
-    private int snappedAngle(float angle) {
-        float remainder = angle % 90;
-        int current = (int) (angle / 90); // truncates
-        if (remainder < -45) {
-            --current;
-        } else if (remainder > 45) {
-            ++current;
-        }
-        return current * 90;
-    }
-
     private static final Paint gPaint = new Paint();
 
     private void computeValue() {
-        if (mCurrentX == mTouchCenterX && mCurrentY == mTouchCenterY) {
-            return;
-        }
-        float dX1 = mTouchCenterX - mCenterX;
-        float dY1 = mTouchCenterY - mCenterY;
-        float dX2 = mCurrentX - mCenterX;
-        float dY2 = mCurrentY - mCenterY;
-
-        float angleA = angleFor(dX1, dY1);
-        float angleB = angleFor(dX2, dY2);
-        float angle = (angleB - angleA) % 360;
+        float angle = getCurrentTouchAngle();
         mAngle = (mBaseAngle - angle) % 360;
     }
 
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java b/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java
index f4a2184..5c476eb 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
+import android.graphics.Color;
 import android.graphics.Paint;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -57,7 +58,10 @@
     private boolean mDirtyGeometry = true;
 
     private Bitmap mBackgroundImage = null;
- // TODO: remove protected here, it should be private
+    private final boolean USE_BACKGROUND_IMAGE = false;
+    private static int mBackgroundColor = Color.RED;
+
+    // TODO: remove protected here, it should be private
     protected Bitmap mForegroundImage = null;
     protected Bitmap mFilteredImage = null;
 
@@ -88,23 +92,45 @@
     private SeekBar mSeekBar = null;
     private PanelController mController = null;
 
+    public static void setDefaultBackgroundColor(int value) {
+        mBackgroundColor = value;
+    }
+
+    public static void setTextSize(int value) {
+        mTextSize = value;
+    }
+
+    public static void setTextPadding(int value) {
+        mTextPadding = value;
+    }
+
     private final Handler mHandler = new Handler();
 
     public void select() {
         if (getCurrentFilter() != null) {
             int parameter = getCurrentFilter().getParameter();
-            updateSeekBar(parameter);
+            int maxp = getCurrentFilter().getMaxParameter();
+            int minp = getCurrentFilter().getMinParameter();
+            updateSeekBar(parameter,minp,maxp);
         }
         if (mSeekBar != null) {
             mSeekBar.setOnSeekBarChangeListener(this);
         }
     }
 
-    public void updateSeekBar(int parameter) {
+    private int parameterToUI(int parameter,int minp,int maxp,int uimax){
+        return (uimax*(parameter-minp))/(maxp-minp);
+    }
+
+    private int uiToParameter(int ui,int minp,int maxp,int uimax){
+        return  ((maxp-minp)*ui)/uimax+minp;
+    }
+    public void updateSeekBar(int parameter,int minp,int maxp) {
         if (mSeekBar == null) {
             return;
         }
-        int progress = parameter + 100;
+        int seekMax  = mSeekBar.getMax();
+        int progress = parameterToUI(parameter,minp,maxp,seekMax);
         mSeekBar.setProgress(progress);
         if (getPanelController() != null) {
             getPanelController().onNewValue(parameter);
@@ -115,8 +141,18 @@
 
     }
 
+    public boolean hasModifications() {
+        if (getImagePreset() == null) {
+            return false;
+        }
+        return getImagePreset().hasModifications();
+    }
+
     public void resetParameter() {
-        onNewValue(0);
+        ImageFilter currentFilter = getCurrentFilter();
+        if (currentFilter!=null) {
+            onNewValue(currentFilter.getDefaultParameter());
+        }
         if (USE_SLIDER_GESTURE) {
             mSliderController.reset();
         }
@@ -131,18 +167,22 @@
     }
 
     @Override
-    public void onNewValue(int value) {
+    public void onNewValue(int parameter) {
+        int maxp = 100;
+        int minp = -100;
         if (getCurrentFilter() != null) {
-            getCurrentFilter().setParameter(value);
+            getCurrentFilter().setParameter(parameter);
+            maxp = getCurrentFilter().getMaxParameter();
+            minp = getCurrentFilter().getMinParameter();
         }
         if (getImagePreset() != null) {
             mImageLoader.resetImageForPreset(getImagePreset(), this);
             getImagePreset().fillImageStateAdapter(mImageStateAdapter);
         }
         if (getPanelController() != null) {
-            getPanelController().onNewValue(value);
+            getPanelController().onNewValue(parameter);
         }
-        updateSeekBar(value);
+        updateSeekBar(parameter,minp,maxp);
         invalidate();
     }
 
@@ -278,7 +318,7 @@
             canvas.drawRect(textRect, mPaint);
             mPaint.setARGB(255, 200, 200, 200);
             canvas.drawText(getImagePreset().name(), mTextPadding,
-                    10 + mTextPadding, mPaint);
+                    1.5f * mTextPadding, mPaint);
         }
 
         if (showControls()) {
@@ -333,14 +373,18 @@
     }
 
     public void drawBackground(Canvas canvas) {
-        if (mBackgroundImage == null) {
-            mBackgroundImage = mImageLoader.getBackgroundBitmap(getResources());
-        }
-        if (mBackgroundImage != null) {
-            Rect s = new Rect(0, 0, mBackgroundImage.getWidth(),
-                    mBackgroundImage.getHeight());
-            Rect d = new Rect(0, 0, getWidth(), getHeight());
-            canvas.drawBitmap(mBackgroundImage, s, d, mPaint);
+        if (USE_BACKGROUND_IMAGE) {
+            if (mBackgroundImage == null) {
+                mBackgroundImage = mImageLoader.getBackgroundBitmap(getResources());
+            }
+            if (mBackgroundImage != null) {
+                Rect s = new Rect(0, 0, mBackgroundImage.getWidth(),
+                        mBackgroundImage.getHeight());
+                Rect d = new Rect(0, 0, getWidth(), getHeight());
+                canvas.drawBitmap(mBackgroundImage, s, d, mPaint);
+            }
+        } else {
+            canvas.drawColor(mBackgroundColor);
         }
     }
 
@@ -495,7 +539,14 @@
 
     @Override
     public void onProgressChanged(SeekBar arg0, int progress, boolean arg2) {
-        onNewValue(progress - 100);
+        int parameter = progress;
+        if (getCurrentFilter()!=null){
+            int maxp = getCurrentFilter().getMaxParameter();
+            int minp = getCurrentFilter().getMinParameter();
+            parameter = uiToParameter(progress,minp,maxp,arg0.getMax());
+        }
+
+        onNewValue(parameter);
     }
 
     @Override
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageSmallBorder.java b/src/com/android/gallery3d/filtershow/imageshow/ImageSmallBorder.java
index d0c67f7..0cf6389 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageSmallBorder.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageSmallBorder.java
@@ -18,7 +18,7 @@
     protected final int mSelectedBackgroundColor = Color.WHITE;
     protected final int mInnerBorderColor = Color.BLACK;
     protected final int mInnerBorderWidth = 2;
-    protected final float mImageScaleFactor = 2.5f;
+    protected final float mImageScaleFactor = 3.5f;
 
     public ImageSmallBorder(Context context) {
         super(context);
@@ -31,15 +31,16 @@
     @Override
     public void onDraw(Canvas canvas) {
         getFilteredImage();
-        if (mIsSelected) {
-            canvas.drawColor(mSelectedBackgroundColor);
-        } else {
-            canvas.drawColor(mBackgroundColor);
-        }
+        canvas.drawColor(mBackgroundColor);
         // TODO: simplify & make faster...
+        RectF border = new RectF(mMargin, 2*mMargin, getWidth() - mMargin - 1, getWidth());
+
+        if (mIsSelected) {
+            mPaint.setColor(mSelectedBackgroundColor);
+            canvas.drawRect(0, mMargin, getWidth(), getWidth() + mMargin, mPaint);
+        }
+
         mPaint.setColor(mInnerBorderColor);
-        RectF border = new RectF(mMargin, mMargin, getWidth() - mMargin - 1, getHeight() - mMargin);
-        canvas.drawLine(0, 0, getWidth(), 0, mPaint);
         mPaint.setStrokeWidth(mInnerBorderWidth);
         Path path = new Path();
         path.addRect(border, Path.Direction.CCW);
@@ -47,9 +48,9 @@
         canvas.drawPath(path, mPaint);
         mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
         canvas.save();
-        canvas.clipRect(mMargin + 1, mMargin, getWidth() - mMargin - 2, getHeight() - mMargin - 1,
+        canvas.clipRect(mMargin + 1, 2*mMargin, getWidth() - mMargin - 2, getWidth() - 1,
                 Region.Op.INTERSECT);
-        canvas.translate(mMargin, mMargin + 1);
+        canvas.translate(mMargin + 1, 2*mMargin + 1);
         canvas.scale(mImageScaleFactor, mImageScaleFactor);
         Rect d = new Rect(0, 0, getWidth(), getWidth());
         drawImage(canvas, mFilteredImage, d);
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageSmallFilter.java b/src/com/android/gallery3d/filtershow/imageshow/ImageSmallFilter.java
index a358e0c..f4bd41c 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageSmallFilter.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageSmallFilter.java
@@ -23,16 +23,22 @@
     private boolean mSetBorder = false;
     protected final Paint mPaint = new Paint();
     protected boolean mIsSelected = false;
-    protected boolean mNextIsSelected = false;
-    private ImageSmallFilter mPreviousImageSmallFilter = null;
 
     // TODO: move this to xml.
-    protected final int mMargin = 12;
-    protected final int mTextMargin = 8;
+    protected static int mMargin = 12;
+    protected static int mTextMargin = 8;
     protected final int mBackgroundColor = Color.argb(255, 30, 32, 40);
     protected final int mSelectedBackgroundColor = Color.WHITE;
     protected final int mTextColor = Color.WHITE;
 
+    public static void setMargin(int value) {
+        mMargin = value;
+    }
+
+    public static void setTextMargin(int value) {
+        mTextMargin = value;
+    }
+
     public ImageSmallFilter(Context context, AttributeSet attrs) {
         super(context, attrs);
         setOnClickListener(this);
@@ -50,28 +56,14 @@
         mImagePreset.add(mImageFilter);
     }
 
-    public void setPreviousImageSmallFilter(ImageSmallFilter previous) {
-        mPreviousImageSmallFilter = previous;
-    }
-
     @Override
     public void setSelected(boolean value) {
         if (mIsSelected != value) {
             invalidate();
-            if (mPreviousImageSmallFilter != null) {
-                mPreviousImageSmallFilter.setNextSelected(value);
-            }
         }
         mIsSelected = value;
     }
 
-    public void setNextSelected(boolean value) {
-        if (mNextIsSelected != value) {
-            invalidate();
-        }
-        mNextIsSelected = value;
-    }
-
     public void setBorder(boolean value) {
         mSetBorder = value;
     }
@@ -138,37 +130,40 @@
     public void onDraw(Canvas canvas) {
         getFilteredImage();
         canvas.drawColor(mBackgroundColor);
-        Rect d = new Rect(0, mMargin, getWidth() - mMargin, getWidth());
-        float textWidth = mPaint.measureText(getImagePreset().name());
+        float textWidth = mPaint.measureText(mImageFilter.getName());
         int h = mTextSize + 2 * mTextPadding;
         int x = (int) ((getWidth() - textWidth) / 2);
         int y = getHeight();
         if (mIsSelected) {
             mPaint.setColor(mSelectedBackgroundColor);
-            canvas.drawRect(0, 0, getWidth(), getWidth() + mMargin, mPaint);
+            canvas.drawRect(0, mMargin, getWidth(), getWidth() + mMargin, mPaint);
         }
-        if (mNextIsSelected) {
-            mPaint.setColor(mSelectedBackgroundColor);
-            canvas.drawRect(getWidth() - mMargin, 0, getWidth(), getWidth() + mMargin, mPaint);
-        }
-        drawImage(canvas, mFilteredImage, d);
+        Rect destination = new Rect(mMargin, 2*mMargin, getWidth() - mMargin, getWidth());
+        drawImage(canvas, mFilteredImage, destination);
         mPaint.setTextSize(mTextSize);
         mPaint.setColor(mTextColor);
-        canvas.drawText(getImagePreset().name(), x, y - mTextMargin, mPaint);
+        canvas.drawText(mImageFilter.getName(), x, y - mTextMargin, mPaint);
     }
 
-    public void drawImage(Canvas canvas, Bitmap image, Rect d) {
+    public void drawImage(Canvas canvas, Bitmap image, Rect destination) {
         if (image != null) {
             int iw = image.getWidth();
             int ih = image.getHeight();
-            int iy = (int) ((ih - iw) / 2.0f);
-            int ix = 0;
+            int x = 0;
+            int y = 0;
+            int size = 0;
+            Rect source = null;
             if (iw > ih) {
-                iy = 0;
-                ix = (int) ((iw - ih) / 2.0f);
+                size = ih;
+                x = (int) ((iw - size) / 2.0f);
+                y = 0;
+            } else {
+                size = iw;
+                x = 0;
+                y = (int) ((ih - size) / 2.0f);
             }
-            Rect s = new Rect(ix, iy, ix + iw, iy + iw);
-            canvas.drawBitmap(image, s, d, mPaint);
+            source = new Rect(x, y, x + size, y + size);
+            canvas.drawBitmap(image, source, destination, mPaint);
         }
     }
 
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java b/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java
index a94d629..26c9671 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java
@@ -46,29 +46,21 @@
         mBaseAngle = mAngle = getLocalStraighten();
     }
 
+    private void setCropToStraighten(){
+        setLocalCropBounds(getUntranslatedStraightenCropBounds(getLocalPhotoBounds(),
+                getLocalStraighten()));
+    }
+
     @Override
     protected void setActionMove(float x, float y) {
         super.setActionMove(x, y);
         computeValue();
         setLocalStraighten(mAngle);
-    }
-
-    private float angleFor(float dx, float dy) {
-        return (float) (Math.atan2(dx, dy) * 180 / Math.PI);
+        setCropToStraighten();
     }
 
     private void computeValue() {
-        if (mCurrentX == mTouchCenterX && mCurrentY == mTouchCenterY) {
-            return;
-        }
-        float dX1 = mTouchCenterX - mCenterX;
-        float dY1 = mTouchCenterY - mCenterY;
-        float dX2 = mCurrentX - mCenterX;
-        float dY2 = mCurrentY - mCenterY;
-
-        float angleA = angleFor(dX1, dY1);
-        float angleB = angleFor(dX2, dY2);
-        float angle = (angleB - angleA) % 360;
+        float angle = getCurrentTouchAngle();
         mAngle = (mBaseAngle - angle) % 360;
         mAngle = Math.max(MIN_STRAIGHTEN_ANGLE, mAngle);
         mAngle = Math.min(MAX_STRAIGHTEN_ANGLE, mAngle);
@@ -80,8 +72,19 @@
     }
 
     @Override
+    protected void gainedVisibility(){
+        setCropToStraighten();
+    }
+
+    @Override
+    protected void setActionUp() {
+        super.setActionUp();
+        setCropToStraighten();
+    }
+
+    @Override
     public void onNewValue(int value) {
-        setLocalStraighten(clamp(value, MIN_STRAIGHTEN_ANGLE, MAX_STRAIGHTEN_ANGLE));
+        setLocalStraighten(GeometryMath.clamp(value, MIN_STRAIGHTEN_ANGLE, MAX_STRAIGHTEN_ANGLE));
         if (getPanelController() != null) {
             getPanelController().onNewValue((int) getLocalStraighten());
         }
@@ -98,7 +101,7 @@
         drawTransformedBitmap(canvas, image, gPaint, false);
 
         // Draw the grid
-        RectF bounds = cropBounds(image);
+        RectF bounds = straightenBounds();
         Path path = new Path();
         path.addRect(bounds, Path.Direction.CCW);
         gPaint.setARGB(255, 255, 255, 255);
diff --git a/src/com/android/gallery3d/filtershow/presets/ImagePreset.java b/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
index 42dbcb9..f303d4c 100644
--- a/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
+++ b/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
@@ -37,7 +37,9 @@
 
     public ImagePreset(ImagePreset source, String historyName) {
         this(source);
-        if (historyName!=null) setHistoryName(historyName);
+        if (historyName != null) {
+            setHistoryName(historyName);
+        }
     }
 
     public ImagePreset(ImagePreset source) {
@@ -58,6 +60,22 @@
         mGeoData.set(source.mGeoData);
     }
 
+    public boolean hasModifications() {
+        if (mImageBorder != null && !mImageBorder.isNil()) {
+            return true;
+        }
+        if (mGeoData.hasModifications()) {
+            return true;
+        }
+        for (int i = 0; i < mFilters.size(); i++) {
+            ImageFilter filter = mFilters.elementAt(i);
+            if (!filter.isNil()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     public void setGeometry(GeometryMetadata m) {
         mGeoData.set(m);
     }
@@ -123,27 +141,25 @@
 
     public void add(ImageFilter filter) {
 
-        if (filter.getFilterType() == ImageFilter.TYPE_BORDER){
+        if (filter.getFilterType() == ImageFilter.TYPE_BORDER) {
             setHistoryName("Border");
             setBorder(filter);
-        } else if (filter.getFilterType() == ImageFilter.TYPE_FX){
-
+        } else if (filter.getFilterType() == ImageFilter.TYPE_FX) {
             boolean found = false;
             for (int i = 0; i < mFilters.size(); i++) {
                 byte type = mFilters.get(i).getFilterType();
                 if (found) {
-                    if (type != ImageFilter.TYPE_VIGNETTE){
+                    if (type != ImageFilter.TYPE_VIGNETTE) {
                         mFilters.remove(i);
                         continue;
                     }
                 }
-                if (type==ImageFilter.TYPE_FX){
+                if (type == ImageFilter.TYPE_FX) {
                     mFilters.remove(i);
                     mFilters.add(i, filter);
                     setHistoryName(filter.getName());
                     found = true;
                 }
-
             }
             if (!found) {
                 mFilters.add(filter);
diff --git a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java b/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java
index 49cc33a..23f4972 100644
--- a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java
+++ b/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java
@@ -30,6 +30,7 @@
 import android.view.Gravity;
 import android.widget.Toast;
 
+import com.android.camera.R;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
 
 //import com.android.gallery3d.R;
@@ -49,9 +50,8 @@
  */
 public class SaveCopyTask extends AsyncTask<ProcessedBitmap, Void, Uri> {
 
-    public static final String DOWNLOAD = "download";
-    public static final String DEFAULT_SAVE_DIRECTORY = "Download";
     private static final int DEFAULT_COMPRESS_QUALITY = 95;
+    private static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos";
 
     /**
      * Saves the bitmap in the final destination
@@ -114,13 +114,17 @@
                 System.currentTimeMillis()));
     }
 
-    public static File getNewFile(Context context, Uri sourceUri) {
+    public static File getFinalSaveDirectory(Context context, Uri sourceUri) {
         File saveDirectory = getSaveDirectory(context, sourceUri);
         if ((saveDirectory == null) || !saveDirectory.canWrite()) {
             saveDirectory = new File(Environment.getExternalStorageDirectory(),
-                    DOWNLOAD);
+                    DEFAULT_SAVE_DIRECTORY);
         }
+        return saveDirectory;
+    }
 
+    public static File getNewFile(Context context, Uri sourceUri) {
+        File saveDirectory = getFinalSaveDirectory(context, sourceUri);
         String filename = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(
                 System.currentTimeMillis()));
         return new File(saveDirectory, filename + ".JPG");
diff --git a/src/com/android/gallery3d/filtershow/ui/ControlPoint.java b/src/com/android/gallery3d/filtershow/ui/ControlPoint.java
index 68b799a..0c08e76 100644
--- a/src/com/android/gallery3d/filtershow/ui/ControlPoint.java
+++ b/src/com/android/gallery3d/filtershow/ui/ControlPoint.java
@@ -2,26 +2,19 @@
 package com.android.gallery3d.filtershow.ui;
 
 public class ControlPoint implements Comparable {
+    public float x;
+    public float y;
+
     public ControlPoint(float px, float py) {
         x = px;
         y = py;
     }
 
-    public ControlPoint multiply(float m) {
-        return new ControlPoint(x * m, y * m);
+    public ControlPoint(ControlPoint point) {
+        x = point.x;
+        y = point.y;
     }
 
-    public ControlPoint add(ControlPoint v) {
-        return new ControlPoint(x + v.x, y + v.y);
-    }
-
-    public ControlPoint sub(ControlPoint v) {
-        return new ControlPoint(x - v.x, y - v.y);
-    }
-
-    public float x;
-    public float y;
-
     public ControlPoint copy() {
         return new ControlPoint(x, y);
     }
diff --git a/src/com/android/gallery3d/filtershow/ui/ImageButtonTitle.java b/src/com/android/gallery3d/filtershow/ui/ImageButtonTitle.java
index 7f0b043..3d43dc7 100644
--- a/src/com/android/gallery3d/filtershow/ui/ImageButtonTitle.java
+++ b/src/com/android/gallery3d/filtershow/ui/ImageButtonTitle.java
@@ -1,16 +1,15 @@
 
 package com.android.gallery3d.filtershow.ui;
 
-import com.android.gallery3d.R;
-
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.widget.ImageButton;
 
+import com.android.gallery3d.R;
+
 public class ImageButtonTitle extends ImageButton {
     private static final String LOGTAG = "ImageButtonTitle";
     private String mText = null;
@@ -18,6 +17,18 @@
     private static int mTextPadding = 20;
     private static Paint gPaint = new Paint();
 
+    public static void setTextSize(int value) {
+        mTextSize = value;
+    }
+
+    public static void setTextPadding(int value) {
+        mTextPadding = value;
+    }
+
+    public void setText(String text) {
+        mText = text;
+    }
+
     public ImageButtonTitle(Context context, AttributeSet attrs) {
         super(context, attrs);
         TypedArray a = getContext().obtainStyledAttributes(
@@ -26,6 +37,11 @@
         mText = a.getString(R.styleable.ImageButtonTitle_android_text);
     }
 
+    public String getText(){
+        return mText;
+    }
+
+    @Override
     public void onDraw(Canvas canvas) {
         super.onDraw(canvas);
         if (mText != null) {
diff --git a/src/com/android/gallery3d/filtershow/ui/ImageCurves.java b/src/com/android/gallery3d/filtershow/ui/ImageCurves.java
index 660a4fa..a8445b8 100644
--- a/src/com/android/gallery3d/filtershow/ui/ImageCurves.java
+++ b/src/com/android/gallery3d/filtershow/ui/ImageCurves.java
@@ -2,17 +2,17 @@
 package com.android.gallery3d.filtershow.ui;
 
 import android.content.Context;
+import android.graphics.Bitmap;
 import android.graphics.Canvas;
+import android.graphics.Color;
 import android.graphics.Paint;
 import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.os.AsyncTask;
 import android.util.AttributeSet;
-import android.view.MenuItem;
 import android.view.MotionEvent;
-import android.view.View;
-import android.widget.PopupMenu;
-import android.widget.Toast;
 
-import com.android.gallery3d.R;
 import com.android.gallery3d.filtershow.filters.ImageFilterCurves;
 import com.android.gallery3d.filtershow.imageshow.ImageSlave;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
@@ -21,15 +21,18 @@
 
     private static final String LOGTAG = "ImageCurves";
     Paint gPaint = new Paint();
-    Spline mSpline = null;
+    Spline[] mSplines = new Spline[4];
     Path gPathSpline = new Path();
-    float[] mAppliedCurve = new float[256];
+
+    private int mCurrentCurveIndex = 0;
     private boolean mDidAddPoint = false;
     private boolean mDidDelete = false;
     private ControlPoint mCurrentControlPoint = null;
-    private boolean mUseRed = true;
-    private boolean mUseGreen = true;
-    private boolean mUseBlue = true;
+    private ImagePreset mLastPreset = null;
+    int[] redHistogram = new int[256];
+    int[] greenHistogram = new int[256];
+    int[] blueHistogram = new int[256];
+    Path gHistoPath = new Path();
 
     public ImageCurves(Context context) {
         super(context);
@@ -41,23 +44,16 @@
         resetCurve();
     }
 
+    public void nextChannel() {
+        mCurrentCurveIndex = ((mCurrentCurveIndex + 1) % 4);
+        invalidate();
+    }
+
     @Override
     public boolean showTitle() {
         return false;
     }
 
-    public void setUseRed(boolean value) {
-        mUseRed = value;
-    }
-
-    public void setUseGreen(boolean value) {
-        mUseGreen = value;
-    }
-
-    public void setUseBlue(boolean value) {
-        mUseBlue = value;
-    }
-
     public void reloadCurve() {
         if (getMaster() != null) {
             String filterName = getFilterName();
@@ -67,7 +63,12 @@
                 resetCurve();
                 return;
             }
-            mSpline = new Spline(filter.getSpline());
+            for (int i = 0; i < 4; i++) {
+                Spline spline = filter.getSpline(i);
+                if (spline != null) {
+                    mSplines[i] = new Spline(spline);
+                }
+            }
             applyNewCurve();
         }
     }
@@ -76,13 +77,19 @@
     public void resetParameter() {
         super.resetParameter();
         resetCurve();
+        mLastPreset = null;
+        invalidate();
     }
 
     public void resetCurve() {
-        mSpline = new Spline();
+        Spline spline = new Spline();
 
-        mSpline.addPoint(0.0f, 1.0f);
-        mSpline.addPoint(1.0f, 0.0f);
+        spline.addPoint(0.0f, 1.0f);
+        spline.addPoint(1.0f, 0.0f);
+
+        for (int i = 0; i < 4; i++) {
+            mSplines[i] = new Spline(spline);
+        }
         if (getMaster() != null) {
             applyNewCurve();
         }
@@ -93,86 +100,48 @@
         super.onDraw(canvas);
 
         gPaint.setAntiAlias(true);
-        gPaint.setFilterBitmap(true);
-        gPaint.setDither(true);
 
-        drawGrid(canvas);
-        drawSpline(canvas);
-
-        drawToast(canvas);
-    }
-
-    private void drawGrid(Canvas canvas) {
-        float w = getWidth();
-        float h = getHeight();
-
-        // Grid
-        gPaint.setARGB(128, 150, 150, 150);
-        gPaint.setStrokeWidth(1);
-
-        float stepH = h / 9;
-        float stepW = w / 9;
-
-        // central diagonal
-        gPaint.setARGB(255, 100, 100, 100);
-        gPaint.setStrokeWidth(2);
-        canvas.drawLine(0, h, w, 0, gPaint);
-
-        gPaint.setARGB(128, 200, 200, 200);
-        gPaint.setStrokeWidth(4);
-        stepH = h / 3;
-        stepW = w / 3;
-        for (int j = 1; j < 3; j++) {
-            canvas.drawLine(0, j * stepH, w, j * stepH, gPaint);
-            canvas.drawLine(j * stepW, 0, j * stepW, h, gPaint);
+        if (getImagePreset() != mLastPreset) {
+            new ComputeHistogramTask().execute(mFilteredImage);
+            mLastPreset = getImagePreset();
         }
-    }
 
-    private void drawSpline(Canvas canvas) {
-        float w = getWidth();
-        float h = getHeight();
-
-        gPathSpline.reset();
-        for (int x = 0; x < w; x += 11) {
-            float fx = x / w;
-            ControlPoint drawPoint = mSpline.getPoint(fx);
-            float newX = drawPoint.x * w;
-            float newY = drawPoint.y * h;
-            if (x == 0) {
-                gPathSpline.moveTo(newX, newY);
-            } else {
-                gPathSpline.lineTo(newX, newY);
+        if (mCurrentCurveIndex == Spline.RGB || mCurrentCurveIndex == Spline.RED) {
+            drawHistogram(canvas, redHistogram, Color.RED, PorterDuff.Mode.SCREEN);
+        }
+        if (mCurrentCurveIndex == Spline.RGB || mCurrentCurveIndex == Spline.GREEN) {
+            drawHistogram(canvas, greenHistogram, Color.GREEN, PorterDuff.Mode.SCREEN);
+        }
+        if (mCurrentCurveIndex == Spline.RGB || mCurrentCurveIndex == Spline.BLUE) {
+            drawHistogram(canvas, blueHistogram, Color.BLUE, PorterDuff.Mode.SCREEN);
+        }
+        // We only display the other channels curves when showing the RGB curve
+        if (mCurrentCurveIndex == Spline.RGB) {
+            for (int i = 0; i < 4; i++) {
+                Spline spline = mSplines[i];
+                if (i != mCurrentCurveIndex && !spline.isOriginal()) {
+                    // And we only display a curve if it has more than two
+                    // points
+                    spline.draw(canvas, Spline.colorForCurve(i), getWidth(), getHeight(), false);
+                }
             }
         }
+        // ...but we always display the current curve.
+        mSplines[mCurrentCurveIndex]
+                .draw(canvas, Spline.colorForCurve(mCurrentCurveIndex), getWidth(), getHeight(),
+                        true);
+        drawToast(canvas);
 
-        gPaint.setStrokeWidth(10);
-        gPaint.setStyle(Paint.Style.STROKE);
-        gPaint.setARGB(255, 50, 50, 50);
-        canvas.drawPath(gPathSpline, gPaint);
-        gPaint.setStrokeWidth(5);
-        gPaint.setARGB(255, 150, 150, 150);
-        canvas.drawPath(gPathSpline, gPaint);
-
-        gPaint.setARGB(255, 150, 150, 150);
-        for (int j = 1; j < mSpline.getNbPoints() - 1; j++) {
-            ControlPoint point = mSpline.getPoint(j);
-            gPaint.setStrokeWidth(10);
-            gPaint.setARGB(255, 50, 50, 100);
-            canvas.drawCircle(point.x * w, point.y * h, 30, gPaint);
-            gPaint.setStrokeWidth(5);
-            gPaint.setARGB(255, 150, 150, 200);
-            canvas.drawCircle(point.x * w, point.y * h, 30, gPaint);
-        }
     }
 
     private int pickControlPoint(float x, float y) {
         int pick = 0;
-        float px = mSpline.getPoint(0).x;
-        float py = mSpline.getPoint(0).y;
+        float px = mSplines[mCurrentCurveIndex].getPoint(0).x;
+        float py = mSplines[mCurrentCurveIndex].getPoint(0).y;
         double delta = Math.sqrt((px - x) * (px - x) + (py - y) * (py - y));
-        for (int i = 1; i < mSpline.getNbPoints(); i++) {
-            px = mSpline.getPoint(i).x;
-            py = mSpline.getPoint(i).y;
+        for (int i = 1; i < mSplines[mCurrentCurveIndex].getNbPoints(); i++) {
+            px = mSplines[mCurrentCurveIndex].getPoint(i).x;
+            py = mSplines[mCurrentCurveIndex].getPoint(i).y;
             double currentDelta = Math.sqrt((px - x) * (px - x) + (py - y)
                     * (py - y));
             if (currentDelta < delta) {
@@ -182,77 +151,35 @@
         }
 
         if (!mDidAddPoint && (delta * getWidth() > 100)
-                && (mSpline.getNbPoints() < 10)) {
+                && (mSplines[mCurrentCurveIndex].getNbPoints() < 10)) {
             return -1;
         }
 
-        return pick;// mSpline.getPoint(pick);
-    }
-
-    public void showPopupMenu(View v) {
-        // TODO: sort out the popup menu UI for curves
-        final Context context = v.getContext();
-        PopupMenu popupMenu = new PopupMenu(v.getContext(), v);
-        popupMenu.getMenuInflater().inflate(R.menu.filtershow_menu_curves,
-                popupMenu.getMenu());
-
-        popupMenu
-                .setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
-
-                    @Override
-                    public boolean onMenuItemClick(MenuItem item) {
-                        Toast.makeText(context, item.toString(),
-                                Toast.LENGTH_LONG).show();
-                        return true;
-                    }
-                });
-
-        popupMenu.show();
+        return pick;
     }
 
     private String getFilterName() {
-        String filterName = "Curves";
-        if (mUseRed && !mUseGreen && !mUseBlue) {
-            filterName = "CurvesRed";
-        } else if (!mUseRed && mUseGreen && !mUseBlue) {
-            filterName = "CurvesGreen";
-        } else if (!mUseRed && !mUseGreen && mUseBlue) {
-            filterName = "CurvesBlue";
-        }
-        return filterName;
+        return "Curves";
     }
 
     @Override
     public synchronized boolean onTouchEvent(MotionEvent e) {
         float posX = e.getX() / getWidth();
-        float posY = e.getY() / getHeight();
-
-        /*
-         * if (true) { showPopupMenu(this); return true; }
-         */
-
-        // ControlPoint point = null;
-
-        // Log.v(LOGTAG, "onTouchEvent - " + e + " action masked : " +
-        // e.getActionMasked());
+        float posY = e.getY();
+        float margin = Spline.curveHandleSize() / 2;
+        if (posY < margin) {
+            posY = margin;
+        }
+        if (posY > getHeight() - margin) {
+            posY = getHeight() - margin;
+        }
+        posY = (posY - margin) / (getHeight() - 2 * margin);
 
         if (e.getActionMasked() == MotionEvent.ACTION_UP) {
             applyNewCurve();
-            // Log.v(LOGTAG, "ACTION UP, mCurrentControlPoint set to null!");
             mCurrentControlPoint = null;
-            String name = null;
-            if (mUseRed && mUseGreen && mUseBlue) {
-                name = "Curves (RGB)";
-            } else if (mUseRed) {
-                name = "Curves (Red)";
-            } else if (mUseGreen) {
-                name = "Curves (Green)";
-            } else if (mUseBlue) {
-                name = "Curves (Blue)";
-            }
-
-
-            ImagePreset copy = new ImagePreset(getImagePreset(),name);
+            String name = "Curves";
+            ImagePreset copy = new ImagePreset(getImagePreset(), name);
 
             copy.setIsFx(false);
             mImageLoader.getHistory().insert(copy, 0);
@@ -268,103 +195,31 @@
         if (mDidDelete) {
             return true;
         }
-        // Log.v(LOGTAG, "ACTION DOWN, mCurrentControlPoint is " +
-        // mCurrentControlPoint);
 
         int pick = pickControlPoint(posX, posY);
-        // Log.v(LOGTAG, "ACTION DOWN, pick is " + pick);
         if (mCurrentControlPoint == null) {
             if (pick == -1) {
                 mCurrentControlPoint = new ControlPoint(posX, posY);
-                mSpline.addPoint(mCurrentControlPoint);
+                mSplines[mCurrentCurveIndex].addPoint(mCurrentControlPoint);
                 mDidAddPoint = true;
-                // Log.v(LOGTAG, "ACTION DOWN - 2, added a new control point! "
-                // + mCurrentControlPoint);
-
             } else {
-                mCurrentControlPoint = mSpline.getPoint(pick);
-                // Log.v(LOGTAG, "ACTION DOWN - 2, picking up control point " +
-                // mCurrentControlPoint + " at pick " + pick);
+                mCurrentControlPoint = mSplines[mCurrentCurveIndex].getPoint(pick);
             }
         }
-        // Log.v(LOGTAG, "ACTION DOWN - 3, pick is " + pick);
 
-        if (!((mCurrentControlPoint.x == 0 && mCurrentControlPoint.y == 1) || (mCurrentControlPoint.x == 1 && mCurrentControlPoint.y == 0))) {
-            if (mSpline.isPointContained(posX, pick)) {
-                mCurrentControlPoint.x = posX;
-                mCurrentControlPoint.y = posY;
-                // Log.v(LOGTAG, "ACTION DOWN - 4, move control point " +
-                // mCurrentControlPoint);
-            } else if (pick != -1) {
-                // Log.v(LOGTAG, "ACTION DOWN - 4, delete pick " + pick);
-                mSpline.deletePoint(pick);
-                mDidDelete = true;
-            }
+        if (mSplines[mCurrentCurveIndex].isPointContained(posX, pick)) {
+            mCurrentControlPoint.x = posX;
+            mCurrentControlPoint.y = posY;
+        } else if (pick != -1) {
+            mSplines[mCurrentCurveIndex].deletePoint(pick);
+            mDidDelete = true;
         }
-        // Log.v(LOGTAG, "ACTION DOWN - 5, DONE");
         applyNewCurve();
         invalidate();
         return true;
     }
 
     public synchronized void applyNewCurve() {
-        ControlPoint[] points = new ControlPoint[256];
-        for (int i = 0; i < 256; i++) {
-            float v = i / 255.0f;
-            ControlPoint p = mSpline.getPoint(v);
-            points[i] = p;
-        }
-        for (int i = 0; i < 256; i++) {
-            mAppliedCurve[i] = -1;
-        }
-        for (int i = 0; i < 256; i++) {
-            int index = (int) (points[i].x * 255);
-            if (index >= 0 && index <= 255) {
-                float v = 1.0f - points[i].y;
-                if (v < 0) {
-                    v = 0;
-                }
-                if (v > 1.0f) {
-                    v = 1.0f;
-                }
-                mAppliedCurve[index] = v;
-            }
-        }
-        float prev = 0;
-        for (int i = 0; i < 256; i++) {
-            if (mAppliedCurve[i] == -1) {
-                // need to interpolate...
-                int j = i + 1;
-                if (j > 255) {
-                    j = 255;
-                }
-                for (; j < 256; j++) {
-                    if (mAppliedCurve[j] != -1) {
-                        break;
-                    }
-                }
-                if (j > 255) {
-                    j = 255;
-                }
-                // interpolate linearly between i and j - 1
-                float start = prev;
-                float end = mAppliedCurve[j];
-                float delta = (end - start) / (j - i + 1);
-                for (int k = i; k < j; k++) {
-                    start = start + delta;
-                    mAppliedCurve[k] = start;
-                }
-                i = j;
-            }
-            prev = mAppliedCurve[i];
-        }
-        for (int i = 0; i < 256; i++) {
-            mAppliedCurve[i] = mAppliedCurve[i] * 255;
-        }
-        float[] appliedCurve = new float[256];
-        for (int i = 0; i < 256; i++) {
-            appliedCurve[i] = mAppliedCurve[i];
-        }
         // update image
         if (getImagePreset() != null) {
             String filterName = getFilterName();
@@ -379,15 +234,92 @@
             }
 
             if (filter != null) {
-                filter.setSpline(new Spline(mSpline));
-                filter.setCurve(appliedCurve);
-                filter.setUseRed(mUseRed);
-                filter.setUseGreen(mUseGreen);
-                filter.setUseBlue(mUseBlue);
+                for (int i = 0; i < 4; i++) {
+                    filter.setSpline(new Spline(mSplines[i]), i);
+                }
             }
             mImageLoader.resetImageForPreset(getImagePreset(), this);
             invalidate();
         }
     }
 
+    class ComputeHistogramTask extends AsyncTask<Bitmap, Void, int[]> {
+        @Override
+        protected int[] doInBackground(Bitmap... params) {
+            int[] histo = new int[256 * 3];
+            Bitmap bitmap = params[0];
+            int w = bitmap.getWidth();
+            int h = bitmap.getHeight();
+            int[] pixels = new int[w * h];
+            bitmap.getPixels(pixels, 0, w, 0, 0, w, h);
+            for (int i = 0; i < w; i++) {
+                for (int j = 0; j < h; j++) {
+                    int index = j * w + i;
+                    int r = Color.red(pixels[index]);
+                    int g = Color.green(pixels[index]);
+                    int b = Color.blue(pixels[index]);
+                    histo[r]++;
+                    histo[256 + g]++;
+                    histo[512 + b]++;
+                }
+            }
+            return histo;
+        }
+
+        @Override
+        protected void onPostExecute(int[] result) {
+            System.arraycopy(result, 0, redHistogram, 0, 256);
+            System.arraycopy(result, 256, greenHistogram, 0, 256);
+            System.arraycopy(result, 512, blueHistogram, 0, 256);
+            invalidate();
+        }
+    }
+
+    private void drawHistogram(Canvas canvas, int[] histogram, int color, PorterDuff.Mode mode) {
+        int max = 0;
+        for (int i = 0; i < histogram.length; i++) {
+            if (histogram[i] > max) {
+                max = histogram[i];
+            }
+        }
+        float w = getWidth();
+        float h = getHeight();
+        float wl = w / histogram.length;
+        float wh = (0.3f * h) / max;
+        Paint paint = new Paint();
+        paint.setARGB(100, 255, 255, 255);
+        paint.setStrokeWidth((int) Math.ceil(wl));
+
+        Paint paint2 = new Paint();
+        paint2.setColor(color);
+        paint2.setStrokeWidth(6);
+        paint2.setXfermode(new PorterDuffXfermode(mode));
+        gHistoPath.reset();
+        gHistoPath.moveTo(0, h);
+        boolean firstPointEncountered = false;
+        float prev = 0;
+        float last = 0;
+        for (int i = 0; i < histogram.length; i++) {
+            float x = i * wl;
+            float l = histogram[i] * wh;
+            if (l != 0) {
+                float v = h - (l + prev) / 2.0f;
+                if (!firstPointEncountered) {
+                    gHistoPath.lineTo(x, h);
+                    firstPointEncountered = true;
+                }
+                gHistoPath.lineTo(x, v);
+                prev = l;
+                last = x;
+            }
+        }
+        gHistoPath.lineTo(last, h);
+        gHistoPath.lineTo(w, h);
+        gHistoPath.close();
+        canvas.drawPath(gHistoPath, paint2);
+        paint2.setStrokeWidth(2);
+        paint2.setStyle(Paint.Style.STROKE);
+        paint2.setARGB(255, 200, 200, 200);
+        canvas.drawPath(gHistoPath, paint2);
+    }
 }
diff --git a/src/com/android/gallery3d/filtershow/ui/Spline.java b/src/com/android/gallery3d/filtershow/ui/Spline.java
index a272d28..b5c7974 100644
--- a/src/com/android/gallery3d/filtershow/ui/Spline.java
+++ b/src/com/android/gallery3d/filtershow/ui/Spline.java
@@ -1,10 +1,28 @@
 
 package com.android.gallery3d.filtershow.ui;
 
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.drawable.Drawable;
+
 import java.util.Collections;
 import java.util.Vector;
 
 public class Spline {
+    private final Vector<ControlPoint> mPoints;
+    private static Drawable mCurveHandle;
+    private static int mCurveHandleSize;
+    private static int mCurveWidth;
+
+    public static final int RGB = 0;
+    public static final int RED = 1;
+    public static final int GREEN = 2;
+    public static final int BLUE = 3;
+
+    private final Paint gPaint = new Paint();
+
     public Spline() {
         mPoints = new Vector<ControlPoint>();
     }
@@ -13,28 +31,276 @@
         mPoints = new Vector<ControlPoint>();
         for (int i = 0; i < spline.mPoints.size(); i++) {
             ControlPoint p = spline.mPoints.elementAt(i);
-            mPoints.add(p);
+            mPoints.add(new ControlPoint(p));
         }
         Collections.sort(mPoints);
-        delta_t = 1.0f / mPoints.size();
     }
 
-    public ControlPoint interpolate(float t, ControlPoint p1,
-            ControlPoint p2, ControlPoint p3, ControlPoint p4) {
+    public static void setCurveHandle(Drawable drawable, int size) {
+        mCurveHandle = drawable;
+        mCurveHandleSize = size;
+    }
 
-        float t3 = t * t * t;
-        float t2 = t * t;
-        float b1 = 0.5f * (-t3 + 2 * t2 - t);
-        float b2 = 0.5f * (3 * t3 - 5 * t2 + 2);
-        float b3 = 0.5f * (-3 * t3 + 4 * t2 + t);
-        float b4 = 0.5f * (t3 - t2);
+    public static void setCurveWidth(int width) {
+        mCurveWidth = width;
+    }
 
-        ControlPoint b1p1 = p1.multiply(b1);
-        ControlPoint b2p2 = p2.multiply(b2);
-        ControlPoint b3p3 = p3.multiply(b3);
-        ControlPoint b4p4 = p4.multiply(b4);
+    public static int curveHandleSize() {
+        return mCurveHandleSize;
+    }
 
-        return b1p1.add(b2p2.add(b3p3.add(b4p4)));
+    public static int colorForCurve(int curveIndex) {
+        switch (curveIndex) {
+            case Spline.RED:
+                return Color.RED;
+            case GREEN:
+                return Color.GREEN;
+            case BLUE:
+                return Color.BLUE;
+        }
+        return Color.WHITE;
+    }
+
+    public boolean isOriginal() {
+        if (this.getNbPoints() > 2) {
+            return false;
+        }
+        if (mPoints.elementAt(0).x != 0 || mPoints.elementAt(0).y != 1) {
+            return false;
+        }
+        if (mPoints.elementAt(1).x != 1 || mPoints.elementAt(1).y != 0) {
+            return false;
+        }
+        return true;
+    }
+
+    private void drawHandles(Canvas canvas, Drawable indicator, float centerX, float centerY) {
+        int left = (int) centerX - mCurveHandleSize / 2;
+        int top = (int) centerY - mCurveHandleSize / 2;
+        indicator.setBounds(left, top, left + mCurveHandleSize, top + mCurveHandleSize);
+        indicator.draw(canvas);
+    }
+
+    public float[] getAppliedCurve() {
+        float[] curve = new float[256];
+        ControlPoint[] points = new ControlPoint[mPoints.size()];
+        for (int i = 0; i < mPoints.size(); i++) {
+            ControlPoint p = mPoints.get(i);
+            points[i] = new ControlPoint(p.x, p.y);
+        }
+        double[] derivatives = solveSystem(points);
+        int start = 0;
+        if (points[0].x != 0) {
+            start = (int) (points[0].x * 256);
+        }
+        for (int i = 0; i < start; i++) {
+            curve[i] = 1.0f - points[0].y;
+        }
+        for (int i = start; i < 256; i++) {
+            ControlPoint cur = null;
+            ControlPoint next = null;
+            double x = i / 256.0;
+            int pivot = 0;
+            for (int j = 0; j < points.length - 1; j++) {
+                if (x >= points[j].x && x <= points[j + 1].x) {
+                    pivot = j;
+                }
+            }
+            cur = points[pivot];
+            next = points[pivot + 1];
+            if (x <= next.x) {
+                double x1 = cur.x;
+                double x2 = next.x;
+                double y1 = cur.y;
+                double y2 = next.y;
+
+                // Use the second derivatives to apply the cubic spline
+                // equation:
+                double delta = (x2 - x1);
+                double delta2 = delta * delta;
+                double b = (x - x1) / delta;
+                double a = 1 - b;
+                double ta = a * y1;
+                double tb = b * y2;
+                double tc = (a * a * a - a) * derivatives[pivot];
+                double td = (b * b * b - b) * derivatives[pivot + 1];
+                double y = ta + tb + (delta2 / 6) * (tc + td);
+                if (y > 1.0f) {
+                    y = 1.0f;
+                }
+                if (y < 0) {
+                    y = 0;
+                }
+                curve[i] = (float) (1.0f - y);
+            } else {
+                curve[i] = 1.0f - next.y;
+            }
+        }
+        return curve;
+    }
+
+    private void drawGrid(Canvas canvas, float w, float h) {
+        // Grid
+        gPaint.setARGB(128, 150, 150, 150);
+        gPaint.setStrokeWidth(1);
+
+        float stepH = h / 9;
+        float stepW = w / 9;
+
+        // central diagonal
+        gPaint.setARGB(255, 100, 100, 100);
+        gPaint.setStrokeWidth(2);
+        canvas.drawLine(0, h, w, 0, gPaint);
+
+        gPaint.setARGB(128, 200, 200, 200);
+        gPaint.setStrokeWidth(4);
+        stepH = h / 3;
+        stepW = w / 3;
+        for (int j = 1; j < 3; j++) {
+            canvas.drawLine(0, j * stepH, w, j * stepH, gPaint);
+            canvas.drawLine(j * stepW, 0, j * stepW, h, gPaint);
+        }
+        canvas.drawLine(0, 0, 0, h, gPaint);
+        canvas.drawLine(w, 0, w, h, gPaint);
+        canvas.drawLine(0, 0, w, 0, gPaint);
+        canvas.drawLine(0, h, w, h, gPaint);
+    }
+
+    public void draw(Canvas canvas, int color, int canvasWidth, int canvasHeight,
+            boolean showHandles) {
+        float w = canvasWidth;
+        float h = canvasHeight - mCurveHandleSize;
+        float dx = 0;
+        float dy = mCurveHandleSize / 2;
+
+        // The cubic spline equation is (from numerical recipes in C):
+        // y = a(y_i) + b(y_i+1) + c(y"_i) + d(y"_i+1)
+        //
+        // with c(y"_i) and d(y"_i+1):
+        // c(y"_i) = 1/6 (a^3 - a) delta^2 (y"_i)
+        // d(y"_i_+1) = 1/6 (b^3 - b) delta^2 (y"_i+1)
+        //
+        // and delta:
+        // delta = x_i+1 - x_i
+        //
+        // To find the second derivatives y", we can rearrange the equation as:
+        // A(y"_i-1) + B(y"_i) + C(y"_i+1) = D
+        //
+        // With the coefficients A, B, C, D:
+        // A = 1/6 (x_i - x_i-1)
+        // B = 1/3 (x_i+1 - x_i-1)
+        // C = 1/6 (x_i+1 - x_i)
+        // D = (y_i+1 - y_i)/(x_i+1 - x_i) - (y_i - y_i-1)/(x_i - x_i-1)
+        //
+        // We can now easily solve the equation to find the second derivatives:
+        ControlPoint[] points = new ControlPoint[mPoints.size()];
+        for (int i = 0; i < mPoints.size(); i++) {
+            ControlPoint p = mPoints.get(i);
+            points[i] = new ControlPoint(p.x * w, p.y * h);
+        }
+        double[] derivatives = solveSystem(points);
+
+        Path path = new Path();
+        path.moveTo(0, points[0].y);
+        for (int i = 0; i < points.length - 1; i++) {
+            double x1 = points[i].x;
+            double x2 = points[i + 1].x;
+            double y1 = points[i].y;
+            double y2 = points[i + 1].y;
+
+            for (double x = x1; x < x2; x += 20) {
+                // Use the second derivatives to apply the cubic spline
+                // equation:
+                double delta = (x2 - x1);
+                double delta2 = delta * delta;
+                double b = (x - x1) / delta;
+                double a = 1 - b;
+                double ta = a * y1;
+                double tb = b * y2;
+                double tc = (a * a * a - a) * derivatives[i];
+                double td = (b * b * b - b) * derivatives[i + 1];
+                double y = ta + tb + (delta2 / 6) * (tc + td);
+                if (y > h) {
+                    y = h;
+                }
+                if (y < 0) {
+                    y = 0;
+                }
+                path.lineTo((float) x, (float) y);
+            }
+        }
+        canvas.save();
+        canvas.translate(dx, dy);
+        drawGrid(canvas, w, h);
+        ControlPoint lastPoint = points[points.length - 1];
+        path.lineTo(lastPoint.x, lastPoint.y);
+        path.lineTo(w, lastPoint.y);
+        Paint paint = new Paint();
+        paint.setAntiAlias(true);
+        paint.setFilterBitmap(true);
+        paint.setDither(true);
+        paint.setStyle(Paint.Style.STROKE);
+        int curveWidth = mCurveWidth;
+        if (showHandles) {
+            curveWidth *= 1.5;
+        }
+        paint.setStrokeWidth(curveWidth + 2);
+        paint.setColor(Color.BLACK);
+        canvas.drawPath(path, paint);
+        paint.setStrokeWidth(curveWidth);
+        paint.setColor(color);
+        canvas.drawPath(path, paint);
+        if (showHandles) {
+            for (int i = 0; i < points.length; i++) {
+                float x = points[i].x;
+                float y = points[i].y;
+                drawHandles(canvas, mCurveHandle, x, y);
+            }
+        }
+        canvas.restore();
+    }
+
+    double[] solveSystem(ControlPoint[] points) {
+        int n = points.length;
+        double[][] system = new double[n][3];
+        double[] result = new double[n]; // d
+        double[] solution = new double[n]; // returned coefficients
+        system[0][1] = 1;
+        system[n - 1][1] = 1;
+        double d6 = 1.0 / 6.0;
+        double d3 = 1.0 / 3.0;
+
+        // let's create a tridiagonal matrix representing the
+        // system, and apply the TDMA algorithm to solve it
+        // (see http://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm)
+        for (int i = 1; i < n - 1; i++) {
+            double deltaPrevX = points[i].x - points[i - 1].x;
+            double deltaX = points[i + 1].x - points[i - 1].x;
+            double deltaNextX = points[i + 1].x - points[i].x;
+            double deltaNextY = points[i + 1].y - points[i].y;
+            double deltaPrevY = points[i].y - points[i - 1].y;
+            system[i][0] = d6 * deltaPrevX; // a_i
+            system[i][1] = d3 * deltaX; // b_i
+            system[i][2] = d6 * deltaNextX; // c_i
+            result[i] = (deltaNextY / deltaNextX) - (deltaPrevY / deltaPrevX); // d_i
+        }
+
+        // Forward sweep
+        for (int i = 1; i < n; i++) {
+            // m = a_i/b_i-1
+            double m = system[i][0] / system[i - 1][1];
+            // b_i = b_i - m(c_i-1)
+            system[i][1] = system[i][1] - m * system[i - 1][2];
+            // d_i = d_i - m(d_i-1)
+            result[i] = result[i] - m * result[i - 1];
+        }
+
+        // Back substitution
+        solution[n - 1] = result[n - 1] / system[n - 1][1];
+        for (int i = n - 2; i >= 0; --i) {
+            solution[i] = (result[i] - system[i][2] * solution[i + 1]) / system[i][1];
+        }
+        return solution;
     }
 
     public void addPoint(float x, float y) {
@@ -44,42 +310,11 @@
     public void addPoint(ControlPoint v) {
         mPoints.add(v);
         Collections.sort(mPoints);
-        delta_t = 1.0f / mPoints.size();
     }
 
-    public ControlPoint getPoint(float t) {
-        int p = (int) (t / delta_t);
-        int p0 = p - 1;
-        int max = mPoints.size() - 1;
-
-        if (p0 < 0) {
-            p0 = 0;
-        } else if (p0 >= max) {
-            p0 = max;
-        }
-        int p1 = p;
-        if (p1 < 0) {
-            p1 = 0;
-        } else if (p1 >= max) {
-            p1 = max;
-        }
-        int p2 = p + 1;
-        if (p2 < 0) {
-            p2 = 0;
-        } else if (p2 >= max) {
-            p2 = max;
-        }
-        int p3 = p + 2;
-        if (p3 < 0) {
-            p3 = 0;
-        } else if (p3 >= max) {
-            p3 = max;
-        }
-        float lt = (t - delta_t * (float) p) / delta_t;
-        return interpolate(lt, mPoints.elementAt(p0),
-                mPoints.elementAt(p1), mPoints.elementAt(p2),
-                mPoints.elementAt(p3));
-
+    public void deletePoint(int n) {
+        mPoints.remove(n);
+        Collections.sort(mPoints);
     }
 
     public int getNbPoints() {
@@ -106,15 +341,6 @@
         return true;
     }
 
-    public void deletePoint(int n) {
-        mPoints.remove(n);
-        Collections.sort(mPoints);
-        delta_t = 1.0f / (mPoints.size() - 1f);
-    }
-
-    private Vector<ControlPoint> mPoints;
-    private float delta_t;
-
     public Spline copy() {
         Spline spline = new Spline();
         for (int i = 0; i < mPoints.size(); i++) {
@@ -123,4 +349,5 @@
         }
         return spline;
     }
+
 }
diff --git a/src/com/android/gallery3d/ui/ActionModeHandler.java b/src/com/android/gallery3d/ui/ActionModeHandler.java
index 8cebddd..3384b88 100644
--- a/src/com/android/gallery3d/ui/ActionModeHandler.java
+++ b/src/com/android/gallery3d/ui/ActionModeHandler.java
@@ -280,6 +280,7 @@
                 intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360);
                 intent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
             }
+            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
         }
 
         return intent;
@@ -315,6 +316,7 @@
                 intent.setAction(Intent.ACTION_SEND).setType(mimeType);
                 intent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
             }
+            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
             setNfcBeamPushUris(uris.toArray(new Uri[uris.size()]));
         } else {
             setNfcBeamPushUris(null);
diff --git a/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java b/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java
index d5337f0..9b3f29f 100644
--- a/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java
+++ b/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java
@@ -71,7 +71,7 @@
         mWaitLoadingTexture = new ColorTexture(mPlaceholderColor);
         mWaitLoadingTexture.setSize(1, 1);
         mCameraOverlay = new ResourceTexture(activity,
-                R.drawable.frame_overlay_gallery_camera);
+                R.drawable.ic_cameraalbum_overlay);
     }
 
     public void setPressedIndex(int index) {
diff --git a/src/com/android/gallery3d/ui/BasicTexture.java b/src/com/android/gallery3d/ui/BasicTexture.java
index 7b8e30d..99cf057 100644
--- a/src/com/android/gallery3d/ui/BasicTexture.java
+++ b/src/com/android/gallery3d/ui/BasicTexture.java
@@ -42,8 +42,8 @@
     protected int mWidth = UNSPECIFIED;
     protected int mHeight = UNSPECIFIED;
 
-    private int mTextureWidth;
-    private int mTextureHeight;
+    protected int mTextureWidth;
+    protected int mTextureHeight;
 
     private boolean mHasBorder;
 
diff --git a/src/com/android/gallery3d/ui/BitmapScreenNail.java b/src/com/android/gallery3d/ui/BitmapScreenNail.java
index bf31bcb..741eefb 100644
--- a/src/com/android/gallery3d/ui/BitmapScreenNail.java
+++ b/src/com/android/gallery3d/ui/BitmapScreenNail.java
@@ -19,209 +19,40 @@
 import android.graphics.Bitmap;
 import android.graphics.RectF;
 
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.data.BitmapPool;
-import com.android.gallery3d.data.MediaItem;
-
-// This is a ScreenNail wraps a Bitmap. There are some extra functions:
-//
-// - If we need to draw before the bitmap is available, we draw a rectange of
-// placeholder color (gray).
-//
-// - When the the bitmap is available, and we have drawn the placeholder color
-// before, we will do a fade-in animation.
 public class BitmapScreenNail implements ScreenNail {
-    @SuppressWarnings("unused")
-    private static final String TAG = "BitmapScreenNail";
-
-    // The duration of the fading animation in milliseconds
-    private static final int DURATION = 180;
-
-    private static int sMaxSide = 640;
-
-    // These are special values for mAnimationStartTime
-    private static final long ANIMATION_NOT_NEEDED = -1;
-    private static final long ANIMATION_NEEDED = -2;
-    private static final long ANIMATION_DONE = -3;
-
-    private int mWidth;
-    private int mHeight;
-    private Bitmap mBitmap;
-    private BitmapTexture mTexture;
-    private long mAnimationStartTime = ANIMATION_NOT_NEEDED;
+    private final BitmapTexture mBitmapTexture;
 
     public BitmapScreenNail(Bitmap bitmap) {
-        mWidth = bitmap.getWidth();
-        mHeight = bitmap.getHeight();
-        mBitmap = bitmap;
-        // We create mTexture lazily, so we don't incur the cost if we don't
-        // actually need it.
-    }
-
-    public BitmapScreenNail(int width, int height) {
-        setSize(width, height);
-    }
-
-    // This gets overridden by bitmap_screennail_placeholder
-    // in GalleryUtils.initialize
-    private static int mPlaceholderColor = 0xFF222222;
-    private static boolean mDrawPlaceholder = true;
-
-    public static void setPlaceholderColor(int color) {
-        mPlaceholderColor = color;
-    }
-
-    private void setSize(int width, int height) {
-        if (width == 0 || height == 0) {
-            width = sMaxSide;
-            height = sMaxSide * 3 / 4;
-        }
-        float scale = Math.min(1, (float) sMaxSide / Math.max(width, height));
-        mWidth = Math.round(scale * width);
-        mHeight = Math.round(scale * height);
-    }
-
-    private static void recycleBitmap(BitmapPool pool, Bitmap bitmap) {
-        if (pool == null || bitmap == null) return;
-        pool.recycle(bitmap);
-    }
-
-    // Combines the two ScreenNails.
-    // Returns the used one and recycle the unused one.
-    public ScreenNail combine(ScreenNail other) {
-        if (other == null) {
-            return this;
-        }
-
-        if (!(other instanceof BitmapScreenNail)) {
-            recycle();
-            return other;
-        }
-
-        // Now both are BitmapScreenNail. Move over the information about width,
-        // height, and Bitmap, then recycle the other.
-        BitmapScreenNail newer = (BitmapScreenNail) other;
-        mWidth = newer.mWidth;
-        mHeight = newer.mHeight;
-        if (newer.mBitmap != null) {
-            recycleBitmap(MediaItem.getThumbPool(), mBitmap);
-            mBitmap = newer.mBitmap;
-            newer.mBitmap = null;
-
-            if (mTexture != null) {
-                mTexture.recycle();
-                mTexture = null;
-            }
-        }
-
-        newer.recycle();
-        return this;
-    }
-
-    public void updatePlaceholderSize(int width, int height) {
-        if (mBitmap != null) return;
-        if (width == 0 || height == 0) return;
-        setSize(width, height);
+        mBitmapTexture = new BitmapTexture(bitmap);
     }
 
     @Override
     public int getWidth() {
-        return mWidth;
+        return mBitmapTexture.getWidth();
     }
 
     @Override
     public int getHeight() {
-        return mHeight;
-    }
-
-    @Override
-    public void noDraw() {
-    }
-
-    @Override
-    public void recycle() {
-        if (mTexture != null) {
-            mTexture.recycle();
-            mTexture = null;
-        }
-        recycleBitmap(MediaItem.getThumbPool(), mBitmap);
-        mBitmap = null;
-    }
-
-    public static void disableDrawPlaceholder() {
-        mDrawPlaceholder = false;
-    }
-
-    public static void enableDrawPlaceholder() {
-        mDrawPlaceholder = true;
+        return mBitmapTexture.getHeight();
     }
 
     @Override
     public void draw(GLCanvas canvas, int x, int y, int width, int height) {
-        if (mBitmap == null) {
-            if (mAnimationStartTime == ANIMATION_NOT_NEEDED) {
-                mAnimationStartTime = ANIMATION_NEEDED;
-            }
-            if(mDrawPlaceholder) {
-                canvas.fillRect(x, y, width, height, mPlaceholderColor);
-            }
-            return;
-        }
+        mBitmapTexture.draw(canvas, x, y, width, height);
+    }
 
-        if (mTexture == null) {
-            mTexture = new BitmapTexture(mBitmap);
-        }
+    @Override
+    public void noDraw() {
+        // do nothing
+    }
 
-        if (mAnimationStartTime == ANIMATION_NEEDED) {
-            mAnimationStartTime = now();
-        }
-
-        if (isAnimating()) {
-            canvas.drawMixed(mTexture, mPlaceholderColor, getRatio(), x, y,
-                    width, height);
-        } else {
-            mTexture.draw(canvas, x, y, width, height);
-        }
+    @Override
+    public void recycle() {
+        mBitmapTexture.recycle();
     }
 
     @Override
     public void draw(GLCanvas canvas, RectF source, RectF dest) {
-        if (mBitmap == null) {
-            canvas.fillRect(dest.left, dest.top, dest.width(), dest.height(),
-                    mPlaceholderColor);
-            return;
-        }
-
-        if (mTexture == null) {
-            mTexture = new BitmapTexture(mBitmap);
-        }
-
-        canvas.drawTexture(mTexture, source, dest);
-    }
-
-    public boolean isAnimating() {
-        if (mAnimationStartTime < 0) return false;
-        if (now() - mAnimationStartTime >= DURATION) {
-            mAnimationStartTime = ANIMATION_DONE;
-            return false;
-        }
-        return true;
-    }
-
-    private static long now() {
-        return AnimationTime.get();
-    }
-
-    private float getRatio() {
-        float r = (float)(now() - mAnimationStartTime) / DURATION;
-        return Utils.clamp(1.0f - r, 0.0f, 1.0f);
-    }
-
-    public boolean isShowingPlaceholder() {
-        return (mBitmap == null) || isAnimating();
-    }
-
-    public static void setMaxSide(int size) {
-        sMaxSide = size;
+        canvas.drawTexture(mBitmapTexture, source, dest);
     }
 }
diff --git a/src/com/android/gallery3d/ui/GLCanvas.java b/src/com/android/gallery3d/ui/GLCanvas.java
index e3a32ef..6f8baef 100644
--- a/src/com/android/gallery3d/ui/GLCanvas.java
+++ b/src/com/android/gallery3d/ui/GLCanvas.java
@@ -99,6 +99,13 @@
     public void drawMixed(BasicTexture from, int toColor,
             float ratio, int x, int y, int w, int h);
 
+    // Draw a region of a texture and a specified color to the specified
+    // rectangle. The actual color used is from * (1 - ratio) + to * ratio.
+    // The region of the texture is defined by parameter "src". The target
+    // rectangle is specified by parameter "target".
+    public void drawMixed(BasicTexture from, int toColor,
+            float ratio, RectF src, RectF target);
+
     // Gets the underlying GL instance. This is used only when direct access to
     // GL is needed.
     public GL11 getGLInstance();
diff --git a/src/com/android/gallery3d/ui/GLCanvasImpl.java b/src/com/android/gallery3d/ui/GLCanvasImpl.java
index d83daf3..45903b3 100644
--- a/src/com/android/gallery3d/ui/GLCanvasImpl.java
+++ b/src/com/android/gallery3d/ui/GLCanvasImpl.java
@@ -415,7 +415,7 @@
     // This function changes the source coordinate to the texture coordinates.
     // It also clips the source and target coordinates if it is beyond the
     // bound of the texture.
-    private void convertCoordinate(RectF source, RectF target,
+    private static void convertCoordinate(RectF source, RectF target,
             BasicTexture texture) {
 
         int width = texture.getWidth();
@@ -465,6 +465,82 @@
         color[3] = alpha;
     }
 
+    private void setMixedColor(int toColor, float ratio, float alpha) {
+        //
+        // The formula we want:
+        //     alpha * ((1 - ratio) * from + ratio * to)
+        //
+        // The formula that GL supports is in the form of:
+        //     combo * from + (1 - combo) * to * scale
+        //
+        // So, we have combo = alpha * (1 - ratio)
+        //     and     scale = alpha * ratio / (1 - combo)
+        //
+        float combo = alpha * (1 - ratio);
+        float scale = alpha * ratio / (1 - combo);
+
+        // Specify the interpolation factor via the alpha component of
+        // GL_TEXTURE_ENV_COLORs.
+        // RGB component are get from toColor and will used as SRC1
+        float colorScale = scale * (toColor >>> 24) / (0xff * 0xff);
+        setTextureColor(((toColor >>> 16) & 0xff) * colorScale,
+                ((toColor >>> 8) & 0xff) * colorScale,
+                (toColor & 0xff) * colorScale, combo);
+        GL11 gl = mGL;
+        gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0);
+
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_RGB, GL11.GL_CONSTANT);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_RGB, GL11.GL_SRC_COLOR);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_ALPHA, GL11.GL_CONSTANT);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_ALPHA, GL11.GL_SRC_ALPHA);
+
+        // Wire up the interpolation factor for RGB.
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA);
+
+        // Wire up the interpolation factor for alpha.
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA);
+
+    }
+
+    @Override
+    public void drawMixed(BasicTexture from, int toColor, float ratio,
+            RectF source, RectF target) {
+        if (target.width() <= 0 || target.height() <= 0) return;
+
+        if (ratio <= 0.01f) {
+            drawTexture(from, source, target);
+            return;
+        } else if (ratio >= 1) {
+            fillRect(target.left, target.top, target.width(), target.height(), toColor);
+            return;
+        }
+
+        float alpha = mAlpha;
+
+        // Copy the input to avoid changing it.
+        mDrawTextureSourceRect.set(source);
+        mDrawTextureTargetRect.set(target);
+        source = mDrawTextureSourceRect;
+        target = mDrawTextureTargetRect;
+
+        mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque()
+                || !Utils.isOpaque(toColor) || alpha < OPAQUE_ALPHA));
+
+        if (!bindTexture(from)) return;
+
+        // Interpolate the RGB and alpha values between both textures.
+        mGLState.setTexEnvMode(GL11.GL_COMBINE);
+        setMixedColor(toColor, ratio, alpha);
+        convertCoordinate(source, target, from);
+        setTextureCoords(source);
+        textureRect(target.left, target.top, target.width(), target.height());
+        mGLState.setTexEnvMode(GL11.GL_REPLACE);
+    }
+
     private void drawMixed(BasicTexture from, int toColor,
             float ratio, int x, int y, int width, int height, float alpha) {
         // change from 0 to 0.01f to prevent getting divided by zero below
@@ -482,45 +558,9 @@
         final GL11 gl = mGL;
         if (!bindTexture(from)) return;
 
-        //
-        // The formula we want:
-        //     alpha * ((1 - ratio) * from + ratio * to)
-        //
-        // The formula that GL supports is in the form of:
-        //     combo * from + (1 - combo) * to * scale
-        //
-        // So, we have combo = alpha * (1 - ratio)
-        //     and     scale = alpha * ratio / (1 - combo)
-        //
-        float combo = alpha * (1 - ratio);
-        float scale = alpha * ratio / (1 - combo);
-
         // Interpolate the RGB and alpha values between both textures.
         mGLState.setTexEnvMode(GL11.GL_COMBINE);
-
-        // Specify the interpolation factor via the alpha component of
-        // GL_TEXTURE_ENV_COLORs.
-        // RGB component are get from toColor and will used as SRC1
-        float colorScale = scale * (toColor >>> 24) / (0xff * 0xff);
-        setTextureColor(((toColor >>> 16) & 0xff) * colorScale,
-                ((toColor >>> 8) & 0xff) * colorScale,
-                (toColor & 0xff) * colorScale, combo);
-        gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0);
-
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE);
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE);
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_RGB, GL11.GL_CONSTANT);
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_RGB, GL11.GL_SRC_COLOR);
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_ALPHA, GL11.GL_CONSTANT);
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_ALPHA, GL11.GL_SRC_ALPHA);
-
-        // Wire up the interpolation factor for RGB.
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT);
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA);
-
-        // Wire up the interpolation factor for alpha.
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT);
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA);
+        setMixedColor(toColor, ratio, alpha);
 
         drawBoundTexture(from, x, y, width, height);
         mGLState.setTexEnvMode(GL11.GL_REPLACE);
diff --git a/src/com/android/gallery3d/ui/GLRoot.java b/src/com/android/gallery3d/ui/GLRoot.java
index 1651b43..13b610b 100644
--- a/src/com/android/gallery3d/ui/GLRoot.java
+++ b/src/com/android/gallery3d/ui/GLRoot.java
@@ -16,6 +16,7 @@
 
 package com.android.gallery3d.ui;
 
+import android.content.Context;
 import android.graphics.Matrix;
 
 import com.android.gallery3d.anim.CanvasAnimation;
@@ -45,4 +46,6 @@
     public void freeze();
     public void unfreeze();
     public void setLightsOutMode(boolean enabled);
+
+    public Context getContext();
 }
diff --git a/src/com/android/gallery3d/ui/GLView.java b/src/com/android/gallery3d/ui/GLView.java
index 3924c6e..664012c 100644
--- a/src/com/android/gallery3d/ui/GLView.java
+++ b/src/com/android/gallery3d/ui/GLView.java
@@ -21,6 +21,7 @@
 import android.view.MotionEvent;
 
 import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.anim.StateTransitionAnimation;
 import com.android.gallery3d.common.Utils;
 
 import java.util.ArrayList;
@@ -77,6 +78,9 @@
     protected int mScrollHeight = 0;
     protected int mScrollWidth = 0;
 
+    private float [] mBackgroundColor;
+    private StateTransitionAnimation mTransition;
+
     public void startAnimation(CanvasAnimation animation) {
         GLRoot root = getGLRoot();
         if (root == null) throw new IllegalStateException();
@@ -217,13 +221,46 @@
     }
 
     protected void render(GLCanvas canvas) {
+        boolean transitionActive = false;
+        if (mTransition != null && mTransition.calculate(AnimationTime.get())) {
+            invalidate();
+            transitionActive = mTransition.isActive();
+        }
         renderBackground(canvas);
+        canvas.save();
+        if (transitionActive) {
+            mTransition.applyContentTransform(this, canvas);
+        }
         for (int i = 0, n = getComponentCount(); i < n; ++i) {
             renderChild(canvas, getComponent(i));
         }
+        canvas.restore();
+        if (transitionActive) {
+            mTransition.applyOverlay(this, canvas);
+        }
+    }
+
+    public void setIntroAnimation(StateTransitionAnimation intro) {
+        mTransition = intro;
+        if (mTransition != null) mTransition.start();
+    }
+
+    public float [] getBackgroundColor() {
+        return mBackgroundColor;
+    }
+
+    public void setBackgroundColor(float [] color) {
+        mBackgroundColor = color;
     }
 
     protected void renderBackground(GLCanvas view) {
+        if (mBackgroundColor != null) {
+            view.clearBuffer(mBackgroundColor);
+        }
+        if (mTransition != null && mTransition.isActive()) {
+            mTransition.applyBackground(this, view);
+            return;
+        }
     }
 
     protected void renderChild(GLCanvas canvas, GLView component) {
diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java
index 5978159..747a34c 100644
--- a/src/com/android/gallery3d/ui/PhotoView.java
+++ b/src/com/android/gallery3d/ui/PhotoView.java
@@ -17,6 +17,7 @@
 package com.android.gallery3d.ui;
 
 import android.content.Context;
+import android.content.res.Configuration;
 import android.graphics.Color;
 import android.graphics.Matrix;
 import android.graphics.Rect;
@@ -546,10 +547,20 @@
     }
 
     private int getPanoramaRotation() {
-        // Panorama only support rotations of 0 and 90, so if it is greater
-        // than that flip the output surface texture to compensate
-        if (mDisplayRotation > 180)
+        // This function is magic
+        // The issue here is that Pano makes bad assumptions about rotation and
+        // orientation. The first is it assumes only two rotations are possible,
+        // 0 and 90. Thus, if display rotation is >= 180, we invert the output.
+        // The second is that it assumes landscape is a 90 rotation from portrait,
+        // however on landscape devices this is not true. Thus, if we are in portrait
+        // on a landscape device, we need to invert the output
+        int orientation = getGLRoot().getContext().getResources().getConfiguration().orientation;
+        boolean invertPortrait = (orientation == Configuration.ORIENTATION_PORTRAIT
+                && (mDisplayRotation == 90 || mDisplayRotation == 270));
+        boolean invert = (mDisplayRotation >= 180);
+        if (invert != invertPortrait) {
             return (mCompensation + 180) % 360;
+        }
         return mCompensation;
     }
 
@@ -839,8 +850,8 @@
         }
 
         private boolean isScreenNailAnimating() {
-            return (mScreenNail instanceof BitmapScreenNail)
-                    && ((BitmapScreenNail) mScreenNail).isAnimating();
+            return (mScreenNail instanceof TiledScreenNail)
+                    && ((TiledScreenNail) mScreenNail).isAnimating();
         }
 
         @Override
@@ -1002,8 +1013,8 @@
             // onDoubleTap happened on the second ACTION_DOWN.
             // We need to ignore the next UP event.
             mIgnoreUpEvent = true;
-            if (scale <= 1.0f || controller.isAtMinimalScale()) {
-                controller.zoomIn(x, y, Math.max(1.5f, scale * 1.5f));
+            if (scale <= .75f || controller.isAtMinimalScale()) {
+                controller.zoomIn(x, y, Math.max(1.0f, scale * 1.5f));
             } else {
                 controller.resetToFullView();
             }
@@ -1781,8 +1792,8 @@
             MediaItem item = mModel.getMediaItem(i);
             if (item == null) continue;
             ScreenNail sc = mModel.getScreenNail(i);
-            if (!(sc instanceof BitmapScreenNail)
-                    || ((BitmapScreenNail) sc).isShowingPlaceholder()) continue;
+            if (!(sc instanceof TiledScreenNail)
+                    || ((TiledScreenNail) sc).isShowingPlaceholder()) continue;
 
             // Now, sc is BitmapScreenNail and is not showing placeholder
             Rect rect = new Rect(getPhotoRect(i));
diff --git a/src/com/android/gallery3d/ui/PositionController.java b/src/com/android/gallery3d/ui/PositionController.java
index fffa7e0..6a4bcea 100644
--- a/src/com/android/gallery3d/ui/PositionController.java
+++ b/src/com/android/gallery3d/ui/PositionController.java
@@ -67,7 +67,7 @@
         SNAPBACK_ANIMATION_TIME,  // ANIM_KIND_SNAPBACK
         400,  // ANIM_KIND_SLIDE
         300,  // ANIM_KIND_ZOOM
-        PhotoPage.ANIM_TIME_OPENING,  // ANIM_KIND_OPENING
+        300,  // ANIM_KIND_OPENING
         0,    // ANIM_KIND_FLING (the duration is calculated dynamically)
         0,    // ANIM_KIND_FLING_X (see the comment above)
         0,    // ANIM_KIND_DELETE (the duration is calculated dynamically)
diff --git a/src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java b/src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java
index 812e831..36e7f4b 100644
--- a/src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java
+++ b/src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java
@@ -6,7 +6,7 @@
 import com.android.gallery3d.ui.GLRoot.OnGLIdleListener;
 
 public class PreparePageFadeoutTexture implements OnGLIdleListener {
-    private static final long TIMEOUT = FadeTexture.DURATION;
+    private static final long TIMEOUT = 200;
     public static final String KEY_FADE_TEXTURE = "fade_texture";
 
     private RawTexture mTexture;
@@ -15,10 +15,20 @@
     private GLView mRootPane;
 
     public PreparePageFadeoutTexture(GLView rootPane) {
-        mTexture = new RawTexture(rootPane.getWidth(), rootPane.getHeight(), true);
+        int w = rootPane.getWidth();
+        int h = rootPane.getHeight();
+        if (w == 0 || h == 0) {
+            mCancelled = true;
+            return;
+        }
+        mTexture = new RawTexture(w, h, true);
         mRootPane =  rootPane;
     }
 
+    public boolean isCancelled() {
+        return mCancelled;
+    }
+
     public synchronized RawTexture get() {
         if (mCancelled) {
             return null;
@@ -32,21 +42,26 @@
 
     @Override
     public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
-            if(!mCancelled) {
+        if (!mCancelled) {
+            try {
                 canvas.beginRenderTarget(mTexture);
                 mRootPane.render(canvas);
                 canvas.endRenderTarget();
-            } else {
+            } catch (RuntimeException e) {
                 mTexture = null;
             }
-            mResultReady.open();
-            return false;
+        } else {
+            mTexture = null;
+        }
+        mResultReady.open();
+        return false;
     }
 
     public static void prepareFadeOutTexture(AbstractGalleryActivity activity,
             GLView rootPane) {
-        GLRoot root = activity.getGLRoot();
         PreparePageFadeoutTexture task = new PreparePageFadeoutTexture(rootPane);
+        if (task.isCancelled()) return;
+        GLRoot root = activity.getGLRoot();
         RawTexture texture = null;
         root.unlockRenderThread();
         try {
@@ -56,8 +71,9 @@
             root.lockRenderThread();
         }
 
-        if (texture != null) {
-            activity.getTransitionStore().put(KEY_FADE_TEXTURE, texture);
+        if (texture == null) {
+            return;
         }
+        activity.getTransitionStore().put(KEY_FADE_TEXTURE, texture);
     }
 }
diff --git a/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java b/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java
index 1930e38..7cb8948 100644
--- a/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java
+++ b/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java
@@ -109,6 +109,7 @@
             canvas.translate(cx, cy);
             canvas.scale(1, -1, 1);
             canvas.translate(-cx, -cy);
+            updateTransformMatrix(mTransform);
             canvas.drawTexture(mExtTexture, mTransform, x, y, width, height);
             canvas.restore();
         }
@@ -119,6 +120,8 @@
         throw new UnsupportedOperationException();
     }
 
+    protected void updateTransformMatrix(float[] matrix) {}
+
     @Override
     abstract public void noDraw();
 
diff --git a/src/com/android/gallery3d/ui/TileImageView.java b/src/com/android/gallery3d/ui/TileImageView.java
index 5ce06be..8f26981 100644
--- a/src/com/android/gallery3d/ui/TileImageView.java
+++ b/src/com/android/gallery3d/ui/TileImageView.java
@@ -462,8 +462,8 @@
     }
 
     private boolean isScreenNailAnimating() {
-        return (mScreenNail instanceof BitmapScreenNail)
-                && ((BitmapScreenNail) mScreenNail).isAnimating();
+        return (mScreenNail instanceof TiledScreenNail)
+                && ((TiledScreenNail) mScreenNail).isAnimating();
     }
 
     private void uploadBackgroundTiles(GLCanvas canvas) {
@@ -575,8 +575,10 @@
                 }
                 if (tile == null) break;
                 if (!tile.isContentValid()) {
+                    boolean hasBeenLoaded = tile.isLoaded();
                     Utils.assertTrue(tile.mTileState == STATE_DECODED);
                     tile.updateContent(canvas);
+                    if (!hasBeenLoaded) tile.draw(canvas, 0, 0);
                     --quota;
                 }
             }
@@ -621,7 +623,6 @@
         }
     }
 
-    // TODO: avoid drawing the unused part of the textures.
     static boolean drawTile(
             Tile tile, GLCanvas canvas, RectF source, RectF target) {
         while (true) {
diff --git a/src/com/android/gallery3d/ui/TileImageViewAdapter.java b/src/com/android/gallery3d/ui/TileImageViewAdapter.java
index 08d3379..45e2ce2 100644
--- a/src/com/android/gallery3d/ui/TileImageViewAdapter.java
+++ b/src/com/android/gallery3d/ui/TileImageViewAdapter.java
@@ -40,51 +40,25 @@
     public TileImageViewAdapter() {
     }
 
-    public TileImageViewAdapter(
-            Bitmap bitmap, BitmapRegionDecoder regionDecoder) {
-        Utils.checkNotNull(bitmap);
-        updateScreenNail(new BitmapScreenNail(bitmap), true);
-        mRegionDecoder = regionDecoder;
-        mImageWidth = regionDecoder.getWidth();
-        mImageHeight = regionDecoder.getHeight();
-        mLevelCount = calculateLevelCount();
-    }
-
     public synchronized void clear() {
-        updateScreenNail(null, false);
+        mScreenNail = null;
         mImageWidth = 0;
         mImageHeight = 0;
         mLevelCount = 0;
         mRegionDecoder = null;
     }
 
-    public synchronized void setScreenNail(Bitmap bitmap, int width, int height) {
-        Utils.checkNotNull(bitmap);
-        updateScreenNail(new BitmapScreenNail(bitmap), true);
-        mImageWidth = width;
-        mImageHeight = height;
-        mRegionDecoder = null;
-        mLevelCount = 0;
-    }
-
+    // Caller is responsible to recycle the ScreenNail
     public synchronized void setScreenNail(
             ScreenNail screenNail, int width, int height) {
         Utils.checkNotNull(screenNail);
-        updateScreenNail(screenNail, false);
+        mScreenNail = screenNail;
         mImageWidth = width;
         mImageHeight = height;
         mRegionDecoder = null;
         mLevelCount = 0;
     }
 
-    private void updateScreenNail(ScreenNail screenNail, boolean own) {
-        if (mScreenNail != null && mOwnScreenNail) {
-            mScreenNail.recycle();
-        }
-        mScreenNail = screenNail;
-        mOwnScreenNail = own;
-    }
-
     public synchronized void setRegionDecoder(BitmapRegionDecoder decoder) {
         mRegionDecoder = Utils.checkNotNull(decoder);
         mImageWidth = decoder.getWidth();
diff --git a/src/com/android/gallery3d/ui/TiledScreenNail.java b/src/com/android/gallery3d/ui/TiledScreenNail.java
new file mode 100644
index 0000000..d2b34e3
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TiledScreenNail.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.RectF;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.BitmapPool;
+import com.android.gallery3d.data.MediaItem;
+
+// This is a ScreenNail wraps a Bitmap. There are some extra functions:
+//
+// - If we need to draw before the bitmap is available, we draw a rectange of
+// placeholder color (gray).
+//
+// - When the the bitmap is available, and we have drawn the placeholder color
+// before, we will do a fade-in animation.
+public class TiledScreenNail implements ScreenNail {
+    @SuppressWarnings("unused")
+    private static final String TAG = "TiledScreenNail";
+
+    // The duration of the fading animation in milliseconds
+    private static final int DURATION = 180;
+
+    private static int sMaxSide = 640;
+
+    // These are special values for mAnimationStartTime
+    private static final long ANIMATION_NOT_NEEDED = -1;
+    private static final long ANIMATION_NEEDED = -2;
+    private static final long ANIMATION_DONE = -3;
+
+    private int mWidth;
+    private int mHeight;
+    private long mAnimationStartTime = ANIMATION_NOT_NEEDED;
+
+    private Bitmap mBitmap;
+    private TiledTexture mTexture;
+
+    public TiledScreenNail(Bitmap bitmap) {
+        mWidth = bitmap.getWidth();
+        mHeight = bitmap.getHeight();
+        mBitmap = bitmap;
+        mTexture = new TiledTexture(bitmap);
+    }
+
+    public TiledScreenNail(int width, int height) {
+        setSize(width, height);
+    }
+
+    // This gets overridden by bitmap_screennail_placeholder
+    // in GalleryUtils.initialize
+    private static int mPlaceholderColor = 0xFF222222;
+    private static boolean mDrawPlaceholder = true;
+
+    public static void setPlaceholderColor(int color) {
+        mPlaceholderColor = color;
+    }
+
+    private void setSize(int width, int height) {
+        if (width == 0 || height == 0) {
+            width = sMaxSide;
+            height = sMaxSide * 3 / 4;
+        }
+        float scale = Math.min(1, (float) sMaxSide / Math.max(width, height));
+        mWidth = Math.round(scale * width);
+        mHeight = Math.round(scale * height);
+    }
+
+    private static void recycleBitmap(BitmapPool pool, Bitmap bitmap) {
+        if (pool == null || bitmap == null) return;
+        pool.recycle(bitmap);
+    }
+
+    // Combines the two ScreenNails.
+    // Returns the used one and recycle the unused one.
+    public ScreenNail combine(ScreenNail other) {
+        if (other == null) {
+            return this;
+        }
+
+        if (!(other instanceof TiledScreenNail)) {
+            recycle();
+            return other;
+        }
+
+        // Now both are TiledScreenNail. Move over the information about width,
+        // height, and Bitmap, then recycle the other.
+        TiledScreenNail newer = (TiledScreenNail) other;
+        mWidth = newer.mWidth;
+        mHeight = newer.mHeight;
+        if (newer.mTexture != null) {
+            recycleBitmap(MediaItem.getThumbPool(), mBitmap);
+            if (mTexture != null) mTexture.recycle();
+            mBitmap = newer.mBitmap;
+            mTexture = newer.mTexture;
+            newer.mBitmap = null;
+            newer.mTexture = null;
+        }
+        newer.recycle();
+        return this;
+    }
+
+    public void updatePlaceholderSize(int width, int height) {
+        if (mBitmap != null) return;
+        if (width == 0 || height == 0) return;
+        setSize(width, height);
+    }
+
+    @Override
+    public int getWidth() {
+        return mWidth;
+    }
+
+    @Override
+    public int getHeight() {
+        return mHeight;
+    }
+
+    @Override
+    public void noDraw() {
+    }
+
+    @Override
+    public void recycle() {
+        if (mTexture != null) {
+            mTexture.recycle();
+            mTexture = null;
+        }
+        recycleBitmap(MediaItem.getThumbPool(), mBitmap);
+        mBitmap = null;
+    }
+
+    public static void disableDrawPlaceholder() {
+        mDrawPlaceholder = false;
+    }
+
+    public static void enableDrawPlaceholder() {
+        mDrawPlaceholder = true;
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+        if (mTexture == null || !mTexture.isReady()) {
+            if (mAnimationStartTime == ANIMATION_NOT_NEEDED) {
+                mAnimationStartTime = ANIMATION_NEEDED;
+            }
+            if(mDrawPlaceholder) {
+                canvas.fillRect(x, y, width, height, mPlaceholderColor);
+            }
+            return;
+        }
+
+        if (mAnimationStartTime == ANIMATION_NEEDED) {
+            mAnimationStartTime = AnimationTime.get();
+        }
+
+        if (isAnimating()) {
+            mTexture.drawMixed(canvas, mPlaceholderColor, getRatio(), x, y,
+                    width, height);
+        } else {
+            mTexture.draw(canvas, x, y, width, height);
+        }
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, RectF source, RectF dest) {
+        if (mTexture == null || !mTexture.isReady()) {
+            canvas.fillRect(dest.left, dest.top, dest.width(), dest.height(),
+                    mPlaceholderColor);
+            return;
+        }
+
+        mTexture.draw(canvas, source, dest);
+    }
+
+    public boolean isAnimating() {
+        if (mAnimationStartTime < 0) return false;
+        if (AnimationTime.get() - mAnimationStartTime >= DURATION) {
+            mAnimationStartTime = ANIMATION_DONE;
+            return false;
+        }
+        return true;
+    }
+
+    private float getRatio() {
+        float r = (float) (AnimationTime.get() - mAnimationStartTime) / DURATION;
+        return Utils.clamp(1.0f - r, 0.0f, 1.0f);
+    }
+
+    public boolean isShowingPlaceholder() {
+        return (mBitmap == null) || isAnimating();
+    }
+
+    public TiledTexture getTexture() {
+        return mTexture;
+    }
+
+    public static void setMaxSide(int size) {
+        sMaxSide = size;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/TiledTexture.java b/src/com/android/gallery3d/ui/TiledTexture.java
new file mode 100644
index 0000000..6e9ad9e
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TiledTexture.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.RectF;
+
+import com.android.gallery3d.ui.GLRoot.OnGLIdleListener;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+
+// This class is similar to BitmapTexture, except the bitmap is
+// split into tiles. By doing so, we may increase the time required to
+// upload the whole bitmap but we reduce the time of uploading each tile
+// so it make the animation more smooth and prevents jank.
+public class TiledTexture {
+    private static final int CONTENT_SIZE = 254;
+    private static final int BORDER_SIZE = 1;
+    private static final int TILE_SIZE = CONTENT_SIZE + 2 * BORDER_SIZE;
+    private static final int INIT_CAPACITY = 8;
+
+    private static Tile sFreeTileHead = null;
+    private static final Object sFreeTileLock = new Object();
+
+    private static Bitmap sUploadBitmap;
+    private static Canvas sCanvas;
+    private static Paint sPaint;
+
+    private int mUploadIndex = 0;
+
+    private final Tile[] mTiles;
+    private final int mWidth;
+    private final int mHeight;
+    private final RectF mSrcRect = new RectF();
+    private final RectF mDestRect = new RectF();
+
+    public static class Uploader implements OnGLIdleListener {
+        private final ArrayDeque<TiledTexture> mTextures =
+                new ArrayDeque<TiledTexture>(INIT_CAPACITY);
+
+        private final GLRoot mGlRoot;
+        private boolean mIsQueued = false;
+
+        public Uploader(GLRoot glRoot) {
+            mGlRoot = glRoot;
+        }
+
+        public synchronized void clear() {
+            mTextures.clear();
+        }
+
+        public synchronized void addTexture(TiledTexture t) {
+            if (t.isReady()) return;
+            mTextures.addLast(t);
+
+            if (mIsQueued) return;
+            mIsQueued = true;
+            mGlRoot.addOnGLIdleListener(this);
+        }
+
+
+        @Override
+        public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
+            ArrayDeque<TiledTexture> deque = mTextures;
+            synchronized (this) {
+                if (!deque.isEmpty()) {
+                    TiledTexture t = deque.peekFirst();
+                    if (t.uploadNextTile(canvas)) {
+                        deque.removeFirst();
+                        mGlRoot.requestRender();
+                    }
+                }
+                mIsQueued = !mTextures.isEmpty();
+
+                // return true to keep this listener in the queue
+                return mIsQueued;
+            }
+        }
+    }
+
+    private static class Tile extends UploadedTexture {
+        public int offsetX;
+        public int offsetY;
+        public Bitmap bitmap;
+        public Tile nextFreeTile;
+        public int contentWidth;
+        public int contentHeight;
+
+        @Override
+        public void setSize(int width, int height) {
+            contentWidth = width;
+            contentHeight = height;
+            mWidth = width + 2 * BORDER_SIZE;
+            mHeight = height + 2 * BORDER_SIZE;
+            mTextureWidth = TILE_SIZE;
+            mTextureHeight = TILE_SIZE;
+        }
+
+        @Override
+        protected Bitmap onGetBitmap() {
+            int x = BORDER_SIZE - offsetX;
+            int y = BORDER_SIZE - offsetY;
+            int r = bitmap.getWidth() - x;
+            int b = bitmap.getHeight() - y ;
+            sCanvas.drawBitmap(bitmap, x, y, null);
+            bitmap = null;
+
+            // draw borders if need
+            if (x > 0) sCanvas.drawLine(x - 1, 0, x - 1, TILE_SIZE, sPaint);
+            if (y > 0) sCanvas.drawLine(0, y - 1, TILE_SIZE, y - 1, sPaint);
+            if (r < CONTENT_SIZE) sCanvas.drawLine(r, 0, r, TILE_SIZE, sPaint);
+            if (b < CONTENT_SIZE) sCanvas.drawLine(0, b, TILE_SIZE, b, sPaint);
+
+            return sUploadBitmap;
+        }
+
+        @Override
+        protected void onFreeBitmap(Bitmap bitmap) {
+            // do nothing
+        }
+    }
+
+    private static void freeTile(Tile tile) {
+        tile.invalidateContent();
+        tile.bitmap = null;
+        synchronized (sFreeTileLock) {
+            tile.nextFreeTile = sFreeTileHead;
+            sFreeTileHead = tile;
+        }
+    }
+
+    private static Tile obtainTile() {
+        synchronized (sFreeTileLock) {
+            Tile result = sFreeTileHead;
+            if (result == null) return new Tile();
+            sFreeTileHead = result.nextFreeTile;
+            result.nextFreeTile = null;
+            return result;
+        }
+    }
+
+    private boolean uploadNextTile(GLCanvas canvas) {
+        if (mUploadIndex == mTiles.length) return true;
+        Tile next = mTiles[mUploadIndex++];
+        boolean hasBeenLoad = next.isLoaded();
+        next.updateContent(canvas);
+
+        // It will take some time for a texture to be drawn for the first
+        // time. When scrolling, we need to draw several tiles on the screen
+        // at the same time. It may cause a UI jank even these textures has
+        // been uploaded.
+        if (!hasBeenLoad) next.draw(canvas, 0, 0);
+        return mUploadIndex == mTiles.length;
+    }
+
+    public TiledTexture(Bitmap bitmap) {
+        mWidth = bitmap.getWidth();
+        mHeight = bitmap.getHeight();
+        ArrayList<Tile> list = new ArrayList<Tile>();
+
+        for (int x = 0, w = mWidth; x < w; x += CONTENT_SIZE) {
+            for (int y = 0, h = mHeight; y < h; y += CONTENT_SIZE) {
+                Tile tile = obtainTile();
+                tile.offsetX = x;
+                tile.offsetY = y;
+                tile.bitmap = bitmap;
+                tile.setSize(
+                        Math.min(CONTENT_SIZE, mWidth - x),
+                        Math.min(CONTENT_SIZE, mHeight - y));
+                list.add(tile);
+            }
+        }
+        mTiles = list.toArray(new Tile[list.size()]);
+    }
+
+    public boolean isReady() {
+        return mUploadIndex == mTiles.length;
+    }
+
+    public void recycle() {
+        for (int i = 0, n = mTiles.length; i < n; ++i) {
+            freeTile(mTiles[i]);
+        }
+    }
+
+    public static void freeResources() {
+        sUploadBitmap = null;
+        sCanvas = null;
+        sPaint = null;
+    }
+
+    public static void prepareResources() {
+        sUploadBitmap = Bitmap.createBitmap(TILE_SIZE, TILE_SIZE, Config.ARGB_8888);
+        sCanvas = new Canvas(sUploadBitmap);
+        sPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
+        sPaint.setColor(Color.TRANSPARENT);
+        sPaint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
+    }
+
+    // We want to draw the "source" on the "target".
+    // This method is to find the "output" rectangle which is
+    // the corresponding area of the "src".
+    //                                   (x,y)  target
+    // (x0,y0)  source                     +---------------+
+    //    +----------+                     |               |
+    //    | src      |                     | output        |
+    //    | +--+     |    linear map       | +----+        |
+    //    | +--+     |    ---------->      | |    |        |
+    //    |          | by (scaleX, scaleY) | +----+        |
+    //    +----------+                     |               |
+    //      Texture                        +---------------+
+    //                                          Canvas
+    private static void mapRect(RectF output,
+            RectF src, float x0, float y0, float x, float y, float scaleX,
+            float scaleY) {
+        output.set(x + (src.left - x0) * scaleX,
+                y + (src.top - y0) * scaleY,
+                x + (src.right - x0) * scaleX,
+                y + (src.bottom - y0) * scaleY);
+    }
+
+    // Draws a mixed color of this texture and a specified color onto the
+    // a rectangle. The used color is: from * (1 - ratio) + to * ratio.
+    public void drawMixed(GLCanvas canvas, int color, float ratio,
+            int x, int y, int width, int height) {
+        RectF src = mSrcRect;
+        RectF dest = mDestRect;
+        float scaleX = (float) width / mWidth ;
+        float scaleY = (float) height / mHeight;
+        for (int i = 0, n = mTiles.length; i < n; ++i) {
+            Tile t = mTiles[i];
+            src.set(0, 0, t.contentWidth, t.contentHeight);
+            src.offset(t.offsetX, t.offsetY);
+            mapRect(dest, src, 0, 0, x, y, scaleX, scaleY);
+            src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
+            canvas.drawMixed(t, color, ratio, mSrcRect, mDestRect);
+        }
+    }
+
+    // Draws the texture on to the specified rectangle.
+    public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+        RectF src = mSrcRect;
+        RectF dest = mDestRect;
+        float scaleX = (float) width / mWidth ;
+        float scaleY = (float) height / mHeight;
+        for (int i = 0, n = mTiles.length; i < n; ++i) {
+            Tile t = mTiles[i];
+            src.set(0, 0, t.contentWidth, t.contentHeight);
+            src.offset(t.offsetX, t.offsetY);
+            mapRect(dest, src, 0, 0, x, y, scaleX, scaleY);
+            src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
+            canvas.drawTexture(t, mSrcRect, mDestRect);
+        }
+    }
+
+    // Draws a sub region of this texture on to the specified rectangle.
+    public void draw(GLCanvas canvas, RectF source, RectF target) {
+        RectF src = mSrcRect;
+        RectF dest = mDestRect;
+        float x0 = source.left;
+        float y0 = source.top;
+        float x = target.left;
+        float y = target.top;
+        float scaleX = target.width() / source.width();
+        float scaleY = target.height() / source.height();
+
+        for (int i = 0, n = mTiles.length; i < n; ++i) {
+            Tile t = mTiles[i];
+            src.set(0, 0, t.contentWidth, t.contentHeight);
+            src.offset(t.offsetX, t.offsetY);
+            if (!src.intersect(source)) continue;
+            mapRect(dest, src, x0, y0, x, y, scaleX, scaleY);
+            src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
+            canvas.drawTexture(t, src, dest);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/util/BucketNames.java b/src/com/android/gallery3d/util/BucketNames.java
index 043dd3d..df7684a 100644
--- a/src/com/android/gallery3d/util/BucketNames.java
+++ b/src/com/android/gallery3d/util/BucketNames.java
@@ -23,4 +23,5 @@
 
     public static final String IMPORTED = "Imported";
     public static final String DOWNLOAD = "download";
+    public static final String EDITED_ONLINE_PHOTOS = "EditedOnlinePhotos";
 }
diff --git a/src/com/android/gallery3d/util/GalleryUtils.java b/src/com/android/gallery3d/util/GalleryUtils.java
index 1955521..62f2235 100644
--- a/src/com/android/gallery3d/util/GalleryUtils.java
+++ b/src/com/android/gallery3d/util/GalleryUtils.java
@@ -42,7 +42,7 @@
 import com.android.gallery3d.common.ApiHelper;
 import com.android.gallery3d.data.DataManager;
 import com.android.gallery3d.data.MediaItem;
-import com.android.gallery3d.ui.BitmapScreenNail;
+import com.android.gallery3d.ui.TiledScreenNail;
 import com.android.gallery3d.util.ThreadPool.CancelListener;
 import com.android.gallery3d.util.ThreadPool.JobContext;
 
@@ -81,7 +81,7 @@
         wm.getDefaultDisplay().getMetrics(metrics);
         sPixelDensity = metrics.density;
         Resources r = context.getResources();
-        BitmapScreenNail.setPlaceholderColor(r.getColor(
+        TiledScreenNail.setPlaceholderColor(r.getColor(
                 R.color.bitmap_screennail_placeholder));
         initializeThumbnailSizes(metrics, r);
     }
@@ -91,7 +91,7 @@
         // Never need to completely fill the screen
         maxDimensionPixels = maxDimensionPixels / 2;
         MediaItem.setThumbnailSizes(maxDimensionPixels, 200);
-        BitmapScreenNail.setMaxSide(maxDimensionPixels);
+        TiledScreenNail.setMaxSide(maxDimensionPixels);
     }
 
     public static boolean isHighResolution(Context context) {
diff --git a/src/com/android/gallery3d/util/MediaSetUtils.java b/src/com/android/gallery3d/util/MediaSetUtils.java
index 9f5cbba..83b6b32 100644
--- a/src/com/android/gallery3d/util/MediaSetUtils.java
+++ b/src/com/android/gallery3d/util/MediaSetUtils.java
@@ -33,6 +33,9 @@
     public static final int DOWNLOAD_BUCKET_ID = GalleryUtils.getBucketId(
             Environment.getExternalStorageDirectory().toString() + "/"
             + BucketNames.DOWNLOAD);
+    public static final int EDITED_ONLINE_PHOTOS_BUCKET_ID = GalleryUtils.getBucketId(
+            Environment.getExternalStorageDirectory().toString() + "/"
+            + BucketNames.EDITED_ONLINE_PHOTOS);
     public static final int IMPORTED_BUCKET_ID = GalleryUtils.getBucketId(
             Environment.getExternalStorageDirectory().toString() + "/"
             + BucketNames.IMPORTED);
diff --git a/src_pd/com/android/gallery3d/util/LightCycleHelper.java b/src_pd/com/android/gallery3d/util/LightCycleHelper.java
index 995eac8..6ebd9ec 100644
--- a/src_pd/com/android/gallery3d/util/LightCycleHelper.java
+++ b/src_pd/com/android/gallery3d/util/LightCycleHelper.java
@@ -17,7 +17,6 @@
 package com.android.gallery3d.util;
 
 import android.app.Activity;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
@@ -39,6 +38,29 @@
         }
     }
 
+    public static class PanoramaViewHelper {
+
+        public PanoramaViewHelper(Activity activity) {
+            /* Do nothing */
+        }
+
+        public void onStart() {
+            /* Do nothing */
+        }
+
+        public void onCreate() {
+            /* Do nothing */
+        }
+
+        public void onStop() {
+            /* Do nothing */
+        }
+
+        public void showPanorama(Uri uri) {
+            /* Do nothing */
+        }
+    }
+
     public static void setupCaptureIntent(Context context, Intent it, String outputDir) {
         /* Do nothing */
     }
@@ -47,10 +69,6 @@
         return false;
     }
 
-    public static void viewPanorama(Activity activity, Uri uri) {
-        /* Do nothing */
-    }
-
     public static PanoramaMetadata getPanoramaMetadata(Context context, Uri uri) {
         return null;
     }
diff --git a/tests/src/com/android/gallery3d/ui/GLCanvasStub.java b/tests/src/com/android/gallery3d/ui/GLCanvasStub.java
index 2f2d753..5a08b85 100644
--- a/tests/src/com/android/gallery3d/ui/GLCanvasStub.java
+++ b/tests/src/com/android/gallery3d/ui/GLCanvasStub.java
@@ -83,4 +83,6 @@
     public void dumpStatisticsAndClear() {}
     public void beginRenderTarget(RawTexture texture) {}
     public void endRenderTarget() {}
+    public void drawMixed(BasicTexture from, int toColor,
+            float ratio, RectF src, RectF target) {}
 }
diff --git a/tests/src/com/android/gallery3d/ui/GLRootMock.java b/tests/src/com/android/gallery3d/ui/GLRootMock.java
index 467edfc..b1c4355 100644
--- a/tests/src/com/android/gallery3d/ui/GLRootMock.java
+++ b/tests/src/com/android/gallery3d/ui/GLRootMock.java
@@ -16,6 +16,7 @@
 
 package com.android.gallery3d.ui;
 
+import android.content.Context;
 import android.graphics.Matrix;
 import com.android.gallery3d.anim.CanvasAnimation;
 
@@ -42,4 +43,5 @@
     public void freeze() {}
     public void unfreeze() {}
     public void setLightsOutMode(boolean enabled) {}
+    public Context getContext() { return null; }
 }
diff --git a/tests/src/com/android/gallery3d/ui/GLRootStub.java b/tests/src/com/android/gallery3d/ui/GLRootStub.java
index 0f3a001..7f134db 100644
--- a/tests/src/com/android/gallery3d/ui/GLRootStub.java
+++ b/tests/src/com/android/gallery3d/ui/GLRootStub.java
@@ -16,6 +16,7 @@
 
 package com.android.gallery3d.ui;
 
+import android.content.Context;
 import android.graphics.Matrix;
 import com.android.gallery3d.anim.CanvasAnimation;
 
@@ -35,4 +36,5 @@
     public void freeze() {}
     public void unfreeze() {}
     public void setLightsOutMode(boolean enabled) {}
+    public Context getContext() { return null; }
 }