Home | History | Annotate | Download | only in camera
      1 /*
      2  * Copyright (C) 2007 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 com.android.camera.gallery.BaseImageList;
     20 import com.android.camera.gallery.DrmImageList;
     21 import com.android.camera.gallery.IImage;
     22 import com.android.camera.gallery.IImageList;
     23 import com.android.camera.gallery.ImageList;
     24 import com.android.camera.gallery.ImageListUber;
     25 import com.android.camera.gallery.SingleImageList;
     26 import com.android.camera.gallery.VideoList;
     27 import com.android.camera.gallery.VideoObject;
     28 
     29 import android.content.ContentResolver;
     30 import android.content.ContentValues;
     31 import android.database.Cursor;
     32 import android.graphics.Bitmap;
     33 import android.graphics.Bitmap.CompressFormat;
     34 import android.location.Location;
     35 import android.media.ExifInterface;
     36 import android.net.Uri;
     37 import android.os.Environment;
     38 import android.os.Parcel;
     39 import android.os.Parcelable;
     40 import android.provider.DrmStore;
     41 import android.provider.MediaStore;
     42 import android.provider.MediaStore.Images;
     43 import android.util.Log;
     44 
     45 import java.io.File;
     46 import java.io.FileNotFoundException;
     47 import java.io.FileOutputStream;
     48 import java.io.IOException;
     49 import java.io.OutputStream;
     50 import java.util.ArrayList;
     51 import java.util.HashMap;
     52 import java.util.Iterator;
     53 
     54 /**
     55  * ImageManager is used to retrieve and store images
     56  * in the media content provider.
     57  */
     58 public class ImageManager {
     59     private static final String TAG = "ImageManager";
     60 
     61     private static final Uri STORAGE_URI = Images.Media.EXTERNAL_CONTENT_URI;
     62     private static final Uri THUMB_URI
     63             = Images.Thumbnails.EXTERNAL_CONTENT_URI;
     64 
     65     private static final Uri VIDEO_STORAGE_URI =
     66             Uri.parse("content://media/external/video/media");
     67 
     68     // ImageListParam specifies all the parameters we need to create an image
     69     // list (we also need a ContentResolver).
     70     public static class ImageListParam implements Parcelable {
     71         public DataLocation mLocation;
     72         public int mInclusion;
     73         public int mSort;
     74         public String mBucketId;
     75 
     76         // This is only used if we are creating a single image list.
     77         public Uri mSingleImageUri;
     78 
     79         // This is only used if we are creating an empty image list.
     80         public boolean mIsEmptyImageList;
     81 
     82         public ImageListParam() {}
     83 
     84         public void writeToParcel(Parcel out, int flags) {
     85             out.writeInt(mLocation.ordinal());
     86             out.writeInt(mInclusion);
     87             out.writeInt(mSort);
     88             out.writeString(mBucketId);
     89             out.writeParcelable(mSingleImageUri, flags);
     90             out.writeInt(mIsEmptyImageList ? 1 : 0);
     91         }
     92 
     93         private ImageListParam(Parcel in) {
     94             mLocation = DataLocation.values()[in.readInt()];
     95             mInclusion = in.readInt();
     96             mSort = in.readInt();
     97             mBucketId = in.readString();
     98             mSingleImageUri = in.readParcelable(null);
     99             mIsEmptyImageList = (in.readInt() != 0);
    100         }
    101 
    102         public String toString() {
    103             return String.format("ImageListParam{loc=%s,inc=%d,sort=%d," +
    104                 "bucket=%s,empty=%b,single=%s}", mLocation, mInclusion,
    105                 mSort, mBucketId, mIsEmptyImageList, mSingleImageUri);
    106         }
    107 
    108         public static final Parcelable.Creator CREATOR
    109                 = new Parcelable.Creator() {
    110             public ImageListParam createFromParcel(Parcel in) {
    111                 return new ImageListParam(in);
    112             }
    113 
    114             public ImageListParam[] newArray(int size) {
    115                 return new ImageListParam[size];
    116             }
    117         };
    118 
    119         public int describeContents() {
    120             return 0;
    121         }
    122     }
    123 
    124     // Location
    125     public static enum DataLocation { NONE, INTERNAL, EXTERNAL, ALL }
    126 
    127     // Inclusion
    128     public static final int INCLUDE_IMAGES = (1 << 0);
    129     public static final int INCLUDE_DRM_IMAGES = (1 << 1);
    130     public static final int INCLUDE_VIDEOS = (1 << 2);
    131 
    132     // Sort
    133     public static final int SORT_ASCENDING = 1;
    134     public static final int SORT_DESCENDING = 2;
    135 
    136     public static final String CAMERA_IMAGE_BUCKET_NAME =
    137             Environment.getExternalStorageDirectory().toString()
    138             + "/DCIM/Camera";
    139     public static final String CAMERA_IMAGE_BUCKET_ID =
    140             getBucketId(CAMERA_IMAGE_BUCKET_NAME);
    141 
    142     /**
    143      * Matches code in MediaProvider.computeBucketValues. Should be a common
    144      * function.
    145      */
    146     public static String getBucketId(String path) {
    147         return String.valueOf(path.toLowerCase().hashCode());
    148     }
    149 
    150     /**
    151      * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be
    152      * imported. This is a temporary fix for bug#1655552.
    153      */
    154     public static void ensureOSXCompatibleFolder() {
    155         File nnnAAAAA = new File(
    156             Environment.getExternalStorageDirectory().toString()
    157             + "/DCIM/100ANDRO");
    158         if ((!nnnAAAAA.exists()) && (!nnnAAAAA.mkdir())) {
    159             Log.e(TAG, "create NNNAAAAA file: " + nnnAAAAA.getPath()
    160                     + " failed");
    161         }
    162     }
    163 
    164     /**
    165      * @return true if the mimetype is an image mimetype.
    166      */
    167     public static boolean isImageMimeType(String mimeType) {
    168         return mimeType.startsWith("image/");
    169     }
    170 
    171     /**
    172      * @return true if the mimetype is a video mimetype.
    173      */
    174     /* This is commented out because isVideo is not calling this now.
    175     public static boolean isVideoMimeType(String mimeType) {
    176         return mimeType.startsWith("video/");
    177     }
    178     */
    179 
    180     /**
    181      * @return true if the image is an image.
    182      */
    183     public static boolean isImage(IImage image) {
    184         return isImageMimeType(image.getMimeType());
    185     }
    186 
    187     /**
    188      * @return true if the image is a video.
    189      */
    190     public static boolean isVideo(IImage image) {
    191         // This is the right implementation, but we use instanceof for speed.
    192         //return isVideoMimeType(image.getMimeType());
    193         return (image instanceof VideoObject);
    194     }
    195 
    196     //
    197     // Stores a bitmap or a jpeg byte array to a file (using the specified
    198     // directory and filename). Also add an entry to the media store for
    199     // this picture. The title, dateTaken, location are attributes for the
    200     // picture. The degree is a one element array which returns the orientation
    201     // of the picture.
    202     //
    203     public static Uri addImage(ContentResolver cr, String title, long dateTaken,
    204             Location location, String directory, String filename,
    205             Bitmap source, byte[] jpegData, int[] degree) {
    206         // We should store image data earlier than insert it to ContentProvider, otherwise
    207         // we may not be able to generate thumbnail in time.
    208         OutputStream outputStream = null;
    209         String filePath = directory + "/" + filename;
    210         try {
    211             File dir = new File(directory);
    212             if (!dir.exists()) dir.mkdirs();
    213             File file = new File(directory, filename);
    214             outputStream = new FileOutputStream(file);
    215             if (source != null) {
    216                 source.compress(CompressFormat.JPEG, 75, outputStream);
    217                 degree[0] = 0;
    218             } else {
    219                 outputStream.write(jpegData);
    220                 degree[0] = getExifOrientation(filePath);
    221             }
    222         } catch (FileNotFoundException ex) {
    223             Log.w(TAG, ex);
    224             return null;
    225         } catch (IOException ex) {
    226             Log.w(TAG, ex);
    227             return null;
    228         } finally {
    229             Util.closeSilently(outputStream);
    230         }
    231 
    232         ContentValues values = new ContentValues(7);
    233         values.put(Images.Media.TITLE, title);
    234 
    235         // That filename is what will be handed to Gmail when a user shares a
    236         // photo. Gmail gets the name of the picture attachment from the
    237         // "DISPLAY_NAME" field.
    238         values.put(Images.Media.DISPLAY_NAME, filename);
    239         values.put(Images.Media.DATE_TAKEN, dateTaken);
    240         values.put(Images.Media.MIME_TYPE, "image/jpeg");
    241         values.put(Images.Media.ORIENTATION, degree[0]);
    242         values.put(Images.Media.DATA, filePath);
    243 
    244         if (location != null) {
    245             values.put(Images.Media.LATITUDE, location.getLatitude());
    246             values.put(Images.Media.LONGITUDE, location.getLongitude());
    247         }
    248 
    249         return cr.insert(STORAGE_URI, values);
    250     }
    251 
    252     public static int getExifOrientation(String filepath) {
    253         int degree = 0;
    254         ExifInterface exif = null;
    255         try {
    256             exif = new ExifInterface(filepath);
    257         } catch (IOException ex) {
    258             Log.e(TAG, "cannot read exif", ex);
    259         }
    260         if (exif != null) {
    261             int orientation = exif.getAttributeInt(
    262                 ExifInterface.TAG_ORIENTATION, -1);
    263             if (orientation != -1) {
    264                 // We only recognize a subset of orientation tag values.
    265                 switch(orientation) {
    266                     case ExifInterface.ORIENTATION_ROTATE_90:
    267                         degree = 90;
    268                         break;
    269                     case ExifInterface.ORIENTATION_ROTATE_180:
    270                         degree = 180;
    271                         break;
    272                     case ExifInterface.ORIENTATION_ROTATE_270:
    273                         degree = 270;
    274                         break;
    275                 }
    276 
    277             }
    278         }
    279         return degree;
    280     }
    281 
    282     // This is the factory function to create an image list.
    283     public static IImageList makeImageList(ContentResolver cr,
    284             ImageListParam param) {
    285         DataLocation location = param.mLocation;
    286         int inclusion = param.mInclusion;
    287         int sort = param.mSort;
    288         String bucketId = param.mBucketId;
    289         Uri singleImageUri = param.mSingleImageUri;
    290         boolean isEmptyImageList = param.mIsEmptyImageList;
    291 
    292         if (isEmptyImageList || cr == null) {
    293             return new EmptyImageList();
    294         }
    295 
    296         if (singleImageUri != null) {
    297             return new SingleImageList(cr, singleImageUri);
    298         }
    299 
    300         // false ==> don't require write access
    301         boolean haveSdCard = hasStorage(false);
    302 
    303         // use this code to merge videos and stills into the same list
    304         ArrayList<BaseImageList> l = new ArrayList<BaseImageList>();
    305 
    306         if (haveSdCard && location != DataLocation.INTERNAL) {
    307             if ((inclusion & INCLUDE_IMAGES) != 0) {
    308                 l.add(new ImageList(cr, STORAGE_URI, sort, bucketId));
    309             }
    310             if ((inclusion & INCLUDE_VIDEOS) != 0) {
    311                 l.add(new VideoList(cr, VIDEO_STORAGE_URI, sort, bucketId));
    312             }
    313         }
    314         if (location == DataLocation.INTERNAL || location == DataLocation.ALL) {
    315             if ((inclusion & INCLUDE_IMAGES) != 0) {
    316                 l.add(new ImageList(cr,
    317                         Images.Media.INTERNAL_CONTENT_URI, sort, bucketId));
    318             }
    319             if ((inclusion & INCLUDE_DRM_IMAGES) != 0) {
    320                 l.add(new DrmImageList(
    321                         cr, DrmStore.Images.CONTENT_URI, sort, bucketId));
    322             }
    323         }
    324 
    325         // Optimization: If some of the lists are empty, remove them.
    326         // If there is only one remaining list, return it directly.
    327         Iterator<BaseImageList> iter = l.iterator();
    328         while (iter.hasNext()) {
    329             BaseImageList sublist = iter.next();
    330             if (sublist.isEmpty()) {
    331                 sublist.close();
    332                 iter.remove();
    333             }
    334         }
    335 
    336         if (l.size() == 1) {
    337             BaseImageList list = l.get(0);
    338             return list;
    339         }
    340 
    341         ImageListUber uber = new ImageListUber(
    342                 l.toArray(new IImageList[l.size()]), sort);
    343         return uber;
    344     }
    345 
    346     // This is a convenience function to create an image list from a Uri.
    347     public static IImageList makeImageList(ContentResolver cr, Uri uri,
    348             int sort) {
    349         String uriString = (uri != null) ? uri.toString() : "";
    350 
    351         // TODO: we need to figure out whether we're viewing
    352         // DRM images in a better way.  Is there a constant
    353         // for content://drm somewhere??
    354 
    355         if (uriString.startsWith("content://drm")) {
    356             return makeImageList(cr, DataLocation.ALL, INCLUDE_DRM_IMAGES, sort,
    357                     null);
    358         } else if (uriString.startsWith("content://media/external/video")) {
    359             return makeImageList(cr, DataLocation.EXTERNAL, INCLUDE_VIDEOS,
    360                     sort, null);
    361         } else if (isSingleImageMode(uriString)) {
    362             return makeSingleImageList(cr, uri);
    363         } else {
    364             String bucketId = uri.getQueryParameter("bucketId");
    365             return makeImageList(cr, DataLocation.ALL, INCLUDE_IMAGES, sort,
    366                     bucketId);
    367         }
    368     }
    369 
    370     static boolean isSingleImageMode(String uriString) {
    371         return !uriString.startsWith(
    372                 MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())
    373                 && !uriString.startsWith(
    374                 MediaStore.Images.Media.INTERNAL_CONTENT_URI.toString());
    375     }
    376 
    377     private static class EmptyImageList implements IImageList {
    378         public void close() {
    379         }
    380 
    381         public HashMap<String, String> getBucketIds() {
    382             return new HashMap<String, String>();
    383         }
    384 
    385         public int getCount() {
    386             return 0;
    387         }
    388 
    389         public boolean isEmpty() {
    390             return true;
    391         }
    392 
    393         public IImage getImageAt(int i) {
    394             return null;
    395         }
    396 
    397         public IImage getImageForUri(Uri uri) {
    398             return null;
    399         }
    400 
    401         public boolean removeImage(IImage image) {
    402             return false;
    403         }
    404 
    405         public boolean removeImageAt(int i) {
    406             return false;
    407         }
    408 
    409         public int getImageIndex(IImage image) {
    410             throw new UnsupportedOperationException();
    411         }
    412     }
    413 
    414     public static ImageListParam getImageListParam(DataLocation location,
    415          int inclusion, int sort, String bucketId) {
    416          ImageListParam param = new ImageListParam();
    417          param.mLocation = location;
    418          param.mInclusion = inclusion;
    419          param.mSort = sort;
    420          param.mBucketId = bucketId;
    421          return param;
    422     }
    423 
    424     public static ImageListParam getSingleImageListParam(Uri uri) {
    425         ImageListParam param = new ImageListParam();
    426         param.mSingleImageUri = uri;
    427         return param;
    428     }
    429 
    430     public static ImageListParam getEmptyImageListParam() {
    431         ImageListParam param = new ImageListParam();
    432         param.mIsEmptyImageList = true;
    433         return param;
    434     }
    435 
    436     public static IImageList makeImageList(ContentResolver cr,
    437             DataLocation location, int inclusion, int sort, String bucketId) {
    438         ImageListParam param = getImageListParam(location, inclusion, sort,
    439                 bucketId);
    440         return makeImageList(cr, param);
    441     }
    442 
    443     public static IImageList makeEmptyImageList() {
    444         return makeImageList(null, getEmptyImageListParam());
    445     }
    446 
    447     public static IImageList  makeSingleImageList(ContentResolver cr, Uri uri) {
    448         return makeImageList(cr, getSingleImageListParam(uri));
    449     }
    450 
    451     private static boolean checkFsWritable() {
    452         // Create a temporary file to see whether a volume is really writeable.
    453         // It's important not to put it in the root directory which may have a
    454         // limit on the number of files.
    455         String directoryName =
    456                 Environment.getExternalStorageDirectory().toString() + "/DCIM";
    457         File directory = new File(directoryName);
    458         if (!directory.isDirectory()) {
    459             if (!directory.mkdirs()) {
    460                 return false;
    461             }
    462         }
    463         File f = new File(directoryName, ".probe");
    464         try {
    465             // Remove stale file if any
    466             if (f.exists()) {
    467                 f.delete();
    468             }
    469             if (!f.createNewFile()) {
    470                 return false;
    471             }
    472             f.delete();
    473             return true;
    474         } catch (IOException ex) {
    475             return false;
    476         }
    477     }
    478 
    479     public static boolean hasStorage() {
    480         return hasStorage(true);
    481     }
    482 
    483     public static boolean hasStorage(boolean requireWriteAccess) {
    484         String state = Environment.getExternalStorageState();
    485 
    486         if (Environment.MEDIA_MOUNTED.equals(state)) {
    487             if (requireWriteAccess) {
    488                 boolean writable = checkFsWritable();
    489                 return writable;
    490             } else {
    491                 return true;
    492             }
    493         } else if (!requireWriteAccess
    494                 && Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
    495             return true;
    496         }
    497         return false;
    498     }
    499 
    500     private static Cursor query(ContentResolver resolver, Uri uri,
    501             String[] projection, String selection, String[] selectionArgs,
    502             String sortOrder) {
    503         try {
    504             if (resolver == null) {
    505                 return null;
    506             }
    507             return resolver.query(
    508                     uri, projection, selection, selectionArgs, sortOrder);
    509          } catch (UnsupportedOperationException ex) {
    510             return null;
    511         }
    512 
    513     }
    514 
    515     public static boolean isMediaScannerScanning(ContentResolver cr) {
    516         boolean result = false;
    517         Cursor cursor = query(cr, MediaStore.getMediaScannerUri(),
    518                 new String [] {MediaStore.MEDIA_SCANNER_VOLUME},
    519                 null, null, null);
    520         if (cursor != null) {
    521             if (cursor.getCount() == 1) {
    522                 cursor.moveToFirst();
    523                 result = "external".equals(cursor.getString(0));
    524             }
    525             cursor.close();
    526         }
    527 
    528         return result;
    529     }
    530 }
    531