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 java.io.File;
20import java.io.FileOutputStream;
21
22import android.annotation.TargetApi;
23import android.content.ContentResolver;
24import android.content.ContentValues;
25import android.location.Location;
26import android.net.Uri;
27import android.os.Build;
28import android.os.Environment;
29import android.os.StatFs;
30import android.provider.MediaStore.Images;
31import android.provider.MediaStore.Images.ImageColumns;
32import android.provider.MediaStore.MediaColumns;
33import android.util.Log;
34
35import com.android.camera.data.LocalData;
36import com.android.camera.exif.ExifInterface;
37import com.android.camera.util.ApiHelper;
38
39public class Storage {
40    private static final String TAG = "CameraStorage";
41
42    public static final String DCIM =
43            Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString();
44
45    public static final String DIRECTORY = DCIM + "/Camera";
46    public static final String JPEG_POSTFIX = ".jpg";
47
48    // Match the code in MediaProvider.computeBucketValues().
49    public static final String BUCKET_ID =
50            String.valueOf(DIRECTORY.toLowerCase().hashCode());
51
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
57    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
58    private static void setImageSize(ContentValues values, int width, int height) {
59        // The two fields are available since ICS but got published in JB
60        if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) {
61            values.put(MediaColumns.WIDTH, width);
62            values.put(MediaColumns.HEIGHT, height);
63        }
64    }
65
66    public static void writeFile(String path, byte[] jpeg, ExifInterface exif) {
67        if (exif != null) {
68            try {
69                exif.writeExif(jpeg, path);
70            } catch (Exception e) {
71                Log.e(TAG, "Failed to write data", e);
72            }
73        } else {
74            writeFile(path, jpeg);
75        }
76    }
77
78    public static void writeFile(String path, byte[] data) {
79        FileOutputStream out = null;
80        try {
81            out = new FileOutputStream(path);
82            out.write(data);
83        } catch (Exception e) {
84            Log.e(TAG, "Failed to write data", e);
85        } finally {
86            try {
87                out.close();
88            } catch (Exception e) {
89                Log.e(TAG, "Failed to close file after write", e);
90            }
91        }
92    }
93
94    // Save the image and add it to the MediaStore.
95    public static Uri addImage(ContentResolver resolver, String title, long date,
96            Location location, int orientation, ExifInterface exif, byte[] jpeg, int width,
97            int height) {
98
99        return addImage(resolver, title, date, location, orientation, exif, jpeg, width, height,
100                LocalData.MIME_TYPE_JPEG);
101    }
102
103    // Save the image with a given mimeType and add it the MediaStore.
104    public static Uri addImage(ContentResolver resolver, String title, long date,
105            Location location, int orientation, ExifInterface exif, byte[] jpeg, int width,
106            int height, String mimeType) {
107
108        String path = generateFilepath(title);
109        writeFile(path, jpeg, exif);
110        return addImage(resolver, title, date, location, orientation,
111                jpeg.length, path, width, height, mimeType);
112    }
113
114    // Get a ContentValues object for the given photo data
115    public static ContentValues getContentValuesForData(String title,
116            long date, Location location, int orientation, int jpegLength,
117            String path, int width, int height, String mimeType) {
118
119        ContentValues values = new ContentValues(11);
120        values.put(ImageColumns.TITLE, title);
121        values.put(ImageColumns.DISPLAY_NAME, title + JPEG_POSTFIX);
122        values.put(ImageColumns.DATE_TAKEN, date);
123        values.put(ImageColumns.MIME_TYPE, mimeType);
124        // Clockwise rotation in degrees. 0, 90, 180, or 270.
125        values.put(ImageColumns.ORIENTATION, orientation);
126        values.put(ImageColumns.DATA, path);
127        values.put(ImageColumns.SIZE, jpegLength);
128
129        setImageSize(values, width, height);
130
131        if (location != null) {
132            values.put(ImageColumns.LATITUDE, location.getLatitude());
133            values.put(ImageColumns.LONGITUDE, location.getLongitude());
134        }
135        return values;
136    }
137
138    // Add the image to media store.
139    public static Uri addImage(ContentResolver resolver, String title,
140            long date, Location location, int orientation, int jpegLength,
141            String path, int width, int height, String mimeType) {
142        // Insert into MediaStore.
143        ContentValues values =
144                getContentValuesForData(title, date, location, orientation, jpegLength, path,
145                        width, height, mimeType);
146
147         return insertImage(resolver, values);
148    }
149
150    // Overwrites the file and updates the MediaStore, or inserts the image if
151    // one does not already exist.
152    public static void updateImage(Uri imageUri, ContentResolver resolver, String title, long date,
153            Location location, int orientation, ExifInterface exif, byte[] jpeg, int width,
154            int height, String mimeType) {
155        String path = generateFilepath(title);
156        writeFile(path, jpeg, exif);
157        updateImage(imageUri, resolver, title, date, location, orientation, jpeg.length, path,
158                width, height, mimeType);
159    }
160
161    // Updates the image values in MediaStore, or inserts the image if one does
162    // not already exist.
163    public static void updateImage(Uri imageUri, ContentResolver resolver, String title,
164            long date, Location location, int orientation, int jpegLength,
165            String path, int width, int height, String mimeType) {
166
167        ContentValues values =
168                getContentValuesForData(title, date, location, orientation, jpegLength, path,
169                        width, height, mimeType);
170
171        // Update the MediaStore
172        int rowsModified = resolver.update(imageUri, values, null, null);
173
174        if (rowsModified == 0) {
175            // If no prior row existed, insert a new one.
176            Log.w(TAG, "updateImage called with no prior image at uri: " + imageUri);
177            insertImage(resolver, values);
178        } else if (rowsModified != 1) {
179            // This should never happen
180            throw new IllegalStateException("Bad number of rows (" + rowsModified
181                    + ") updated for uri: " + imageUri);
182        }
183    }
184
185    public static void deleteImage(ContentResolver resolver, Uri uri) {
186        try {
187            resolver.delete(uri, null, null);
188        } catch (Throwable th) {
189            Log.e(TAG, "Failed to delete image: " + uri);
190        }
191    }
192
193    public static String generateFilepath(String title) {
194        return DIRECTORY + '/' + title + ".jpg";
195    }
196
197    public static long getAvailableSpace() {
198        String state = Environment.getExternalStorageState();
199        Log.d(TAG, "External storage state=" + state);
200        if (Environment.MEDIA_CHECKING.equals(state)) {
201            return PREPARING;
202        }
203        if (!Environment.MEDIA_MOUNTED.equals(state)) {
204            return UNAVAILABLE;
205        }
206
207        File dir = new File(DIRECTORY);
208        dir.mkdirs();
209        if (!dir.isDirectory() || !dir.canWrite()) {
210            return UNAVAILABLE;
211        }
212
213        try {
214            StatFs stat = new StatFs(DIRECTORY);
215            return stat.getAvailableBlocks() * (long) stat.getBlockSize();
216        } catch (Exception e) {
217            Log.i(TAG, "Fail to access external storage", e);
218        }
219        return UNKNOWN_SIZE;
220    }
221
222    /**
223     * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be
224     * imported. This is a temporary fix for bug#1655552.
225     */
226    public static void ensureOSXCompatible() {
227        File nnnAAAAA = new File(DCIM, "100ANDRO");
228        if (!(nnnAAAAA.exists() || nnnAAAAA.mkdirs())) {
229            Log.e(TAG, "Failed to create " + nnnAAAAA.getPath());
230        }
231    }
232
233    private static Uri insertImage(ContentResolver resolver, ContentValues values) {
234        Uri uri = null;
235        try {
236            uri = resolver.insert(Images.Media.EXTERNAL_CONTENT_URI, values);
237        } catch (Throwable th)  {
238            // This can happen when the external volume is already mounted, but
239            // MediaScanner has not notify MediaProvider to add that volume.
240            // The picture is still safe and MediaScanner will find it and
241            // insert it into MediaProvider. The only problem is that the user
242            // cannot click the thumbnail to review the picture.
243            Log.e(TAG, "Failed to write MediaStore" + th);
244        }
245        return uri;
246    }
247}
248