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.content.ContentResolver;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.graphics.Bitmap;
23 import android.graphics.BitmapFactory;
24 import android.graphics.Point;
25 import android.location.Location;
26 import android.net.Uri;
27 import android.os.Environment;
28 import android.os.StatFs;
29 import android.provider.MediaStore.Images.Media;
30 import android.util.LruCache;
31 
32 import com.android.camera.data.FilmstripItemData;
33 import com.android.camera.debug.Log;
34 import com.android.camera.exif.ExifInterface;
35 import com.android.camera.util.AndroidContext;
36 import com.android.camera.util.Size;
37 import com.google.common.base.Optional;
38 
39 import java.io.File;
40 import java.io.FileNotFoundException;
41 import java.io.IOException;
42 import java.io.OutputStream;
43 import java.util.HashMap;
44 import java.util.UUID;
45 
46 import javax.annotation.Nonnull;
47 
48 public class Storage {
49     public final String DIRECTORY;
50     public static final String JPEG_POSTFIX = ".jpg";
51     public static final String GIF_POSTFIX = ".gif";
52     public static final long UNAVAILABLE = -1L;
53     public static final long PREPARING = -2L;
54     public static final long UNKNOWN_SIZE = -3L;
55     public static final long ACCESS_FAILURE = -4L;
56     public static final long LOW_STORAGE_THRESHOLD_BYTES = 50000000;
57     public static final String CAMERA_SESSION_SCHEME = "camera_session";
58     private static final Log.Tag TAG = new Log.Tag("Storage");
59     private static final String GOOGLE_COM = "google.com";
60     private HashMap<Uri, Uri> sSessionsToContentUris = new HashMap<>();
61     private HashMap<Uri, Uri> sContentUrisToSessions = new HashMap<>();
62     private LruCache<Uri, Bitmap> sSessionsToPlaceholderBitmap =
63             // 20MB cache as an upper bound for session bitmap storage
64             new LruCache<Uri, Bitmap>(20 * 1024 * 1024) {
65                 @Override
66                 protected int sizeOf(Uri key, Bitmap value) {
67                     return value.getByteCount();
68                 }
69             };
70     private HashMap<Uri, Point> sSessionsToSizes = new HashMap<>();
71     private HashMap<Uri, Integer> sSessionsToPlaceholderVersions = new HashMap<>();
72 
73     private static class Singleton {
74         private static final Storage INSTANCE = new Storage(AndroidContext.instance().get());
75     }
76 
instance()77     public static Storage instance() {
78         return Singleton.INSTANCE;
79     }
80 
Storage(Context context)81     private Storage(Context context) {
82         DIRECTORY = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES).getPath();
83     }
84 
85     /**
86      * Save the image with default JPEG MIME type and add it to the MediaStore.
87      *
88      * @param resolver The The content resolver to use.
89      * @param title The title of the media file.
90      * @param date The date for the media file.
91      * @param location The location of the media file.
92      * @param orientation The orientation of the media file.
93      * @param exif The EXIF info. Can be {@code null}.
94      * @param jpeg The JPEG data.
95      * @param width The width of the media file after the orientation is
96      *              applied.
97      * @param height The height of the media file after the orientation is
98      *               applied.
99      */
addImage(ContentResolver resolver, String title, long date, Location location, int orientation, ExifInterface exif, byte[] jpeg, int width, int height)100     public Uri addImage(ContentResolver resolver, String title, long date,
101             Location location, int orientation, ExifInterface exif, byte[] jpeg, int width,
102             int height) throws IOException {
103 
104         return addImage(resolver, title, date, location, orientation, exif, jpeg, width, height,
105               FilmstripItemData.MIME_TYPE_JPEG);
106     }
107 
108     /**
109      * Saves the media with a given MIME type and adds it to the MediaStore.
110      * <p>
111      * The path will be automatically generated according to the title.
112      * </p>
113      *
114      * @param resolver The The content resolver to use.
115      * @param title The title of the media file.
116      * @param data The data to save.
117      * @param date The date for the media file.
118      * @param location The location of the media file.
119      * @param orientation The orientation of the media file.
120      * @param exif The EXIF info. Can be {@code null}.
121      * @param width The width of the media file after the orientation is
122      *            applied.
123      * @param height The height of the media file after the orientation is
124      *            applied.
125      * @param mimeType The MIME type of the data.
126      * @return The URI of the added image, or null if the image could not be
127      *         added.
128      */
addImage(ContentResolver resolver, String title, long date, Location location, int orientation, ExifInterface exif, byte[] data, int width, int height, String mimeType)129     public Uri addImage(ContentResolver resolver, String title, long date,
130             Location location, int orientation, ExifInterface exif, byte[] data, int width,
131             int height, String mimeType) throws IOException {
132 
133         if (data.length >= 0) {
134             Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
135             return addImageToMediaStore(resolver, title, date, location, orientation, data.length,
136                     bitmap, width, height, mimeType, exif);
137         }
138         return null;
139     }
140 
141     /**
142      * Add the entry for the media file to media store.
143      *
144      * @param resolver The The content resolver to use.
145      * @param title The title of the media file.
146      * @param date The date for the media file.
147      * @param location The location of the media file.
148      * @param orientation The orientation of the media file.
149      * @param bitmap The bitmap representation of the media to store.
150      * @param width The width of the media file after the orientation is
151      *            applied.
152      * @param height The height of the media file after the orientation is
153      *            applied.
154      * @param mimeType The MIME type of the data.
155      * @param exif The exif of the image.
156      * @return The content URI of the inserted media file or null, if the image
157      *         could not be added.
158      */
addImageToMediaStore(ContentResolver resolver, String title, long date, Location location, int orientation, long jpegLength, Bitmap bitmap, int width, int height, String mimeType, ExifInterface exif)159     public Uri addImageToMediaStore(ContentResolver resolver, String title, long date,
160             Location location, int orientation, long jpegLength, Bitmap bitmap, int width,
161             int height, String mimeType, ExifInterface exif) {
162         // Insert into MediaStore.
163         ContentValues values = getContentValuesForData(title, date, location, mimeType, true);
164 
165         Uri uri = null;
166         try {
167             uri = resolver.insert(Media.EXTERNAL_CONTENT_URI, values);
168             writeBitmap(uri, exif, bitmap, resolver);
169         } catch (Throwable th)  {
170             // This can happen when the external volume is already mounted, but
171             // MediaScanner has not notify MediaProvider to add that volume.
172             // The picture is still safe and MediaScanner will find it and
173             // insert it into MediaProvider. The only problem is that the user
174             // cannot click the thumbnail to review the picture.
175             Log.e(TAG, "Failed to write MediaStore" + th);
176             if (uri != null) {
177                 resolver.delete(uri, null, null);
178             }
179         }
180         return uri;
181     }
182 
writeBitmap(Uri uri, ExifInterface exif, Bitmap bitmap, ContentResolver resolver)183     private void writeBitmap(Uri uri, ExifInterface exif, Bitmap bitmap, ContentResolver resolver)
184             throws FileNotFoundException, IOException {
185         OutputStream os = resolver.openOutputStream(uri);
186         if (exif != null) {
187             exif.writeExif(bitmap, os);
188         } else {
189             bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os);
190         }
191         os.close();
192 
193         ContentValues publishValues = new ContentValues();
194         publishValues.put(Media.IS_PENDING, 0);
195         resolver.update(uri, publishValues, null, null);
196         Log.i(TAG, "Image with uri: " + uri + " was published to the MediaStore");
197     }
198 
199     // Get a ContentValues object for the given photo data
getContentValuesForData(String title, long date, Location location, String mimeType, boolean isPending)200     public ContentValues getContentValuesForData(String title, long date, Location location,
201                                                  String mimeType, boolean isPending) {
202 
203         ContentValues values = new ContentValues(11);
204         values.put(Media.TITLE, title);
205         values.put(Media.DISPLAY_NAME, title + JPEG_POSTFIX);
206         values.put(Media.DATE_TAKEN, date);
207         values.put(Media.MIME_TYPE, mimeType);
208 
209         if (isPending) {
210             values.put(Media.IS_PENDING, 1);
211         } else {
212             values.put(Media.IS_PENDING, 0);
213         }
214 
215         if (location != null) {
216             values.put(Media.LATITUDE, location.getLatitude());
217             values.put(Media.LONGITUDE, location.getLongitude());
218         }
219         return values;
220     }
221 
222     /**
223      * Add a placeholder for a new image that does not exist yet.
224      *
225      * @param placeholder the placeholder image
226      * @return A new URI used to reference this placeholder
227      */
addPlaceholder(Bitmap placeholder)228     public Uri addPlaceholder(Bitmap placeholder) {
229         Uri uri = generateUniquePlaceholderUri();
230         replacePlaceholder(uri, placeholder);
231         return uri;
232     }
233 
234     /**
235      * Remove a placeholder from in memory storage.
236      */
removePlaceholder(Uri uri)237     public void removePlaceholder(Uri uri) {
238         sSessionsToSizes.remove(uri);
239         sSessionsToPlaceholderBitmap.remove(uri);
240         sSessionsToPlaceholderVersions.remove(uri);
241     }
242 
243     /**
244      * Add or replace placeholder for a new image that does not exist yet.
245      *
246      * @param uri the uri of the placeholder to replace, or null if this is a
247      *            new one
248      * @param placeholder the placeholder image
249      * @return A URI used to reference this placeholder
250      */
replacePlaceholder(Uri uri, Bitmap placeholder)251     public void replacePlaceholder(Uri uri, Bitmap placeholder) {
252         Log.v(TAG, "session bitmap cache size: " + sSessionsToPlaceholderBitmap.size());
253         Point size = new Point(placeholder.getWidth(), placeholder.getHeight());
254         sSessionsToSizes.put(uri, size);
255         sSessionsToPlaceholderBitmap.put(uri, placeholder);
256         Integer currentVersion = sSessionsToPlaceholderVersions.get(uri);
257         sSessionsToPlaceholderVersions.put(uri, currentVersion == null ? 0 : currentVersion + 1);
258     }
259 
260     /**
261      * Creates an empty placeholder.
262      *
263      * @param size the size of the placeholder in pixels.
264      * @return A new URI used to reference this placeholder
265      */
266     @Nonnull
addEmptyPlaceholder(@onnull Size size)267     public Uri addEmptyPlaceholder(@Nonnull Size size) {
268         Uri uri = generateUniquePlaceholderUri();
269         sSessionsToSizes.put(uri, new Point(size.getWidth(), size.getHeight()));
270         sSessionsToPlaceholderBitmap.remove(uri);
271         Integer currentVersion = sSessionsToPlaceholderVersions.get(uri);
272         sSessionsToPlaceholderVersions.put(uri, currentVersion == null ? 0 : currentVersion + 1);
273         return uri;
274     }
275 
276     /**
277      * Take jpeg bytes and add them to the media store, either replacing an existing item
278      * or a placeholder uri to replace
279      * @param imageUri The content uri or session uri of the image being updated
280      * @param resolver The content resolver to use
281      * @param title of the image
282      * @param date of the image
283      * @param location of the image
284      * @param orientation of the image
285      * @param exif of the image
286      * @param jpeg bytes of the image
287      * @param width of the image
288      * @param height of the image
289      * @param mimeType of the image
290      * @return The content uri of the newly inserted or replaced item.
291      */
updateImage(Uri imageUri, ContentResolver resolver, String title, long date, Location location, int orientation, ExifInterface exif, byte[] jpeg, int width, int height, String mimeType)292     public Uri updateImage(Uri imageUri, ContentResolver resolver, String title, long date,
293            Location location, int orientation, ExifInterface exif,
294            byte[] jpeg, int width, int height, String mimeType) throws IOException {
295         Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length);
296         return updateImage(imageUri, resolver, title, date, location, orientation, jpeg.length,
297                 bitmap, width, height, mimeType, exif);
298     }
299 
generateUniquePlaceholderUri()300     private Uri generateUniquePlaceholderUri() {
301         Uri.Builder builder = new Uri.Builder();
302         String uuid = UUID.randomUUID().toString();
303         builder.scheme(CAMERA_SESSION_SCHEME).authority(GOOGLE_COM).appendPath(uuid);
304         return builder.build();
305     }
306 
307     /**
308      * Renames a file.
309      *
310      * <p/>
311      * Can only be used for regular files, not directories.
312      *
313      * @param inputPath the original path of the file
314      * @param newFilePath the new path of the file
315      * @return false if rename was not successful
316      */
renameFile(File inputPath, File newFilePath)317     public boolean renameFile(File inputPath, File newFilePath) {
318         if (newFilePath.exists()) {
319             Log.e(TAG, "File path already exists: " + newFilePath.getAbsolutePath());
320             return false;
321         }
322         if (inputPath.isDirectory()) {
323             Log.e(TAG, "Input path is directory: " + inputPath.getAbsolutePath());
324             return false;
325         }
326         if (!createDirectoryIfNeeded(newFilePath.getAbsolutePath())) {
327             Log.e(TAG, "Failed to create parent directory for file: " +
328                     newFilePath.getAbsolutePath());
329             return false;
330         }
331         return inputPath.renameTo(newFilePath);
332     }
333 
334     /**
335      * Given a file path, makes sure the directory it's in exists, and if not
336      * that it is created.
337      *
338      * @param filePath the absolute path of a file, e.g. '/foo/bar/file.jpg'.
339      * @return Whether the directory exists. If 'false' is returned, this file
340      *         cannot be written to since the parent directory could not be
341      *         created.
342      */
createDirectoryIfNeeded(String filePath)343     private boolean createDirectoryIfNeeded(String filePath) {
344         File parentFile = new File(filePath).getParentFile();
345 
346         // If the parent exists, return 'true' if it is a directory. If it's a
347         // file, return 'false'.
348         if (parentFile.exists()) {
349             return parentFile.isDirectory();
350         }
351 
352         // If the parent does not exists, attempt to create it and return
353         // whether creating it succeeded.
354         return parentFile.mkdirs();
355     }
356 
357     /** Updates the image values in MediaStore. */
updateImage(Uri imageUri, ContentResolver resolver, String title, long date, Location location, int orientation, int jpegLength, Bitmap bitmap, int width, int height, String mimeType, ExifInterface exif)358     private Uri updateImage(Uri imageUri, ContentResolver resolver, String title,
359             long date, Location location, int orientation, int jpegLength,
360             Bitmap bitmap, int width, int height, String mimeType, ExifInterface exif) {
361 
362         Uri resultUri = imageUri;
363         if (isSessionUri(imageUri)) {
364             // If this is a session uri, then we need to add the image
365             resultUri = addImageToMediaStore(resolver, title, date, location, orientation,
366                     jpegLength, bitmap, width, height, mimeType, exif);
367             sSessionsToContentUris.put(imageUri, resultUri);
368             sContentUrisToSessions.put(resultUri, imageUri);
369         } else {
370             // Update the MediaStore
371             ContentValues values = getContentValuesForData(title, date, location, mimeType, false);
372             resolver.update(imageUri, values, null, null);
373             Log.i(TAG, "Image with uri: " + imageUri + " was updated in the MediaStore");
374         }
375         return resultUri;
376     }
377 
generateFilepath(String title, String mimeType)378     private String generateFilepath(String title, String mimeType) {
379         return generateFilepath(DIRECTORY, title, mimeType);
380     }
381 
generateFilepath(String directory, String title, String mimeType)382     public String generateFilepath(String directory, String title, String mimeType) {
383         String extension = null;
384         if (FilmstripItemData.MIME_TYPE_JPEG.equals(mimeType)) {
385             extension = JPEG_POSTFIX;
386         } else if (FilmstripItemData.MIME_TYPE_GIF.equals(mimeType)) {
387             extension = GIF_POSTFIX;
388         } else {
389             throw new IllegalArgumentException("Invalid mimeType: " + mimeType);
390         }
391         return (new File(directory, title + extension)).getAbsolutePath();
392     }
393 
394     /**
395      * Returns the jpeg bytes for a placeholder session
396      *
397      * @param uri the session uri to look up
398      * @return The bitmap or null
399      */
getPlaceholderForSession(Uri uri)400     public Optional<Bitmap> getPlaceholderForSession(Uri uri) {
401         return Optional.fromNullable(sSessionsToPlaceholderBitmap.get(uri));
402     }
403 
404     /**
405      * @return Whether a placeholder size for the session with the given URI
406      *         exists.
407      */
containsPlaceholderSize(Uri uri)408     public boolean containsPlaceholderSize(Uri uri) {
409         return sSessionsToSizes.containsKey(uri);
410     }
411 
412     /**
413      * Returns the dimensions of the placeholder image
414      *
415      * @param uri the session uri to look up
416      * @return The size
417      */
getSizeForSession(Uri uri)418     public Point getSizeForSession(Uri uri) {
419         return sSessionsToSizes.get(uri);
420     }
421 
422     /**
423      * Takes a session URI and returns the finished image's content URI
424      *
425      * @param uri the uri of the session that was replaced
426      * @return The uri of the new media item, if it exists, or null.
427      */
getContentUriForSessionUri(Uri uri)428     public Uri getContentUriForSessionUri(Uri uri) {
429         return sSessionsToContentUris.get(uri);
430     }
431 
432     /**
433      * Takes a content URI and returns the original Session Uri if any
434      *
435      * @param contentUri the uri of the media store content
436      * @return The session uri of the original session, if it exists, or null.
437      */
getSessionUriFromContentUri(Uri contentUri)438     public Uri getSessionUriFromContentUri(Uri contentUri) {
439         return sContentUrisToSessions.get(contentUri);
440     }
441 
442     /**
443      * Determines if a URI points to a camera session
444      *
445      * @param uri the uri to check
446      * @return true if it is a session uri.
447      */
isSessionUri(Uri uri)448     public boolean isSessionUri(Uri uri) {
449         return uri.getScheme().equals(CAMERA_SESSION_SCHEME);
450     }
451 
getAvailableSpace()452     public long getAvailableSpace() {
453         String state = Environment.getExternalStorageState();
454         Log.d(TAG, "External storage state=" + state);
455         if (Environment.MEDIA_CHECKING.equals(state)) {
456             return PREPARING;
457         }
458         if (!Environment.MEDIA_MOUNTED.equals(state)) {
459             return UNAVAILABLE;
460         }
461 
462         File dir = new File(DIRECTORY);
463         dir.mkdirs();
464         if (!dir.isDirectory() || !dir.canWrite()) {
465             Log.d(TAG, DIRECTORY + " mounted, but isn't directory or cannot write");
466             return UNAVAILABLE;
467         }
468 
469         try {
470             StatFs stat = new StatFs(DIRECTORY);
471             return stat.getAvailableBlocks() * (long) stat.getBlockSize();
472         } catch (Exception e) {
473             Log.i(TAG, "Fail to access external storage", e);
474         }
475         return UNKNOWN_SIZE;
476     }
477 
478     /**
479      * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be
480      * imported. This is a temporary fix for bug#1655552.
481      */
ensureOSXCompatible()482     public void ensureOSXCompatible() {
483         File nnnAAAAA = new File(DIRECTORY, "100ANDRO");
484         if (!(nnnAAAAA.exists() || nnnAAAAA.mkdirs())) {
485             Log.e(TAG, "Failed to create " + nnnAAAAA.getPath());
486         }
487     }
488 
489 }
490