blob: 10fcfccd8e9406f3af93c72c745d38693a9d7de7 [file] [log] [blame]
Michael Kolb8872c232013-01-29 10:33:22 -08001/*
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;
Seth Raphael455ba5a2014-02-13 15:10:06 -080022import android.graphics.Point;
Michael Kolb8872c232013-01-29 10:33:22 -080023import 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;
Michael Kolb8872c232013-01-29 10:33:22 -080031
ztenghuia16e7b52013-08-23 11:47:56 -070032import com.android.camera.data.LocalData;
Angus Kong2bca2102014-03-11 16:27:30 -070033import com.android.camera.debug.Log;
ztenghuia16e7b52013-08-23 11:47:56 -070034import com.android.camera.exif.ExifInterface;
Angus Kongb50b5cb2013-08-09 14:55:20 -070035import com.android.camera.util.ApiHelper;
Sascha Haeberling7cb8c792014-03-11 09:14:53 -070036import com.android.camera.util.ImageLoader;
Michael Kolb8872c232013-01-29 10:33:22 -080037
Sascha Haeberlinga63dbb62013-11-22 11:55:32 -080038import java.io.File;
39import java.io.FileOutputStream;
Seth Raphael455ba5a2014-02-13 15:10:06 -080040import java.util.HashMap;
41import java.util.UUID;
Sascha Haeberlinga63dbb62013-11-22 11:55:32 -080042
Michael Kolb8872c232013-01-29 10:33:22 -080043public class Storage {
Michael Kolb8872c232013-01-29 10:33:22 -080044 public static final String DCIM =
45 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString();
Michael Kolb8872c232013-01-29 10:33:22 -080046 public static final String DIRECTORY = DCIM + "/Camera";
Ruben Brunk7cfcafd2013-10-17 15:41:44 -070047 public static final String JPEG_POSTFIX = ".jpg";
Michael Kolb8872c232013-01-29 10:33:22 -080048 // Match the code in MediaProvider.computeBucketValues().
49 public static final String BUCKET_ID =
50 String.valueOf(DIRECTORY.toLowerCase().hashCode());
Michael Kolb8872c232013-01-29 10:33:22 -080051 public static final long UNAVAILABLE = -1L;
52 public static final long PREPARING = -2L;
53 public static final long UNKNOWN_SIZE = -3L;
Angus Kong2dcc0a92013-09-25 13:00:08 -070054 public static final long LOW_STORAGE_THRESHOLD_BYTES = 50000000;
Seth Raphael455ba5a2014-02-13 15:10:06 -080055 public static final String CAMERA_SESSION_SCHEME = "camera_session";
Angus Kong2bca2102014-03-11 16:27:30 -070056 private static final Log.Tag TAG = new Log.Tag("Storage");
Seth Raphael455ba5a2014-02-13 15:10:06 -080057 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>();
Michael Kolb8872c232013-01-29 10:33:22 -080061
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
Ruben Brunk7cfcafd2013-10-17 15:41:44 -070071 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
Michael Kolb8872c232013-01-29 10:33:22 -080083 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) {
Ruben Brunk7cfcafd2013-10-17 15:41:44 -070094 Log.e(TAG, "Failed to close file after write", e);
Michael Kolb8872c232013-01-29 10:33:22 -080095 }
96 }
97 }
98
Ruben Brunk7cfcafd2013-10-17 15:41:44 -070099 // 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
Michael Kolb8872c232013-01-29 10:33:22 -0800113 String path = generateFilepath(title);
Ruben Brunk7cfcafd2013-10-17 15:41:44 -0700114 writeFile(path, jpeg, exif);
Michael Kolb8872c232013-01-29 10:33:22 -0800115 return addImage(resolver, title, date, location, orientation,
Ruben Brunk7cfcafd2013-10-17 15:41:44 -0700116 jpeg.length, path, width, height, mimeType);
Michael Kolb8872c232013-01-29 10:33:22 -0800117 }
118
Ruben Brunk7cfcafd2013-10-17 15:41:44 -0700119 // Get a ContentValues object for the given photo data
120 public static ContentValues getContentValuesForData(String title,
Michael Kolb8872c232013-01-29 10:33:22 -0800121 long date, Location location, int orientation, int jpegLength,
Ruben Brunk7cfcafd2013-10-17 15:41:44 -0700122 String path, int width, int height, String mimeType) {
123
124 ContentValues values = new ContentValues(11);
Michael Kolb8872c232013-01-29 10:33:22 -0800125 values.put(ImageColumns.TITLE, title);
Ruben Brunk7cfcafd2013-10-17 15:41:44 -0700126 values.put(ImageColumns.DISPLAY_NAME, title + JPEG_POSTFIX);
Michael Kolb8872c232013-01-29 10:33:22 -0800127 values.put(ImageColumns.DATE_TAKEN, date);
Ruben Brunk7cfcafd2013-10-17 15:41:44 -0700128 values.put(ImageColumns.MIME_TYPE, mimeType);
Michael Kolb8872c232013-01-29 10:33:22 -0800129 // 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 }
Ruben Brunk7cfcafd2013-10-17 15:41:44 -0700140 return values;
141 }
142
Seth Raphael455ba5a2014-02-13 15:10:06 -0800143 /**
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
Ruben Brunk7cfcafd2013-10-17 15:41:44 -0700175 // 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);
Michael Kolb8872c232013-01-29 10:33:22 -0800183
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
Ruben Brunk7cfcafd2013-10-17 15:41:44 -0700198 // Overwrites the file and updates the MediaStore
Seth Raphael455ba5a2014-02-13 15:10:06 -0800199
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) {
Ruben Brunk7cfcafd2013-10-17 15:41:44 -0700219 String path = generateFilepath(title);
220 writeFile(path, jpeg, exif);
Seth Raphael455ba5a2014-02-13 15:10:06 -0800221 return updateImage(imageUri, resolver, title, date, location, orientation, jpeg.length, path,
Ruben Brunk7cfcafd2013-10-17 15:41:44 -0700222 width, height, mimeType);
223 }
224
Seth Raphael455ba5a2014-02-13 15:10:06 -0800225
Ruben Brunk7cfcafd2013-10-17 15:41:44 -0700226 // Updates the image values in MediaStore
Seth Raphael455ba5a2014-02-13 15:10:06 -0800227 private static Uri updateImage(Uri imageUri, ContentResolver resolver, String title,
Ruben Brunk7cfcafd2013-10-17 15:41:44 -0700228 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
Seth Raphael455ba5a2014-02-13 15:10:06 -0800235
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 int rowsModified = resolver.update(imageUri, values, null, null);
245 if (rowsModified != 1) {
246 // This should never happen
247 throw new IllegalStateException("Bad number of rows (" + rowsModified
248 + ") updated for uri: " + imageUri);
249 }
Ruben Brunk7cfcafd2013-10-17 15:41:44 -0700250 }
Seth Raphael455ba5a2014-02-13 15:10:06 -0800251 return resultUri;
Ruben Brunk7cfcafd2013-10-17 15:41:44 -0700252 }
253
Sascha Haeberlinga63dbb62013-11-22 11:55:32 -0800254 /**
255 * Update the image from the file that has changed.
256 * <p>
257 * Note: This will update the DATE_TAKEN to right now. We could consider not
258 * changing it to preserve the original timestamp.
Sascha Haeberlinga63dbb62013-11-22 11:55:32 -0800259 */
Sascha Haeberling93be42a2014-03-05 16:03:36 -0800260 public static void updateImageFromChangedFile(Uri mediaUri, Location location,
261 ContentResolver resolver, String mimeType) {
Sascha Haeberlinga63dbb62013-11-22 11:55:32 -0800262 File mediaFile = new File(ImageLoader.getLocalPathFromUri(resolver, mediaUri));
263 if (!mediaFile.exists()) {
264 throw new IllegalArgumentException("Provided URI is not an existent file: "
265 + mediaUri.getPath());
266 }
267
268 ContentValues values = new ContentValues();
269 // TODO: Read the date from file.
270 values.put(Images.Media.DATE_TAKEN, System.currentTimeMillis());
Sascha Haeberling14ff6c82013-12-13 13:29:58 -0800271 values.put(Images.Media.MIME_TYPE, mimeType);
Sascha Haeberlinga63dbb62013-11-22 11:55:32 -0800272 values.put(Images.Media.SIZE, mediaFile.length());
Sascha Haeberling93be42a2014-03-05 16:03:36 -0800273 if (location != null) {
274 values.put(ImageColumns.LATITUDE, location.getLatitude());
275 values.put(ImageColumns.LONGITUDE, location.getLongitude());
276 }
Sascha Haeberlinga63dbb62013-11-22 11:55:32 -0800277
278 resolver.update(mediaUri, values, null, null);
279 }
280
281 /**
282 * Updates the item's mime type to the given one. This is useful e.g. when
283 * switching an image to an in-progress type for re-processing.
284 *
285 * @param uri the URI of the item to change
Seth Raphael455ba5a2014-02-13 15:10:06 -0800286 * @param mimeType the new mime type of the item
Sascha Haeberlinga63dbb62013-11-22 11:55:32 -0800287 */
288 public static void updateItemMimeType(Uri uri, String mimeType, ContentResolver resolver) {
289 ContentValues values = new ContentValues(1);
290 values.put(ImageColumns.MIME_TYPE, mimeType);
291
292 // Update the MediaStore
293 int rowsModified = resolver.update(uri, values, null, null);
294 if (rowsModified != 1) {
295 // This should never happen
296 throw new IllegalStateException("Bad number of rows (" + rowsModified
297 + ") updated for uri: " + uri);
298 }
299 }
300
Michael Kolb8872c232013-01-29 10:33:22 -0800301 public static void deleteImage(ContentResolver resolver, Uri uri) {
302 try {
303 resolver.delete(uri, null, null);
304 } catch (Throwable th) {
305 Log.e(TAG, "Failed to delete image: " + uri);
306 }
307 }
308
309 public static String generateFilepath(String title) {
310 return DIRECTORY + '/' + title + ".jpg";
311 }
312
Seth Raphael455ba5a2014-02-13 15:10:06 -0800313 /**
314 * Returns the jpeg bytes for a placeholder session
315 *
316 * @param uri the session uri to look up
317 * @return The jpeg bytes or null
318 */
319 public static byte[] getJpegForSession(Uri uri) {
320 return sSessionsToPlaceholderBytes.get(uri);
321 }
322
323 /**
324 * Returns the dimensions of the placeholder image
325 *
326 * @param uri the session uri to look up
327 * @return The size
328 */
329 public static Point getSizeForSession(Uri uri) {
330 return sSessionsToSizes.get(uri);
331 }
332
333 /**
334 * Takes a session URI and returns the finished image's content URI
335 *
336 * @param uri the uri of the session that was replaced
337 * @return The uri of the new media item, if it exists, or null.
338 */
339 public static Uri getContentUriForSessionUri(Uri uri) {
340 return sSessionsToContentUris.get(uri);
341 }
342
343 /**
344 * Determines if a URI points to a camera session
345 *
346 * @param uri the uri to check
347 * @return true if it is a session uri.
348 */
349 public static boolean isSessionUri(Uri uri) {
350 return uri.getScheme().equals(CAMERA_SESSION_SCHEME);
351 }
352
Michael Kolb8872c232013-01-29 10:33:22 -0800353 public static long getAvailableSpace() {
354 String state = Environment.getExternalStorageState();
355 Log.d(TAG, "External storage state=" + state);
356 if (Environment.MEDIA_CHECKING.equals(state)) {
357 return PREPARING;
358 }
359 if (!Environment.MEDIA_MOUNTED.equals(state)) {
360 return UNAVAILABLE;
361 }
362
363 File dir = new File(DIRECTORY);
364 dir.mkdirs();
365 if (!dir.isDirectory() || !dir.canWrite()) {
366 return UNAVAILABLE;
367 }
368
369 try {
370 StatFs stat = new StatFs(DIRECTORY);
371 return stat.getAvailableBlocks() * (long) stat.getBlockSize();
372 } catch (Exception e) {
373 Log.i(TAG, "Fail to access external storage", e);
374 }
375 return UNKNOWN_SIZE;
376 }
377
378 /**
379 * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be
380 * imported. This is a temporary fix for bug#1655552.
381 */
382 public static void ensureOSXCompatible() {
383 File nnnAAAAA = new File(DCIM, "100ANDRO");
384 if (!(nnnAAAAA.exists() || nnnAAAAA.mkdirs())) {
385 Log.e(TAG, "Failed to create " + nnnAAAAA.getPath());
386 }
387 }
Seth Raphael455ba5a2014-02-13 15:10:06 -0800388
Michael Kolb8872c232013-01-29 10:33:22 -0800389}