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.graphics.Bitmap.CompressFormat; 26 import android.media.ExifInterface; 27 import android.net.Uri; 28 import android.os.AsyncTask; 29 import android.os.Environment; 30 import android.provider.MediaStore.Images; 31 import android.provider.MediaStore.Images.ImageColumns; 32 import android.util.Log; 33 34 import com.android.gallery3d.filtershow.cache.ImageLoader; 35 import com.android.gallery3d.filtershow.presets.ImagePreset; 36 import com.android.gallery3d.util.XmpUtilHelper; 37 38 import java.io.Closeable; 39 import java.io.File; 40 import java.io.FileNotFoundException; 41 import java.io.FileOutputStream; 42 import java.io.IOException; 43 import java.io.InputStream; 44 import java.io.OutputStream; 45 import java.sql.Date; 46 import java.text.SimpleDateFormat; 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 54 private static final String LOGTAG = "SaveCopyTask"; 55 private static final int DEFAULT_COMPRESS_QUALITY = 95; 56 private static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos"; 57 58 /** 59 * Saves the bitmap in the final destination 60 */ 61 public static void saveBitmap(Bitmap bitmap, File destination, Object xmp) { 62 OutputStream os = null; 63 try { 64 os = new FileOutputStream(destination); 65 bitmap.compress(CompressFormat.JPEG, DEFAULT_COMPRESS_QUALITY, os); 66 } catch (FileNotFoundException e) { 67 Log.v(LOGTAG,"Error in writing "+destination.getAbsolutePath()); 68 } finally { 69 closeStream(os); 70 } 71 if (xmp != null) { 72 XmpUtilHelper.writeXMPMeta(destination.getAbsolutePath(), xmp); 73 } 74 } 75 76 private static void closeStream(Closeable stream) { 77 if (stream != null) { 78 try { 79 stream.close(); 80 } catch (IOException e) { 81 e.printStackTrace(); 82 } 83 } 84 } 85 86 /** 87 * Callback for the completed asynchronous task. 88 */ 89 public interface Callback { 90 91 void onComplete(Uri result); 92 } 93 94 private interface ContentResolverQueryCallback { 95 96 void onCursorResult(Cursor cursor); 97 } 98 99 private static final String TIME_STAMP_NAME = "'IMG'_yyyyMMdd_HHmmss"; 100 101 private final Context context; 102 private final Uri sourceUri; 103 private final Callback callback; 104 private final String saveFileName; 105 private final File destinationFile; 106 107 public SaveCopyTask(Context context, Uri sourceUri, File destination, Callback callback) { 108 this.context = context; 109 this.sourceUri = sourceUri; 110 this.callback = callback; 111 112 if (destination == null) { 113 this.destinationFile = getNewFile(context, sourceUri); 114 } else { 115 this.destinationFile = destination; 116 } 117 118 saveFileName = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date( 119 System.currentTimeMillis())); 120 } 121 122 public static File getFinalSaveDirectory(Context context, Uri sourceUri) { 123 File saveDirectory = getSaveDirectory(context, sourceUri); 124 if ((saveDirectory == null) || !saveDirectory.canWrite()) { 125 saveDirectory = new File(Environment.getExternalStorageDirectory(), 126 DEFAULT_SAVE_DIRECTORY); 127 } 128 // Create the directory if it doesn't exist 129 if (!saveDirectory.exists()) saveDirectory.mkdirs(); 130 return saveDirectory; 131 } 132 133 public static File getNewFile(Context context, Uri sourceUri) { 134 File saveDirectory = getFinalSaveDirectory(context, sourceUri); 135 String filename = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date( 136 System.currentTimeMillis())); 137 return new File(saveDirectory, filename + ".JPG"); 138 } 139 140 private Bitmap loadMutableBitmap() throws FileNotFoundException { 141 BitmapFactory.Options options = new BitmapFactory.Options(); 142 // TODO: on <3.x we need a copy of the bitmap (inMutable doesn't 143 // exist) 144 options.inMutable = true; 145 146 InputStream is = context.getContentResolver().openInputStream(sourceUri); 147 Bitmap bitmap = BitmapFactory.decodeStream(is, null, options); 148 int orientation = ImageLoader.getOrientation(context, sourceUri); 149 bitmap = ImageLoader.rotateToPortrait(bitmap, orientation); 150 return bitmap; 151 } 152 153 private static final String[] COPY_EXIF_ATTRIBUTES = new String[] { 154 ExifInterface.TAG_APERTURE, 155 ExifInterface.TAG_DATETIME, 156 ExifInterface.TAG_EXPOSURE_TIME, 157 ExifInterface.TAG_FLASH, 158 ExifInterface.TAG_FOCAL_LENGTH, 159 ExifInterface.TAG_GPS_ALTITUDE, 160 ExifInterface.TAG_GPS_ALTITUDE_REF, 161 ExifInterface.TAG_GPS_DATESTAMP, 162 ExifInterface.TAG_GPS_LATITUDE, 163 ExifInterface.TAG_GPS_LATITUDE_REF, 164 ExifInterface.TAG_GPS_LONGITUDE, 165 ExifInterface.TAG_GPS_LONGITUDE_REF, 166 ExifInterface.TAG_GPS_PROCESSING_METHOD, 167 ExifInterface.TAG_GPS_DATESTAMP, 168 ExifInterface.TAG_ISO, 169 ExifInterface.TAG_MAKE, 170 ExifInterface.TAG_MODEL, 171 ExifInterface.TAG_WHITE_BALANCE, 172 }; 173 174 private static void copyExif(String sourcePath, String destPath) { 175 try { 176 ExifInterface source = new ExifInterface(sourcePath); 177 ExifInterface dest = new ExifInterface(destPath); 178 boolean needsSave = false; 179 for (String tag : COPY_EXIF_ATTRIBUTES) { 180 String value = source.getAttribute(tag); 181 if (value != null) { 182 needsSave = true; 183 dest.setAttribute(tag, value); 184 } 185 } 186 if (needsSave) { 187 dest.saveAttributes(); 188 } 189 } catch (IOException ex) { 190 Log.w(LOGTAG, "Failed to copy exif metadata", ex); 191 } 192 } 193 194 private void copyExif(Uri sourceUri, String destPath) { 195 if (ContentResolver.SCHEME_FILE.equals(sourceUri.getScheme())) { 196 copyExif(sourceUri.getPath(), destPath); 197 return; 198 } 199 200 final String[] PROJECTION = new String[] { 201 ImageColumns.DATA 202 }; 203 try { 204 Cursor c = context.getContentResolver().query(sourceUri, PROJECTION, 205 null, null, null); 206 if (c.moveToFirst()) { 207 String path = c.getString(0); 208 if (new File(path).exists()) { 209 copyExif(path, destPath); 210 } 211 } 212 c.close(); 213 } catch (Exception e) { 214 Log.w(LOGTAG, "Failed to copy exif", e); 215 } 216 } 217 218 /** 219 * The task should be executed with one given bitmap to be saved. 220 */ 221 @Override 222 protected Uri doInBackground(ImagePreset... params) { 223 // TODO: Support larger dimensions for photo saving. 224 if (params[0] == null) { 225 return null; 226 } 227 228 ImagePreset preset = params[0]; 229 230 try { 231 Bitmap bitmap = preset.apply(loadMutableBitmap()); 232 233 Object xmp = null; 234 InputStream is = null; 235 if (preset.isPanoramaSafe()) { 236 is = context.getContentResolver().openInputStream(sourceUri); 237 xmp = XmpUtilHelper.extractXMPMeta(is); 238 } 239 saveBitmap(bitmap, this.destinationFile, xmp); 240 copyExif(sourceUri, destinationFile.getAbsolutePath()); 241 242 Uri uri = insertContent(context, sourceUri, this.destinationFile, saveFileName); 243 bitmap.recycle(); 244 return uri; 245 246 } catch (FileNotFoundException ex) { 247 Log.w(LOGTAG, "Failed to save image!", ex); 248 return null; 249 } 250 } 251 252 @Override 253 protected void onPostExecute(Uri result) { 254 if (callback != null) { 255 callback.onComplete(result); 256 } 257 } 258 259 private static void querySource(Context context, Uri sourceUri, String[] projection, 260 ContentResolverQueryCallback callback) { 261 ContentResolver contentResolver = context.getContentResolver(); 262 Cursor cursor = null; 263 try { 264 cursor = contentResolver.query(sourceUri, projection, null, null, 265 null); 266 if ((cursor != null) && cursor.moveToNext()) { 267 callback.onCursorResult(cursor); 268 } 269 } catch (Exception e) { 270 // Ignore error for lacking the data column from the source. 271 } finally { 272 if (cursor != null) { 273 cursor.close(); 274 } 275 } 276 } 277 278 private static File getSaveDirectory(Context context, Uri sourceUri) { 279 final File[] dir = new File[1]; 280 querySource(context, sourceUri, new String[] { 281 ImageColumns.DATA 282 }, 283 new ContentResolverQueryCallback() { 284 285 @Override 286 public void onCursorResult(Cursor cursor) { 287 dir[0] = new File(cursor.getString(0)).getParentFile(); 288 } 289 }); 290 return dir[0]; 291 } 292 293 /** 294 * Insert the content (saved file) with proper source photo properties. 295 */ 296 public static Uri insertContent(Context context, Uri sourceUri, File file, String saveFileName) { 297 long now = System.currentTimeMillis() / 1000; 298 299 final ContentValues values = new ContentValues(); 300 values.put(Images.Media.TITLE, saveFileName); 301 values.put(Images.Media.DISPLAY_NAME, file.getName()); 302 values.put(Images.Media.MIME_TYPE, "image/jpeg"); 303 values.put(Images.Media.DATE_TAKEN, now); 304 values.put(Images.Media.DATE_MODIFIED, now); 305 values.put(Images.Media.DATE_ADDED, now); 306 values.put(Images.Media.ORIENTATION, 0); 307 values.put(Images.Media.DATA, file.getAbsolutePath()); 308 values.put(Images.Media.SIZE, file.length()); 309 310 final String[] projection = new String[] { 311 ImageColumns.DATE_TAKEN, 312 ImageColumns.LATITUDE, ImageColumns.LONGITUDE, 313 }; 314 querySource(context, sourceUri, projection, 315 new ContentResolverQueryCallback() { 316 317 @Override 318 public void onCursorResult(Cursor cursor) { 319 values.put(Images.Media.DATE_TAKEN, cursor.getLong(0)); 320 321 double latitude = cursor.getDouble(1); 322 double longitude = cursor.getDouble(2); 323 // TODO: Change || to && after the default location issue is 324 // fixed. 325 if ((latitude != 0f) || (longitude != 0f)) { 326 values.put(Images.Media.LATITUDE, latitude); 327 values.put(Images.Media.LONGITUDE, longitude); 328 } 329 } 330 }); 331 332 return context.getContentResolver().insert( 333 Images.Media.EXTERNAL_CONTENT_URI, values); 334 } 335 336 } 337