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