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 17 package com.android.camera; 18 19 import com.android.camera.gallery.BaseImageList; 20 import com.android.camera.gallery.DrmImageList; 21 import com.android.camera.gallery.IImage; 22 import com.android.camera.gallery.IImageList; 23 import com.android.camera.gallery.ImageList; 24 import com.android.camera.gallery.ImageListUber; 25 import com.android.camera.gallery.SingleImageList; 26 import com.android.camera.gallery.VideoList; 27 import com.android.camera.gallery.VideoObject; 28 29 import android.content.ContentResolver; 30 import android.content.ContentValues; 31 import android.database.Cursor; 32 import android.graphics.Bitmap; 33 import android.graphics.Bitmap.CompressFormat; 34 import android.location.Location; 35 import android.media.ExifInterface; 36 import android.net.Uri; 37 import android.os.Environment; 38 import android.os.Parcel; 39 import android.os.Parcelable; 40 import android.provider.DrmStore; 41 import android.provider.MediaStore; 42 import android.provider.MediaStore.Images; 43 import android.util.Log; 44 45 import java.io.File; 46 import java.io.FileNotFoundException; 47 import java.io.FileOutputStream; 48 import java.io.IOException; 49 import java.io.OutputStream; 50 import java.util.ArrayList; 51 import java.util.HashMap; 52 import java.util.Iterator; 53 54 /** 55 * ImageManager is used to retrieve and store images 56 * in the media content provider. 57 */ 58 public class ImageManager { 59 private static final String TAG = "ImageManager"; 60 61 private static final Uri STORAGE_URI = Images.Media.EXTERNAL_CONTENT_URI; 62 private static final Uri THUMB_URI 63 = Images.Thumbnails.EXTERNAL_CONTENT_URI; 64 65 private static final Uri VIDEO_STORAGE_URI = 66 Uri.parse("content://media/external/video/media"); 67 68 // ImageListParam specifies all the parameters we need to create an image 69 // list (we also need a ContentResolver). 70 public static class ImageListParam implements Parcelable { 71 public DataLocation mLocation; 72 public int mInclusion; 73 public int mSort; 74 public String mBucketId; 75 76 // This is only used if we are creating a single image list. 77 public Uri mSingleImageUri; 78 79 // This is only used if we are creating an empty image list. 80 public boolean mIsEmptyImageList; 81 ImageListParam()82 public ImageListParam() {} 83 writeToParcel(Parcel out, int flags)84 public void writeToParcel(Parcel out, int flags) { 85 out.writeInt(mLocation.ordinal()); 86 out.writeInt(mInclusion); 87 out.writeInt(mSort); 88 out.writeString(mBucketId); 89 out.writeParcelable(mSingleImageUri, flags); 90 out.writeInt(mIsEmptyImageList ? 1 : 0); 91 } 92 ImageListParam(Parcel in)93 private ImageListParam(Parcel in) { 94 mLocation = DataLocation.values()[in.readInt()]; 95 mInclusion = in.readInt(); 96 mSort = in.readInt(); 97 mBucketId = in.readString(); 98 mSingleImageUri = in.readParcelable(null); 99 mIsEmptyImageList = (in.readInt() != 0); 100 } 101 toString()102 public String toString() { 103 return String.format("ImageListParam{loc=%s,inc=%d,sort=%d," + 104 "bucket=%s,empty=%b,single=%s}", mLocation, mInclusion, 105 mSort, mBucketId, mIsEmptyImageList, mSingleImageUri); 106 } 107 108 public static final Parcelable.Creator CREATOR 109 = new Parcelable.Creator() { 110 public ImageListParam createFromParcel(Parcel in) { 111 return new ImageListParam(in); 112 } 113 114 public ImageListParam[] newArray(int size) { 115 return new ImageListParam[size]; 116 } 117 }; 118 describeContents()119 public int describeContents() { 120 return 0; 121 } 122 } 123 124 // Location 125 public static enum DataLocation { NONE, INTERNAL, EXTERNAL, ALL } 126 127 // Inclusion 128 public static final int INCLUDE_IMAGES = (1 << 0); 129 public static final int INCLUDE_DRM_IMAGES = (1 << 1); 130 public static final int INCLUDE_VIDEOS = (1 << 2); 131 132 // Sort 133 public static final int SORT_ASCENDING = 1; 134 public static final int SORT_DESCENDING = 2; 135 136 public static final String CAMERA_IMAGE_BUCKET_NAME = 137 Environment.getExternalStorageDirectory().toString() 138 + "/DCIM/Camera"; 139 public static final String CAMERA_IMAGE_BUCKET_ID = 140 getBucketId(CAMERA_IMAGE_BUCKET_NAME); 141 142 /** 143 * Matches code in MediaProvider.computeBucketValues. Should be a common 144 * function. 145 */ getBucketId(String path)146 public static String getBucketId(String path) { 147 return String.valueOf(path.toLowerCase().hashCode()); 148 } 149 150 /** 151 * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be 152 * imported. This is a temporary fix for bug#1655552. 153 */ ensureOSXCompatibleFolder()154 public static void ensureOSXCompatibleFolder() { 155 File nnnAAAAA = new File( 156 Environment.getExternalStorageDirectory().toString() 157 + "/DCIM/100ANDRO"); 158 if ((!nnnAAAAA.exists()) && (!nnnAAAAA.mkdir())) { 159 Log.e(TAG, "create NNNAAAAA file: " + nnnAAAAA.getPath() 160 + " failed"); 161 } 162 } 163 roundOrientation(int orientationInput)164 public static int roundOrientation(int orientationInput) { 165 int orientation = orientationInput; 166 if (orientation == -1) { 167 orientation = 0; 168 } 169 170 orientation = orientation % 360; 171 int retVal; 172 if (orientation < (0 * 90) + 45) { 173 retVal = 0; 174 } else if (orientation < (1 * 90) + 45) { 175 retVal = 90; 176 } else if (orientation < (2 * 90) + 45) { 177 retVal = 180; 178 } else if (orientation < (3 * 90) + 45) { 179 retVal = 270; 180 } else { 181 retVal = 0; 182 } 183 184 return retVal; 185 } 186 187 /** 188 * @return true if the mimetype is an image mimetype. 189 */ isImageMimeType(String mimeType)190 public static boolean isImageMimeType(String mimeType) { 191 return mimeType.startsWith("image/"); 192 } 193 194 /** 195 * @return true if the mimetype is a video mimetype. 196 */ 197 /* This is commented out because isVideo is not calling this now. 198 public static boolean isVideoMimeType(String mimeType) { 199 return mimeType.startsWith("video/"); 200 } 201 */ 202 203 /** 204 * @return true if the image is an image. 205 */ isImage(IImage image)206 public static boolean isImage(IImage image) { 207 return isImageMimeType(image.getMimeType()); 208 } 209 210 /** 211 * @return true if the image is a video. 212 */ isVideo(IImage image)213 public static boolean isVideo(IImage image) { 214 // This is the right implementation, but we use instanceof for speed. 215 //return isVideoMimeType(image.getMimeType()); 216 return (image instanceof VideoObject); 217 } 218 setImageSize(ContentResolver cr, Uri uri, long size)219 public static void setImageSize(ContentResolver cr, Uri uri, long size) { 220 ContentValues values = new ContentValues(); 221 values.put(Images.Media.SIZE, size); 222 cr.update(uri, values, null, null); 223 } 224 225 // 226 // Stores a bitmap or a jpeg byte array to a file (using the specified 227 // directory and filename). Also add an entry to the media store for 228 // this picture. The title, dateTaken, location are attributes for the 229 // picture. The degree is a one element array which returns the orientation 230 // of the picture. 231 // addImage(ContentResolver cr, String title, long dateTaken, Location location, String directory, String filename, Bitmap source, byte[] jpegData, int[] degree)232 public static Uri addImage(ContentResolver cr, String title, long dateTaken, 233 Location location, String directory, String filename, 234 Bitmap source, byte[] jpegData, int[] degree) { 235 // We should store image data earlier than insert it to ContentProvider, otherwise 236 // we may not be able to generate thumbnail in time. 237 OutputStream outputStream = null; 238 String filePath = directory + "/" + filename; 239 try { 240 File dir = new File(directory); 241 if (!dir.exists()) dir.mkdirs(); 242 File file = new File(directory, filename); 243 outputStream = new FileOutputStream(file); 244 if (source != null) { 245 source.compress(CompressFormat.JPEG, 75, outputStream); 246 degree[0] = 0; 247 } else { 248 outputStream.write(jpegData); 249 degree[0] = getExifOrientation(filePath); 250 } 251 } catch (FileNotFoundException ex) { 252 Log.w(TAG, ex); 253 return null; 254 } catch (IOException ex) { 255 Log.w(TAG, ex); 256 return null; 257 } finally { 258 Util.closeSilently(outputStream); 259 } 260 261 ContentValues values = new ContentValues(7); 262 values.put(Images.Media.TITLE, title); 263 264 // That filename is what will be handed to Gmail when a user shares a 265 // photo. Gmail gets the name of the picture attachment from the 266 // "DISPLAY_NAME" field. 267 values.put(Images.Media.DISPLAY_NAME, filename); 268 values.put(Images.Media.DATE_TAKEN, dateTaken); 269 values.put(Images.Media.MIME_TYPE, "image/jpeg"); 270 values.put(Images.Media.ORIENTATION, degree[0]); 271 values.put(Images.Media.DATA, filePath); 272 273 if (location != null) { 274 values.put(Images.Media.LATITUDE, location.getLatitude()); 275 values.put(Images.Media.LONGITUDE, location.getLongitude()); 276 } 277 278 return cr.insert(STORAGE_URI, values); 279 } 280 getExifOrientation(String filepath)281 public static int getExifOrientation(String filepath) { 282 int degree = 0; 283 ExifInterface exif = null; 284 try { 285 exif = new ExifInterface(filepath); 286 } catch (IOException ex) { 287 Log.e(TAG, "cannot read exif", ex); 288 } 289 if (exif != null) { 290 int orientation = exif.getAttributeInt( 291 ExifInterface.TAG_ORIENTATION, -1); 292 if (orientation != -1) { 293 // We only recognize a subset of orientation tag values. 294 switch(orientation) { 295 case ExifInterface.ORIENTATION_ROTATE_90: 296 degree = 90; 297 break; 298 case ExifInterface.ORIENTATION_ROTATE_180: 299 degree = 180; 300 break; 301 case ExifInterface.ORIENTATION_ROTATE_270: 302 degree = 270; 303 break; 304 } 305 306 } 307 } 308 return degree; 309 } 310 311 // This is the factory function to create an image list. makeImageList(ContentResolver cr, ImageListParam param)312 public static IImageList makeImageList(ContentResolver cr, 313 ImageListParam param) { 314 DataLocation location = param.mLocation; 315 int inclusion = param.mInclusion; 316 int sort = param.mSort; 317 String bucketId = param.mBucketId; 318 Uri singleImageUri = param.mSingleImageUri; 319 boolean isEmptyImageList = param.mIsEmptyImageList; 320 321 if (isEmptyImageList || cr == null) { 322 return new EmptyImageList(); 323 } 324 325 if (singleImageUri != null) { 326 return new SingleImageList(cr, singleImageUri); 327 } 328 329 // false ==> don't require write access 330 boolean haveSdCard = hasStorage(false); 331 332 // use this code to merge videos and stills into the same list 333 ArrayList<BaseImageList> l = new ArrayList<BaseImageList>(); 334 335 if (haveSdCard && location != DataLocation.INTERNAL) { 336 if ((inclusion & INCLUDE_IMAGES) != 0) { 337 l.add(new ImageList( 338 cr, STORAGE_URI, THUMB_URI, sort, bucketId)); 339 } 340 if ((inclusion & INCLUDE_VIDEOS) != 0) { 341 l.add(new VideoList(cr, VIDEO_STORAGE_URI, sort, bucketId)); 342 } 343 } 344 if (location == DataLocation.INTERNAL || location == DataLocation.ALL) { 345 if ((inclusion & INCLUDE_IMAGES) != 0) { 346 l.add(new ImageList(cr, 347 Images.Media.INTERNAL_CONTENT_URI, 348 Images.Thumbnails.INTERNAL_CONTENT_URI, 349 sort, bucketId)); 350 } 351 if ((inclusion & INCLUDE_DRM_IMAGES) != 0) { 352 l.add(new DrmImageList( 353 cr, DrmStore.Images.CONTENT_URI, sort, bucketId)); 354 } 355 } 356 357 // Optimization: If some of the lists are empty, remove them. 358 // If there is only one remaining list, return it directly. 359 Iterator<BaseImageList> iter = l.iterator(); 360 while (iter.hasNext()) { 361 BaseImageList sublist = iter.next(); 362 if (sublist.isEmpty()) { 363 sublist.close(); 364 iter.remove(); 365 } 366 } 367 368 if (l.size() == 1) { 369 BaseImageList list = l.get(0); 370 return list; 371 } 372 373 ImageListUber uber = new ImageListUber( 374 l.toArray(new IImageList[l.size()]), sort); 375 return uber; 376 } 377 378 // This is a convenience function to create an image list from a Uri. makeImageList(ContentResolver cr, Uri uri, int sort)379 public static IImageList makeImageList(ContentResolver cr, Uri uri, 380 int sort) { 381 String uriString = (uri != null) ? uri.toString() : ""; 382 383 // TODO: we need to figure out whether we're viewing 384 // DRM images in a better way. Is there a constant 385 // for content://drm somewhere?? 386 387 if (uriString.startsWith("content://drm")) { 388 return makeImageList(cr, DataLocation.ALL, INCLUDE_DRM_IMAGES, sort, 389 null); 390 } else if (uriString.startsWith("content://media/external/video")) { 391 return makeImageList(cr, DataLocation.EXTERNAL, INCLUDE_VIDEOS, 392 sort, null); 393 } else if (isSingleImageMode(uriString)) { 394 return makeSingleImageList(cr, uri); 395 } else { 396 String bucketId = uri.getQueryParameter("bucketId"); 397 return makeImageList(cr, DataLocation.ALL, INCLUDE_IMAGES, sort, 398 bucketId); 399 } 400 } 401 isSingleImageMode(String uriString)402 static boolean isSingleImageMode(String uriString) { 403 return !uriString.startsWith( 404 MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString()) 405 && !uriString.startsWith( 406 MediaStore.Images.Media.INTERNAL_CONTENT_URI.toString()); 407 } 408 409 private static class EmptyImageList implements IImageList { close()410 public void close() { 411 } 412 getBucketIds()413 public HashMap<String, String> getBucketIds() { 414 return new HashMap<String, String>(); 415 } 416 getCount()417 public int getCount() { 418 return 0; 419 } 420 isEmpty()421 public boolean isEmpty() { 422 return true; 423 } 424 getImageAt(int i)425 public IImage getImageAt(int i) { 426 return null; 427 } 428 getImageForUri(Uri uri)429 public IImage getImageForUri(Uri uri) { 430 return null; 431 } 432 removeImage(IImage image)433 public boolean removeImage(IImage image) { 434 return false; 435 } 436 removeImageAt(int i)437 public boolean removeImageAt(int i) { 438 return false; 439 } 440 getImageIndex(IImage image)441 public int getImageIndex(IImage image) { 442 throw new UnsupportedOperationException(); 443 } 444 } 445 getImageListParam(DataLocation location, int inclusion, int sort, String bucketId)446 public static ImageListParam getImageListParam(DataLocation location, 447 int inclusion, int sort, String bucketId) { 448 ImageListParam param = new ImageListParam(); 449 param.mLocation = location; 450 param.mInclusion = inclusion; 451 param.mSort = sort; 452 param.mBucketId = bucketId; 453 return param; 454 } 455 getSingleImageListParam(Uri uri)456 public static ImageListParam getSingleImageListParam(Uri uri) { 457 ImageListParam param = new ImageListParam(); 458 param.mSingleImageUri = uri; 459 return param; 460 } 461 getEmptyImageListParam()462 public static ImageListParam getEmptyImageListParam() { 463 ImageListParam param = new ImageListParam(); 464 param.mIsEmptyImageList = true; 465 return param; 466 } 467 makeImageList(ContentResolver cr, DataLocation location, int inclusion, int sort, String bucketId)468 public static IImageList makeImageList(ContentResolver cr, 469 DataLocation location, int inclusion, int sort, String bucketId) { 470 ImageListParam param = getImageListParam(location, inclusion, sort, 471 bucketId); 472 return makeImageList(cr, param); 473 } 474 makeEmptyImageList()475 public static IImageList makeEmptyImageList() { 476 return makeImageList(null, getEmptyImageListParam()); 477 } 478 makeSingleImageList(ContentResolver cr, Uri uri)479 public static IImageList makeSingleImageList(ContentResolver cr, Uri uri) { 480 return makeImageList(cr, getSingleImageListParam(uri)); 481 } 482 checkFsWritable()483 private static boolean checkFsWritable() { 484 // Create a temporary file to see whether a volume is really writeable. 485 // It's important not to put it in the root directory which may have a 486 // limit on the number of files. 487 String directoryName = 488 Environment.getExternalStorageDirectory().toString() + "/DCIM"; 489 File directory = new File(directoryName); 490 if (!directory.isDirectory()) { 491 if (!directory.mkdirs()) { 492 return false; 493 } 494 } 495 File f = new File(directoryName, ".probe"); 496 try { 497 // Remove stale file if any 498 if (f.exists()) { 499 f.delete(); 500 } 501 if (!f.createNewFile()) { 502 return false; 503 } 504 f.delete(); 505 return true; 506 } catch (IOException ex) { 507 return false; 508 } 509 } 510 hasStorage()511 public static boolean hasStorage() { 512 return hasStorage(true); 513 } 514 hasStorage(boolean requireWriteAccess)515 public static boolean hasStorage(boolean requireWriteAccess) { 516 String state = Environment.getExternalStorageState(); 517 518 if (Environment.MEDIA_MOUNTED.equals(state)) { 519 if (requireWriteAccess) { 520 boolean writable = checkFsWritable(); 521 return writable; 522 } else { 523 return true; 524 } 525 } else if (!requireWriteAccess 526 && Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { 527 return true; 528 } 529 return false; 530 } 531 query(ContentResolver resolver, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)532 private static Cursor query(ContentResolver resolver, Uri uri, 533 String[] projection, String selection, String[] selectionArgs, 534 String sortOrder) { 535 try { 536 if (resolver == null) { 537 return null; 538 } 539 return resolver.query( 540 uri, projection, selection, selectionArgs, sortOrder); 541 } catch (UnsupportedOperationException ex) { 542 return null; 543 } 544 545 } 546 isMediaScannerScanning(ContentResolver cr)547 public static boolean isMediaScannerScanning(ContentResolver cr) { 548 boolean result = false; 549 Cursor cursor = query(cr, MediaStore.getMediaScannerUri(), 550 new String [] {MediaStore.MEDIA_SCANNER_VOLUME}, 551 null, null, null); 552 if (cursor != null) { 553 if (cursor.getCount() == 1) { 554 cursor.moveToFirst(); 555 result = "external".equals(cursor.getString(0)); 556 } 557 cursor.close(); 558 } 559 560 return result; 561 } 562 getLastImageThumbPath()563 public static String getLastImageThumbPath() { 564 return Environment.getExternalStorageDirectory().toString() + 565 "/DCIM/.thumbnails/image_last_thumb"; 566 } 567 getLastVideoThumbPath()568 public static String getLastVideoThumbPath() { 569 return Environment.getExternalStorageDirectory().toString() + 570 "/DCIM/.thumbnails/video_last_thumb"; 571 } 572 getTempJpegPath()573 public static String getTempJpegPath() { 574 return Environment.getExternalStorageDirectory().toString() + 575 "/DCIM/.tempjpeg"; 576 } 577 } 578