Home | History | Annotate | Download | only in crop
      1 /*
      2  * Copyright (C) 2013 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.crop;
     18 
     19 import android.content.ContentResolver;
     20 import android.content.ContentValues;
     21 import android.content.Context;
     22 import android.database.Cursor;
     23 import android.database.sqlite.SQLiteException;
     24 import android.graphics.Bitmap;
     25 import android.graphics.BitmapFactory;
     26 import android.graphics.Rect;
     27 import android.net.Uri;
     28 import android.os.Environment;
     29 import android.provider.MediaStore;
     30 import android.provider.MediaStore.Images;
     31 import android.provider.MediaStore.Images.ImageColumns;
     32 import android.util.Log;
     33 
     34 import com.android.gallery3d.common.Utils;
     35 import com.android.gallery3d.exif.ExifInterface;
     36 
     37 import java.io.File;
     38 import java.io.FileNotFoundException;
     39 import java.io.IOException;
     40 import java.io.InputStream;
     41 import java.sql.Date;
     42 import java.text.SimpleDateFormat;
     43 
     44 /**
     45  * This class contains static methods for loading a bitmap and
     46  * maintains no instance state.
     47  */
     48 public abstract class CropLoader {
     49     public static final String LOGTAG = "CropLoader";
     50     public static final String JPEG_MIME_TYPE = "image/jpeg";
     51 
     52     private static final String TIME_STAMP_NAME = "'IMG'_yyyyMMdd_HHmmss";
     53     public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos";
     54 
     55     /**
     56      * Returns the orientation of image at the given URI as one of 0, 90, 180,
     57      * 270.
     58      *
     59      * @param uri URI of image to open.
     60      * @param context context whose ContentResolver to use.
     61      * @return the orientation of the image. Defaults to 0.
     62      */
     63     public static int getMetadataRotation(Uri uri, Context context) {
     64         if (uri == null || context == null) {
     65             throw new IllegalArgumentException("bad argument to getScaledBitmap");
     66         }
     67         if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
     68             String mimeType = context.getContentResolver().getType(uri);
     69             if (mimeType != JPEG_MIME_TYPE) {
     70                 return 0;
     71             }
     72             String path = uri.getPath();
     73             int orientation = 0;
     74             ExifInterface exif = new ExifInterface();
     75             try {
     76                 exif.readExif(path);
     77                 orientation = ExifInterface.getRotationForOrientationValue(
     78                         exif.getTagIntValue(ExifInterface.TAG_ORIENTATION).shortValue());
     79             } catch (IOException e) {
     80                 Log.w(LOGTAG, "Failed to read EXIF orientation", e);
     81             }
     82             return orientation;
     83         }
     84         Cursor cursor = null;
     85         try {
     86             cursor = context.getContentResolver().query(uri,
     87                     new String[] { MediaStore.Images.ImageColumns.ORIENTATION },
     88                     null, null, null);
     89             if (cursor.moveToNext()) {
     90                 int ori = cursor.getInt(0);
     91                 return (ori < 0) ? 0 : ori;
     92             }
     93         } catch (SQLiteException e) {
     94             return 0;
     95         } catch (IllegalArgumentException e) {
     96             return 0;
     97         } finally {
     98             Utils.closeSilently(cursor);
     99         }
    100         return 0;
    101     }
    102 
    103     /**
    104      * Gets a bitmap at a given URI that is downsampled so that both sides are
    105      * smaller than maxSideLength. The Bitmap's original dimensions are stored
    106      * in the rect originalBounds.
    107      *
    108      * @param uri URI of image to open.
    109      * @param context context whose ContentResolver to use.
    110      * @param maxSideLength max side length of returned bitmap.
    111      * @param originalBounds set to the actual bounds of the stored bitmap.
    112      * @return downsampled bitmap or null if this operation failed.
    113      */
    114     public static Bitmap getConstrainedBitmap(Uri uri, Context context, int maxSideLength,
    115             Rect originalBounds) {
    116         if (maxSideLength <= 0 || originalBounds == null || uri == null || context == null) {
    117             throw new IllegalArgumentException("bad argument to getScaledBitmap");
    118         }
    119         InputStream is = null;
    120         try {
    121             // Get width and height of stored bitmap
    122             is = context.getContentResolver().openInputStream(uri);
    123             BitmapFactory.Options options = new BitmapFactory.Options();
    124             options.inJustDecodeBounds = true;
    125             BitmapFactory.decodeStream(is, null, options);
    126             int w = options.outWidth;
    127             int h = options.outHeight;
    128             originalBounds.set(0, 0, w, h);
    129 
    130             // If bitmap cannot be decoded, return null
    131             if (w <= 0 || h <= 0) {
    132                 return null;
    133             }
    134 
    135             options = new BitmapFactory.Options();
    136 
    137             // Find best downsampling size
    138             int imageSide = Math.max(w, h);
    139             options.inSampleSize = 1;
    140             if (imageSide > maxSideLength) {
    141                 int shifts = 1 + Integer.numberOfLeadingZeros(maxSideLength)
    142                         - Integer.numberOfLeadingZeros(imageSide);
    143                 options.inSampleSize <<= shifts;
    144             }
    145 
    146             // Make sure sample size is reasonable
    147             if (options.inSampleSize <= 0 ||
    148                     0 >= (int) (Math.min(w, h) / options.inSampleSize)) {
    149                 return null;
    150             }
    151 
    152             // Decode actual bitmap.
    153             options.inMutable = true;
    154             is.close();
    155             is = context.getContentResolver().openInputStream(uri);
    156             return BitmapFactory.decodeStream(is, null, options);
    157         } catch (FileNotFoundException e) {
    158             Log.e(LOGTAG, "FileNotFoundException: " + uri, e);
    159         } catch (IOException e) {
    160             Log.e(LOGTAG, "IOException: " + uri, e);
    161         } finally {
    162             Utils.closeSilently(is);
    163         }
    164         return null;
    165     }
    166 
    167     /**
    168      * Gets a bitmap that has been downsampled using sampleSize.
    169      *
    170      * @param uri URI of image to open.
    171      * @param context context whose ContentResolver to use.
    172      * @param sampleSize downsampling amount.
    173      * @return downsampled bitmap.
    174      */
    175     public static Bitmap getBitmap(Uri uri, Context context, int sampleSize) {
    176         if (uri == null || context == null) {
    177             throw new IllegalArgumentException("bad argument to getScaledBitmap");
    178         }
    179         InputStream is = null;
    180         try {
    181             is = context.getContentResolver().openInputStream(uri);
    182             BitmapFactory.Options options = new BitmapFactory.Options();
    183             options.inMutable = true;
    184             options.inSampleSize = sampleSize;
    185             return BitmapFactory.decodeStream(is, null, options);
    186         } catch (FileNotFoundException e) {
    187             Log.e(LOGTAG, "FileNotFoundException: " + uri, e);
    188         } finally {
    189             Utils.closeSilently(is);
    190         }
    191         return null;
    192     }
    193 
    194     // TODO: Super gnarly (copied from SaveCopyTask.java), do cleanup.
    195 
    196     public static File getFinalSaveDirectory(Context context, Uri sourceUri) {
    197         File saveDirectory = getSaveDirectory(context, sourceUri);
    198         if ((saveDirectory == null) || !saveDirectory.canWrite()) {
    199             saveDirectory = new File(Environment.getExternalStorageDirectory(),
    200                     DEFAULT_SAVE_DIRECTORY);
    201         }
    202         // Create the directory if it doesn't exist
    203         if (!saveDirectory.exists())
    204             saveDirectory.mkdirs();
    205         return saveDirectory;
    206     }
    207 
    208 
    209 
    210     public static String getNewFileName(long time) {
    211         return new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(time));
    212     }
    213 
    214     public static File getNewFile(Context context, Uri sourceUri, String filename) {
    215         File saveDirectory = getFinalSaveDirectory(context, sourceUri);
    216         return new File(saveDirectory, filename  + ".JPG");
    217     }
    218 
    219     private interface ContentResolverQueryCallback {
    220 
    221         void onCursorResult(Cursor cursor);
    222     }
    223 
    224     private static void querySource(Context context, Uri sourceUri, String[] projection,
    225             ContentResolverQueryCallback callback) {
    226         ContentResolver contentResolver = context.getContentResolver();
    227         Cursor cursor = null;
    228         try {
    229             cursor = contentResolver.query(sourceUri, projection, null, null,
    230                     null);
    231             if ((cursor != null) && cursor.moveToNext()) {
    232                 callback.onCursorResult(cursor);
    233             }
    234         } catch (Exception e) {
    235             // Ignore error for lacking the data column from the source.
    236         } finally {
    237             if (cursor != null) {
    238                 cursor.close();
    239             }
    240         }
    241     }
    242 
    243     private static File getSaveDirectory(Context context, Uri sourceUri) {
    244         final File[] dir = new File[1];
    245         querySource(context, sourceUri, new String[] {
    246                 ImageColumns.DATA }, new ContentResolverQueryCallback() {
    247                     @Override
    248                     public void onCursorResult(Cursor cursor) {
    249                         dir[0] = new File(cursor.getString(0)).getParentFile();
    250                     }
    251                 });
    252         return dir[0];
    253     }
    254 
    255     public static Uri insertContent(Context context, Uri sourceUri, File file, String saveFileName,
    256             long time) {
    257         time /= 1000;
    258 
    259         final ContentValues values = new ContentValues();
    260         values.put(Images.Media.TITLE, saveFileName);
    261         values.put(Images.Media.DISPLAY_NAME, file.getName());
    262         values.put(Images.Media.MIME_TYPE, "image/jpeg");
    263         values.put(Images.Media.DATE_TAKEN, time);
    264         values.put(Images.Media.DATE_MODIFIED, time);
    265         values.put(Images.Media.DATE_ADDED, time);
    266         values.put(Images.Media.ORIENTATION, 0);
    267         values.put(Images.Media.DATA, file.getAbsolutePath());
    268         values.put(Images.Media.SIZE, file.length());
    269 
    270         final String[] projection = new String[] {
    271                 ImageColumns.DATE_TAKEN,
    272                 ImageColumns.LATITUDE, ImageColumns.LONGITUDE,
    273         };
    274         querySource(context, sourceUri, projection,
    275                 new ContentResolverQueryCallback() {
    276 
    277                     @Override
    278                     public void onCursorResult(Cursor cursor) {
    279                         values.put(Images.Media.DATE_TAKEN, cursor.getLong(0));
    280 
    281                         double latitude = cursor.getDouble(1);
    282                         double longitude = cursor.getDouble(2);
    283                         // TODO: Change || to && after the default location
    284                         // issue is fixed.
    285                         if ((latitude != 0f) || (longitude != 0f)) {
    286                             values.put(Images.Media.LATITUDE, latitude);
    287                             values.put(Images.Media.LONGITUDE, longitude);
    288                         }
    289                     }
    290                 });
    291 
    292         return context.getContentResolver().insert(
    293                 Images.Media.EXTERNAL_CONTENT_URI, values);
    294     }
    295 
    296     public static Uri makeAndInsertUri(Context context, Uri sourceUri) {
    297         long time = System.currentTimeMillis();
    298         String filename = getNewFileName(time);
    299         File file = getNewFile(context, sourceUri, filename);
    300         return insertContent(context, sourceUri, file, filename, time);
    301     }
    302 }
    303