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    // 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 fo 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 fo 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    private 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);
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 fo 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    private 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);
242        writeFile(path, jpeg, exif);
243        return updateImage(imageUri, resolver, title, date, location, orientation, jpeg.length, path,
244                width, height, mimeType);
245    }
246
247    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
248    private static void setImageSize(ContentValues values, int width, int height) {
249        // The two fields are available since ICS but got published in JB
250        if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) {
251            values.put(MediaColumns.WIDTH, width);
252            values.put(MediaColumns.HEIGHT, height);
253        }
254    }
255
256    /**
257     * Writes the JPEG data to a file. If there's EXIF info, the EXIF header
258     * will be added.
259     *
260     * @param path The path to the target file.
261     * @param jpeg The JPEG data.
262     * @param exif The EXIF info. Can be {@code null}.
263     *
264     * @return The size of the file. -1 if failed.
265     */
266    private static long writeFile(String path, byte[] jpeg, ExifInterface exif) {
267        if (exif != null) {
268            try {
269                exif.writeExif(jpeg, path);
270                File f = new File(path);
271                return f.length();
272            } catch (Exception e) {
273                Log.e(TAG, "Failed to write data", e);
274            }
275        } else {
276            return writeFile(path, jpeg);
277        }
278        return -1;
279    }
280
281    /**
282     * Writes the data to a file.
283     *
284     * @param path The path to the target file.
285     * @param data The data to save.
286     *
287     * @return The size of the file. -1 if failed.
288     */
289    private static long writeFile(String path, byte[] data) {
290        FileOutputStream out = null;
291        try {
292            out = new FileOutputStream(path);
293            out.write(data);
294            return data.length;
295        } catch (Exception e) {
296            Log.e(TAG, "Failed to write data", e);
297        } finally {
298            try {
299                out.close();
300            } catch (Exception e) {
301                Log.e(TAG, "Failed to close file after write", e);
302            }
303        }
304        return -1;
305    }
306
307    // Updates the image values in MediaStore
308    private static Uri updateImage(Uri imageUri, ContentResolver resolver, String title,
309            long date, Location location, int orientation, int jpegLength,
310            String path, int width, int height, String mimeType) {
311
312        ContentValues values =
313                getContentValuesForData(title, date, location, orientation, jpegLength, path,
314                        width, height, mimeType);
315
316
317        Uri resultUri = imageUri;
318        if (Storage.isSessionUri(imageUri)) {
319            // If this is a session uri, then we need to add the image
320            resultUri = addImageToMediaStore(resolver, title, date, location, orientation,
321                    jpegLength, path, width, height, mimeType);
322            sSessionsToContentUris.put(imageUri, resultUri);
323            sContentUrisToSessions.put(resultUri, imageUri);
324        } else {
325            // Update the MediaStore
326            resolver.update(imageUri, values, null, null);
327        }
328        return resultUri;
329    }
330
331    private static String generateFilepath(String title) {
332        return DIRECTORY + '/' + title + ".jpg";
333    }
334
335    /**
336     * Returns the jpeg bytes for a placeholder session
337     *
338     * @param uri the session uri to look up
339     * @return The jpeg bytes or null
340     */
341    public static byte[] getJpegForSession(Uri uri) {
342        return sSessionsToPlaceholderBytes.get(uri);
343    }
344
345    /**
346     * Returns the current version of a placeholder for a session. The version will increment
347     * with each call to replacePlaceholder.
348     *
349     * @param uri the session uri to look up.
350     * @return the current version int.
351     */
352    public static int getJpegVersionForSession(Uri uri) {
353        return sSessionsToPlaceholderVersions.get(uri);
354    }
355
356    /**
357     * Returns the dimensions of the placeholder image
358     *
359     * @param uri the session uri to look up
360     * @return The size
361     */
362    public static Point getSizeForSession(Uri uri) {
363        return sSessionsToSizes.get(uri);
364    }
365
366    /**
367     * Takes a session URI and returns the finished image's content URI
368     *
369     * @param uri the uri of the session that was replaced
370     * @return The uri of the new media item, if it exists, or null.
371     */
372    public static Uri getContentUriForSessionUri(Uri uri) {
373        return sSessionsToContentUris.get(uri);
374    }
375
376    /**
377     * Takes a content URI and returns the original Session Uri if any
378     *
379     * @param contentUri the uri of the media store content
380     * @return The session uri of the original session, if it exists, or null.
381     */
382    public static Uri getSessionUriFromContentUri(Uri contentUri) {
383        return sContentUrisToSessions.get(contentUri);
384    }
385
386    /**
387     * Determines if a URI points to a camera session
388     *
389     * @param uri the uri to check
390     * @return true if it is a session uri.
391     */
392    public static boolean isSessionUri(Uri uri) {
393        return uri.getScheme().equals(CAMERA_SESSION_SCHEME);
394    }
395
396    public static long getAvailableSpace() {
397        String state = Environment.getExternalStorageState();
398        Log.d(TAG, "External storage state=" + state);
399        if (Environment.MEDIA_CHECKING.equals(state)) {
400            return PREPARING;
401        }
402        if (!Environment.MEDIA_MOUNTED.equals(state)) {
403            return UNAVAILABLE;
404        }
405
406        File dir = new File(DIRECTORY);
407        dir.mkdirs();
408        if (!dir.isDirectory() || !dir.canWrite()) {
409            return UNAVAILABLE;
410        }
411
412        try {
413            StatFs stat = new StatFs(DIRECTORY);
414            return stat.getAvailableBlocks() * (long) stat.getBlockSize();
415        } catch (Exception e) {
416            Log.i(TAG, "Fail to access external storage", e);
417        }
418        return UNKNOWN_SIZE;
419    }
420
421    /**
422     * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be
423     * imported. This is a temporary fix for bug#1655552.
424     */
425    public static void ensureOSXCompatible() {
426        File nnnAAAAA = new File(DCIM, "100ANDRO");
427        if (!(nnnAAAAA.exists() || nnnAAAAA.mkdirs())) {
428            Log.e(TAG, "Failed to create " + nnnAAAAA.getPath());
429        }
430    }
431
432}
433