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.content.Intent; 23 import android.database.Cursor; 24 import android.graphics.Bitmap; 25 import android.net.Uri; 26 import android.os.Environment; 27 import android.provider.MediaStore; 28 import android.provider.MediaStore.Images; 29 import android.provider.MediaStore.Images.ImageColumns; 30 import android.util.Log; 31 import android.widget.Toast; 32 33 import com.android.gallery3d.R; 34 import com.android.gallery3d.common.Utils; 35 import com.android.gallery3d.exif.ExifInterface; 36 import com.android.gallery3d.filtershow.FilterShowActivity; 37 import com.android.gallery3d.filtershow.cache.ImageLoader; 38 import com.android.gallery3d.filtershow.filters.FilterRepresentation; 39 import com.android.gallery3d.filtershow.filters.FiltersManager; 40 import com.android.gallery3d.filtershow.imageshow.MasterImage; 41 import com.android.gallery3d.filtershow.pipeline.CachingPipeline; 42 import com.android.gallery3d.filtershow.pipeline.ImagePreset; 43 import com.android.gallery3d.filtershow.pipeline.ProcessingService; 44 import com.android.gallery3d.util.XmpUtilHelper; 45 46 import java.io.File; 47 import java.io.FileNotFoundException; 48 import java.io.FilenameFilter; 49 import java.io.IOException; 50 import java.io.InputStream; 51 import java.io.OutputStream; 52 import java.sql.Date; 53 import java.text.SimpleDateFormat; 54 import java.util.TimeZone; 55 56 /** 57 * Handles saving edited photo 58 */ 59 public class SaveImage { 60 private static final String LOGTAG = "SaveImage"; 61 62 /** 63 * Callback for updates 64 */ 65 public interface Callback { 66 void onPreviewSaved(Uri uri); 67 void onProgress(int max, int current); 68 } 69 70 public interface ContentResolverQueryCallback { 71 void onCursorResult(Cursor cursor); 72 } 73 74 private static final String TIME_STAMP_NAME = "_yyyyMMdd_HHmmss"; 75 private static final String PREFIX_PANO = "PANO"; 76 private static final String PREFIX_IMG = "IMG"; 77 private static final String POSTFIX_JPG = ".jpg"; 78 private static final String AUX_DIR_NAME = ".aux"; 79 80 private final Context mContext; 81 private final Uri mSourceUri; 82 private final Callback mCallback; 83 private final File mDestinationFile; 84 private final Uri mSelectedImageUri; 85 private final Bitmap mPreviewImage; 86 87 private int mCurrentProcessingStep = 1; 88 89 public static final int MAX_PROCESSING_STEPS = 6; 90 public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos"; 91 92 // In order to support the new edit-save behavior such that user won't see 93 // the edited image together with the original image, we are adding a new 94 // auxiliary directory for the edited image. Basically, the original image 95 // will be hidden in that directory after edit and user will see the edited 96 // image only. 97 // Note that deletion on the edited image will also cause the deletion of 98 // the original image under auxiliary directory. 99 // 100 // There are several situations we need to consider: 101 // 1. User edit local image local01.jpg. A local02.jpg will be created in the 102 // same directory, and original image will be moved to auxiliary directory as 103 // ./.aux/local02.jpg. 104 // If user edit the local02.jpg, local03.jpg will be created in the local 105 // directory and ./.aux/local02.jpg will be renamed to ./.aux/local03.jpg 106 // 107 // 2. User edit remote image remote01.jpg from picassa or other server. 108 // remoteSavedLocal01.jpg will be saved under proper local directory. 109 // In remoteSavedLocal01.jpg, there will be a reference pointing to the 110 // remote01.jpg. There will be no local copy of remote01.jpg. 111 // If user edit remoteSavedLocal01.jpg, then a new remoteSavedLocal02.jpg 112 // will be generated and still pointing to the remote01.jpg 113 // 114 // 3. User delete any local image local.jpg. 115 // Since the filenames are kept consistent in auxiliary directory, every 116 // time a local.jpg get deleted, the files in auxiliary directory whose 117 // names starting with "local." will be deleted. 118 // This pattern will facilitate the multiple images deletion in the auxiliary 119 // directory. 120 121 /** 122 * @param context 123 * @param sourceUri The Uri for the original image, which can be the hidden 124 * image under the auxiliary directory or the same as selectedImageUri. 125 * @param selectedImageUri The Uri for the image selected by the user. 126 * In most cases, it is a content Uri for local image or remote image. 127 * @param destination Destinaton File, if this is null, a new file will be 128 * created under the same directory as selectedImageUri. 129 * @param callback Let the caller know the saving has completed. 130 * @return the newSourceUri 131 */ 132 public SaveImage(Context context, Uri sourceUri, Uri selectedImageUri, 133 File destination, Bitmap previewImage, Callback callback) { 134 mContext = context; 135 mSourceUri = sourceUri; 136 mCallback = callback; 137 mPreviewImage = previewImage; 138 if (destination == null) { 139 mDestinationFile = getNewFile(context, selectedImageUri); 140 } else { 141 mDestinationFile = destination; 142 } 143 144 mSelectedImageUri = selectedImageUri; 145 } 146 147 public static File getFinalSaveDirectory(Context context, Uri sourceUri) { 148 File saveDirectory = SaveImage.getSaveDirectory(context, sourceUri); 149 if ((saveDirectory == null) || !saveDirectory.canWrite()) { 150 saveDirectory = new File(Environment.getExternalStorageDirectory(), 151 SaveImage.DEFAULT_SAVE_DIRECTORY); 152 } 153 // Create the directory if it doesn't exist 154 if (!saveDirectory.exists()) 155 saveDirectory.mkdirs(); 156 return saveDirectory; 157 } 158 159 public static File getNewFile(Context context, Uri sourceUri) { 160 File saveDirectory = getFinalSaveDirectory(context, sourceUri); 161 String filename = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date( 162 System.currentTimeMillis())); 163 if (hasPanoPrefix(context, sourceUri)) { 164 return new File(saveDirectory, PREFIX_PANO + filename + POSTFIX_JPG); 165 } 166 return new File(saveDirectory, PREFIX_IMG + filename + POSTFIX_JPG); 167 } 168 169 /** 170 * Remove the files in the auxiliary directory whose names are the same as 171 * the source image. 172 * @param contentResolver The application's contentResolver 173 * @param srcContentUri The content Uri for the source image. 174 */ 175 public static void deleteAuxFiles(ContentResolver contentResolver, 176 Uri srcContentUri) { 177 final String[] fullPath = new String[1]; 178 String[] queryProjection = new String[] { ImageColumns.DATA }; 179 querySourceFromContentResolver(contentResolver, 180 srcContentUri, queryProjection, 181 new ContentResolverQueryCallback() { 182 @Override 183 public void onCursorResult(Cursor cursor) { 184 fullPath[0] = cursor.getString(0); 185 } 186 } 187 ); 188 if (fullPath[0] != null) { 189 // Construct the auxiliary directory given the source file's path. 190 // Then select and delete all the files starting with the same name 191 // under the auxiliary directory. 192 File currentFile = new File(fullPath[0]); 193 194 String filename = currentFile.getName(); 195 int firstDotPos = filename.indexOf("."); 196 final String filenameNoExt = (firstDotPos == -1) ? filename : 197 filename.substring(0, firstDotPos); 198 File auxDir = getLocalAuxDirectory(currentFile); 199 if (auxDir.exists()) { 200 FilenameFilter filter = new FilenameFilter() { 201 @Override 202 public boolean accept(File dir, String name) { 203 if (name.startsWith(filenameNoExt + ".")) { 204 return true; 205 } else { 206 return false; 207 } 208 } 209 }; 210 211 // Delete all auxiliary files whose name is matching the 212 // current local image. 213 File[] auxFiles = auxDir.listFiles(filter); 214 for (File file : auxFiles) { 215 file.delete(); 216 } 217 } 218 } 219 } 220 221 public Object getPanoramaXMPData(Uri source, ImagePreset preset) { 222 Object xmp = null; 223 if (preset.isPanoramaSafe()) { 224 InputStream is = null; 225 try { 226 is = mContext.getContentResolver().openInputStream(source); 227 xmp = XmpUtilHelper.extractXMPMeta(is); 228 } catch (FileNotFoundException e) { 229 Log.w(LOGTAG, "Failed to get XMP data from image: ", e); 230 } finally { 231 Utils.closeSilently(is); 232 } 233 } 234 return xmp; 235 } 236 237 public boolean putPanoramaXMPData(File file, Object xmp) { 238 if (xmp != null) { 239 return XmpUtilHelper.writeXMPMeta(file.getAbsolutePath(), xmp); 240 } 241 return false; 242 } 243 244 public ExifInterface getExifData(Uri source) { 245 ExifInterface exif = new ExifInterface(); 246 String mimeType = mContext.getContentResolver().getType(mSelectedImageUri); 247 if (mimeType == null) { 248 mimeType = ImageLoader.getMimeType(mSelectedImageUri); 249 } 250 if (mimeType.equals(ImageLoader.JPEG_MIME_TYPE)) { 251 InputStream inStream = null; 252 try { 253 inStream = mContext.getContentResolver().openInputStream(source); 254 exif.readExif(inStream); 255 } catch (FileNotFoundException e) { 256 Log.w(LOGTAG, "Cannot find file: " + source, e); 257 } catch (IOException e) { 258 Log.w(LOGTAG, "Cannot read exif for: " + source, e); 259 } finally { 260 Utils.closeSilently(inStream); 261 } 262 } 263 return exif; 264 } 265 266 public boolean putExifData(File file, ExifInterface exif, Bitmap image, 267 int jpegCompressQuality) { 268 boolean ret = false; 269 OutputStream s = null; 270 try { 271 s = exif.getExifWriterStream(file.getAbsolutePath()); 272 image.compress(Bitmap.CompressFormat.JPEG, 273 (jpegCompressQuality > 0) ? jpegCompressQuality : 1, s); 274 s.flush(); 275 s.close(); 276 s = null; 277 ret = true; 278 } catch (FileNotFoundException e) { 279 Log.w(LOGTAG, "File not found: " + file.getAbsolutePath(), e); 280 } catch (IOException e) { 281 Log.w(LOGTAG, "Could not write exif: ", e); 282 } finally { 283 Utils.closeSilently(s); 284 } 285 return ret; 286 } 287 288 private Uri resetToOriginalImageIfNeeded(ImagePreset preset, boolean doAuxBackup) { 289 Uri uri = null; 290 if (!preset.hasModifications()) { 291 // This can happen only when preset has no modification but save 292 // button is enabled, it means the file is loaded with filters in 293 // the XMP, then all the filters are removed or restore to default. 294 // In this case, when mSourceUri exists, rename it to the 295 // destination file. 296 File srcFile = getLocalFileFromUri(mContext, mSourceUri); 297 // If the source is not a local file, then skip this renaming and 298 // create a local copy as usual. 299 if (srcFile != null) { 300 srcFile.renameTo(mDestinationFile); 301 uri = SaveImage.linkNewFileToUri(mContext, mSelectedImageUri, 302 mDestinationFile, System.currentTimeMillis(), doAuxBackup); 303 } 304 } 305 return uri; 306 } 307 308 private void resetProgress() { 309 mCurrentProcessingStep = 0; 310 } 311 312 private void updateProgress() { 313 if (mCallback != null) { 314 mCallback.onProgress(MAX_PROCESSING_STEPS, ++mCurrentProcessingStep); 315 } 316 } 317 318 private void updateExifData(ExifInterface exif, long time) { 319 // Set tags 320 exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, time, 321 TimeZone.getDefault()); 322 exif.setTag(exif.buildTag(ExifInterface.TAG_ORIENTATION, 323 ExifInterface.Orientation.TOP_LEFT)); 324 // Remove old thumbnail 325 exif.removeCompressedThumbnail(); 326 } 327 328 public Uri processAndSaveImage(ImagePreset preset, boolean flatten, 329 int quality, float sizeFactor, boolean exit) { 330 331 Uri uri = null; 332 if (exit) { 333 uri = resetToOriginalImageIfNeeded(preset, !flatten); 334 } 335 if (uri != null) { 336 return null; 337 } 338 339 resetProgress(); 340 341 boolean noBitmap = true; 342 int num_tries = 0; 343 int sampleSize = 1; 344 345 // If necessary, move the source file into the auxiliary directory, 346 // newSourceUri is then pointing to the new location. 347 // If no file is moved, newSourceUri will be the same as mSourceUri. 348 Uri newSourceUri = mSourceUri; 349 if (!flatten) { 350 newSourceUri = moveSrcToAuxIfNeeded(mSourceUri, mDestinationFile); 351 } 352 353 Uri savedUri = mSelectedImageUri; 354 if (mPreviewImage != null) { 355 if (flatten) { 356 Object xmp = getPanoramaXMPData(newSourceUri, preset); 357 ExifInterface exif = getExifData(newSourceUri); 358 long time = System.currentTimeMillis(); 359 updateExifData(exif, time); 360 if (putExifData(mDestinationFile, exif, mPreviewImage, quality)) { 361 putPanoramaXMPData(mDestinationFile, xmp); 362 ContentValues values = getContentValues(mContext, mSelectedImageUri, mDestinationFile, time); 363 Object result = mContext.getContentResolver().insert( 364 Images.Media.EXTERNAL_CONTENT_URI, values); 365 366 } 367 } else { 368 Object xmp = getPanoramaXMPData(newSourceUri, preset); 369 ExifInterface exif = getExifData(newSourceUri); 370 long time = System.currentTimeMillis(); 371 updateExifData(exif, time); 372 // If we succeed in writing the bitmap as a jpeg, return a uri. 373 if (putExifData(mDestinationFile, exif, mPreviewImage, quality)) { 374 putPanoramaXMPData(mDestinationFile, xmp); 375 // mDestinationFile will save the newSourceUri info in the XMP. 376 if (!flatten) { 377 XmpPresets.writeFilterXMP(mContext, newSourceUri, 378 mDestinationFile, preset); 379 } 380 // After this call, mSelectedImageUri will be actually 381 // pointing at the new file mDestinationFile. 382 savedUri = SaveImage.linkNewFileToUri(mContext, mSelectedImageUri, 383 mDestinationFile, time, !flatten); 384 } 385 } 386 if (mCallback != null) { 387 mCallback.onPreviewSaved(savedUri); 388 } 389 } 390 391 // Stopgap fix for low-memory devices. 392 while (noBitmap) { 393 try { 394 updateProgress(); 395 // Try to do bitmap operations, downsample if low-memory 396 Bitmap bitmap = ImageLoader.loadOrientedBitmapWithBackouts(mContext, newSourceUri, 397 sampleSize); 398 if (bitmap == null) { 399 return null; 400 } 401 if (sizeFactor != 1f) { 402 // if we have a valid size 403 int w = (int) (bitmap.getWidth() * sizeFactor); 404 int h = (int) (bitmap.getHeight() * sizeFactor); 405 if (w == 0 || h == 0) { 406 w = 1; 407 h = 1; 408 } 409 bitmap = Bitmap.createScaledBitmap(bitmap, w, h, true); 410 } 411 updateProgress(); 412 CachingPipeline pipeline = new CachingPipeline(FiltersManager.getManager(), 413 "Saving"); 414 415 bitmap = pipeline.renderFinalImage(bitmap, preset); 416 updateProgress(); 417 418 Object xmp = getPanoramaXMPData(newSourceUri, preset); 419 ExifInterface exif = getExifData(newSourceUri); 420 long time = System.currentTimeMillis(); 421 updateProgress(); 422 423 updateExifData(exif, time); 424 updateProgress(); 425 426 // If we succeed in writing the bitmap as a jpeg, return a uri. 427 if (putExifData(mDestinationFile, exif, bitmap, quality)) { 428 putPanoramaXMPData(mDestinationFile, xmp); 429 // mDestinationFile will save the newSourceUri info in the XMP. 430 if (!flatten) { 431 XmpPresets.writeFilterXMP(mContext, newSourceUri, 432 mDestinationFile, preset); 433 uri = updateFile(mContext, savedUri, mDestinationFile, time); 434 435 } else { 436 437 ContentValues values = getContentValues(mContext, mSelectedImageUri, mDestinationFile, time); 438 Object result = mContext.getContentResolver().insert( 439 Images.Media.EXTERNAL_CONTENT_URI, values); 440 } 441 } 442 updateProgress(); 443 444 noBitmap = false; 445 } catch (OutOfMemoryError e) { 446 // Try 5 times before failing for good. 447 if (++num_tries >= 5) { 448 throw e; 449 } 450 System.gc(); 451 sampleSize *= 2; 452 resetProgress(); 453 } 454 } 455 return uri; 456 } 457 458 /** 459 * Move the source file to auxiliary directory if needed and return the Uri 460 * pointing to this new source file. If any file error happens, then just 461 * don't move into the auxiliary directory. 462 * @param srcUri Uri to the source image. 463 * @param dstFile Providing the destination file info to help to build the 464 * auxiliary directory and new source file's name. 465 * @return the newSourceUri pointing to the new source image. 466 */ 467 private Uri moveSrcToAuxIfNeeded(Uri srcUri, File dstFile) { 468 File srcFile = getLocalFileFromUri(mContext, srcUri); 469 if (srcFile == null) { 470 Log.d(LOGTAG, "Source file is not a local file, no update."); 471 return srcUri; 472 } 473 474 // Get the destination directory and create the auxilliary directory 475 // if necessary. 476 File auxDiretory = getLocalAuxDirectory(dstFile); 477 if (!auxDiretory.exists()) { 478 boolean success = auxDiretory.mkdirs(); 479 if (!success) { 480 return srcUri; 481 } 482 } 483 484 // Make sure there is a .nomedia file in the auxiliary directory, such 485 // that MediaScanner will not report those files under this directory. 486 File noMedia = new File(auxDiretory, ".nomedia"); 487 if (!noMedia.exists()) { 488 try { 489 noMedia.createNewFile(); 490 } catch (IOException e) { 491 Log.e(LOGTAG, "Can't create the nomedia"); 492 return srcUri; 493 } 494 } 495 // We are using the destination file name such that photos sitting in 496 // the auxiliary directory are matching the parent directory. 497 File newSrcFile = new File(auxDiretory, dstFile.getName()); 498 // Maintain the suffix during move 499 String to = newSrcFile.getName(); 500 String from = srcFile.getName(); 501 to = to.substring(to.lastIndexOf(".")); 502 from = from.substring(from.lastIndexOf(".")); 503 504 if (!to.equals(from)) { 505 String name = dstFile.getName(); 506 name = name.substring(0, name.lastIndexOf(".")) + from; 507 newSrcFile = new File(auxDiretory, name); 508 } 509 510 if (!newSrcFile.exists()) { 511 boolean success = srcFile.renameTo(newSrcFile); 512 if (!success) { 513 return srcUri; 514 } 515 } 516 517 return Uri.fromFile(newSrcFile); 518 519 } 520 521 private static File getLocalAuxDirectory(File dstFile) { 522 File dstDirectory = dstFile.getParentFile(); 523 File auxDiretory = new File(dstDirectory + "/" + AUX_DIR_NAME); 524 return auxDiretory; 525 } 526 527 public static Uri makeAndInsertUri(Context context, Uri sourceUri) { 528 long time = System.currentTimeMillis(); 529 String filename = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(time)); 530 File saveDirectory = getFinalSaveDirectory(context, sourceUri); 531 File file = new File(saveDirectory, filename + ".JPG"); 532 return linkNewFileToUri(context, sourceUri, file, time, false); 533 } 534 535 public static void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity, 536 File destination) { 537 Uri selectedImageUri = filterShowActivity.getSelectedImageUri(); 538 Uri sourceImageUri = MasterImage.getImage().getUri(); 539 boolean flatten = false; 540 if (preset.contains(FilterRepresentation.TYPE_TINYPLANET)){ 541 flatten = true; 542 } 543 Intent processIntent = ProcessingService.getSaveIntent(filterShowActivity, preset, 544 destination, selectedImageUri, sourceImageUri, flatten, 90, 1f, true); 545 546 filterShowActivity.startService(processIntent); 547 548 if (!filterShowActivity.isSimpleEditAction()) { 549 String toastMessage = filterShowActivity.getResources().getString( 550 R.string.save_and_processing); 551 Toast.makeText(filterShowActivity, 552 toastMessage, 553 Toast.LENGTH_SHORT).show(); 554 } 555 } 556 557 public static void querySource(Context context, Uri sourceUri, String[] projection, 558 ContentResolverQueryCallback callback) { 559 ContentResolver contentResolver = context.getContentResolver(); 560 querySourceFromContentResolver(contentResolver, sourceUri, projection, callback); 561 } 562 563 private static void querySourceFromContentResolver( 564 ContentResolver contentResolver, Uri sourceUri, String[] projection, 565 ContentResolverQueryCallback callback) { 566 Cursor cursor = null; 567 try { 568 cursor = contentResolver.query(sourceUri, projection, null, null, 569 null); 570 if ((cursor != null) && cursor.moveToNext()) { 571 callback.onCursorResult(cursor); 572 } 573 } catch (Exception e) { 574 // Ignore error for lacking the data column from the source. 575 } finally { 576 if (cursor != null) { 577 cursor.close(); 578 } 579 } 580 } 581 582 private static File getSaveDirectory(Context context, Uri sourceUri) { 583 File file = getLocalFileFromUri(context, sourceUri); 584 if (file != null) { 585 return file.getParentFile(); 586 } else { 587 return null; 588 } 589 } 590 591 /** 592 * Construct a File object based on the srcUri. 593 * @return The file object. Return null if srcUri is invalid or not a local 594 * file. 595 */ 596 private static File getLocalFileFromUri(Context context, Uri srcUri) { 597 if (srcUri == null) { 598 Log.e(LOGTAG, "srcUri is null."); 599 return null; 600 } 601 602 String scheme = srcUri.getScheme(); 603 if (scheme == null) { 604 Log.e(LOGTAG, "scheme is null."); 605 return null; 606 } 607 608 final File[] file = new File[1]; 609 // sourceUri can be a file path or a content Uri, it need to be handled 610 // differently. 611 if (scheme.equals(ContentResolver.SCHEME_CONTENT)) { 612 if (srcUri.getAuthority().equals(MediaStore.AUTHORITY)) { 613 querySource(context, srcUri, new String[] { 614 ImageColumns.DATA 615 }, 616 new ContentResolverQueryCallback() { 617 618 @Override 619 public void onCursorResult(Cursor cursor) { 620 file[0] = new File(cursor.getString(0)); 621 } 622 }); 623 } 624 } else if (scheme.equals(ContentResolver.SCHEME_FILE)) { 625 file[0] = new File(srcUri.getPath()); 626 } 627 return file[0]; 628 } 629 630 /** 631 * Gets the actual filename for a Uri from Gallery's ContentProvider. 632 */ 633 private static String getTrueFilename(Context context, Uri src) { 634 if (context == null || src == null) { 635 return null; 636 } 637 final String[] trueName = new String[1]; 638 querySource(context, src, new String[] { 639 ImageColumns.DATA 640 }, new ContentResolverQueryCallback() { 641 @Override 642 public void onCursorResult(Cursor cursor) { 643 trueName[0] = new File(cursor.getString(0)).getName(); 644 } 645 }); 646 return trueName[0]; 647 } 648 649 /** 650 * Checks whether the true filename has the panorama image prefix. 651 */ 652 private static boolean hasPanoPrefix(Context context, Uri src) { 653 String name = getTrueFilename(context, src); 654 return name != null && name.startsWith(PREFIX_PANO); 655 } 656 657 /** 658 * If the <code>sourceUri</code> is a local content Uri, update the 659 * <code>sourceUri</code> to point to the <code>file</code>. 660 * At the same time, the old file <code>sourceUri</code> used to point to 661 * will be removed if it is local. 662 * If the <code>sourceUri</code> is not a local content Uri, then the 663 * <code>file</code> will be inserted as a new content Uri. 664 * @return the final Uri referring to the <code>file</code>. 665 */ 666 public static Uri linkNewFileToUri(Context context, Uri sourceUri, 667 File file, long time, boolean deleteOriginal) { 668 File oldSelectedFile = getLocalFileFromUri(context, sourceUri); 669 final ContentValues values = getContentValues(context, sourceUri, file, time); 670 671 Uri result = sourceUri; 672 673 // In the case of incoming Uri is just a local file Uri (like a cached 674 // file), we can't just update the Uri. We have to create a new Uri. 675 boolean fileUri = isFileUri(sourceUri); 676 677 if (fileUri || oldSelectedFile == null || !deleteOriginal) { 678 result = context.getContentResolver().insert( 679 Images.Media.EXTERNAL_CONTENT_URI, values); 680 } else { 681 context.getContentResolver().update(sourceUri, values, null, null); 682 if (oldSelectedFile.exists()) { 683 oldSelectedFile.delete(); 684 } 685 } 686 return result; 687 } 688 689 public static Uri updateFile(Context context, Uri sourceUri, File file, long time) { 690 final ContentValues values = getContentValues(context, sourceUri, file, time); 691 context.getContentResolver().update(sourceUri, values, null, null); 692 return sourceUri; 693 } 694 695 private static ContentValues getContentValues(Context context, Uri sourceUri, 696 File file, long time) { 697 final ContentValues values = new ContentValues(); 698 699 time /= 1000; 700 values.put(Images.Media.TITLE, file.getName()); 701 values.put(Images.Media.DISPLAY_NAME, file.getName()); 702 values.put(Images.Media.MIME_TYPE, "image/jpeg"); 703 values.put(Images.Media.DATE_TAKEN, time); 704 values.put(Images.Media.DATE_MODIFIED, time); 705 values.put(Images.Media.DATE_ADDED, time); 706 values.put(Images.Media.ORIENTATION, 0); 707 values.put(Images.Media.DATA, file.getAbsolutePath()); 708 values.put(Images.Media.SIZE, file.length()); 709 // This is a workaround to trigger the MediaProvider to re-generate the 710 // thumbnail. 711 values.put(Images.Media.MINI_THUMB_MAGIC, 0); 712 713 final String[] projection = new String[] { 714 ImageColumns.DATE_TAKEN, 715 ImageColumns.LATITUDE, ImageColumns.LONGITUDE, 716 }; 717 718 SaveImage.querySource(context, sourceUri, projection, 719 new ContentResolverQueryCallback() { 720 721 @Override 722 public void onCursorResult(Cursor cursor) { 723 values.put(Images.Media.DATE_TAKEN, cursor.getLong(0)); 724 725 double latitude = cursor.getDouble(1); 726 double longitude = cursor.getDouble(2); 727 // TODO: Change || to && after the default location 728 // issue is fixed. 729 if ((latitude != 0f) || (longitude != 0f)) { 730 values.put(Images.Media.LATITUDE, latitude); 731 values.put(Images.Media.LONGITUDE, longitude); 732 } 733 } 734 }); 735 return values; 736 } 737 738 /** 739 * @param sourceUri 740 * @return true if the sourceUri is a local file Uri. 741 */ 742 private static boolean isFileUri(Uri sourceUri) { 743 String scheme = sourceUri.getScheme(); 744 if (scheme != null && scheme.equals(ContentResolver.SCHEME_FILE)) { 745 return true; 746 } 747 return false; 748 } 749 750 } 751