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