Storage.java revision 837d8f2687cdf53e657c03c1d01b1c5df3902442
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;
36import com.android.camera.util.ImageLoader;
37
38import java.io.File;
39import java.io.FileOutputStream;
40import java.util.HashMap;
41import java.util.UUID;
42import java.util.concurrent.TimeUnit;
43
44public class Storage {
45    public static final String DCIM =
46            Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString();
47    public static final String DIRECTORY = DCIM + "/Camera";
48    public static final String JPEG_POSTFIX = ".jpg";
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
64    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
65    private static void setImageSize(ContentValues values, int width, int height) {
66        // The two fields are available since ICS but got published in JB
67        if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) {
68            values.put(MediaColumns.WIDTH, width);
69            values.put(MediaColumns.HEIGHT, height);
70        }
71    }
72
73    public static void writeFile(String path, byte[] jpeg, ExifInterface exif) {
74        if (exif != null) {
75            try {
76                exif.writeExif(jpeg, path);
77            } catch (Exception e) {
78                Log.e(TAG, "Failed to write data", e);
79            }
80        } else {
81            writeFile(path, jpeg);
82        }
83    }
84
85    public static void writeFile(String path, byte[] data) {
86        FileOutputStream out = null;
87        try {
88            out = new FileOutputStream(path);
89            out.write(data);
90        } catch (Exception e) {
91            Log.e(TAG, "Failed to write data", e);
92        } finally {
93            try {
94                out.close();
95            } catch (Exception e) {
96                Log.e(TAG, "Failed to close file after write", e);
97            }
98        }
99    }
100
101    // Save the image and add it to the MediaStore.
102    public static Uri addImage(ContentResolver resolver, String title, long date,
103            Location location, int orientation, ExifInterface exif, byte[] jpeg, int width,
104            int height) {
105
106        return addImage(resolver, title, date, location, orientation, exif, jpeg, width, height,
107                LocalData.MIME_TYPE_JPEG);
108    }
109
110    // Save the image with a given mimeType and add it the MediaStore.
111    public static Uri addImage(ContentResolver resolver, String title, long date,
112            Location location, int orientation, ExifInterface exif, byte[] jpeg, int width,
113            int height, String mimeType) {
114
115        String path = generateFilepath(title);
116        writeFile(path, jpeg, exif);
117        return addImage(resolver, title, date, location, orientation,
118                jpeg.length, path, width, height, mimeType);
119    }
120
121    // Get a ContentValues object for the given photo data
122    public static ContentValues getContentValuesForData(String title,
123            long date, Location location, int orientation, int jpegLength,
124            String path, int width, int height, String mimeType) {
125
126        File file = new File(path);
127        long dateModifiedSeconds = TimeUnit.MILLISECONDS.toSeconds(file.lastModified());
128
129        ContentValues values = new ContentValues(11);
130        values.put(ImageColumns.TITLE, title);
131        values.put(ImageColumns.DISPLAY_NAME, title + JPEG_POSTFIX);
132        values.put(ImageColumns.DATE_TAKEN, date);
133        values.put(ImageColumns.MIME_TYPE, mimeType);
134        values.put(ImageColumns.DATE_MODIFIED, dateModifiedSeconds);
135        // Clockwise rotation in degrees. 0, 90, 180, or 270.
136        values.put(ImageColumns.ORIENTATION, orientation);
137        values.put(ImageColumns.DATA, path);
138        values.put(ImageColumns.SIZE, jpegLength);
139
140        setImageSize(values, width, height);
141
142        if (location != null) {
143            values.put(ImageColumns.LATITUDE, location.getLatitude());
144            values.put(ImageColumns.LONGITUDE, location.getLongitude());
145        }
146        return values;
147    }
148
149    /**
150     * Add a placeholder for a new image that does not exist yet.
151     * @param jpeg the bytes of the placeholder image
152     * @param width the image's width
153     * @param height the image's height
154     * @return A new URI used to reference this placeholder
155     */
156    public static Uri addPlaceholder(byte[] jpeg, int width, int height) {
157        Uri uri;
158        Uri.Builder builder = new Uri.Builder();
159        String uuid = UUID.randomUUID().toString();
160        builder.scheme(CAMERA_SESSION_SCHEME).authority(GOOGLE_COM).appendPath(uuid);
161        uri = builder.build();
162
163        replacePlaceholder(uri, jpeg, width, height);
164        return uri;
165    }
166
167    /**
168     * Add or replace placeholder for a new image that does not exist yet.
169     * @param uri the uri of the placeholder to replace, or null if this is a new one
170     * @param jpeg the bytes of the placeholder image
171     * @param width the image's width
172     * @param height the image's height
173     * @return A URI used to reference this placeholder
174     */
175    public static void replacePlaceholder(Uri uri, byte[] jpeg, int width, int height) {
176        Point size = new Point(width, height);
177        sSessionsToSizes.put(uri, size);
178        sSessionsToPlaceholderBytes.put(uri, jpeg);
179    }
180
181    // Add the image to media store.
182    public static Uri addImage(ContentResolver resolver, String title,
183            long date, Location location, int orientation, int jpegLength,
184            String path, int width, int height, String mimeType) {
185        // Insert into MediaStore.
186        ContentValues values =
187                getContentValuesForData(title, date, location, orientation, jpegLength, path,
188                        width, height, mimeType);
189
190        Uri uri = null;
191        try {
192            uri = resolver.insert(Images.Media.EXTERNAL_CONTENT_URI, values);
193        } catch (Throwable th)  {
194            // This can happen when the external volume is already mounted, but
195            // MediaScanner has not notify MediaProvider to add that volume.
196            // The picture is still safe and MediaScanner will find it and
197            // insert it into MediaProvider. The only problem is that the user
198            // cannot click the thumbnail to review the picture.
199            Log.e(TAG, "Failed to write MediaStore" + th);
200        }
201        return uri;
202    }
203
204    // Overwrites the file and updates the MediaStore
205
206    /**
207     * Take jpeg bytes and add them to the media store, either replacing an existing item
208     * or a placeholder uri to replace
209     * @param imageUri The content uri or session uri of the image being updated
210     * @param resolver The content resolver to use
211     * @param title of the image
212     * @param date of the image
213     * @param location of the image
214     * @param orientation of the image
215     * @param exif of the image
216     * @param jpeg bytes of the image
217     * @param width of the image
218     * @param height of the image
219     * @param mimeType of the image
220     * @return The content uri of the newly inserted or replaced item.
221     */
222    public static Uri updateImage(Uri imageUri, ContentResolver resolver, String title, long date,
223           Location location, int orientation, ExifInterface exif,
224           byte[] jpeg, int width, int height, String mimeType) {
225        String path = generateFilepath(title);
226        writeFile(path, jpeg, exif);
227        return updateImage(imageUri, resolver, title, date, location, orientation, jpeg.length, path,
228                width, height, mimeType);
229    }
230
231
232    // Updates the image values in MediaStore
233    private static Uri updateImage(Uri imageUri, ContentResolver resolver, String title,
234            long date, Location location, int orientation, int jpegLength,
235            String path, int width, int height, String mimeType) {
236
237        ContentValues values =
238                getContentValuesForData(title, date, location, orientation, jpegLength, path,
239                        width, height, mimeType);
240
241
242        Uri resultUri = imageUri;
243        if (Storage.isSessionUri(imageUri)) {
244            // If this is a session uri, then we need to add the image
245            resultUri = addImage(resolver, title, date, location, orientation, jpegLength, path,
246                    width, height, mimeType);
247            sSessionsToContentUris.put(imageUri, resultUri);
248            sContentUrisToSessions.put(resultUri, imageUri);
249        } else {
250            // Update the MediaStore
251            resolver.update(imageUri, values, null, null);
252        }
253        return resultUri;
254    }
255
256    /**
257     * Update the image from the file that has changed.
258     * <p>
259     * Note: This will update the DATE_TAKEN to right now. We could consider not
260     * changing it to preserve the original timestamp.
261     */
262    public static void updateImageFromChangedFile(Uri mediaUri, Location location,
263            ContentResolver resolver, String mimeType) {
264        File mediaFile = new File(ImageLoader.getLocalPathFromUri(resolver, mediaUri));
265        if (!mediaFile.exists()) {
266            throw new IllegalArgumentException("Provided URI is not an existent file: "
267                    + mediaUri.getPath());
268        }
269
270        ContentValues values = new ContentValues();
271        // TODO: Read the date from file.
272        values.put(Images.Media.DATE_TAKEN, System.currentTimeMillis());
273        values.put(Images.Media.MIME_TYPE, mimeType);
274        values.put(Images.Media.SIZE, mediaFile.length());
275        if (location != null) {
276            values.put(ImageColumns.LATITUDE, location.getLatitude());
277            values.put(ImageColumns.LONGITUDE, location.getLongitude());
278        }
279
280        resolver.update(mediaUri, values, null, null);
281    }
282
283    /**
284     * Updates the item's mime type to the given one. This is useful e.g. when
285     * switching an image to an in-progress type for re-processing.
286     *
287     * @param uri the URI of the item to change
288     * @param mimeType the new mime type of the item
289     */
290    public static void updateItemMimeType(Uri uri, String mimeType, ContentResolver resolver) {
291        ContentValues values = new ContentValues(1);
292        values.put(ImageColumns.MIME_TYPE, mimeType);
293
294        // Update the MediaStore
295        int rowsModified = resolver.update(uri, values, null, null);
296        if (rowsModified != 1) {
297            // This should never happen
298            throw new IllegalStateException("Bad number of rows (" + rowsModified
299                    + ") updated for uri: " + uri);
300        }
301    }
302
303    public static void deleteImage(ContentResolver resolver, Uri uri) {
304        try {
305            resolver.delete(uri, null, null);
306        } catch (Throwable th) {
307            Log.e(TAG, "Failed to delete image: " + uri);
308        }
309    }
310
311    public static String generateFilepath(String title) {
312        return DIRECTORY + '/' + title + ".jpg";
313    }
314
315    /**
316     * Returns the jpeg bytes for a placeholder session
317     *
318     * @param uri the session uri to look up
319     * @return The jpeg bytes or null
320     */
321    public static byte[] getJpegForSession(Uri uri) {
322        return sSessionsToPlaceholderBytes.get(uri);
323    }
324
325    /**
326     * Returns the dimensions of the placeholder image
327     *
328     * @param uri the session uri to look up
329     * @return The size
330     */
331    public static Point getSizeForSession(Uri uri) {
332        return sSessionsToSizes.get(uri);
333    }
334
335    /**
336     * Takes a session URI and returns the finished image's content URI
337     *
338     * @param uri the uri of the session that was replaced
339     * @return The uri of the new media item, if it exists, or null.
340     */
341    public static Uri getContentUriForSessionUri(Uri uri) {
342        return sSessionsToContentUris.get(uri);
343    }
344
345    /**
346     * Takes a content URI and returns the original Session Uri if any
347     *
348     * @param contentUri the uri of the media store content
349     * @return The session uri of the original session, if it exists, or null.
350     */
351    public static Uri getSessionUriFromContentUri(Uri contentUri) {
352        return sContentUrisToSessions.get(contentUri);
353    }
354
355    /**
356     * Determines if a URI points to a camera session
357     *
358     * @param uri the uri to check
359     * @return true if it is a session uri.
360     */
361    public static boolean isSessionUri(Uri uri) {
362        return uri.getScheme().equals(CAMERA_SESSION_SCHEME);
363    }
364
365    public static long getAvailableSpace() {
366        String state = Environment.getExternalStorageState();
367        Log.d(TAG, "External storage state=" + state);
368        if (Environment.MEDIA_CHECKING.equals(state)) {
369            return PREPARING;
370        }
371        if (!Environment.MEDIA_MOUNTED.equals(state)) {
372            return UNAVAILABLE;
373        }
374
375        File dir = new File(DIRECTORY);
376        dir.mkdirs();
377        if (!dir.isDirectory() || !dir.canWrite()) {
378            return UNAVAILABLE;
379        }
380
381        try {
382            StatFs stat = new StatFs(DIRECTORY);
383            return stat.getAvailableBlocks() * (long) stat.getBlockSize();
384        } catch (Exception e) {
385            Log.i(TAG, "Fail to access external storage", e);
386        }
387        return UNKNOWN_SIZE;
388    }
389
390    /**
391     * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be
392     * imported. This is a temporary fix for bug#1655552.
393     */
394    public static void ensureOSXCompatible() {
395        File nnnAAAAA = new File(DCIM, "100ANDRO");
396        if (!(nnnAAAAA.exists() || nnnAAAAA.mkdirs())) {
397            Log.e(TAG, "Failed to create " + nnnAAAAA.getPath());
398        }
399    }
400
401}
402