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