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