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