OSDN Git Service

Clear Gallery's bitmap pool on photo editor start. Minor refactoring.
[android-x86/packages-apps-Gallery2.git] / src / com / android / gallery3d / filtershow / cache / ImageLoader.java
index e00a1b7..08ed403 100644 (file)
@@ -1,3 +1,18 @@
+/*
+ * 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.cache;
 
@@ -7,6 +22,7 @@ import android.content.res.Resources;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteException;
 import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
 import android.graphics.BitmapFactory;
 import android.graphics.BitmapRegionDecoder;
 import android.graphics.Matrix;
@@ -16,22 +32,33 @@ import android.net.Uri;
 import android.provider.MediaStore;
 import android.util.Log;
 
+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.ExifInvalidFormatException;
+import com.android.gallery3d.exif.ExifParser;
+import com.android.gallery3d.exif.ExifTag;
 import com.android.gallery3d.filtershow.FilterShowActivity;
 import com.android.gallery3d.filtershow.HistoryAdapter;
 import com.android.gallery3d.filtershow.imageshow.ImageShow;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
-import com.android.gallery3d.filtershow.tools.ProcessedBitmap;
+import com.android.gallery3d.filtershow.tools.BitmapTask;
 import com.android.gallery3d.filtershow.tools.SaveCopyTask;
+import com.android.gallery3d.util.InterruptableOutputStream;
+import com.android.gallery3d.util.XmpUtilHelper;
 
-import java.io.Closeable;
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.util.Vector;
+import java.util.concurrent.locks.ReentrantLock;
 
+
+// TODO: this class has waaaay to much bitmap copying.  Cleanup.
 public class ImageLoader {
 
     private static final String LOGTAG = "ImageLoader";
@@ -39,35 +66,68 @@ public class ImageLoader {
     private Bitmap mOriginalBitmapSmall = null;
     private Bitmap mOriginalBitmapLarge = null;
     private Bitmap mBackgroundBitmap = null;
-    private Bitmap mFullOriginalBitmap = null;
-    private Bitmap mSaveCopy = null;
 
-    private Cache mCache = null;
-    private Cache mHiresCache = null;
     private final ZoomCache mZoomCache = new ZoomCache();
 
     private int mOrientation = 0;
     private HistoryAdapter mAdapter = null;
-    private static int PORTRAIT_ORIENTATION = 6;
 
+    private FilterShowActivity mActivity = null;
+
+    public static final String JPEG_MIME_TYPE = "image/jpeg";
+
+    public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos";
+    public static final int DEFAULT_COMPRESS_QUALITY = 95;
+
+    public static final int ORI_NORMAL = ExifInterface.ORIENTATION_NORMAL;
+    public static final int ORI_ROTATE_90 = ExifInterface.ORIENTATION_ROTATE_90;
+    public static final int ORI_ROTATE_180 = ExifInterface.ORIENTATION_ROTATE_180;
+    public static final int ORI_ROTATE_270 = ExifInterface.ORIENTATION_ROTATE_270;
+    public static final int ORI_FLIP_HOR = ExifInterface.ORIENTATION_FLIP_HORIZONTAL;
+    public static final int ORI_FLIP_VERT = ExifInterface.ORIENTATION_FLIP_VERTICAL;
+    public static final int ORI_TRANSPOSE = ExifInterface.ORIENTATION_TRANSPOSE;
+    public static final int ORI_TRANSVERSE = ExifInterface.ORIENTATION_TRANSVERSE;
+
+    private static final int BITMAP_LOAD_BACKOUT_ATTEMPTS = 5;
     private Context mContext = null;
     private Uri mUri = null;
 
     private Rect mOriginalBounds = null;
+    private static int mZoomOrientation = ORI_NORMAL;
 
-    public ImageLoader(Context context) {
+    private ReentrantLock mLoadingLock = new ReentrantLock();
+
+    public ImageLoader(FilterShowActivity activity, Context context) {
+        mActivity = activity;
         mContext = context;
-        mCache = new DelayedPresetCache(this, 30);
-        mHiresCache = new DelayedPresetCache(this, 2);
     }
 
-    public void loadBitmap(Uri uri,int size) {
-        mUri = uri;
-        mOrientation = getOrientation(uri);
+    public static int getZoomOrientation() {
+        return mZoomOrientation;
+    }
+
+    public FilterShowActivity getActivity() {
+        return mActivity;
+    }
 
+    public boolean loadBitmap(Uri uri, int size) {
+        mLoadingLock.lock();
+        mUri = uri;
+        mOrientation = getOrientation(mContext, uri);
         mOriginalBitmapSmall = loadScaledBitmap(uri, 160);
+        if (mOriginalBitmapSmall == null) {
+            // Couldn't read the bitmap, let's exit
+            mLoadingLock.unlock();
+            return false;
+        }
         mOriginalBitmapLarge = loadScaledBitmap(uri, size);
+        if (mOriginalBitmapLarge == null) {
+            mLoadingLock.unlock();
+            return false;
+        }
         updateBitmaps();
+        mLoadingLock.unlock();
+        return true;
     }
 
     public Uri getUri() {
@@ -78,81 +138,148 @@ public class ImageLoader {
         return mOriginalBounds;
     }
 
-    private int getOrientation(Uri uri) {
+    public static int getOrientation(Context context, Uri uri) {
         if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
-            return getOrientationFromPath(uri.getPath());
+            String mimeType = context.getContentResolver().getType(uri);
+            if (mimeType != ImageLoader.JPEG_MIME_TYPE) {
+                return -1;
+            }
+            String path = uri.getPath();
+            int orientation = -1;
+            InputStream is = null;
+            try {
+                is = new FileInputStream(path);
+                ExifParser parser = ExifParser.parse(is, ExifParser.OPTION_IFD_0);
+                int event = parser.next();
+                while (event != ExifParser.EVENT_END) {
+                    if (event == ExifParser.EVENT_NEW_TAG) {
+                        ExifTag tag = parser.getTag();
+                        if (tag.getTagId() == ExifTag.TAG_ORIENTATION) {
+                            orientation = (int) tag.getValueAt(0);
+                            break;
+                        }
+                    }
+                    event = parser.next();
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            } catch (ExifInvalidFormatException e) {
+                e.printStackTrace();
+            } finally {
+                Utils.closeSilently(is);
+            }
+            return orientation;
         }
-
         Cursor cursor = null;
         try {
-            cursor = mContext.getContentResolver().query(uri,
+            cursor = context.getContentResolver().query(uri,
                     new String[] {
                         MediaStore.Images.ImageColumns.ORIENTATION
                     },
                     null, null, null);
-            return cursor.moveToNext() ? cursor.getInt(0) : -1;
-        } catch (SQLiteException e){
+            if (cursor.moveToNext()) {
+                int ori = cursor.getInt(0);
+
+                switch (ori) {
+                    case 0:
+                        return ORI_NORMAL;
+                    case 90:
+                        return ORI_ROTATE_90;
+                    case 270:
+                        return ORI_ROTATE_270;
+                    case 180:
+                        return ORI_ROTATE_180;
+                    default:
+                        return -1;
+                }
+            } else {
+                return -1;
+            }
+        } catch (SQLiteException e) {
+            return ExifInterface.ORIENTATION_UNDEFINED;
+        } catch (IllegalArgumentException e) {
             return ExifInterface.ORIENTATION_UNDEFINED;
         } finally {
             Utils.closeSilently(cursor);
         }
     }
 
-    private int getOrientationFromPath(String path) {
-        int orientation = -1;
-        try {
-            ExifInterface EXIF = new ExifInterface(path);
-            orientation = EXIF.getAttributeInt(ExifInterface.TAG_ORIENTATION,
-                    1);
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
-        return orientation;
-    }
-
     private void updateBitmaps() {
-        mCache.setOriginalBitmap(mOriginalBitmapSmall);
-        mHiresCache.setOriginalBitmap(mOriginalBitmapLarge);
-        if (mOrientation == PORTRAIT_ORIENTATION) {
-            mOriginalBitmapSmall = rotateToPortrait(mOriginalBitmapSmall);
-            mOriginalBitmapLarge = rotateToPortrait(mOriginalBitmapLarge);
+        if (mOrientation > 1) {
+            mOriginalBitmapSmall = rotateToPortrait(mOriginalBitmapSmall, mOrientation);
+            mOriginalBitmapLarge = rotateToPortrait(mOriginalBitmapLarge, mOrientation);
         }
+        mZoomOrientation = mOrientation;
         warnListeners();
     }
 
-    private Bitmap rotateToPortrait(Bitmap bitmap) {
-        Matrix matrix = new Matrix();
-        matrix.postRotate(90);
-        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
-                bitmap.getHeight(), matrix, true);
+    public Bitmap decodeImage(int id, BitmapFactory.Options options) {
+        return BitmapFactory.decodeResource(mContext.getResources(), id, options);
     }
 
-    private void closeStream(Closeable stream) {
-        if (stream != null) {
-            try {
-                stream.close();
-            } catch (IOException e) {
-                e.printStackTrace();
-            }
+    public static Bitmap rotateToPortrait(Bitmap bitmap, int ori) {
+        Matrix matrix = new Matrix();
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+        if (ori == ORI_ROTATE_90 ||
+                ori == ORI_ROTATE_270 ||
+                ori == ORI_TRANSPOSE ||
+                ori == ORI_TRANSVERSE) {
+            int tmp = w;
+            w = h;
+            h = tmp;
         }
+        switch (ori) {
+            case ORI_ROTATE_90:
+                matrix.setRotate(90, w / 2f, h / 2f);
+                break;
+            case ORI_ROTATE_180:
+                matrix.setRotate(180, w / 2f, h / 2f);
+                break;
+            case ORI_ROTATE_270:
+                matrix.setRotate(270, w / 2f, h / 2f);
+                break;
+            case ORI_FLIP_HOR:
+                matrix.preScale(-1, 1);
+                break;
+            case ORI_FLIP_VERT:
+                matrix.preScale(1, -1);
+                break;
+            case ORI_TRANSPOSE:
+                matrix.setRotate(90, w / 2f, h / 2f);
+                matrix.preScale(1, -1);
+                break;
+            case ORI_TRANSVERSE:
+                matrix.setRotate(270, w / 2f, h / 2f);
+                matrix.preScale(1, -1);
+                break;
+            case ORI_NORMAL:
+            default:
+                return bitmap;
+        }
+
+        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
+                bitmap.getHeight(), matrix, true);
     }
 
-    private Bitmap loadRegionBitmap(Uri uri, Rect bounds) {
+    private Bitmap loadRegionBitmap(Uri uri, BitmapFactory.Options options, Rect bounds) {
         InputStream is = null;
         try {
             is = mContext.getContentResolver().openInputStream(uri);
             BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
-            return decoder.decodeRegion(bounds, null);
+            return decoder.decodeRegion(bounds, options);
         } catch (FileNotFoundException e) {
             Log.e(LOGTAG, "FileNotFoundException: " + uri);
         } catch (Exception e) {
             e.printStackTrace();
         } finally {
-            closeStream(is);
+            Utils.closeSilently(is);
         }
         return null;
     }
 
+    static final int MAX_BITMAP_DIM = 900;
+
     private Bitmap loadScaledBitmap(Uri uri, int size) {
         InputStream is = null;
         try {
@@ -170,8 +297,11 @@ public class ImageLoader {
 
             int scale = 1;
             while (true) {
-                if (width_tmp / 2 < size || height_tmp / 2 < size)
-                    break;
+                if (width_tmp <= MAX_BITMAP_DIM && height_tmp <= MAX_BITMAP_DIM) {
+                    if (width_tmp / 2 < size || height_tmp / 2 < size) {
+                        break;
+                    }
+                }
                 width_tmp /= 2;
                 height_tmp /= 2;
                 scale *= 2;
@@ -180,8 +310,9 @@ public class ImageLoader {
             // decode with inSampleSize
             BitmapFactory.Options o2 = new BitmapFactory.Options();
             o2.inSampleSize = scale;
+            o2.inMutable = true;
 
-            closeStream(is);
+            Utils.closeSilently(is);
             is = mContext.getContentResolver().openInputStream(uri);
             return BitmapFactory.decodeStream(is, null, o2);
         } catch (FileNotFoundException e) {
@@ -189,7 +320,7 @@ public class ImageLoader {
         } catch (Exception e) {
             e.printStackTrace();
         } finally {
-            closeStream(is);
+            Utils.closeSilently(is);
         }
         return null;
     }
@@ -212,108 +343,292 @@ public class ImageLoader {
     }
 
     public void addListener(ImageShow imageShow) {
+        mLoadingLock.lock();
         if (!mListeners.contains(imageShow)) {
             mListeners.add(imageShow);
         }
+        mLoadingLock.unlock();
     }
 
-    public void warnListeners() {
-        for (int i = 0; i < mListeners.size(); i++) {
-            ImageShow imageShow = mListeners.elementAt(i);
-            imageShow.updateImage();
-        }
+    private void warnListeners() {
+        mActivity.runOnUiThread(mWarnListenersRunnable);
     }
 
-    // TODO: this currently does the loading + filtering on the UI thread -- need to
-    // move this to a background thread.
+    private Runnable mWarnListenersRunnable = new Runnable() {
+
+        @Override
+        public void run() {
+            for (int i = 0; i < mListeners.size(); i++) {
+                ImageShow imageShow = mListeners.elementAt(i);
+                imageShow.imageLoaded();
+            }
+        }
+    };
+
+    // 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,
-            boolean force) {
+                                            Rect destination, boolean force) {
+        mLoadingLock.lock();
         Bitmap bmp = mZoomCache.getImage(imagePreset, bounds);
         if (force || bmp == null) {
-            bmp = loadRegionBitmap(mUri, bounds);
+            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;
+                }
+            }
+            bmp = loadRegionBitmap(mUri, options, bounds);
+            if (destination != null) {
+                mLoadingLock.unlock();
+                return bmp;
+            }
             if (bmp != null) {
-                // TODO: this workaround for RS might not be needed ultimately
-                Bitmap bmp2 = bmp.copy(Bitmap.Config.ARGB_8888, true);
                 float scaleFactor = imagePreset.getScaleFactor();
-                imagePreset.setScaleFactor(1.0f);
-                bmp2 = imagePreset.apply(bmp2);
+                float scale = (float) bmp.getWidth() / (float) getOriginalBounds().width();
+                imagePreset.setScaleFactor(scale);
+                imagePreset.setupEnvironment();
+                bmp = imagePreset.apply(bmp);
                 imagePreset.setScaleFactor(scaleFactor);
-                mZoomCache.setImage(imagePreset, bounds, bmp2);
-                return bmp2;
+                mZoomCache.setImage(imagePreset, bounds, bmp);
+                mLoadingLock.unlock();
+                return bmp;
             }
         }
+        mLoadingLock.unlock();
         return bmp;
     }
 
-    // Caching method
-    public Bitmap getImageForPreset(ImageShow caller, ImagePreset imagePreset,
-            boolean hiRes) {
-        if (mOriginalBitmapSmall == null) {
-            return null;
-        }
-        if (mOriginalBitmapLarge == null) {
+    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
+            public void onComplete(Uri result) {
+                filterShowActivity.completeSaveImage(result);
+            }
+
+        }).execute(preset);
+    }
+
+    public static Bitmap loadMutableBitmap(Context context, Uri sourceUri) {
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        return loadMutableBitmap(context, sourceUri, options);
+    }
+
+    public static Bitmap loadMutableBitmap(Context context, Uri sourceUri,
+            BitmapFactory.Options options) {
+        // TODO: on <3.x we need a copy of the bitmap (inMutable doesn't
+        // exist)
+        options.inMutable = true;
+
+        Bitmap bitmap = decodeUriWithBackouts(context, sourceUri, options);
+        if (bitmap == null) {
             return null;
         }
+        int orientation = ImageLoader.getOrientation(context, sourceUri);
+        bitmap = ImageLoader.rotateToPortrait(bitmap, orientation);
+        return bitmap;
+    }
 
-        Bitmap filteredImage = null;
+    public static Bitmap decodeUriWithBackouts(Context context, Uri sourceUri,
+            BitmapFactory.Options options) {
+        boolean noBitmap = true;
+        int num_tries = 0;
+        InputStream is = getInputStream(context, sourceUri);
 
-        if (hiRes) {
-            filteredImage = mHiresCache.get(imagePreset);
-        } else {
-            filteredImage = mCache.get(imagePreset);
+        if (options.inSampleSize < 1) {
+            options.inSampleSize = 1;
         }
-
-        if (filteredImage == null) {
-            if (hiRes) {
-                cachePreset(imagePreset, mHiresCache, caller);
-            } else {
-                cachePreset(imagePreset, mCache, caller);
+        // Stopgap fix for low-memory devices.
+        Bitmap bmap = null;
+        while (noBitmap) {
+            if (is == null) {
+                return null;
+            }
+            try {
+                // Try to decode, downsample if low-memory.
+                bmap = BitmapFactory.decodeStream(is, null, options);
+                noBitmap = false;
+            } catch (java.lang.OutOfMemoryError e) {
+                // Try 5 times before failing for good.
+                if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
+                    throw e;
+                }
+                is = null;
+                bmap = null;
+                System.gc();
+                is = getInputStream(context, sourceUri);
+                options.inSampleSize *= 2;
             }
         }
-        return filteredImage;
+        Utils.closeSilently(is);
+        return bmap;
     }
 
-    public void resetImageForPreset(ImagePreset imagePreset, ImageShow caller) {
-        mHiresCache.reset(imagePreset);
-        mCache.reset(imagePreset);
-        mZoomCache.reset(imagePreset);
+    private static InputStream getInputStream(Context context, Uri sourceUri) {
+        InputStream is = null;
+        try {
+            is = context.getContentResolver().openInputStream(sourceUri);
+        } catch (FileNotFoundException e) {
+            Log.w(LOGTAG, "could not load bitmap ", e);
+            Utils.closeSilently(is);
+            is = null;
+        }
+        return is;
     }
 
-    public Uri saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity,
-            File destination) {
-        BitmapFactory.Options options = new BitmapFactory.Options();
-        options.inMutable = true;
-
-        if (mFullOriginalBitmap != null) {
-            mFullOriginalBitmap.recycle();
+    public static Bitmap decodeResourceWithBackouts(Resources res, BitmapFactory.Options options,
+            int id) {
+        boolean noBitmap = true;
+        int num_tries = 0;
+        if (options.inSampleSize < 1) {
+            options.inSampleSize = 1;
+        }
+        // Stopgap fix for low-memory devices.
+        Bitmap bmap = null;
+        while (noBitmap) {
+            try {
+                // Try to decode, downsample if low-memory.
+                bmap = BitmapFactory.decodeResource(
+                        res, id, options);
+                noBitmap = false;
+            } catch (java.lang.OutOfMemoryError e) {
+                // Try 5 times before failing for good.
+                if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
+                    throw e;
+                }
+                bmap = null;
+                System.gc();
+                options.inSampleSize *= 2;
+            }
         }
+        return bmap;
+    }
 
-        InputStream is = null;
-        Uri saveUri = null;
-        try {
-            is = mContext.getContentResolver().openInputStream(mUri);
-            mFullOriginalBitmap = BitmapFactory.decodeStream(is, null, options);
-            // TODO: on <3.x we need a copy of the bitmap (inMutable doesn't
-            // exist)
-            mSaveCopy = mFullOriginalBitmap;
-            preset.setIsHighQuality(true);
-            preset.setScaleFactor(1.0f);
-            ProcessedBitmap processedBitmap = new ProcessedBitmap(mSaveCopy, preset);
-            new SaveCopyTask(mContext, mUri, destination, new SaveCopyTask.Callback() {
-
-                @Override
-                public void onComplete(Uri result) {
-                    filterShowActivity.completeSaveImage(result);
+    public void returnFilteredResult(ImagePreset preset,
+            final FilterShowActivity filterShowActivity) {
+        preset.setQuality(ImagePreset.QUALITY_FINAL);
+        preset.setScaleFactor(1.0f);
+
+        BitmapTask.Callbacks<ImagePreset, Bitmap> cb = new BitmapTask.Callbacks<ImagePreset, Bitmap>() {
+
+            @Override
+            public void onComplete(Bitmap result) {
+                filterShowActivity.onFilteredResult(result);
+            }
+
+            @Override
+            public void onCancel() {
+            }
+
+            @Override
+            public Bitmap onExecute(ImagePreset param) {
+                if (param == null || mUri == null) {
+                    return null;
+                }
+                BitmapFactory.Options options = new BitmapFactory.Options();
+                boolean noBitmap = true;
+                int num_tries = 0;
+                if (options.inSampleSize < 1) {
+                    options.inSampleSize = 1;
                 }
+                Bitmap bitmap = null;
+                // Stopgap fix for low-memory devices.
+                while (noBitmap) {
+                    try {
+                        // Try to do bitmap operations, downsample if low-memory
+                        bitmap = loadMutableBitmap(mContext, mUri, options);
+                        if (bitmap == null) {
+                            Log.w(LOGTAG, "Failed to save image!");
+                            return null;
+                        }
+                        param.setupEnvironment();
+                        bitmap = param.applyGeometry(bitmap);
+                        bitmap = param.apply(bitmap);
+                        noBitmap = false;
+                    } catch (java.lang.OutOfMemoryError e) {
+                        // Try 5 times before failing for good.
+                        if (++num_tries >= 5) {
+                            throw e;
+                        }
+                        bitmap = null;
+                        System.gc();
+                        options.inSampleSize *= 2;
+                    }
+                }
+                return bitmap;
+            }
+        };
+
+        (new BitmapTask<ImagePreset, Bitmap>(cb)).execute(preset);
+    }
+
+    private String getFileExtension(String requestFormat) {
+        String outputFormat = (requestFormat == null)
+                ? "jpg"
+                : requestFormat;
+        outputFormat = outputFormat.toLowerCase();
+        return (outputFormat.equals("png") || outputFormat.equals("gif"))
+                ? "png" // We don't support gif compression.
+                : "jpg";
+    }
 
-            }).execute(processedBitmap);
+    private CompressFormat convertExtensionToCompressFormat(String extension) {
+        return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
+    }
+
+    public void saveToUri(Bitmap bmap, Uri uri, final String outputFormat,
+            final FilterShowActivity filterShowActivity) {
+
+        OutputStream out = null;
+        try {
+            out = filterShowActivity.getContentResolver().openOutputStream(uri);
         } catch (FileNotFoundException e) {
-            e.printStackTrace();
+            Log.w(LOGTAG, "cannot write output", e);
+            out = null;
         } finally {
-            closeStream(is);
+            if (bmap == null || out == null) {
+                return;
+            }
         }
 
-        return saveUri;
+        final InterruptableOutputStream ios = new InterruptableOutputStream(out);
+
+        BitmapTask.Callbacks<Bitmap, Bitmap> cb = new BitmapTask.Callbacks<Bitmap, Bitmap>() {
+
+            @Override
+            public void onComplete(Bitmap result) {
+                filterShowActivity.done();
+            }
+
+            @Override
+            public void onCancel() {
+                ios.interrupt();
+            }
+
+            @Override
+            public Bitmap onExecute(Bitmap param) {
+                if (param == null) {
+                    return null;
+                }
+                CompressFormat cf = convertExtensionToCompressFormat(getFileExtension(outputFormat));
+                param.compress(cf, DEFAULT_COMPRESS_QUALITY, ios);
+                Utils.closeSilently(ios);
+                return null;
+            }
+        };
+
+        (new BitmapTask<Bitmap, Bitmap>(cb)).execute(bmap);
     }
 
     public void setAdapter(HistoryAdapter adapter) {
@@ -324,8 +639,56 @@ public class ImageLoader {
         return mAdapter;
     }
 
-    private void cachePreset(ImagePreset preset, Cache cache, ImageShow caller) {
-        cache.prepare(preset);
-        cache.addObserver(caller);
+    public XMPMeta getXmpObject() {
+        try {
+            InputStream is = mContext.getContentResolver().openInputStream(getUri());
+            return XmpUtilHelper.extractXMPMeta(is);
+        } catch (FileNotFoundException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Determine if this is a light cycle 360 image
+     *
+     * @return true if it is a light Cycle image that is full 360
+     */
+    public boolean queryLightCycle360() {
+        InputStream is = null;
+        try {
+            is = mContext.getContentResolver().openInputStream(getUri());
+            XMPMeta meta = XmpUtilHelper.extractXMPMeta(is);
+            if (meta == null) {
+                return false;
+            }
+            String name = meta.getPacketHeader();
+            String namespace = "http://ns.google.com/photos/1.0/panorama/";
+            String cropWidthName = "GPano:CroppedAreaImageWidthPixels";
+            String fullWidthName = "GPano:FullPanoWidthPixels";
+
+            if (!meta.doesPropertyExist(namespace, cropWidthName)) {
+                return false;
+            }
+            if (!meta.doesPropertyExist(namespace, fullWidthName)) {
+                return false;
+            }
+
+            Integer cropValue = meta.getPropertyInteger(namespace, cropWidthName);
+            Integer fullValue = meta.getPropertyInteger(namespace, fullWidthName);
+
+            // Definition of a 360:
+            // GFullPanoWidthPixels == CroppedAreaImageWidthPixels
+            if (cropValue != null && fullValue != null) {
+                return cropValue.equals(fullValue);
+            }
+
+            return false;
+        } catch (FileNotFoundException e) {
+            return false;
+        } catch (XMPException e) {
+            return false;
+        } finally {
+            Utils.closeSilently(is);
+        }
     }
 }