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 {
onPreviewSaved(Uri uri)66         void onPreviewSaved(Uri uri);
onProgress(int max, int current)67         void onProgress(int max, int current);
68     }
69 
70     public interface ContentResolverQueryCallback {
onCursorResult(Cursor cursor)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      */
SaveImage(Context context, Uri sourceUri, Uri selectedImageUri, File destination, Bitmap previewImage, Callback callback)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 
getFinalSaveDirectory(Context context, Uri sourceUri)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 
getNewFile(Context context, Uri sourceUri)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      */
deleteAuxFiles(ContentResolver contentResolver, Uri srcContentUri)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 
getPanoramaXMPData(Uri source, ImagePreset preset)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 
putPanoramaXMPData(File file, Object xmp)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 
getExifData(Uri source)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 
putExifData(File file, ExifInterface exif, Bitmap image, int jpegCompressQuality)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 
resetToOriginalImageIfNeeded(ImagePreset preset, boolean doAuxBackup)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 
resetProgress()308     private void resetProgress() {
309         mCurrentProcessingStep = 0;
310     }
311 
updateProgress()312     private void updateProgress() {
313         if (mCallback != null) {
314             mCallback.onProgress(MAX_PROCESSING_STEPS, ++mCurrentProcessingStep);
315         }
316     }
317 
updateExifData(ExifInterface exif, long time)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 
processAndSaveImage(ImagePreset preset, boolean flatten, int quality, float sizeFactor, boolean exit)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      */
moveSrcToAuxIfNeeded(Uri srcUri, File dstFile)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 
getLocalAuxDirectory(File dstFile)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 
makeAndInsertUri(Context context, Uri sourceUri)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 
saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity, File destination)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 
querySource(Context context, Uri sourceUri, String[] projection, ContentResolverQueryCallback callback)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 
querySourceFromContentResolver( ContentResolver contentResolver, Uri sourceUri, String[] projection, ContentResolverQueryCallback callback)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 
getSaveDirectory(Context context, Uri sourceUri)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      */
getLocalFileFromUri(Context context, Uri srcUri)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      */
getTrueFilename(Context context, Uri src)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      */
hasPanoPrefix(Context context, Uri src)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      */
linkNewFileToUri(Context context, Uri sourceUri, File file, long time, boolean deleteOriginal)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 
updateFile(Context context, Uri sourceUri, File file, long time)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 
getContentValues(Context context, Uri sourceUri, File file, long time)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      */
isFileUri(Uri sourceUri)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