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