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