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.BaseCancelable; 20 import com.android.camera.gallery.BaseImageList; 21 import com.android.camera.gallery.Cancelable; 22 import com.android.camera.gallery.DrmImageList; 23 import com.android.camera.gallery.IImage; 24 import com.android.camera.gallery.IImageList; 25 import com.android.camera.gallery.Image; 26 import com.android.camera.gallery.ImageList; 27 import com.android.camera.gallery.ImageListUber; 28 import com.android.camera.gallery.SingleImageList; 29 import com.android.camera.gallery.VideoList; 30 import com.android.camera.gallery.VideoObject; 31 32 import android.content.ContentResolver; 33 import android.content.ContentUris; 34 import android.content.ContentValues; 35 import android.database.Cursor; 36 import android.graphics.Bitmap; 37 import android.location.Location; 38 import android.net.Uri; 39 import android.os.Environment; 40 import android.os.Parcel; 41 import android.provider.DrmStore; 42 import android.provider.MediaStore; 43 import android.provider.MediaStore.Images; 44 import android.provider.MediaStore.Images.ImageColumns; 45 import android.util.Log; 46 47 import java.io.File; 48 import java.io.IOException; 49 import java.util.ArrayList; 50 import java.util.HashMap; 51 import java.util.Iterator; 52 import java.util.concurrent.ExecutionException; 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 /** 69 * Enumerate type for the location of the images in gallery. 70 */ 71 public static enum DataLocation { NONE, INTERNAL, EXTERNAL, ALL } 72 73 public static final Bitmap DEFAULT_THUMBNAIL = 74 Bitmap.createBitmap(32, 32, Bitmap.Config.RGB_565); 75 public static final Bitmap NO_IMAGE_BITMAP = 76 Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565); 77 78 public static final int SORT_ASCENDING = 1; 79 public static final int SORT_DESCENDING = 2; 80 81 public static final int INCLUDE_IMAGES = (1 << 0); 82 public static final int INCLUDE_DRM_IMAGES = (1 << 1); 83 public static final int INCLUDE_VIDEOS = (1 << 2); 84 85 public static final String CAMERA_IMAGE_BUCKET_NAME = 86 Environment.getExternalStorageDirectory().toString() 87 + "/DCIM/Camera"; 88 public static final String CAMERA_IMAGE_BUCKET_ID = 89 getBucketId(CAMERA_IMAGE_BUCKET_NAME); 90 91 /** 92 * Matches code in MediaProvider.computeBucketValues. Should be a common 93 * function. 94 */ getBucketId(String path)95 public static String getBucketId(String path) { 96 return String.valueOf(path.toLowerCase().hashCode()); 97 } 98 99 /** 100 * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be 101 * imported. This is a temporary fix for bug#1655552. 102 */ ensureOSXCompatibleFolder()103 public static void ensureOSXCompatibleFolder() { 104 File nnnAAAAA = new File( 105 Environment.getExternalStorageDirectory().toString() 106 + "/DCIM/100ANDRO"); 107 if ((!nnnAAAAA.exists()) && (!nnnAAAAA.mkdir())) { 108 Log.e(TAG, "create NNNAAAAA file: " + nnnAAAAA.getPath() 109 + " failed"); 110 } 111 } 112 roundOrientation(int orientationInput)113 public static int roundOrientation(int orientationInput) { 114 int orientation = orientationInput; 115 if (orientation == -1) { 116 orientation = 0; 117 } 118 119 orientation = orientation % 360; 120 int retVal; 121 if (orientation < (0 * 90) + 45) { 122 retVal = 0; 123 } else if (orientation < (1 * 90) + 45) { 124 retVal = 90; 125 } else if (orientation < (2 * 90) + 45) { 126 retVal = 180; 127 } else if (orientation < (3 * 90) + 45) { 128 retVal = 270; 129 } else { 130 retVal = 0; 131 } 132 133 return retVal; 134 } 135 136 /** 137 * @return true if the mimetype is an image mimetype. 138 */ isImageMimeType(String mimeType)139 public static boolean isImageMimeType(String mimeType) { 140 return mimeType.startsWith("image/"); 141 } 142 143 /** 144 * @return true if the mimetype is a video mimetype. 145 */ isVideoMimeType(String mimeType)146 public static boolean isVideoMimeType(String mimeType) { 147 return mimeType.startsWith("video/"); 148 } 149 150 /** 151 * @return true if the image is an image. 152 */ isImage(IImage image)153 public static boolean isImage(IImage image) { 154 return isImageMimeType(image.getMimeType()); 155 } 156 157 /** 158 * @return true if the image is a video. 159 */ isVideo(IImage image)160 public static boolean isVideo(IImage image) { 161 // This is the right implementation, but we use instanceof for speed. 162 //return isVideoMimeType(image.getMimeType()); 163 return (image instanceof VideoObject); 164 } 165 setImageSize(ContentResolver cr, Uri uri, long size)166 public static void setImageSize(ContentResolver cr, Uri uri, long size) { 167 ContentValues values = new ContentValues(); 168 values.put(Images.Media.SIZE, size); 169 cr.update(uri, values, null, null); 170 } 171 addImage(ContentResolver cr, String title, long dateTaken, Location location, int orientation, String directory, String filename)172 public static Uri addImage(ContentResolver cr, String title, 173 long dateTaken, Location location, 174 int orientation, String directory, String filename) { 175 176 ContentValues values = new ContentValues(7); 177 values.put(Images.Media.TITLE, title); 178 179 // That filename is what will be handed to Gmail when a user shares a 180 // photo. Gmail gets the name of the picture attachment from the 181 // "DISPLAY_NAME" field. 182 values.put(Images.Media.DISPLAY_NAME, filename); 183 values.put(Images.Media.DATE_TAKEN, dateTaken); 184 values.put(Images.Media.MIME_TYPE, "image/jpeg"); 185 values.put(Images.Media.ORIENTATION, orientation); 186 187 if (location != null) { 188 values.put(Images.Media.LATITUDE, location.getLatitude()); 189 values.put(Images.Media.LONGITUDE, location.getLongitude()); 190 } 191 192 if (directory != null && filename != null) { 193 String value = directory + "/" + filename; 194 values.put(Images.Media.DATA, value); 195 } 196 197 return cr.insert(STORAGE_URI, values); 198 } 199 200 private static class AddImageCancelable extends BaseCancelable<Void> { 201 private final Uri mUri; 202 private final ContentResolver mCr; 203 private final int mOrientation; 204 private final Bitmap mSource; 205 private final byte [] mJpegData; 206 AddImageCancelable(Uri uri, ContentResolver cr, int orientation, Bitmap source, byte[] jpegData)207 public AddImageCancelable(Uri uri, ContentResolver cr, 208 int orientation, Bitmap source, byte[] jpegData) { 209 if (source == null && jpegData == null || uri == null) { 210 throw new IllegalArgumentException("source cannot be null"); 211 } 212 mUri = uri; 213 mCr = cr; 214 mOrientation = orientation; 215 mSource = source; 216 mJpegData = jpegData; 217 } 218 219 @Override execute()220 protected Void execute() throws InterruptedException, 221 ExecutionException { 222 boolean complete = false; 223 try { 224 long id = ContentUris.parseId(mUri); 225 BaseImageList il = new ImageList( 226 STORAGE_URI, THUMB_URI, SORT_ASCENDING, null); 227 il.open(mCr); 228 229 // TODO: Redesign the process of adding new images. We should 230 // create an <code>IImage</code> in "ImageManager.addImage" 231 // and pass the image object to here. 232 Image image = new Image(il, mCr, id, 0, il.contentUri(id), null, 233 0, null, 0, null, null, 0); 234 String[] projection = new String[] { 235 ImageColumns._ID, 236 ImageColumns.MINI_THUMB_MAGIC, ImageColumns.DATA}; 237 Cursor c = mCr.query(mUri, projection, null, null, null); 238 String filepath; 239 try { 240 c.moveToPosition(0); 241 filepath = c.getString(2); 242 } finally { 243 c.close(); 244 } 245 runSubTask(image.saveImageContents( 246 mSource, mJpegData, mOrientation, true, filepath)); 247 248 ContentValues values = new ContentValues(); 249 values.put(ImageColumns.MINI_THUMB_MAGIC, 0); 250 mCr.update(mUri, values, null, null); 251 complete = true; 252 return null; 253 } finally { 254 if (!complete) { 255 try { 256 mCr.delete(mUri, null, null); 257 } catch (Throwable t) { 258 // ignore it while clean up. 259 } 260 } 261 } 262 } 263 } 264 storeImage( Uri uri, ContentResolver cr, int orientation, Bitmap source, byte [] jpegData)265 public static Cancelable<Void> storeImage( 266 Uri uri, ContentResolver cr, int orientation, 267 Bitmap source, byte [] jpegData) { 268 return new AddImageCancelable( 269 uri, cr, orientation, source, jpegData); 270 } 271 makeImageList(Uri uri, ContentResolver cr, int sort)272 public static IImageList makeImageList(Uri uri, ContentResolver cr, 273 int sort) { 274 String uriString = (uri != null) ? uri.toString() : ""; 275 276 // TODO: we need to figure out whether we're viewing 277 // DRM images in a better way. Is there a constant 278 // for content://drm somewhere?? 279 IImageList imageList; 280 281 if (uriString.startsWith("content://drm")) { 282 imageList = ImageManager.allImages( 283 cr, ImageManager.DataLocation.ALL, 284 ImageManager.INCLUDE_DRM_IMAGES, sort); 285 } else if (uriString.startsWith("content://media/external/video")) { 286 imageList = ImageManager.allImages( 287 cr, ImageManager.DataLocation.EXTERNAL, 288 ImageManager.INCLUDE_VIDEOS, sort); 289 } else if (isSingleImageMode(uriString)) { 290 imageList = new SingleImageList(uri); 291 ((SingleImageList) imageList).open(cr); 292 } else { 293 String bucketId = uri.getQueryParameter("bucketId"); 294 imageList = ImageManager.allImages( 295 cr, ImageManager.DataLocation.ALL, 296 ImageManager.INCLUDE_IMAGES, sort, bucketId); 297 } 298 return imageList; 299 } 300 isSingleImageMode(String uriString)301 static boolean isSingleImageMode(String uriString) { 302 return !uriString.startsWith( 303 MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString()) 304 && !uriString.startsWith( 305 MediaStore.Images.Media.INTERNAL_CONTENT_URI.toString()); 306 } 307 308 private static class EmptyImageList implements IImageList { 309 public static final Creator<EmptyImageList> CREATOR = 310 new Creator<EmptyImageList>() { 311 public EmptyImageList createFromParcel(Parcel in) { 312 return new EmptyImageList(); 313 } 314 315 public EmptyImageList[] newArray(int size) { 316 return new EmptyImageList[size]; 317 } 318 }; 319 open(ContentResolver resolver)320 public void open(ContentResolver resolver) { 321 } 322 close()323 public void close() { 324 } 325 checkThumbnail(int index)326 public void checkThumbnail(int index) { 327 } 328 deactivate()329 public void deactivate() { 330 } 331 getBucketIds()332 public HashMap<String, String> getBucketIds() { 333 return new HashMap<String, String>(); 334 } 335 getCount()336 public int getCount() { 337 return 0; 338 } 339 isEmpty()340 public boolean isEmpty() { 341 return true; 342 } 343 getImageAt(int i)344 public IImage getImageAt(int i) { 345 return null; 346 } 347 getImageForUri(Uri uri)348 public IImage getImageForUri(Uri uri) { 349 return null; 350 } 351 removeImage(IImage image)352 public boolean removeImage(IImage image) { 353 return false; 354 } 355 removeImageAt(int i)356 public boolean removeImageAt(int i) { 357 return false; 358 } 359 getImageIndex(IImage image)360 public int getImageIndex(IImage image) { 361 throw new UnsupportedOperationException(); 362 } 363 describeContents()364 public int describeContents() { 365 return 0; 366 } 367 writeToParcel(Parcel dest, int flags)368 public void writeToParcel(Parcel dest, int flags) { 369 } 370 } 371 emptyImageList()372 public static IImageList emptyImageList() { 373 return new EmptyImageList(); 374 } 375 allImages(ContentResolver cr, DataLocation location, int inclusion, int sort)376 public static IImageList allImages(ContentResolver cr, 377 DataLocation location, int inclusion, int sort) { 378 return allImages(cr, location, inclusion, sort, null); 379 } 380 allImages(ContentResolver cr, DataLocation location, int inclusion, int sort, String bucketId)381 public static IImageList allImages(ContentResolver cr, 382 DataLocation location, int inclusion, int sort, String bucketId) { 383 if (cr == null) { 384 return null; 385 } 386 387 // false ==> don't require write access 388 boolean haveSdCard = hasStorage(false); 389 390 // use this code to merge videos and stills into the same list 391 ArrayList<BaseImageList> l = new ArrayList<BaseImageList>(); 392 393 if (haveSdCard && location != DataLocation.INTERNAL) { 394 if ((inclusion & INCLUDE_IMAGES) != 0) { 395 l.add(new ImageList( 396 STORAGE_URI, THUMB_URI, sort, bucketId)); 397 } 398 if ((inclusion & INCLUDE_VIDEOS) != 0) { 399 l.add(new VideoList(VIDEO_STORAGE_URI, sort, bucketId)); 400 } 401 } 402 if (location == DataLocation.INTERNAL || location == DataLocation.ALL) { 403 if ((inclusion & INCLUDE_IMAGES) != 0) { 404 l.add(new ImageList( 405 Images.Media.INTERNAL_CONTENT_URI, 406 Images.Thumbnails.INTERNAL_CONTENT_URI, 407 sort, bucketId)); 408 } 409 if ((inclusion & INCLUDE_DRM_IMAGES) != 0) { 410 l.add(new DrmImageList( 411 DrmStore.Images.CONTENT_URI, sort, bucketId)); 412 } 413 } 414 415 // Optimization: If some of the lists are empty, remove them. 416 // If there is only one remaining list, return it directly. 417 Iterator<BaseImageList> iter = l.iterator(); 418 while (iter.hasNext()) { 419 BaseImageList sublist = iter.next(); 420 sublist.open(cr); 421 if (sublist.isEmpty()) iter.remove(); 422 sublist.close(); 423 } 424 425 if (l.size() == 1) { 426 BaseImageList list = l.get(0); 427 list.open(cr); 428 return list; 429 } 430 431 ImageListUber uber = new ImageListUber( 432 l.toArray(new IImageList[l.size()]), sort); 433 uber.open(cr); 434 return uber; 435 } 436 checkFsWritable()437 private static boolean checkFsWritable() { 438 // Create a temporary file to see whether a volume is really writeable. 439 // It's important not to put it in the root directory which may have a 440 // limit on the number of files. 441 String directoryName = 442 Environment.getExternalStorageDirectory().toString() + "/DCIM"; 443 File directory = new File(directoryName); 444 if (!directory.isDirectory()) { 445 if (!directory.mkdirs()) { 446 return false; 447 } 448 } 449 File f = new File(directoryName, ".probe"); 450 try { 451 // Remove stale file if any 452 if (f.exists()) { 453 f.delete(); 454 } 455 if (!f.createNewFile()) { 456 return false; 457 } 458 f.delete(); 459 return true; 460 } catch (IOException ex) { 461 return false; 462 } 463 } 464 hasStorage()465 public static boolean hasStorage() { 466 return hasStorage(true); 467 } 468 hasStorage(boolean requireWriteAccess)469 public static boolean hasStorage(boolean requireWriteAccess) { 470 String state = Environment.getExternalStorageState(); 471 472 if (Environment.MEDIA_MOUNTED.equals(state)) { 473 if (requireWriteAccess) { 474 boolean writable = checkFsWritable(); 475 return writable; 476 } else { 477 return true; 478 } 479 } else if (!requireWriteAccess 480 && Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { 481 return true; 482 } 483 return false; 484 } 485 query(ContentResolver resolver, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)486 private static Cursor query(ContentResolver resolver, Uri uri, 487 String[] projection, String selection, String[] selectionArgs, 488 String sortOrder) { 489 try { 490 if (resolver == null) { 491 return null; 492 } 493 return resolver.query( 494 uri, projection, selection, selectionArgs, sortOrder); 495 } catch (UnsupportedOperationException ex) { 496 return null; 497 } 498 499 } 500 isMediaScannerScanning(ContentResolver cr)501 public static boolean isMediaScannerScanning(ContentResolver cr) { 502 boolean result = false; 503 Cursor cursor = query(cr, MediaStore.getMediaScannerUri(), 504 new String [] {MediaStore.MEDIA_SCANNER_VOLUME}, 505 null, null, null); 506 if (cursor != null) { 507 if (cursor.getCount() == 1) { 508 cursor.moveToFirst(); 509 result = "external".equals(cursor.getString(0)); 510 } 511 cursor.close(); 512 } 513 514 return result; 515 } 516 getLastImageThumbPath()517 public static String getLastImageThumbPath() { 518 return Environment.getExternalStorageDirectory().toString() + 519 "/DCIM/.thumbnails/image_last_thumb"; 520 } 521 getLastVideoThumbPath()522 public static String getLastVideoThumbPath() { 523 return Environment.getExternalStorageDirectory().toString() + 524 "/DCIM/.thumbnails/video_last_thumb"; 525 } 526 } 527