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