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