Storage.java revision 4361352633d1e106c1574c02ddd27c8891c5ee78
1/* 2 * Copyright (C) 2010 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 android.content.ContentResolver; 20import android.content.ContentValues; 21import android.graphics.Bitmap; 22import android.graphics.Point; 23import android.location.Location; 24import android.net.Uri; 25import android.os.Environment; 26import android.os.StatFs; 27import android.provider.MediaStore.Images; 28import android.provider.MediaStore.Images.ImageColumns; 29import android.provider.MediaStore.MediaColumns; 30import android.util.LruCache; 31import com.android.camera.data.FilmstripItemData; 32import com.android.camera.debug.Log; 33import com.android.camera.exif.ExifInterface; 34import com.android.camera.util.ApiHelper; 35import com.android.camera.util.Size; 36import com.google.common.base.Optional; 37 38import javax.annotation.Nonnull; 39import java.io.File; 40import java.io.FileOutputStream; 41import java.io.IOException; 42import java.util.HashMap; 43import java.util.UUID; 44import java.util.concurrent.TimeUnit; 45 46public class Storage { 47 public static final String DCIM = 48 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString(); 49 public static final String DIRECTORY = DCIM + "/Camera"; 50 public static final File DIRECTORY_FILE = new File(DIRECTORY); 51 public static final String JPEG_POSTFIX = ".jpg"; 52 public static final String GIF_POSTFIX = ".gif"; 53 // Match the code in MediaProvider.computeBucketValues(). 54 public static final String BUCKET_ID = 55 String.valueOf(DIRECTORY.toLowerCase().hashCode()); 56 public static final long UNAVAILABLE = -1L; 57 public static final long PREPARING = -2L; 58 public static final long UNKNOWN_SIZE = -3L; 59 public static final long ACCESS_FAILURE = -4L; 60 public static final long LOW_STORAGE_THRESHOLD_BYTES = 50000000; 61 public static final String CAMERA_SESSION_SCHEME = "camera_session"; 62 private static final Log.Tag TAG = new Log.Tag("Storage"); 63 private static final String GOOGLE_COM = "google.com"; 64 private static HashMap<Uri, Uri> sSessionsToContentUris = new HashMap<>(); 65 private static HashMap<Uri, Uri> sContentUrisToSessions = new HashMap<>(); 66 private static LruCache<Uri, Bitmap> sSessionsToPlaceholderBitmap = 67 // 20MB cache as an upper bound for session bitmap storage 68 new LruCache<Uri, Bitmap>(20 * 1024 * 1024) { 69 @Override 70 protected int sizeOf(Uri key, Bitmap value) { 71 return value.getByteCount(); 72 } 73 }; 74 private static HashMap<Uri, Point> sSessionsToSizes = new HashMap<>(); 75 private static HashMap<Uri, Integer> sSessionsToPlaceholderVersions = new HashMap<>(); 76 77 /** 78 * Save the image with default JPEG MIME type and add it to the MediaStore. 79 * 80 * @param resolver The The content resolver to use. 81 * @param title The title of the media file. 82 * @param date The date for the media file. 83 * @param location The location of the media file. 84 * @param orientation The orientation of the media file. 85 * @param exif The EXIF info. Can be {@code null}. 86 * @param jpeg The JPEG data. 87 * @param width The width of the media file after the orientation is 88 * applied. 89 * @param height The height of the media file after the orientation is 90 * applied. 91 */ 92 public static Uri addImage(ContentResolver resolver, String title, long date, 93 Location location, int orientation, ExifInterface exif, byte[] jpeg, int width, 94 int height) throws IOException { 95 96 return addImage(resolver, title, date, location, orientation, exif, jpeg, width, height, 97 FilmstripItemData.MIME_TYPE_JPEG); 98 } 99 100 /** 101 * Saves the media with a given MIME type and adds it to the MediaStore. 102 * <p> 103 * The path will be automatically generated according to the title. 104 * </p> 105 * 106 * @param resolver The The content resolver to use. 107 * @param title The title of the media file. 108 * @param data The data to save. 109 * @param date The date for the media file. 110 * @param location The location of the media file. 111 * @param orientation The orientation of the media file. 112 * @param exif The EXIF info. Can be {@code null}. 113 * @param width The width of the media file after the orientation is 114 * applied. 115 * @param height The height of the media file after the orientation is 116 * applied. 117 * @param mimeType The MIME type of the data. 118 * @return The URI of the added image, or null if the image could not be 119 * added. 120 */ 121 public static Uri addImage(ContentResolver resolver, String title, long date, 122 Location location, int orientation, ExifInterface exif, byte[] data, int width, 123 int height, String mimeType) throws IOException { 124 125 String path = generateFilepath(title, mimeType); 126 long fileLength = writeFile(path, data, exif); 127 if (fileLength >= 0) { 128 return addImageToMediaStore(resolver, title, date, location, orientation, fileLength, 129 path, width, height, mimeType); 130 } 131 return null; 132 } 133 134 /** 135 * Add the entry for the media file to media store. 136 * 137 * @param resolver The The content resolver to use. 138 * @param title The title of the media file. 139 * @param date The date for the media file. 140 * @param location The location of the media file. 141 * @param orientation The orientation of the media file. 142 * @param width The width of the media file after the orientation is 143 * applied. 144 * @param height The height of the media file after the orientation is 145 * applied. 146 * @param mimeType The MIME type of the data. 147 * @return The content URI of the inserted media file or null, if the image 148 * could not be added. 149 */ 150 public static Uri addImageToMediaStore(ContentResolver resolver, String title, long date, 151 Location location, int orientation, long jpegLength, String path, int width, int height, 152 String mimeType) { 153 // Insert into MediaStore. 154 ContentValues values = 155 getContentValuesForData(title, date, location, orientation, jpegLength, path, width, 156 height, mimeType); 157 158 Uri uri = null; 159 try { 160 uri = resolver.insert(Images.Media.EXTERNAL_CONTENT_URI, values); 161 } catch (Throwable th) { 162 // This can happen when the external volume is already mounted, but 163 // MediaScanner has not notify MediaProvider to add that volume. 164 // The picture is still safe and MediaScanner will find it and 165 // insert it into MediaProvider. The only problem is that the user 166 // cannot click the thumbnail to review the picture. 167 Log.e(TAG, "Failed to write MediaStore" + th); 168 } 169 return uri; 170 } 171 172 // Get a ContentValues object for the given photo data 173 public static ContentValues getContentValuesForData(String title, 174 long date, Location location, int orientation, long jpegLength, 175 String path, int width, int height, String mimeType) { 176 177 File file = new File(path); 178 long dateModifiedSeconds = TimeUnit.MILLISECONDS.toSeconds(file.lastModified()); 179 180 ContentValues values = new ContentValues(11); 181 values.put(ImageColumns.TITLE, title); 182 values.put(ImageColumns.DISPLAY_NAME, title + JPEG_POSTFIX); 183 values.put(ImageColumns.DATE_TAKEN, date); 184 values.put(ImageColumns.MIME_TYPE, mimeType); 185 values.put(ImageColumns.DATE_MODIFIED, dateModifiedSeconds); 186 // Clockwise rotation in degrees. 0, 90, 180, or 270. 187 values.put(ImageColumns.ORIENTATION, orientation); 188 values.put(ImageColumns.DATA, path); 189 values.put(ImageColumns.SIZE, jpegLength); 190 191 setImageSize(values, width, height); 192 193 if (location != null) { 194 values.put(ImageColumns.LATITUDE, location.getLatitude()); 195 values.put(ImageColumns.LONGITUDE, location.getLongitude()); 196 } 197 return values; 198 } 199 200 /** 201 * Add a placeholder for a new image that does not exist yet. 202 * 203 * @param placeholder the placeholder image 204 * @return A new URI used to reference this placeholder 205 */ 206 public static Uri addPlaceholder(Bitmap placeholder) { 207 Uri uri = generateUniquePlaceholderUri(); 208 replacePlaceholder(uri, placeholder); 209 return uri; 210 } 211 212 /** 213 * Remove a placeholder from in memory storage. 214 */ 215 public static void removePlaceholder(Uri uri) { 216 sSessionsToSizes.remove(uri); 217 sSessionsToPlaceholderBitmap.remove(uri); 218 sSessionsToPlaceholderVersions.remove(uri); 219 } 220 221 /** 222 * Add or replace placeholder for a new image that does not exist yet. 223 * 224 * @param uri the uri of the placeholder to replace, or null if this is a 225 * new one 226 * @param placeholder the placeholder image 227 * @return A URI used to reference this placeholder 228 */ 229 public static void replacePlaceholder(Uri uri, Bitmap placeholder) { 230 Log.v(TAG, "session bitmap cache size: " + sSessionsToPlaceholderBitmap.size()); 231 Point size = new Point(placeholder.getWidth(), placeholder.getHeight()); 232 sSessionsToSizes.put(uri, size); 233 sSessionsToPlaceholderBitmap.put(uri, placeholder); 234 Integer currentVersion = sSessionsToPlaceholderVersions.get(uri); 235 sSessionsToPlaceholderVersions.put(uri, currentVersion == null ? 0 : currentVersion + 1); 236 } 237 238 /** 239 * Creates an empty placeholder. 240 * 241 * @param size the size of the placeholder in pixels. 242 * @return A new URI used to reference this placeholder 243 */ 244 @Nonnull 245 public static Uri addEmptyPlaceholder(@Nonnull Size size) { 246 Uri uri = generateUniquePlaceholderUri(); 247 sSessionsToSizes.put(uri, new Point(size.getWidth(), size.getHeight())); 248 sSessionsToPlaceholderBitmap.remove(uri); 249 Integer currentVersion = sSessionsToPlaceholderVersions.get(uri); 250 sSessionsToPlaceholderVersions.put(uri, currentVersion == null ? 0 : currentVersion + 1); 251 return uri; 252 } 253 254 /** 255 * Take jpeg bytes and add them to the media store, either replacing an existing item 256 * or a placeholder uri to replace 257 * @param imageUri The content uri or session uri of the image being updated 258 * @param resolver The content resolver to use 259 * @param title of the image 260 * @param date of the image 261 * @param location of the image 262 * @param orientation of the image 263 * @param exif of the image 264 * @param jpeg bytes of the image 265 * @param width of the image 266 * @param height of the image 267 * @param mimeType of the image 268 * @return The content uri of the newly inserted or replaced item. 269 */ 270 public static Uri updateImage(Uri imageUri, ContentResolver resolver, String title, long date, 271 Location location, int orientation, ExifInterface exif, 272 byte[] jpeg, int width, int height, String mimeType) throws IOException { 273 String path = generateFilepath(title, mimeType); 274 writeFile(path, jpeg, exif); 275 return updateImage(imageUri, resolver, title, date, location, orientation, jpeg.length, path, 276 width, height, mimeType); 277 } 278 279 private static Uri generateUniquePlaceholderUri() { 280 Uri.Builder builder = new Uri.Builder(); 281 String uuid = UUID.randomUUID().toString(); 282 builder.scheme(CAMERA_SESSION_SCHEME).authority(GOOGLE_COM).appendPath(uuid); 283 return builder.build(); 284 } 285 286 private static void setImageSize(ContentValues values, int width, int height) { 287 // The two fields are available since ICS but got published in JB 288 if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) { 289 values.put(MediaColumns.WIDTH, width); 290 values.put(MediaColumns.HEIGHT, height); 291 } 292 } 293 294 /** 295 * Writes the JPEG data to a file. If there's EXIF info, the EXIF header 296 * will be added. 297 * 298 * @param path The path to the target file. 299 * @param jpeg The JPEG data. 300 * @param exif The EXIF info. Can be {@code null}. 301 * 302 * @return The size of the file. -1 if failed. 303 */ 304 public static long writeFile(String path, byte[] jpeg, ExifInterface exif) throws IOException { 305 if (!createDirectoryIfNeeded(path)) { 306 Log.e(TAG, "Failed to create parent directory for file: " + path); 307 return -1; 308 } 309 if (exif != null) { 310 exif.writeExif(jpeg, path); 311 File f = new File(path); 312 return f.length(); 313 } else { 314 return writeFile(path, jpeg); 315 } 316// return -1; 317 } 318 319 /** 320 * Renames a file. 321 * 322 * <p/> 323 * Can only be used for regular files, not directories. 324 * 325 * @param inputPath the original path of the file 326 * @param newFilePath the new path of the file 327 * @return false if rename was not successful 328 */ 329 public static boolean renameFile(File inputPath, File newFilePath) { 330 if (newFilePath.exists()) { 331 Log.e(TAG, "File path already exists: " + newFilePath.getAbsolutePath()); 332 return false; 333 } 334 if (inputPath.isDirectory()) { 335 Log.e(TAG, "Input path is directory: " + inputPath.getAbsolutePath()); 336 return false; 337 } 338 if (!createDirectoryIfNeeded(newFilePath.getAbsolutePath())) { 339 Log.e(TAG, "Failed to create parent directory for file: " + 340 newFilePath.getAbsolutePath()); 341 return false; 342 } 343 return inputPath.renameTo(newFilePath); 344 } 345 346 /** 347 * Writes the data to a file. 348 * 349 * @param path The path to the target file. 350 * @param data The data to save. 351 * 352 * @return The size of the file. -1 if failed. 353 */ 354 private static long writeFile(String path, byte[] data) { 355 FileOutputStream out = null; 356 try { 357 out = new FileOutputStream(path); 358 out.write(data); 359 return data.length; 360 } catch (Exception e) { 361 Log.e(TAG, "Failed to write data", e); 362 } finally { 363 try { 364 out.close(); 365 } catch (Exception e) { 366 Log.e(TAG, "Failed to close file after write", e); 367 } 368 } 369 return -1; 370 } 371 372 /** 373 * Given a file path, makes sure the directory it's in exists, and if not 374 * that it is created. 375 * 376 * @param filePath the absolute path of a file, e.g. '/foo/bar/file.jpg'. 377 * @return Whether the directory exists. If 'false' is returned, this file 378 * cannot be written to since the parent directory could not be 379 * created. 380 */ 381 private static boolean createDirectoryIfNeeded(String filePath) { 382 File parentFile = new File(filePath).getParentFile(); 383 384 // If the parent exists, return 'true' if it is a directory. If it's a 385 // file, return 'false'. 386 if (parentFile.exists()) { 387 return parentFile.isDirectory(); 388 } 389 390 // If the parent does not exists, attempt to create it and return 391 // whether creating it succeeded. 392 return parentFile.mkdirs(); 393 } 394 395 /** Updates the image values in MediaStore. */ 396 private static Uri updateImage(Uri imageUri, ContentResolver resolver, String title, 397 long date, Location location, int orientation, int jpegLength, 398 String path, int width, int height, String mimeType) { 399 400 ContentValues values = 401 getContentValuesForData(title, date, location, orientation, jpegLength, path, 402 width, height, mimeType); 403 404 405 Uri resultUri = imageUri; 406 if (Storage.isSessionUri(imageUri)) { 407 // If this is a session uri, then we need to add the image 408 resultUri = addImageToMediaStore(resolver, title, date, location, orientation, 409 jpegLength, path, width, height, mimeType); 410 sSessionsToContentUris.put(imageUri, resultUri); 411 sContentUrisToSessions.put(resultUri, imageUri); 412 } else { 413 // Update the MediaStore 414 resolver.update(imageUri, values, null, null); 415 } 416 return resultUri; 417 } 418 419 private static String generateFilepath(String title, String mimeType) { 420 return generateFilepath(DIRECTORY, title, mimeType); 421 } 422 423 public static String generateFilepath(String directory, String title, String mimeType) { 424 String extension = null; 425 if (FilmstripItemData.MIME_TYPE_JPEG.equals(mimeType)) { 426 extension = JPEG_POSTFIX; 427 } else if (FilmstripItemData.MIME_TYPE_GIF.equals(mimeType)) { 428 extension = GIF_POSTFIX; 429 } else { 430 throw new IllegalArgumentException("Invalid mimeType: " + mimeType); 431 } 432 return (new File(directory, title + extension)).getAbsolutePath(); 433 } 434 435 /** 436 * Returns the jpeg bytes for a placeholder session 437 * 438 * @param uri the session uri to look up 439 * @return The bitmap or null 440 */ 441 public static Optional<Bitmap> getPlaceholderForSession(Uri uri) { 442 return Optional.fromNullable(sSessionsToPlaceholderBitmap.get(uri)); 443 } 444 445 /** 446 * Returns the current version of a placeholder for a session. The version will increment 447 * with each call to replacePlaceholder. 448 * 449 * @param uri the session uri to look up. 450 * @return the current version int. 451 */ 452 public static int getPlaceholderVersionForSession(Uri uri) { 453 return sSessionsToPlaceholderVersions.get(uri); 454 } 455 456 /** 457 * Returns the dimensions of the placeholder image 458 * 459 * @param uri the session uri to look up 460 * @return The size 461 */ 462 public static Point getSizeForSession(Uri uri) { 463 return sSessionsToSizes.get(uri); 464 } 465 466 /** 467 * Takes a session URI and returns the finished image's content URI 468 * 469 * @param uri the uri of the session that was replaced 470 * @return The uri of the new media item, if it exists, or null. 471 */ 472 public static Uri getContentUriForSessionUri(Uri uri) { 473 return sSessionsToContentUris.get(uri); 474 } 475 476 /** 477 * Takes a content URI and returns the original Session Uri if any 478 * 479 * @param contentUri the uri of the media store content 480 * @return The session uri of the original session, if it exists, or null. 481 */ 482 public static Uri getSessionUriFromContentUri(Uri contentUri) { 483 return sContentUrisToSessions.get(contentUri); 484 } 485 486 /** 487 * Determines if a URI points to a camera session 488 * 489 * @param uri the uri to check 490 * @return true if it is a session uri. 491 */ 492 public static boolean isSessionUri(Uri uri) { 493 return uri.getScheme().equals(CAMERA_SESSION_SCHEME); 494 } 495 496 public static long getAvailableSpace() { 497 String state = Environment.getExternalStorageState(); 498 Log.d(TAG, "External storage state=" + state); 499 if (Environment.MEDIA_CHECKING.equals(state)) { 500 return PREPARING; 501 } 502 if (!Environment.MEDIA_MOUNTED.equals(state)) { 503 return UNAVAILABLE; 504 } 505 506 File dir = new File(DIRECTORY); 507 dir.mkdirs(); 508 if (!dir.isDirectory() || !dir.canWrite()) { 509 return UNAVAILABLE; 510 } 511 512 try { 513 StatFs stat = new StatFs(DIRECTORY); 514 return stat.getAvailableBlocks() * (long) stat.getBlockSize(); 515 } catch (Exception e) { 516 Log.i(TAG, "Fail to access external storage", e); 517 } 518 return UNKNOWN_SIZE; 519 } 520 521 /** 522 * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be 523 * imported. This is a temporary fix for bug#1655552. 524 */ 525 public static void ensureOSXCompatible() { 526 File nnnAAAAA = new File(DCIM, "100ANDRO"); 527 if (!(nnnAAAAA.exists() || nnnAAAAA.mkdirs())) { 528 Log.e(TAG, "Failed to create " + nnnAAAAA.getPath()); 529 } 530 } 531 532} 533