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.Bitmap;
22import android.graphics.Point;
23import android.location.Location;
24import android.net.Uri;
25import android.os.Environment;
26import android.os.StatFs;
27import android.provider.MediaStore.Images;
28import android.provider.MediaStore.Images.ImageColumns;
29import android.provider.MediaStore.MediaColumns;
30import android.util.LruCache;
31
32import com.android.camera.data.FilmstripItemData;
33import com.android.camera.debug.Log;
34import com.android.camera.exif.ExifInterface;
35import com.android.camera.util.ApiHelper;
36import com.android.camera.util.Size;
37import com.google.common.base.Optional;
38
39import java.io.File;
40import java.io.FileOutputStream;
41import java.io.IOException;
42import java.util.HashMap;
43import java.util.UUID;
44import java.util.concurrent.TimeUnit;
45
46import javax.annotation.Nonnull;
47
48public class Storage {
49    public static final String DCIM =
50            Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString();
51    public static final String DIRECTORY = DCIM + "/Camera";
52    public static final File DIRECTORY_FILE = new File(DIRECTORY);
53    public static final String JPEG_POSTFIX = ".jpg";
54    public static final String GIF_POSTFIX = ".gif";
55    public static final long UNAVAILABLE = -1L;
56    public static final long PREPARING = -2L;
57    public static final long UNKNOWN_SIZE = -3L;
58    public static final long ACCESS_FAILURE = -4L;
59    public static final long LOW_STORAGE_THRESHOLD_BYTES = 50000000;
60    public static final String CAMERA_SESSION_SCHEME = "camera_session";
61    private static final Log.Tag TAG = new Log.Tag("Storage");
62    private static final String GOOGLE_COM = "google.com";
63    private static HashMap<Uri, Uri> sSessionsToContentUris = new HashMap<>();
64    private static HashMap<Uri, Uri> sContentUrisToSessions = new HashMap<>();
65    private static LruCache<Uri, Bitmap> sSessionsToPlaceholderBitmap =
66            // 20MB cache as an upper bound for session bitmap storage
67            new LruCache<Uri, Bitmap>(20 * 1024 * 1024) {
68                @Override
69                protected int sizeOf(Uri key, Bitmap value) {
70                    return value.getByteCount();
71                }
72            };
73    private static HashMap<Uri, Point> sSessionsToSizes = new HashMap<>();
74    private static HashMap<Uri, Integer> sSessionsToPlaceholderVersions = new HashMap<>();
75
76    /**
77     * Save the image with default JPEG MIME type and add it to the MediaStore.
78     *
79     * @param resolver The The content resolver to use.
80     * @param title The title of the media file.
81     * @param date The date for the media file.
82     * @param location The location of the media file.
83     * @param orientation The orientation of the media file.
84     * @param exif The EXIF info. Can be {@code null}.
85     * @param jpeg The JPEG data.
86     * @param width The width of the media file after the orientation is
87     *              applied.
88     * @param height The height of the media file after the orientation is
89     *               applied.
90     */
91    public static Uri addImage(ContentResolver resolver, String title, long date,
92            Location location, int orientation, ExifInterface exif, byte[] jpeg, int width,
93            int height) throws IOException {
94
95        return addImage(resolver, title, date, location, orientation, exif, jpeg, width, height,
96              FilmstripItemData.MIME_TYPE_JPEG);
97    }
98
99    /**
100     * Saves the media with a given MIME type and adds it to the MediaStore.
101     * <p>
102     * The path will be automatically generated according to the title.
103     * </p>
104     *
105     * @param resolver The The content resolver to use.
106     * @param title The title of the media file.
107     * @param data The data to save.
108     * @param date The date for the media file.
109     * @param location The location of the media file.
110     * @param orientation The orientation of the media file.
111     * @param exif The EXIF info. Can be {@code null}.
112     * @param width The width of the media file after the orientation is
113     *            applied.
114     * @param height The height of the media file after the orientation is
115     *            applied.
116     * @param mimeType The MIME type of the data.
117     * @return The URI of the added image, or null if the image could not be
118     *         added.
119     */
120    public static Uri addImage(ContentResolver resolver, String title, long date,
121            Location location, int orientation, ExifInterface exif, byte[] data, int width,
122            int height, String mimeType) throws IOException {
123
124        String path = generateFilepath(title, mimeType);
125        long fileLength = writeFile(path, data, exif);
126        if (fileLength >= 0) {
127            return addImageToMediaStore(resolver, title, date, location, orientation, fileLength,
128                    path, width, height, mimeType);
129        }
130        return null;
131    }
132
133    /**
134     * Add the entry for the media file to media store.
135     *
136     * @param resolver The The content resolver to use.
137     * @param title The title of the media file.
138     * @param date The date for the media file.
139     * @param location The location of the media file.
140     * @param orientation The orientation of the media file.
141     * @param width The width of the media file after the orientation is
142     *            applied.
143     * @param height The height of the media file after the orientation is
144     *            applied.
145     * @param mimeType The MIME type of the data.
146     * @return The content URI of the inserted media file or null, if the image
147     *         could not be added.
148     */
149    public static Uri addImageToMediaStore(ContentResolver resolver, String title, long date,
150            Location location, int orientation, long jpegLength, String path, int width, int height,
151            String mimeType) {
152        // Insert into MediaStore.
153        ContentValues values =
154                getContentValuesForData(title, date, location, orientation, jpegLength, path, width,
155                        height, mimeType);
156
157        Uri uri = null;
158        try {
159            uri = resolver.insert(Images.Media.EXTERNAL_CONTENT_URI, values);
160        } catch (Throwable th)  {
161            // This can happen when the external volume is already mounted, but
162            // MediaScanner has not notify MediaProvider to add that volume.
163            // The picture is still safe and MediaScanner will find it and
164            // insert it into MediaProvider. The only problem is that the user
165            // cannot click the thumbnail to review the picture.
166            Log.e(TAG, "Failed to write MediaStore" + th);
167        }
168        return uri;
169    }
170
171    // Get a ContentValues object for the given photo data
172    public static ContentValues getContentValuesForData(String title,
173            long date, Location location, int orientation, long jpegLength,
174            String path, int width, int height, String mimeType) {
175
176        File file = new File(path);
177        long dateModifiedSeconds = TimeUnit.MILLISECONDS.toSeconds(file.lastModified());
178
179        ContentValues values = new ContentValues(11);
180        values.put(ImageColumns.TITLE, title);
181        values.put(ImageColumns.DISPLAY_NAME, title + JPEG_POSTFIX);
182        values.put(ImageColumns.DATE_TAKEN, date);
183        values.put(ImageColumns.MIME_TYPE, mimeType);
184        values.put(ImageColumns.DATE_MODIFIED, dateModifiedSeconds);
185        // Clockwise rotation in degrees. 0, 90, 180, or 270.
186        values.put(ImageColumns.ORIENTATION, orientation);
187        values.put(ImageColumns.DATA, path);
188        values.put(ImageColumns.SIZE, jpegLength);
189
190        setImageSize(values, width, height);
191
192        if (location != null) {
193            values.put(ImageColumns.LATITUDE, location.getLatitude());
194            values.put(ImageColumns.LONGITUDE, location.getLongitude());
195        }
196        return values;
197    }
198
199    /**
200     * Add a placeholder for a new image that does not exist yet.
201     *
202     * @param placeholder the placeholder image
203     * @return A new URI used to reference this placeholder
204     */
205    public static Uri addPlaceholder(Bitmap placeholder) {
206        Uri uri = generateUniquePlaceholderUri();
207        replacePlaceholder(uri, placeholder);
208        return uri;
209    }
210
211    /**
212     * Remove a placeholder from in memory storage.
213     */
214    public static void removePlaceholder(Uri uri) {
215        sSessionsToSizes.remove(uri);
216        sSessionsToPlaceholderBitmap.remove(uri);
217        sSessionsToPlaceholderVersions.remove(uri);
218    }
219
220    /**
221     * Add or replace placeholder for a new image that does not exist yet.
222     *
223     * @param uri the uri of the placeholder to replace, or null if this is a
224     *            new one
225     * @param placeholder the placeholder image
226     * @return A URI used to reference this placeholder
227     */
228    public static void replacePlaceholder(Uri uri, Bitmap placeholder) {
229        Log.v(TAG, "session bitmap cache size: " + sSessionsToPlaceholderBitmap.size());
230        Point size = new Point(placeholder.getWidth(), placeholder.getHeight());
231        sSessionsToSizes.put(uri, size);
232        sSessionsToPlaceholderBitmap.put(uri, placeholder);
233        Integer currentVersion = sSessionsToPlaceholderVersions.get(uri);
234        sSessionsToPlaceholderVersions.put(uri, currentVersion == null ? 0 : currentVersion + 1);
235    }
236
237    /**
238     * Creates an empty placeholder.
239     *
240     * @param size the size of the placeholder in pixels.
241     * @return A new URI used to reference this placeholder
242     */
243    @Nonnull
244    public static Uri addEmptyPlaceholder(@Nonnull Size size) {
245        Uri uri = generateUniquePlaceholderUri();
246        sSessionsToSizes.put(uri, new Point(size.getWidth(), size.getHeight()));
247        sSessionsToPlaceholderBitmap.remove(uri);
248        Integer currentVersion = sSessionsToPlaceholderVersions.get(uri);
249        sSessionsToPlaceholderVersions.put(uri, currentVersion == null ? 0 : currentVersion + 1);
250        return uri;
251    }
252
253    /**
254     * Take jpeg bytes and add them to the media store, either replacing an existing item
255     * or a placeholder uri to replace
256     * @param imageUri The content uri or session uri of the image being updated
257     * @param resolver The content resolver to use
258     * @param title of the image
259     * @param date of the image
260     * @param location of the image
261     * @param orientation of the image
262     * @param exif of the image
263     * @param jpeg bytes of the image
264     * @param width of the image
265     * @param height of the image
266     * @param mimeType of the image
267     * @return The content uri of the newly inserted or replaced item.
268     */
269    public static Uri updateImage(Uri imageUri, ContentResolver resolver, String title, long date,
270           Location location, int orientation, ExifInterface exif,
271           byte[] jpeg, int width, int height, String mimeType) throws IOException {
272        String path = generateFilepath(title, mimeType);
273        writeFile(path, jpeg, exif);
274        return updateImage(imageUri, resolver, title, date, location, orientation, jpeg.length, path,
275                width, height, mimeType);
276    }
277
278    private static Uri generateUniquePlaceholderUri() {
279        Uri.Builder builder = new Uri.Builder();
280        String uuid = UUID.randomUUID().toString();
281        builder.scheme(CAMERA_SESSION_SCHEME).authority(GOOGLE_COM).appendPath(uuid);
282        return builder.build();
283    }
284
285    private static void setImageSize(ContentValues values, int width, int height) {
286        // The two fields are available since ICS but got published in JB
287        if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) {
288            values.put(MediaColumns.WIDTH, width);
289            values.put(MediaColumns.HEIGHT, height);
290        }
291    }
292
293    /**
294     * Writes the JPEG data to a file. If there's EXIF info, the EXIF header
295     * will be added.
296     *
297     * @param path The path to the target file.
298     * @param jpeg The JPEG data.
299     * @param exif The EXIF info. Can be {@code null}.
300     *
301     * @return The size of the file. -1 if failed.
302     */
303    public static long writeFile(String path, byte[] jpeg, ExifInterface exif) throws IOException {
304        if (!createDirectoryIfNeeded(path)) {
305            Log.e(TAG, "Failed to create parent directory for file: " + path);
306            return -1;
307        }
308        if (exif != null) {
309                exif.writeExif(jpeg, path);
310                File f = new File(path);
311                return f.length();
312        } else {
313            return writeFile(path, jpeg);
314        }
315//        return -1;
316    }
317
318    /**
319     * Renames a file.
320     *
321     * <p/>
322     * Can only be used for regular files, not directories.
323     *
324     * @param inputPath the original path of the file
325     * @param newFilePath the new path of the file
326     * @return false if rename was not successful
327     */
328    public static boolean renameFile(File inputPath, File newFilePath) {
329        if (newFilePath.exists()) {
330            Log.e(TAG, "File path already exists: " + newFilePath.getAbsolutePath());
331            return false;
332        }
333        if (inputPath.isDirectory()) {
334            Log.e(TAG, "Input path is directory: " + inputPath.getAbsolutePath());
335            return false;
336        }
337        if (!createDirectoryIfNeeded(newFilePath.getAbsolutePath())) {
338            Log.e(TAG, "Failed to create parent directory for file: " +
339                    newFilePath.getAbsolutePath());
340            return false;
341        }
342        return inputPath.renameTo(newFilePath);
343    }
344
345    /**
346     * Writes the data to a file.
347     *
348     * @param path The path to the target file.
349     * @param data The data to save.
350     *
351     * @return The size of the file. -1 if failed.
352     */
353    private static long writeFile(String path, byte[] data) {
354        FileOutputStream out = null;
355        try {
356            out = new FileOutputStream(path);
357            out.write(data);
358            return data.length;
359        } catch (Exception e) {
360            Log.e(TAG, "Failed to write data", e);
361        } finally {
362            try {
363                out.close();
364            } catch (Exception e) {
365                Log.e(TAG, "Failed to close file after write", e);
366            }
367        }
368        return -1;
369    }
370
371    /**
372     * Given a file path, makes sure the directory it's in exists, and if not
373     * that it is created.
374     *
375     * @param filePath the absolute path of a file, e.g. '/foo/bar/file.jpg'.
376     * @return Whether the directory exists. If 'false' is returned, this file
377     *         cannot be written to since the parent directory could not be
378     *         created.
379     */
380    private static boolean createDirectoryIfNeeded(String filePath) {
381        File parentFile = new File(filePath).getParentFile();
382
383        // If the parent exists, return 'true' if it is a directory. If it's a
384        // file, return 'false'.
385        if (parentFile.exists()) {
386            return parentFile.isDirectory();
387        }
388
389        // If the parent does not exists, attempt to create it and return
390        // whether creating it succeeded.
391        return parentFile.mkdirs();
392    }
393
394    /** Updates the image values in MediaStore. */
395    private static Uri updateImage(Uri imageUri, ContentResolver resolver, String title,
396            long date, Location location, int orientation, int jpegLength,
397            String path, int width, int height, String mimeType) {
398
399        ContentValues values =
400                getContentValuesForData(title, date, location, orientation, jpegLength, path,
401                        width, height, mimeType);
402
403
404        Uri resultUri = imageUri;
405        if (Storage.isSessionUri(imageUri)) {
406            // If this is a session uri, then we need to add the image
407            resultUri = addImageToMediaStore(resolver, title, date, location, orientation,
408                    jpegLength, path, width, height, mimeType);
409            sSessionsToContentUris.put(imageUri, resultUri);
410            sContentUrisToSessions.put(resultUri, imageUri);
411        } else {
412            // Update the MediaStore
413            resolver.update(imageUri, values, null, null);
414        }
415        return resultUri;
416    }
417
418    private static String generateFilepath(String title, String mimeType) {
419        return generateFilepath(DIRECTORY, title, mimeType);
420    }
421
422    public static String generateFilepath(String directory, String title, String mimeType) {
423        String extension = null;
424        if (FilmstripItemData.MIME_TYPE_JPEG.equals(mimeType)) {
425            extension = JPEG_POSTFIX;
426        } else if (FilmstripItemData.MIME_TYPE_GIF.equals(mimeType)) {
427            extension = GIF_POSTFIX;
428        } else {
429            throw new IllegalArgumentException("Invalid mimeType: " + mimeType);
430        }
431        return (new File(directory, title + extension)).getAbsolutePath();
432    }
433
434    /**
435     * Returns the jpeg bytes for a placeholder session
436     *
437     * @param uri the session uri to look up
438     * @return The bitmap or null
439     */
440    public static Optional<Bitmap> getPlaceholderForSession(Uri uri) {
441        return Optional.fromNullable(sSessionsToPlaceholderBitmap.get(uri));
442    }
443
444    /**
445     * @return Whether a placeholder size for the session with the given URI
446     *         exists.
447     */
448    public static boolean containsPlaceholderSize(Uri uri) {
449        return sSessionsToSizes.containsKey(uri);
450    }
451
452    /**
453     * Returns the dimensions of the placeholder image
454     *
455     * @param uri the session uri to look up
456     * @return The size
457     */
458    public static Point getSizeForSession(Uri uri) {
459        return sSessionsToSizes.get(uri);
460    }
461
462    /**
463     * Takes a session URI and returns the finished image's content URI
464     *
465     * @param uri the uri of the session that was replaced
466     * @return The uri of the new media item, if it exists, or null.
467     */
468    public static Uri getContentUriForSessionUri(Uri uri) {
469        return sSessionsToContentUris.get(uri);
470    }
471
472    /**
473     * Takes a content URI and returns the original Session Uri if any
474     *
475     * @param contentUri the uri of the media store content
476     * @return The session uri of the original session, if it exists, or null.
477     */
478    public static Uri getSessionUriFromContentUri(Uri contentUri) {
479        return sContentUrisToSessions.get(contentUri);
480    }
481
482    /**
483     * Determines if a URI points to a camera session
484     *
485     * @param uri the uri to check
486     * @return true if it is a session uri.
487     */
488    public static boolean isSessionUri(Uri uri) {
489        return uri.getScheme().equals(CAMERA_SESSION_SCHEME);
490    }
491
492    public static long getAvailableSpace() {
493        String state = Environment.getExternalStorageState();
494        Log.d(TAG, "External storage state=" + state);
495        if (Environment.MEDIA_CHECKING.equals(state)) {
496            return PREPARING;
497        }
498        if (!Environment.MEDIA_MOUNTED.equals(state)) {
499            return UNAVAILABLE;
500        }
501
502        File dir = new File(DIRECTORY);
503        dir.mkdirs();
504        if (!dir.isDirectory() || !dir.canWrite()) {
505            return UNAVAILABLE;
506        }
507
508        try {
509            StatFs stat = new StatFs(DIRECTORY);
510            return stat.getAvailableBlocks() * (long) stat.getBlockSize();
511        } catch (Exception e) {
512            Log.i(TAG, "Fail to access external storage", e);
513        }
514        return UNKNOWN_SIZE;
515    }
516
517    /**
518     * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be
519     * imported. This is a temporary fix for bug#1655552.
520     */
521    public static void ensureOSXCompatible() {
522        File nnnAAAAA = new File(DCIM, "100ANDRO");
523        if (!(nnnAAAAA.exists() || nnnAAAAA.mkdirs())) {
524            Log.e(TAG, "Failed to create " + nnnAAAAA.getPath());
525        }
526    }
527
528}
529