Add background processing service

bug:7298624

Change-Id: Ie79f88fd84fdf8f4dab6a8071f06a819e247b357
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 68780f7..ef1d914 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -219,6 +219,11 @@
             android:grantUriPermissions="true"
             android:readPermission="com.android.gallery3d.filtershow.permission.READ"
             android:writePermission="com.android.gallery3d.filtershow.permission.WRITE" />
+
+        <service
+                android:name=".filtershow.pipeline.ProcessingService"
+                android:exported="false" />
+
         <activity
             android:name="com.android.gallery3d.filtershow.FilterShowActivity"
             android:label="@string/title_activity_filter_show"
diff --git a/res/values/filtershow_strings.xml b/res/values/filtershow_strings.xml
index e142c0a..0a33108 100644
--- a/res/values/filtershow_strings.xml
+++ b/res/values/filtershow_strings.xml
@@ -199,4 +199,9 @@
     <!--  Name used to indicate the final image in the state panel [CHAR LIMIT=20] -->
     <string name="state_panel_result">Result</string>
 
+    <!-- Label for the notification [CHAR LIMIT=50] -->
+    <string name="filtershow_notification_label">Saving Image</string>
+    <!-- Label for the notification message [CHAR LIMIT=50] -->
+    <string name="filtershow_notification_message">Processing...</string>
+
 </resources>
diff --git a/src/com/android/gallery3d/data/LocalImage.java b/src/com/android/gallery3d/data/LocalImage.java
index a7c98af..eb1efae 100644
--- a/src/com/android/gallery3d/data/LocalImage.java
+++ b/src/com/android/gallery3d/data/LocalImage.java
@@ -37,7 +37,7 @@
 import com.android.gallery3d.common.BitmapUtils;
 import com.android.gallery3d.exif.ExifInterface;
 import com.android.gallery3d.exif.ExifTag;
-import com.android.gallery3d.filtershow.tools.SaveCopyTask;
+import com.android.gallery3d.filtershow.tools.SaveImage;
 import com.android.gallery3d.util.GalleryUtils;
 import com.android.gallery3d.util.ThreadPool.Job;
 import com.android.gallery3d.util.ThreadPool.JobContext;
@@ -270,7 +270,7 @@
         GalleryUtils.assertNotInRenderThread();
         Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
         ContentResolver contentResolver = mApplication.getContentResolver();
-        SaveCopyTask.deleteAuxFiles(contentResolver, getContentUri());
+        SaveImage.deleteAuxFiles(contentResolver, getContentUri());
         contentResolver.delete(baseUri, "_id=?",
                 new String[]{String.valueOf(id)});
     }
diff --git a/src/com/android/gallery3d/filtershow/FilterShowActivity.java b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
index bee764b..41d1784 100644
--- a/src/com/android/gallery3d/filtershow/FilterShowActivity.java
+++ b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
@@ -19,28 +19,29 @@
 import android.app.ActionBar;
 import android.app.AlertDialog;
 import android.app.ProgressDialog;
+import android.content.ComponentName;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.content.ServiceConnection;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
-import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.IBinder;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentActivity;
 import android.support.v4.app.FragmentTransaction;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.TypedValue;
-import android.view.Display;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
@@ -55,20 +56,19 @@
 import android.widget.Toast;
 
 import com.android.gallery3d.R;
+import com.android.gallery3d.app.PhotoPage;
 import com.android.gallery3d.data.LocalAlbum;
 import com.android.gallery3d.filtershow.pipeline.CachingPipeline;
 import com.android.gallery3d.filtershow.pipeline.FilteringPipeline;
 import com.android.gallery3d.filtershow.cache.ImageLoader;
 import com.android.gallery3d.filtershow.category.Action;
 import com.android.gallery3d.filtershow.category.CategoryAdapter;
-import com.android.gallery3d.filtershow.category.CategoryView;
 import com.android.gallery3d.filtershow.category.MainPanel;
 import com.android.gallery3d.filtershow.editors.BasicEditor;
 import com.android.gallery3d.filtershow.editors.Editor;
 import com.android.gallery3d.filtershow.editors.EditorCrop;
 import com.android.gallery3d.filtershow.editors.EditorDraw;
 import com.android.gallery3d.filtershow.editors.EditorFlip;
-import com.android.gallery3d.filtershow.editors.EditorInfo;
 import com.android.gallery3d.filtershow.editors.EditorManager;
 import com.android.gallery3d.filtershow.editors.EditorPanel;
 import com.android.gallery3d.filtershow.editors.EditorRedEye;
@@ -76,8 +76,6 @@
 import com.android.gallery3d.filtershow.editors.EditorStraighten;
 import com.android.gallery3d.filtershow.editors.EditorTinyPlanet;
 import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
-import com.android.gallery3d.filtershow.filters.FilterFxRepresentation;
-import com.android.gallery3d.filtershow.filters.FilterImageBorderRepresentation;
 import com.android.gallery3d.filtershow.filters.FilterRepresentation;
 import com.android.gallery3d.filtershow.filters.FiltersManager;
 import com.android.gallery3d.filtershow.filters.ImageFilter;
@@ -88,9 +86,10 @@
 import com.android.gallery3d.filtershow.imageshow.ImageShow;
 import com.android.gallery3d.filtershow.imageshow.MasterImage;
 import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+import com.android.gallery3d.filtershow.pipeline.ProcessingService;
 import com.android.gallery3d.filtershow.provider.SharedImageProvider;
 import com.android.gallery3d.filtershow.state.StateAdapter;
-import com.android.gallery3d.filtershow.tools.SaveCopyTask;
+import com.android.gallery3d.filtershow.tools.SaveImage;
 import com.android.gallery3d.filtershow.tools.XmpPresets;
 import com.android.gallery3d.filtershow.tools.XmpPresets.XMresults;
 import com.android.gallery3d.filtershow.ui.FramedTextButton;
@@ -101,6 +100,7 @@
 
 import java.io.File;
 import java.lang.ref.WeakReference;
+import java.util.ArrayList;
 import java.util.Vector;
 
 public class FilterShowActivity extends FragmentActivity implements OnItemClickListener,
@@ -149,6 +149,75 @@
     private CategoryAdapter mCategoryFiltersAdapter = null;
     private int mCurrentPanel = MainPanel.LOOKS;
 
+    private ProcessingService mBoundService;
+    private boolean mIsBound = false;
+
+    public ProcessingService getProcessingService() {
+        return mBoundService;
+    }
+
+    public boolean isSimpleEditAction() {
+        return !PhotoPage.ACTION_NEXTGEN_EDIT.equalsIgnoreCase(mAction);
+    }
+
+    private ServiceConnection mConnection = new ServiceConnection() {
+        public void onServiceConnected(ComponentName className, IBinder service) {
+            /*
+             * This is called when the connection with the service has been
+             * established, giving us the service object we can use to
+             * interact with the service.  Because we have bound to a explicit
+             * service that we know is running in our own process, we can
+             * cast its IBinder to a concrete class and directly access it.
+             */
+            mBoundService = ((ProcessingService.LocalBinder)service).getService();
+            mBoundService.setFiltershowActivity(FilterShowActivity.this);
+            mBoundService.onStart();
+        }
+
+        public void onServiceDisconnected(ComponentName className) {
+            /*
+             * This is called when the connection with the service has been
+             * unexpectedly disconnected -- that is, its process crashed.
+             * Because it is running in our same process, we should never
+             * see this happen.
+             */
+            mBoundService = null;
+        }
+    };
+
+    void doBindService() {
+        /*
+         * Establish a connection with the service.  We use an explicit
+         * class name because we want a specific service implementation that
+         * we know will be running in our own process (and thus won't be
+         * supporting component replacement by other applications).
+         */
+        bindService(new Intent(FilterShowActivity.this, ProcessingService.class),
+                mConnection, Context.BIND_AUTO_CREATE);
+        mIsBound = true;
+    }
+
+    void doUnbindService() {
+        if (mIsBound) {
+            // Detach our existing connection.
+            unbindService(mConnection);
+            mIsBound = false;
+        }
+    }
+
+    private void setupPipeline() {
+        doBindService();
+        ImageFilter.setActivityForMemoryToasts(this);
+    }
+
+    public void updateUIAfterServiceStarted() {
+        fillCategories();
+        loadMainPanel();
+        setDefaultPreset();
+        extractXMPData();
+        processIntent();
+    }
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -160,19 +229,13 @@
         MasterImage.setMaster(mMasterImage);
 
         clearGalleryBitmapPool();
+        setupPipeline();
 
-        CachingPipeline.createRenderscriptContext(this);
         setupMasterImage();
         setDefaultValues();
         fillEditors();
 
         loadXML();
-        loadMainPanel();
-
-        setDefaultPreset();
-
-        extractXMPData();
-        processIntent();
         UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_EDITOR, "Main");
         UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
                 UsageStatistics.CATEGORY_LIFECYCLE, UsageStatistics.LIFECYCLE_START);
@@ -251,26 +314,25 @@
         setupEditors();
 
         mEditorPlaceHolder.hide();
-
         mImageShow.bindAsImageLoadListener();
 
-        fillFx();
-        fillBorders();
-        fillGeometry();
-        fillFilters();
-
         setupStatePanel();
     }
 
+    public void fillCategories() {
+        fillLooks();
+        fillBorders();
+        fillTools();
+        fillEffects();
+    }
+
     public void setupStatePanel() {
         MasterImage.getImage().setHistoryManager(mMasterImage.getHistory());
     }
 
-    private void fillFilters() {
-        Vector<FilterRepresentation> filtersRepresentations = new Vector<FilterRepresentation>();
+    private void fillEffects() {
         FiltersManager filtersManager = FiltersManager.getManager();
-        filtersManager.addEffects(filtersRepresentations);
-
+        ArrayList<FilterRepresentation> filtersRepresentations = filtersManager.getEffects();
         mCategoryFiltersAdapter = new CategoryAdapter(this);
         for (FilterRepresentation representation : filtersRepresentations) {
             if (representation.getTextId() != 0) {
@@ -280,28 +342,9 @@
         }
     }
 
-    private void fillGeometry() {
-        Vector<FilterRepresentation> filtersRepresentations = new Vector<FilterRepresentation>();
+    private void fillTools() {
         FiltersManager filtersManager = FiltersManager.getManager();
-
-        GeometryMetadata geo = new GeometryMetadata();
-        int[] editorsId = geo.getEditorIds();
-        for (int i = 0; i < editorsId.length; i++) {
-            int editorId = editorsId[i];
-            GeometryMetadata geometry = new GeometryMetadata(geo);
-            geometry.setEditorId(editorId);
-            EditorInfo editorInfo = (EditorInfo) mEditorPlaceHolder.getEditor(editorId);
-            geometry.setTextId(editorInfo.getTextId());
-            geometry.setOverlayId(editorInfo.getOverlayId());
-            geometry.setOverlayOnly(editorInfo.getOverlayOnly());
-            if (geometry.getTextId() != 0) {
-                geometry.setName(getString(geometry.getTextId()));
-            }
-            filtersRepresentations.add(geometry);
-        }
-
-        filtersManager.addTools(filtersRepresentations);
-
+        ArrayList<FilterRepresentation> filtersRepresentations = filtersManager.getTools();
         mCategoryGeometryAdapter = new CategoryAdapter(this);
         for (FilterRepresentation representation : filtersRepresentations) {
             mCategoryGeometryAdapter.add(new Action(this, representation));
@@ -331,7 +374,6 @@
         mEditorPlaceHolder.setContainer((FrameLayout) findViewById(R.id.editorContainer));
         EditorManager.addEditors(mEditorPlaceHolder);
         mEditorPlaceHolder.setOldViews(mImageViews);
-
     }
 
     private void fillEditors() {
@@ -347,10 +389,7 @@
     }
 
     private void setDefaultValues() {
-        ImageFilter.setActivityForMemoryToasts(this);
-
         Resources res = getResources();
-        FiltersManager.setResources(res);
 
         // TODO: get those values from XML.
         FramedTextButton.setTextSize((int) getPixelsFromDip(14));
@@ -379,16 +418,11 @@
     }
 
     private void fillBorders() {
-        Vector<FilterRepresentation> borders = new Vector<FilterRepresentation>();
-
-        // The "no border" implementation
-        borders.add(new FilterImageBorderRepresentation(0));
-
-        // Google-build borders
-        FiltersManager.getManager().addBorders(this, borders);
+        FiltersManager filtersManager = FiltersManager.getManager();
+        ArrayList<FilterRepresentation> borders = filtersManager.getBorders();
 
         for (int i = 0; i < borders.size(); i++) {
-            FilterRepresentation filter = borders.elementAt(i);
+            FilterRepresentation filter = borders.get(i);
             filter.setName(getString(R.string.borders));
             if (i == 0) {
                 filter.setName(getString(R.string.none));
@@ -628,16 +662,7 @@
         if (mLoadBitmapTask != null) {
             mLoadBitmapTask.cancel(false);
         }
-        // TODO:  refactor, don't use so many singletons.
-        FilteringPipeline.getPipeline().turnOnPipeline(false);
-        MasterImage.reset();
-        FilteringPipeline.reset();
-        ImageFilter.resetStatics();
-        FiltersManager.getPreviewManager().freeRSFilterScripts();
-        FiltersManager.getManager().freeRSFilterScripts();
-        FiltersManager.getHighresManager().freeRSFilterScripts();
-        FiltersManager.reset();
-        CachingPipeline.destroyRenderScriptContext();
+        doUnbindService();
         super.onDestroy();
     }
 
@@ -713,7 +738,7 @@
         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
         intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
         intent.setType(SharedImageProvider.MIME_TYPE);
-        mSharedOutputFile = SaveCopyTask.getNewFile(this, MasterImage.getImage().getUri());
+        mSharedOutputFile = SaveImage.getNewFile(this, MasterImage.getImage().getUri());
         Uri uri = Uri.withAppendedPath(SharedImageProvider.CONTENT_URI,
                 Uri.encode(mSharedOutputFile.getAbsolutePath()));
         intent.putExtra(Intent.EXTRA_STREAM, uri);
@@ -744,7 +769,6 @@
     @Override
     public void onPause() {
         super.onPause();
-        rsPause();
         if (mShareActionProvider != null) {
             mShareActionProvider.setOnShareTargetSelectedListener(null);
         }
@@ -753,48 +777,11 @@
     @Override
     public void onResume() {
         super.onResume();
-        rsResume();
         if (mShareActionProvider != null) {
             mShareActionProvider.setOnShareTargetSelectedListener(this);
         }
     }
 
-    private void rsResume() {
-        ImageFilter.setActivityForMemoryToasts(this);
-        MasterImage.setMaster(mMasterImage);
-        if (CachingPipeline.getRenderScriptContext() == null) {
-            CachingPipeline.createRenderscriptContext(this);
-        }
-        FiltersManager.setResources(getResources());
-        if (!mLoading) {
-            Bitmap largeBitmap = MasterImage.getImage().getOriginalBitmapLarge();
-            FilteringPipeline pipeline = FilteringPipeline.getPipeline();
-            pipeline.setOriginal(largeBitmap);
-            float previewScale = (float) largeBitmap.getWidth() /
-                    (float) MasterImage.getImage().getOriginalBounds().width();
-            pipeline.setPreviewScaleFactor(previewScale);
-            Bitmap highresBitmap = MasterImage.getImage().getOriginalBitmapHighres();
-            if (highresBitmap != null) {
-                float highResPreviewScale = (float) highresBitmap.getWidth() /
-                        (float) MasterImage.getImage().getOriginalBounds().width();
-                pipeline.setHighResPreviewScaleFactor(highResPreviewScale);
-            }
-            pipeline.turnOnPipeline(true);
-            MasterImage.getImage().setOriginalGeometry(largeBitmap);
-        }
-    }
-
-    private void rsPause() {
-        FilteringPipeline.getPipeline().turnOnPipeline(false);
-        FilteringPipeline.reset();
-        ImageFilter.resetStatics();
-        FiltersManager.getPreviewManager().freeRSFilterScripts();
-        FiltersManager.getManager().freeRSFilterScripts();
-        FiltersManager.getHighresManager().freeRSFilterScripts();
-        FiltersManager.reset();
-        CachingPipeline.destroyRenderScriptContext();
-    }
-
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getItemId()) {
@@ -844,16 +831,13 @@
         }
     }
 
-    private void fillFx() {
-        FilterFxRepresentation nullFx =
-                new FilterFxRepresentation(getString(R.string.none), 0, R.string.none);
-        Vector<FilterRepresentation> filtersRepresentations = new Vector<FilterRepresentation>();
-        FiltersManager.getManager().addLooks(this, filtersRepresentations);
+    private void fillLooks() {
+        FiltersManager filtersManager = FiltersManager.getManager();
+        ArrayList<FilterRepresentation> filtersRepresentations = filtersManager.getLooks();
 
         mCategoryLooksAdapter = new CategoryAdapter(this);
         int verticalItemHeight = (int) getResources().getDimension(R.dimen.action_item_height);
         mCategoryLooksAdapter.setItemHeight(verticalItemHeight);
-        mCategoryLooksAdapter.add(new Action(this, nullFx, Action.FULL_VIEW));
         for (FilterRepresentation representation : filtersRepresentations) {
             mCategoryLooksAdapter.add(new Action(this, representation, Action.FULL_VIEW));
         }
@@ -1030,7 +1014,7 @@
     public void saveImage() {
         if (mImageShow.hasModifications()) {
             // Get the name of the album, to which the image will be saved
-            File saveDir = SaveCopyTask.getFinalSaveDirectory(this, mSelectedImageUri);
+            File saveDir = SaveImage.getFinalSaveDirectory(this, mSelectedImageUri);
             int bucketId = GalleryUtils.getBucketId(saveDir.getPath());
             String albumName = LocalAlbum.getLocalizedName(getResources(), bucketId, null);
             showSavingProgress(albumName);
@@ -1063,9 +1047,4 @@
         return mSelectedImageUri;
     }
 
-    static {
-        System.loadLibrary("jni_filtershow_filters");
-    }
-
-
 }
diff --git a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
index 7c594c6..b6c72fd 100644
--- a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
+++ b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
@@ -18,6 +18,7 @@
 
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.Intent;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteException;
@@ -33,6 +34,7 @@
 
 import com.adobe.xmp.XMPException;
 import com.adobe.xmp.XMPMeta;
+import com.android.gallery3d.R;
 import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.exif.ExifInterface;
 import com.android.gallery3d.filtershow.imageshow.MasterImage;
@@ -286,17 +288,20 @@
      * @param uri URI of image to open.
      * @param context context whose ContentResolver to use.
      * @param maxSideLength max side length of returned bitmap.
-     * @param originalBounds set to the actual bounds of the stored bitmap.
+     * @param originalBounds If not null, set to the actual bounds of the stored bitmap.
+     * @param useMin use min or max side of the original image
      * @return downsampled bitmap or null if this operation failed.
      */
     public static Bitmap loadConstrainedBitmap(Uri uri, Context context, int maxSideLength,
-            Rect originalBounds) {
-        if (maxSideLength <= 0 || originalBounds == null || uri == null || context == null) {
+            Rect originalBounds, boolean useMin) {
+        if (maxSideLength <= 0 || uri == null || context == null) {
             throw new IllegalArgumentException("bad argument to getScaledBitmap");
         }
         // Get width and height of stored bitmap
         Rect storedBounds = loadBitmapBounds(context, uri);
-        originalBounds.set(storedBounds);
+        if (originalBounds != null) {
+            originalBounds.set(storedBounds);
+        }
         int w = storedBounds.width();
         int h = storedBounds.height();
 
@@ -306,7 +311,12 @@
         }
 
         // Find best downsampling size
-        int imageSide = Math.max(w, h);
+        int imageSide = 0;
+        if (useMin) {
+            imageSide = Math.min(w, h);
+        } else {
+            imageSide = Math.max(w, h);
+        }
         int sampleSize = 1;
         while (imageSide > maxSideLength) {
             imageSide >>>= 1;
@@ -336,7 +346,7 @@
      */
     public static Bitmap loadOrientedConstrainedBitmap(Uri uri, Context context, int maxSideLength,
             int orientation, Rect originalBounds) {
-        Bitmap bmap = loadConstrainedBitmap(uri, context, maxSideLength, originalBounds);
+        Bitmap bmap = loadConstrainedBitmap(uri, context, maxSideLength, originalBounds, false);
         if (bmap != null) {
             bmap = orientBitmap(bmap, orientation);
         }
diff --git a/src/com/android/gallery3d/filtershow/crop/CropActivity.java b/src/com/android/gallery3d/filtershow/crop/CropActivity.java
index d14c090..0a0c367 100644
--- a/src/com/android/gallery3d/filtershow/crop/CropActivity.java
+++ b/src/com/android/gallery3d/filtershow/crop/CropActivity.java
@@ -45,7 +45,7 @@
 import com.android.gallery3d.R;
 import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.filtershow.cache.ImageLoader;
-import com.android.gallery3d.filtershow.tools.SaveCopyTask;
+import com.android.gallery3d.filtershow.tools.SaveImage;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -264,7 +264,7 @@
         protected Bitmap doInBackground(Uri... params) {
             Uri uri = params[0];
             Bitmap bmap = ImageLoader.loadConstrainedBitmap(uri, mContext, mBitmapSize,
-                    mOriginalBounds);
+                    mOriginalBounds, false);
             mOrientation = ImageLoader.getMetadataRotation(mContext, uri);
             return bmap;
         }
@@ -299,7 +299,7 @@
             }
         }
         if (flags == 0) {
-            destinationUri = SaveCopyTask.makeAndInsertUri(this, mSourceUri);
+            destinationUri = SaveImage.makeAndInsertUri(this, mSourceUri);
             if (destinationUri != null) {
                 flags |= DO_EXTRA_OUTPUT;
             }
diff --git a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
index c0a6c13..1d3a10d 100644
--- a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
+++ b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
@@ -20,8 +20,10 @@
 import android.util.Log;
 
 import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.imageshow.GeometryMetadata;
 import com.android.gallery3d.filtershow.pipeline.ImagePreset;
 
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Vector;
 
@@ -30,6 +32,11 @@
     protected HashMap<String, FilterRepresentation> mRepresentationLookup = null;
     private static final String LOGTAG = "BaseFiltersManager";
 
+    protected ArrayList<FilterRepresentation> mLooks = new ArrayList<FilterRepresentation>();
+    protected ArrayList<FilterRepresentation> mBorders = new ArrayList<FilterRepresentation>();
+    protected ArrayList<FilterRepresentation> mTools = new ArrayList<FilterRepresentation>();
+    protected ArrayList<FilterRepresentation> mEffects = new ArrayList<FilterRepresentation>();
+
     protected void init() {
         mFilters = new HashMap<Class, ImageFilter>();
         mRepresentationLookup = new HashMap<String, FilterRepresentation>();
@@ -133,11 +140,27 @@
         filters.add(ImageFilterGeometry.class);
     }
 
-    public void addBorders(Context context, Vector<FilterRepresentation> representations) {
+    public ArrayList<FilterRepresentation> getLooks() {
+        return mLooks;
+    }
+
+    public ArrayList<FilterRepresentation> getBorders() {
+        return mBorders;
+    }
+
+    public ArrayList<FilterRepresentation> getTools() {
+        return mTools;
+    }
+
+    public ArrayList<FilterRepresentation> getEffects() {
+        return mEffects;
+    }
+
+    public void addBorders(Context context) {
 
     }
 
-    public void addLooks(Context context, Vector<FilterRepresentation> representations) {
+    public void addLooks(Context context) {
         int[] drawid = {
                 R.drawable.filtershow_fx_0005_punch,
                 R.drawable.filtershow_fx_0000_vintage,
@@ -175,37 +198,72 @@
                 "LUT3D_XPROCESS"
         };
 
+        FilterFxRepresentation nullFx =
+                new FilterFxRepresentation(context.getString(R.string.none),
+                        0, R.string.none);
+        mLooks.add(nullFx);
+
         for (int i = 0; i < drawid.length; i++) {
             FilterFxRepresentation fx = new FilterFxRepresentation(
                     context.getString(fxNameid[i]), drawid[i], fxNameid[i]);
             fx.setSerializationName(serializationNames[i]);
-            representations.add(fx);
+            mLooks.add(fx);
             addRepresentation(fx);
         }
     }
 
-    public void addEffects(Vector<FilterRepresentation> representations) {
-        representations.add(getRepresentation(ImageFilterTinyPlanet.class));
-        representations.add(getRepresentation(ImageFilterWBalance.class));
-        representations.add(getRepresentation(ImageFilterExposure.class));
-        representations.add(getRepresentation(ImageFilterVignette.class));
-        representations.add(getRepresentation(ImageFilterContrast.class));
-        representations.add(getRepresentation(ImageFilterShadows.class));
-        representations.add(getRepresentation(ImageFilterHighlights.class));
-        representations.add(getRepresentation(ImageFilterVibrance.class));
-        representations.add(getRepresentation(ImageFilterSharpen.class));
-        representations.add(getRepresentation(ImageFilterCurves.class));
-        representations.add(getRepresentation(ImageFilterHue.class));
-        representations.add(getRepresentation(ImageFilterSaturated.class));
-        representations.add(getRepresentation(ImageFilterBwFilter.class));
-        representations.add(getRepresentation(ImageFilterNegative.class));
-        representations.add(getRepresentation(ImageFilterEdge.class));
-        representations.add(getRepresentation(ImageFilterKMeans.class));
+    public void addEffects() {
+        mEffects.add(getRepresentation(ImageFilterTinyPlanet.class));
+        mEffects.add(getRepresentation(ImageFilterWBalance.class));
+        mEffects.add(getRepresentation(ImageFilterExposure.class));
+        mEffects.add(getRepresentation(ImageFilterVignette.class));
+        mEffects.add(getRepresentation(ImageFilterContrast.class));
+        mEffects.add(getRepresentation(ImageFilterShadows.class));
+        mEffects.add(getRepresentation(ImageFilterHighlights.class));
+        mEffects.add(getRepresentation(ImageFilterVibrance.class));
+        mEffects.add(getRepresentation(ImageFilterSharpen.class));
+        mEffects.add(getRepresentation(ImageFilterCurves.class));
+        mEffects.add(getRepresentation(ImageFilterHue.class));
+        mEffects.add(getRepresentation(ImageFilterSaturated.class));
+        mEffects.add(getRepresentation(ImageFilterBwFilter.class));
+        mEffects.add(getRepresentation(ImageFilterNegative.class));
+        mEffects.add(getRepresentation(ImageFilterEdge.class));
+        mEffects.add(getRepresentation(ImageFilterKMeans.class));
     }
 
-    public void addTools(Vector<FilterRepresentation> representations) {
-        representations.add(getRepresentation(ImageFilterRedEye.class));
-        representations.add(getRepresentation(ImageFilterDraw.class));
+    public void addTools(Context context) {
+        GeometryMetadata geo = new GeometryMetadata();
+        int[] editorsId = geo.getEditorIds();
+
+        int[] textId = {
+                R.string.crop,
+                R.string.straighten,
+                R.string.rotate,
+                R.string.mirror
+        };
+
+        int[] overlayId = {
+                R.drawable.filtershow_button_geometry_crop,
+                R.drawable.filtershow_button_geometry_straighten,
+                R.drawable.filtershow_button_geometry_rotate,
+                R.drawable.filtershow_button_geometry_flip
+        };
+
+        for (int i = 0; i < editorsId.length; i++) {
+            int editorId = editorsId[i];
+            GeometryMetadata geometry = new GeometryMetadata(geo);
+            geometry.setEditorId(editorId);
+            geometry.setTextId(textId[i]);
+            geometry.setOverlayId(overlayId[i]);
+            geometry.setOverlayOnly(true);
+            if (geometry.getTextId() != 0) {
+                geometry.setName(context.getString(geometry.getTextId()));
+            }
+            mTools.add(geometry);
+        }
+
+        mTools.add(getRepresentation(ImageFilterRedEye.class));
+        mTools.add(getRepresentation(ImageFilterDraw.class));
     }
 
     public void setFilterResources(Resources resources) {
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java b/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java
index 2da358c..1a304db 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java
@@ -40,7 +40,7 @@
 import com.android.gallery3d.filtershow.cache.ImageLoader;
 import com.android.gallery3d.filtershow.filters.ImageFilter;
 import com.android.gallery3d.filtershow.pipeline.ImagePreset;
-import com.android.gallery3d.filtershow.tools.SaveCopyTask;
+import com.android.gallery3d.filtershow.tools.SaveImage;
 
 import java.io.File;
 
@@ -395,7 +395,7 @@
     }
 
     public void saveImage(FilterShowActivity filterShowActivity, File file) {
-        SaveCopyTask.saveImage(getImagePreset(), filterShowActivity, file);
+        SaveImage.saveImage(getImagePreset(), filterShowActivity, file);
     }
 
 
diff --git a/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java
index ed09fb1..01fe3c1 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java
@@ -482,6 +482,9 @@
     }
 
     public void needsUpdatePartialPreview() {
+        if (mPreset == null) {
+            return;
+        }
         if (!mPreset.canDoPartialRendering()) {
             invalidatePartialPreview();
             return;
diff --git a/src/com/android/gallery3d/filtershow/pipeline/CachingPipeline.java b/src/com/android/gallery3d/filtershow/pipeline/CachingPipeline.java
index a7580a8..535d02f 100644
--- a/src/com/android/gallery3d/filtershow/pipeline/CachingPipeline.java
+++ b/src/com/android/gallery3d/filtershow/pipeline/CachingPipeline.java
@@ -16,7 +16,7 @@
 
 package com.android.gallery3d.filtershow.pipeline;
 
-import android.app.Activity;
+import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.support.v8.renderscript.Allocation;
@@ -68,7 +68,7 @@
         return sRS;
     }
 
-    public static synchronized void createRenderscriptContext(Activity context) {
+    public static synchronized void createRenderscriptContext(Context context) {
         if (sRS != null) {
             Log.w(LOGTAG, "A prior RS context exists when calling setRenderScriptContext");
             destroyRenderScriptContext();
diff --git a/src/com/android/gallery3d/filtershow/pipeline/FilteringPipeline.java b/src/com/android/gallery3d/filtershow/pipeline/FilteringPipeline.java
index a302b19..0e9b83d 100644
--- a/src/com/android/gallery3d/filtershow/pipeline/FilteringPipeline.java
+++ b/src/com/android/gallery3d/filtershow/pipeline/FilteringPipeline.java
@@ -21,7 +21,6 @@
 import android.os.Process;
 import android.util.Log;
 
-import com.android.gallery3d.filtershow.cache.ImageLoader;
 import com.android.gallery3d.filtershow.filters.FiltersManager;
 import com.android.gallery3d.filtershow.imageshow.MasterImage;
 
diff --git a/src/com/android/gallery3d/filtershow/pipeline/ImagePreset.java b/src/com/android/gallery3d/filtershow/pipeline/ImagePreset.java
index 78a4d21..2b9e370 100644
--- a/src/com/android/gallery3d/filtershow/pipeline/ImagePreset.java
+++ b/src/com/android/gallery3d/filtershow/pipeline/ImagePreset.java
@@ -55,7 +55,7 @@
 
     private boolean mPartialRendering = false;
     private Rect mPartialRenderingBounds;
-    private static final boolean DEBUG = true;
+    private static final boolean DEBUG = false;
 
     public ImagePreset() {
     }
@@ -607,7 +607,7 @@
                 if (DEBUG) {
                     Log.v(LOGTAG, "Serialization: " + sname);
                     if (sname == null) {
-                        Log.v(LOGTAG, "Serialization: " + filter);
+                        Log.v(LOGTAG, "Serialization name null for filter: " + filter);
                     }
                 }
                 writer.name(sname);
diff --git a/src/com/android/gallery3d/filtershow/pipeline/ImageSavingTask.java b/src/com/android/gallery3d/filtershow/pipeline/ImageSavingTask.java
new file mode 100644
index 0000000..e93ec16
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/ImageSavingTask.java
@@ -0,0 +1,123 @@
+/*
+ * 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.pipeline;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.tools.SaveImage;
+
+import java.io.File;
+
+public class ImageSavingTask extends ProcessingTask {
+    private ProcessingService mProcessingService;
+
+    static class SaveRequest implements Request {
+        Uri sourceUri;
+        Uri selectedUri;
+        File destinationFile;
+        ImagePreset preset;
+    }
+
+    static class UpdateBitmap implements Update {
+        Bitmap bitmap;
+    }
+
+    static class UpdateProgress implements Update {
+        int max;
+        int current;
+    }
+
+    static class URIResult implements Result {
+        Uri uri;
+    }
+
+    public ImageSavingTask(ProcessingService service) {
+        mProcessingService = service;
+    }
+
+    public void saveImage(Uri sourceUri, Uri selectedUri,
+                          File destinationFile, ImagePreset preset) {
+        SaveRequest request = new SaveRequest();
+        request.sourceUri = sourceUri;
+        request.selectedUri = selectedUri;
+        request.destinationFile = destinationFile;
+        request.preset = preset;
+        postRequest(request);
+    }
+
+    public Result doInBackground(Request message) {
+        SaveRequest request = (SaveRequest) message;
+        Uri sourceUri = request.sourceUri;
+        Uri selectedUri = request.selectedUri;
+        File destinationFile = request.destinationFile;
+        ImagePreset preset = request.preset;
+
+        // We create a small bitmap showing the result that we can
+        // give to the notification
+        UpdateBitmap updateBitmap = new UpdateBitmap();
+        updateBitmap.bitmap = createNotificationBitmap(sourceUri, preset);
+        postUpdate(updateBitmap);
+
+        SaveImage saveImage = new SaveImage(mProcessingService, sourceUri,
+                selectedUri, destinationFile,
+                new SaveImage.Callback() {
+                    @Override
+                    public void onProgress(int max, int current) {
+                        UpdateProgress updateProgress = new UpdateProgress();
+                        updateProgress.max = max;
+                        updateProgress.current = current;
+                        postUpdate(updateProgress);
+                    }
+                });
+
+        Uri uri = saveImage.processAndSaveImage(preset);
+        URIResult result = new URIResult();
+        result.uri = uri;
+        return result;
+    }
+
+    @Override
+    public void onResult(Result message) {
+        URIResult result = (URIResult) message;
+        mProcessingService.completeSaveImage(result.uri);
+    }
+
+    @Override
+    public void onUpdate(Update message) {
+        if (message instanceof UpdateBitmap) {
+            Bitmap bitmap = ((UpdateBitmap) message).bitmap;
+            mProcessingService.updateNotificationWithBitmap(bitmap);
+        }
+        if (message instanceof UpdateProgress) {
+            UpdateProgress progress = (UpdateProgress) message;
+            mProcessingService.updateProgress(progress.max, progress.current);
+        }
+    }
+
+    private Bitmap createNotificationBitmap(Uri sourceUri, ImagePreset preset) {
+        int notificationBitmapSize = Resources.getSystem().getDimensionPixelSize(
+                android.R.dimen.notification_large_icon_width);
+        Bitmap bitmap = ImageLoader.loadConstrainedBitmap(sourceUri, getContext(),
+                notificationBitmapSize, null, true);
+        CachingPipeline pipeline = new CachingPipeline(FiltersManager.getManager(), "Thumb");
+        return pipeline.renderFinalImage(bitmap, preset);
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/ProcessingService.java b/src/com/android/gallery3d/filtershow/pipeline/ProcessingService.java
new file mode 100644
index 0000000..0320247
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/ProcessingService.java
@@ -0,0 +1,217 @@
+/*
+ * 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.pipeline;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+import android.util.Log;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.filters.ImageFilter;
+import com.android.gallery3d.filtershow.tools.SaveImage;
+
+import java.io.File;
+
+public class ProcessingService extends Service {
+    private static final String LOGTAG = "ProcessingService";
+    private static final boolean SHOW_IMAGE = false;
+    private int mNotificationId;
+    private NotificationManager mNotifyMgr = null;
+    private Notification.Builder mBuilder = null;
+
+    private static final String PRESET = "preset";
+    private static final String SOURCE_URI = "sourceUri";
+    private static final String SELECTED_URI = "selectedUri";
+    private static final String DESTINATION_FILE = "destinationFile";
+    private static final String SAVING = "saving";
+
+    private ProcessingTaskController mProcessingTaskController;
+    private ImageSavingTask mImageSavingTask;
+
+    private final IBinder mBinder = new LocalBinder();
+    private FilterShowActivity mFiltershowActivity;
+
+    private boolean mSaving = false;
+    private boolean mNeedsAlive = false;
+
+    public void setFiltershowActivity(FilterShowActivity filtershowActivity) {
+        mFiltershowActivity = filtershowActivity;
+    }
+
+    public class LocalBinder extends Binder {
+        public ProcessingService getService() {
+            return ProcessingService.this;
+        }
+    }
+
+    public static Intent getSaveIntent(Context context, ImagePreset preset, File destination,
+                                        Uri selectedImageUri, Uri sourceImageUri) {
+        Intent processIntent = new Intent(context, ProcessingService.class);
+        processIntent.putExtra(ProcessingService.SOURCE_URI,
+                sourceImageUri.toString());
+        processIntent.putExtra(ProcessingService.SELECTED_URI,
+                selectedImageUri.toString());
+        if (destination != null) {
+            processIntent.putExtra(ProcessingService.DESTINATION_FILE, destination.toString());
+        }
+        processIntent.putExtra(ProcessingService.PRESET,
+                preset.getJsonString(context.getString(R.string.saved)));
+        processIntent.putExtra(ProcessingService.SAVING, true);
+        return processIntent;
+    }
+
+
+    @Override
+    public void onCreate() {
+        mProcessingTaskController = new ProcessingTaskController(this);
+        mImageSavingTask = new ImageSavingTask(this);
+        mProcessingTaskController.add(mImageSavingTask);
+        setupPipeline();
+    }
+
+    @Override
+    public void onDestroy() {
+        tearDownPipeline();
+        mProcessingTaskController.quit();
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        mNeedsAlive = true;
+        if (intent != null && intent.getBooleanExtra(SAVING, false)) {
+            // we save using an intent to keep the service around after the
+            // activity has been destroyed.
+            String presetJson = intent.getStringExtra(PRESET);
+            String source = intent.getStringExtra(SOURCE_URI);
+            String selected = intent.getStringExtra(SELECTED_URI);
+            String destination = intent.getStringExtra(DESTINATION_FILE);
+            Uri sourceUri = Uri.parse(source);
+            Uri selectedUri = null;
+            if (selected != null) {
+                selectedUri = Uri.parse(selected);
+            }
+            File destinationFile = null;
+            if (destination != null) {
+                destinationFile = new File(destination);
+            }
+            ImagePreset preset = new ImagePreset();
+            preset.readJsonFromString(presetJson);
+            mNeedsAlive = false;
+            mSaving = true;
+            handleSaveRequest(sourceUri, selectedUri, destinationFile, preset);
+        }
+        return START_REDELIVER_INTENT;
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mBinder;
+    }
+
+    public void onStart() {
+        mNeedsAlive = true;
+        if (!mSaving && mFiltershowActivity != null) {
+            mFiltershowActivity.updateUIAfterServiceStarted();
+        }
+    }
+
+    public void handleSaveRequest(Uri sourceUri, Uri selectedUri,
+                                  File destinationFile, ImagePreset preset) {
+        mNotifyMgr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+
+        mNotificationId++;
+
+        mBuilder =
+                new Notification.Builder(this)
+                        .setSmallIcon(R.drawable.filtershow_button_fx)
+                        .setContentTitle(getString(R.string.filtershow_notification_label))
+                        .setContentText(getString(R.string.filtershow_notification_message));
+
+        startForeground(mNotificationId, mBuilder.build());
+
+        updateProgress(SaveImage.MAX_PROCESSING_STEPS, 0);
+
+        // Process the image
+
+        mImageSavingTask.saveImage(sourceUri, selectedUri, destinationFile, preset);
+    }
+
+    public void updateNotificationWithBitmap(Bitmap bitmap) {
+        mBuilder.setLargeIcon(bitmap);
+        mNotifyMgr.notify(mNotificationId, mBuilder.build());
+    }
+
+    public void updateProgress(int max, int current) {
+        mBuilder.setProgress(max, current, false);
+        mNotifyMgr.notify(mNotificationId, mBuilder.build());
+    }
+
+    public void completeSaveImage(Uri result) {
+        if (SHOW_IMAGE) {
+            // TODO: we should update the existing image in Gallery instead
+            Intent viewImage = new Intent(Intent.ACTION_VIEW, result);
+            viewImage.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            startActivity(viewImage);
+        }
+        stopForeground(true);
+        stopSelf();
+        if (mNeedsAlive) {
+            // If the app has been restarted while we were saving...
+            mFiltershowActivity.updateUIAfterServiceStarted();
+        } else if (mFiltershowActivity.isSimpleEditAction()) {
+            // terminate now
+            mFiltershowActivity.completeSaveImage(result);
+        }
+    }
+
+    private void setupPipeline() {
+        Resources res = getResources();
+        FiltersManager.setResources(res);
+        CachingPipeline.createRenderscriptContext(this);
+
+        FiltersManager filtersManager = FiltersManager.getManager();
+        filtersManager.addLooks(this);
+        filtersManager.addBorders(this);
+        filtersManager.addTools(this);
+        filtersManager.addEffects();
+    }
+
+    private void tearDownPipeline() {
+        FilteringPipeline.getPipeline().turnOnPipeline(false);
+        FilteringPipeline.reset();
+        ImageFilter.resetStatics();
+        FiltersManager.getPreviewManager().freeRSFilterScripts();
+        FiltersManager.getManager().freeRSFilterScripts();
+        FiltersManager.getHighresManager().freeRSFilterScripts();
+        FiltersManager.reset();
+        CachingPipeline.destroyRenderScriptContext();
+    }
+
+    static {
+        System.loadLibrary("jni_filtershow_filters");
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/ProcessingTask.java b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTask.java
new file mode 100644
index 0000000..c3687ee
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTask.java
@@ -0,0 +1,72 @@
+/*
+ * 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.pipeline;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+
+public abstract class ProcessingTask {
+    private ProcessingTaskController mTaskController;
+    private Handler mProcessingHandler;
+    private Handler mResultHandler;
+    private int mType;
+
+    static interface Request {}
+    static interface Update {}
+    static interface Result {}
+
+    public void postRequest(Request message) {
+        Message msg = mProcessingHandler.obtainMessage(mType);
+        msg.obj = message;
+        mProcessingHandler.sendMessage(msg);
+    }
+
+    public void postUpdate(Update message) {
+        Message msg = mResultHandler.obtainMessage(mType);
+        msg.obj = message;
+        msg.arg1 = ProcessingTaskController.UPDATE;
+        mResultHandler.sendMessage(msg);
+    }
+
+    public void processRequest(Request message) {
+        Object result = doInBackground(message);
+        Message msg = mResultHandler.obtainMessage(mType);
+        msg.obj = result;
+        msg.arg1 = ProcessingTaskController.RESULT;
+        mResultHandler.sendMessage(msg);
+    }
+
+    public void added(ProcessingTaskController taskController) {
+        mTaskController = taskController;
+        mResultHandler = taskController.getResultHandler();
+        mProcessingHandler = taskController.getProcessingHandler();
+        mType = taskController.getReservedType();
+    }
+
+    public int getType() {
+        return mType;
+    }
+
+    public Context getContext() {
+        return mTaskController.getContext();
+    }
+
+    public abstract Result doInBackground(Request message);
+    public abstract void onResult(Result message);
+    public void onUpdate(Update message) {}
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/ProcessingTaskController.java b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTaskController.java
new file mode 100644
index 0000000..218ea63
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTaskController.java
@@ -0,0 +1,98 @@
+/*
+ * 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.pipeline;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.util.Log;
+
+import java.util.HashMap;
+
+public class ProcessingTaskController implements Handler.Callback {
+    private static final String LOGTAG = "ProcessingTaskController";
+
+    private Context mContext;
+    private HandlerThread mHandlerThread = null;
+    private Handler mProcessingHandler = null;
+    private int mCurrentType;
+    private HashMap<Integer, ProcessingTask> mTasks = new HashMap<Integer, ProcessingTask>();
+
+    public final static int RESULT = 1;
+    public final static int UPDATE = 2;
+
+    private final Handler mResultHandler = new Handler() {
+
+        @Override
+        public void handleMessage(Message msg) {
+            ProcessingTask task = mTasks.get(msg.what);
+            if (task != null) {
+                if (msg.arg1 == RESULT) {
+                    task.onResult((ProcessingTask.Result) msg.obj);
+                } else if (msg.arg1 == UPDATE) {
+                    task.onUpdate((ProcessingTask.Update) msg.obj);
+                } else {
+                    Log.w(LOGTAG, "received unknown message! " + msg.arg1);
+                }
+            }
+        }
+    };
+
+    @Override
+    public boolean handleMessage(Message msg) {
+        ProcessingTask task = mTasks.get(msg.what);
+        if (task != null) {
+            task.processRequest((ProcessingTask.Request) msg.obj);
+            return true;
+        }
+        return false;
+    }
+
+    public ProcessingTaskController(Context context) {
+        mContext = context;
+        mHandlerThread = new HandlerThread("ProcessingTaskController",
+                android.os.Process.THREAD_PRIORITY_FOREGROUND);
+        mHandlerThread.start();
+        mProcessingHandler = new Handler(mHandlerThread.getLooper(), this);
+    }
+
+    public Handler getProcessingHandler() {
+        return mProcessingHandler;
+    }
+
+    public Handler getResultHandler() {
+        return mResultHandler;
+    }
+
+    public int getReservedType() {
+        return mCurrentType++;
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
+    public void add(ProcessingTask task) {
+        task.added(this);
+        mTasks.put(task.getType(), task);
+    }
+
+    public void quit() {
+        mHandlerThread.quit();
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java b/src/com/android/gallery3d/filtershow/tools/SaveImage.java
similarity index 90%
rename from src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java
rename to src/com/android/gallery3d/filtershow/tools/SaveImage.java
index dcf0ae1..9b13af1 100644
--- a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java
+++ b/src/com/android/gallery3d/filtershow/tools/SaveImage.java
@@ -19,18 +19,18 @@
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.Intent;
 import android.database.Cursor;
 import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
 import android.net.Uri;
-import android.os.AsyncTask;
 import android.os.Environment;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Images;
 import android.provider.MediaStore.Images.ImageColumns;
-import android.provider.MediaStore.Images.Media;
 import android.util.Log;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.PhotoPage;
 import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.exif.ExifInterface;
 import com.android.gallery3d.filtershow.FilterShowActivity;
@@ -39,6 +39,7 @@
 import com.android.gallery3d.filtershow.filters.FiltersManager;
 import com.android.gallery3d.filtershow.imageshow.MasterImage;
 import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+import com.android.gallery3d.filtershow.pipeline.ProcessingService;
 import com.android.gallery3d.util.UsageStatistics;
 import com.android.gallery3d.util.XmpUtilHelper;
 
@@ -52,17 +53,16 @@
 import java.util.TimeZone;
 
 /**
- * Asynchronous task for saving edited photo as a new copy.
+ * Handles saving edited photo
  */
-public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
-
-    private static final String LOGTAG = "SaveCopyTask";
+public class SaveImage {
+    private static final String LOGTAG = "SaveImage";
 
     /**
-     * Callback for the completed asynchronous task.
+     * Callback for updates
      */
     public interface Callback {
-        void onComplete(Uri result);
+        void onProgress(int max, int current);
     }
 
     public interface ContentResolverQueryCallback {
@@ -85,6 +85,10 @@
     private final Callback mCallback;
     private final File mDestinationFile;
     private final Uri mSelectedImageUri;
+
+    private int mCurrentProcessingStep = 1;
+
+    public static final int MAX_PROCESSING_STEPS = 6;
     public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos";
 
     // In order to support the new edit-save behavior such that user won't see
@@ -129,8 +133,8 @@
      * @param callback Let the caller know the saving has completed.
      * @return the newSourceUri
      */
-    public SaveCopyTask(Context context, Uri sourceUri, Uri selectedImageUri,
-            File destination, Callback callback)  {
+    public SaveImage(Context context, Uri sourceUri, Uri selectedImageUri,
+                     File destination, Callback callback)  {
         mContext = context;
         mSourceUri = sourceUri;
         mCallback = callback;
@@ -144,10 +148,10 @@
     }
 
     public static File getFinalSaveDirectory(Context context, Uri sourceUri) {
-        File saveDirectory = SaveCopyTask.getSaveDirectory(context, sourceUri);
+        File saveDirectory = SaveImage.getSaveDirectory(context, sourceUri);
         if ((saveDirectory == null) || !saveDirectory.canWrite()) {
             saveDirectory = new File(Environment.getExternalStorageDirectory(),
-                    SaveCopyTask.DEFAULT_SAVE_DIRECTORY);
+                    SaveImage.DEFAULT_SAVE_DIRECTORY);
         }
         // Create the directory if it doesn't exist
         if (!saveDirectory.exists())
@@ -275,17 +279,7 @@
         return ret;
     }
 
-    /**
-     * The task should be executed with one given bitmap to be saved.
-     */
-    @Override
-    protected Uri doInBackground(ImagePreset... params) {
-        // TODO: Support larger dimensions for photo saving.
-        if (params[0] == null || mSourceUri == null || mSelectedImageUri == null) {
-            return null;
-        }
-
-        ImagePreset preset = params[0];
+    private Uri resetToOriginalImageIfNeeded(ImagePreset preset) {
         Uri uri = null;
         if (!preset.hasModifications()) {
             // This can happen only when preset has no modification but save
@@ -298,12 +292,32 @@
             // create a local copy as usual.
             if (srcFile != null) {
                 srcFile.renameTo(mDestinationFile);
-                uri = SaveCopyTask.insertContent(mContext, mSelectedImageUri, mDestinationFile,
+                uri = SaveImage.insertContent(mContext, mSelectedImageUri, mDestinationFile,
                         System.currentTimeMillis());
                 removeSelectedImage();
-                return uri;
             }
         }
+        return uri;
+    }
+
+    private void resetProgress() {
+        mCurrentProcessingStep = 0;
+    }
+
+    private void updateProgress() {
+        if (mCallback != null) {
+            mCallback.onProgress(MAX_PROCESSING_STEPS, ++mCurrentProcessingStep);
+        }
+    }
+
+    public Uri processAndSaveImage(ImagePreset preset) {
+
+        Uri uri = resetToOriginalImageIfNeeded(preset);
+        if (uri != null) {
+            return null;
+        }
+
+        resetProgress();
 
         boolean noBitmap = true;
         int num_tries = 0;
@@ -321,15 +335,19 @@
         // Stopgap fix for low-memory devices.
         while (noBitmap) {
             try {
+                updateProgress();
                 // Try to do bitmap operations, downsample if low-memory
                 Bitmap bitmap = ImageLoader.loadOrientedBitmapWithBackouts(mContext, newSourceUri,
                         sampleSize);
                 if (bitmap == null) {
                     return null;
                 }
+                updateProgress();
                 CachingPipeline pipeline = new CachingPipeline(FiltersManager.getManager(),
                         "Saving");
+
                 bitmap = pipeline.renderFinalImage(bitmap, preset);
+                updateProgress();
 
                 Object xmp = getPanoramaXMPData(mSelectedImageUri, preset);
                 ExifInterface exif = getExifData(mSelectedImageUri);
@@ -347,28 +365,34 @@
                 // If we succeed in writing the bitmap as a jpeg, return a uri.
                 if (putExifData(mDestinationFile, exif, bitmap)) {
                     putPanoramaXMPData(mDestinationFile, xmp);
-                    uri = SaveCopyTask.insertContent(mContext, mSelectedImageUri, mDestinationFile,
+                    uri = SaveImage.insertContent(mContext, mSelectedImageUri, mDestinationFile,
                             time);
                 }
+                updateProgress();
 
                 // mDestinationFile will save the newSourceUri info in the XMP.
                 XmpPresets.writeFilterXMP(mContext, newSourceUri, mDestinationFile, preset);
+                updateProgress();
 
                 // Since we have a new image inserted to media store, we can
                 // safely remove the old one which is selected by the user.
+                // TODO: we should fix that, do an update instead of insert+remove,
+                //       as well as asking Gallery to update its cached version of the image
                 if (USE_AUX_DIR) {
                     removeSelectedImage();
                 }
+                updateProgress();
                 noBitmap = false;
                 UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
                         "SaveComplete", null);
-            } catch (java.lang.OutOfMemoryError e) {
+            } catch (OutOfMemoryError e) {
                 // Try 5 times before failing for good.
                 if (++num_tries >= 5) {
                     throw e;
                 }
                 System.gc();
                 sampleSize *= 2;
+                resetProgress();
             }
         }
         return uri;
@@ -434,13 +458,6 @@
         return auxDiretory;
     }
 
-    @Override
-    protected void onPostExecute(Uri result) {
-        if (mCallback != null) {
-            mCallback.onComplete(result);
-        }
-    }
-
     public static Uri makeAndInsertUri(Context context, Uri sourceUri) {
         long time = System.currentTimeMillis();
         String filename = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(time));
@@ -452,19 +469,19 @@
     public static void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity,
             File destination) {
         Uri selectedImageUri = filterShowActivity.getSelectedImageUri();
-        new SaveCopyTask(filterShowActivity, MasterImage.getImage().getUri(), selectedImageUri,
-                destination,
-                new SaveCopyTask.Callback() {
+        Uri sourceImageUri = MasterImage.getImage().getUri();
 
-                    @Override
-                    public void onComplete(Uri result) {
-                        filterShowActivity.completeSaveImage(result);
-                    }
+        Intent processIntent = ProcessingService.getSaveIntent(filterShowActivity, preset,
+                destination, selectedImageUri, sourceImageUri);
 
-                }).execute(preset);
+        filterShowActivity.startService(processIntent);
+
+        if (!filterShowActivity.isSimpleEditAction()) {
+            // terminate for now
+            filterShowActivity.completeSaveImage(selectedImageUri);
+        }
     }
 
-
     public static void querySource(Context context, Uri sourceUri, String[] projection,
             ContentResolverQueryCallback callback) {
         ContentResolver contentResolver = context.getContentResolver();
@@ -586,8 +603,8 @@
                 ImageColumns.DATE_TAKEN,
                 ImageColumns.LATITUDE, ImageColumns.LONGITUDE,
         };
-        SaveCopyTask.querySource(context, sourceUri, projection,
-                new SaveCopyTask.ContentResolverQueryCallback() {
+        SaveImage.querySource(context, sourceUri, projection,
+                new SaveImage.ContentResolverQueryCallback() {
 
                     @Override
                     public void onCursorResult(Cursor cursor) {
diff --git a/src/com/android/gallery3d/util/SaveVideoFileUtils.java b/src/com/android/gallery3d/util/SaveVideoFileUtils.java
index da0970b..10c41de 100644
--- a/src/com/android/gallery3d/util/SaveVideoFileUtils.java
+++ b/src/com/android/gallery3d/util/SaveVideoFileUtils.java
@@ -25,7 +25,7 @@
 import android.provider.MediaStore.Video;
 import android.provider.MediaStore.Video.VideoColumns;
 
-import com.android.gallery3d.filtershow.tools.SaveCopyTask.ContentResolverQueryCallback;
+import com.android.gallery3d.filtershow.tools.SaveImage.ContentResolverQueryCallback;
 
 import java.io.File;
 import java.sql.Date;
diff --git a/src_pd/com/android/gallery3d/filtershow/filters/FiltersManager.java b/src_pd/com/android/gallery3d/filtershow/filters/FiltersManager.java
index c5a6435..d4035cd 100644
--- a/src_pd/com/android/gallery3d/filtershow/filters/FiltersManager.java
+++ b/src_pd/com/android/gallery3d/filtershow/filters/FiltersManager.java
@@ -22,9 +22,6 @@
 
 import com.android.gallery3d.R;
 
-import java.util.HashMap;
-import java.util.Vector;
-
 public class FiltersManager extends BaseFiltersManager {
     private static FiltersManager sInstance = null;
     private static FiltersManager sPreviewInstance = null;
@@ -49,7 +46,7 @@
     }
 
     @Override
-    public void addBorders(Context context, Vector<FilterRepresentation> representations) {
+    public void addBorders(Context context) {
 
         // Do not localize
         String[] serializationNames = {
@@ -66,55 +63,59 @@
                 "FRAME_CREAM_ROUNDED"
         };
 
+        // The "no border" implementation
         int i = 0;
+        FilterRepresentation rep = new FilterImageBorderRepresentation(0);
+        mBorders.add(rep);
+
         // Regular borders
-        FilterRepresentation rep = new FilterImageBorderRepresentation(R.drawable.filtershow_border_4x5);
+        rep = new FilterImageBorderRepresentation(R.drawable.filtershow_border_4x5);
         rep.setSerializationName(serializationNames[i++]);
-        representations.add(rep);
+        mBorders.add(rep);
 
         rep = new FilterImageBorderRepresentation(R.drawable.filtershow_border_brush);
         rep.setSerializationName(serializationNames[i++]);
-        representations.add(rep);
+        mBorders.add(rep);
 
         rep = new FilterImageBorderRepresentation(R.drawable.filtershow_border_grunge);
         rep.setSerializationName(serializationNames[i++]);
-        representations.add(rep);
+        mBorders.add(rep);
 
         rep = new FilterImageBorderRepresentation(R.drawable.filtershow_border_sumi_e);
         rep.setSerializationName(serializationNames[i++]);
-        representations.add(rep);
+        mBorders.add(rep);
 
         rep = new FilterImageBorderRepresentation(R.drawable.filtershow_border_tape);
         rep.setSerializationName(serializationNames[i++]);
-        representations.add(rep);
+        mBorders.add(rep);
 
         rep = new FilterColorBorderRepresentation(Color.BLACK, mImageBorderSize, 0);
         rep.setSerializationName(serializationNames[i++]);
-        representations.add(rep);
+        mBorders.add(rep);
 
         rep = new FilterColorBorderRepresentation(Color.BLACK, mImageBorderSize,
                 mImageBorderSize);
         rep.setSerializationName(serializationNames[i++]);
-        representations.add(rep);
+        mBorders.add(rep);
 
         rep = new FilterColorBorderRepresentation(Color.WHITE, mImageBorderSize, 0);
         rep.setSerializationName(serializationNames[i++]);
-        representations.add(rep);
+        mBorders.add(rep);
 
         rep = new FilterColorBorderRepresentation(Color.WHITE, mImageBorderSize,
                 mImageBorderSize);
         rep.setSerializationName(serializationNames[i++]);
-        representations.add(rep);
+        mBorders.add(rep);
 
         int creamColor = Color.argb(255, 237, 237, 227);
         rep = new FilterColorBorderRepresentation(creamColor, mImageBorderSize, 0);
         rep.setSerializationName(serializationNames[i++]);
-        representations.add(rep);
+        mBorders.add(rep);
 
         rep = new FilterColorBorderRepresentation(creamColor, mImageBorderSize,
                 mImageBorderSize);
         rep.setSerializationName(serializationNames[i++]);
-        representations.add(rep);
+        mBorders.add(rep);
     }
 
     public static FiltersManager getHighresManager() {