1 /*
2  * Copyright (C) 2010 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 android.annotation.TargetApi;
20 import android.content.ContentResolver;
21 import android.content.ContentValues;
22 import android.graphics.Point;
23 import android.location.Location;
24 import android.net.Uri;
25 import android.os.Build;
26 import android.os.Environment;
27 import android.os.StatFs;
28 import android.provider.MediaStore.Images;
29 import android.provider.MediaStore.Images.ImageColumns;
30 import android.provider.MediaStore.MediaColumns;
31 
32 import com.android.camera.data.LocalData;
33 import com.android.camera.debug.Log;
34 import com.android.camera.exif.ExifInterface;
35 import com.android.camera.util.ApiHelper;
36 
37 import java.io.File;
38 import java.io.FileOutputStream;
39 import java.util.HashMap;
40 import java.util.UUID;
41 import java.util.concurrent.TimeUnit;
42 
43 public class Storage {
44     public static final String DCIM =
45             Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString();
46     public static final String DIRECTORY = DCIM + "/Camera";
47     public static final String JPEG_POSTFIX = ".jpg";
48     // Match the code in MediaProvider.computeBucketValues().
49     public static final String BUCKET_ID =
50             String.valueOf(DIRECTORY.toLowerCase().hashCode());
51     public static final long UNAVAILABLE = -1L;
52     public static final long PREPARING = -2L;
53     public static final long UNKNOWN_SIZE = -3L;
54     public static final long LOW_STORAGE_THRESHOLD_BYTES = 50000000;
55     public static final String CAMERA_SESSION_SCHEME = "camera_session";
56     private static final Log.Tag TAG = new Log.Tag("Storage");
57     private static final String GOOGLE_COM = "google.com";
58     private static HashMap<Uri, Uri> sSessionsToContentUris = new HashMap<Uri, Uri>();
59     private static HashMap<Uri, Uri> sContentUrisToSessions = new HashMap<Uri, Uri>();
60     private static HashMap<Uri, byte[]> sSessionsToPlaceholderBytes = new HashMap<Uri, byte[]>();
61     private static HashMap<Uri, Point> sSessionsToSizes = new HashMap<Uri, Point>();
62     private static HashMap<Uri, Integer> sSessionsToPlaceholderVersions =
63         new HashMap<Uri, Integer>();
64 
65     /**
66      * Save the image with default JPEG MIME type and add it to the MediaStore.
67      *
68      * @param resolver The The content resolver to use.
69      * @param title The title of the media file.
70      * @param date The date fo the media file.
71      * @param location The location of the media file.
72      * @param orientation The orientation of the media file.
73      * @param exif The EXIF info. Can be {@code null}.
74      * @param jpeg The JPEG data.
75      * @param width The width of the media file after the orientation is
76      *              applied.
77      * @param height The height of the media file after the orientation is
78      *               applied.
79      */
addImage(ContentResolver resolver, String title, long date, Location location, int orientation, ExifInterface exif, byte[] jpeg, int width, int height)80     public static Uri addImage(ContentResolver resolver, String title, long date,
81             Location location, int orientation, ExifInterface exif, byte[] jpeg, int width,
82             int height) {
83 
84         return addImage(resolver, title, date, location, orientation, exif, jpeg, width, height,
85                 LocalData.MIME_TYPE_JPEG);
86     }
87 
88     /**
89      * Saves the media with a given MIME type and adds it to the MediaStore.
90      * <p>
91      * The path will be automatically generated according to the title.
92      * </p>
93      *
94      * @param resolver The The content resolver to use.
95      * @param title The title of the media file.
96      * @param data The data to save.
97      * @param date The date fo the media file.
98      * @param location The location of the media file.
99      * @param orientation The orientation of the media file.
100      * @param exif The EXIF info. Can be {@code null}.
101      * @param width The width of the media file after the orientation is
102      *            applied.
103      * @param height The height of the media file after the orientation is
104      *            applied.
105      * @param mimeType The MIME type of the data.
106      * @return The URI of the added image, or null if the image could not be
107      *         added.
108      */
addImage(ContentResolver resolver, String title, long date, Location location, int orientation, ExifInterface exif, byte[] data, int width, int height, String mimeType)109     private static Uri addImage(ContentResolver resolver, String title, long date,
110             Location location, int orientation, ExifInterface exif, byte[] data, int width,
111             int height, String mimeType) {
112 
113         String path = generateFilepath(title);
114         long fileLength = writeFile(path, data, exif);
115         if (fileLength >= 0) {
116             return addImageToMediaStore(resolver, title, date, location, orientation, fileLength,
117                     path, width, height, mimeType);
118         }
119         return null;
120     }
121 
122     /**
123      * Add the entry for the media file to media store.
124      *
125      * @param resolver The The content resolver to use.
126      * @param title The title of the media file.
127      * @param date The date fo the media file.
128      * @param location The location of the media file.
129      * @param orientation The orientation of the media file.
130      * @param width The width of the media file after the orientation is
131      *            applied.
132      * @param height The height of the media file after the orientation is
133      *            applied.
134      * @param mimeType The MIME type of the data.
135      * @return The content URI of the inserted media file or null, if the image
136      *         could not be added.
137      */
addImageToMediaStore(ContentResolver resolver, String title, long date, Location location, int orientation, long jpegLength, String path, int width, int height, String mimeType)138     private static Uri addImageToMediaStore(ContentResolver resolver, String title, long date,
139             Location location, int orientation, long jpegLength, String path, int width, int height,
140             String mimeType) {
141         // Insert into MediaStore.
142         ContentValues values =
143                 getContentValuesForData(title, date, location, orientation, jpegLength, path, width,
144                         height, mimeType);
145 
146         Uri uri = null;
147         try {
148             uri = resolver.insert(Images.Media.EXTERNAL_CONTENT_URI, values);
149         } catch (Throwable th)  {
150             // This can happen when the external volume is already mounted, but
151             // MediaScanner has not notify MediaProvider to add that volume.
152             // The picture is still safe and MediaScanner will find it and
153             // insert it into MediaProvider. The only problem is that the user
154             // cannot click the thumbnail to review the picture.
155             Log.e(TAG, "Failed to write MediaStore" + th);
156         }
157         return uri;
158     }
159 
160     // Get a ContentValues object for the given photo data
getContentValuesForData(String title, long date, Location location, int orientation, long jpegLength, String path, int width, int height, String mimeType)161     public static ContentValues getContentValuesForData(String title,
162             long date, Location location, int orientation, long jpegLength,
163             String path, int width, int height, String mimeType) {
164 
165         File file = new File(path);
166         long dateModifiedSeconds = TimeUnit.MILLISECONDS.toSeconds(file.lastModified());
167 
168         ContentValues values = new ContentValues(11);
169         values.put(ImageColumns.TITLE, title);
170         values.put(ImageColumns.DISPLAY_NAME, title + JPEG_POSTFIX);
171         values.put(ImageColumns.DATE_TAKEN, date);
172         values.put(ImageColumns.MIME_TYPE, mimeType);
173         values.put(ImageColumns.DATE_MODIFIED, dateModifiedSeconds);
174         // Clockwise rotation in degrees. 0, 90, 180, or 270.
175         values.put(ImageColumns.ORIENTATION, orientation);
176         values.put(ImageColumns.DATA, path);
177         values.put(ImageColumns.SIZE, jpegLength);
178 
179         setImageSize(values, width, height);
180 
181         if (location != null) {
182             values.put(ImageColumns.LATITUDE, location.getLatitude());
183             values.put(ImageColumns.LONGITUDE, location.getLongitude());
184         }
185         return values;
186     }
187 
188     /**
189      * Add a placeholder for a new image that does not exist yet.
190      * @param jpeg the bytes of the placeholder image
191      * @param width the image's width
192      * @param height the image's height
193      * @return A new URI used to reference this placeholder
194      */
addPlaceholder(byte[] jpeg, int width, int height)195     public static Uri addPlaceholder(byte[] jpeg, int width, int height) {
196         Uri uri;
197         Uri.Builder builder = new Uri.Builder();
198         String uuid = UUID.randomUUID().toString();
199         builder.scheme(CAMERA_SESSION_SCHEME).authority(GOOGLE_COM).appendPath(uuid);
200         uri = builder.build();
201 
202         replacePlaceholder(uri, jpeg, width, height);
203         return uri;
204     }
205 
206     /**
207      * Add or replace placeholder for a new image that does not exist yet.
208      * @param uri the uri of the placeholder to replace, or null if this is a new one
209      * @param jpeg the bytes of the placeholder image
210      * @param width the image's width
211      * @param height the image's height
212      * @return A URI used to reference this placeholder
213      */
replacePlaceholder(Uri uri, byte[] jpeg, int width, int height)214     public static void replacePlaceholder(Uri uri, byte[] jpeg, int width, int height) {
215         Point size = new Point(width, height);
216         sSessionsToSizes.put(uri, size);
217         sSessionsToPlaceholderBytes.put(uri, jpeg);
218         Integer currentVersion = sSessionsToPlaceholderVersions.get(uri);
219         sSessionsToPlaceholderVersions.put(uri, currentVersion == null ? 0 : currentVersion + 1);
220     }
221 
222     /**
223      * Take jpeg bytes and add them to the media store, either replacing an existing item
224      * or a placeholder uri to replace
225      * @param imageUri The content uri or session uri of the image being updated
226      * @param resolver The content resolver to use
227      * @param title of the image
228      * @param date of the image
229      * @param location of the image
230      * @param orientation of the image
231      * @param exif of the image
232      * @param jpeg bytes of the image
233      * @param width of the image
234      * @param height of the image
235      * @param mimeType of the image
236      * @return The content uri of the newly inserted or replaced item.
237      */
updateImage(Uri imageUri, ContentResolver resolver, String title, long date, Location location, int orientation, ExifInterface exif, byte[] jpeg, int width, int height, String mimeType)238     public static Uri updateImage(Uri imageUri, ContentResolver resolver, String title, long date,
239            Location location, int orientation, ExifInterface exif,
240            byte[] jpeg, int width, int height, String mimeType) {
241         String path = generateFilepath(title);
242         writeFile(path, jpeg, exif);
243         return updateImage(imageUri, resolver, title, date, location, orientation, jpeg.length, path,
244                 width, height, mimeType);
245     }
246 
247     @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
setImageSize(ContentValues values, int width, int height)248     private static void setImageSize(ContentValues values, int width, int height) {
249         // The two fields are available since ICS but got published in JB
250         if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) {
251             values.put(MediaColumns.WIDTH, width);
252             values.put(MediaColumns.HEIGHT, height);
253         }
254     }
255 
256     /**
257      * Writes the JPEG data to a file. If there's EXIF info, the EXIF header
258      * will be added.
259      *
260      * @param path The path to the target file.
261      * @param jpeg The JPEG data.
262      * @param exif The EXIF info. Can be {@code null}.
263      *
264      * @return The size of the file. -1 if failed.
265      */
writeFile(String path, byte[] jpeg, ExifInterface exif)266     private static long writeFile(String path, byte[] jpeg, ExifInterface exif) {
267         if (exif != null) {
268             try {
269                 exif.writeExif(jpeg, path);
270                 File f = new File(path);
271                 return f.length();
272             } catch (Exception e) {
273                 Log.e(TAG, "Failed to write data", e);
274             }
275         } else {
276             return writeFile(path, jpeg);
277         }
278         return -1;
279     }
280 
281     /**
282      * Writes the data to a file.
283      *
284      * @param path The path to the target file.
285      * @param data The data to save.
286      *
287      * @return The size of the file. -1 if failed.
288      */
writeFile(String path, byte[] data)289     private static long writeFile(String path, byte[] data) {
290         FileOutputStream out = null;
291         try {
292             out = new FileOutputStream(path);
293             out.write(data);
294             return data.length;
295         } catch (Exception e) {
296             Log.e(TAG, "Failed to write data", e);
297         } finally {
298             try {
299                 out.close();
300             } catch (Exception e) {
301                 Log.e(TAG, "Failed to close file after write", e);
302             }
303         }
304         return -1;
305     }
306 
307     // Updates the image values in MediaStore
updateImage(Uri imageUri, ContentResolver resolver, String title, long date, Location location, int orientation, int jpegLength, String path, int width, int height, String mimeType)308     private static Uri updateImage(Uri imageUri, ContentResolver resolver, String title,
309             long date, Location location, int orientation, int jpegLength,
310             String path, int width, int height, String mimeType) {
311 
312         ContentValues values =
313                 getContentValuesForData(title, date, location, orientation, jpegLength, path,
314                         width, height, mimeType);
315 
316 
317         Uri resultUri = imageUri;
318         if (Storage.isSessionUri(imageUri)) {
319             // If this is a session uri, then we need to add the image
320             resultUri = addImageToMediaStore(resolver, title, date, location, orientation,
321                     jpegLength, path, width, height, mimeType);
322             sSessionsToContentUris.put(imageUri, resultUri);
323             sContentUrisToSessions.put(resultUri, imageUri);
324         } else {
325             // Update the MediaStore
326             resolver.update(imageUri, values, null, null);
327         }
328         return resultUri;
329     }
330 
generateFilepath(String title)331     private static String generateFilepath(String title) {
332         return DIRECTORY + '/' + title + ".jpg";
333     }
334 
335     /**
336      * Returns the jpeg bytes for a placeholder session
337      *
338      * @param uri the session uri to look up
339      * @return The jpeg bytes or null
340      */
getJpegForSession(Uri uri)341     public static byte[] getJpegForSession(Uri uri) {
342         return sSessionsToPlaceholderBytes.get(uri);
343     }
344 
345     /**
346      * Returns the current version of a placeholder for a session. The version will increment
347      * with each call to replacePlaceholder.
348      *
349      * @param uri the session uri to look up.
350      * @return the current version int.
351      */
getJpegVersionForSession(Uri uri)352     public static int getJpegVersionForSession(Uri uri) {
353         return sSessionsToPlaceholderVersions.get(uri);
354     }
355 
356     /**
357      * Returns the dimensions of the placeholder image
358      *
359      * @param uri the session uri to look up
360      * @return The size
361      */
getSizeForSession(Uri uri)362     public static Point getSizeForSession(Uri uri) {
363         return sSessionsToSizes.get(uri);
364     }
365 
366     /**
367      * Takes a session URI and returns the finished image's content URI
368      *
369      * @param uri the uri of the session that was replaced
370      * @return The uri of the new media item, if it exists, or null.
371      */
getContentUriForSessionUri(Uri uri)372     public static Uri getContentUriForSessionUri(Uri uri) {
373         return sSessionsToContentUris.get(uri);
374     }
375 
376     /**
377      * Takes a content URI and returns the original Session Uri if any
378      *
379      * @param contentUri the uri of the media store content
380      * @return The session uri of the original session, if it exists, or null.
381      */
getSessionUriFromContentUri(Uri contentUri)382     public static Uri getSessionUriFromContentUri(Uri contentUri) {
383         return sContentUrisToSessions.get(contentUri);
384     }
385 
386     /**
387      * Determines if a URI points to a camera session
388      *
389      * @param uri the uri to check
390      * @return true if it is a session uri.
391      */
isSessionUri(Uri uri)392     public static boolean isSessionUri(Uri uri) {
393         return uri.getScheme().equals(CAMERA_SESSION_SCHEME);
394     }
395 
getAvailableSpace()396     public static long getAvailableSpace() {
397         String state = Environment.getExternalStorageState();
398         Log.d(TAG, "External storage state=" + state);
399         if (Environment.MEDIA_CHECKING.equals(state)) {
400             return PREPARING;
401         }
402         if (!Environment.MEDIA_MOUNTED.equals(state)) {
403             return UNAVAILABLE;
404         }
405 
406         File dir = new File(DIRECTORY);
407         dir.mkdirs();
408         if (!dir.isDirectory() || !dir.canWrite()) {
409             return UNAVAILABLE;
410         }
411 
412         try {
413             StatFs stat = new StatFs(DIRECTORY);
414             return stat.getAvailableBlocks() * (long) stat.getBlockSize();
415         } catch (Exception e) {
416             Log.i(TAG, "Fail to access external storage", e);
417         }
418         return UNKNOWN_SIZE;
419     }
420 
421     /**
422      * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be
423      * imported. This is a temporary fix for bug#1655552.
424      */
ensureOSXCompatible()425     public static void ensureOSXCompatible() {
426         File nnnAAAAA = new File(DCIM, "100ANDRO");
427         if (!(nnnAAAAA.exists() || nnnAAAAA.mkdirs())) {
428             Log.e(TAG, "Failed to create " + nnnAAAAA.getPath());
429         }
430     }
431 
432 }
433