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.annotation.TargetApi; 20 import android.content.ContentResolver; 21 import android.content.ContentValues; 22 import android.graphics.Point; 23 import android.location.Location; 24 import android.net.Uri; 25 import android.os.Build; 26 import android.os.Environment; 27 import android.os.StatFs; 28 import android.provider.MediaStore.Images; 29 import android.provider.MediaStore.Images.ImageColumns; 30 import android.provider.MediaStore.MediaColumns; 31 32 import com.android.camera.data.LocalData; 33 import com.android.camera.debug.Log; 34 import com.android.camera.exif.ExifInterface; 35 import com.android.camera.util.ApiHelper; 36 37 import java.io.File; 38 import java.io.FileOutputStream; 39 import java.util.HashMap; 40 import java.util.UUID; 41 import java.util.concurrent.TimeUnit; 42 43 public 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 // 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 fo 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 */ addImage(ContentResolver resolver, String title, long date, Location location, int orientation, ExifInterface exif, byte[] jpeg, int width, int height)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 fo 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 */ addImage(ContentResolver resolver, String title, long date, Location location, int orientation, ExifInterface exif, byte[] data, int width, int height, String mimeType)109 private 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); 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 fo 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 */ addImageToMediaStore(ContentResolver resolver, String title, long date, Location location, int orientation, long jpegLength, String path, int width, int height, String mimeType)138 private 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 getContentValuesForData(String title, long date, Location location, int orientation, long jpegLength, String path, int width, int height, String mimeType)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 */ addPlaceholder(byte[] jpeg, int width, int height)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 */ replacePlaceholder(Uri uri, byte[] jpeg, int width, int height)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 */ updateImage(Uri imageUri, ContentResolver resolver, String title, long date, Location location, int orientation, ExifInterface exif, byte[] jpeg, int width, int height, String mimeType)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); 242 writeFile(path, jpeg, exif); 243 return updateImage(imageUri, resolver, title, date, location, orientation, jpeg.length, path, 244 width, height, mimeType); 245 } 246 247 @TargetApi(Build.VERSION_CODES.JELLY_BEAN) setImageSize(ContentValues values, int width, int height)248 private static void setImageSize(ContentValues values, int width, int height) { 249 // The two fields are available since ICS but got published in JB 250 if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) { 251 values.put(MediaColumns.WIDTH, width); 252 values.put(MediaColumns.HEIGHT, height); 253 } 254 } 255 256 /** 257 * Writes the JPEG data to a file. If there's EXIF info, the EXIF header 258 * will be added. 259 * 260 * @param path The path to the target file. 261 * @param jpeg The JPEG data. 262 * @param exif The EXIF info. Can be {@code null}. 263 * 264 * @return The size of the file. -1 if failed. 265 */ writeFile(String path, byte[] jpeg, ExifInterface exif)266 private static long writeFile(String path, byte[] jpeg, ExifInterface exif) { 267 if (exif != null) { 268 try { 269 exif.writeExif(jpeg, path); 270 File f = new File(path); 271 return f.length(); 272 } catch (Exception e) { 273 Log.e(TAG, "Failed to write data", e); 274 } 275 } else { 276 return writeFile(path, jpeg); 277 } 278 return -1; 279 } 280 281 /** 282 * Writes the data to a file. 283 * 284 * @param path The path to the target file. 285 * @param data The data to save. 286 * 287 * @return The size of the file. -1 if failed. 288 */ writeFile(String path, byte[] data)289 private static long writeFile(String path, byte[] data) { 290 FileOutputStream out = null; 291 try { 292 out = new FileOutputStream(path); 293 out.write(data); 294 return data.length; 295 } catch (Exception e) { 296 Log.e(TAG, "Failed to write data", e); 297 } finally { 298 try { 299 out.close(); 300 } catch (Exception e) { 301 Log.e(TAG, "Failed to close file after write", e); 302 } 303 } 304 return -1; 305 } 306 307 // Updates the image values in MediaStore updateImage(Uri imageUri, ContentResolver resolver, String title, long date, Location location, int orientation, int jpegLength, String path, int width, int height, String mimeType)308 private static Uri updateImage(Uri imageUri, ContentResolver resolver, String title, 309 long date, Location location, int orientation, int jpegLength, 310 String path, int width, int height, String mimeType) { 311 312 ContentValues values = 313 getContentValuesForData(title, date, location, orientation, jpegLength, path, 314 width, height, mimeType); 315 316 317 Uri resultUri = imageUri; 318 if (Storage.isSessionUri(imageUri)) { 319 // If this is a session uri, then we need to add the image 320 resultUri = addImageToMediaStore(resolver, title, date, location, orientation, 321 jpegLength, path, width, height, mimeType); 322 sSessionsToContentUris.put(imageUri, resultUri); 323 sContentUrisToSessions.put(resultUri, imageUri); 324 } else { 325 // Update the MediaStore 326 resolver.update(imageUri, values, null, null); 327 } 328 return resultUri; 329 } 330 generateFilepath(String title)331 private static String generateFilepath(String title) { 332 return DIRECTORY + '/' + title + ".jpg"; 333 } 334 335 /** 336 * Returns the jpeg bytes for a placeholder session 337 * 338 * @param uri the session uri to look up 339 * @return The jpeg bytes or null 340 */ getJpegForSession(Uri uri)341 public static byte[] getJpegForSession(Uri uri) { 342 return sSessionsToPlaceholderBytes.get(uri); 343 } 344 345 /** 346 * Returns the current version of a placeholder for a session. The version will increment 347 * with each call to replacePlaceholder. 348 * 349 * @param uri the session uri to look up. 350 * @return the current version int. 351 */ getJpegVersionForSession(Uri uri)352 public static int getJpegVersionForSession(Uri uri) { 353 return sSessionsToPlaceholderVersions.get(uri); 354 } 355 356 /** 357 * Returns the dimensions of the placeholder image 358 * 359 * @param uri the session uri to look up 360 * @return The size 361 */ getSizeForSession(Uri uri)362 public static Point getSizeForSession(Uri uri) { 363 return sSessionsToSizes.get(uri); 364 } 365 366 /** 367 * Takes a session URI and returns the finished image's content URI 368 * 369 * @param uri the uri of the session that was replaced 370 * @return The uri of the new media item, if it exists, or null. 371 */ getContentUriForSessionUri(Uri uri)372 public static Uri getContentUriForSessionUri(Uri uri) { 373 return sSessionsToContentUris.get(uri); 374 } 375 376 /** 377 * Takes a content URI and returns the original Session Uri if any 378 * 379 * @param contentUri the uri of the media store content 380 * @return The session uri of the original session, if it exists, or null. 381 */ getSessionUriFromContentUri(Uri contentUri)382 public static Uri getSessionUriFromContentUri(Uri contentUri) { 383 return sContentUrisToSessions.get(contentUri); 384 } 385 386 /** 387 * Determines if a URI points to a camera session 388 * 389 * @param uri the uri to check 390 * @return true if it is a session uri. 391 */ isSessionUri(Uri uri)392 public static boolean isSessionUri(Uri uri) { 393 return uri.getScheme().equals(CAMERA_SESSION_SCHEME); 394 } 395 getAvailableSpace()396 public static long getAvailableSpace() { 397 String state = Environment.getExternalStorageState(); 398 Log.d(TAG, "External storage state=" + state); 399 if (Environment.MEDIA_CHECKING.equals(state)) { 400 return PREPARING; 401 } 402 if (!Environment.MEDIA_MOUNTED.equals(state)) { 403 return UNAVAILABLE; 404 } 405 406 File dir = new File(DIRECTORY); 407 dir.mkdirs(); 408 if (!dir.isDirectory() || !dir.canWrite()) { 409 return UNAVAILABLE; 410 } 411 412 try { 413 StatFs stat = new StatFs(DIRECTORY); 414 return stat.getAvailableBlocks() * (long) stat.getBlockSize(); 415 } catch (Exception e) { 416 Log.i(TAG, "Fail to access external storage", e); 417 } 418 return UNKNOWN_SIZE; 419 } 420 421 /** 422 * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be 423 * imported. This is a temporary fix for bug#1655552. 424 */ ensureOSXCompatible()425 public static void ensureOSXCompatible() { 426 File nnnAAAAA = new File(DCIM, "100ANDRO"); 427 if (!(nnnAAAAA.exists() || nnnAAAAA.mkdirs())) { 428 Log.e(TAG, "Failed to create " + nnnAAAAA.getPath()); 429 } 430 } 431 432 } 433