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