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