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                     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