1/* 2 * Copyright (C) 2007 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.camera; 18 19import com.android.camera.gallery.BaseImageList; 20import com.android.camera.gallery.IImage; 21import com.android.camera.gallery.IImageList; 22import com.android.camera.gallery.ImageList; 23import com.android.camera.gallery.ImageListUber; 24import com.android.camera.gallery.VideoList; 25 26import android.content.ContentResolver; 27import android.content.ContentValues; 28import android.database.Cursor; 29import android.graphics.Bitmap; 30import android.graphics.Bitmap.CompressFormat; 31import android.location.Location; 32import android.media.ExifInterface; 33import android.net.Uri; 34import android.os.Environment; 35import android.os.Parcel; 36import android.os.Parcelable; 37import android.provider.MediaStore; 38import android.provider.MediaStore.Images; 39import android.util.Log; 40import android.view.OrientationEventListener; 41 42import java.io.File; 43import java.io.FileNotFoundException; 44import java.io.FileOutputStream; 45import java.io.IOException; 46import java.io.OutputStream; 47import java.util.ArrayList; 48import java.util.Iterator; 49 50/** 51 * {@code ImageManager} is used to retrieve and store images 52 * in the media content provider. 53 */ 54public class ImageManager { 55 private static final String TAG = "ImageManager"; 56 57 private static final Uri STORAGE_URI = Images.Media.EXTERNAL_CONTENT_URI; 58 private static final Uri VIDEO_STORAGE_URI = 59 Uri.parse("content://media/external/video/media"); 60 61 private ImageManager() { 62 } 63 64 /** 65 * {@code ImageListParam} specifies all the parameters we need to create an 66 * image list (we also need a ContentResolver). 67 */ 68 public static class ImageListParam implements Parcelable { 69 public DataLocation mLocation; 70 public int mInclusion; 71 public int mSort; 72 public String mBucketId; 73 74 // This is only used if we are creating an empty image list. 75 public boolean mIsEmptyImageList; 76 77 public ImageListParam() { 78 } 79 80 public void writeToParcel(Parcel out, int flags) { 81 out.writeInt(mLocation.ordinal()); 82 out.writeInt(mInclusion); 83 out.writeInt(mSort); 84 out.writeString(mBucketId); 85 out.writeInt(mIsEmptyImageList ? 1 : 0); 86 } 87 88 private ImageListParam(Parcel in) { 89 mLocation = DataLocation.values()[in.readInt()]; 90 mInclusion = in.readInt(); 91 mSort = in.readInt(); 92 mBucketId = in.readString(); 93 mIsEmptyImageList = (in.readInt() != 0); 94 } 95 96 @Override 97 public String toString() { 98 return String.format("ImageListParam{loc=%s,inc=%d,sort=%d," + 99 "bucket=%s,empty=%b}", mLocation, mInclusion, 100 mSort, mBucketId, mIsEmptyImageList); 101 } 102 103 public static final Parcelable.Creator<ImageListParam> CREATOR 104 = new Parcelable.Creator<ImageListParam>() { 105 public ImageListParam createFromParcel(Parcel in) { 106 return new ImageListParam(in); 107 } 108 109 public ImageListParam[] newArray(int size) { 110 return new ImageListParam[size]; 111 } 112 }; 113 114 public int describeContents() { 115 return 0; 116 } 117 } 118 119 // Location 120 public static enum DataLocation { NONE, INTERNAL, EXTERNAL, ALL } 121 122 // Inclusion 123 public static final int INCLUDE_IMAGES = (1 << 0); 124 public static final int INCLUDE_VIDEOS = (1 << 2); 125 126 // Sort 127 public static final int SORT_ASCENDING = 1; 128 public static final int SORT_DESCENDING = 2; 129 130 public static final String CAMERA_IMAGE_BUCKET_NAME = 131 Environment.getExternalStorageDirectory().toString() 132 + "/DCIM/Camera"; 133 public static final String CAMERA_IMAGE_BUCKET_ID = 134 getBucketId(CAMERA_IMAGE_BUCKET_NAME); 135 136 /** 137 * Matches code in MediaProvider.computeBucketValues. Should be a common 138 * function. 139 */ 140 public static String getBucketId(String path) { 141 return String.valueOf(path.toLowerCase().hashCode()); 142 } 143 144 /** 145 * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be 146 * imported. This is a temporary fix for bug#1655552. 147 */ 148 public static void ensureOSXCompatibleFolder() { 149 File nnnAAAAA = new File( 150 Environment.getExternalStorageDirectory().toString() 151 + "/DCIM/100ANDRO"); 152 if ((!nnnAAAAA.exists()) && (!nnnAAAAA.mkdir())) { 153 Log.e(TAG, "create NNNAAAAA file: " + nnnAAAAA.getPath() 154 + " failed"); 155 } 156 } 157 158 public static int roundOrientation(int orientationInput) { 159 int orientation = orientationInput; 160 161 if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) { 162 orientation = 0; 163 } 164 165 orientation = orientation % 360; 166 int retVal; 167 if (orientation < (0 * 90) + 45) { 168 retVal = 0; 169 } else if (orientation < (1 * 90) + 45) { 170 retVal = 90; 171 } else if (orientation < (2 * 90) + 45) { 172 retVal = 180; 173 } else if (orientation < (3 * 90) + 45) { 174 retVal = 270; 175 } else { 176 retVal = 0; 177 } 178 179 return retVal; 180 } 181 182 // 183 // Stores a bitmap or a jpeg byte array to a file (using the specified 184 // directory and filename). Also add an entry to the media store for 185 // this picture. The title, dateTaken, location are attributes for the 186 // picture. The degree is a one element array which returns the orientation 187 // of the picture. 188 // 189 public static Uri addImage(ContentResolver cr, String title, long dateTaken, 190 Location location, String directory, String filename, 191 Bitmap source, byte[] jpegData, int[] degree) { 192 // We should store image data earlier than insert it to ContentProvider, 193 // otherwise we may not be able to generate thumbnail in time. 194 OutputStream outputStream = null; 195 String filePath = directory + "/" + filename; 196 try { 197 File dir = new File(directory); 198 if (!dir.exists()) dir.mkdirs(); 199 File file = new File(directory, filename); 200 outputStream = new FileOutputStream(file); 201 if (source != null) { 202 source.compress(CompressFormat.JPEG, 75, outputStream); 203 degree[0] = 0; 204 } else { 205 outputStream.write(jpegData); 206 degree[0] = getExifOrientation(filePath); 207 } 208 } catch (FileNotFoundException ex) { 209 Log.w(TAG, ex); 210 return null; 211 } catch (IOException ex) { 212 Log.w(TAG, ex); 213 return null; 214 } finally { 215 Util.closeSilently(outputStream); 216 } 217 218 // Read back the compressed file size. 219 long size = new File(directory, filename).length(); 220 221 ContentValues values = new ContentValues(9); 222 values.put(Images.Media.TITLE, title); 223 224 // That filename is what will be handed to Gmail when a user shares a 225 // photo. Gmail gets the name of the picture attachment from the 226 // "DISPLAY_NAME" field. 227 values.put(Images.Media.DISPLAY_NAME, filename); 228 values.put(Images.Media.DATE_TAKEN, dateTaken); 229 values.put(Images.Media.MIME_TYPE, "image/jpeg"); 230 values.put(Images.Media.ORIENTATION, degree[0]); 231 values.put(Images.Media.DATA, filePath); 232 values.put(Images.Media.SIZE, size); 233 234 if (location != null) { 235 values.put(Images.Media.LATITUDE, location.getLatitude()); 236 values.put(Images.Media.LONGITUDE, location.getLongitude()); 237 } 238 239 return cr.insert(STORAGE_URI, values); 240 } 241 242 public static int getExifOrientation(String filepath) { 243 int degree = 0; 244 ExifInterface exif = null; 245 try { 246 exif = new ExifInterface(filepath); 247 } catch (IOException ex) { 248 Log.e(TAG, "cannot read exif", ex); 249 } 250 if (exif != null) { 251 int orientation = exif.getAttributeInt( 252 ExifInterface.TAG_ORIENTATION, -1); 253 if (orientation != -1) { 254 // We only recognize a subset of orientation tag values. 255 switch(orientation) { 256 case ExifInterface.ORIENTATION_ROTATE_90: 257 degree = 90; 258 break; 259 case ExifInterface.ORIENTATION_ROTATE_180: 260 degree = 180; 261 break; 262 case ExifInterface.ORIENTATION_ROTATE_270: 263 degree = 270; 264 break; 265 } 266 267 } 268 } 269 return degree; 270 } 271 272 // This is the factory function to create an image list. 273 public static IImageList makeImageList(ContentResolver cr, 274 ImageListParam param) { 275 DataLocation location = param.mLocation; 276 int inclusion = param.mInclusion; 277 int sort = param.mSort; 278 String bucketId = param.mBucketId; 279 boolean isEmptyImageList = param.mIsEmptyImageList; 280 281 if (isEmptyImageList || cr == null) { 282 return new EmptyImageList(); 283 } 284 285 // false ==> don't require write access 286 boolean haveSdCard = hasStorage(false); 287 288 // use this code to merge videos and stills into the same list 289 ArrayList<BaseImageList> l = new ArrayList<BaseImageList>(); 290 291 if (haveSdCard && location != DataLocation.INTERNAL) { 292 if ((inclusion & INCLUDE_IMAGES) != 0) { 293 l.add(new ImageList(cr, STORAGE_URI, sort, bucketId)); 294 } 295 if ((inclusion & INCLUDE_VIDEOS) != 0) { 296 l.add(new VideoList(cr, VIDEO_STORAGE_URI, sort, bucketId)); 297 } 298 } 299 if (location == DataLocation.INTERNAL || location == DataLocation.ALL) { 300 if ((inclusion & INCLUDE_IMAGES) != 0) { 301 l.add(new ImageList(cr, 302 Images.Media.INTERNAL_CONTENT_URI, sort, bucketId)); 303 } 304 } 305 306 // Optimization: If some of the lists are empty, remove them. 307 // If there is only one remaining list, return it directly. 308 Iterator<BaseImageList> iter = l.iterator(); 309 while (iter.hasNext()) { 310 BaseImageList sublist = iter.next(); 311 if (sublist.isEmpty()) { 312 sublist.close(); 313 iter.remove(); 314 } 315 } 316 317 if (l.size() == 1) { 318 BaseImageList list = l.get(0); 319 return list; 320 } 321 322 ImageListUber uber = new ImageListUber( 323 l.toArray(new IImageList[l.size()]), sort); 324 return uber; 325 } 326 327 private static class EmptyImageList implements IImageList { 328 public void close() { 329 } 330 331 public int getCount() { 332 return 0; 333 } 334 335 public IImage getImageAt(int i) { 336 return null; 337 } 338 } 339 340 public static ImageListParam getImageListParam(DataLocation location, 341 int inclusion, int sort, String bucketId) { 342 ImageListParam param = new ImageListParam(); 343 param.mLocation = location; 344 param.mInclusion = inclusion; 345 param.mSort = sort; 346 param.mBucketId = bucketId; 347 return param; 348 } 349 350 public static IImageList makeImageList(ContentResolver cr, 351 DataLocation location, int inclusion, int sort, String bucketId) { 352 ImageListParam param = getImageListParam(location, inclusion, sort, 353 bucketId); 354 return makeImageList(cr, param); 355 } 356 357 private static boolean checkFsWritable() { 358 // Create a temporary file to see whether a volume is really writeable. 359 // It's important not to put it in the root directory which may have a 360 // limit on the number of files. 361 String directoryName = 362 Environment.getExternalStorageDirectory().toString() + "/DCIM"; 363 File directory = new File(directoryName); 364 if (!directory.isDirectory()) { 365 if (!directory.mkdirs()) { 366 return false; 367 } 368 } 369 return directory.canWrite(); 370 } 371 372 public static boolean hasStorage() { 373 return hasStorage(true); 374 } 375 376 public static boolean hasStorage(boolean requireWriteAccess) { 377 String state = Environment.getExternalStorageState(); 378 379 if (Environment.MEDIA_MOUNTED.equals(state)) { 380 if (requireWriteAccess) { 381 boolean writable = checkFsWritable(); 382 return writable; 383 } else { 384 return true; 385 } 386 } else if (!requireWriteAccess 387 && Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { 388 return true; 389 } 390 return false; 391 } 392 393 private static Cursor query(ContentResolver resolver, Uri uri, 394 String[] projection, String selection, String[] selectionArgs, 395 String sortOrder) { 396 try { 397 if (resolver == null) { 398 return null; 399 } 400 return resolver.query( 401 uri, projection, selection, selectionArgs, sortOrder); 402 } catch (UnsupportedOperationException ex) { 403 return null; 404 } 405 406 } 407 408 public static boolean isMediaScannerScanning(ContentResolver cr) { 409 boolean result = false; 410 Cursor cursor = query(cr, MediaStore.getMediaScannerUri(), 411 new String [] {MediaStore.MEDIA_SCANNER_VOLUME}, 412 null, null, null); 413 if (cursor != null) { 414 if (cursor.getCount() == 1) { 415 cursor.moveToFirst(); 416 result = "external".equals(cursor.getString(0)); 417 } 418 cursor.close(); 419 } 420 421 return result; 422 } 423 424 public static String getLastImageThumbPath() { 425 return Environment.getExternalStorageDirectory().toString() + 426 "/DCIM/.thumbnails/image_last_thumb"; 427 } 428 429 public static String getLastVideoThumbPath() { 430 return Environment.getExternalStorageDirectory().toString() + 431 "/DCIM/.thumbnails/video_last_thumb"; 432 } 433 434 public static String getTempJpegPath() { 435 return Environment.getExternalStorageDirectory().toString() + 436 "/DCIM/.tempjpeg"; 437 } 438} 439