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