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 82 public ImageListParam() {} 83 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 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 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 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 */ 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 */ 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 164 /** 165 * @return true if the mimetype is an image mimetype. 166 */ 167 public static boolean isImageMimeType(String mimeType) { 168 return mimeType.startsWith("image/"); 169 } 170 171 /** 172 * @return true if the mimetype is a video mimetype. 173 */ 174 /* This is commented out because isVideo is not calling this now. 175 public static boolean isVideoMimeType(String mimeType) { 176 return mimeType.startsWith("video/"); 177 } 178 */ 179 180 /** 181 * @return true if the image is an image. 182 */ 183 public static boolean isImage(IImage image) { 184 return isImageMimeType(image.getMimeType()); 185 } 186 187 /** 188 * @return true if the image is a video. 189 */ 190 public static boolean isVideo(IImage image) { 191 // This is the right implementation, but we use instanceof for speed. 192 //return isVideoMimeType(image.getMimeType()); 193 return (image instanceof VideoObject); 194 } 195 196 // 197 // Stores a bitmap or a jpeg byte array to a file (using the specified 198 // directory and filename). Also add an entry to the media store for 199 // this picture. The title, dateTaken, location are attributes for the 200 // picture. The degree is a one element array which returns the orientation 201 // of the picture. 202 // 203 public static Uri addImage(ContentResolver cr, String title, long dateTaken, 204 Location location, String directory, String filename, 205 Bitmap source, byte[] jpegData, int[] degree) { 206 // We should store image data earlier than insert it to ContentProvider, otherwise 207 // we may not be able to generate thumbnail in time. 208 OutputStream outputStream = null; 209 String filePath = directory + "/" + filename; 210 try { 211 File dir = new File(directory); 212 if (!dir.exists()) dir.mkdirs(); 213 File file = new File(directory, filename); 214 outputStream = new FileOutputStream(file); 215 if (source != null) { 216 source.compress(CompressFormat.JPEG, 75, outputStream); 217 degree[0] = 0; 218 } else { 219 outputStream.write(jpegData); 220 degree[0] = getExifOrientation(filePath); 221 } 222 } catch (FileNotFoundException ex) { 223 Log.w(TAG, ex); 224 return null; 225 } catch (IOException ex) { 226 Log.w(TAG, ex); 227 return null; 228 } finally { 229 Util.closeSilently(outputStream); 230 } 231 232 ContentValues values = new ContentValues(7); 233 values.put(Images.Media.TITLE, title); 234 235 // That filename is what will be handed to Gmail when a user shares a 236 // photo. Gmail gets the name of the picture attachment from the 237 // "DISPLAY_NAME" field. 238 values.put(Images.Media.DISPLAY_NAME, filename); 239 values.put(Images.Media.DATE_TAKEN, dateTaken); 240 values.put(Images.Media.MIME_TYPE, "image/jpeg"); 241 values.put(Images.Media.ORIENTATION, degree[0]); 242 values.put(Images.Media.DATA, filePath); 243 244 if (location != null) { 245 values.put(Images.Media.LATITUDE, location.getLatitude()); 246 values.put(Images.Media.LONGITUDE, location.getLongitude()); 247 } 248 249 return cr.insert(STORAGE_URI, values); 250 } 251 252 public static int getExifOrientation(String filepath) { 253 int degree = 0; 254 ExifInterface exif = null; 255 try { 256 exif = new ExifInterface(filepath); 257 } catch (IOException ex) { 258 Log.e(TAG, "cannot read exif", ex); 259 } 260 if (exif != null) { 261 int orientation = exif.getAttributeInt( 262 ExifInterface.TAG_ORIENTATION, -1); 263 if (orientation != -1) { 264 // We only recognize a subset of orientation tag values. 265 switch(orientation) { 266 case ExifInterface.ORIENTATION_ROTATE_90: 267 degree = 90; 268 break; 269 case ExifInterface.ORIENTATION_ROTATE_180: 270 degree = 180; 271 break; 272 case ExifInterface.ORIENTATION_ROTATE_270: 273 degree = 270; 274 break; 275 } 276 277 } 278 } 279 return degree; 280 } 281 282 // This is the factory function to create an image list. 283 public static IImageList makeImageList(ContentResolver cr, 284 ImageListParam param) { 285 DataLocation location = param.mLocation; 286 int inclusion = param.mInclusion; 287 int sort = param.mSort; 288 String bucketId = param.mBucketId; 289 Uri singleImageUri = param.mSingleImageUri; 290 boolean isEmptyImageList = param.mIsEmptyImageList; 291 292 if (isEmptyImageList || cr == null) { 293 return new EmptyImageList(); 294 } 295 296 if (singleImageUri != null) { 297 return new SingleImageList(cr, singleImageUri); 298 } 299 300 // false ==> don't require write access 301 boolean haveSdCard = hasStorage(false); 302 303 // use this code to merge videos and stills into the same list 304 ArrayList<BaseImageList> l = new ArrayList<BaseImageList>(); 305 306 if (haveSdCard && location != DataLocation.INTERNAL) { 307 if ((inclusion & INCLUDE_IMAGES) != 0) { 308 l.add(new ImageList(cr, STORAGE_URI, sort, bucketId)); 309 } 310 if ((inclusion & INCLUDE_VIDEOS) != 0) { 311 l.add(new VideoList(cr, VIDEO_STORAGE_URI, sort, bucketId)); 312 } 313 } 314 if (location == DataLocation.INTERNAL || location == DataLocation.ALL) { 315 if ((inclusion & INCLUDE_IMAGES) != 0) { 316 l.add(new ImageList(cr, 317 Images.Media.INTERNAL_CONTENT_URI, sort, bucketId)); 318 } 319 if ((inclusion & INCLUDE_DRM_IMAGES) != 0) { 320 l.add(new DrmImageList( 321 cr, DrmStore.Images.CONTENT_URI, sort, bucketId)); 322 } 323 } 324 325 // Optimization: If some of the lists are empty, remove them. 326 // If there is only one remaining list, return it directly. 327 Iterator<BaseImageList> iter = l.iterator(); 328 while (iter.hasNext()) { 329 BaseImageList sublist = iter.next(); 330 if (sublist.isEmpty()) { 331 sublist.close(); 332 iter.remove(); 333 } 334 } 335 336 if (l.size() == 1) { 337 BaseImageList list = l.get(0); 338 return list; 339 } 340 341 ImageListUber uber = new ImageListUber( 342 l.toArray(new IImageList[l.size()]), sort); 343 return uber; 344 } 345 346 // This is a convenience function to create an image list from a Uri. 347 public static IImageList makeImageList(ContentResolver cr, Uri uri, 348 int sort) { 349 String uriString = (uri != null) ? uri.toString() : ""; 350 351 // TODO: we need to figure out whether we're viewing 352 // DRM images in a better way. Is there a constant 353 // for content://drm somewhere?? 354 355 if (uriString.startsWith("content://drm")) { 356 return makeImageList(cr, DataLocation.ALL, INCLUDE_DRM_IMAGES, sort, 357 null); 358 } else if (uriString.startsWith("content://media/external/video")) { 359 return makeImageList(cr, DataLocation.EXTERNAL, INCLUDE_VIDEOS, 360 sort, null); 361 } else if (isSingleImageMode(uriString)) { 362 return makeSingleImageList(cr, uri); 363 } else { 364 String bucketId = uri.getQueryParameter("bucketId"); 365 return makeImageList(cr, DataLocation.ALL, INCLUDE_IMAGES, sort, 366 bucketId); 367 } 368 } 369 370 static boolean isSingleImageMode(String uriString) { 371 return !uriString.startsWith( 372 MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString()) 373 && !uriString.startsWith( 374 MediaStore.Images.Media.INTERNAL_CONTENT_URI.toString()); 375 } 376 377 private static class EmptyImageList implements IImageList { 378 public void close() { 379 } 380 381 public HashMap<String, String> getBucketIds() { 382 return new HashMap<String, String>(); 383 } 384 385 public int getCount() { 386 return 0; 387 } 388 389 public boolean isEmpty() { 390 return true; 391 } 392 393 public IImage getImageAt(int i) { 394 return null; 395 } 396 397 public IImage getImageForUri(Uri uri) { 398 return null; 399 } 400 401 public boolean removeImage(IImage image) { 402 return false; 403 } 404 405 public boolean removeImageAt(int i) { 406 return false; 407 } 408 409 public int getImageIndex(IImage image) { 410 throw new UnsupportedOperationException(); 411 } 412 } 413 414 public static ImageListParam getImageListParam(DataLocation location, 415 int inclusion, int sort, String bucketId) { 416 ImageListParam param = new ImageListParam(); 417 param.mLocation = location; 418 param.mInclusion = inclusion; 419 param.mSort = sort; 420 param.mBucketId = bucketId; 421 return param; 422 } 423 424 public static ImageListParam getSingleImageListParam(Uri uri) { 425 ImageListParam param = new ImageListParam(); 426 param.mSingleImageUri = uri; 427 return param; 428 } 429 430 public static ImageListParam getEmptyImageListParam() { 431 ImageListParam param = new ImageListParam(); 432 param.mIsEmptyImageList = true; 433 return param; 434 } 435 436 public static IImageList makeImageList(ContentResolver cr, 437 DataLocation location, int inclusion, int sort, String bucketId) { 438 ImageListParam param = getImageListParam(location, inclusion, sort, 439 bucketId); 440 return makeImageList(cr, param); 441 } 442 443 public static IImageList makeEmptyImageList() { 444 return makeImageList(null, getEmptyImageListParam()); 445 } 446 447 public static IImageList makeSingleImageList(ContentResolver cr, Uri uri) { 448 return makeImageList(cr, getSingleImageListParam(uri)); 449 } 450 451 private static boolean checkFsWritable() { 452 // Create a temporary file to see whether a volume is really writeable. 453 // It's important not to put it in the root directory which may have a 454 // limit on the number of files. 455 String directoryName = 456 Environment.getExternalStorageDirectory().toString() + "/DCIM"; 457 File directory = new File(directoryName); 458 if (!directory.isDirectory()) { 459 if (!directory.mkdirs()) { 460 return false; 461 } 462 } 463 File f = new File(directoryName, ".probe"); 464 try { 465 // Remove stale file if any 466 if (f.exists()) { 467 f.delete(); 468 } 469 if (!f.createNewFile()) { 470 return false; 471 } 472 f.delete(); 473 return true; 474 } catch (IOException ex) { 475 return false; 476 } 477 } 478 479 public static boolean hasStorage() { 480 return hasStorage(true); 481 } 482 483 public static boolean hasStorage(boolean requireWriteAccess) { 484 String state = Environment.getExternalStorageState(); 485 486 if (Environment.MEDIA_MOUNTED.equals(state)) { 487 if (requireWriteAccess) { 488 boolean writable = checkFsWritable(); 489 return writable; 490 } else { 491 return true; 492 } 493 } else if (!requireWriteAccess 494 && Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { 495 return true; 496 } 497 return false; 498 } 499 500 private static Cursor query(ContentResolver resolver, Uri uri, 501 String[] projection, String selection, String[] selectionArgs, 502 String sortOrder) { 503 try { 504 if (resolver == null) { 505 return null; 506 } 507 return resolver.query( 508 uri, projection, selection, selectionArgs, sortOrder); 509 } catch (UnsupportedOperationException ex) { 510 return null; 511 } 512 513 } 514 515 public static boolean isMediaScannerScanning(ContentResolver cr) { 516 boolean result = false; 517 Cursor cursor = query(cr, MediaStore.getMediaScannerUri(), 518 new String [] {MediaStore.MEDIA_SCANNER_VOLUME}, 519 null, null, null); 520 if (cursor != null) { 521 if (cursor.getCount() == 1) { 522 cursor.moveToFirst(); 523 result = "external".equals(cursor.getString(0)); 524 } 525 cursor.close(); 526 } 527 528 return result; 529 } 530 } 531