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