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
17package com.android.gallery3d.filtershow.tools;
18
19import android.content.ContentResolver;
20import android.content.ContentValues;
21import android.content.Context;
22import android.content.Intent;
23import android.database.Cursor;
24import android.graphics.Bitmap;
25import android.net.Uri;
26import android.os.Environment;
27import android.provider.MediaStore;
28import android.provider.MediaStore.Images;
29import android.provider.MediaStore.Images.ImageColumns;
30import android.util.Log;
31import android.widget.Toast;
32
33import com.android.gallery3d.R;
34import com.android.gallery3d.common.Utils;
35import com.android.gallery3d.exif.ExifInterface;
36import com.android.gallery3d.filtershow.FilterShowActivity;
37import com.android.gallery3d.filtershow.cache.ImageLoader;
38import com.android.gallery3d.filtershow.filters.FilterRepresentation;
39import com.android.gallery3d.filtershow.filters.FiltersManager;
40import com.android.gallery3d.filtershow.imageshow.MasterImage;
41import com.android.gallery3d.filtershow.pipeline.CachingPipeline;
42import com.android.gallery3d.filtershow.pipeline.ImagePreset;
43import com.android.gallery3d.filtershow.pipeline.ProcessingService;
44import com.android.gallery3d.util.XmpUtilHelper;
45
46import java.io.File;
47import java.io.FileNotFoundException;
48import java.io.FilenameFilter;
49import java.io.IOException;
50import java.io.InputStream;
51import java.io.OutputStream;
52import java.text.SimpleDateFormat;
53import java.util.Date;
54import java.util.TimeZone;
55
56/**
57 * Handles saving edited photo
58 */
59public 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