Merge "Add initial implementation of MediaCache." into gb-ub-photos-bryce
diff --git a/src/com/android/camera/CameraManager.java b/src/com/android/camera/CameraManager.java
index 854e105..65b2bb8 100644
--- a/src/com/android/camera/CameraManager.java
+++ b/src/com/android/camera/CameraManager.java
@@ -34,22 +34,24 @@
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Message;
-import android.view.SurfaceHolder;
 import android.util.Log;
+import android.view.SurfaceHolder;
 
 import com.android.gallery3d.common.ApiHelper;
 
 import java.io.IOException;
+import java.util.concurrent.SynchronousQueue;
 
 public class CameraManager {
     private static final String TAG = "CameraManager";
     private static CameraManager sCameraManager = new CameraManager();
 
-    // Thread progress signals
-    private ConditionVariable mSig = new ConditionVariable();
-
     private Parameters mParameters;
-    private IOException mReconnectException;
+    private boolean mParametersIsDirty;
+    private SynchronousQueue<Parameters> mParametersQueue =
+            new SynchronousQueue<Parameters>();
+    private SynchronousQueue<IOExceptionHolder> mReconnectExceptionQueue =
+            new SynchronousQueue<IOExceptionHolder>();
 
     private static final int RELEASE = 1;
     private static final int RECONNECT = 2;
@@ -72,15 +74,21 @@
     private static final int SET_PARAMETERS = 19;
     private static final int GET_PARAMETERS = 20;
     private static final int SET_PARAMETERS_ASYNC = 21;
-    private static final int WAIT_FOR_IDLE = 22;
-    private static final int SET_PREVIEW_DISPLAY_ASYNC = 23;
-    private static final int SET_PREVIEW_CALLBACK = 24;
-    private static final int ENABLE_SHUTTER_SOUND = 25;
+    private static final int SET_PREVIEW_DISPLAY_ASYNC = 22;
+    private static final int SET_PREVIEW_CALLBACK = 23;
+    private static final int ENABLE_SHUTTER_SOUND = 24;
 
     private Handler mCameraHandler;
     private CameraProxy mCameraProxy;
     private android.hardware.Camera mCamera;
 
+    // This holder is used when we need to pass the exception
+    // back to the calling thread. SynchornousQueue doesn't
+    // allow we to pass a null object thus a holder is needed.
+    private class IOExceptionHolder {
+        public IOException ex;
+    }
+
     public static CameraManager instance() {
         return sCameraManager;
     }
@@ -115,7 +123,7 @@
         private void setPreviewTexture(Object surfaceTexture) {
             try {
                 mCamera.setPreviewTexture((SurfaceTexture) surfaceTexture);
-            } catch(IOException e) {
+            } catch (IOException e) {
                 throw new RuntimeException(e);
             }
         }
@@ -137,114 +145,119 @@
                         mCamera.release();
                         mCamera = null;
                         mCameraProxy = null;
-                        break;
+                        return;
 
                     case RECONNECT:
-                        mReconnectException = null;
+                        IOExceptionHolder holder = new IOExceptionHolder();
+                        holder.ex = null;
                         try {
                             mCamera.reconnect();
                         } catch (IOException ex) {
-                            mReconnectException = ex;
+                            holder.ex = ex;
                         }
-                        break;
+                        try {
+                            mReconnectExceptionQueue.put(holder);
+                        } catch (InterruptedException ex) {
+                        }
+                        return;
 
                     case UNLOCK:
                         mCamera.unlock();
-                        break;
+                        return;
 
                     case LOCK:
                         mCamera.lock();
-                        break;
+                        return;
 
                     case SET_PREVIEW_TEXTURE_ASYNC:
                         setPreviewTexture(msg.obj);
-                        return;  // no need to call mSig.open()
+                        return;
 
                     case SET_PREVIEW_DISPLAY_ASYNC:
                         try {
                             mCamera.setPreviewDisplay((SurfaceHolder) msg.obj);
-                        } catch(IOException e) {
+                        } catch (IOException e) {
                             throw new RuntimeException(e);
                         }
-                        return;  // no need to call mSig.open()
+                        return;
 
                     case START_PREVIEW_ASYNC:
                         mCamera.startPreview();
-                        return;  // no need to call mSig.open()
+                        return;
 
                     case STOP_PREVIEW:
                         mCamera.stopPreview();
-                        break;
+                        return;
 
                     case SET_PREVIEW_CALLBACK_WITH_BUFFER:
                         mCamera.setPreviewCallbackWithBuffer(
                             (PreviewCallback) msg.obj);
-                        break;
+                        return;
 
                     case ADD_CALLBACK_BUFFER:
                         mCamera.addCallbackBuffer((byte[]) msg.obj);
-                        break;
+                        return;
 
                     case AUTO_FOCUS:
                         mCamera.autoFocus((AutoFocusCallback) msg.obj);
-                        break;
+                        return;
 
                     case CANCEL_AUTO_FOCUS:
                         mCamera.cancelAutoFocus();
-                        break;
+                        return;
 
                     case SET_AUTO_FOCUS_MOVE_CALLBACK:
                         setAutoFocusMoveCallback(mCamera, msg.obj);
-                        break;
+                        return;
 
                     case SET_DISPLAY_ORIENTATION:
                         mCamera.setDisplayOrientation(msg.arg1);
-                        break;
+                        return;
 
                     case SET_ZOOM_CHANGE_LISTENER:
                         mCamera.setZoomChangeListener(
                             (OnZoomChangeListener) msg.obj);
-                        break;
+                        return;
 
                     case SET_FACE_DETECTION_LISTENER:
                         setFaceDetectionListener((FaceDetectionListener) msg.obj);
-                        break;
+                        return;
 
                     case START_FACE_DETECTION:
                         startFaceDetection();
-                        break;
+                        return;
 
                     case STOP_FACE_DETECTION:
                         stopFaceDetection();
-                        break;
+                        return;
 
                     case SET_ERROR_CALLBACK:
                         mCamera.setErrorCallback((ErrorCallback) msg.obj);
-                        break;
+                        return;
 
                     case SET_PARAMETERS:
                         mCamera.setParameters((Parameters) msg.obj);
-                        break;
+                        return;
 
                     case GET_PARAMETERS:
-                        mParameters = mCamera.getParameters();
-                        break;
+                        try {
+                            mParametersQueue.put(mCamera.getParameters());
+                        } catch (InterruptedException e) {
+                            throw new RuntimeException(e);
+                        }
+                        return;
 
                     case SET_PARAMETERS_ASYNC:
                         mCamera.setParameters((Parameters) msg.obj);
-                        return;  // no need to call mSig.open()
+                        return;
 
                     case SET_PREVIEW_CALLBACK:
                         mCamera.setPreviewCallback((PreviewCallback) msg.obj);
-                        break;
+                        return;
 
                     case ENABLE_SHUTTER_SOUND:
                         enableShutterSound((msg.arg1 == 1) ? true : false);
-                        break;
-
-                    case WAIT_FOR_IDLE:
-                        // do nothing
-                        break;
+                        return;
 
                     default:
                         throw new RuntimeException("Invalid CameraProxy message=" + msg.what);
@@ -261,7 +274,6 @@
                 }
                 throw e;
             }
-            mSig.open();
         }
     }
 
@@ -283,6 +295,7 @@
         mCamera = android.hardware.Camera.open(cameraId);
         if (mCamera != null) {
             mCameraProxy = new CameraProxy();
+            mParametersIsDirty = true;
             return mCameraProxy;
         } else {
             return null;
@@ -290,6 +303,15 @@
     }
 
     public class CameraProxy {
+        private ConditionVariable mWaitDoneLock = new ConditionVariable();
+        private Runnable mUnlockRunnable = new Runnable() {
+            @Override
+            public void run() {
+                mWaitDoneLock.open();
+            }
+        };
+
+
         private CameraProxy() {
             Assert(mCamera != null);
         }
@@ -299,30 +321,30 @@
         }
 
         public void release() {
-            mSig.close();
+            // release() must be synchronous so we know exactly when the camera
+            // is released and can continue on.
             mCameraHandler.sendEmptyMessage(RELEASE);
-            mSig.block();
+            waitDone();
         }
 
         public void reconnect() throws IOException {
-            mSig.close();
             mCameraHandler.sendEmptyMessage(RECONNECT);
-            mSig.block();
-            if (mReconnectException != null) {
-                throw mReconnectException;
+            IOExceptionHolder holder = null;
+            try {
+                holder = mReconnectExceptionQueue.take();
+            } catch (InterruptedException ex) {
+            }
+            if (holder == null || holder.ex != null) {
+                throw holder.ex;
             }
         }
 
         public void unlock() {
-            mSig.close();
             mCameraHandler.sendEmptyMessage(UNLOCK);
-            mSig.block();
         }
 
         public void lock() {
-            mSig.close();
             mCameraHandler.sendEmptyMessage(LOCK);
-            mSig.block();
         }
 
         @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
@@ -339,66 +361,49 @@
         }
 
         public void stopPreview() {
-            mSig.close();
             mCameraHandler.sendEmptyMessage(STOP_PREVIEW);
-            mSig.block();
         }
 
         public void setPreviewCallback(final PreviewCallback cb) {
-            mSig.close();
             mCameraHandler.obtainMessage(SET_PREVIEW_CALLBACK, cb).sendToTarget();
-            mSig.block();
         }
 
         public void setPreviewCallbackWithBuffer(final PreviewCallback cb) {
-            mSig.close();
             mCameraHandler.obtainMessage(SET_PREVIEW_CALLBACK_WITH_BUFFER, cb).sendToTarget();
-            mSig.block();
         }
 
         public void addCallbackBuffer(byte[] callbackBuffer) {
-            mSig.close();
             mCameraHandler.obtainMessage(ADD_CALLBACK_BUFFER, callbackBuffer).sendToTarget();
-            mSig.block();
         }
 
         public void autoFocus(AutoFocusCallback cb) {
-            mSig.close();
             mCameraHandler.obtainMessage(AUTO_FOCUS, cb).sendToTarget();
-            mSig.block();
         }
 
         public void cancelAutoFocus() {
-            mSig.close();
+            mCameraHandler.removeMessages(AUTO_FOCUS);
             mCameraHandler.sendEmptyMessage(CANCEL_AUTO_FOCUS);
-            mSig.block();
         }
 
         @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
         public void setAutoFocusMoveCallback(AutoFocusMoveCallback cb) {
-            mSig.close();
             mCameraHandler.obtainMessage(SET_AUTO_FOCUS_MOVE_CALLBACK, cb).sendToTarget();
-            mSig.block();
         }
 
         public void takePicture(final ShutterCallback shutter, final PictureCallback raw,
                 final PictureCallback postview, final PictureCallback jpeg) {
-            mSig.close();
             // Too many parameters, so use post for simplicity
             mCameraHandler.post(new Runnable() {
                 @Override
                 public void run() {
                     mCamera.takePicture(shutter, raw, postview, jpeg);
-                    mSig.open();
                 }
             });
-            mSig.block();
         }
 
         public void takePicture2(final ShutterCallback shutter, final PictureCallback raw,
                 final PictureCallback postview, final PictureCallback jpeg,
                 final int cameraState, final int focusState) {
-            mSig.close();
             // Too many parameters, so use post for simplicity
             mCameraHandler.post(new Runnable() {
                 @Override
@@ -410,81 +415,69 @@
                             + ", focusState:" + focusState);
                         throw e;
                     }
-                    mSig.open();
                 }
             });
-            mSig.block();
         }
 
         public void setDisplayOrientation(int degrees) {
-            mSig.close();
             mCameraHandler.obtainMessage(SET_DISPLAY_ORIENTATION, degrees, 0)
                     .sendToTarget();
-            mSig.block();
         }
 
         public void setZoomChangeListener(OnZoomChangeListener listener) {
-            mSig.close();
             mCameraHandler.obtainMessage(SET_ZOOM_CHANGE_LISTENER, listener).sendToTarget();
-            mSig.block();
         }
 
         @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
         public void setFaceDetectionListener(FaceDetectionListener listener) {
-            mSig.close();
             mCameraHandler.obtainMessage(SET_FACE_DETECTION_LISTENER, listener).sendToTarget();
-            mSig.block();
         }
 
         public void startFaceDetection() {
-            mSig.close();
             mCameraHandler.sendEmptyMessage(START_FACE_DETECTION);
-            mSig.block();
         }
 
         public void stopFaceDetection() {
-            mSig.close();
             mCameraHandler.sendEmptyMessage(STOP_FACE_DETECTION);
-            mSig.block();
         }
 
         public void setErrorCallback(ErrorCallback cb) {
-            mSig.close();
             mCameraHandler.obtainMessage(SET_ERROR_CALLBACK, cb).sendToTarget();
-            mSig.block();
         }
 
         public void setParameters(Parameters params) {
-            mSig.close();
+            // TODO: check if this synchronous version is necessary
+            mParametersIsDirty = true;
             mCameraHandler.obtainMessage(SET_PARAMETERS, params).sendToTarget();
-            mSig.block();
         }
 
         public void setParametersAsync(Parameters params) {
+            mParametersIsDirty = true;
             mCameraHandler.removeMessages(SET_PARAMETERS_ASYNC);
             mCameraHandler.obtainMessage(SET_PARAMETERS_ASYNC, params).sendToTarget();
         }
 
         public Parameters getParameters() {
-            mSig.close();
-            mCameraHandler.sendEmptyMessage(GET_PARAMETERS);
-            mSig.block();
-            Parameters parameters = mParameters;
-            mParameters = null;
-            return parameters;
+            if (mParametersIsDirty || mParameters == null) {
+                mCameraHandler.sendEmptyMessage(GET_PARAMETERS);
+                try {
+                    mParameters = mParametersQueue.take();
+                    mParametersIsDirty = false;
+                } catch (InterruptedException ex) {
+                }
+            }
+            return mParameters;
         }
 
         public void enableShutterSound(boolean enable) {
-            mSig.close();
             mCameraHandler.obtainMessage(
                     ENABLE_SHUTTER_SOUND, (enable ? 1 : 0), 0).sendToTarget();
-            mSig.block();
         }
 
-        public void waitForIdle() {
-            mSig.close();
-            mCameraHandler.sendEmptyMessage(WAIT_FOR_IDLE);
-            mSig.block();
+        public void waitDone() {
+            mWaitDoneLock.close();
+            mCameraHandler.post(mUnlockRunnable);
+            mWaitDoneLock.block();
         }
     }
 }
diff --git a/src/com/android/camera/Exif.java b/src/com/android/camera/Exif.java
index ee39d67..c6ec6af 100644
--- a/src/com/android/camera/Exif.java
+++ b/src/com/android/camera/Exif.java
@@ -20,32 +20,35 @@
 
 import com.android.gallery3d.exif.ExifInterface;
 
-import java.io.ByteArrayInputStream;
 import java.io.IOException;
-import java.io.InputStream;
 
 public class Exif {
     private static final String TAG = "CameraExif";
 
-    // Returns the degrees in clockwise. Values are 0, 90, 180, or 270.
-    public static int getOrientation(byte[] jpeg) {
-        if (jpeg == null) {
-            return 0;
-        }
-
+    public static ExifInterface getExif(byte[] jpegData) {
         ExifInterface exif = new ExifInterface();
-        InputStream is = new ByteArrayInputStream(jpeg);
         try {
-            exif.readExif(is);
-            Integer val = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION);
-            if (val == null) {
-                return 0;
-            } else {
-                return ExifInterface.getRotationForOrientationValue(val.shortValue());
-            }
+            exif.readExif(jpegData);
         } catch (IOException e) {
-            Log.w(TAG, "Failed to read EXIF orientation", e);
-            return 0;
+            Log.w(TAG, "Failed to read EXIF data", e);
         }
+        return exif;
+    }
+
+    // Returns the degrees in clockwise. Values are 0, 90, 180, or 270.
+    public static int getOrientation(ExifInterface exif) {
+        Integer val = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION);
+        if (val == null) {
+            return 0;
+        } else {
+            return ExifInterface.getRotationForOrientationValue(val.shortValue());
+        }
+    }
+
+    public static int getOrientation(byte[] jpegData) {
+        if (jpegData == null) return 0;
+
+        ExifInterface exif = getExif(jpegData);
+        return getOrientation(exif);
     }
 }
diff --git a/src/com/android/camera/MediaSaveService.java b/src/com/android/camera/MediaSaveService.java
index 48fb629..b1f47df 100644
--- a/src/com/android/camera/MediaSaveService.java
+++ b/src/com/android/camera/MediaSaveService.java
@@ -26,6 +26,8 @@
 import android.os.IBinder;
 import android.util.Log;
 
+import com.android.gallery3d.exif.ExifInterface;
+
 /*
  * Service for saving images in the background thread.
  */
@@ -77,14 +79,14 @@
 
     // Runs in main thread
     public void addImage(final byte[] data, String title, long date, Location loc,
-            int width, int height, int orientation,
+            int width, int height, int orientation, ExifInterface exif,
             OnMediaSavedListener l, ContentResolver resolver) {
         if (isQueueFull()) {
             Log.e(TAG, "Cannot add image when the queue is full");
             return;
         }
         SaveTask t = new SaveTask(data, title, date, (loc == null) ? null : new Location(loc),
-                width, height, orientation, resolver, l);
+                width, height, orientation, exif, resolver, l);
 
         mTaskNumber++;
         if (isQueueFull()) {
@@ -114,12 +116,13 @@
         private Location loc;
         private int width, height;
         private int orientation;
+        private ExifInterface exif;
         private ContentResolver resolver;
         private OnMediaSavedListener listener;
 
         public SaveTask(byte[] data, String title, long date, Location loc,
-                int width, int height, int orientation, ContentResolver resolver,
-                OnMediaSavedListener listener) {
+                int width, int height, int orientation, ExifInterface exif,
+                ContentResolver resolver, OnMediaSavedListener listener) {
             this.data = data;
             this.title = title;
             this.date = date;
@@ -127,6 +130,7 @@
             this.width = width;
             this.height = height;
             this.orientation = orientation;
+            this.exif = exif;
             this.resolver = resolver;
             this.listener = listener;
         }
@@ -139,7 +143,7 @@
         @Override
         protected Uri doInBackground(Void... v) {
             return Storage.addImage(
-                    resolver, title, date, loc, orientation, data, width, height);
+                    resolver, title, date, loc, orientation, exif, data, width, height);
         }
 
         @Override
diff --git a/src/com/android/camera/PhotoModule.java b/src/com/android/camera/PhotoModule.java
index 037e0fd..f4bd4ce 100644
--- a/src/com/android/camera/PhotoModule.java
+++ b/src/com/android/camera/PhotoModule.java
@@ -21,6 +21,7 @@
 import android.app.AlertDialog;
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
+import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.SharedPreferences.Editor;
@@ -31,6 +32,10 @@
 import android.hardware.Camera.Parameters;
 import android.hardware.Camera.PictureCallback;
 import android.hardware.Camera.Size;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
 import android.location.Location;
 import android.media.CameraProfile;
 import android.net.Uri;
@@ -56,6 +61,9 @@
 import com.android.camera.ui.RotateTextToast;
 import com.android.gallery3d.R;
 import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.exif.ExifTag;
+import com.android.gallery3d.exif.Rational;
 import com.android.gallery3d.filtershow.CropExtras;
 import com.android.gallery3d.filtershow.FilterShowActivity;
 import com.android.gallery3d.util.UsageStatistics;
@@ -77,7 +85,8 @@
     CameraPreference.OnPreferenceChangedListener,
     ShutterButton.OnShutterButtonListener,
     MediaSaveService.Listener,
-    OnCountDownFinishedListener {
+    OnCountDownFinishedListener,
+    SensorEventListener {
 
     private static final String TAG = "CAM_PhotoModule";
 
@@ -164,6 +173,13 @@
         }
     };
 
+    private Runnable mFlashRunnable = new Runnable() {
+        @Override
+        public void run() {
+            animateFlash();
+        }
+    };
+
     private final StringBuilder mBuilder = new StringBuilder();
     private final Formatter mFormatter = new Formatter(mBuilder);
     private final Object[] mFormatterArgs = new Object[1];
@@ -196,7 +212,6 @@
 
     private LocationManager mLocationManager;
 
-    private final ShutterCallback mShutterCallback = new ShutterCallback();
     private final PostViewPictureCallback mPostViewPictureCallback =
             new PostViewPictureCallback();
     private final RawPictureCallback mRawPictureCallback =
@@ -239,6 +254,12 @@
     CameraStartUpThread mCameraStartUpThread;
     ConditionVariable mStartPreviewPrerequisiteReady = new ConditionVariable();
 
+    private SensorManager mSensorManager;
+    private float[] mGData = new float[3];
+    private float[] mMData = new float[3];
+    private float[] mR = new float[16];
+    private int mHeading = -1;
+
     private MediaSaveService.OnMediaSavedListener mOnMediaSavedListener =
             new MediaSaveService.OnMediaSavedListener() {
                 @Override
@@ -417,6 +438,8 @@
         initializeControlByIntent();
         mQuickCapture = mActivity.getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false);
         mLocationManager = new LocationManager(mActivity, mUI);
+        mSensorManager = (SensorManager)(mActivity.getSystemService(Context.SENSOR_SERVICE));
+
     }
 
     private void initializeControlByIntent() {
@@ -704,11 +727,21 @@
 
     private final class ShutterCallback
             implements android.hardware.Camera.ShutterCallback {
+
+        private boolean mAnimateFlash;
+
+        public ShutterCallback(boolean animateFlash) {
+            mAnimateFlash = animateFlash;
+        }
+
         @Override
         public void onShutter() {
             mShutterCallbackTime = System.currentTimeMillis();
             mShutterLag = mShutterCallbackTime - mCaptureStartTime;
             Log.v(TAG, "mShutterLag = " + mShutterLag + "ms");
+            if (mAnimateFlash) {
+                mActivity.runOnUiThread(mFlashRunnable);
+            }
         }
     }
 
@@ -792,7 +825,8 @@
             if (!mIsImageCaptureIntent) {
                 // Calculate the width and the height of the jpeg.
                 Size s = mParameters.getPictureSize();
-                int orientation = Exif.getOrientation(jpegData);
+                ExifInterface exif = Exif.getExif(jpegData);
+                int orientation = Exif.getOrientation(exif);
                 int width, height;
                 if ((mJpegRotation + orientation) % 180 == 0) {
                     width = s.width;
@@ -807,9 +841,20 @@
                     Log.e(TAG, "Unbalanced name/data pair");
                 } else {
                     if (date == -1) date = mCaptureStartTime;
+                    if (mHeading >= 0) {
+                        // heading direction has been updated by the sensor.
+                        ExifTag directionRefTag = exif.buildTag(
+                                ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
+                                ExifInterface.GpsTrackRef.MAGNETIC_DIRECTION);
+                        ExifTag directionTag = exif.buildTag(
+                                ExifInterface.TAG_GPS_IMG_DIRECTION,
+                                new Rational(mHeading, 1));
+                        exif.setTag(directionRefTag);
+                        exif.setTag(directionTag);
+                    }
                     mActivity.getMediaSaveService().addImage(
                             jpegData, title, date, mLocation, width, height,
-                            orientation, mOnMediaSavedListener, mContentResolver);
+                            orientation, exif, mOnMediaSavedListener, mContentResolver);
                 }
             } else {
                 mJpegImageData = jpegData;
@@ -959,13 +1004,10 @@
         Util.setGpsParameters(mParameters, loc);
         mCameraDevice.setParameters(mParameters);
 
-        mCameraDevice.takePicture2(mShutterCallback, mRawPictureCallback,
-                mPostViewPictureCallback, new JpegPictureCallback(loc),
-                mCameraState, mFocusManager.getFocusState());
-
-        if (!animateBefore) {
-            animateFlash();
-        }
+        mCameraDevice.takePicture2(new ShutterCallback(!animateBefore),
+                mRawPictureCallback, mPostViewPictureCallback,
+                new JpegPictureCallback(loc), mCameraState,
+                mFocusManager.getFocusState());
 
         mNamedImages.nameNewImage(mContentResolver, mCaptureStartTime);
 
@@ -1091,7 +1133,8 @@
                     Util.closeSilently(outputStream);
                 }
             } else {
-                int orientation = Exif.getOrientation(data);
+                ExifInterface exif = Exif.getExif(data);
+                int orientation = Exif.getOrientation(exif);
                 Bitmap bitmap = Util.makeBitmap(data, 50 * 1024);
                 bitmap = Util.rotate(bitmap, orientation);
                 mActivity.setResultEx(Activity.RESULT_OK,
@@ -1258,6 +1301,16 @@
         PopupManager.getInstance(mActivity).notifyShowPopup(null);
         UsageStatistics.onContentViewChanged(
                 UsageStatistics.COMPONENT_CAMERA, "PhotoModule");
+
+        Sensor gsensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+        if (gsensor != null) {
+            mSensorManager.registerListener(this, gsensor, SensorManager.SENSOR_DELAY_NORMAL);
+        }
+
+        Sensor msensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
+        if (msensor != null) {
+            mSensorManager.registerListener(this, msensor, SensorManager.SENSOR_DELAY_NORMAL);
+        }
     }
 
     void waitCameraStartUpThread() {
@@ -1276,6 +1329,15 @@
     @Override
     public void onPauseBeforeSuper() {
         mPaused = true;
+        Sensor gsensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+        if (gsensor != null) {
+            mSensorManager.unregisterListener(this, gsensor);
+        }
+
+        Sensor msensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
+        if (msensor != null) {
+            mSensorManager.unregisterListener(this, msensor);
+        }
     }
 
     @Override
@@ -1975,4 +2037,32 @@
             s.setListener(this);
         }
     }
+
+    @Override
+    public void onAccuracyChanged(Sensor sensor, int accuracy) {
+    }
+
+    @Override
+    public void onSensorChanged(SensorEvent event) {
+        int type = event.sensor.getType();
+        float[] data;
+        if (type == Sensor.TYPE_ACCELEROMETER) {
+            data = mGData;
+        } else if (type == Sensor.TYPE_MAGNETIC_FIELD) {
+            data = mMData;
+        } else {
+            // we should not be here.
+            return;
+        }
+        for (int i = 0; i < 3 ; i++) {
+            data[i] = event.values[i];
+        }
+        float[] orientation = new float[3];
+        SensorManager.getRotationMatrix(mR, null, mGData, mMData);
+        SensorManager.getOrientation(mR, orientation);
+        mHeading = (int) (orientation[0] * 180f / Math.PI) % 360;
+        if (mHeading < 0) {
+            mHeading += 360;
+        }
+    }
 }
diff --git a/src/com/android/camera/Storage.java b/src/com/android/camera/Storage.java
index 648fa7d..ba995ed 100644
--- a/src/com/android/camera/Storage.java
+++ b/src/com/android/camera/Storage.java
@@ -30,6 +30,7 @@
 import android.util.Log;
 
 import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.exif.ExifInterface;
 
 import java.io.File;
 import java.io.FileOutputStream;
@@ -49,7 +50,7 @@
     public static final long UNAVAILABLE = -1L;
     public static final long PREPARING = -2L;
     public static final long UNKNOWN_SIZE = -3L;
-    public static final long LOW_STORAGE_THRESHOLD= 50000000;
+    public static final long LOW_STORAGE_THRESHOLD = 50000000;
 
     @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
     private static void setImageSize(ContentValues values, int width, int height) {
@@ -77,11 +78,19 @@
 
     // Save the image and add it to media store.
     public static Uri addImage(ContentResolver resolver, String title,
-            long date, Location location, int orientation, byte[] jpeg,
-            int width, int height) {
+            long date, Location location, int orientation, ExifInterface exif,
+            byte[] jpeg, int width, int height) {
         // Save the image.
         String path = generateFilepath(title);
-        writeFile(path, jpeg);
+        if (exif != null) {
+            try {
+                exif.writeExif(jpeg, path);
+            } catch (Exception e) {
+                Log.e(TAG, "Failed to write data", e);
+            }
+        } else {
+            writeFile(path, jpeg);
+        }
         return addImage(resolver, title, date, location, orientation,
                 jpeg.length, path, width, height);
     }
diff --git a/src/com/android/camera/VideoModule.java b/src/com/android/camera/VideoModule.java
index 998bbf7..1f31778 100644
--- a/src/com/android/camera/VideoModule.java
+++ b/src/com/android/camera/VideoModule.java
@@ -58,6 +58,7 @@
 import com.android.camera.ui.RotateTextToast;
 import com.android.gallery3d.R;
 import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.exif.ExifInterface;
 import com.android.gallery3d.util.AccessibilityUtils;
 import com.android.gallery3d.util.UsageStatistics;
 
@@ -587,7 +588,7 @@
 
     @Override
     public void onShutterButtonFocus(boolean pressed) {
-        // Do nothing (everything happens in onShutterButtonClick).
+        mUI.setShutterPressed(pressed);
     }
 
     private void readVideoPreferences() {
@@ -1122,6 +1123,7 @@
         setupMediaRecorderPreviewDisplay();
         // Unlock the camera object before passing it to media recorder.
         mActivity.mCameraDevice.unlock();
+        mActivity.mCameraDevice.waitDone();
         mMediaRecorder.setCamera(mActivity.mCameraDevice.getCamera());
         if (!mCaptureTimeLapse) {
             mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
@@ -1646,6 +1648,7 @@
             releaseMediaRecorder();
             if (!mPaused) {
                 mActivity.mCameraDevice.lock();
+                mActivity.mCameraDevice.waitDone();
                 if (ApiHelper.HAS_SURFACE_TEXTURE &&
                     !ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
                     stopPreview();
@@ -2213,11 +2216,12 @@
     private void storeImage(final byte[] data, Location loc) {
         long dateTaken = System.currentTimeMillis();
         String title = Util.createJpegName(dateTaken);
-        int orientation = Exif.getOrientation(data);
+        ExifInterface exif = Exif.getExif(data);
+        int orientation = Exif.getOrientation(exif);
         Size s = mParameters.getPictureSize();
         mActivity.getMediaSaveService().addImage(
-                data, title, dateTaken, loc, s.width, s.height,
-                orientation, mOnMediaSavedListener, mContentResolver);
+                data, title, dateTaken, loc, s.width, s.height, orientation,
+                exif, mOnMediaSavedListener, mContentResolver);
     }
 
     private boolean resetEffect() {
diff --git a/src/com/android/camera/VideoUI.java b/src/com/android/camera/VideoUI.java
index 63c1909..0eac49d 100644
--- a/src/com/android/camera/VideoUI.java
+++ b/src/com/android/camera/VideoUI.java
@@ -320,6 +320,12 @@
         return false;
     }
 
+    // disable preview gestures after shutter is pressed
+    public void setShutterPressed(boolean pressed) {
+        if (mGestures == null) return;
+        mGestures.setEnabled(!pressed);
+    }
+
     public void enableShutter(boolean enable) {
         if (mShutterButton != null) {
             mShutterButton.setEnabled(enable);
diff --git a/src/com/android/camera/ui/CameraSwitcher.java b/src/com/android/camera/ui/CameraSwitcher.java
index e9551ad..b046ff7 100644
--- a/src/com/android/camera/ui/CameraSwitcher.java
+++ b/src/com/android/camera/ui/CameraSwitcher.java
@@ -313,6 +313,8 @@
                     // Verify that we weren't canceled
                     if (showsPopup()) {
                         setVisibility(View.INVISIBLE);
+                        // request layout to make sure popup is laid out correctly on ICS
+                        mPopup.requestLayout();
                     }
                 }
             };
diff --git a/src/com/android/gallery3d/filtershow/FilterShowActivity.java b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
index a642839..409f1e3 100644
--- a/src/com/android/gallery3d/filtershow/FilterShowActivity.java
+++ b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
@@ -50,6 +50,7 @@
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.data.LocalAlbum;
+import com.android.gallery3d.filtershow.cache.CachingPipeline;
 import com.android.gallery3d.filtershow.cache.FilteringPipeline;
 import com.android.gallery3d.filtershow.cache.ImageLoader;
 import com.android.gallery3d.filtershow.editors.BasicEditor;
@@ -68,7 +69,6 @@
 import com.android.gallery3d.filtershow.imageshow.ImageCrop;
 import com.android.gallery3d.filtershow.imageshow.ImageShow;
 import com.android.gallery3d.filtershow.imageshow.ImageTinyPlanet;
-import com.android.gallery3d.filtershow.imageshow.ImageZoom;
 import com.android.gallery3d.filtershow.imageshow.MasterImage;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
 import com.android.gallery3d.filtershow.provider.SharedImageProvider;
@@ -136,8 +136,8 @@
 
         clearGalleryBitmapPool();
 
+        CachingPipeline.createRenderscriptContext(this);
         setupMasterImage();
-        ImageFilterRS.createRenderscriptContext(this);
         setDefaultValues();
         fillEditors();
 
@@ -356,7 +356,6 @@
 
         ImageShow.setDefaultBackgroundColor(res.getColor(R.color.background_screen));
         // TODO: get those values from XML.
-        ImageZoom.setZoomedSize(getPixelsFromDip(256));
         FramedTextButton.setTextSize((int) getPixelsFromDip(14));
         FramedTextButton.setTrianglePadding((int) getPixelsFromDip(4));
         FramedTextButton.setTriangleSize((int) getPixelsFromDip(10));
@@ -528,7 +527,7 @@
         FiltersManager.getPreviewManager().freeRSFilterScripts();
         FiltersManager.getManager().freeRSFilterScripts();
         FiltersManager.reset();
-        ImageFilterRS.destroyRenderScriptContext();
+        CachingPipeline.destroyRenderScriptContext();
         super.onDestroy();
     }
 
diff --git a/src/com/android/gallery3d/filtershow/PanelController.java b/src/com/android/gallery3d/filtershow/PanelController.java
index 8352032..6b20fe1 100644
--- a/src/com/android/gallery3d/filtershow/PanelController.java
+++ b/src/com/android/gallery3d/filtershow/PanelController.java
@@ -151,7 +151,7 @@
         private final View mView;
         private final LinearLayout mAccessoryViewList;
         private Vector<View> mAccessoryViews = new Vector<View>();
-        private final TextView mTextView;
+        private final Button mTextView;
         private boolean mSelected = false;
         private String mEffectName = null;
         private int mParameterValue = 0;
@@ -160,10 +160,9 @@
         public UtilityPanel(Context context, View utilityPanel) {
             mView = utilityPanel;
             View accessoryViewList = mView.findViewById(R.id.panelAccessoryViewList);
-            Button textView = (Button) mView.findViewById(R.id.applyEffect);
+            mTextView = (Button) mView.findViewById(R.id.applyEffect);
             mContext = context;
             mAccessoryViewList = (LinearLayout) accessoryViewList;
-            mTextView = (TextView) textView;
         }
 
         public boolean selected() {
@@ -212,6 +211,16 @@
         public View getEditControl() {
             return mView.findViewById(R.id.controlArea);
         }
+
+        public void removeControlChildren() {
+            LinearLayout controlArea = (LinearLayout) mView.findViewById(R.id.controlArea);
+            controlArea.removeAllViews();
+        }
+
+        public Button getEditTitle() {
+            return mTextView;
+        }
+
         public void updateText() {
             String s;
             if (mCurrentEditor == null) {
@@ -589,8 +598,10 @@
                     if (mEditorPlaceHolder.contains(representation.getEditorId())) {
                         mCurrentEditor = mEditorPlaceHolder.showEditor(
                                 representation.getEditorId());
-                        mCurrentEditor.setUtilityPanelUI(
-                                mUtilityPanel.getActionControl(), mUtilityPanel.getEditControl());
+                        mUtilityPanel.removeControlChildren();
+                        mCurrentEditor.setUpEditorUI(
+                                mUtilityPanel.getActionControl(), mUtilityPanel.getEditControl(),
+                                mUtilityPanel.getEditTitle());
                         mCurrentImage = mCurrentEditor.getImageShow();
                         mCurrentEditor.setPanelController(this);
 
diff --git a/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java b/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java
index 1190ea4..bc1d450 100644
--- a/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java
+++ b/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java
@@ -16,6 +16,8 @@
 
 package com.android.gallery3d.filtershow.cache;
 
+import android.app.Activity;
+import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.support.v8.renderscript.Allocation;
 import android.support.v8.renderscript.RenderScript;
@@ -24,6 +26,7 @@
 import com.android.gallery3d.filtershow.filters.ImageFilterRS;
 import com.android.gallery3d.filtershow.imageshow.GeometryMetadata;
 import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.presets.FilterEnvironment;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
 
 public class CachingPipeline {
@@ -32,10 +35,15 @@
 
     private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
 
+    private static volatile RenderScript sRS = null;
+    private static volatile Resources sResources = null;
+
     private FiltersManager mFiltersManager = null;
     private volatile Bitmap mOriginalBitmap = null;
     private volatile Bitmap mResizedOriginalBitmap = null;
 
+    private FilterEnvironment mEnvironment = new FilterEnvironment();
+
     private volatile Allocation mOriginalAllocation = null;
     private volatile Allocation mFiltersOnlyOriginalAllocation =  null;
 
@@ -53,24 +61,60 @@
         mName = name;
     }
 
-    public synchronized void reset() {
-        mOriginalBitmap = null; // just a reference to the bitmap in ImageLoader
-        if (mResizedOriginalBitmap != null) {
-            mResizedOriginalBitmap.recycle();
-            mResizedOriginalBitmap = null;
-        }
-        if (mOriginalAllocation != null) {
-            mOriginalAllocation.destroy();
-            mOriginalAllocation = null;
-        }
-        if (mFiltersOnlyOriginalAllocation != null) {
-            mFiltersOnlyOriginalAllocation.destroy();
-            mFiltersOnlyOriginalAllocation = null;
-        }
-        mPreviousGeometry = null;
-        mPreviewScaleFactor = 1.0f;
+    public static synchronized Resources getResources() {
+        return sResources;
+    }
 
-        destroyPixelAllocations();
+    public static synchronized void setResources(Resources resources) {
+        sResources = resources;
+    }
+
+    public static synchronized RenderScript getRenderScriptContext() {
+        return sRS;
+    }
+
+    public static synchronized void setRenderScriptContext(RenderScript RS) {
+        sRS = RS;
+    }
+
+    public static synchronized void createRenderscriptContext(Activity context) {
+        if (sRS != null) {
+            Log.w(LOGTAG, "A prior RS context exists when calling setRenderScriptContext");
+            destroyRenderScriptContext();
+        }
+        sRS = RenderScript.create(context);
+        sResources = context.getResources();
+    }
+
+    public static synchronized void destroyRenderScriptContext() {
+        sRS.destroy();
+        sRS = null;
+        sResources = null;
+    }
+
+    public synchronized void reset() {
+        synchronized (CachingPipeline.class) {
+            if (getRenderScriptContext() == null) {
+                return;
+            }
+            mOriginalBitmap = null; // just a reference to the bitmap in ImageLoader
+            if (mResizedOriginalBitmap != null) {
+                mResizedOriginalBitmap.recycle();
+                mResizedOriginalBitmap = null;
+            }
+            if (mOriginalAllocation != null) {
+                mOriginalAllocation.destroy();
+                mOriginalAllocation = null;
+            }
+            if (mFiltersOnlyOriginalAllocation != null) {
+                mFiltersOnlyOriginalAllocation.destroy();
+                mFiltersOnlyOriginalAllocation = null;
+            }
+            mPreviousGeometry = null;
+            mPreviewScaleFactor = 1.0f;
+
+            destroyPixelAllocations();
+        }
     }
 
     private synchronized void destroyPixelAllocations() {
@@ -108,18 +152,19 @@
         return "UNKNOWN TYPE!";
     }
 
-    private void setPresetParameters(ImagePreset preset) {
-        preset.setScaleFactor(mPreviewScaleFactor);
-        preset.setQuality(ImagePreset.QUALITY_PREVIEW);
-        preset.setupEnvironment(mFiltersManager);
-        preset.getEnvironment().setCachingPipeline(this);
+    private void setupEnvironment(ImagePreset preset) {
+        mEnvironment.setCachingPipeline(this);
+        mEnvironment.setFiltersManager(mFiltersManager);
+        mEnvironment.setScaleFactor(mPreviewScaleFactor);
+        mEnvironment.setQuality(ImagePreset.QUALITY_PREVIEW);
+        mEnvironment.setImagePreset(preset);
     }
 
     public void setOriginal(Bitmap bitmap) {
         mOriginalBitmap = bitmap;
         Log.v(LOGTAG,"setOriginal, size " + bitmap.getWidth() + " x " + bitmap.getHeight());
         ImagePreset preset = MasterImage.getImage().getPreset();
-        preset.setupEnvironment(mFiltersManager);
+        setupEnvironment(preset);
         updateOriginalAllocation(preset);
     }
 
@@ -139,7 +184,7 @@
             Log.v(LOGTAG, "geometry has changed");
         }
 
-        RenderScript RS = ImageFilterRS.getRenderScriptContext();
+        RenderScript RS = getRenderScriptContext();
 
         Allocation filtersOnlyOriginalAllocation = mFiltersOnlyOriginalAllocation;
         mFiltersOnlyOriginalAllocation = Allocation.createFromBitmap(RS, originalBitmap,
@@ -149,7 +194,7 @@
         }
 
         Allocation originalAllocation = mOriginalAllocation;
-        mResizedOriginalBitmap = preset.applyGeometry(originalBitmap);
+        mResizedOriginalBitmap = preset.applyGeometry(originalBitmap, mEnvironment);
         mOriginalAllocation = Allocation.createFromBitmap(RS, mResizedOriginalBitmap,
                 Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
         if (originalAllocation != null) {
@@ -161,109 +206,138 @@
     }
 
     public synchronized void render(RenderingRequest request) {
-        if ((request.getType() != RenderingRequest.PARTIAL_RENDERING
-                && request.getBitmap() == null)
-                || request.getImagePreset() == null) {
-            return;
-        }
-
-        if (DEBUG) {
-            Log.v(LOGTAG, "render image of type " + getType(request));
-        }
-
-        Bitmap bitmap = request.getBitmap();
-        ImagePreset preset = request.getImagePreset();
-        setPresetParameters(preset);
-        mFiltersManager.freeFilterResources(preset);
-
-        if (request.getType() == RenderingRequest.PARTIAL_RENDERING) {
-            ImageLoader loader = MasterImage.getImage().getImageLoader();
-            if (loader == null) {
-                Log.w(LOGTAG, "loader not yet setup, cannot handle: " + getType(request));
+        synchronized (CachingPipeline.class) {
+            if (getRenderScriptContext() == null) {
                 return;
             }
-            bitmap = loader.getScaleOneImageForPreset(null, preset,
-                    request.getBounds(), request.getDestination(), false);
-            if (bitmap == null) {
-                Log.w(LOGTAG, "could not get bitmap for: " + getType(request));
+            if ((request.getType() != RenderingRequest.PARTIAL_RENDERING
+                    && request.getBitmap() == null)
+                    || request.getImagePreset() == null) {
                 return;
             }
-        }
 
-        if (request.getType() == RenderingRequest.FULL_RENDERING
-                || request.getType() == RenderingRequest.GEOMETRY_RENDERING
-                || request.getType() == RenderingRequest.FILTERS_RENDERING) {
-            updateOriginalAllocation(preset);
-        }
+            if (DEBUG) {
+                Log.v(LOGTAG, "render image of type " + getType(request));
+            }
 
-        if (DEBUG) {
-            Log.v(LOGTAG, "after update, req bitmap (" + bitmap.getWidth() + "x" + bitmap.getHeight()
-                    +" ? resizeOriginal (" + mResizedOriginalBitmap.getWidth() + "x"
-                    + mResizedOriginalBitmap.getHeight());
-        }
-
-        if (request.getType() == RenderingRequest.FULL_RENDERING
-                || request.getType() == RenderingRequest.GEOMETRY_RENDERING) {
-            mOriginalAllocation.copyTo(bitmap);
-        } else if (request.getType() == RenderingRequest.FILTERS_RENDERING) {
-            mFiltersOnlyOriginalAllocation.copyTo(bitmap);
-        }
-
-        if (request.getType() == RenderingRequest.FULL_RENDERING
-                || request.getType() == RenderingRequest.FILTERS_RENDERING
-                || request.getType() == RenderingRequest.ICON_RENDERING
-                || request.getType() == RenderingRequest.PARTIAL_RENDERING) {
-            Bitmap bmp = preset.apply(bitmap);
-            request.setBitmap(bmp);
+            Bitmap bitmap = request.getBitmap();
+            ImagePreset preset = request.getImagePreset();
+            setupEnvironment(preset);
             mFiltersManager.freeFilterResources(preset);
-        }
 
+            if (request.getType() == RenderingRequest.PARTIAL_RENDERING) {
+                ImageLoader loader = MasterImage.getImage().getImageLoader();
+                if (loader == null) {
+                    Log.w(LOGTAG, "loader not yet setup, cannot handle: " + getType(request));
+                    return;
+                }
+                bitmap = loader.getScaleOneImageForPreset(request.getBounds(),
+                        request.getDestination());
+                if (bitmap == null) {
+                    Log.w(LOGTAG, "could not get bitmap for: " + getType(request));
+                    return;
+                }
+            }
+
+            if (request.getType() == RenderingRequest.FULL_RENDERING
+                    || request.getType() == RenderingRequest.GEOMETRY_RENDERING
+                    || request.getType() == RenderingRequest.FILTERS_RENDERING) {
+                updateOriginalAllocation(preset);
+            }
+
+            if (DEBUG) {
+                Log.v(LOGTAG, "after update, req bitmap (" + bitmap.getWidth() + "x" + bitmap.getHeight()
+                        + " ? resizeOriginal (" + mResizedOriginalBitmap.getWidth() + "x"
+                        + mResizedOriginalBitmap.getHeight());
+            }
+
+            if (request.getType() == RenderingRequest.FULL_RENDERING
+                    || request.getType() == RenderingRequest.GEOMETRY_RENDERING) {
+                mOriginalAllocation.copyTo(bitmap);
+            } else if (request.getType() == RenderingRequest.FILTERS_RENDERING) {
+                mFiltersOnlyOriginalAllocation.copyTo(bitmap);
+            }
+
+            if (request.getType() == RenderingRequest.FULL_RENDERING
+                    || request.getType() == RenderingRequest.FILTERS_RENDERING
+                    || request.getType() == RenderingRequest.ICON_RENDERING
+                    || request.getType() == RenderingRequest.PARTIAL_RENDERING) {
+                Bitmap bmp = preset.apply(bitmap, mEnvironment);
+                request.setBitmap(bmp);
+                mFiltersManager.freeFilterResources(preset);
+            }
+        }
     }
 
     public synchronized Bitmap renderFinalImage(Bitmap bitmap, ImagePreset preset) {
-        setPresetParameters(preset);
-        mFiltersManager.freeFilterResources(preset);
-        bitmap = preset.applyGeometry(bitmap);
-        bitmap = preset.apply(bitmap);
-        return bitmap;
+        synchronized (CachingPipeline.class) {
+            if (getRenderScriptContext() == null) {
+                return bitmap;
+            }
+            setupEnvironment(preset);
+            mEnvironment.setQuality(ImagePreset.QUALITY_FINAL);
+            mEnvironment.setScaleFactor(1.0f);
+            mFiltersManager.freeFilterResources(preset);
+            bitmap = preset.applyGeometry(bitmap, mEnvironment);
+            bitmap = preset.apply(bitmap, mEnvironment);
+            return bitmap;
+        }
+    }
+
+    public synchronized Bitmap renderGeometryIcon(Bitmap bitmap, ImagePreset preset) {
+        synchronized (CachingPipeline.class) {
+            if (getRenderScriptContext() == null) {
+                return bitmap;
+            }
+            setupEnvironment(preset);
+            mEnvironment.setQuality(ImagePreset.QUALITY_PREVIEW);
+            mFiltersManager.freeFilterResources(preset);
+            bitmap = preset.applyGeometry(bitmap, mEnvironment);
+            return bitmap;
+        }
     }
 
     public synchronized void compute(TripleBufferBitmap buffer, ImagePreset preset, int type) {
-        if (DEBUG) {
-            Log.v(LOGTAG, "compute preset " + preset);
-            preset.showFilters();
-        }
+        synchronized (CachingPipeline.class) {
+            if (getRenderScriptContext() == null) {
+                return;
+            }
+            if (DEBUG) {
+                Log.v(LOGTAG, "compute preset " + preset);
+                preset.showFilters();
+            }
 
-        String thread = Thread.currentThread().getName();
-        long time = System.currentTimeMillis();
-        setPresetParameters(preset);
-        mFiltersManager.freeFilterResources(preset);
+            String thread = Thread.currentThread().getName();
+            long time = System.currentTimeMillis();
+            setupEnvironment(preset);
+            mFiltersManager.freeFilterResources(preset);
 
-        Bitmap resizedOriginalBitmap = mResizedOriginalBitmap;
-        if (updateOriginalAllocation(preset)) {
-            resizedOriginalBitmap = mResizedOriginalBitmap;
-            buffer.updateBitmaps(resizedOriginalBitmap);
-        }
-        Bitmap bitmap = buffer.getProducer();
-        long time2 = System.currentTimeMillis();
+            Bitmap resizedOriginalBitmap = mResizedOriginalBitmap;
+            if (updateOriginalAllocation(preset)) {
+                resizedOriginalBitmap = mResizedOriginalBitmap;
+                buffer.updateBitmaps(resizedOriginalBitmap);
+            }
+            Bitmap bitmap = buffer.getProducer();
+            long time2 = System.currentTimeMillis();
 
-        if (bitmap == null || (bitmap.getWidth() != resizedOriginalBitmap.getWidth())
-                || (bitmap.getHeight() != resizedOriginalBitmap.getHeight())) {
-            buffer.updateBitmaps(resizedOriginalBitmap);
-            bitmap = buffer.getProducer();
-        }
-        mOriginalAllocation.copyTo(bitmap);
+            if (bitmap == null || (bitmap.getWidth() != resizedOriginalBitmap.getWidth())
+                    || (bitmap.getHeight() != resizedOriginalBitmap.getHeight())) {
+                buffer.updateBitmaps(resizedOriginalBitmap);
+                bitmap = buffer.getProducer();
+            }
+            mOriginalAllocation.copyTo(bitmap);
 
-        bitmap = preset.apply(bitmap);
+            bitmap = preset.apply(bitmap, mEnvironment);
 
-        mFiltersManager.freeFilterResources(preset);
+            mFiltersManager.freeFilterResources(preset);
 
-        time = System.currentTimeMillis() - time;
-        time2 = System.currentTimeMillis() - time2;
-        if (DEBUG) {
-            Log.v(LOGTAG, "Applying type " + type + " filters to bitmap "
-                    + bitmap + " (" + bitmap.getWidth() + " x " + bitmap.getHeight()
-                    + ") took " + time + " ms, " + time2 + " ms for the filter, on thread " + thread);
+            time = System.currentTimeMillis() - time;
+            time2 = System.currentTimeMillis() - time2;
+            if (DEBUG) {
+                Log.v(LOGTAG, "Applying type " + type + " filters to bitmap "
+                        + bitmap + " (" + bitmap.getWidth() + " x " + bitmap.getHeight()
+                        + ") took " + time + " ms, " + time2 + " ms for the filter, on thread " + thread);
+            }
         }
     }
 
@@ -278,11 +352,11 @@
     }
 
     public synchronized boolean isInitialized() {
-        return mOriginalBitmap != null;
+        return getRenderScriptContext() != null && mOriginalBitmap != null;
     }
 
     public boolean prepareRenderscriptAllocations(Bitmap bitmap) {
-        RenderScript RS = ImageFilterRS.getRenderScriptContext();
+        RenderScript RS = getRenderScriptContext();
         boolean needsUpdate = false;
         if (mOutPixelsAllocation == null || mInPixelsAllocation == null ||
                 bitmap.getWidth() != mWidth || bitmap.getHeight() != mHeight) {
@@ -321,4 +395,5 @@
     public String getName() {
         return mName;
     }
+
 }
diff --git a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
index ef3e3d5..a6a0bcf 100644
--- a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
+++ b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
@@ -40,6 +40,7 @@
 import com.android.gallery3d.exif.ExifInterface;
 import com.android.gallery3d.filtershow.FilterShowActivity;
 import com.android.gallery3d.filtershow.HistoryAdapter;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
 import com.android.gallery3d.filtershow.imageshow.ImageShow;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
 import com.android.gallery3d.filtershow.tools.BitmapTask;
@@ -358,51 +359,28 @@
         }
     };
 
-    // FIXME: this currently does the loading + filtering on the UI thread --
-    // need to move this to a background thread.
-    public Bitmap getScaleOneImageForPreset(ImageShow caller, ImagePreset imagePreset, Rect bounds,
-                                            Rect destination, boolean force) {
+    public Bitmap getScaleOneImageForPreset(Rect bounds, Rect destination) {
         mLoadingLock.lock();
-        Bitmap bmp = mZoomCache.getImage(imagePreset, bounds);
-        if (force || bmp == null) {
-            BitmapFactory.Options options = new BitmapFactory.Options();
-            options.inMutable = true;
-            if (destination != null) {
-                if (bounds.width() > destination.width()) {
-                    int sampleSize = 1;
-                    int w = bounds.width();
-                    while (w > destination.width()) {
-                        sampleSize *= 2;
-                        w /= sampleSize;
-                    }
-                    options.inSampleSize = sampleSize;
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inMutable = true;
+        if (destination != null) {
+            if (bounds.width() > destination.width()) {
+                int sampleSize = 1;
+                int w = bounds.width();
+                while (w > destination.width()) {
+                    sampleSize *= 2;
+                    w /= sampleSize;
                 }
-            }
-            bmp = loadRegionBitmap(mUri, options, bounds);
-            if (destination != null) {
-                mLoadingLock.unlock();
-                return bmp;
-            }
-            if (bmp != null) {
-                float scaleFactor = imagePreset.getScaleFactor();
-                float scale = (float) bmp.getWidth() / (float) getOriginalBounds().width();
-                imagePreset.setScaleFactor(scale);
-                imagePreset.setupEnvironment();
-                bmp = imagePreset.apply(bmp);
-                imagePreset.setScaleFactor(scaleFactor);
-                mZoomCache.setImage(imagePreset, bounds, bmp);
-                mLoadingLock.unlock();
-                return bmp;
+                options.inSampleSize = sampleSize;
             }
         }
+        Bitmap bmp = loadRegionBitmap(mUri, options, bounds);
         mLoadingLock.unlock();
         return bmp;
     }
 
     public void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity,
             File destination) {
-        preset.setQuality(ImagePreset.QUALITY_FINAL);
-        preset.setScaleFactor(1.0f);
         new SaveCopyTask(mContext, mUri, destination, new SaveCopyTask.Callback() {
 
             @Override
@@ -510,9 +488,6 @@
 
     public void returnFilteredResult(ImagePreset preset,
             final FilterShowActivity filterShowActivity) {
-        preset.setQuality(ImagePreset.QUALITY_FINAL);
-        preset.setScaleFactor(1.0f);
-
         BitmapTask.Callbacks<ImagePreset> cb = new BitmapTask.Callbacks<ImagePreset>() {
 
             @Override
@@ -545,9 +520,9 @@
                             Log.w(LOGTAG, "Failed to save image!");
                             return null;
                         }
-                        param.setupEnvironment();
-                        bitmap = param.applyGeometry(bitmap);
-                        bitmap = param.apply(bitmap);
+                        CachingPipeline pipeline = new CachingPipeline(
+                                FiltersManager.getManager(), "Saving");
+                        bitmap = pipeline.renderFinalImage(bitmap, param);
                         noBitmap = false;
                     } catch (java.lang.OutOfMemoryError e) {
                         // Try 5 times before failing for good.
diff --git a/src/com/android/gallery3d/filtershow/cache/RenderingRequest.java b/src/com/android/gallery3d/filtershow/cache/RenderingRequest.java
index 3cc61f9..138abb0 100644
--- a/src/com/android/gallery3d/filtershow/cache/RenderingRequest.java
+++ b/src/com/android/gallery3d/filtershow/cache/RenderingRequest.java
@@ -19,7 +19,9 @@
 import android.graphics.Bitmap;
 import android.graphics.Rect;
 import com.android.gallery3d.app.Log;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
 import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.presets.FilterEnvironment;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
 
 public class RenderingRequest {
@@ -54,8 +56,9 @@
         if (type == FULL_RENDERING
                 || type == GEOMETRY_RENDERING
                 || type == ICON_RENDERING) {
-            preset.setupEnvironment();
-            bitmap = preset.applyGeometry(source);
+            CachingPipeline pipeline = new CachingPipeline(
+                    FiltersManager.getManager(), "Icon");
+            bitmap = pipeline.renderGeometryIcon(source, preset);
         } else if (type != PARTIAL_RENDERING) {
             bitmap = Bitmap.createBitmap(source.getWidth(), source.getHeight(), mConfig);
         }
diff --git a/src/com/android/gallery3d/filtershow/controller/ActionSlider.java b/src/com/android/gallery3d/filtershow/controller/ActionSlider.java
new file mode 100644
index 0000000..6ed2467
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/ActionSlider.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.filtershow.controller;
+
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.Editor;
+
+public class ActionSlider extends TitledSlider {
+    private static final String LOGTAG = "ActionSlider";
+    ImageButton mActionButton;
+    public ActionSlider() {
+        mLayoutID = R.layout.filtershow_control_action_slider;
+    }
+
+    @Override
+    public void setUp(ViewGroup container, Parameter parameter, Editor editor) {
+        super.setUp(container, parameter, editor);
+        mActionButton = (ImageButton) mTopView.findViewById(R.id.actionButton);
+        mActionButton.setOnClickListener(new OnClickListener() {
+
+            @Override
+            public void onClick(View v) {
+                ((ParameterActionAndInt) mParameter).fireAction();
+            }
+        });
+    }
+
+    @Override
+    public void updateUI() {
+        super.updateUI();
+        if (mActionButton != null) {
+            int iconId = ((ParameterActionAndInt) mParameter).getActionIcon();
+            mActionButton.setImageResource(iconId);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/BasicSlider.java b/src/com/android/gallery3d/filtershow/controller/BasicSlider.java
new file mode 100644
index 0000000..c54fe77
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/BasicSlider.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.filtershow.controller;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.Editor;
+
+public class BasicSlider implements Control {
+    private SeekBar mSeekBar;
+    private ParameterInteger mParameter;
+    Editor mEditor;
+
+    @Override
+    public void setUp(ViewGroup container, Parameter parameter, Editor editor) {
+        mEditor = editor;
+        Context context = container.getContext();
+        mParameter = (ParameterInteger) parameter;
+        LayoutInflater inflater =
+                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        LinearLayout lp = (LinearLayout) inflater.inflate(
+                R.layout.filtershow_seekbar, container, true);
+        mSeekBar = (SeekBar) lp.findViewById(R.id.primarySeekBar);
+
+        updateUI();
+        mSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+            }
+
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                if (mParameter != null) {
+                    mParameter.setValue(progress + mParameter.getMinimum());
+                    mEditor.commitLocalRepresentation();
+
+                }
+            }
+        });
+    }
+
+    @Override
+    public View getTopView() {
+        return mSeekBar;
+    }
+
+    @Override
+    public void setPrameter(Parameter parameter) {
+        mParameter = (ParameterInteger) parameter;
+        if (mSeekBar != null) {
+            updateUI();
+        }
+    }
+
+    @Override
+    public void updateUI() {
+        mSeekBar.setMax(mParameter.getMaximum() - mParameter.getMinimum());
+        mSeekBar.setProgress(mParameter.getValue() - mParameter.getMinimum());
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/Control.java b/src/com/android/gallery3d/filtershow/controller/Control.java
new file mode 100644
index 0000000..4342290
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/Control.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.filtershow.controller;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.gallery3d.filtershow.editors.Editor;
+
+public interface Control {
+    public void setUp(ViewGroup container, Parameter parameter, Editor editor);
+
+    public View getTopView();
+
+    public void setPrameter(Parameter parameter);
+
+    public void updateUI();
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/Parameter.java b/src/com/android/gallery3d/filtershow/controller/Parameter.java
new file mode 100644
index 0000000..1e8694a
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/Parameter.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.filtershow.controller;
+
+public interface Parameter {
+    String getParameterName();
+
+    String getParameterType();
+
+    String getValueString();
+
+    public void setController(Control c);
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/ParameterActionAndInt.java b/src/com/android/gallery3d/filtershow/controller/ParameterActionAndInt.java
new file mode 100644
index 0000000..04567e2
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/ParameterActionAndInt.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.filtershow.controller;
+
+public interface ParameterActionAndInt extends ParameterInteger {
+    static String sParameterType = "ParameterActionAndInt";
+
+    public void fireAction();
+
+    public int getActionIcon();
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/ParameterInteger.java b/src/com/android/gallery3d/filtershow/controller/ParameterInteger.java
new file mode 100644
index 0000000..0bfd201
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/ParameterInteger.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.filtershow.controller;
+
+public interface ParameterInteger extends Parameter {
+    static String sParameterType = "ParameterInteger";
+
+    int getMaximum();
+
+    int getMinimum();
+
+    int getDefaultValue();
+
+    int getValue();
+
+    void setValue(int value);
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/TitledSlider.java b/src/com/android/gallery3d/filtershow/controller/TitledSlider.java
new file mode 100644
index 0000000..30b6fdb
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/TitledSlider.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.filtershow.controller;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.Editor;
+
+public class TitledSlider implements Control {
+    private final String LOGTAG = "ParametricEditor";
+    private SeekBar mSeekBar;
+    private TextView mControlName;
+    private TextView mControlValue;
+    protected ParameterInteger mParameter;
+    Editor mEditor;
+    View mTopView;
+    protected int mLayoutID = R.layout.filtershow_control_title_slider;
+
+    @Override
+    public void setUp(ViewGroup container, Parameter parameter, Editor editor) {
+        mEditor = editor;
+        Context context = container.getContext();
+        mParameter = (ParameterInteger) parameter;
+        LayoutInflater inflater =
+                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mTopView = inflater.inflate(mLayoutID, container, true);
+        mTopView.setVisibility(View.VISIBLE);
+        mSeekBar = (SeekBar) mTopView.findViewById(R.id.controlValueSeekBar);
+        mControlName = (TextView) mTopView.findViewById(R.id.controlName);
+        mControlValue = (TextView) mTopView.findViewById(R.id.controlValue);
+        updateUI();
+        mSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+            }
+
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                if (mParameter != null) {
+                    mParameter.setValue(progress + mParameter.getMinimum());
+                    if (mControlName != null) {
+                        mControlName.setText(mParameter.getParameterName());
+                    }
+                    if (mControlValue != null) {
+                        mControlValue.setText(Integer.toString(mParameter.getValue()));
+                    }
+                    mEditor.commitLocalRepresentation();
+                }
+            }
+        });
+    }
+
+    @Override
+    public void setPrameter(Parameter parameter) {
+        mParameter = (ParameterInteger) parameter;
+        if (mSeekBar != null)
+            updateUI();
+    }
+
+    @Override
+    public void updateUI() {
+        if (mControlName != null) {
+            mControlName.setText(mParameter.getParameterName());
+        }
+        if (mControlValue != null) {
+            mControlValue.setText(
+                    Integer.toString(mParameter.getValue()));
+        }
+        mSeekBar.setMax(mParameter.getMaximum() - mParameter.getMinimum());
+        mSeekBar.setProgress(mParameter.getValue() - mParameter.getMinimum());
+        mEditor.commitLocalRepresentation();
+    }
+
+    @Override
+    public View getTopView() {
+        return mTopView;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/Editor.java b/src/com/android/gallery3d/filtershow/editors/Editor.java
index 69266ff..013590c 100644
--- a/src/com/android/gallery3d/filtershow/editors/Editor.java
+++ b/src/com/android/gallery3d/filtershow/editors/Editor.java
@@ -17,18 +17,22 @@
 package com.android.gallery3d.filtershow.editors;
 
 import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.text.Html;
+import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.*;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.SeekBar;
 import android.widget.SeekBar.OnSeekBarChangeListener;
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.filtershow.PanelController;
 import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.controller.Control;
 import com.android.gallery3d.filtershow.filters.FilterRepresentation;
 import com.android.gallery3d.filtershow.imageshow.ImageShow;
 import com.android.gallery3d.filtershow.imageshow.MasterImage;
@@ -43,6 +47,7 @@
     protected ImageShow mImageShow;
     protected FrameLayout mFrameLayout;
     protected SeekBar mSeekBar;
+    Button mEditTitle;
     protected PanelController mPanelController;
     protected int mID;
     private final String LOGTAG = "Editor";
@@ -57,7 +62,7 @@
     }
 
     public String calculateUserMessage(Context context, String effectName, Object parameterValue) {
-        String apply = context.getString(R.string.apply_effect);
+        String apply = "";
         if (mShowParameter == SHOW_VALUE_INT) {
             apply += " " + effectName + " " + parameterValue;
         } else {
@@ -83,6 +88,12 @@
         return true;
     }
 
+    public void setUpEditorUI(View actionButton, View editControl, Button editTitle) {
+        this.mEditTitle = editTitle;
+        setMenuIcon(true);
+        setUtilityPanelUI(actionButton, editControl);
+    }
+
     public boolean showsPopupIndicator() {
         return true;
     }
@@ -92,17 +103,28 @@
      * @param editControl this is the black area for sliders etc
      */
     public void setUtilityPanelUI(View actionButton, View editControl) {
-        mSeekBar = (SeekBar) editControl.findViewById(R.id.primarySeekBar);
+
+        AttributeSet aset;
+        Context context = editControl.getContext();
+        LayoutInflater inflater =
+                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        LinearLayout lp = (LinearLayout) inflater.inflate(
+                R.layout.filtershow_seekbar, (ViewGroup) editControl, true);
+        mSeekBar = (SeekBar) lp.findViewById(R.id.primarySeekBar);
+        mSeekBar.setOnSeekBarChangeListener(this);
+
         if (showsSeekBar()) {
             mSeekBar.setOnSeekBarChangeListener(this);
             mSeekBar.setVisibility(View.VISIBLE);
         } else {
             mSeekBar.setVisibility(View.INVISIBLE);
         }
+
         Button button = (Button) actionButton.findViewById(R.id.applyEffect);
         if (button != null) {
             if (showsPopupIndicator()) {
-                button.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.filtershow_menu_marker, 0);
+                button.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0,
+                        R.drawable.filtershow_menu_marker, 0);
             } else {
                 button.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0);
             }
@@ -205,19 +227,29 @@
     }
 
     public void openUtilityPanel(LinearLayout mAccessoryViewList) {
+        setMenuIcon(false);
         if (mImageShow != null) {
             mImageShow.openUtilityPanel(mAccessoryViewList);
         }
     }
 
+    protected void setMenuIcon(boolean on) {
+        mEditTitle.setCompoundDrawablesRelativeWithIntrinsicBounds(
+                0, 0, on ? R.drawable.filtershow_menu_marker : 0, 0);
+    }
     protected void createMenu(int[] strId, View button) {
         PopupMenu pmenu = new PopupMenu(mContext, button);
         Menu menu = pmenu.getMenu();
         for (int i = 0; i < strId.length; i++) {
             menu.add(Menu.NONE, Menu.FIRST + i, 0, mContext.getString(strId[i]));
         }
+        setMenuIcon(true);
+
     }
 
+    public Control[] getControls() {
+        return null;
+    }
     @Override
     public void onStartTrackingTouch(SeekBar arg0) {
 
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorVignette.java b/src/com/android/gallery3d/filtershow/editors/EditorVignette.java
index a7d99e4..7127b21 100644
--- a/src/com/android/gallery3d/filtershow/editors/EditorVignette.java
+++ b/src/com/android/gallery3d/filtershow/editors/EditorVignette.java
@@ -24,7 +24,7 @@
 import com.android.gallery3d.filtershow.filters.FilterVignetteRepresentation;
 import com.android.gallery3d.filtershow.imageshow.ImageVignette;
 
-public class EditorVignette extends BasicEditor {
+public class EditorVignette extends ParametricEditor {
     public static final int ID = R.id.vignetteEditor;
     private static final String LOGTAG = "EditorVignettePlanet";
     ImageVignette mImageVignette;
diff --git a/src/com/android/gallery3d/filtershow/editors/ParametricEditor.java b/src/com/android/gallery3d/filtershow/editors/ParametricEditor.java
new file mode 100644
index 0000000..cf00f4a
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/ParametricEditor.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.filtershow.editors;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.controller.ActionSlider;
+import com.android.gallery3d.filtershow.controller.BasicSlider;
+import com.android.gallery3d.filtershow.controller.Control;
+import com.android.gallery3d.filtershow.controller.Parameter;
+import com.android.gallery3d.filtershow.controller.ParameterActionAndInt;
+import com.android.gallery3d.filtershow.controller.ParameterInteger;
+import com.android.gallery3d.filtershow.controller.TitledSlider;
+import com.android.gallery3d.filtershow.filters.FilterBasicRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+
+import java.lang.reflect.Constructor;
+import java.util.HashMap;
+
+public class ParametricEditor extends Editor {
+    private int mLayoutID;
+    private int mViewID;
+    public static int ID = R.id.editorParametric;
+    private final String LOGTAG = "ParametricEditor";
+    protected Control mControl;
+    public static final int MINIMUM_WIDTH = 600;
+    public static final int MINIMUM_HEIGHT = 800;
+
+    static HashMap<String, Class> portraitMap = new HashMap<String, Class>();
+    static HashMap<String, Class> landscapeMap = new HashMap<String, Class>();
+    static {
+        portraitMap.put(ParameterInteger.sParameterType, BasicSlider.class);
+        landscapeMap.put(ParameterInteger.sParameterType, TitledSlider.class);
+        portraitMap.put(ParameterActionAndInt.sParameterType, ActionSlider.class);
+        landscapeMap.put(ParameterActionAndInt.sParameterType, ActionSlider.class);
+    }
+
+    static Constructor getConstructor(Class cl) {
+        try {
+            return cl.getConstructor(Context.class, ViewGroup.class);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    public ParametricEditor() {
+        super(ID);
+    }
+
+    protected ParametricEditor(int id) {
+        super(id);
+    }
+
+    protected ParametricEditor(int id, int layoutID, int viewID) {
+        super(id);
+        mLayoutID = layoutID;
+        mViewID = viewID;
+    }
+
+    @Override
+    public String calculateUserMessage(Context context, String effectName, Object parameterValue) {
+        String apply = "";
+
+        if (mShowParameter == SHOW_VALUE_INT & useCompact(context)) {
+           if (getLocalRepresentation() instanceof FilterBasicRepresentation) {
+            FilterBasicRepresentation interval = (FilterBasicRepresentation) getLocalRepresentation();
+            apply += " " + effectName + " " + interval.getStateRepresentation();
+           } else {
+                apply += " " + effectName + " " + parameterValue;
+           }
+        } else {
+            apply += " " + effectName;
+        }
+        return apply;
+    }
+
+    @Override
+    public void createEditor(Context context, FrameLayout frameLayout) {
+        super.createEditor(context, frameLayout);
+        unpack(mViewID, mLayoutID);
+    }
+
+    @Override
+    public void reflectCurrentFilter() {
+        super.reflectCurrentFilter();
+        if (getLocalRepresentation() != null
+                && getLocalRepresentation() instanceof FilterBasicRepresentation) {
+            FilterBasicRepresentation interval = (FilterBasicRepresentation) getLocalRepresentation();
+            mControl.setPrameter(interval);
+        }
+    }
+
+    @Override
+    public Control[] getControls() {
+        BasicSlider slider = new BasicSlider();
+        return new Control[] {
+                slider
+        };
+    }
+
+    // TODO: need a better way to decide when which representation
+    static boolean useCompact(Context context) {
+        WindowManager w = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE));
+        Point size = new Point();
+        w.getDefaultDisplay().getSize(size);
+        if (size.x < size.y) { // if tall than wider
+            return true;
+        }
+        if (size.x < MINIMUM_WIDTH) {
+            return true;
+        }
+        if (size.y < MINIMUM_HEIGHT) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void setUtilityPanelUI(View actionButton, View editControl) {
+        FilterRepresentation rep = getLocalRepresentation();
+        if (this instanceof Parameter) {
+            control((Parameter) this, editControl);
+        } else if (rep instanceof Parameter) {
+            control((Parameter) rep, editControl);
+        } else {
+            mSeekBar = new SeekBar(editControl.getContext());
+            LayoutParams lp = new LinearLayout.LayoutParams(
+                    LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+            mSeekBar.setLayoutParams(lp);
+            ((LinearLayout) editControl).addView(mSeekBar);
+            mSeekBar.setOnSeekBarChangeListener(this);
+        }
+    }
+
+    protected void control(Parameter p, View editControl) {
+        String pType = p.getParameterType();
+        Context context = editControl.getContext();
+        Class c = ((useCompact(context)) ? portraitMap : landscapeMap).get(pType);
+
+        if (c != null) {
+            try {
+                mControl = (Control) c.newInstance();
+                mControl.setUp((ViewGroup) editControl, p, this);
+
+            } catch (Exception e) {
+                Log.e(LOGTAG, "Error in loading Control ", e);
+            }
+        } else {
+            Log.e(LOGTAG, "Unable to find class for " + pType);
+            for (String string : portraitMap.keySet()) {
+                Log.e(LOGTAG, "for " + string + " use " + portraitMap.get(string));
+            }
+        }
+    }
+
+    @Override
+    public void commitLocalRepresentation() {
+        super.commitLocalRepresentation();
+        FilterRepresentation rep = getLocalRepresentation();
+    }
+
+    @Override
+    public void onProgressChanged(SeekBar sbar, int progress, boolean arg2) {
+    }
+
+    @Override
+    public void onStartTrackingTouch(SeekBar arg0) {
+    }
+
+    @Override
+    public void onStopTrackingTouch(SeekBar arg0) {
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
index d84c9f2..b5c4de0 100644
--- a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
+++ b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
@@ -65,7 +65,7 @@
         if (preset == null) {
             return;
         }
-        Vector<ImageFilter> usedFilters = preset.getUsedFilters();
+        Vector<ImageFilter> usedFilters = preset.getUsedFilters(this);
         for (Class c : mFilters.keySet()) {
             ImageFilter filter = mFilters.get(c);
             if (!usedFilters.contains(filter)) {
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java
index 2410ebe..34323c4 100644
--- a/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java
+++ b/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java
@@ -17,8 +17,10 @@
 package com.android.gallery3d.filtershow.filters;
 
 import com.android.gallery3d.app.Log;
+import com.android.gallery3d.filtershow.controller.Control;
+import com.android.gallery3d.filtershow.controller.ParameterInteger;
 
-public class FilterBasicRepresentation extends FilterRepresentation {
+public class FilterBasicRepresentation extends FilterRepresentation implements ParameterInteger {
     private static final String LOGTAG = "FilterBasicRepresentation";
     private int mMinimum;
     private int mValue;
@@ -33,6 +35,7 @@
         setValue(value);
     }
 
+    @Override
     public String toString() {
         return getName() + " : " + mMinimum + " < " + mValue + " < " + mMaximum;
     }
@@ -47,6 +50,7 @@
         return representation;
     }
 
+    @Override
     public void useParametersFrom(FilterRepresentation a) {
         if (a instanceof FilterBasicRepresentation) {
             FilterBasicRepresentation representation = (FilterBasicRepresentation) a;
@@ -76,6 +80,7 @@
         return false;
     }
 
+    @Override
     public int getMinimum() {
         return mMinimum;
     }
@@ -84,10 +89,12 @@
         mMinimum = minimum;
     }
 
+    @Override
     public int getValue() {
         return mValue;
     }
 
+    @Override
     public void setValue(int value) {
         mValue = value;
         if (mValue < mMinimum) {
@@ -98,6 +105,7 @@
         }
     }
 
+    @Override
     public int getMaximum() {
         return mMaximum;
     }
@@ -110,6 +118,7 @@
         mDefaultValue = defaultValue;
     }
 
+    @Override
     public int getDefaultValue() {
         return mDefaultValue;
     }
@@ -122,7 +131,27 @@
         mPreviewValue = previewValue;
     }
 
+    @Override
     public String getStateRepresentation() {
         return "" + getValue();
     }
+
+    @Override
+    public String getParameterType(){
+        return sParameterType;
+    }
+
+    @Override
+    public void setController(Control control) {
+    }
+
+    @Override
+    public String getValueString() {
+        return getStateRepresentation();
+    }
+
+    @Override
+    public String getParameterName() {
+        return getName();
+    }
 }
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java
index 56e848a..2aeaed8 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java
@@ -29,22 +29,21 @@
     private static final String LOGTAG = "ImageFilterRS";
     private boolean DEBUG = false;
 
-    private static volatile RenderScript sRS = null;
-    private static volatile Resources sResources = null;
     private volatile boolean mResourcesLoaded = false;
 
-    // This must be used inside block synchronized on ImageFilterRS class object
     protected abstract void createFilter(android.content.res.Resources res,
             float scaleFactor, int quality);
 
-    // This must be used inside block synchronized on ImageFilterRS class object
     protected abstract void runFilter();
 
-    // This must be used inside block synchronized on ImageFilterRS class object
     protected void update(Bitmap bitmap) {
         getOutPixelsAllocation().copyTo(bitmap);
     }
 
+    protected RenderScript getRenderScriptContext() {
+        return CachingPipeline.getRenderScriptContext();
+    }
+
     protected Allocation getInPixelsAllocation() {
         CachingPipeline pipeline = getEnvironment().getCachingPipeline();
         return pipeline.getInPixelsAllocation();
@@ -61,27 +60,22 @@
             return bitmap;
         }
         try {
-            synchronized(ImageFilterRS.class) {
-                if (sRS == null)  {
-                    Log.w(LOGTAG, "Cannot apply before calling createRenderScriptContext");
-                    return bitmap;
-                }
-                CachingPipeline pipeline = getEnvironment().getCachingPipeline();
-                if (DEBUG) {
-                    Log.v(LOGTAG, "apply filter " + getName() + " in pipeline " + pipeline.getName());
-                }
-                boolean needsUpdate = pipeline.prepareRenderscriptAllocations(bitmap);
-                if (needsUpdate || !isResourcesLoaded()) {
-                    // the allocations changed size
-                    freeResources();
-                    createFilter(sResources, scaleFactor, quality);
-                    setResourcesLoaded(true);
-                }
-                runFilter();
-                update(bitmap);
-                if (DEBUG) {
-                    Log.v(LOGTAG, "DONE apply filter " + getName() + " in pipeline " + pipeline.getName());
-                }
+            CachingPipeline pipeline = getEnvironment().getCachingPipeline();
+            if (DEBUG) {
+                Log.v(LOGTAG, "apply filter " + getName() + " in pipeline " + pipeline.getName());
+            }
+            Resources rsc = pipeline.getResources();
+            if (pipeline.prepareRenderscriptAllocations(bitmap)
+                    || !isResourcesLoaded()) {
+                freeResources();
+                createFilter(rsc, scaleFactor, quality);
+                setResourcesLoaded(true);
+            }
+            bindScriptValues();
+            runFilter();
+            update(bitmap);
+            if (DEBUG) {
+                Log.v(LOGTAG, "DONE apply filter " + getName() + " in pipeline " + pipeline.getName());
             }
         } catch (android.renderscript.RSIllegalArgumentException e) {
             Log.e(LOGTAG, "Illegal argument? " + e);
@@ -96,53 +90,32 @@
         return bitmap;
     }
 
-    public static synchronized RenderScript getRenderScriptContext() {
-        return sRS;
-    }
-
-    public static synchronized void createRenderscriptContext(Activity context) {
-        if( sRS != null) {
-            Log.w(LOGTAG, "A prior RS context exists when calling setRenderScriptContext");
-            destroyRenderScriptContext();
-        }
-        sRS = RenderScript.create(context);
-        sResources = context.getResources();
-    }
-
-    public static synchronized void destroyRenderScriptContext() {
-        sRS.destroy();
-        sRS = null;
-        sResources = null;
-    }
-
-    private static synchronized Allocation convertBitmap(Bitmap bitmap) {
-        return Allocation.createFromBitmap(sRS, bitmap,
+    private static Allocation convertBitmap(Bitmap bitmap) {
+        return Allocation.createFromBitmap(CachingPipeline.getRenderScriptContext(), bitmap,
                 Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
     }
 
-    private static synchronized Allocation convertRGBAtoA(Bitmap bitmap) {
-        Type.Builder tb_a8 = new Type.Builder(sRS, Element.A_8(sRS));
-        ScriptC_grey greyConvert = new ScriptC_grey(sRS,
-                sRS.getApplicationContext().getResources(), R.raw.grey);
+    private static Allocation convertRGBAtoA(Bitmap bitmap) {
+        RenderScript RS = CachingPipeline.getRenderScriptContext();
+        Type.Builder tb_a8 = new Type.Builder(RS, Element.A_8(RS));
+        ScriptC_grey greyConvert = new ScriptC_grey(RS,
+                RS.getApplicationContext().getResources(), R.raw.grey);
 
         Allocation bitmapTemp = convertBitmap(bitmap);
-        if (bitmapTemp.getType().getElement().isCompatible(Element.A_8(sRS))) {
+        if (bitmapTemp.getType().getElement().isCompatible(Element.A_8(RS))) {
             return bitmapTemp;
         }
 
         tb_a8.setX(bitmapTemp.getType().getX());
         tb_a8.setY(bitmapTemp.getType().getY());
-        Allocation bitmapAlloc = Allocation.createTyped(sRS, tb_a8.create());
+        Allocation bitmapAlloc = Allocation.createTyped(RS, tb_a8.create());
         greyConvert.forEach_RGBAtoA(bitmapTemp, bitmapAlloc);
 
         return bitmapAlloc;
     }
 
     public Allocation loadScaledResourceAlpha(int resource, int inSampleSize) {
-        Resources res = null;
-        synchronized(ImageFilterRS.class) {
-            res = sRS.getApplicationContext().getResources();
-        }
+        Resources res = CachingPipeline.getResources();
         final BitmapFactory.Options options = new BitmapFactory.Options();
         options.inPreferredConfig = Bitmap.Config.ALPHA_8;
         options.inSampleSize      = inSampleSize;
@@ -159,10 +132,7 @@
     }
 
     public Allocation loadResource(int resource) {
-        Resources res = null;
-        synchronized(ImageFilterRS.class) {
-            res = sRS.getApplicationContext().getResources();
-        }
+        Resources res = CachingPipeline.getResources();
         final BitmapFactory.Options options = new BitmapFactory.Options();
         options.inPreferredConfig = Bitmap.Config.ARGB_8888;
         Bitmap bitmap = BitmapFactory.decodeResource(
@@ -191,13 +161,16 @@
      */
     abstract protected void resetScripts();
 
+    /**
+     * Scripts values should be bound here
+     */
+    abstract protected void bindScriptValues();
+
     public void freeResources() {
         if (!isResourcesLoaded()) {
             return;
         }
-        synchronized(ImageFilterRS.class) {
-            resetAllocations();
-            setResourcesLoaded(false);
-        }
+        resetAllocations();
+        setResourcesLoaded(false);
     }
 }
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java
index f545cd9..76ae475 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java
@@ -22,7 +22,6 @@
 
     private static final String LOGTAG = "ImageFilterSharpen";
     private ScriptC_convolve3x3 mScript;
-    float mScaleFactor;
 
     private FilterBasicRepresentation mParameters;
 
@@ -63,19 +62,14 @@
     @Override
     protected void createFilter(android.content.res.Resources res, float scaleFactor,
             int quality) {
-        int w = getInPixelsAllocation().getType().getX();
-        int h = getInPixelsAllocation().getType().getY();
-        mScaleFactor = scaleFactor;
-
         if (mScript == null) {
             mScript = new ScriptC_convolve3x3(getRenderScriptContext(), res, R.raw.convolve3x3);
         }
-        mScript.set_gWidth(w);
-        mScript.set_gHeight(h);
     }
 
     private void computeKernel() {
-        float p1 = mParameters.getValue() * mScaleFactor;
+        float scaleFactor = getEnvironment().getScaleFactor();
+        float p1 = mParameters.getValue() * scaleFactor;
         float value = p1 / 100.0f;
         float f[] = new float[9];
         float p = value;
@@ -92,6 +86,14 @@
     }
 
     @Override
+    protected void bindScriptValues() {
+        int w = getInPixelsAllocation().getType().getX();
+        int h = getInPixelsAllocation().getType().getY();
+        mScript.set_gWidth(w);
+        mScript.set_gHeight(h);
+    }
+
+    @Override
     protected void runFilter() {
         if (mParameters == null) {
             return;
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java b/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java
index 1149263..7ce9e51 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java
@@ -65,10 +65,6 @@
                     mActiveHandle = -1;
                     break;
                 case MotionEvent.ACTION_DOWN:
-                    if (event.getPointerCount() == 1) {
-                        Log.v(LOGTAG, "################### ACTION_DOWN odd " + mActiveHandle
-                                + " touches=1");
-                    }
                     break;
             }
         }
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageZoom.java b/src/com/android/gallery3d/filtershow/imageshow/ImageZoom.java
deleted file mode 100644
index eb568c3..0000000
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageZoom.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * 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.filtershow.imageshow;
-
-import android.content.Context;
-import android.graphics.*;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-
-import com.android.gallery3d.filtershow.cache.ImageLoader;
-
-public class ImageZoom extends ImageShow {
-    private static final String LOGTAG = "ImageZoom";
-    private boolean mTouchDown = false;
-    private boolean mZoomedIn = false;
-    private Rect mZoomBounds = null;
-    private static float mMaxSize = 512;
-
-    public ImageZoom(Context context) {
-        super(context);
-    }
-
-    public ImageZoom(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    public static void setZoomedSize(float size) {
-        mMaxSize = size;
-    }
-
-    @Override
-    public void resetParameter() {
-        super.resetParameter();
-        mZoomedIn = false;
-        mTouchDown = false;
-    }
-
-    public void onTouchDown(float x, float y) {
-        if (mZoomedIn || mTouchDown) {
-            return;
-        }
-        mTouchDown = true;
-        GeometryMetadata geo = getImagePreset().mGeoData;
-        Matrix originalToScreen = geo.getOriginalToScreen(true,
-                mImageLoader.getOriginalBounds().width(),
-                mImageLoader.getOriginalBounds().height(),
-                getWidth(), getHeight());
-        float[] point = new float[2];
-        point[0] = x;
-        point[1] = y;
-        Matrix inverse = new Matrix();
-        originalToScreen.invert(inverse);
-        inverse.mapPoints(point);
-
-        float ratio = (float) getWidth() / (float) getHeight();
-        float mh = mMaxSize;
-        float mw = ratio * mh;
-        Point touch = getTouchPoint();
-        RectF zoomRect = new RectF(touch.x - mw, touch.y - mh, touch.x + mw, touch.y + mw);
-        inverse.mapRect(zoomRect);
-        zoomRect.set(zoomRect.centerX() - mw, zoomRect.centerY() - mh,
-                zoomRect.centerX() + mw, zoomRect.centerY() + mh);
-        mZoomBounds = new Rect((int) zoomRect.left, (int) zoomRect.top,
-                (int) zoomRect.right, (int) zoomRect.bottom);
-        invalidate();
-    }
-
-    public void onTouchUp() {
-        mTouchDown = false;
-    }
-
-    @Override
-    public void onDraw(Canvas canvas) {
-        drawBackground(canvas);
-
-        Bitmap filteredImage = null;
-        if ((mZoomedIn || mTouchDown) && mImageLoader != null) {
-            filteredImage = mImageLoader.getScaleOneImageForPreset(this, getImagePreset(),
-                    mZoomBounds, null, false);
-        } else {
-            filteredImage = getFilteredImage();
-        }
-        canvas.save();
-        if (mZoomedIn || mTouchDown) {
-            int orientation = ImageLoader.getZoomOrientation();
-            switch (orientation) {
-                case ImageLoader.ORI_ROTATE_90: {
-                    canvas.rotate(90, getWidth() / 2, getHeight() / 2);
-                    break;
-                }
-                case ImageLoader.ORI_ROTATE_270: {
-                    canvas.rotate(270, getWidth() / 2, getHeight() / 2);
-                    break;
-                }
-                case ImageLoader.ORI_TRANSPOSE: {
-                    canvas.rotate(90, getWidth() / 2, getHeight() / 2);
-                    canvas.scale(1,  -1);
-                    break;
-                }
-                case ImageLoader.ORI_TRANSVERSE: {
-                    canvas.rotate(270, getWidth() / 2, getHeight() / 2);
-                    canvas.scale(1,  -1);
-                    break;
-                }
-            }
-        }
-        drawImage(canvas, filteredImage);
-        canvas.restore();
-
-        drawToast(canvas);
-    }
-
-    @Override
-    public boolean onDoubleTap(MotionEvent event) {
-
-        if (!mZoomedIn) {
-            onTouchDown(event.getX(), event.getY());
-        } else {
-            onTouchUp();
-        }
-        mZoomedIn = !mZoomedIn;
-        invalidate();
-        return false;
-    }
-}
diff --git a/src/com/android/gallery3d/filtershow/presets/FilterEnvironment.java b/src/com/android/gallery3d/filtershow/presets/FilterEnvironment.java
index c450360..b474b84 100644
--- a/src/com/android/gallery3d/filtershow/presets/FilterEnvironment.java
+++ b/src/com/android/gallery3d/filtershow/presets/FilterEnvironment.java
@@ -77,4 +77,5 @@
     public void setCachingPipeline(CachingPipeline cachingPipeline) {
         mCachingPipeline = cachingPipeline;
     }
+
 }
diff --git a/src/com/android/gallery3d/filtershow/presets/ImagePreset.java b/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
index b75ac64..b2dd6b7 100644
--- a/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
+++ b/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
@@ -22,6 +22,7 @@
 
 import com.android.gallery3d.filtershow.ImageStateAdapter;
 import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.filters.BaseFiltersManager;
 import com.android.gallery3d.filtershow.filters.FilterRepresentation;
 import com.android.gallery3d.filtershow.filters.FiltersManager;
 import com.android.gallery3d.filtershow.filters.ImageFilter;
@@ -35,11 +36,9 @@
     private static final String LOGTAG = "ImagePreset";
 
     private FilterRepresentation mBorder = null;
-    private float mScaleFactor = 1.0f;
     public static final int QUALITY_ICON = 0;
     public static final int QUALITY_PREVIEW = 1;
     public static final int QUALITY_FINAL = 2;
-    private int mQuality = QUALITY_PREVIEW;
     private ImageLoader mImageLoader = null;
 
     private Vector<FilterRepresentation> mFilters = new Vector<FilterRepresentation>();
@@ -57,8 +56,6 @@
 
     private Bitmap mPreviewImage;
 
-    private FilterEnvironment mEnvironment = new FilterEnvironment();
-
     public ImagePreset() {
         setup();
     }
@@ -412,42 +409,31 @@
         // do nothing here
     }
 
-    public void setupEnvironment() {
-        setupEnvironment(FiltersManager.getManager());
-    }
-
-    public void setupEnvironment(FiltersManager filtersManager) {
-        getEnvironment().setImagePreset(this);
-        getEnvironment().setScaleFactor(mScaleFactor);
-        getEnvironment().setQuality(mQuality);
-        getEnvironment().setFiltersManager(filtersManager);
-    }
-
-    public Bitmap apply(Bitmap original) {
+    public Bitmap apply(Bitmap original, FilterEnvironment environment) {
         Bitmap bitmap = original;
-        bitmap = applyFilters(bitmap, -1, -1);
-        return applyBorder(bitmap);
+        bitmap = applyFilters(bitmap, -1, -1, environment);
+        return applyBorder(bitmap, environment);
     }
 
-    public Bitmap applyGeometry(Bitmap bitmap) {
+    public Bitmap applyGeometry(Bitmap bitmap, FilterEnvironment environment) {
         // Apply any transform -- 90 rotate, flip, straighten, crop
         // Returns a new bitmap.
         if (mDoApplyGeometry) {
             mGeoData.synchronizeRepresentation();
-            bitmap = mEnvironment.applyRepresentation(mGeoData, bitmap);
+            bitmap = environment.applyRepresentation(mGeoData, bitmap);
         }
         return bitmap;
     }
 
-    public Bitmap applyBorder(Bitmap bitmap) {
+    public Bitmap applyBorder(Bitmap bitmap, FilterEnvironment environment) {
         if (mBorder != null && mDoApplyGeometry) {
             mBorder.synchronizeRepresentation();
-            bitmap = mEnvironment.applyRepresentation(mBorder, bitmap);
+            bitmap = environment.applyRepresentation(mBorder, bitmap);
         }
         return bitmap;
     }
 
-    public Bitmap applyFilters(Bitmap bitmap, int from, int to) {
+    public Bitmap applyFilters(Bitmap bitmap, int from, int to, FilterEnvironment environment) {
         if (mDoApplyFilters) {
             if (from < 0) {
                 from = 0;
@@ -461,7 +447,7 @@
                     representation = mFilters.elementAt(i);
                     representation.synchronizeRepresentation();
                 }
-                bitmap = mEnvironment.applyRepresentation(representation, bitmap);
+                bitmap = environment.applyRepresentation(representation, bitmap);
             }
         }
 
@@ -500,22 +486,6 @@
         imageStateAdapter.notifyDataSetChanged();
     }
 
-    public float getScaleFactor() {
-        return mScaleFactor;
-    }
-
-    public int getQuality() {
-        return mQuality;
-    }
-
-    public void setQuality(int value) {
-        mQuality = value;
-    }
-
-    public void setScaleFactor(float value) {
-        mScaleFactor = value;
-    }
-
     public void setPartialRendering(boolean partialRendering, Rect bounds) {
         mPartialRendering = partialRendering;
         mPartialRenderingBounds = bounds;
@@ -537,18 +507,14 @@
         mPreviewImage = previewImage;
     }
 
-    public Vector<ImageFilter> getUsedFilters() {
+    public Vector<ImageFilter> getUsedFilters(BaseFiltersManager filtersManager) {
         Vector<ImageFilter> usedFilters = new Vector<ImageFilter>();
         for (int i = 0; i < mFilters.size(); i++) {
             FilterRepresentation representation = mFilters.elementAt(i);
-            FiltersManager filtersManager = getEnvironment().getFiltersManager();
             ImageFilter filter = filtersManager.getFilterForRepresentation(representation);
             usedFilters.add(filter);
         }
         return usedFilters;
     }
 
-    public FilterEnvironment getEnvironment() {
-        return mEnvironment;
-    }
 }
diff --git a/src/com/android/photos/views/HeaderGridView.java b/src/com/android/photos/views/HeaderGridView.java
new file mode 100644
index 0000000..45a5eaf
--- /dev/null
+++ b/src/com/android/photos/views/HeaderGridView.java
@@ -0,0 +1,466 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.photos.views;
+
+import android.content.Context;
+import android.database.DataSetObservable;
+import android.database.DataSetObserver;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.FrameLayout;
+import android.widget.GridView;
+import android.widget.ListAdapter;
+import android.widget.WrapperListAdapter;
+
+import java.util.ArrayList;
+
+/**
+ * A {@link GridView} that supports adding header rows in a
+ * very similar way to {@link ListView}.
+ * See {@link HeaderGridView#addHeaderView(View, Object, boolean)}
+ */
+public class HeaderGridView extends GridView {
+    private static final String TAG = "HeaderGridView";
+
+    /**
+     * A class that represents a fixed view in a list, for example a header at the top
+     * or a footer at the bottom.
+     */
+    private static class FixedViewInfo {
+        /** The view to add to the grid */
+        public View view;
+        public ViewGroup viewContainer;
+        /** The data backing the view. This is returned from {@link ListAdapter#getItem(int)}. */
+        public Object data;
+        /** <code>true</code> if the fixed view should be selectable in the grid */
+        public boolean isSelectable;
+    }
+
+    private ArrayList<FixedViewInfo> mHeaderViewInfos = new ArrayList<FixedViewInfo>();
+
+    private void initHeaderGridView() {
+        super.setClipChildren(false);
+    }
+
+    public HeaderGridView(Context context) {
+        super(context);
+        initHeaderGridView();
+    }
+
+    public HeaderGridView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        initHeaderGridView();
+    }
+
+    public HeaderGridView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        initHeaderGridView();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        ListAdapter adapter = getAdapter();
+        if (adapter != null && adapter instanceof HeaderViewGridAdapter) {
+            ((HeaderViewGridAdapter) adapter).setNumColumns(getNumColumns());
+        }
+    }
+
+    @Override
+    public void setClipChildren(boolean clipChildren) {
+       // Ignore, since the header rows depend on not being clipped
+    }
+
+    /**
+     * Add a fixed view to appear at the top of the grid. If addHeaderView is
+     * called more than once, the views will appear in the order they were
+     * added. Views added using this call can take focus if they want.
+     * <p>
+     * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap
+     * the supplied cursor with one that will also account for header views.
+     *
+     * @param v The view to add.
+     * @param data Data to associate with this view
+     * @param isSelectable whether the item is selectable
+     */
+    public void addHeaderView(View v, Object data, boolean isSelectable) {
+        ListAdapter adapter = getAdapter();
+
+        if (adapter != null && ! (adapter instanceof HeaderViewGridAdapter)) {
+            throw new IllegalStateException(
+                    "Cannot add header view to grid -- setAdapter has already been called.");
+        }
+
+        FixedViewInfo info = new FixedViewInfo();
+        FrameLayout fl = new FullWidthFixedViewLayout(getContext());
+        fl.addView(v);
+        info.view = v;
+        info.viewContainer = fl;
+        info.data = data;
+        info.isSelectable = isSelectable;
+        mHeaderViewInfos.add(info);
+
+        // in the case of re-adding a header view, or adding one later on,
+        // we need to notify the observer
+        if (adapter != null) {
+            ((HeaderViewGridAdapter) adapter).notifyDataSetChanged();
+        }
+    }
+
+    /**
+     * Add a fixed view to appear at the top of the grid. If addHeaderView is
+     * called more than once, the views will appear in the order they were
+     * added. Views added using this call can take focus if they want.
+     * <p>
+     * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap
+     * the supplied cursor with one that will also account for header views.
+     *
+     * @param v The view to add.
+     */
+    public void addHeaderView(View v) {
+        addHeaderView(v, null, true);
+    }
+
+    public int getHeaderViewCount() {
+        return mHeaderViewInfos.size();
+    }
+
+    /**
+     * Removes a previously-added header view.
+     *
+     * @param v The view to remove
+     * @return true if the view was removed, false if the view was not a header
+     *         view
+     */
+    public boolean removeHeaderView(View v) {
+        if (mHeaderViewInfos.size() > 0) {
+            boolean result = false;
+            ListAdapter adapter = getAdapter();
+            if (adapter != null && ((HeaderViewGridAdapter) adapter).removeHeader(v)) {
+                result = true;
+            }
+            removeFixedViewInfo(v, mHeaderViewInfos);
+            return result;
+        }
+        return false;
+    }
+
+    private void removeFixedViewInfo(View v, ArrayList<FixedViewInfo> where) {
+        int len = where.size();
+        for (int i = 0; i < len; ++i) {
+            FixedViewInfo info = where.get(i);
+            if (info.view == v) {
+                where.remove(i);
+                break;
+            }
+        }
+    }
+
+    @Override
+    public void setAdapter(ListAdapter adapter) {
+        if (mHeaderViewInfos.size() > 0) {
+            HeaderViewGridAdapter hadapter = new HeaderViewGridAdapter(mHeaderViewInfos, adapter);
+            int numColumns = getNumColumns();
+            if (numColumns > 1) {
+                hadapter.setNumColumns(numColumns);
+            }
+            super.setAdapter(hadapter);
+        } else {
+            super.setAdapter(adapter);
+        }
+    }
+
+    private class FullWidthFixedViewLayout extends FrameLayout {
+        public FullWidthFixedViewLayout(Context context) {
+            super(context);
+        }
+
+        @Override
+        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+            int targetWidth = HeaderGridView.this.getMeasuredWidth()
+                    - HeaderGridView.this.getPaddingLeft()
+                    - HeaderGridView.this.getPaddingRight();
+            widthMeasureSpec = MeasureSpec.makeMeasureSpec(targetWidth,
+                    MeasureSpec.getMode(widthMeasureSpec));
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        }
+    }
+
+    /**
+     * ListAdapter used when a HeaderGridView has header views. This ListAdapter
+     * wraps another one and also keeps track of the header views and their
+     * associated data objects.
+     *<p>This is intended as a base class; you will probably not need to
+     * use this class directly in your own code.
+     */
+    private static class HeaderViewGridAdapter implements WrapperListAdapter, Filterable {
+
+        // This is used to notify the container of updates relating to number of columns
+        // or headers changing, which changes the number of placeholders needed
+        private final DataSetObservable mDataSetObservable = new DataSetObservable();
+
+        private final ListAdapter mAdapter;
+        private int mNumColumns = 1;
+
+        // This ArrayList is assumed to NOT be null.
+        ArrayList<FixedViewInfo> mHeaderViewInfos;
+
+        boolean mAreAllFixedViewsSelectable;
+
+        private final boolean mIsFilterable;
+
+        public HeaderViewGridAdapter(ArrayList<FixedViewInfo> headerViewInfos, ListAdapter adapter) {
+            mAdapter = adapter;
+            mIsFilterable = adapter instanceof Filterable;
+
+            if (headerViewInfos == null) {
+                throw new IllegalArgumentException("headerViewInfos cannot be null");
+            }
+            mHeaderViewInfos = headerViewInfos;
+
+            mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos);
+        }
+
+        public int getHeadersCount() {
+            return mHeaderViewInfos.size();
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return (mAdapter == null || mAdapter.isEmpty()) && getHeadersCount() == 0;
+        }
+
+        public void setNumColumns(int numColumns) {
+            if (numColumns < 1) {
+                throw new IllegalArgumentException("Number of columns must be 1 or more");
+            }
+            if (mNumColumns != numColumns) {
+                mNumColumns = numColumns;
+                notifyDataSetChanged();
+            }
+        }
+
+        private boolean areAllListInfosSelectable(ArrayList<FixedViewInfo> infos) {
+            if (infos != null) {
+                for (FixedViewInfo info : infos) {
+                    if (!info.isSelectable) {
+                        return false;
+                    }
+                }
+            }
+            return true;
+        }
+
+        public boolean removeHeader(View v) {
+            for (int i = 0; i < mHeaderViewInfos.size(); i++) {
+                FixedViewInfo info = mHeaderViewInfos.get(i);
+                if (info.view == v) {
+                    mHeaderViewInfos.remove(i);
+
+                    mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos);
+
+                    mDataSetObservable.notifyChanged();
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        @Override
+        public int getCount() {
+            if (mAdapter != null) {
+                return getHeadersCount() * mNumColumns + mAdapter.getCount();
+            } else {
+                return getHeadersCount() * mNumColumns;
+            }
+        }
+
+        @Override
+        public boolean areAllItemsEnabled() {
+            if (mAdapter != null) {
+                return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled();
+            } else {
+                return true;
+            }
+        }
+
+        @Override
+        public boolean isEnabled(int position) {
+            // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
+            int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
+            if (position < numHeadersAndPlaceholders) {
+                return (position % mNumColumns == 0)
+                        && mHeaderViewInfos.get(position / mNumColumns).isSelectable;
+            }
+
+            // Adapter
+            final int adjPosition = position - numHeadersAndPlaceholders;
+            int adapterCount = 0;
+            if (mAdapter != null) {
+                adapterCount = mAdapter.getCount();
+                if (adjPosition < adapterCount) {
+                    return mAdapter.isEnabled(adjPosition);
+                }
+            }
+
+            throw new ArrayIndexOutOfBoundsException(position);
+        }
+
+        @Override
+        public Object getItem(int position) {
+            // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
+            int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
+            if (position < numHeadersAndPlaceholders) {
+                if (position % mNumColumns == 0) {
+                    return mHeaderViewInfos.get(position / mNumColumns).data;
+                }
+                return null;
+            }
+
+            // Adapter
+            final int adjPosition = position - numHeadersAndPlaceholders;
+            int adapterCount = 0;
+            if (mAdapter != null) {
+                adapterCount = mAdapter.getCount();
+                if (adjPosition < adapterCount) {
+                    return mAdapter.getItem(adjPosition);
+                }
+            }
+
+            throw new ArrayIndexOutOfBoundsException(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
+            if (mAdapter != null && position >= numHeadersAndPlaceholders) {
+                int adjPosition = position - numHeadersAndPlaceholders;
+                int adapterCount = mAdapter.getCount();
+                if (adjPosition < adapterCount) {
+                    return mAdapter.getItemId(adjPosition);
+                }
+            }
+            return -1;
+        }
+
+        @Override
+        public boolean hasStableIds() {
+            if (mAdapter != null) {
+                return mAdapter.hasStableIds();
+            }
+            return false;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
+            int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns ;
+            if (position < numHeadersAndPlaceholders) {
+                View headerViewContainer = mHeaderViewInfos
+                        .get(position / mNumColumns).viewContainer;
+                if (position % mNumColumns == 0) {
+                    return headerViewContainer;
+                } else {
+                    if (convertView == null) {
+                        convertView = new View(parent.getContext());
+                    }
+                    // We need to do this because GridView uses the height of the last item
+                    // in a row to determine the height for the entire row.
+                    convertView.setVisibility(View.INVISIBLE);
+                    convertView.setMinimumHeight(headerViewContainer.getHeight());
+                    return convertView;
+                }
+            }
+
+            // Adapter
+            final int adjPosition = position - numHeadersAndPlaceholders;
+            int adapterCount = 0;
+            if (mAdapter != null) {
+                adapterCount = mAdapter.getCount();
+                if (adjPosition < adapterCount) {
+                    return mAdapter.getView(adjPosition, convertView, parent);
+                }
+            }
+
+            throw new ArrayIndexOutOfBoundsException(position);
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
+            if (position < numHeadersAndPlaceholders && (position % mNumColumns != 0)) {
+                // Placeholders get the last view type number
+                return mAdapter != null ? mAdapter.getViewTypeCount() : 1;
+            }
+            if (mAdapter != null && position >= numHeadersAndPlaceholders) {
+                int adjPosition = position - numHeadersAndPlaceholders;
+                int adapterCount = mAdapter.getCount();
+                if (adjPosition < adapterCount) {
+                    return mAdapter.getItemViewType(adjPosition);
+                }
+            }
+
+            return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
+        }
+
+        @Override
+        public int getViewTypeCount() {
+            if (mAdapter != null) {
+                return mAdapter.getViewTypeCount() + 1;
+            }
+            return 2;
+        }
+
+        @Override
+        public void registerDataSetObserver(DataSetObserver observer) {
+            mDataSetObservable.registerObserver(observer);
+            if (mAdapter != null) {
+                mAdapter.registerDataSetObserver(observer);
+            }
+        }
+
+        @Override
+        public void unregisterDataSetObserver(DataSetObserver observer) {
+            mDataSetObservable.unregisterObserver(observer);
+            if (mAdapter != null) {
+                mAdapter.unregisterDataSetObserver(observer);
+            }
+        }
+
+        @Override
+        public Filter getFilter() {
+            if (mIsFilterable) {
+                return ((Filterable) mAdapter).getFilter();
+            }
+            return null;
+        }
+
+        @Override
+        public ListAdapter getWrappedAdapter() {
+            return mAdapter;
+        }
+
+        public void notifyDataSetChanged() {
+            mDataSetObservable.notifyChanged();
+        }
+    }
+}