Storage.java revision 837d8f2687cdf53e657c03c1d01b1c5df3902442
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; 36import com.android.camera.util.ImageLoader; 37 38import java.io.File; 39import java.io.FileOutputStream; 40import java.util.HashMap; 41import java.util.UUID; 42import java.util.concurrent.TimeUnit; 43 44public class Storage { 45 public static final String DCIM = 46 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString(); 47 public static final String DIRECTORY = DCIM + "/Camera"; 48 public static final String JPEG_POSTFIX = ".jpg"; 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 64 @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 65 private static void setImageSize(ContentValues values, int width, int height) { 66 // The two fields are available since ICS but got published in JB 67 if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) { 68 values.put(MediaColumns.WIDTH, width); 69 values.put(MediaColumns.HEIGHT, height); 70 } 71 } 72 73 public static void writeFile(String path, byte[] jpeg, ExifInterface exif) { 74 if (exif != null) { 75 try { 76 exif.writeExif(jpeg, path); 77 } catch (Exception e) { 78 Log.e(TAG, "Failed to write data", e); 79 } 80 } else { 81 writeFile(path, jpeg); 82 } 83 } 84 85 public static void writeFile(String path, byte[] data) { 86 FileOutputStream out = null; 87 try { 88 out = new FileOutputStream(path); 89 out.write(data); 90 } catch (Exception e) { 91 Log.e(TAG, "Failed to write data", e); 92 } finally { 93 try { 94 out.close(); 95 } catch (Exception e) { 96 Log.e(TAG, "Failed to close file after write", e); 97 } 98 } 99 } 100 101 // Save the image and add it to the MediaStore. 102 public static Uri addImage(ContentResolver resolver, String title, long date, 103 Location location, int orientation, ExifInterface exif, byte[] jpeg, int width, 104 int height) { 105 106 return addImage(resolver, title, date, location, orientation, exif, jpeg, width, height, 107 LocalData.MIME_TYPE_JPEG); 108 } 109 110 // Save the image with a given mimeType and add it the MediaStore. 111 public static Uri addImage(ContentResolver resolver, String title, long date, 112 Location location, int orientation, ExifInterface exif, byte[] jpeg, int width, 113 int height, String mimeType) { 114 115 String path = generateFilepath(title); 116 writeFile(path, jpeg, exif); 117 return addImage(resolver, title, date, location, orientation, 118 jpeg.length, path, width, height, mimeType); 119 } 120 121 // Get a ContentValues object for the given photo data 122 public static ContentValues getContentValuesForData(String title, 123 long date, Location location, int orientation, int jpegLength, 124 String path, int width, int height, String mimeType) { 125 126 File file = new File(path); 127 long dateModifiedSeconds = TimeUnit.MILLISECONDS.toSeconds(file.lastModified()); 128 129 ContentValues values = new ContentValues(11); 130 values.put(ImageColumns.TITLE, title); 131 values.put(ImageColumns.DISPLAY_NAME, title + JPEG_POSTFIX); 132 values.put(ImageColumns.DATE_TAKEN, date); 133 values.put(ImageColumns.MIME_TYPE, mimeType); 134 values.put(ImageColumns.DATE_MODIFIED, dateModifiedSeconds); 135 // Clockwise rotation in degrees. 0, 90, 180, or 270. 136 values.put(ImageColumns.ORIENTATION, orientation); 137 values.put(ImageColumns.DATA, path); 138 values.put(ImageColumns.SIZE, jpegLength); 139 140 setImageSize(values, width, height); 141 142 if (location != null) { 143 values.put(ImageColumns.LATITUDE, location.getLatitude()); 144 values.put(ImageColumns.LONGITUDE, location.getLongitude()); 145 } 146 return values; 147 } 148 149 /** 150 * Add a placeholder for a new image that does not exist yet. 151 * @param jpeg the bytes of the placeholder image 152 * @param width the image's width 153 * @param height the image's height 154 * @return A new URI used to reference this placeholder 155 */ 156 public static Uri addPlaceholder(byte[] jpeg, int width, int height) { 157 Uri uri; 158 Uri.Builder builder = new Uri.Builder(); 159 String uuid = UUID.randomUUID().toString(); 160 builder.scheme(CAMERA_SESSION_SCHEME).authority(GOOGLE_COM).appendPath(uuid); 161 uri = builder.build(); 162 163 replacePlaceholder(uri, jpeg, width, height); 164 return uri; 165 } 166 167 /** 168 * Add or replace placeholder for a new image that does not exist yet. 169 * @param uri the uri of the placeholder to replace, or null if this is a new one 170 * @param jpeg the bytes of the placeholder image 171 * @param width the image's width 172 * @param height the image's height 173 * @return A URI used to reference this placeholder 174 */ 175 public static void replacePlaceholder(Uri uri, byte[] jpeg, int width, int height) { 176 Point size = new Point(width, height); 177 sSessionsToSizes.put(uri, size); 178 sSessionsToPlaceholderBytes.put(uri, jpeg); 179 } 180 181 // Add the image to media store. 182 public static Uri addImage(ContentResolver resolver, String title, 183 long date, Location location, int orientation, int jpegLength, 184 String path, int width, int height, String mimeType) { 185 // Insert into MediaStore. 186 ContentValues values = 187 getContentValuesForData(title, date, location, orientation, jpegLength, path, 188 width, height, mimeType); 189 190 Uri uri = null; 191 try { 192 uri = resolver.insert(Images.Media.EXTERNAL_CONTENT_URI, values); 193 } catch (Throwable th) { 194 // This can happen when the external volume is already mounted, but 195 // MediaScanner has not notify MediaProvider to add that volume. 196 // The picture is still safe and MediaScanner will find it and 197 // insert it into MediaProvider. The only problem is that the user 198 // cannot click the thumbnail to review the picture. 199 Log.e(TAG, "Failed to write MediaStore" + th); 200 } 201 return uri; 202 } 203 204 // Overwrites the file and updates the MediaStore 205 206 /** 207 * Take jpeg bytes and add them to the media store, either replacing an existing item 208 * or a placeholder uri to replace 209 * @param imageUri The content uri or session uri of the image being updated 210 * @param resolver The content resolver to use 211 * @param title of the image 212 * @param date of the image 213 * @param location of the image 214 * @param orientation of the image 215 * @param exif of the image 216 * @param jpeg bytes of the image 217 * @param width of the image 218 * @param height of the image 219 * @param mimeType of the image 220 * @return The content uri of the newly inserted or replaced item. 221 */ 222 public static Uri updateImage(Uri imageUri, ContentResolver resolver, String title, long date, 223 Location location, int orientation, ExifInterface exif, 224 byte[] jpeg, int width, int height, String mimeType) { 225 String path = generateFilepath(title); 226 writeFile(path, jpeg, exif); 227 return updateImage(imageUri, resolver, title, date, location, orientation, jpeg.length, path, 228 width, height, mimeType); 229 } 230 231 232 // Updates the image values in MediaStore 233 private static Uri updateImage(Uri imageUri, ContentResolver resolver, String title, 234 long date, Location location, int orientation, int jpegLength, 235 String path, int width, int height, String mimeType) { 236 237 ContentValues values = 238 getContentValuesForData(title, date, location, orientation, jpegLength, path, 239 width, height, mimeType); 240 241 242 Uri resultUri = imageUri; 243 if (Storage.isSessionUri(imageUri)) { 244 // If this is a session uri, then we need to add the image 245 resultUri = addImage(resolver, title, date, location, orientation, jpegLength, path, 246 width, height, mimeType); 247 sSessionsToContentUris.put(imageUri, resultUri); 248 sContentUrisToSessions.put(resultUri, imageUri); 249 } else { 250 // Update the MediaStore 251 resolver.update(imageUri, values, null, null); 252 } 253 return resultUri; 254 } 255 256 /** 257 * Update the image from the file that has changed. 258 * <p> 259 * Note: This will update the DATE_TAKEN to right now. We could consider not 260 * changing it to preserve the original timestamp. 261 */ 262 public static void updateImageFromChangedFile(Uri mediaUri, Location location, 263 ContentResolver resolver, String mimeType) { 264 File mediaFile = new File(ImageLoader.getLocalPathFromUri(resolver, mediaUri)); 265 if (!mediaFile.exists()) { 266 throw new IllegalArgumentException("Provided URI is not an existent file: " 267 + mediaUri.getPath()); 268 } 269 270 ContentValues values = new ContentValues(); 271 // TODO: Read the date from file. 272 values.put(Images.Media.DATE_TAKEN, System.currentTimeMillis()); 273 values.put(Images.Media.MIME_TYPE, mimeType); 274 values.put(Images.Media.SIZE, mediaFile.length()); 275 if (location != null) { 276 values.put(ImageColumns.LATITUDE, location.getLatitude()); 277 values.put(ImageColumns.LONGITUDE, location.getLongitude()); 278 } 279 280 resolver.update(mediaUri, values, null, null); 281 } 282 283 /** 284 * Updates the item's mime type to the given one. This is useful e.g. when 285 * switching an image to an in-progress type for re-processing. 286 * 287 * @param uri the URI of the item to change 288 * @param mimeType the new mime type of the item 289 */ 290 public static void updateItemMimeType(Uri uri, String mimeType, ContentResolver resolver) { 291 ContentValues values = new ContentValues(1); 292 values.put(ImageColumns.MIME_TYPE, mimeType); 293 294 // Update the MediaStore 295 int rowsModified = resolver.update(uri, values, null, null); 296 if (rowsModified != 1) { 297 // This should never happen 298 throw new IllegalStateException("Bad number of rows (" + rowsModified 299 + ") updated for uri: " + uri); 300 } 301 } 302 303 public static void deleteImage(ContentResolver resolver, Uri uri) { 304 try { 305 resolver.delete(uri, null, null); 306 } catch (Throwable th) { 307 Log.e(TAG, "Failed to delete image: " + uri); 308 } 309 } 310 311 public static String generateFilepath(String title) { 312 return DIRECTORY + '/' + title + ".jpg"; 313 } 314 315 /** 316 * Returns the jpeg bytes for a placeholder session 317 * 318 * @param uri the session uri to look up 319 * @return The jpeg bytes or null 320 */ 321 public static byte[] getJpegForSession(Uri uri) { 322 return sSessionsToPlaceholderBytes.get(uri); 323 } 324 325 /** 326 * Returns the dimensions of the placeholder image 327 * 328 * @param uri the session uri to look up 329 * @return The size 330 */ 331 public static Point getSizeForSession(Uri uri) { 332 return sSessionsToSizes.get(uri); 333 } 334 335 /** 336 * Takes a session URI and returns the finished image's content URI 337 * 338 * @param uri the uri of the session that was replaced 339 * @return The uri of the new media item, if it exists, or null. 340 */ 341 public static Uri getContentUriForSessionUri(Uri uri) { 342 return sSessionsToContentUris.get(uri); 343 } 344 345 /** 346 * Takes a content URI and returns the original Session Uri if any 347 * 348 * @param contentUri the uri of the media store content 349 * @return The session uri of the original session, if it exists, or null. 350 */ 351 public static Uri getSessionUriFromContentUri(Uri contentUri) { 352 return sContentUrisToSessions.get(contentUri); 353 } 354 355 /** 356 * Determines if a URI points to a camera session 357 * 358 * @param uri the uri to check 359 * @return true if it is a session uri. 360 */ 361 public static boolean isSessionUri(Uri uri) { 362 return uri.getScheme().equals(CAMERA_SESSION_SCHEME); 363 } 364 365 public static long getAvailableSpace() { 366 String state = Environment.getExternalStorageState(); 367 Log.d(TAG, "External storage state=" + state); 368 if (Environment.MEDIA_CHECKING.equals(state)) { 369 return PREPARING; 370 } 371 if (!Environment.MEDIA_MOUNTED.equals(state)) { 372 return UNAVAILABLE; 373 } 374 375 File dir = new File(DIRECTORY); 376 dir.mkdirs(); 377 if (!dir.isDirectory() || !dir.canWrite()) { 378 return UNAVAILABLE; 379 } 380 381 try { 382 StatFs stat = new StatFs(DIRECTORY); 383 return stat.getAvailableBlocks() * (long) stat.getBlockSize(); 384 } catch (Exception e) { 385 Log.i(TAG, "Fail to access external storage", e); 386 } 387 return UNKNOWN_SIZE; 388 } 389 390 /** 391 * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be 392 * imported. This is a temporary fix for bug#1655552. 393 */ 394 public static void ensureOSXCompatible() { 395 File nnnAAAAA = new File(DCIM, "100ANDRO"); 396 if (!(nnnAAAAA.exists() || nnnAAAAA.mkdirs())) { 397 Log.e(TAG, "Failed to create " + nnnAAAAA.getPath()); 398 } 399 } 400 401} 402