ImageManager.java revision b62254a185d04ecfe2ddc837b7e0e54f0273c478
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
17package com.android.camera;
18
19import com.android.camera.gallery.BaseImageList;
20import com.android.camera.gallery.DrmImageList;
21import com.android.camera.gallery.IImage;
22import com.android.camera.gallery.IImageList;
23import com.android.camera.gallery.ImageList;
24import com.android.camera.gallery.ImageListUber;
25import com.android.camera.gallery.SingleImageList;
26import com.android.camera.gallery.VideoList;
27import com.android.camera.gallery.VideoObject;
28
29import android.content.ContentResolver;
30import android.content.ContentValues;
31import android.database.Cursor;
32import android.graphics.Bitmap;
33import android.graphics.Bitmap.CompressFormat;
34import android.location.Location;
35import android.media.ExifInterface;
36import android.net.Uri;
37import android.os.Environment;
38import android.os.Parcel;
39import android.os.Parcelable;
40import android.provider.DrmStore;
41import android.provider.MediaStore;
42import android.provider.MediaStore.Images;
43import android.util.Log;
44
45import java.io.File;
46import java.io.FileNotFoundException;
47import java.io.FileOutputStream;
48import java.io.IOException;
49import java.io.OutputStream;
50import java.util.ArrayList;
51import java.util.HashMap;
52import java.util.Iterator;
53
54/**
55 * ImageManager is used to retrieve and store images
56 * in the media content provider.
57 */
58public 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