Storage.java revision 01d56038a53487a9b7989bd17c732b8919da64f1
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.Point;
22import android.location.Location;
23import android.net.Uri;
24import android.os.Environment;
25import android.os.StatFs;
26import android.provider.MediaStore.Images;
27import android.provider.MediaStore.Images.ImageColumns;
28import android.provider.MediaStore.MediaColumns;
29
30import com.android.camera.data.LocalData;
31import com.android.camera.debug.Log;
32import com.android.camera.exif.ExifInterface;
33import com.android.camera.util.ApiHelper;
34
35import java.io.File;
36import java.io.FileOutputStream;
37import java.util.HashMap;
38import java.util.UUID;
39import java.util.concurrent.TimeUnit;
40
41public class Storage {
42    public static final String DCIM =
43            Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString();
44    public static final String DIRECTORY = DCIM + "/Camera";
45    public static final File DIRECTORY_FILE = new File(DIRECTORY);
46    public static final String JPEG_POSTFIX = ".jpg";
47    public static final String GIF_POSTFIX = ".gif";
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 for 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     */
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 for 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     */
109    public 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, mimeType);
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 for 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     */
138    public 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
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     */
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     */
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     */
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, mimeType);
242        writeFile(path, jpeg, exif);
243        return updateImage(imageUri, resolver, title, date, location, orientation, jpeg.length, path,
244                width, height, mimeType);
245    }
246
247    private static void setImageSize(ContentValues values, int width, int height) {
248        // The two fields are available since ICS but got published in JB
249        if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) {
250            values.put(MediaColumns.WIDTH, width);
251            values.put(MediaColumns.HEIGHT, height);
252        }
253    }
254
255    /**
256     * Writes the JPEG data to a file. If there's EXIF info, the EXIF header
257     * will be added.
258     *
259     * @param path The path to the target file.
260     * @param jpeg The JPEG data.
261     * @param exif The EXIF info. Can be {@code null}.
262     *
263     * @return The size of the file. -1 if failed.
264     */
265    public static long writeFile(String path, byte[] jpeg, ExifInterface exif) {
266        if (!createDirectoryIfNeeded(path)) {
267            Log.e(TAG, "Failed to create parent directory for file: " + path);
268            return -1;
269        }
270        if (exif != null) {
271            try {
272                exif.writeExif(jpeg, path);
273                File f = new File(path);
274                return f.length();
275            } catch (Exception e) {
276                Log.e(TAG, "Failed to write data", e);
277            }
278        } else {
279            return writeFile(path, jpeg);
280        }
281        return -1;
282    }
283
284    /**
285     * Writes the data to a file.
286     *
287     * @param path The path to the target file.
288     * @param data The data to save.
289     *
290     * @return The size of the file. -1 if failed.
291     */
292    private static long writeFile(String path, byte[] data) {
293        FileOutputStream out = null;
294        try {
295            out = new FileOutputStream(path);
296            out.write(data);
297            return data.length;
298        } catch (Exception e) {
299            Log.e(TAG, "Failed to write data", e);
300        } finally {
301            try {
302                out.close();
303            } catch (Exception e) {
304                Log.e(TAG, "Failed to close file after write", e);
305            }
306        }
307        return -1;
308    }
309
310    /**
311     * Given a file path, makes sure the directory it's in exists, and if not
312     * that it is created.
313     *
314     * @param filePath the absolute path of a file, e.g. '/foo/bar/file.jpg'.
315     * @return Whether the directory exists. If 'false' is returned, this file
316     *         cannot be written to since the parent directory could not be
317     *         created.
318     */
319    private static boolean createDirectoryIfNeeded(String filePath) {
320        File parentFile = new File(filePath).getParentFile();
321
322        // If the parent exists, return 'true' if it is a directory. If it's a
323        // file, return 'false'.
324        if (parentFile.exists()) {
325            return parentFile.isDirectory();
326        }
327
328        // If the parent does not exists, attempt to create it and return
329        // whether creating it succeeded.
330        return parentFile.mkdirs();
331    }
332
333    /** Updates the image values in MediaStore. */
334    private static Uri updateImage(Uri imageUri, ContentResolver resolver, String title,
335            long date, Location location, int orientation, int jpegLength,
336            String path, int width, int height, String mimeType) {
337
338        ContentValues values =
339                getContentValuesForData(title, date, location, orientation, jpegLength, path,
340                        width, height, mimeType);
341
342
343        Uri resultUri = imageUri;
344        if (Storage.isSessionUri(imageUri)) {
345            // If this is a session uri, then we need to add the image
346            resultUri = addImageToMediaStore(resolver, title, date, location, orientation,
347                    jpegLength, path, width, height, mimeType);
348            sSessionsToContentUris.put(imageUri, resultUri);
349            sContentUrisToSessions.put(resultUri, imageUri);
350        } else {
351            // Update the MediaStore
352            resolver.update(imageUri, values, null, null);
353        }
354        return resultUri;
355    }
356
357    private static String generateFilepath(String title, String mimeType) {
358        return generateFilepath(DIRECTORY, title, mimeType);
359    }
360
361    public static String generateFilepath(String directory, String title, String mimeType) {
362        String extension = null;
363        if (LocalData.MIME_TYPE_JPEG.equals(mimeType)) {
364            extension = JPEG_POSTFIX;
365        } else if (LocalData.MIME_TYPE_GIF.equals(mimeType)) {
366            extension = GIF_POSTFIX;
367        } else {
368            throw new IllegalArgumentException("Invalid mimeType: " + mimeType);
369        }
370        return (new File(directory, title + extension)).getAbsolutePath();
371    }
372
373    /**
374     * Returns the jpeg bytes for a placeholder session
375     *
376     * @param uri the session uri to look up
377     * @return The jpeg bytes or null
378     */
379    public static byte[] getJpegForSession(Uri uri) {
380        return sSessionsToPlaceholderBytes.get(uri);
381    }
382
383    /**
384     * Returns the current version of a placeholder for a session. The version will increment
385     * with each call to replacePlaceholder.
386     *
387     * @param uri the session uri to look up.
388     * @return the current version int.
389     */
390    public static int getJpegVersionForSession(Uri uri) {
391        return sSessionsToPlaceholderVersions.get(uri);
392    }
393
394    /**
395     * Returns the dimensions of the placeholder image
396     *
397     * @param uri the session uri to look up
398     * @return The size
399     */
400    public static Point getSizeForSession(Uri uri) {
401        return sSessionsToSizes.get(uri);
402    }
403
404    /**
405     * Takes a session URI and returns the finished image's content URI
406     *
407     * @param uri the uri of the session that was replaced
408     * @return The uri of the new media item, if it exists, or null.
409     */
410    public static Uri getContentUriForSessionUri(Uri uri) {
411        return sSessionsToContentUris.get(uri);
412    }
413
414    /**
415     * Takes a content URI and returns the original Session Uri if any
416     *
417     * @param contentUri the uri of the media store content
418     * @return The session uri of the original session, if it exists, or null.
419     */
420    public static Uri getSessionUriFromContentUri(Uri contentUri) {
421        return sContentUrisToSessions.get(contentUri);
422    }
423
424    /**
425     * Determines if a URI points to a camera session
426     *
427     * @param uri the uri to check
428     * @return true if it is a session uri.
429     */
430    public static boolean isSessionUri(Uri uri) {
431        return uri.getScheme().equals(CAMERA_SESSION_SCHEME);
432    }
433
434    public static long getAvailableSpace() {
435        String state = Environment.getExternalStorageState();
436        Log.d(TAG, "External storage state=" + state);
437        if (Environment.MEDIA_CHECKING.equals(state)) {
438            return PREPARING;
439        }
440        if (!Environment.MEDIA_MOUNTED.equals(state)) {
441            return UNAVAILABLE;
442        }
443
444        File dir = new File(DIRECTORY);
445        dir.mkdirs();
446        if (!dir.isDirectory() || !dir.canWrite()) {
447            return UNAVAILABLE;
448        }
449
450        try {
451            StatFs stat = new StatFs(DIRECTORY);
452            return stat.getAvailableBlocks() * (long) stat.getBlockSize();
453        } catch (Exception e) {
454            Log.i(TAG, "Fail to access external storage", e);
455        }
456        return UNKNOWN_SIZE;
457    }
458
459    /**
460     * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be
461     * imported. This is a temporary fix for bug#1655552.
462     */
463    public static void ensureOSXCompatible() {
464        File nnnAAAAA = new File(DCIM, "100ANDRO");
465        if (!(nnnAAAAA.exists() || nnnAAAAA.mkdirs())) {
466            Log.e(TAG, "Failed to create " + nnnAAAAA.getPath());
467        }
468    }
469
470}
471