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.database.Cursor;
     23 import android.graphics.Bitmap;
     24 import android.graphics.BitmapFactory;
     25 import android.graphics.Bitmap.CompressFormat;
     26 import android.media.ExifInterface;
     27 import android.net.Uri;
     28 import android.os.AsyncTask;
     29 import android.os.Environment;
     30 import android.provider.MediaStore.Images;
     31 import android.provider.MediaStore.Images.ImageColumns;
     32 import android.util.Log;
     33 
     34 import com.android.gallery3d.filtershow.cache.ImageLoader;
     35 import com.android.gallery3d.filtershow.presets.ImagePreset;
     36 import com.android.gallery3d.util.XmpUtilHelper;
     37 
     38 import java.io.Closeable;
     39 import java.io.File;
     40 import java.io.FileNotFoundException;
     41 import java.io.FileOutputStream;
     42 import java.io.IOException;
     43 import java.io.InputStream;
     44 import java.io.OutputStream;
     45 import java.sql.Date;
     46 import java.text.SimpleDateFormat;
     47 
     48 /**
     49  * Asynchronous task for saving edited photo as a new copy.
     50  */
     51 public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
     52 
     53 
     54     private static final String LOGTAG = "SaveCopyTask";
     55     private static final int DEFAULT_COMPRESS_QUALITY = 95;
     56     private static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos";
     57 
     58     /**
     59      * Saves the bitmap in the final destination
     60      */
     61     public static void saveBitmap(Bitmap bitmap, File destination, Object xmp) {
     62         OutputStream os = null;
     63         try {
     64             os = new FileOutputStream(destination);
     65             bitmap.compress(CompressFormat.JPEG, DEFAULT_COMPRESS_QUALITY, os);
     66         } catch (FileNotFoundException e) {
     67             Log.v(LOGTAG,"Error in writing "+destination.getAbsolutePath());
     68         } finally {
     69             closeStream(os);
     70         }
     71         if (xmp != null) {
     72             XmpUtilHelper.writeXMPMeta(destination.getAbsolutePath(), xmp);
     73         }
     74     }
     75 
     76     private static void closeStream(Closeable stream) {
     77         if (stream != null) {
     78             try {
     79                 stream.close();
     80             } catch (IOException e) {
     81                 e.printStackTrace();
     82             }
     83         }
     84     }
     85 
     86     /**
     87      * Callback for the completed asynchronous task.
     88      */
     89     public interface Callback {
     90 
     91         void onComplete(Uri result);
     92     }
     93 
     94     private interface ContentResolverQueryCallback {
     95 
     96         void onCursorResult(Cursor cursor);
     97     }
     98 
     99     private static final String TIME_STAMP_NAME = "'IMG'_yyyyMMdd_HHmmss";
    100 
    101     private final Context context;
    102     private final Uri sourceUri;
    103     private final Callback callback;
    104     private final String saveFileName;
    105     private final File destinationFile;
    106 
    107     public SaveCopyTask(Context context, Uri sourceUri, File destination, Callback callback) {
    108         this.context = context;
    109         this.sourceUri = sourceUri;
    110         this.callback = callback;
    111 
    112         if (destination == null) {
    113             this.destinationFile = getNewFile(context, sourceUri);
    114         } else {
    115             this.destinationFile = destination;
    116         }
    117 
    118         saveFileName = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(
    119                 System.currentTimeMillis()));
    120     }
    121 
    122     public static File getFinalSaveDirectory(Context context, Uri sourceUri) {
    123         File saveDirectory = getSaveDirectory(context, sourceUri);
    124         if ((saveDirectory == null) || !saveDirectory.canWrite()) {
    125             saveDirectory = new File(Environment.getExternalStorageDirectory(),
    126                     DEFAULT_SAVE_DIRECTORY);
    127         }
    128         // Create the directory if it doesn't exist
    129         if (!saveDirectory.exists()) saveDirectory.mkdirs();
    130         return saveDirectory;
    131     }
    132 
    133     public static File getNewFile(Context context, Uri sourceUri) {
    134         File saveDirectory = getFinalSaveDirectory(context, sourceUri);
    135         String filename = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(
    136                 System.currentTimeMillis()));
    137         return new File(saveDirectory, filename + ".JPG");
    138     }
    139 
    140     private Bitmap loadMutableBitmap() throws FileNotFoundException {
    141         BitmapFactory.Options options = new BitmapFactory.Options();
    142         // TODO: on <3.x we need a copy of the bitmap (inMutable doesn't
    143         // exist)
    144         options.inMutable = true;
    145 
    146         InputStream is = context.getContentResolver().openInputStream(sourceUri);
    147         Bitmap bitmap = BitmapFactory.decodeStream(is, null, options);
    148         int orientation = ImageLoader.getOrientation(context, sourceUri);
    149         bitmap = ImageLoader.rotateToPortrait(bitmap, orientation);
    150         return bitmap;
    151     }
    152 
    153     private static final String[] COPY_EXIF_ATTRIBUTES = new String[] {
    154         ExifInterface.TAG_APERTURE,
    155         ExifInterface.TAG_DATETIME,
    156         ExifInterface.TAG_EXPOSURE_TIME,
    157         ExifInterface.TAG_FLASH,
    158         ExifInterface.TAG_FOCAL_LENGTH,
    159         ExifInterface.TAG_GPS_ALTITUDE,
    160         ExifInterface.TAG_GPS_ALTITUDE_REF,
    161         ExifInterface.TAG_GPS_DATESTAMP,
    162         ExifInterface.TAG_GPS_LATITUDE,
    163         ExifInterface.TAG_GPS_LATITUDE_REF,
    164         ExifInterface.TAG_GPS_LONGITUDE,
    165         ExifInterface.TAG_GPS_LONGITUDE_REF,
    166         ExifInterface.TAG_GPS_PROCESSING_METHOD,
    167         ExifInterface.TAG_GPS_DATESTAMP,
    168         ExifInterface.TAG_ISO,
    169         ExifInterface.TAG_MAKE,
    170         ExifInterface.TAG_MODEL,
    171         ExifInterface.TAG_WHITE_BALANCE,
    172     };
    173 
    174     private static void copyExif(String sourcePath, String destPath) {
    175         try {
    176             ExifInterface source = new ExifInterface(sourcePath);
    177             ExifInterface dest = new ExifInterface(destPath);
    178             boolean needsSave = false;
    179             for (String tag : COPY_EXIF_ATTRIBUTES) {
    180                 String value = source.getAttribute(tag);
    181                 if (value != null) {
    182                     needsSave = true;
    183                     dest.setAttribute(tag, value);
    184                 }
    185             }
    186             if (needsSave) {
    187                 dest.saveAttributes();
    188             }
    189         } catch (IOException ex) {
    190             Log.w(LOGTAG, "Failed to copy exif metadata", ex);
    191         }
    192     }
    193 
    194     private void copyExif(Uri sourceUri, String destPath) {
    195         if (ContentResolver.SCHEME_FILE.equals(sourceUri.getScheme())) {
    196             copyExif(sourceUri.getPath(), destPath);
    197             return;
    198         }
    199 
    200         final String[] PROJECTION = new String[] {
    201                 ImageColumns.DATA
    202         };
    203         try {
    204             Cursor c = context.getContentResolver().query(sourceUri, PROJECTION,
    205                     null, null, null);
    206             if (c.moveToFirst()) {
    207                 String path = c.getString(0);
    208                 if (new File(path).exists()) {
    209                     copyExif(path, destPath);
    210                 }
    211             }
    212             c.close();
    213         } catch (Exception e) {
    214             Log.w(LOGTAG, "Failed to copy exif", e);
    215         }
    216     }
    217 
    218     /**
    219      * The task should be executed with one given bitmap to be saved.
    220      */
    221     @Override
    222     protected Uri doInBackground(ImagePreset... params) {
    223         // TODO: Support larger dimensions for photo saving.
    224         if (params[0] == null) {
    225             return null;
    226         }
    227 
    228         ImagePreset preset = params[0];
    229 
    230         try {
    231             Bitmap bitmap = preset.apply(loadMutableBitmap());
    232 
    233             Object xmp = null;
    234             InputStream is = null;
    235             if (preset.isPanoramaSafe()) {
    236                 is = context.getContentResolver().openInputStream(sourceUri);
    237                 xmp =  XmpUtilHelper.extractXMPMeta(is);
    238             }
    239             saveBitmap(bitmap, this.destinationFile, xmp);
    240             copyExif(sourceUri, destinationFile.getAbsolutePath());
    241 
    242             Uri uri = insertContent(context, sourceUri, this.destinationFile, saveFileName);
    243             bitmap.recycle();
    244             return uri;
    245 
    246         } catch (FileNotFoundException ex) {
    247             Log.w(LOGTAG, "Failed to save image!", ex);
    248             return null;
    249         }
    250     }
    251 
    252     @Override
    253     protected void onPostExecute(Uri result) {
    254         if (callback != null) {
    255             callback.onComplete(result);
    256         }
    257     }
    258 
    259     private static void querySource(Context context, Uri sourceUri, String[] projection,
    260             ContentResolverQueryCallback callback) {
    261         ContentResolver contentResolver = context.getContentResolver();
    262         Cursor cursor = null;
    263         try {
    264             cursor = contentResolver.query(sourceUri, projection, null, null,
    265                     null);
    266             if ((cursor != null) && cursor.moveToNext()) {
    267                 callback.onCursorResult(cursor);
    268             }
    269         } catch (Exception e) {
    270             // Ignore error for lacking the data column from the source.
    271         } finally {
    272             if (cursor != null) {
    273                 cursor.close();
    274             }
    275         }
    276     }
    277 
    278     private static File getSaveDirectory(Context context, Uri sourceUri) {
    279         final File[] dir = new File[1];
    280         querySource(context, sourceUri, new String[] {
    281                 ImageColumns.DATA
    282         },
    283                 new ContentResolverQueryCallback() {
    284 
    285                     @Override
    286                     public void onCursorResult(Cursor cursor) {
    287                         dir[0] = new File(cursor.getString(0)).getParentFile();
    288                     }
    289                 });
    290         return dir[0];
    291     }
    292 
    293     /**
    294      * Insert the content (saved file) with proper source photo properties.
    295      */
    296     public static Uri insertContent(Context context, Uri sourceUri, File file, String saveFileName) {
    297         long now = System.currentTimeMillis() / 1000;
    298 
    299         final ContentValues values = new ContentValues();
    300         values.put(Images.Media.TITLE, saveFileName);
    301         values.put(Images.Media.DISPLAY_NAME, file.getName());
    302         values.put(Images.Media.MIME_TYPE, "image/jpeg");
    303         values.put(Images.Media.DATE_TAKEN, now);
    304         values.put(Images.Media.DATE_MODIFIED, now);
    305         values.put(Images.Media.DATE_ADDED, now);
    306         values.put(Images.Media.ORIENTATION, 0);
    307         values.put(Images.Media.DATA, file.getAbsolutePath());
    308         values.put(Images.Media.SIZE, file.length());
    309 
    310         final String[] projection = new String[] {
    311                 ImageColumns.DATE_TAKEN,
    312                 ImageColumns.LATITUDE, ImageColumns.LONGITUDE,
    313         };
    314         querySource(context, sourceUri, projection,
    315                 new ContentResolverQueryCallback() {
    316 
    317             @Override
    318             public void onCursorResult(Cursor cursor) {
    319                 values.put(Images.Media.DATE_TAKEN, cursor.getLong(0));
    320 
    321                 double latitude = cursor.getDouble(1);
    322                 double longitude = cursor.getDouble(2);
    323                 // TODO: Change || to && after the default location issue is
    324                 // fixed.
    325                 if ((latitude != 0f) || (longitude != 0f)) {
    326                     values.put(Images.Media.LATITUDE, latitude);
    327                     values.put(Images.Media.LONGITUDE, longitude);
    328                 }
    329             }
    330         });
    331 
    332         return context.getContentResolver().insert(
    333                 Images.Media.EXTERNAL_CONTENT_URI, values);
    334     }
    335 
    336 }
    337