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