Save XMP & Exif data on edited photos

 Bug: 7293391
 Bug: 7329199
 Bug: 7376660

Change-Id: I23b1637a9a494c1dc43b1fc1359cdaf3e75bc23f
diff --git a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
index 6beaed6..d0cab76 100644
--- a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
+++ b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
@@ -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;
 
@@ -22,7 +37,6 @@
 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.SaveCopyTask;
 
 import java.io.Closeable;
@@ -39,8 +53,6 @@
     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;
@@ -74,7 +86,7 @@
 
     public void loadBitmap(Uri uri,int size) {
         mUri = uri;
-        mOrientation = getOrientation(uri);
+        mOrientation = getOrientation(mContext, uri);
         mOriginalBitmapSmall = loadScaledBitmap(uri, 160);
         if (mOriginalBitmapSmall == null) {
             // Couldn't read the bitmap, let's exit
@@ -92,14 +104,14 @@
         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());
         }
 
         Cursor cursor = null;
         try {
-            cursor = mContext.getContentResolver().query(uri,
+            cursor = context.getContentResolver().query(uri,
                     new String[] {
                         MediaStore.Images.ImageColumns.ORIENTATION
                     },
@@ -125,7 +137,7 @@
         }
     }
 
-    private int getOrientationFromPath(String path) {
+    static int getOrientationFromPath(String path) {
         int orientation = -1;
         try {
             ExifInterface EXIF = new ExifInterface(path);
@@ -139,15 +151,15 @@
 
     private void updateBitmaps() {
         if (mOrientation > 1) {
-            mOriginalBitmapSmall = rotateToPortrait(mOriginalBitmapSmall,mOrientation);
-            mOriginalBitmapLarge = rotateToPortrait(mOriginalBitmapLarge,mOrientation);
+            mOriginalBitmapSmall = rotateToPortrait(mOriginalBitmapSmall, mOrientation);
+            mOriginalBitmapLarge = rotateToPortrait(mOriginalBitmapLarge, mOrientation);
         }
         mCache.setOriginalBitmap(mOriginalBitmapSmall);
         mHiresCache.setOriginalBitmap(mOriginalBitmapLarge);
         warnListeners();
     }
 
-    private Bitmap rotateToPortrait(Bitmap bitmap,int ori) {
+    public static Bitmap rotateToPortrait(Bitmap bitmap,int ori) {
            Matrix matrix = new Matrix();
            int w = bitmap.getWidth();
            int h = bitmap.getHeight();
@@ -344,42 +356,18 @@
         mZoomCache.reset(imagePreset);
     }
 
-    public Uri saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity,
+    public void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity,
             File destination) {
-        BitmapFactory.Options options = new BitmapFactory.Options();
-        options.inMutable = true;
+        preset.setIsHighQuality(true);
+        preset.setScaleFactor(1.0f);
+        new SaveCopyTask(mContext, mUri, destination, new SaveCopyTask.Callback() {
 
-        if (mFullOriginalBitmap != null) {
-            mFullOriginalBitmap.recycle();
-        }
+            @Override
+            public void onComplete(Uri result) {
+                filterShowActivity.completeSaveImage(result);
+            }
 
-        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)
-            mFullOriginalBitmap = rotateToPortrait(mFullOriginalBitmap,mOrientation);
-            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);
-                }
-
-            }).execute(processedBitmap);
-        } catch (FileNotFoundException e) {
-            e.printStackTrace();
-        } finally {
-            closeStream(is);
-        }
-
-        return saveUri;
+        }).execute(preset);
     }
 
     public void setAdapter(HistoryAdapter adapter) {
diff --git a/src/com/android/gallery3d/filtershow/presets/ImagePreset.java b/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
index f303d4c..0d8fc31 100644
--- a/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
+++ b/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
@@ -76,6 +76,23 @@
         return false;
     }
 
+    public boolean isPanoramaSafe() {
+        if (mImageBorder != null && !mImageBorder.isNil()) {
+            return false;
+        }
+        if (mGeoData.hasModifications()) {
+            return false;
+        }
+        for (ImageFilter filter : mFilters) {
+            if (filter.getFilterType() == ImageFilter.TYPE_VIGNETTE
+                    && !filter.isNil()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+
     public void setGeometry(GeometryMetadata m) {
         mGeoData.set(m);
     }
diff --git a/src/com/android/gallery3d/filtershow/tools/ProcessedBitmap.java b/src/com/android/gallery3d/filtershow/tools/ProcessedBitmap.java
deleted file mode 100644
index 24e38fb..0000000
--- a/src/com/android/gallery3d/filtershow/tools/ProcessedBitmap.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.android.gallery3d.filtershow.tools;
-
-import android.graphics.Bitmap;
-
-import com.android.gallery3d.filtershow.presets.ImagePreset;
-
-public class ProcessedBitmap {
-    private Bitmap mBitmap;
-    private final ImagePreset mPreset;
-    public ProcessedBitmap(Bitmap bitmap, ImagePreset preset) {
-        mBitmap = bitmap;
-        mPreset = preset;
-    }
-    public Bitmap apply() {
-        mBitmap = mPreset.apply(mBitmap);
-        return mBitmap;
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java b/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java
index 8b3eb53..9c55623 100644
--- a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java
+++ b/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java
@@ -21,26 +21,26 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.graphics.Bitmap.CompressFormat;
+import android.media.ExifInterface;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Environment;
 import android.provider.MediaStore.Images;
 import android.provider.MediaStore.Images.ImageColumns;
-import android.view.Gravity;
-import android.widget.Toast;
+import android.util.Log;
 
-import com.android.camera.R;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
-
-//import com.android.gallery3d.R;
-//import com.android.gallery3d.util.BucketNames;
+import com.android.gallery3d.util.XmpUtilHelper;
 
 import java.io.Closeable;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
 import java.sql.Date;
 import java.text.SimpleDateFormat;
@@ -48,24 +48,29 @@
 /**
  * Asynchronous task for saving edited photo as a new copy.
  */
-public class SaveCopyTask extends AsyncTask<ProcessedBitmap, Void, Uri> {
+public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
 
+
+    private static final String LOGTAG = "SaveCopyTask";
     private static final int DEFAULT_COMPRESS_QUALITY = 95;
     private static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos";
 
     /**
      * Saves the bitmap in the final destination
      */
-    public static void saveBitmap(Bitmap bitmap, File destination) {
+    public static void saveBitmap(Bitmap bitmap, File destination, Object xmp) {
         OutputStream os = null;
         try {
             os = new FileOutputStream(destination);
             bitmap.compress(CompressFormat.JPEG, DEFAULT_COMPRESS_QUALITY, os);
         } catch (FileNotFoundException e) {
-            e.printStackTrace();
+            Log.v(LOGTAG,"Error in writing "+destination.getAbsolutePath());
         } finally {
             closeStream(os);
         }
+        if (xmp != null) {
+            XmpUtilHelper.writeXMPMeta(destination.getAbsolutePath(), xmp);
+        }
     }
 
     private static void closeStream(Closeable stream) {
@@ -132,24 +137,116 @@
         return new File(saveDirectory, filename + ".JPG");
     }
 
+    private Bitmap loadMutableBitmap() throws FileNotFoundException {
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        // TODO: on <3.x we need a copy of the bitmap (inMutable doesn't
+        // exist)
+        options.inMutable = true;
+
+        InputStream is = context.getContentResolver().openInputStream(sourceUri);
+        Bitmap bitmap = BitmapFactory.decodeStream(is, null, options);
+        int orientation = ImageLoader.getOrientation(context, sourceUri);
+        bitmap = ImageLoader.rotateToPortrait(bitmap, orientation);
+        return bitmap;
+    }
+
+    private static final String[] COPY_EXIF_ATTRIBUTES = new String[] {
+        ExifInterface.TAG_APERTURE,
+        ExifInterface.TAG_DATETIME,
+        ExifInterface.TAG_EXPOSURE_TIME,
+        ExifInterface.TAG_FLASH,
+        ExifInterface.TAG_FOCAL_LENGTH,
+        ExifInterface.TAG_GPS_ALTITUDE,
+        ExifInterface.TAG_GPS_ALTITUDE_REF,
+        ExifInterface.TAG_GPS_DATESTAMP,
+        ExifInterface.TAG_GPS_LATITUDE,
+        ExifInterface.TAG_GPS_LATITUDE_REF,
+        ExifInterface.TAG_GPS_LONGITUDE,
+        ExifInterface.TAG_GPS_LONGITUDE_REF,
+        ExifInterface.TAG_GPS_PROCESSING_METHOD,
+        ExifInterface.TAG_GPS_DATESTAMP,
+        ExifInterface.TAG_ISO,
+        ExifInterface.TAG_MAKE,
+        ExifInterface.TAG_MODEL,
+        ExifInterface.TAG_WHITE_BALANCE,
+    };
+
+    private static void copyExif(String sourcePath, String destPath) {
+        try {
+            ExifInterface source = new ExifInterface(sourcePath);
+            ExifInterface dest = new ExifInterface(destPath);
+            boolean needsSave = false;
+            for (String tag : COPY_EXIF_ATTRIBUTES) {
+                String value = source.getAttribute(tag);
+                if (value != null) {
+                    needsSave = true;
+                    dest.setAttribute(tag, value);
+                }
+            }
+            if (needsSave) {
+                dest.saveAttributes();
+            }
+        } catch (IOException ex) {
+            Log.w(LOGTAG, "Failed to copy exif metadata", ex);
+        }
+    }
+
+    private void copyExif(Uri sourceUri, String destPath) {
+        if (ContentResolver.SCHEME_FILE.equals(sourceUri.getScheme())) {
+            copyExif(sourceUri.getPath(), destPath);
+            return;
+        }
+
+        final String[] PROJECTION = new String[] {
+                ImageColumns.DATA
+        };
+        try {
+            Cursor c = context.getContentResolver().query(sourceUri, PROJECTION,
+                    null, null, null);
+            if (c.moveToFirst()) {
+                String path = c.getString(0);
+                if (new File(path).exists()) {
+                    copyExif(path, destPath);
+                }
+            }
+            c.close();
+        } catch (Exception e) {
+            Log.w(LOGTAG, "Failed to copy exif", e);
+        }
+    }
+
     /**
      * The task should be executed with one given bitmap to be saved.
      */
     @Override
-    protected Uri doInBackground(ProcessedBitmap... params) {
+    protected Uri doInBackground(ImagePreset... params) {
         // TODO: Support larger dimensions for photo saving.
         if (params[0] == null) {
             return null;
         }
 
-        ProcessedBitmap processedBitmap = params[0];
+        ImagePreset preset = params[0];
 
-        Bitmap bitmap = processedBitmap.apply();
-        saveBitmap(bitmap, this.destinationFile);
+        try {
+            Bitmap bitmap = preset.apply(loadMutableBitmap());
 
-        Uri uri = insertContent(context, sourceUri, this.destinationFile, saveFileName);
-        bitmap.recycle();
-        return uri;
+            Object xmp = null;
+            InputStream is = null;
+            if (preset.isPanoramaSafe()) {
+                is = context.getContentResolver().openInputStream(sourceUri);
+                xmp =  XmpUtilHelper.extractXMPMeta(is);
+            }
+            saveBitmap(bitmap, this.destinationFile, xmp);
+            copyExif(sourceUri, destinationFile.getAbsolutePath());
+
+            Uri uri = insertContent(context, sourceUri, this.destinationFile, saveFileName);
+            bitmap.recycle();
+            return uri;
+
+        } catch (FileNotFoundException ex) {
+            Log.w(LOGTAG, "Failed to save image!", ex);
+            return null;
+        }
     }
 
     @Override
@@ -210,11 +307,12 @@
         values.put(Images.Media.DATA, file.getAbsolutePath());
         values.put(Images.Media.SIZE, file.length());
 
-        String[] projection = new String[] {
+        final String[] projection = new String[] {
                 ImageColumns.DATE_TAKEN,
                 ImageColumns.LATITUDE, ImageColumns.LONGITUDE,
         };
-        querySource(context, sourceUri, projection, new ContentResolverQueryCallback() {
+        querySource(context, sourceUri, projection,
+                new ContentResolverQueryCallback() {
 
             @Override
             public void onCursorResult(Cursor cursor) {
diff --git a/src_pd/com/android/gallery3d/util/XmpUtilHelper.java b/src_pd/com/android/gallery3d/util/XmpUtilHelper.java
new file mode 100644
index 0000000..7cbd7f1
--- /dev/null
+++ b/src_pd/com/android/gallery3d/util/XmpUtilHelper.java
@@ -0,0 +1,31 @@
+/*
+ * 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.util;
+
+import java.io.InputStream;
+
+public class XmpUtilHelper {
+
+    public static Object extractXMPMeta(InputStream is) {
+        return null;
+    }
+
+    public static boolean writeXMPMeta(String filename, Object meta) {
+        return false;
+    }
+
+}