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 bitmap = Bitmap.createScaledBitmap(bitmap, w, h, true); 406 } 407 updateProgress(); 408 CachingPipeline pipeline = new CachingPipeline(FiltersManager.getManager(), 409 "Saving"); 410 411 bitmap = pipeline.renderFinalImage(bitmap, preset); 412 updateProgress(); 413 414 Object xmp = getPanoramaXMPData(newSourceUri, preset); 415 ExifInterface exif = getExifData(newSourceUri); 416 long time = System.currentTimeMillis(); 417 updateProgress(); 418 419 updateExifData(exif, time); 420 updateProgress(); 421 422 // If we succeed in writing the bitmap as a jpeg, return a uri. 423 if (putExifData(mDestinationFile, exif, bitmap, quality)) { 424 putPanoramaXMPData(mDestinationFile, xmp); 425 // mDestinationFile will save the newSourceUri info in the XMP. 426 if (!flatten) { 427 XmpPresets.writeFilterXMP(mContext, newSourceUri, 428 mDestinationFile, preset); 429 uri = updateFile(mContext, savedUri, mDestinationFile, time); 430 431 } else { 432 433 ContentValues values = getContentValues(mContext, mSelectedImageUri, mDestinationFile, time); 434 Object result = mContext.getContentResolver().insert( 435 Images.Media.EXTERNAL_CONTENT_URI, values); 436 } 437 } 438 updateProgress(); 439 440 noBitmap = false; 441 } catch (OutOfMemoryError e) { 442 // Try 5 times before failing for good. 443 if (++num_tries >= 5) { 444 throw e; 445 } 446 System.gc(); 447 sampleSize *= 2; 448 resetProgress(); 449 } 450 } 451 return uri; 452 } 453 454 /** 455 * Move the source file to auxiliary directory if needed and return the Uri 456 * pointing to this new source file. If any file error happens, then just 457 * don't move into the auxiliary directory. 458 * @param srcUri Uri to the source image. 459 * @param dstFile Providing the destination file info to help to build the 460 * auxiliary directory and new source file's name. 461 * @return the newSourceUri pointing to the new source image. 462 */ 463 private Uri moveSrcToAuxIfNeeded(Uri srcUri, File dstFile) { 464 File srcFile = getLocalFileFromUri(mContext, srcUri); 465 if (srcFile == null) { 466 Log.d(LOGTAG, "Source file is not a local file, no update."); 467 return srcUri; 468 } 469 470 // Get the destination directory and create the auxilliary directory 471 // if necessary. 472 File auxDiretory = getLocalAuxDirectory(dstFile); 473 if (!auxDiretory.exists()) { 474 boolean success = auxDiretory.mkdirs(); 475 if (!success) { 476 return srcUri; 477 } 478 } 479 480 // Make sure there is a .nomedia file in the auxiliary directory, such 481 // that MediaScanner will not report those files under this directory. 482 File noMedia = new File(auxDiretory, ".nomedia"); 483 if (!noMedia.exists()) { 484 try { 485 noMedia.createNewFile(); 486 } catch (IOException e) { 487 Log.e(LOGTAG, "Can't create the nomedia"); 488 return srcUri; 489 } 490 } 491 // We are using the destination file name such that photos sitting in 492 // the auxiliary directory are matching the parent directory. 493 File newSrcFile = new File(auxDiretory, dstFile.getName()); 494 // Maintain the suffix during move 495 String to = newSrcFile.getName(); 496 String from = srcFile.getName(); 497 to = to.substring(to.lastIndexOf(".")); 498 from = from.substring(from.lastIndexOf(".")); 499 500 if (!to.equals(from)) { 501 String name = dstFile.getName(); 502 name = name.substring(0, name.lastIndexOf(".")) + from; 503 newSrcFile = new File(auxDiretory, name); 504 } 505 506 if (!newSrcFile.exists()) { 507 boolean success = srcFile.renameTo(newSrcFile); 508 if (!success) { 509 return srcUri; 510 } 511 } 512 513 return Uri.fromFile(newSrcFile); 514 515 } 516 517 private static File getLocalAuxDirectory(File dstFile) { 518 File dstDirectory = dstFile.getParentFile(); 519 File auxDiretory = new File(dstDirectory + "/" + AUX_DIR_NAME); 520 return auxDiretory; 521 } 522 523 public static Uri makeAndInsertUri(Context context, Uri sourceUri) { 524 long time = System.currentTimeMillis(); 525 String filename = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(time)); 526 File saveDirectory = getFinalSaveDirectory(context, sourceUri); 527 File file = new File(saveDirectory, filename + ".JPG"); 528 return linkNewFileToUri(context, sourceUri, file, time, false); 529 } 530 531 public static void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity, 532 File destination) { 533 Uri selectedImageUri = filterShowActivity.getSelectedImageUri(); 534 Uri sourceImageUri = MasterImage.getImage().getUri(); 535 boolean flatten = false; 536 if (preset.contains(FilterRepresentation.TYPE_TINYPLANET)){ 537 flatten = true; 538 } 539 Intent processIntent = ProcessingService.getSaveIntent(filterShowActivity, preset, 540 destination, selectedImageUri, sourceImageUri, flatten, 90, 1f, true); 541 542 filterShowActivity.startService(processIntent); 543 544 if (!filterShowActivity.isSimpleEditAction()) { 545 String toastMessage = filterShowActivity.getResources().getString( 546 R.string.save_and_processing); 547 Toast.makeText(filterShowActivity, 548 toastMessage, 549 Toast.LENGTH_SHORT).show(); 550 } 551 } 552 553 public static void querySource(Context context, Uri sourceUri, String[] projection, 554 ContentResolverQueryCallback callback) { 555 ContentResolver contentResolver = context.getContentResolver(); 556 querySourceFromContentResolver(contentResolver, sourceUri, projection, callback); 557 } 558 559 private static void querySourceFromContentResolver( 560 ContentResolver contentResolver, Uri sourceUri, String[] projection, 561 ContentResolverQueryCallback callback) { 562 Cursor cursor = null; 563 try { 564 cursor = contentResolver.query(sourceUri, projection, null, null, 565 null); 566 if ((cursor != null) && cursor.moveToNext()) { 567 callback.onCursorResult(cursor); 568 } 569 } catch (Exception e) { 570 // Ignore error for lacking the data column from the source. 571 } finally { 572 if (cursor != null) { 573 cursor.close(); 574 } 575 } 576 } 577 578 private static File getSaveDirectory(Context context, Uri sourceUri) { 579 File file = getLocalFileFromUri(context, sourceUri); 580 if (file != null) { 581 return file.getParentFile(); 582 } else { 583 return null; 584 } 585 } 586 587 /** 588 * Construct a File object based on the srcUri. 589 * @return The file object. Return null if srcUri is invalid or not a local 590 * file. 591 */ 592 private static File getLocalFileFromUri(Context context, Uri srcUri) { 593 if (srcUri == null) { 594 Log.e(LOGTAG, "srcUri is null."); 595 return null; 596 } 597 598 String scheme = srcUri.getScheme(); 599 if (scheme == null) { 600 Log.e(LOGTAG, "scheme is null."); 601 return null; 602 } 603 604 final File[] file = new File[1]; 605 // sourceUri can be a file path or a content Uri, it need to be handled 606 // differently. 607 if (scheme.equals(ContentResolver.SCHEME_CONTENT)) { 608 if (srcUri.getAuthority().equals(MediaStore.AUTHORITY)) { 609 querySource(context, srcUri, new String[] { 610 ImageColumns.DATA 611 }, 612 new ContentResolverQueryCallback() { 613 614 @Override 615 public void onCursorResult(Cursor cursor) { 616 file[0] = new File(cursor.getString(0)); 617 } 618 }); 619 } 620 } else if (scheme.equals(ContentResolver.SCHEME_FILE)) { 621 file[0] = new File(srcUri.getPath()); 622 } 623 return file[0]; 624 } 625 626 /** 627 * Gets the actual filename for a Uri from Gallery's ContentProvider. 628 */ 629 private static String getTrueFilename(Context context, Uri src) { 630 if (context == null || src == null) { 631 return null; 632 } 633 final String[] trueName = new String[1]; 634 querySource(context, src, new String[] { 635 ImageColumns.DATA 636 }, new ContentResolverQueryCallback() { 637 @Override 638 public void onCursorResult(Cursor cursor) { 639 trueName[0] = new File(cursor.getString(0)).getName(); 640 } 641 }); 642 return trueName[0]; 643 } 644 645 /** 646 * Checks whether the true filename has the panorama image prefix. 647 */ 648 private static boolean hasPanoPrefix(Context context, Uri src) { 649 String name = getTrueFilename(context, src); 650 return name != null && name.startsWith(PREFIX_PANO); 651 } 652 653 /** 654 * If the <code>sourceUri</code> is a local content Uri, update the 655 * <code>sourceUri</code> to point to the <code>file</code>. 656 * At the same time, the old file <code>sourceUri</code> used to point to 657 * will be removed if it is local. 658 * If the <code>sourceUri</code> is not a local content Uri, then the 659 * <code>file</code> will be inserted as a new content Uri. 660 * @return the final Uri referring to the <code>file</code>. 661 */ 662 public static Uri linkNewFileToUri(Context context, Uri sourceUri, 663 File file, long time, boolean deleteOriginal) { 664 File oldSelectedFile = getLocalFileFromUri(context, sourceUri); 665 final ContentValues values = getContentValues(context, sourceUri, file, time); 666 667 Uri result = sourceUri; 668 669 // In the case of incoming Uri is just a local file Uri (like a cached 670 // file), we can't just update the Uri. We have to create a new Uri. 671 boolean fileUri = isFileUri(sourceUri); 672 673 if (fileUri || oldSelectedFile == null || !deleteOriginal) { 674 result = context.getContentResolver().insert( 675 Images.Media.EXTERNAL_CONTENT_URI, values); 676 } else { 677 context.getContentResolver().update(sourceUri, values, null, null); 678 if (oldSelectedFile.exists()) { 679 oldSelectedFile.delete(); 680 } 681 } 682 return result; 683 } 684 685 public static Uri updateFile(Context context, Uri sourceUri, File file, long time) { 686 final ContentValues values = getContentValues(context, sourceUri, file, time); 687 context.getContentResolver().update(sourceUri, values, null, null); 688 return sourceUri; 689 } 690 691 private static ContentValues getContentValues(Context context, Uri sourceUri, 692 File file, long time) { 693 final ContentValues values = new ContentValues(); 694 695 time /= 1000; 696 values.put(Images.Media.TITLE, file.getName()); 697 values.put(Images.Media.DISPLAY_NAME, file.getName()); 698 values.put(Images.Media.MIME_TYPE, "image/jpeg"); 699 values.put(Images.Media.DATE_TAKEN, time); 700 values.put(Images.Media.DATE_MODIFIED, time); 701 values.put(Images.Media.DATE_ADDED, time); 702 values.put(Images.Media.ORIENTATION, 0); 703 values.put(Images.Media.DATA, file.getAbsolutePath()); 704 values.put(Images.Media.SIZE, file.length()); 705 706 final String[] projection = new String[] { 707 ImageColumns.DATE_TAKEN, 708 ImageColumns.LATITUDE, ImageColumns.LONGITUDE, 709 }; 710 711 SaveImage.querySource(context, sourceUri, projection, 712 new ContentResolverQueryCallback() { 713 714 @Override 715 public void onCursorResult(Cursor cursor) { 716 values.put(Images.Media.DATE_TAKEN, cursor.getLong(0)); 717 718 double latitude = cursor.getDouble(1); 719 double longitude = cursor.getDouble(2); 720 // TODO: Change || to && after the default location 721 // issue is fixed. 722 if ((latitude != 0f) || (longitude != 0f)) { 723 values.put(Images.Media.LATITUDE, latitude); 724 values.put(Images.Media.LONGITUDE, longitude); 725 } 726 } 727 }); 728 return values; 729 } 730 731 /** 732 * @param sourceUri 733 * @return true if the sourceUri is a local file Uri. 734 */ 735 private static boolean isFileUri(Uri sourceUri) { 736 String scheme = sourceUri.getScheme(); 737 if (scheme != null && scheme.equals(ContentResolver.SCHEME_FILE)) { 738 return true; 739 } 740 return false; 741 } 742 743 } 744