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