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 
ImageListParam()80         public ImageListParam() {}
81 
writeToParcel(Parcel out, int flags)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 
ImageListParam(Parcel in)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 
toString()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 
describeContents()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      */
getBucketId(String path)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      */
ensureOSXCompatibleFolder()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      */
isImageMimeType(String mimeType)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      */
isImage(IImage image)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      */
isVideo(IImage image)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     //
addImage(ContentResolver cr, String title, long dateTaken, Location location, String directory, String filename, Bitmap source, byte[] jpegData, int[] degree)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 
getExifOrientation(String filepath)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.
makeImageList(ContentResolver cr, ImageListParam param)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.
makeImageList(ContentResolver cr, Uri uri, int sort)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 
isSingleImageMode(String uriString)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 {
close()364         public void close() {
365         }
366 
getBucketIds()367         public HashMap<String, String> getBucketIds() {
368             return new HashMap<String, String>();
369         }
370 
getCount()371         public int getCount() {
372             return 0;
373         }
374 
isEmpty()375         public boolean isEmpty() {
376             return true;
377         }
378 
getImageAt(int i)379         public IImage getImageAt(int i) {
380             return null;
381         }
382 
getImageForUri(Uri uri)383         public IImage getImageForUri(Uri uri) {
384             return null;
385         }
386 
removeImage(IImage image)387         public boolean removeImage(IImage image) {
388             return false;
389         }
390 
removeImageAt(int i)391         public boolean removeImageAt(int i) {
392             return false;
393         }
394 
getImageIndex(IImage image)395         public int getImageIndex(IImage image) {
396             throw new UnsupportedOperationException();
397         }
398     }
399 
getImageListParam(DataLocation location, int inclusion, int sort, String bucketId)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 
getSingleImageListParam(Uri uri)410     public static ImageListParam getSingleImageListParam(Uri uri) {
411         ImageListParam param = new ImageListParam();
412         param.mSingleImageUri = uri;
413         return param;
414     }
415 
getEmptyImageListParam()416     public static ImageListParam getEmptyImageListParam() {
417         ImageListParam param = new ImageListParam();
418         param.mIsEmptyImageList = true;
419         return param;
420     }
421 
makeImageList(ContentResolver cr, DataLocation location, int inclusion, int sort, String bucketId)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 
makeEmptyImageList()429     public static IImageList makeEmptyImageList() {
430         return makeImageList(null, getEmptyImageListParam());
431     }
432 
makeSingleImageList(ContentResolver cr, Uri uri)433     public static IImageList  makeSingleImageList(ContentResolver cr, Uri uri) {
434         return makeImageList(cr, getSingleImageListParam(uri));
435     }
436 
checkFsWritable()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 
hasStorage()465     public static boolean hasStorage() {
466         return hasStorage(true);
467     }
468 
hasStorage(boolean requireWriteAccess)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 
query(ContentResolver resolver, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)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 
isMediaScannerScanning(ContentResolver cr)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