2 * Copyright (C) 2010 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.gallery3d.filtershow.tools;
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;
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;
41 import java.io.FileNotFoundException;
42 import java.io.IOException;
43 import java.io.InputStream;
45 import java.text.SimpleDateFormat;
46 import java.util.TimeZone;
49 * Asynchronous task for saving edited photo as a new copy.
51 public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
53 private static final String LOGTAG = "SaveCopyTask";
56 * Callback for the completed asynchronous task.
58 public interface Callback {
60 void onComplete(Uri result);
63 private interface ContentResolverQueryCallback {
65 void onCursorResult(Cursor cursor);
68 private static final String TIME_STAMP_NAME = "'IMG'_yyyyMMdd_HHmmss";
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;
76 public SaveCopyTask(Context context, Uri sourceUri, File destination, Callback callback) {
77 this.context = context;
78 this.sourceUri = sourceUri;
79 this.callback = callback;
81 if (destination == null) {
82 this.destinationFile = getNewFile(context, sourceUri);
84 this.destinationFile = destination;
87 saveFileName = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(
88 System.currentTimeMillis()));
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);
97 // Create the directory if it doesn't exist
98 if (!saveDirectory.exists())
99 saveDirectory.mkdirs();
100 return saveDirectory;
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");
110 public Object getPanoramaXMPData(Uri source, ImagePreset preset) {
112 if (preset.isPanoramaSafe()) {
113 InputStream is = null;
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);
120 Utils.closeSilently(is);
126 public boolean putPanoramaXMPData(File file, Object xmp) {
128 return XmpUtilHelper.writeXMPMeta(file.getAbsolutePath(), xmp);
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;
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);
146 Utils.closeSilently(inStream);
152 public boolean putExifData(File file, ExifInterface exif, Bitmap image) {
155 exif.writeExif(image, file.getAbsolutePath());
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);
166 * The task should be executed with one given bitmap to be saved.
169 protected Uri doInBackground(ImagePreset... params) {
170 // TODO: Support larger dimensions for photo saving.
171 if (params[0] == null || sourceUri == null) {
174 ImagePreset preset = params[0];
175 BitmapFactory.Options options = new BitmapFactory.Options();
177 boolean noBitmap = true;
179 // Stopgap fix for low-memory devices.
182 // Try to do bitmap operations, downsample if low-memory
183 Bitmap bitmap = ImageLoader.loadMutableBitmap(context, sourceUri, options);
184 if (bitmap == null) {
187 CachingPipeline pipeline = new CachingPipeline(FiltersManager.getManager(), "Saving");
188 bitmap = pipeline.renderFinalImage(bitmap, preset);
190 Object xmp = getPanoramaXMPData(sourceUri, preset);
191 ExifInterface exif = getExifData(sourceUri);
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));
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,
206 XmpPresets.writeFilterXMP(context, sourceUri, this.destinationFile, preset);
209 } catch (java.lang.OutOfMemoryError e) {
210 // Try 5 times before failing for good.
211 if (++num_tries >= 5) {
215 options.inSampleSize *= 2;
223 protected void onPostExecute(Uri result) {
224 if (callback != null) {
225 callback.onComplete(result);
229 private static void querySource(Context context, Uri sourceUri, String[] projection,
230 ContentResolverQueryCallback callback) {
231 ContentResolver contentResolver = context.getContentResolver();
232 Cursor cursor = null;
234 cursor = contentResolver.query(sourceUri, projection, null, null,
236 if ((cursor != null) && cursor.moveToNext()) {
237 callback.onCursorResult(cursor);
239 } catch (Exception e) {
240 // Ignore error for lacking the data column from the source.
242 if (cursor != null) {
248 private static File getSaveDirectory(Context context, Uri sourceUri) {
249 final File[] dir = new File[1];
250 querySource(context, sourceUri, new String[] {
253 new ContentResolverQueryCallback() {
256 public void onCursorResult(Cursor cursor) {
257 dir[0] = new File(cursor.getString(0)).getParentFile();
264 * Insert the content (saved file) with proper source photo properties.
266 public static Uri insertContent(Context context, Uri sourceUri, File file, String saveFileName,
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());
281 final String[] projection = new String[] {
282 ImageColumns.DATE_TAKEN,
283 ImageColumns.LATITUDE, ImageColumns.LONGITUDE,
285 querySource(context, sourceUri, projection,
286 new ContentResolverQueryCallback() {
289 public void onCursorResult(Cursor cursor) {
290 values.put(Images.Media.DATE_TAKEN, cursor.getLong(0));
292 double latitude = cursor.getDouble(1);
293 double longitude = cursor.getDouble(2);
294 // TODO: Change || to && after the default location
296 if ((latitude != 0f) || (longitude != 0f)) {
297 values.put(Images.Media.LATITUDE, latitude);
298 values.put(Images.Media.LONGITUDE, longitude);
303 return context.getContentResolver().insert(
304 Images.Media.EXTERNAL_CONTENT_URI, values);