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