OSDN Git Service

Support for save state in images
[android-x86/packages-apps-Gallery2.git] / src / com / android / gallery3d / filtershow / tools / SaveCopyTask.java
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.gallery3d.filtershow.tools;
18
19 import android.content.ContentResolver;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.database.Cursor;
23 import android.graphics.Bitmap;
24 import android.graphics.BitmapFactory;
25 import android.net.Uri;
26 import android.os.AsyncTask;
27 import android.os.Environment;
28 import android.provider.MediaStore.Images;
29 import android.provider.MediaStore.Images.ImageColumns;
30 import android.util.Log;
31
32 import com.android.gallery3d.common.Utils;
33 import com.android.gallery3d.exif.ExifInterface;
34 import com.android.gallery3d.filtershow.cache.CachingPipeline;
35 import com.android.gallery3d.filtershow.cache.ImageLoader;
36 import com.android.gallery3d.filtershow.filters.FiltersManager;
37 import com.android.gallery3d.filtershow.presets.ImagePreset;
38 import com.android.gallery3d.util.XmpUtilHelper;
39
40 import java.io.File;
41 import java.io.FileNotFoundException;
42 import java.io.IOException;
43 import java.io.InputStream;
44 import java.sql.Date;
45 import java.text.SimpleDateFormat;
46 import java.util.TimeZone;
47
48 /**
49  * Asynchronous task for saving edited photo as a new copy.
50  */
51 public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
52
53     private static final String LOGTAG = "SaveCopyTask";
54
55     /**
56      * Callback for the completed asynchronous task.
57      */
58     public interface Callback {
59
60         void onComplete(Uri result);
61     }
62
63     private interface ContentResolverQueryCallback {
64
65         void onCursorResult(Cursor cursor);
66     }
67
68     private static final String TIME_STAMP_NAME = "'IMG'_yyyyMMdd_HHmmss";
69
70     private final Context context;
71     private final Uri sourceUri;
72     private final Callback callback;
73     private final String saveFileName;
74     private final File destinationFile;
75
76     public SaveCopyTask(Context context, Uri sourceUri, File destination, Callback callback) {
77         this.context = context;
78         this.sourceUri = sourceUri;
79         this.callback = callback;
80
81         if (destination == null) {
82             this.destinationFile = getNewFile(context, sourceUri);
83         } else {
84             this.destinationFile = destination;
85         }
86
87         saveFileName = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(
88                 System.currentTimeMillis()));
89     }
90
91     public static File getFinalSaveDirectory(Context context, Uri sourceUri) {
92         File saveDirectory = getSaveDirectory(context, sourceUri);
93         if ((saveDirectory == null) || !saveDirectory.canWrite()) {
94             saveDirectory = new File(Environment.getExternalStorageDirectory(),
95                     ImageLoader.DEFAULT_SAVE_DIRECTORY);
96         }
97         // Create the directory if it doesn't exist
98         if (!saveDirectory.exists())
99             saveDirectory.mkdirs();
100         return saveDirectory;
101     }
102
103     public static File getNewFile(Context context, Uri sourceUri) {
104         File saveDirectory = getFinalSaveDirectory(context, sourceUri);
105         String filename = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(
106                 System.currentTimeMillis()));
107         return new File(saveDirectory, filename + ".JPG");
108     }
109
110     public Object getPanoramaXMPData(Uri source, ImagePreset preset) {
111         Object xmp = null;
112         if (preset.isPanoramaSafe()) {
113             InputStream is = null;
114             try {
115                 is = context.getContentResolver().openInputStream(source);
116                 xmp = XmpUtilHelper.extractXMPMeta(is);
117             } catch (FileNotFoundException e) {
118                 Log.w(LOGTAG, "Failed to get XMP data from image: ", e);
119             } finally {
120                 Utils.closeSilently(is);
121             }
122         }
123         return xmp;
124     }
125
126     public boolean putPanoramaXMPData(File file, Object xmp) {
127         if (xmp != null) {
128             return XmpUtilHelper.writeXMPMeta(file.getAbsolutePath(), xmp);
129         }
130         return false;
131     }
132
133     public ExifInterface getExifData(Uri source) {
134         ExifInterface exif = new ExifInterface();
135         String mimeType = context.getContentResolver().getType(sourceUri);
136         if (mimeType == ImageLoader.JPEG_MIME_TYPE) {
137             InputStream inStream = null;
138             try {
139                 inStream = context.getContentResolver().openInputStream(source);
140                 exif.readExif(inStream);
141             } catch (FileNotFoundException e) {
142                 Log.w(LOGTAG, "Cannot find file: " + source, e);
143             } catch (IOException e) {
144                 Log.w(LOGTAG, "Cannot read exif for: " + source, e);
145             } finally {
146                 Utils.closeSilently(inStream);
147             }
148         }
149         return exif;
150     }
151
152     public boolean putExifData(File file, ExifInterface exif, Bitmap image) {
153         boolean ret = false;
154         try {
155             exif.writeExif(image, file.getAbsolutePath());
156             ret = true;
157         } catch (FileNotFoundException e) {
158             Log.w(LOGTAG, "File not found: " + file.getAbsolutePath(), e);
159         } catch (IOException e) {
160             Log.w(LOGTAG, "Could not write exif: ", e);
161         }
162         return ret;
163     }
164
165     /**
166      * The task should be executed with one given bitmap to be saved.
167      */
168     @Override
169     protected Uri doInBackground(ImagePreset... params) {
170         // TODO: Support larger dimensions for photo saving.
171         if (params[0] == null || sourceUri == null) {
172             return null;
173         }
174         ImagePreset preset = params[0];
175         BitmapFactory.Options options = new BitmapFactory.Options();
176         Uri uri = null;
177         boolean noBitmap = true;
178         int num_tries = 0;
179         // Stopgap fix for low-memory devices.
180         while (noBitmap) {
181             try {
182                 // Try to do bitmap operations, downsample if low-memory
183                 Bitmap bitmap = ImageLoader.loadMutableBitmap(context, sourceUri, options);
184                 if (bitmap == null) {
185                     return null;
186                 }
187                 CachingPipeline pipeline = new CachingPipeline(FiltersManager.getManager(), "Saving");
188                 bitmap = pipeline.renderFinalImage(bitmap, preset);
189
190                 Object xmp = getPanoramaXMPData(sourceUri, preset);
191                 ExifInterface exif = getExifData(sourceUri);
192
193                 // Set tags
194                 long time = System.currentTimeMillis();
195                 exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, time,
196                         TimeZone.getDefault());
197                 exif.setTag(exif.buildTag(ExifInterface.TAG_ORIENTATION,
198                         ExifInterface.Orientation.TOP_LEFT));
199
200                 // If we succeed in writing the bitmap as a jpeg, return a uri.
201                 if (putExifData(this.destinationFile, exif, bitmap)) {
202                     putPanoramaXMPData(this.destinationFile, xmp);
203                     uri = insertContent(context, sourceUri, this.destinationFile, saveFileName,
204                             time);
205                 }
206                 XmpPresets.writeFilterXMP(context, sourceUri, this.destinationFile, preset);
207
208                 noBitmap = false;
209             } catch (java.lang.OutOfMemoryError e) {
210                 // Try 5 times before failing for good.
211                 if (++num_tries >= 5) {
212                     throw e;
213                 }
214                 System.gc();
215                 options.inSampleSize *= 2;
216             }
217         }
218         return uri;
219     }
220
221
222     @Override
223     protected void onPostExecute(Uri result) {
224         if (callback != null) {
225             callback.onComplete(result);
226         }
227     }
228
229     private static void querySource(Context context, Uri sourceUri, String[] projection,
230             ContentResolverQueryCallback callback) {
231         ContentResolver contentResolver = context.getContentResolver();
232         Cursor cursor = null;
233         try {
234             cursor = contentResolver.query(sourceUri, projection, null, null,
235                     null);
236             if ((cursor != null) && cursor.moveToNext()) {
237                 callback.onCursorResult(cursor);
238             }
239         } catch (Exception e) {
240             // Ignore error for lacking the data column from the source.
241         } finally {
242             if (cursor != null) {
243                 cursor.close();
244             }
245         }
246     }
247
248     private static File getSaveDirectory(Context context, Uri sourceUri) {
249         final File[] dir = new File[1];
250         querySource(context, sourceUri, new String[] {
251                 ImageColumns.DATA
252         },
253                 new ContentResolverQueryCallback() {
254
255                     @Override
256                     public void onCursorResult(Cursor cursor) {
257                         dir[0] = new File(cursor.getString(0)).getParentFile();
258                     }
259                 });
260         return dir[0];
261     }
262
263     /**
264      * Insert the content (saved file) with proper source photo properties.
265      */
266     public static Uri insertContent(Context context, Uri sourceUri, File file, String saveFileName,
267             long time) {
268         time /= 1000;
269
270         final ContentValues values = new ContentValues();
271         values.put(Images.Media.TITLE, saveFileName);
272         values.put(Images.Media.DISPLAY_NAME, file.getName());
273         values.put(Images.Media.MIME_TYPE, "image/jpeg");
274         values.put(Images.Media.DATE_TAKEN, time);
275         values.put(Images.Media.DATE_MODIFIED, time);
276         values.put(Images.Media.DATE_ADDED, time);
277         values.put(Images.Media.ORIENTATION, 0);
278         values.put(Images.Media.DATA, file.getAbsolutePath());
279         values.put(Images.Media.SIZE, file.length());
280
281         final String[] projection = new String[] {
282                 ImageColumns.DATE_TAKEN,
283                 ImageColumns.LATITUDE, ImageColumns.LONGITUDE,
284         };
285         querySource(context, sourceUri, projection,
286                 new ContentResolverQueryCallback() {
287
288                     @Override
289                     public void onCursorResult(Cursor cursor) {
290                         values.put(Images.Media.DATE_TAKEN, cursor.getLong(0));
291
292                         double latitude = cursor.getDouble(1);
293                         double longitude = cursor.getDouble(2);
294                         // TODO: Change || to && after the default location
295                         // issue is fixed.
296                         if ((latitude != 0f) || (longitude != 0f)) {
297                             values.put(Images.Media.LATITUDE, latitude);
298                             values.put(Images.Media.LONGITUDE, longitude);
299                         }
300                     }
301                 });
302
303         return context.getContentResolver().insert(
304                 Images.Media.EXTERNAL_CONTENT_URI, values);
305     }
306
307 }