Home | History | Annotate | Download | only in tools
      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