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.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      */
     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      */
    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      */
    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
    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      */
    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      */
    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      */
    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)
    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      */
    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      */
    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
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    392     public static boolean isSessionUri(Uri uri) {
    393         return uri.getScheme().equals(CAMERA_SESSION_SCHEME);
    394     }
    395 
    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      */
    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