Merge "Fix 5481444 Face clustering should use PWA profile shot instead of random photo" into ics-mr1
diff --git a/src/com/android/gallery3d/app/AbstractGalleryActivity.java b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
index 0fde9cc..d25f60e 100644
--- a/src/com/android/gallery3d/app/AbstractGalleryActivity.java
+++ b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
@@ -68,6 +68,7 @@
     public void onConfigurationChanged(Configuration config) {
         super.onConfigurationChanged(config);
         mStateManager.onConfigurationChange(config);
+        invalidateOptionsMenu();
     }
 
     public Context getAndroidContext() {
diff --git a/src/com/android/gallery3d/app/AlbumPage.java b/src/com/android/gallery3d/app/AlbumPage.java
index eecb8e2..10c2b67 100644
--- a/src/com/android/gallery3d/app/AlbumPage.java
+++ b/src/com/android/gallery3d/app/AlbumPage.java
@@ -17,7 +17,6 @@
 package com.android.gallery3d.app;
 
 import android.app.Activity;
-import android.app.ProgressDialog;
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
@@ -73,6 +72,9 @@
     private static final int REQUEST_PHOTO = 2;
     private static final int REQUEST_DO_ANIMATION = 3;
 
+    private static final int BIT_LOADING_RELOAD = 1;
+    private static final int BIT_LOADING_SYNC = 2;
+
     private static final float USER_DISTANCE_METER = 0.3f;
 
     private boolean mIsActive = false;
@@ -99,11 +101,11 @@
     private boolean mShowDetails;
     private float mUserDistance; // in pixel
 
-    private ProgressDialog mProgressDialog;
-    private Future<?> mPendingTask;
-
     private Future<Integer> mSyncTask = null;
 
+    private int mLoadingBits = 0;
+    private boolean mInitialSynced = false;
+
     private final GLView mRootPane = new GLView() {
         private final float mMatrix[] = new float[16];
 
@@ -175,23 +177,16 @@
             if (mGetContent) {
                 onGetContent(item);
             } else {
-                boolean playVideo =
-                    (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0;
-                if (playVideo) {
-                    // Play the video.
-                    PhotoPage.playVideo((Activity) mActivity, item.getPlayUri(), item.getName());
-                } else {
-                    // Get into the PhotoPage.
-                    Bundle data = new Bundle();
-                    mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
-                    data.putInt(PhotoPage.KEY_INDEX_HINT, slotIndex);
-                    data.putString(PhotoPage.KEY_MEDIA_SET_PATH,
-                            mMediaSetPath.toString());
-                    data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH,
-                            item.getPath().toString());
-                    mActivity.getStateManager().startStateForResult(
-                            PhotoPage.class, REQUEST_PHOTO, data);
-                }
+                // Get into the PhotoPage.
+                Bundle data = new Bundle();
+                mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+                data.putInt(PhotoPage.KEY_INDEX_HINT, slotIndex);
+                data.putString(PhotoPage.KEY_MEDIA_SET_PATH,
+                        mMediaSetPath.toString());
+                data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH,
+                        item.getPath().toString());
+                mActivity.getStateManager().startStateForResult(
+                        PhotoPage.class, REQUEST_PHOTO, data);
             }
         } else {
             mSelectionManager.toggle(item.getPath());
@@ -333,6 +328,10 @@
         mAlbumDataAdapter.resume();
         mAlbumView.resume();
         mActionModeHandler.resume();
+        if (!mInitialSynced) {
+            mSyncTask = mMediaSet.requestSync(this);
+            setLoadingBit(BIT_LOADING_SYNC);
+        }
     }
 
     @Override
@@ -342,16 +341,7 @@
         mAlbumDataAdapter.pause();
         mAlbumView.pause();
         DetailsHelper.pause();
-        Future<?> task = mPendingTask;
-        if (task != null) {
-            // cancel on going task
-            task.cancel();
-            task.waitDone();
-            if (mProgressDialog != null) {
-                mProgressDialog.dismiss();
-                mProgressDialog = null;
-            }
-        }
+
         if (mSyncTask != null) {
             mSyncTask.cancel();
             mSyncTask = null;
@@ -575,9 +565,11 @@
         ((Activity) mActivity).runOnUiThread(new Runnable() {
             @Override
             public void run() {
+                if (resultCode == MediaSet.SYNC_RESULT_SUCCESS) {
+                    mInitialSynced = true;
+                }
                 if (!mIsActive) return;
-                mediaSet.notifyContentChanged(); // force reload to handle spinner
-
+                clearLoadingBit(BIT_LOADING_SYNC);
                 if (resultCode == MediaSet.SYNC_RESULT_ERROR) {
                     Toast.makeText((Context) mActivity, R.string.sync_album_error,
                             Toast.LENGTH_LONG).show();
@@ -586,33 +578,42 @@
         });
     }
 
+    private void setLoadingBit(int loadTaskBit) {
+        if (mLoadingBits == 0) {
+            GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
+        }
+        mLoadingBits |= loadTaskBit;
+    }
+
+    private void clearLoadingBit(int loadTaskBit) {
+        mLoadingBits &= ~loadTaskBit;
+        if (mLoadingBits == 0) {
+            GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
+
+            if (mAlbumDataAdapter.size() == 0) {
+                Toast.makeText((Context) mActivity,
+                        R.string.empty_album, Toast.LENGTH_LONG).show();
+                mActivity.getStateManager().finishState(AlbumPage.this);
+            }
+        }
+    }
+
     private class MyLoadingListener implements LoadingListener {
         @Override
         public void onLoadingStarted() {
-            GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
+            setLoadingBit(BIT_LOADING_RELOAD);
         }
 
         @Override
         public void onLoadingFinished() {
             if (!mIsActive) return;
-            if (mAlbumDataAdapter.size() == 0) {
-                if (mSyncTask == null) {
-                    mSyncTask = mMediaSet.requestSync(AlbumPage.this);
-                }
-                if (mSyncTask.isDone()){
-                    Toast.makeText((Context) mActivity,
-                            R.string.empty_album, Toast.LENGTH_LONG).show();
-                    mActivity.getStateManager().finishState(AlbumPage.this);
-                }
-            }
-            if (mSyncTask == null || mSyncTask.isDone()) {
-                GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
-            }
+            clearLoadingBit(BIT_LOADING_RELOAD);
         }
     }
 
     private class MyDetailsSource implements DetailsHelper.DetailsSource {
         private int mIndex;
+
         public int size() {
             return mAlbumDataAdapter.size();
         }
diff --git a/src/com/android/gallery3d/app/AlbumSetPage.java b/src/com/android/gallery3d/app/AlbumSetPage.java
index b7e0097..c31e920 100644
--- a/src/com/android/gallery3d/app/AlbumSetPage.java
+++ b/src/com/android/gallery3d/app/AlbumSetPage.java
@@ -43,7 +43,6 @@
 import com.android.gallery3d.ui.AlbumSetView;
 import com.android.gallery3d.ui.DetailsHelper;
 import com.android.gallery3d.ui.DetailsHelper.CloseListener;
-import com.android.gallery3d.util.GalleryUtils;
 import com.android.gallery3d.ui.GLCanvas;
 import com.android.gallery3d.ui.GLView;
 import com.android.gallery3d.ui.GridDrawer;
@@ -55,6 +54,7 @@
 import com.android.gallery3d.ui.SlotView;
 import com.android.gallery3d.ui.StaticBackground;
 import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
 
 public class AlbumSetPage extends ActivityState implements
         SelectionManager.SelectionListener, GalleryActionBar.ClusterRunner,
@@ -70,6 +70,9 @@
     private static final int DATA_CACHE_SIZE = 256;
     private static final int REQUEST_DO_ANIMATION = 1;
 
+    private static final int BIT_LOADING_RELOAD = 1;
+    private static final int BIT_LOADING_SYNC = 2;
+
     private boolean mIsActive = false;
     private StaticBackground mStaticBackground;
     private AlbumSetView mAlbumSetView;
@@ -103,6 +106,9 @@
 
     private Future<Integer> mSyncTask = null;
 
+    private int mLoadingBits = 0;
+    private boolean mInitialSynced = false;
+
     private final GLView mRootPane = new GLView() {
         private final float mMatrix[] = new float[16];
 
@@ -284,6 +290,30 @@
         startTransition();
     }
 
+    private void clearLoadingBit(int loadingBit) {
+        mLoadingBits &= ~loadingBit;
+        if (mLoadingBits == 0) {
+            GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
+
+            // Only show toast when there's no album and we are going to finish
+            // the page. Toast is redundant if we are going to stay on this page.
+            if ((mAlbumSetDataAdapter.size() == 0)) {
+                Toast.makeText((Context) mActivity,
+                        R.string.empty_album, Toast.LENGTH_LONG).show();
+                if (mActivity.getStateManager().getStateCount() > 1) {
+                    mActivity.getStateManager().finishState(this);
+                }
+            }
+        }
+    }
+
+    private void setLoadingBit(int loadingBit) {
+        if (mLoadingBits == 0) {
+            GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
+        }
+        mLoadingBits |= loadingBit;
+    }
+
     @Override
     public void onPause() {
         super.onPause();
@@ -298,6 +328,7 @@
         if (mSyncTask != null) {
             mSyncTask.cancel();
             mSyncTask = null;
+            clearLoadingBit(BIT_LOADING_SYNC);
         }
     }
 
@@ -314,6 +345,10 @@
         if (mShowClusterMenu && actionBar != null) {
             actionBar.showClusterMenu(mSelectedAction, this);
         }
+        if (!mInitialSynced) {
+            mSyncTask = mMediaSet.requestSync(AlbumSetPage.this);
+            setLoadingBit(BIT_LOADING_SYNC);
+        }
     }
 
     private void initializeData(Bundle data) {
@@ -571,9 +606,11 @@
         ((Activity) mActivity).runOnUiThread(new Runnable() {
             @Override
             public void run() {
+                if (resultCode == MediaSet.SYNC_RESULT_SUCCESS) {
+                    mInitialSynced = true;
+                }
                 if (!mIsActive) return;
-                mediaSet.notifyContentChanged(); // force reload to handle spinner
-
+                clearLoadingBit(BIT_LOADING_SYNC);
                 if (resultCode == MediaSet.SYNC_RESULT_ERROR) {
                     Toast.makeText((Context) mActivity, R.string.sync_album_set_error,
                             Toast.LENGTH_LONG).show();
@@ -584,29 +621,12 @@
 
     private class MyLoadingListener implements LoadingListener {
         public void onLoadingStarted() {
-            GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
+            setLoadingBit(BIT_LOADING_RELOAD);
         }
 
         public void onLoadingFinished() {
             if (!mIsActive) return;
-
-            if (mSyncTask == null) {
-                // Request sync in case the mediaSet hasn't been sync'ed before.
-                mSyncTask = mMediaSet.requestSync(AlbumSetPage.this);
-            }
-            if (mSyncTask.isDone()){
-                // The mediaSet is in sync. Turn off the loading indicator.
-                GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
-
-                // Only show toast when there's no album and we are going to finish
-                // the page. Toast is redundant if we are going to stay on this page.
-                if ((mAlbumSetDataAdapter.size() == 0)
-                        && (mActivity.getStateManager().getStateCount() > 1)) {
-                    Toast.makeText((Context) mActivity,
-                            R.string.empty_album, Toast.LENGTH_LONG).show();
-                    mActivity.getStateManager().finishState(AlbumSetPage.this);
-                }
-            }
+            clearLoadingBit(BIT_LOADING_RELOAD);
         }
     }
 
diff --git a/src/com/android/gallery3d/app/MoviePlayer.java b/src/com/android/gallery3d/app/MoviePlayer.java
index 07fe71a..9c81c79 100644
--- a/src/com/android/gallery3d/app/MoviePlayer.java
+++ b/src/com/android/gallery3d/app/MoviePlayer.java
@@ -16,11 +16,6 @@
 
 package com.android.gallery3d.app;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.common.BlobCache;
-import com.android.gallery3d.util.CacheManager;
-import com.android.gallery3d.util.GalleryUtils;
-
 import android.app.ActionBar;
 import android.app.AlertDialog;
 import android.content.BroadcastReceiver;
@@ -35,13 +30,16 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
-import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.MediaController;
 import android.widget.VideoView;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.BlobCache;
+import com.android.gallery3d.util.CacheManager;
+import com.android.gallery3d.util.GalleryUtils;
+
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.DataInputStream;
@@ -162,7 +160,8 @@
     }
 
     private void showSystemUi(boolean visible) {
-        int flag = visible ? 0 : View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
+        int flag = visible ? 0 : View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+                View.SYSTEM_UI_FLAG_LOW_PROFILE;
         mVideoView.setSystemUiVisibility(flag);
     }
 
@@ -348,9 +347,7 @@
 
         @Override
         public void onReceive(Context context, Intent intent) {
-            if (mVideoView.isPlaying()) {
-                mVideoView.pause();
-          }
+            if (mVideoView.isPlaying()) pauseVideo();
         }
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/EffectsBar.java b/src/com/android/gallery3d/photoeditor/EffectsBar.java
index acb22b6..4075404 100644
--- a/src/com/android/gallery3d/photoeditor/EffectsBar.java
+++ b/src/com/android/gallery3d/photoeditor/EffectsBar.java
@@ -67,8 +67,6 @@
                 return select;
             }
         });
-
-        setEnabled(false);
     }
 
     private void createEffectsGallery(int effectsId) {
@@ -130,13 +128,12 @@
 
     private boolean exitActiveEffect(final Runnable runnableOnDone) {
         if (activeEffect != null) {
-            final SpinnerProgressDialog progressDialog = SpinnerProgressDialog.show(
-                    (ViewGroup) getRootView().findViewById(R.id.toolbar));
+            SpinnerProgressDialog.showDialog();
             activeEffect.end(new Runnable() {
 
                 @Override
                 public void run() {
-                    progressDialog.dismiss();
+                    SpinnerProgressDialog.dismissDialog();
                     View fullscreenTool = getRootView().findViewById(R.id.fullscreen_effect_tool);
                     if (fullscreenTool != null) {
                         ((ViewGroup) fullscreenTool.getParent()).removeView(fullscreenTool);
diff --git a/src/com/android/gallery3d/photoeditor/FilterStack.java b/src/com/android/gallery3d/photoeditor/FilterStack.java
index 7a15509..fe6fb10 100644
--- a/src/com/android/gallery3d/photoeditor/FilterStack.java
+++ b/src/com/android/gallery3d/photoeditor/FilterStack.java
@@ -17,9 +17,11 @@
 package com.android.gallery3d.photoeditor;
 
 import android.graphics.Bitmap;
+import android.os.Bundle;
 
 import com.android.gallery3d.photoeditor.filters.Filter;
 
+import java.util.ArrayList;
 import java.util.Stack;
 
 /**
@@ -35,6 +37,9 @@
         void onStackChanged(boolean canUndo, boolean canRedo);
     }
 
+    private static final String APPLIED_STACK_KEY = "applied_stack";
+    private static final String REDO_STACK_KEY = "redo_stack";
+
     private final Stack<Filter> appliedStack = new Stack<Filter>();
     private final Stack<Filter> redoStack = new Stack<Filter>();
 
@@ -45,12 +50,28 @@
 
     private Photo source;
     private Runnable queuedTopFilterChange;
-    private boolean topFilterOutputted;
+    private boolean outputTopFilter;
     private volatile boolean paused;
 
-    public FilterStack(PhotoView photoView, StackListener stackListener) {
+    public FilterStack(PhotoView photoView, StackListener stackListener, Bundle savedState) {
         this.photoView = photoView;
         this.stackListener = stackListener;
+        if (savedState != null) {
+            appliedStack.addAll(getFilters(savedState, APPLIED_STACK_KEY));
+            redoStack.addAll(getFilters(savedState, REDO_STACK_KEY));
+            outputTopFilter = true;
+            stackListener.onStackChanged(!appliedStack.empty(), !redoStack.empty());
+       }
+    }
+
+    private ArrayList<Filter> getFilters(Bundle savedState, String key) {
+        // Infer Filter array-list from the Parcelable array-list by the specified returned type.
+        return savedState.getParcelableArrayList(key);
+    }
+
+    public void saveStacks(Bundle outState) {
+        outState.putParcelableArrayList(APPLIED_STACK_KEY, new ArrayList<Filter>(appliedStack));
+        outState.putParcelableArrayList(REDO_STACK_KEY, new ArrayList<Filter>(redoStack));
     }
 
     private void reallocateBuffer(int target) {
@@ -72,18 +93,19 @@
 
             // Source photo will be displayed if there is no filter stacked.
             Photo photo = source;
-            int size = topFilterOutputted ? appliedStack.size() : appliedStack.size() - 1;
+            int size = outputTopFilter ? appliedStack.size() : appliedStack.size() - 1;
             for (int i = 0; i < size && !paused; i++) {
                 photo = runFilter(i);
             }
-            photoView.setPhoto(photo, topFilterOutputted);
+            // Clear photo-view transformation when the top filter will be outputted.
+            photoView.setPhoto(photo, outputTopFilter);
         }
     }
 
     private void invalidateTopFilter() {
         if (!appliedStack.empty()) {
+            outputTopFilter = true;
             photoView.setPhoto(runFilter(appliedStack.size() - 1), true);
-            topFilterOutputted = true;
         }
     }
 
@@ -133,12 +155,12 @@
         });
     }
 
-    public void saveBitmap(final OnDoneBitmapCallback callback) {
+    public void getOutputBitmap(final OnDoneBitmapCallback callback) {
         photoView.queue(new Runnable() {
 
             @Override
             public void run() {
-                int filterIndex = appliedStack.size() - (topFilterOutputted ? 1 : 2);
+                int filterIndex = appliedStack.size() - (outputTopFilter ? 1 : 2);
                 Photo photo = (filterIndex < 0) ? source : buffers[getOutBufferIndex(filterIndex)];
                 final Bitmap bitmap = (photo != null) ? photo.save() : null;
                 photoView.post(new Runnable() {
@@ -166,7 +188,7 @@
 
     private void pushFilterInternal(Filter filter) {
         appliedStack.push(filter);
-        topFilterOutputted = false;
+        outputTopFilter = false;
         stackChanged();
     }
 
diff --git a/src/com/android/gallery3d/photoeditor/PhotoEditor.java b/src/com/android/gallery3d/photoeditor/PhotoEditor.java
index 2294d03..8f3990b 100644
--- a/src/com/android/gallery3d/photoeditor/PhotoEditor.java
+++ b/src/com/android/gallery3d/photoeditor/PhotoEditor.java
@@ -30,15 +30,19 @@
  */
 public class PhotoEditor extends Activity {
 
+    private static final String SAVE_URI_KEY = "save_uri";
+
     private Uri sourceUri;
     private Uri saveUri;
     private FilterStack filterStack;
     private ActionBar actionBar;
+    private EffectsBar effectsBar;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.photoeditor_main);
+        SpinnerProgressDialog.initialize((ViewGroup) findViewById(R.id.toolbar));
 
         Intent intent = getIntent();
         if (Intent.ACTION_EDIT.equalsIgnoreCase(intent.getAction())) {
@@ -53,24 +57,26 @@
                     public void onStackChanged(boolean canUndo, boolean canRedo) {
                         actionBar.updateButtons(canUndo, canRedo);
                     }
-        });
+        }, savedInstanceState);
+        if (savedInstanceState != null) {
+            saveUri = savedInstanceState.getParcelable(SAVE_URI_KEY);
+            actionBar.updateSave(saveUri == null);
+        }
 
-        EffectsBar effectsBar = (EffectsBar) findViewById(R.id.effects_bar);
+        // Effects-bar is initially disabled until photo is successfully loaded.
+        effectsBar = (EffectsBar) findViewById(R.id.effects_bar);
         effectsBar.initialize(filterStack);
+        effectsBar.setEnabled(false);
 
-        actionBar.setClickRunnable(R.id.undo_button, createUndoRedoRunnable(true, effectsBar));
-        actionBar.setClickRunnable(R.id.redo_button, createUndoRedoRunnable(false, effectsBar));
-        actionBar.setClickRunnable(R.id.save_button, createSaveRunnable(effectsBar));
-        actionBar.setClickRunnable(R.id.share_button, createShareRunnable(effectsBar));
-        actionBar.setClickRunnable(R.id.action_bar_back, createBackRunnable(effectsBar));
-    }
-
-    private SpinnerProgressDialog createProgressDialog() {
-        return SpinnerProgressDialog.show((ViewGroup) findViewById(R.id.toolbar));
+        actionBar.setClickRunnable(R.id.undo_button, createUndoRedoRunnable(true));
+        actionBar.setClickRunnable(R.id.redo_button, createUndoRedoRunnable(false));
+        actionBar.setClickRunnable(R.id.save_button, createSaveRunnable());
+        actionBar.setClickRunnable(R.id.share_button, createShareRunnable());
+        actionBar.setClickRunnable(R.id.action_bar_back, createBackRunnable());
     }
 
     private void openPhoto() {
-        final SpinnerProgressDialog progressDialog = createProgressDialog();
+        SpinnerProgressDialog.showDialog();
         LoadScreennailTask.Callback callback = new LoadScreennailTask.Callback() {
 
             @Override
@@ -79,7 +85,8 @@
 
                     @Override
                     public void onDone() {
-                        progressDialog.dismiss();
+                        SpinnerProgressDialog.dismissDialog();
+                        effectsBar.setEnabled(result != null);
                     }
                 });
             }
@@ -87,7 +94,7 @@
         new LoadScreennailTask(this, callback).execute(sourceUri);
     }
 
-    private Runnable createUndoRedoRunnable(final boolean undo, final EffectsBar effectsBar) {
+    private Runnable createUndoRedoRunnable(final boolean undo) {
         return new Runnable() {
 
             @Override
@@ -96,12 +103,12 @@
 
                     @Override
                     public void run() {
-                        final SpinnerProgressDialog progressDialog = createProgressDialog();
+                        SpinnerProgressDialog.showDialog();
                         OnDoneCallback callback = new OnDoneCallback() {
 
                             @Override
                             public void onDone() {
-                                progressDialog.dismiss();
+                                SpinnerProgressDialog.dismissDialog();
                             }
                         };
                         if (undo) {
@@ -115,7 +122,7 @@
         };
     }
 
-    private Runnable createSaveRunnable(final EffectsBar effectsBar) {
+    private Runnable createSaveRunnable() {
         return new Runnable() {
 
             @Override
@@ -124,8 +131,8 @@
 
                     @Override
                     public void run() {
-                        final SpinnerProgressDialog progressDialog = createProgressDialog();
-                        filterStack.saveBitmap(new OnDoneBitmapCallback() {
+                        SpinnerProgressDialog.showDialog();
+                        filterStack.getOutputBitmap(new OnDoneBitmapCallback() {
 
                             @Override
                             public void onDone(Bitmap bitmap) {
@@ -133,9 +140,9 @@
 
                                     @Override
                                     public void onComplete(Uri result) {
-                                        progressDialog.dismiss();
-                                        actionBar.updateSave(result == null);
+                                        SpinnerProgressDialog.dismissDialog();
                                         saveUri = result;
+                                        actionBar.updateSave(saveUri == null);
                                     }
                                 };
                                 new SaveCopyTask(PhotoEditor.this, sourceUri, callback).execute(
@@ -148,7 +155,7 @@
         };
     }
 
-    private Runnable createShareRunnable(final EffectsBar effectsBar) {
+    private Runnable createShareRunnable() {
         return new Runnable() {
 
             @Override
@@ -169,7 +176,7 @@
         };
     }
 
-    private Runnable createBackRunnable(final EffectsBar effectsBar) {
+    private Runnable createBackRunnable() {
         return new Runnable() {
 
             @Override
@@ -200,15 +207,23 @@
     }
 
     @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        filterStack.saveStacks(outState);
+        outState.putParcelable(SAVE_URI_KEY, saveUri);
+    }
+
+    @Override
     public void onBackPressed() {
         actionBar.clickBack();
     }
 
     @Override
     protected void onPause() {
-        // TODO: Close running progress dialogs as all pending operations will be paused.
         super.onPause();
         filterStack.onPause();
+        // Dismiss any running progress dialog as all operations are paused.
+        SpinnerProgressDialog.dismissDialog();
     }
 
     @Override
diff --git a/src/com/android/gallery3d/photoeditor/SaveCopyTask.java b/src/com/android/gallery3d/photoeditor/SaveCopyTask.java
index bedd416..b7d5626 100644
--- a/src/com/android/gallery3d/photoeditor/SaveCopyTask.java
+++ b/src/com/android/gallery3d/photoeditor/SaveCopyTask.java
@@ -136,7 +136,7 @@
 
         ContentValues values = new ContentValues();
         values.put(Images.Media.TITLE, saveFileName);
-        values.put(Images.Media.DISPLAY_NAME, saveFileName);
+        values.put(Images.Media.DISPLAY_NAME, file.getName());
         values.put(Images.Media.MIME_TYPE, "image/jpeg");
         values.put(Images.Media.DATE_TAKEN, dateTaken);
         values.put(Images.Media.DATE_MODIFIED, now);
diff --git a/src/com/android/gallery3d/photoeditor/SpinnerProgressDialog.java b/src/com/android/gallery3d/photoeditor/SpinnerProgressDialog.java
index 9a3d849..065075e 100644
--- a/src/com/android/gallery3d/photoeditor/SpinnerProgressDialog.java
+++ b/src/com/android/gallery3d/photoeditor/SpinnerProgressDialog.java
@@ -18,55 +18,68 @@
 
 import android.app.Dialog;
 import android.view.MotionEvent;
+import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewGroup.LayoutParams;
 import android.widget.ProgressBar;
 
 import com.android.gallery3d.R;
 
+import java.util.ArrayList;
+
 /**
  * Spinner model progress dialog that disables all tools for user interaction after it shows up and
- * and re-enables them after it dismisses.
+ * and re-enables them after it dismisses; this class along with all its methods should be accessed
+ * in only UI thread and allows only one instance at a time.
  */
 public class SpinnerProgressDialog extends Dialog {
 
-    private final ViewGroup tools;
+    private static ViewGroup toolbar;
+    private static SpinnerProgressDialog dialog;
+    private final ArrayList<View> enabledTools = new ArrayList<View>();
 
-    public static SpinnerProgressDialog show(ViewGroup tools) {
-        SpinnerProgressDialog dialog = new SpinnerProgressDialog(tools);
-        dialog.setCancelable(false);
-        dialog.show();
-        return dialog;
+    public static void initialize(ViewGroup toolbar) {
+        SpinnerProgressDialog.toolbar = toolbar;
     }
 
-    private SpinnerProgressDialog(ViewGroup tools) {
-        super(tools.getContext(), R.style.SpinnerProgressDialog);
+    public static void showDialog() {
+        // There should be only one progress dialog running at a time.
+        if (dialog == null) {
+            dialog = new SpinnerProgressDialog();
+            dialog.setCancelable(false);
+            dialog.show();
+            // Disable enabled tools when showing spinner progress dialog.
+            for (int i = 0; i < toolbar.getChildCount(); i++) {
+                View view = toolbar.getChildAt(i);
+                if (view.isEnabled()) {
+                    dialog.enabledTools.add(view);
+                    view.setEnabled(false);
+                }
+            }
+        }
+    }
 
-        addContentView(new ProgressBar(tools.getContext()), new LayoutParams(
+    public static void dismissDialog() {
+        if (dialog != null) {
+            dialog.dismiss();
+            // Enable tools that were disabled by this spinner progress dialog.
+            for (View view : dialog.enabledTools) {
+                view.setEnabled(true);
+            }
+            dialog = null;
+        }
+    }
+
+    private SpinnerProgressDialog() {
+        super(toolbar.getContext(), R.style.SpinnerProgressDialog);
+        addContentView(new ProgressBar(toolbar.getContext()), new LayoutParams(
                 LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
-
-        this.tools = tools;
-        enableTools(false);
-    }
-
-    @Override
-    public void dismiss() {
-        super.dismiss();
-
-        enableTools(true);
     }
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
         super.onTouchEvent(event);
-
         // Pass touch events to tools for killing idle even when the progress dialog is shown.
-        return tools.dispatchTouchEvent(event);
-    }
-
-    private void enableTools(boolean enabled) {
-        for (int i = 0; i < tools.getChildCount(); i++) {
-            tools.getChildAt(i).setEnabled(enabled);
-        }
+        return toolbar.dispatchTouchEvent(event);
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/ColorTemperatureAction.java b/src/com/android/gallery3d/photoeditor/actions/ColorTemperatureAction.java
index 41a89db..24978fa 100644
--- a/src/com/android/gallery3d/photoeditor/actions/ColorTemperatureAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/ColorTemperatureAction.java
@@ -44,7 +44,7 @@
             @Override
             public void onProgressChanged(float progress, boolean fromUser) {
                 if (fromUser) {
-                    filter.setColorTemperature(progress);
+                    filter.setScale(progress);
                     notifyFilterChanged(filter, true);
                 }
             }
diff --git a/src/com/android/gallery3d/photoeditor/actions/Doodle.java b/src/com/android/gallery3d/photoeditor/actions/Doodle.java
new file mode 100644
index 0000000..ea23e23
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/Doodle.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Vector;
+
+/**
+ * Doodle that consists of a color and doodling path for drawing.
+ */
+public class Doodle implements Parcelable {
+
+    private final int color;
+    private final Path normalizedPath = new Path();
+    private final Vector<PointF> points = new Vector<PointF>();
+
+    /**
+     * Creates paint for doodles.
+     */
+    public static Paint createPaint() {
+        Paint paint = new Paint(Paint.DITHER_FLAG | Paint.ANTI_ALIAS_FLAG);
+        paint.setStyle(Paint.Style.STROKE);
+        paint.setStrokeJoin(Paint.Join.ROUND);
+        paint.setStrokeCap(Paint.Cap.ROUND);
+        paint.setStrokeWidth(15);
+        return paint;
+    }
+
+    public Doodle(int color, PointF startPoint) {
+        this.color = Color.argb(192, Color.red(color), Color.green(color), Color.blue(color));
+        normalizedPath.moveTo(startPoint.x, startPoint.y);
+        points.add(startPoint);
+    }
+
+    /**
+     * Adds control points whose coordinates range from 0 to 1 to construct the doodle path.
+     *
+     * @return true if the constructed path is in (0, 0, 1, 1) bounds; otherwise, false.
+     */
+    public boolean addControlPoint(PointF point) {
+        PointF last = points.lastElement();
+        normalizedPath.quadTo(last.x, last.y, (last.x + point.x) / 2, (last.y + point.y) / 2);
+        points.add(point);
+
+        RectF r = new RectF();
+        normalizedPath.computeBounds(r, false);
+        return r.intersects(0, 0, 1, 1);
+    }
+
+    public int getColor() {
+        return color;
+    }
+
+    public boolean isEmpty() {
+        return normalizedPath.isEmpty();
+    }
+
+    /**
+     * Gets the drawing path from the normalized doodle path.
+     */
+    public void getDrawingPath(Matrix matrix, Path path) {
+        path.set(normalizedPath);
+        path.transform(matrix);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(color);
+        dest.writeInt(points.size());
+        for (PointF point : points) {
+            dest.writeParcelable(point, 0);
+        }
+    }
+
+    public static final Parcelable.Creator<Doodle> CREATOR = new Parcelable.Creator<Doodle>() {
+
+        @Override
+        public Doodle createFromParcel(Parcel source) {
+            int color = source.readInt();
+            int size = source.readInt();
+            if (size > 0) {
+                Doodle doodle = new Doodle(color, (PointF) source.readParcelable(null));
+                for (int i = 1; i < size; i++) {
+                    doodle.addControlPoint((PointF) source.readParcelable(null));
+                }
+                return doodle;
+            }
+            return new Doodle(color, new PointF(0, 0));
+        }
+
+        @Override
+        public Doodle[] newArray(int size) {
+            return new Doodle[size];
+        }};
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/DoodleAction.java b/src/com/android/gallery3d/photoeditor/actions/DoodleAction.java
index b82414d..4ad2cfb 100644
--- a/src/com/android/gallery3d/photoeditor/actions/DoodleAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/DoodleAction.java
@@ -17,7 +17,6 @@
 package com.android.gallery3d.photoeditor.actions;
 
 import android.content.Context;
-import android.graphics.Path;
 import android.util.AttributeSet;
 
 import com.android.gallery3d.photoeditor.filters.DoodleFilter;
@@ -64,8 +63,8 @@
             }
 
             @Override
-            public void onDoodleFinished(Path path, int color) {
-                filter.addPath(path, color);
+            public void onDoodleFinished(Doodle doodle) {
+                filter.addDoodle(doodle);
                 notifyFilterChanged(filter, false);
             }
         });
diff --git a/src/com/android/gallery3d/photoeditor/actions/DoodlePaint.java b/src/com/android/gallery3d/photoeditor/actions/DoodlePaint.java
deleted file mode 100644
index bcde9f1..0000000
--- a/src/com/android/gallery3d/photoeditor/actions/DoodlePaint.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.photoeditor.actions;
-
-import android.graphics.Paint;
-
-/**
- * A paint class for doodle effect.
- */
-public class DoodlePaint extends Paint {
-
-    public DoodlePaint() {
-        super(Paint.DITHER_FLAG | Paint.ANTI_ALIAS_FLAG);
-
-        setStyle(Paint.Style.STROKE);
-        setStrokeJoin(Paint.Join.ROUND);
-        setStrokeCap(Paint.Cap.ROUND);
-        setStrokeWidth(15);
-    }
-}
diff --git a/src/com/android/gallery3d/photoeditor/actions/DoodleView.java b/src/com/android/gallery3d/photoeditor/actions/DoodleView.java
index cc5af84..b596861 100644
--- a/src/com/android/gallery3d/photoeditor/actions/DoodleView.java
+++ b/src/com/android/gallery3d/photoeditor/actions/DoodleView.java
@@ -19,7 +19,6 @@
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
-import android.graphics.Color;
 import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.Path;
@@ -40,19 +39,20 @@
 
         void onDoodleInPhotoBounds();
 
-        void onDoodleFinished(Path path, int color);
+        void onDoodleFinished(Doodle doodle);
     }
 
-    private final Path normalizedPath = new Path();
-    private final Path drawingPath = new Path();
-    private final Paint doodlePaint = new DoodlePaint();
     private final Paint bitmapPaint = new Paint(Paint.DITHER_FLAG);
+    private final Paint doodlePaint = Doodle.createPaint();
     private final PointF lastPoint = new PointF();
-    private final Matrix pathMatrix = new Matrix();
+    private final Path drawingPath = new Path();
+    private final Matrix drawingMatrix = new Matrix();
     private final Matrix displayMatrix = new Matrix();
 
     private Bitmap bitmap;
     private Canvas bitmapCanvas;
+    private Doodle doodle;
+    private int color;
     private OnDoodleChangeListener listener;
 
     public DoodleView(Context context, AttributeSet attrs) {
@@ -71,50 +71,51 @@
         if ((bitmap == null) && !r.isEmpty()) {
             bitmap = Bitmap.createBitmap((int) r.width(), (int) r.height(),
                     Bitmap.Config.ARGB_8888);
-            bitmap.eraseColor(0x00000000);
             bitmapCanvas = new Canvas(bitmap);
 
             // Set up a matrix that maps back normalized paths to be drawn on the bitmap or canvas.
-            pathMatrix.setRectToRect(new RectF(0, 0, 1, 1), r, Matrix.ScaleToFit.FILL);
+            drawingMatrix.setRectToRect(new RectF(0, 0, 1, 1), r, Matrix.ScaleToFit.FILL);
         }
         displayMatrix.setRectToRect(r, displayBounds, Matrix.ScaleToFit.FILL);
     }
 
     private void drawDoodle(Canvas canvas) {
-        if ((canvas != null) && !normalizedPath.isEmpty()) {
-            drawingPath.set(normalizedPath);
-            drawingPath.transform(pathMatrix);
+        if ((canvas != null) && (doodle != null)) {
+            doodlePaint.setColor(doodle.getColor());
+            doodle.getDrawingPath(drawingMatrix, drawingPath);
             canvas.drawPath(drawingPath, doodlePaint);
         }
     }
 
     public void setColor(int color) {
-        // Reset path to draw in a new color.
-        finishCurrentPath();
-        normalizedPath.moveTo(lastPoint.x, lastPoint.y);
-        doodlePaint.setColor(Color.argb(192, Color.red(color), Color.green(color),
-                Color.blue(color)));
+        // Restart doodle to draw in a new color.
+        this.color = color;
+        finishDoodle();
+        startDoodle();
     }
 
-    private void finishCurrentPath() {
-        if (!normalizedPath.isEmpty()) {
-            // Update the finished path to the bitmap.
+    private void startDoodle() {
+        doodle = new Doodle(color, new PointF(lastPoint.x, lastPoint.y));
+    }
+
+    private void finishDoodle() {
+        if ((doodle != null) && !doodle.isEmpty()) {
+            // Update the finished non-empty doodle to the bitmap.
             drawDoodle(bitmapCanvas);
             if (listener != null) {
-                listener.onDoodleFinished(new Path(normalizedPath), doodlePaint.getColor());
+                listener.onDoodleFinished(doodle);
             }
-            normalizedPath.rewind();
             invalidate();
         }
+        doodle = null;
     }
 
-    private void checkCurrentPathInBounds() {
-        if ((listener != null) && !normalizedPath.isEmpty()) {
-            RectF r = new RectF();
-            normalizedPath.computeBounds(r, false);
-            if (r.intersects(0, 0, 1, 1)) {
+    private void addLastPointIntoDoodle() {
+        if ((doodle != null) && doodle.addControlPoint(new PointF(lastPoint.x, lastPoint.y))) {
+            if (listener != null) {
                 listener.onDoodleInPhotoBounds();
             }
+            invalidate();
         }
     }
 
@@ -129,26 +130,20 @@
             switch (event.getAction()) {
                 case MotionEvent.ACTION_DOWN:
                     mapPhotoPoint(x, y, lastPoint);
-                    normalizedPath.moveTo(lastPoint.x, lastPoint.y);
+                    startDoodle();
                     break;
 
                 case MotionEvent.ACTION_MOVE:
-                    float lastX = lastPoint.x;
-                    float lastY = lastPoint.y;
                     mapPhotoPoint(x, y, lastPoint);
-                    normalizedPath.quadTo(lastX, lastY, (lastX + lastPoint.x) / 2,
-                            (lastY + lastPoint.y) / 2);
-                    checkCurrentPathInBounds();
-                    invalidate();
+                    addLastPointIntoDoodle();
                     break;
 
                 case MotionEvent.ACTION_CANCEL:
                 case MotionEvent.ACTION_UP:
                     // Line to last position with offset to draw at least dots for single clicks.
                     mapPhotoPoint(x + 1, y + 1, lastPoint);
-                    normalizedPath.lineTo(lastPoint.x, lastPoint.y);
-                    checkCurrentPathInBounds();
-                    finishCurrentPath();
+                    addLastPointIntoDoodle();
+                    finishDoodle();
                     break;
             }
         }
diff --git a/src/com/android/gallery3d/photoeditor/actions/FaceTanAction.java b/src/com/android/gallery3d/photoeditor/actions/FaceTanAction.java
new file mode 100644
index 0000000..a82f330
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/FaceTanAction.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.FaceTanFilter;
+
+/**
+ * An action handling face tanning effect.
+ */
+public class FaceTanAction extends EffectAction {
+
+    private static final float DEFAULT_SCALE = 0.5f;
+
+    private ScaleSeekBar scalePicker;
+
+    public FaceTanAction(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public void doBegin() {
+        final FaceTanFilter filter = new FaceTanFilter();
+
+        scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.GENERIC);
+        scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
+
+            @Override
+            public void onProgressChanged(float progress, boolean fromUser) {
+                if (fromUser) {
+                    filter.setScale(progress);
+                    notifyFilterChanged(filter, true);
+                }
+            }
+        });
+        scalePicker.setProgress(DEFAULT_SCALE);
+
+        filter.setScale(DEFAULT_SCALE);
+        notifyFilterChanged(filter, true);
+    }
+
+    @Override
+    public void doEnd() {
+        scalePicker.setOnScaleChangeListener(null);
+    }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/FillLightAction.java b/src/com/android/gallery3d/photoeditor/actions/FillLightAction.java
index 962486b..73cf3d8 100644
--- a/src/com/android/gallery3d/photoeditor/actions/FillLightAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/FillLightAction.java
@@ -44,7 +44,7 @@
             @Override
             public void onProgressChanged(float progress, boolean fromUser) {
                 if (fromUser) {
-                    filter.setBacklight(progress);
+                    filter.setScale(progress);
                     notifyFilterChanged(filter, true);
                 }
             }
diff --git a/src/com/android/gallery3d/photoeditor/actions/HighlightAction.java b/src/com/android/gallery3d/photoeditor/actions/HighlightAction.java
index cd8f4b2..a3d62d2 100644
--- a/src/com/android/gallery3d/photoeditor/actions/HighlightAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/HighlightAction.java
@@ -44,7 +44,7 @@
             @Override
             public void onProgressChanged(float progress, boolean fromUser) {
                 if (fromUser) {
-                    filter.setHighlight(progress);
+                    filter.setScale(progress);
                     notifyFilterChanged(filter, true);
                 }
             }
diff --git a/src/com/android/gallery3d/photoeditor/actions/SaturationAction.java b/src/com/android/gallery3d/photoeditor/actions/SaturationAction.java
index 31bcfd6..2f67e0a 100644
--- a/src/com/android/gallery3d/photoeditor/actions/SaturationAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/SaturationAction.java
@@ -44,7 +44,7 @@
             @Override
             public void onProgressChanged(float progress, boolean fromUser) {
                 if (fromUser) {
-                    filter.setSaturation(progress);
+                    filter.setScale(progress);
                     notifyFilterChanged(filter, true);
                 }
             }
diff --git a/src/com/android/gallery3d/photoeditor/actions/ShadowAction.java b/src/com/android/gallery3d/photoeditor/actions/ShadowAction.java
index 185febc..15ba850 100644
--- a/src/com/android/gallery3d/photoeditor/actions/ShadowAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/ShadowAction.java
@@ -44,7 +44,7 @@
             @Override
             public void onProgressChanged(float progress, boolean fromUser) {
                 if (fromUser) {
-                    filter.setShadow(progress);
+                    filter.setScale(progress);
                     notifyFilterChanged(filter, true);
                 }
             }
diff --git a/src/com/android/gallery3d/photoeditor/actions/SharpenAction.java b/src/com/android/gallery3d/photoeditor/actions/SharpenAction.java
index 7524c76..c6b240b 100644
--- a/src/com/android/gallery3d/photoeditor/actions/SharpenAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/SharpenAction.java
@@ -44,14 +44,14 @@
             @Override
             public void onProgressChanged(float progress, boolean fromUser) {
                 if (fromUser) {
-                    filter.setSharpen(progress);
+                    filter.setScale(progress);
                     notifyFilterChanged(filter, true);
                 }
             }
         });
         scalePicker.setProgress(DEFAULT_SCALE);
 
-        filter.setSharpen(DEFAULT_SCALE);
+        filter.setScale(DEFAULT_SCALE);
         notifyFilterChanged(filter, true);
     }
 
diff --git a/src/com/android/gallery3d/photoeditor/filters/AbstractScaleFilter.java b/src/com/android/gallery3d/photoeditor/filters/AbstractScaleFilter.java
new file mode 100644
index 0000000..727a98c
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/AbstractScaleFilter.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.os.Parcel;
+
+/**
+ * Filter base that has a scale value ranging from 0 to 1 for adjustments and can persist states.
+ */
+public abstract class AbstractScaleFilter extends Filter {
+
+    protected float scale;
+
+    /**
+     * Sets the scale from 0 to 1.
+     */
+    public void setScale(float scale) {
+        this.scale = scale;
+        validate();
+    }
+
+    @Override
+    protected void writeToParcel(Parcel out) {
+        out.writeFloat(scale);
+    }
+
+    @Override
+    protected void readFromParcel(Parcel in) {
+        scale = in.readFloat();
+    }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/AutoFixFilter.java b/src/com/android/gallery3d/photoeditor/filters/AutoFixFilter.java
index 78153d0..d168a78 100644
--- a/src/com/android/gallery3d/photoeditor/filters/AutoFixFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/AutoFixFilter.java
@@ -24,19 +24,9 @@
 /**
  * Auto-fix filter applied to the image.
  */
-public class AutoFixFilter extends Filter {
+public class AutoFixFilter extends AbstractScaleFilter {
 
-    private float scale;
-
-    /**
-     * Sets the auto-fix level.
-     *
-     * @param scale ranges from 0 to 1.
-     */
-    public void setScale(float scale) {
-        this.scale = scale;
-        validate();
-    }
+    public static final Creator<AutoFixFilter> CREATOR = creatorOf(AutoFixFilter.class);
 
     @Override
     public void process(Photo src, Photo dst) {
diff --git a/src/com/android/gallery3d/photoeditor/filters/ColorTemperatureFilter.java b/src/com/android/gallery3d/photoeditor/filters/ColorTemperatureFilter.java
index f9c6400..c5a6a35 100644
--- a/src/com/android/gallery3d/photoeditor/filters/ColorTemperatureFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/ColorTemperatureFilter.java
@@ -24,19 +24,10 @@
 /**
  * Color temperature filter applied to the image.
  */
-public class ColorTemperatureFilter extends Filter {
+public class ColorTemperatureFilter extends AbstractScaleFilter {
 
-    private float scale;
-
-    /**
-     * Sets the color temperature level.
-     *
-     * @param scale ranges from 0 to 1.
-     */
-    public void setColorTemperature(float scale) {
-        this.scale = scale;
-        validate();
-    }
+    public static final Creator<ColorTemperatureFilter> CREATOR = creatorOf(
+            ColorTemperatureFilter.class);
 
     @Override
     public void process(Photo src, Photo dst) {
diff --git a/src/com/android/gallery3d/photoeditor/filters/CropFilter.java b/src/com/android/gallery3d/photoeditor/filters/CropFilter.java
index f984f3b..ccca813 100644
--- a/src/com/android/gallery3d/photoeditor/filters/CropFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/CropFilter.java
@@ -19,6 +19,7 @@
 import android.graphics.RectF;
 import android.media.effect.Effect;
 import android.media.effect.EffectFactory;
+import android.os.Parcel;
 
 import com.android.gallery3d.photoeditor.Photo;
 
@@ -27,6 +28,8 @@
  */
 public class CropFilter extends Filter {
 
+    public static final Creator<CropFilter> CREATOR = creatorOf(CropFilter.class);
+
     private RectF bounds;
 
     /**
@@ -49,4 +52,14 @@
         effect.setParameter("height", dst.height());
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
+
+    @Override
+    protected void writeToParcel(Parcel out) {
+        out.writeParcelable(bounds, 0);
+    }
+
+    @Override
+    protected void readFromParcel(Parcel in) {
+        bounds = in.readParcelable(null);
+    }
 }
diff --git a/src/com/android/gallery3d/photoeditor/filters/CrossProcessFilter.java b/src/com/android/gallery3d/photoeditor/filters/CrossProcessFilter.java
index bea8a27..e82a667 100644
--- a/src/com/android/gallery3d/photoeditor/filters/CrossProcessFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/CrossProcessFilter.java
@@ -25,6 +25,8 @@
  */
 public class CrossProcessFilter extends Filter {
 
+    public static final Creator<CrossProcessFilter> CREATOR = creatorOf(CrossProcessFilter.class);
+
     public CrossProcessFilter() {
         validate();
     }
diff --git a/src/com/android/gallery3d/photoeditor/filters/DocumentaryFilter.java b/src/com/android/gallery3d/photoeditor/filters/DocumentaryFilter.java
index 4075b27..d6f347b 100644
--- a/src/com/android/gallery3d/photoeditor/filters/DocumentaryFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/DocumentaryFilter.java
@@ -25,6 +25,8 @@
  */
 public class DocumentaryFilter extends Filter {
 
+    public static final Creator<DocumentaryFilter> CREATOR = creatorOf(DocumentaryFilter.class);
+
     public DocumentaryFilter() {
         validate();
     }
diff --git a/src/com/android/gallery3d/photoeditor/filters/DoodleFilter.java b/src/com/android/gallery3d/photoeditor/filters/DoodleFilter.java
index d9e904a..277e06d 100644
--- a/src/com/android/gallery3d/photoeditor/filters/DoodleFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/DoodleFilter.java
@@ -24,9 +24,10 @@
 import android.graphics.RectF;
 import android.media.effect.Effect;
 import android.media.effect.EffectFactory;
+import android.os.Parcel;
 
 import com.android.gallery3d.photoeditor.Photo;
-import com.android.gallery3d.photoeditor.actions.DoodlePaint;
+import com.android.gallery3d.photoeditor.actions.Doodle;
 
 import java.util.Vector;
 
@@ -35,17 +36,9 @@
  */
 public class DoodleFilter extends Filter {
 
-    private static class ColorPath {
-        private final int color;
-        private final Path path;
+    public static final Creator<DoodleFilter> CREATOR = creatorOf(DoodleFilter.class);
 
-        ColorPath(int color, Path path) {
-            this.color = color;
-            this.path = path;
-        }
-    }
-
-    private final Vector<ColorPath> doodles = new Vector<ColorPath>();
+    private final Vector<Doodle> doodles = new Vector<Doodle>();
 
     /**
      * Signals once at least a doodle drawn within photo bounds; this filter is regarded as invalid
@@ -55,11 +48,8 @@
         validate();
     }
 
-    /**
-     * The path coordinates used here should range from 0 to 1.
-     */
-    public void addPath(Path path, int color) {
-        doodles.add(new ColorPath(color, path));
+    public void addDoodle(Doodle doodle) {
+        doodles.add(doodle);
     }
 
     @Override
@@ -72,11 +62,10 @@
                 new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()), Matrix.ScaleToFit.FILL);
 
         Path drawingPath = new Path();
-        Paint paint = new DoodlePaint();
-        for (ColorPath doodle : doodles) {
-            paint.setColor(doodle.color);
-            drawingPath.set(doodle.path);
-            drawingPath.transform(matrix);
+        Paint paint = Doodle.createPaint();
+        for (Doodle doodle : doodles) {
+            paint.setColor(doodle.getColor());
+            doodle.getDrawingPath(matrix, drawingPath);
             canvas.drawPath(drawingPath, paint);
         }
 
@@ -84,4 +73,20 @@
         effect.setParameter("bitmap", bitmap);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
+
+    @Override
+    protected void writeToParcel(Parcel out) {
+        out.writeInt(doodles.size());
+        for (Doodle doodle : doodles) {
+            out.writeParcelable(doodle, 0);
+        }
+    }
+
+    @Override
+    protected void readFromParcel(Parcel in) {
+        int size = in.readInt();
+        for (int i = 0; i < size; i++) {
+            doodles.add((Doodle) in.readParcelable(Doodle.class.getClassLoader()));
+        }
+    }
 }
diff --git a/src/com/android/gallery3d/photoeditor/filters/DuotoneFilter.java b/src/com/android/gallery3d/photoeditor/filters/DuotoneFilter.java
index 89e95b9..b94f95e 100644
--- a/src/com/android/gallery3d/photoeditor/filters/DuotoneFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/DuotoneFilter.java
@@ -18,6 +18,7 @@
 
 import android.media.effect.Effect;
 import android.media.effect.EffectFactory;
+import android.os.Parcel;
 
 import com.android.gallery3d.photoeditor.Photo;
 
@@ -26,6 +27,8 @@
  */
 public class DuotoneFilter extends Filter {
 
+    public static final Creator<DuotoneFilter> CREATOR = creatorOf(DuotoneFilter.class);
+
     private int firstColor;
     private int secondColor;
 
@@ -42,4 +45,16 @@
         effect.setParameter("second_color", secondColor);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
+
+    @Override
+    protected void writeToParcel(Parcel out) {
+        out.writeInt(firstColor);
+        out.writeInt(secondColor);
+    }
+
+    @Override
+    protected void readFromParcel(Parcel in) {
+        firstColor = in.readInt();
+        secondColor = in.readInt();
+    }
 }
diff --git a/src/com/android/gallery3d/photoeditor/filters/FaceTanFilter.java b/src/com/android/gallery3d/photoeditor/filters/FaceTanFilter.java
new file mode 100644
index 0000000..c52bb88
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/FaceTanFilter.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.Effect;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Face tanning filter applied to the image.
+ */
+public class FaceTanFilter extends AbstractScaleFilter {
+
+    public static final Creator<FaceTanFilter> CREATOR = creatorOf(FaceTanFilter.class);
+
+    @Override
+    public void process(Photo src, Photo dst) {
+        Effect effect = getEffect("com.google.android.media.effect.effects.FaceTanningEffect");
+        effect.setParameter("blend", scale);
+        effect.apply(src.texture(), src.width(), src.height(), dst.texture());
+    }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/FaceliftFilter.java b/src/com/android/gallery3d/photoeditor/filters/FaceliftFilter.java
index 3c7a731..c6ad84b 100644
--- a/src/com/android/gallery3d/photoeditor/filters/FaceliftFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/FaceliftFilter.java
@@ -23,19 +23,9 @@
 /**
  * Facelift filter applied to the image.
  */
-public class FaceliftFilter extends Filter {
+public class FaceliftFilter extends AbstractScaleFilter {
 
-    private float scale;
-
-    /**
-     * Sets the facelift level.
-     *
-     * @param scale ranges from 0 to 1.
-     */
-    public void setScale(float scale) {
-        this.scale = scale;
-        validate();
-    }
+    public static final Creator<FaceliftFilter> CREATOR = creatorOf(FaceliftFilter.class);
 
     @Override
     public void process(Photo src, Photo dst) {
diff --git a/src/com/android/gallery3d/photoeditor/filters/FillLightFilter.java b/src/com/android/gallery3d/photoeditor/filters/FillLightFilter.java
index 2346953..3aedd74 100644
--- a/src/com/android/gallery3d/photoeditor/filters/FillLightFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/FillLightFilter.java
@@ -24,24 +24,14 @@
 /**
  * Fill-light filter applied to the image.
  */
-public class FillLightFilter extends Filter {
+public class FillLightFilter extends AbstractScaleFilter {
 
-    private float backlight;
-
-    /**
-     * Sets the backlight level.
-     *
-     * @param backlight ranges from 0 to 1.
-     */
-    public void setBacklight(float backlight) {
-        this.backlight = backlight;
-        validate();
-    }
+    public static final Creator<FillLightFilter> CREATOR = creatorOf(FillLightFilter.class);
 
     @Override
     public void process(Photo src, Photo dst) {
         Effect effect = getEffect(EffectFactory.EFFECT_FILLLIGHT);
-        effect.setParameter("strength", backlight);
+        effect.setParameter("strength", scale);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/filters/Filter.java b/src/com/android/gallery3d/photoeditor/filters/Filter.java
index 8c00dbb..baa3747 100644
--- a/src/com/android/gallery3d/photoeditor/filters/Filter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/Filter.java
@@ -18,6 +18,8 @@
 
 import android.media.effect.Effect;
 import android.media.effect.EffectContext;
+import android.os.Parcel;
+import android.os.Parcelable;
 
 import com.android.gallery3d.photoeditor.Photo;
 
@@ -27,7 +29,7 @@
  * Image filter for photo editing; most of its methods must be called from a single GL thread except
  * validate()/isValid() that are called from UI thread.
  */
-public abstract class Filter {
+public abstract class Filter implements Parcelable {
 
     // TODO: This should be set in MFF instead.
     private static final int DEFAULT_TILE_SIZE = 640;
@@ -91,4 +93,33 @@
      * @param dst destination photo having the same dimension as source photo as the output.
      */
     public abstract void process(Photo src, Photo dst);
+
+    /**
+     * Instantiates CREATOR of subclasses for Parcelable implementations.
+     */
+    protected static <T extends Filter> Parcelable.Creator<T> creatorOf(Class<T> filterClass) {
+        return new FilterCreator<T>(filterClass);
+    }
+
+    /**
+     * Saves states for restoring filter later; subclasses can override this to persist states.
+     */
+    protected void writeToParcel(Parcel out) {
+    }
+
+    /**
+     * Restores filter from the saved states; subclasses can override this to persist states.
+     */
+    protected void readFromParcel(Parcel in) {
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        writeToParcel(dest);
+    }
 }
diff --git a/src/com/android/gallery3d/photoeditor/filters/FilterCreator.java b/src/com/android/gallery3d/photoeditor/filters/FilterCreator.java
new file mode 100644
index 0000000..9b05244
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/FilterCreator.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.reflect.Array;
+
+/**
+ * Creator that creates the specific parcelable filter from the parcel.
+ */
+public class FilterCreator<T extends Filter> implements Parcelable.Creator<T> {
+
+    private final Class<T> filterClass;
+
+    public FilterCreator(Class<T> filterClass) {
+        this.filterClass = filterClass;
+    }
+
+    @Override
+    public T createFromParcel(Parcel source) {
+        try {
+            T filter = filterClass.newInstance();
+            filter.readFromParcel(source);
+            return filter;
+        } catch (InstantiationException e) {
+            throw new RuntimeException(e);
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public T[] newArray(int size) {
+        return (T[]) Array.newInstance(filterClass, size);
+    }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/FisheyeFilter.java b/src/com/android/gallery3d/photoeditor/filters/FisheyeFilter.java
index 6bd406c..7fe6108 100644
--- a/src/com/android/gallery3d/photoeditor/filters/FisheyeFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/FisheyeFilter.java
@@ -24,19 +24,9 @@
 /**
  * Fisheye filter applied to the image.
  */
-public class FisheyeFilter extends Filter {
+public class FisheyeFilter extends AbstractScaleFilter {
 
-    private float scale;
-
-    /**
-     * Sets the fisheye distortion level.
-     *
-     * @param scale ranges from 0 to 1.
-     */
-    public void setScale(float scale) {
-        this.scale = scale;
-        validate();
-    }
+    public static final Creator<FisheyeFilter> CREATOR = creatorOf(FisheyeFilter.class);
 
     @Override
     public void process(Photo src, Photo dst) {
diff --git a/src/com/android/gallery3d/photoeditor/filters/FlipFilter.java b/src/com/android/gallery3d/photoeditor/filters/FlipFilter.java
index 7035912..816aad8 100644
--- a/src/com/android/gallery3d/photoeditor/filters/FlipFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/FlipFilter.java
@@ -18,6 +18,7 @@
 
 import android.media.effect.Effect;
 import android.media.effect.EffectFactory;
+import android.os.Parcel;
 
 import com.android.gallery3d.photoeditor.Photo;
 
@@ -26,20 +27,31 @@
  */
 public class FlipFilter extends Filter {
 
-    private boolean flipHorizontal;
-    private boolean flipVertical;
+    public static final Creator<FlipFilter> CREATOR = creatorOf(FlipFilter.class);
+
+    private final boolean[] flips = new boolean[2];
 
     public void setFlip(boolean flipHorizontal, boolean flipVertical) {
-        this.flipHorizontal = flipHorizontal;
-        this.flipVertical = flipVertical;
+        flips[0] = flipHorizontal;
+        flips[1] = flipVertical;
         validate();
     }
 
     @Override
     public void process(Photo src, Photo dst) {
         Effect effect = getEffect(EffectFactory.EFFECT_FLIP);
-        effect.setParameter("horizontal", flipHorizontal);
-        effect.setParameter("vertical", flipVertical);
+        effect.setParameter("horizontal", flips[0]);
+        effect.setParameter("vertical", flips[1]);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
+
+    @Override
+    protected void writeToParcel(Parcel out) {
+        out.writeBooleanArray(flips);
+    }
+
+    @Override
+    protected void readFromParcel(Parcel in) {
+        in.readBooleanArray(flips);
+    }
 }
diff --git a/src/com/android/gallery3d/photoeditor/filters/GrainFilter.java b/src/com/android/gallery3d/photoeditor/filters/GrainFilter.java
index ddaad7a..04867c6 100644
--- a/src/com/android/gallery3d/photoeditor/filters/GrainFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/GrainFilter.java
@@ -24,19 +24,9 @@
 /**
  * Film grain filter applied to the image.
  */
-public class GrainFilter extends Filter {
+public class GrainFilter extends AbstractScaleFilter {
 
-    private float scale;
-
-    /**
-     * Set the grain noise level.
-     *
-     * @param scale ranges from 0 to 1.
-     */
-    public void setScale(float scale) {
-        this.scale = scale;
-        validate();
-    }
+    public static final Creator<GrainFilter> CREATOR = creatorOf(GrainFilter.class);
 
     @Override
     public void process(Photo src, Photo dst) {
diff --git a/src/com/android/gallery3d/photoeditor/filters/GrayscaleFilter.java b/src/com/android/gallery3d/photoeditor/filters/GrayscaleFilter.java
index d5ef8a0..38dfb52 100644
--- a/src/com/android/gallery3d/photoeditor/filters/GrayscaleFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/GrayscaleFilter.java
@@ -25,6 +25,8 @@
  */
 public class GrayscaleFilter extends Filter {
 
+    public static final Creator<GrayscaleFilter> CREATOR = creatorOf(GrayscaleFilter.class);
+
     public GrayscaleFilter() {
         validate();
     }
diff --git a/src/com/android/gallery3d/photoeditor/filters/HighlightFilter.java b/src/com/android/gallery3d/photoeditor/filters/HighlightFilter.java
index dfaaa65..e079c2e 100644
--- a/src/com/android/gallery3d/photoeditor/filters/HighlightFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/HighlightFilter.java
@@ -24,25 +24,15 @@
 /**
  * Highlight filter applied to the image.
  */
-public class HighlightFilter extends Filter {
+public class HighlightFilter extends AbstractScaleFilter {
 
-    private float white;
-
-    /**
-     * Sets the highlight level.
-     *
-     * @param highlight ranges from 0 to 1.
-     */
-    public void setHighlight(float highlight) {
-        white = 1f - highlight * 0.5f;
-        validate();
-    }
+    public static final Creator<HighlightFilter> CREATOR = creatorOf(HighlightFilter.class);
 
     @Override
     public void process(Photo src, Photo dst) {
         Effect effect = getEffect(EffectFactory.EFFECT_BLACKWHITE);
         effect.setParameter("black", 0f);
-        effect.setParameter("white", white);
+        effect.setParameter("white", 1f - scale * 0.5f);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/filters/LomoishFilter.java b/src/com/android/gallery3d/photoeditor/filters/LomoishFilter.java
index 140f2d6..f8c5173 100644
--- a/src/com/android/gallery3d/photoeditor/filters/LomoishFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/LomoishFilter.java
@@ -25,6 +25,8 @@
  */
 public class LomoishFilter extends Filter {
 
+    public static final Creator<LomoishFilter> CREATOR = creatorOf(LomoishFilter.class);
+
     public LomoishFilter() {
         validate();
     }
diff --git a/src/com/android/gallery3d/photoeditor/filters/NegativeFilter.java b/src/com/android/gallery3d/photoeditor/filters/NegativeFilter.java
index 94bf87e..88bbd58 100644
--- a/src/com/android/gallery3d/photoeditor/filters/NegativeFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/NegativeFilter.java
@@ -25,6 +25,8 @@
  */
 public class NegativeFilter extends Filter {
 
+    public static final Creator<NegativeFilter> CREATOR = creatorOf(NegativeFilter.class);
+
     public NegativeFilter() {
         validate();
     }
diff --git a/src/com/android/gallery3d/photoeditor/filters/PosterizeFilter.java b/src/com/android/gallery3d/photoeditor/filters/PosterizeFilter.java
index 96f5985..186baa9 100644
--- a/src/com/android/gallery3d/photoeditor/filters/PosterizeFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/PosterizeFilter.java
@@ -25,6 +25,8 @@
  */
 public class PosterizeFilter extends Filter {
 
+    public static final Creator<PosterizeFilter> CREATOR = creatorOf(PosterizeFilter.class);
+
     public PosterizeFilter() {
         validate();
     }
diff --git a/src/com/android/gallery3d/photoeditor/filters/RedEyeFilter.java b/src/com/android/gallery3d/photoeditor/filters/RedEyeFilter.java
index b499154..257d322 100644
--- a/src/com/android/gallery3d/photoeditor/filters/RedEyeFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/RedEyeFilter.java
@@ -19,6 +19,7 @@
 import android.graphics.PointF;
 import android.media.effect.Effect;
 import android.media.effect.EffectFactory;
+import android.os.Parcel;
 
 import com.android.gallery3d.photoeditor.Photo;
 
@@ -29,6 +30,8 @@
  */
 public class RedEyeFilter extends Filter {
 
+    public static final Creator<RedEyeFilter> CREATOR = creatorOf(RedEyeFilter.class);
+
     private final Vector<PointF> redeyes = new Vector<PointF>();
 
     /**
@@ -51,4 +54,20 @@
         effect.setParameter("centers", centers);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
+
+    @Override
+    protected void writeToParcel(Parcel out) {
+        out.writeInt(redeyes.size());
+        for (PointF eye : redeyes) {
+            out.writeParcelable(eye, 0);
+        }
+    }
+
+    @Override
+    protected void readFromParcel(Parcel in) {
+        int size = in.readInt();
+        for (int i = 0; i < size; i++) {
+            redeyes.add((PointF) in.readParcelable(null));
+        }
+    }
 }
diff --git a/src/com/android/gallery3d/photoeditor/filters/RotateFilter.java b/src/com/android/gallery3d/photoeditor/filters/RotateFilter.java
index 548cc59..d377f96 100644
--- a/src/com/android/gallery3d/photoeditor/filters/RotateFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/RotateFilter.java
@@ -18,6 +18,7 @@
 
 import android.media.effect.Effect;
 import android.media.effect.EffectFactory;
+import android.os.Parcel;
 
 import com.android.gallery3d.photoeditor.Photo;
 
@@ -26,6 +27,8 @@
  */
 public class RotateFilter extends Filter {
 
+    public static final Creator<RotateFilter> CREATOR = creatorOf(RotateFilter.class);
+
     private float degrees;
 
     public void setAngle(float degrees) {
@@ -42,4 +45,14 @@
         effect.setParameter("angle", (int) degrees);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
+
+    @Override
+    protected void writeToParcel(Parcel out) {
+        out.writeFloat(degrees);
+    }
+
+    @Override
+    protected void readFromParcel(Parcel in) {
+        degrees = in.readFloat();
+    }
 }
diff --git a/src/com/android/gallery3d/photoeditor/filters/SaturationFilter.java b/src/com/android/gallery3d/photoeditor/filters/SaturationFilter.java
index b2c7cce..af08a7b 100644
--- a/src/com/android/gallery3d/photoeditor/filters/SaturationFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/SaturationFilter.java
@@ -24,24 +24,14 @@
 /**
  * Saturation filter applied to the image.
  */
-public class SaturationFilter extends Filter {
+public class SaturationFilter extends AbstractScaleFilter {
 
-    private float scale;
-
-    /**
-     * Sets the saturation level.
-     *
-     * @param scale ranges from 0 to 1.
-     */
-    public void setSaturation(float scale) {
-        this.scale = (scale - 0.5f) * 2;
-        validate();
-    }
+    public static final Creator<SaturationFilter> CREATOR = creatorOf(SaturationFilter.class);
 
     @Override
     public void process(Photo src, Photo dst) {
         Effect effect = getEffect(EffectFactory.EFFECT_SATURATE);
-        effect.setParameter("scale", scale);
+        effect.setParameter("scale", (scale - 0.5f) * 2);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/filters/SepiaFilter.java b/src/com/android/gallery3d/photoeditor/filters/SepiaFilter.java
index 170b95d..6c1a70e 100644
--- a/src/com/android/gallery3d/photoeditor/filters/SepiaFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/SepiaFilter.java
@@ -25,6 +25,8 @@
  */
 public class SepiaFilter extends Filter {
 
+    public static final Creator<SepiaFilter> CREATOR = creatorOf(SepiaFilter.class);
+
     public SepiaFilter() {
         validate();
     }
diff --git a/src/com/android/gallery3d/photoeditor/filters/ShadowFilter.java b/src/com/android/gallery3d/photoeditor/filters/ShadowFilter.java
index 7931874..03960e4 100644
--- a/src/com/android/gallery3d/photoeditor/filters/ShadowFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/ShadowFilter.java
@@ -24,24 +24,14 @@
 /**
  * Shadow filter applied to the image.
  */
-public class ShadowFilter extends Filter {
+public class ShadowFilter extends AbstractScaleFilter {
 
-    private float black;
-
-    /**
-     * Sets the shadow blackness level.
-     *
-     * @param shadow ranges from 0 to 1.
-     */
-    public void setShadow(float shadow) {
-        black = shadow * 0.5f;
-        validate();
-    }
+    public static final Creator<ShadowFilter> CREATOR = creatorOf(ShadowFilter.class);
 
     @Override
     public void process(Photo src, Photo dst) {
         Effect effect = getEffect(EffectFactory.EFFECT_BLACKWHITE);
-        effect.setParameter("black", black);
+        effect.setParameter("black", scale * 0.5f);
         effect.setParameter("white", 1f);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
diff --git a/src/com/android/gallery3d/photoeditor/filters/SharpenFilter.java b/src/com/android/gallery3d/photoeditor/filters/SharpenFilter.java
index e6f7cd5..f066dcf 100644
--- a/src/com/android/gallery3d/photoeditor/filters/SharpenFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/SharpenFilter.java
@@ -24,19 +24,9 @@
 /**
  * Sharpen filter applied to the image.
  */
-public class SharpenFilter extends Filter {
+public class SharpenFilter extends AbstractScaleFilter {
 
-    private float scale;
-
-    /**
-     * Sets the sharpen level.
-     *
-     * @param scale ranges from 0 to 1.
-     */
-    public void setSharpen(float scale) {
-        this.scale = scale;
-        validate();
-    }
+    public static final Creator<SharpenFilter> CREATOR = creatorOf(SharpenFilter.class);
 
     @Override
     public void process(Photo src, Photo dst) {
diff --git a/src/com/android/gallery3d/photoeditor/filters/StraightenFilter.java b/src/com/android/gallery3d/photoeditor/filters/StraightenFilter.java
index 30a8ac5..90738f0 100644
--- a/src/com/android/gallery3d/photoeditor/filters/StraightenFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/StraightenFilter.java
@@ -18,6 +18,7 @@
 
 import android.media.effect.Effect;
 import android.media.effect.EffectFactory;
+import android.os.Parcel;
 
 import com.android.gallery3d.photoeditor.Photo;
 
@@ -26,12 +27,13 @@
  */
 public class StraightenFilter extends Filter {
 
+    public static final Creator<StraightenFilter> CREATOR = creatorOf(StraightenFilter.class);
     public static final float MAX_DEGREES = 30.0f;
 
-    private float angle;
+    private float degrees;
 
     public void setAngle(float degrees) {
-        angle = -degrees;
+        this.degrees = degrees;
         validate();
     }
 
@@ -39,7 +41,17 @@
     public void process(Photo src, Photo dst) {
         Effect effect = getEffect(EffectFactory.EFFECT_STRAIGHTEN);
         effect.setParameter("maxAngle", MAX_DEGREES);
-        effect.setParameter("angle", angle);
+        effect.setParameter("angle", -degrees);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
+
+    @Override
+    protected void writeToParcel(Parcel out) {
+        out.writeFloat(degrees);
+    }
+
+    @Override
+    protected void readFromParcel(Parcel in) {
+        degrees = in.readFloat();
+    }
 }
diff --git a/src/com/android/gallery3d/photoeditor/filters/TintFilter.java b/src/com/android/gallery3d/photoeditor/filters/TintFilter.java
index e5e467b..af3d777 100644
--- a/src/com/android/gallery3d/photoeditor/filters/TintFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/TintFilter.java
@@ -18,6 +18,7 @@
 
 import android.media.effect.Effect;
 import android.media.effect.EffectFactory;
+import android.os.Parcel;
 
 import com.android.gallery3d.photoeditor.Photo;
 
@@ -26,17 +27,29 @@
  */
 public class TintFilter extends Filter {
 
-    private int tint;
+    public static final Creator<TintFilter> CREATOR = creatorOf(TintFilter.class);
+
+    private int color;
 
     public void setTint(int color) {
-        tint = color;
+        this.color = color;
         validate();
     }
 
     @Override
     public void process(Photo src, Photo dst) {
         Effect effect = getEffect(EffectFactory.EFFECT_TINT);
-        effect.setParameter("tint", tint);
+        effect.setParameter("tint", color);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
+
+    @Override
+    protected void writeToParcel(Parcel out) {
+        out.writeInt(color);
+   }
+
+    @Override
+    protected void readFromParcel(Parcel in) {
+        color = in.readInt();
+    }
 }
diff --git a/src/com/android/gallery3d/photoeditor/filters/VignetteFilter.java b/src/com/android/gallery3d/photoeditor/filters/VignetteFilter.java
index ec39393..5903461 100644
--- a/src/com/android/gallery3d/photoeditor/filters/VignetteFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/VignetteFilter.java
@@ -24,19 +24,9 @@
 /**
  * Vignette filter applied to the image.
  */
-public class VignetteFilter extends Filter {
+public class VignetteFilter extends AbstractScaleFilter {
 
-    private float scale;
-
-    /**
-     * Sets the vignette range scale.
-     *
-     * @param scale ranges from 0 to 1.
-     */
-    public void setScale(float scale) {
-        this.scale = scale;
-        validate();
-    }
+    public static final Creator<VignetteFilter> CREATOR = creatorOf(VignetteFilter.class);
 
     @Override
     public void process(Photo src, Photo dst) {
diff --git a/src/com/android/gallery3d/util/SpinnerVisibilitySetter.java b/src/com/android/gallery3d/util/SpinnerVisibilitySetter.java
index 75b1fa4..6ccc264 100644
--- a/src/com/android/gallery3d/util/SpinnerVisibilitySetter.java
+++ b/src/com/android/gallery3d/util/SpinnerVisibilitySetter.java
@@ -21,8 +21,7 @@
 import android.os.Message;
 import android.os.SystemClock;
 
-import java.util.HashMap;
-import java.util.Map;
+import java.util.WeakHashMap;
 
 /**
  * This class manages the visibility of the progress spinner in the action bar for an
@@ -33,10 +32,8 @@
  */
 public class SpinnerVisibilitySetter {
 
-    private static final int SHOW_SPINNER_REQUESTED = 0;
-    private static final int HIDE_SPINNER_REQUESTED = 1;
-    private static final int SHOW_SPINNER_DELAY_REACHED = 2;
-    private static final int HIDE_SPINNER_DELAY_REACHED = 3;
+    private static final int MSG_SHOW_SPINNER = 1;
+    private static final int MSG_HIDE_SPINNER = 2;
 
     // Amount of time after a show request that the progress spinner is actually made visible.
     // This means that any show/hide requests that happen subsequently within this period
@@ -46,88 +43,64 @@
     // The minimum amount of time the progress spinner must be visible before it can be hidden.
     private static final long MIN_SPINNER_DISPLAY_TIME = 2000;
 
-    private boolean mPendingVisibilityRequest = false;
-    private boolean mActiveVisibilityRequest = false;
-    private long mSpinnerVisibilityStartTime;
+    static final WeakHashMap<Activity, SpinnerVisibilitySetter> sInstanceMap =
+            new WeakHashMap<Activity, SpinnerVisibilitySetter>();
 
-    Handler mHandler = new Handler() {
+    private long mSpinnerVisibilityStartTime = -1;
+    private Activity mActivity;
 
+    private Handler mHandler = new Handler() {
         @Override
         public void handleMessage(Message msg) {
             switch(msg.what) {
-                case SHOW_SPINNER_REQUESTED:
-                    mPendingVisibilityRequest = true;
-                    sendEmptyMessageDelayed(SHOW_SPINNER_DELAY_REACHED, SPINNER_DISPLAY_DELAY);
+                case MSG_SHOW_SPINNER:
+                    removeMessages(MSG_SHOW_SPINNER);
+                    if (mSpinnerVisibilityStartTime >= 0) break;
+                    mSpinnerVisibilityStartTime = SystemClock.elapsedRealtime();
+                    mActivity.setProgressBarIndeterminateVisibility(true);
                     break;
-                case HIDE_SPINNER_REQUESTED:
-                    mPendingVisibilityRequest = false;
-                    if (!mActiveVisibilityRequest) {
-                        // We haven't requested to show the spinner so no need to decide
-                        // when to hide it.
-                        break;
-                    }
-
-                    long currTime = SystemClock.uptimeMillis();
-                    if (currTime - mSpinnerVisibilityStartTime > MIN_SPINNER_DISPLAY_TIME) {
-                        // The spinner has already been visible longer than the requisite min
-                        // display time. Send the hide message immediately.
-                        sendEmptyMessage(HIDE_SPINNER_DELAY_REACHED);
+                case MSG_HIDE_SPINNER:
+                    removeMessages(MSG_HIDE_SPINNER);
+                    if (mSpinnerVisibilityStartTime < 0) break;
+                    long t = SystemClock.elapsedRealtime() - mSpinnerVisibilityStartTime;
+                    if (t >= MIN_SPINNER_DISPLAY_TIME) {
+                        mSpinnerVisibilityStartTime = -1;
+                        mActivity.setProgressBarIndeterminateVisibility(false);
                     } else {
-                        // The spinner is visible but hasn't been visible for long enough yet.
-                        // Send a delayed hide message.
-                        sendEmptyMessageAtTime(HIDE_SPINNER_DELAY_REACHED,
-                                mSpinnerVisibilityStartTime + MIN_SPINNER_DISPLAY_TIME);
+                        sendEmptyMessageDelayed(MSG_HIDE_SPINNER, MIN_SPINNER_DISPLAY_TIME - t);
                     }
                     break;
-                case SHOW_SPINNER_DELAY_REACHED:
-                    if (mPendingVisibilityRequest) {
-                        mPendingVisibilityRequest = false;
-                        mActiveVisibilityRequest = true;
-
-                        // Even though the spinner isn't visible quite yet, lets set this
-                        // here to avoid possible cross-thread synchronization issues.
-                        mSpinnerVisibilityStartTime = SystemClock.uptimeMillis();
-                        mActivity.runOnUiThread(new SetProgressVisibilityRunnable(true));
-                    }
-                    break;
-                case HIDE_SPINNER_DELAY_REACHED:
-                    mActiveVisibilityRequest = false;
-                    mActivity.runOnUiThread(new SetProgressVisibilityRunnable(false));
-                    break;
             }
         }
     };
-    static final Map<Activity, SpinnerVisibilitySetter> sInstanceMap =
-            new HashMap<Activity, SpinnerVisibilitySetter>();
-    private Activity mActivity;
+
+    /**
+     *  Gets the <code>SpinnerVisibilitySetter</code> for the given <code>activity</code>.
+     *
+     *  This method must be called from the main thread.
+     */
+    public static SpinnerVisibilitySetter getInstance(Activity activity) {
+        synchronized(sInstanceMap) {
+            SpinnerVisibilitySetter setter = sInstanceMap.get(activity);
+            if (setter == null) {
+                setter = new SpinnerVisibilitySetter(activity);
+                sInstanceMap.put(activity, setter);
+            }
+            return setter;
+        }
+    }
 
     private SpinnerVisibilitySetter(Activity activity) {
         mActivity = activity;
     }
 
-    public static SpinnerVisibilitySetter getInstance(Activity activity) {
-        synchronized(sInstanceMap) {
-            if (sInstanceMap.get(activity) == null) {
-                sInstanceMap.put(activity, new SpinnerVisibilitySetter(activity));
-            }
-            return sInstanceMap.get(activity);
-        }
-    }
-
     public void setSpinnerVisibility(boolean visible) {
-        mHandler.sendEmptyMessage(visible ? SHOW_SPINNER_REQUESTED : HIDE_SPINNER_REQUESTED);
-    }
-
-    private class SetProgressVisibilityRunnable implements Runnable {
-        boolean mVisible;
-
-        public SetProgressVisibilityRunnable(boolean visible) {
-            mVisible = visible;
-        }
-
-        @Override
-        public void run() {
-            mActivity.setProgressBarIndeterminateVisibility(mVisible);
+        if (visible) {
+            mHandler.removeMessages(MSG_HIDE_SPINNER);
+            mHandler.sendEmptyMessageDelayed(MSG_SHOW_SPINNER, SPINNER_DISPLAY_DELAY);
+        } else {
+            mHandler.removeMessages(MSG_SHOW_SPINNER);
+            mHandler.sendEmptyMessage(MSG_HIDE_SPINNER);
         }
     }
 }