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.VideoList;
25
26import android.content.ContentResolver;
27import android.content.ContentValues;
28import android.database.Cursor;
29import android.graphics.Bitmap;
30import android.graphics.Bitmap.CompressFormat;
31import android.location.Location;
32import android.media.ExifInterface;
33import android.net.Uri;
34import android.os.Environment;
35import android.os.Parcel;
36import android.os.Parcelable;
37import android.provider.MediaStore;
38import android.provider.MediaStore.Images;
39import android.util.Log;
40import android.view.OrientationEventListener;
41
42import java.io.File;
43import java.io.FileNotFoundException;
44import java.io.FileOutputStream;
45import java.io.IOException;
46import java.io.OutputStream;
47import java.util.ArrayList;
48import java.util.Iterator;
49
50/**
51 * {@code ImageManager} is used to retrieve and store images
52 * in the media content provider.
53 */
54public 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    public static int roundOrientation(int orientationInput) {
159        int orientation = orientationInput;
160
161        if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) {
162            orientation = 0;
163        }
164
165        orientation = orientation % 360;
166        int retVal;
167        if (orientation < (0 * 90) + 45) {
168            retVal = 0;
169        } else if (orientation < (1 * 90) + 45) {
170            retVal = 90;
171        } else if (orientation < (2 * 90) + 45) {
172            retVal = 180;
173        } else if (orientation < (3 * 90) + 45) {
174            retVal = 270;
175        } else {
176            retVal = 0;
177        }
178
179        return retVal;
180    }
181
182    //
183    // Stores a bitmap or a jpeg byte array to a file (using the specified
184    // directory and filename). Also add an entry to the media store for
185    // this picture. The title, dateTaken, location are attributes for the
186    // picture. The degree is a one element array which returns the orientation
187    // of the picture.
188    //
189    public static Uri addImage(ContentResolver cr, String title, long dateTaken,
190            Location location, String directory, String filename,
191            Bitmap source, byte[] jpegData, int[] degree) {
192        // We should store image data earlier than insert it to ContentProvider,
193        // otherwise we may not be able to generate thumbnail in time.
194        OutputStream outputStream = null;
195        String filePath = directory + "/" + filename;
196        try {
197            File dir = new File(directory);
198            if (!dir.exists()) dir.mkdirs();
199            File file = new File(directory, filename);
200            outputStream = new FileOutputStream(file);
201            if (source != null) {
202                source.compress(CompressFormat.JPEG, 75, outputStream);
203                degree[0] = 0;
204            } else {
205                outputStream.write(jpegData);
206                degree[0] = getExifOrientation(filePath);
207            }
208        } catch (FileNotFoundException ex) {
209            Log.w(TAG, ex);
210            return null;
211        } catch (IOException ex) {
212            Log.w(TAG, ex);
213            return null;
214        } finally {
215            Util.closeSilently(outputStream);
216        }
217
218        // Read back the compressed file size.
219        long size = new File(directory, filename).length();
220
221        ContentValues values = new ContentValues(9);
222        values.put(Images.Media.TITLE, title);
223
224        // That filename is what will be handed to Gmail when a user shares a
225        // photo. Gmail gets the name of the picture attachment from the
226        // "DISPLAY_NAME" field.
227        values.put(Images.Media.DISPLAY_NAME, filename);
228        values.put(Images.Media.DATE_TAKEN, dateTaken);
229        values.put(Images.Media.MIME_TYPE, "image/jpeg");
230        values.put(Images.Media.ORIENTATION, degree[0]);
231        values.put(Images.Media.DATA, filePath);
232        values.put(Images.Media.SIZE, size);
233
234        if (location != null) {
235            values.put(Images.Media.LATITUDE, location.getLatitude());
236            values.put(Images.Media.LONGITUDE, location.getLongitude());
237        }
238
239        return cr.insert(STORAGE_URI, values);
240    }
241
242    public static int getExifOrientation(String filepath) {
243        int degree = 0;
244        ExifInterface exif = null;
245        try {
246            exif = new ExifInterface(filepath);
247        } catch (IOException ex) {
248            Log.e(TAG, "cannot read exif", ex);
249        }
250        if (exif != null) {
251            int orientation = exif.getAttributeInt(
252                ExifInterface.TAG_ORIENTATION, -1);
253            if (orientation != -1) {
254                // We only recognize a subset of orientation tag values.
255                switch(orientation) {
256                    case ExifInterface.ORIENTATION_ROTATE_90:
257                        degree = 90;
258                        break;
259                    case ExifInterface.ORIENTATION_ROTATE_180:
260                        degree = 180;
261                        break;
262                    case ExifInterface.ORIENTATION_ROTATE_270:
263                        degree = 270;
264                        break;
265                }
266
267            }
268        }
269        return degree;
270    }
271
272    // This is the factory function to create an image list.
273    public static IImageList makeImageList(ContentResolver cr,
274            ImageListParam param) {
275        DataLocation location = param.mLocation;
276        int inclusion = param.mInclusion;
277        int sort = param.mSort;
278        String bucketId = param.mBucketId;
279        boolean isEmptyImageList = param.mIsEmptyImageList;
280
281        if (isEmptyImageList || cr == null) {
282            return new EmptyImageList();
283        }
284
285        // false ==> don't require write access
286        boolean haveSdCard = hasStorage(false);
287
288        // use this code to merge videos and stills into the same list
289        ArrayList<BaseImageList> l = new ArrayList<BaseImageList>();
290
291        if (haveSdCard && location != DataLocation.INTERNAL) {
292            if ((inclusion & INCLUDE_IMAGES) != 0) {
293                l.add(new ImageList(cr, STORAGE_URI, sort, bucketId));
294            }
295            if ((inclusion & INCLUDE_VIDEOS) != 0) {
296                l.add(new VideoList(cr, VIDEO_STORAGE_URI, sort, bucketId));
297            }
298        }
299        if (location == DataLocation.INTERNAL || location == DataLocation.ALL) {
300            if ((inclusion & INCLUDE_IMAGES) != 0) {
301                l.add(new ImageList(cr,
302                        Images.Media.INTERNAL_CONTENT_URI, sort, bucketId));
303            }
304        }
305
306        // Optimization: If some of the lists are empty, remove them.
307        // If there is only one remaining list, return it directly.
308        Iterator<BaseImageList> iter = l.iterator();
309        while (iter.hasNext()) {
310            BaseImageList sublist = iter.next();
311            if (sublist.isEmpty()) {
312                sublist.close();
313                iter.remove();
314            }
315        }
316
317        if (l.size() == 1) {
318            BaseImageList list = l.get(0);
319            return list;
320        }
321
322        ImageListUber uber = new ImageListUber(
323                l.toArray(new IImageList[l.size()]), sort);
324        return uber;
325    }
326
327    private static class EmptyImageList implements IImageList {
328        public void close() {
329        }
330
331        public int getCount() {
332            return 0;
333        }
334
335        public IImage getImageAt(int i) {
336            return null;
337        }
338    }
339
340    public static ImageListParam getImageListParam(DataLocation location,
341         int inclusion, int sort, String bucketId) {
342         ImageListParam param = new ImageListParam();
343         param.mLocation = location;
344         param.mInclusion = inclusion;
345         param.mSort = sort;
346         param.mBucketId = bucketId;
347         return param;
348    }
349
350    public static IImageList makeImageList(ContentResolver cr,
351            DataLocation location, int inclusion, int sort, String bucketId) {
352        ImageListParam param = getImageListParam(location, inclusion, sort,
353                bucketId);
354        return makeImageList(cr, param);
355    }
356
357    private static boolean checkFsWritable() {
358        // Create a temporary file to see whether a volume is really writeable.
359        // It's important not to put it in the root directory which may have a
360        // limit on the number of files.
361        String directoryName =
362                Environment.getExternalStorageDirectory().toString() + "/DCIM";
363        File directory = new File(directoryName);
364        if (!directory.isDirectory()) {
365            if (!directory.mkdirs()) {
366                return false;
367            }
368        }
369        return directory.canWrite();
370    }
371
372    public static boolean hasStorage() {
373        return hasStorage(true);
374    }
375
376    public static boolean hasStorage(boolean requireWriteAccess) {
377        String state = Environment.getExternalStorageState();
378
379        if (Environment.MEDIA_MOUNTED.equals(state)) {
380            if (requireWriteAccess) {
381                boolean writable = checkFsWritable();
382                return writable;
383            } else {
384                return true;
385            }
386        } else if (!requireWriteAccess
387                && Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
388            return true;
389        }
390        return false;
391    }
392
393    private static Cursor query(ContentResolver resolver, Uri uri,
394            String[] projection, String selection, String[] selectionArgs,
395            String sortOrder) {
396        try {
397            if (resolver == null) {
398                return null;
399            }
400            return resolver.query(
401                    uri, projection, selection, selectionArgs, sortOrder);
402         } catch (UnsupportedOperationException ex) {
403            return null;
404        }
405
406    }
407
408    public static boolean isMediaScannerScanning(ContentResolver cr) {
409        boolean result = false;
410        Cursor cursor = query(cr, MediaStore.getMediaScannerUri(),
411                new String [] {MediaStore.MEDIA_SCANNER_VOLUME},
412                null, null, null);
413        if (cursor != null) {
414            if (cursor.getCount() == 1) {
415                cursor.moveToFirst();
416                result = "external".equals(cursor.getString(0));
417            }
418            cursor.close();
419        }
420
421        return result;
422    }
423
424    public static String getLastImageThumbPath() {
425        return Environment.getExternalStorageDirectory().toString() +
426               "/DCIM/.thumbnails/image_last_thumb";
427    }
428
429    public static String getLastVideoThumbPath() {
430        return Environment.getExternalStorageDirectory().toString() +
431               "/DCIM/.thumbnails/video_last_thumb";
432    }
433
434    public static String getTempJpegPath() {
435        return Environment.getExternalStorageDirectory().toString() +
436               "/DCIM/.tempjpeg";
437    }
438}
439