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 17 package com.android.camera; 18 19 import android.annotation.TargetApi; 20 import android.content.ContentResolver; 21 import android.content.ContentValues; 22 import android.location.Location; 23 import android.net.Uri; 24 import android.os.Build; 25 import android.os.Environment; 26 import android.os.StatFs; 27 import android.provider.MediaStore.Images; 28 import android.provider.MediaStore.Images.ImageColumns; 29 import android.provider.MediaStore.MediaColumns; 30 import android.util.Log; 31 32 import com.android.gallery3d.common.ApiHelper; 33 34 import java.io.File; 35 import java.io.FileOutputStream; 36 37 public class Storage { 38 private static final String TAG = "CameraStorage"; 39 40 public static final String DCIM = 41 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString(); 42 43 public static final String DIRECTORY = DCIM + "/Camera"; 44 45 // Match the code in MediaProvider.computeBucketValues(). 46 public static final String BUCKET_ID = 47 String.valueOf(DIRECTORY.toLowerCase().hashCode()); 48 49 public static final long UNAVAILABLE = -1L; 50 public static final long PREPARING = -2L; 51 public static final long UNKNOWN_SIZE = -3L; 52 public static final long LOW_STORAGE_THRESHOLD= 50000000; 53 54 @TargetApi(Build.VERSION_CODES.JELLY_BEAN) setImageSize(ContentValues values, int width, int height)55 private static void setImageSize(ContentValues values, int width, int height) { 56 // The two fields are available since ICS but got published in JB 57 if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) { 58 values.put(MediaColumns.WIDTH, width); 59 values.put(MediaColumns.HEIGHT, height); 60 } 61 } 62 writeFile(String path, byte[] data)63 public static void writeFile(String path, byte[] data) { 64 FileOutputStream out = null; 65 try { 66 out = new FileOutputStream(path); 67 out.write(data); 68 } catch (Exception e) { 69 Log.e(TAG, "Failed to write data", e); 70 } finally { 71 try { 72 out.close(); 73 } catch (Exception e) { 74 } 75 } 76 } 77 78 // Save the image and add it to media store. addImage(ContentResolver resolver, String title, long date, Location location, int orientation, byte[] jpeg, int width, int height)79 public static Uri addImage(ContentResolver resolver, String title, 80 long date, Location location, int orientation, byte[] jpeg, 81 int width, int height) { 82 // Save the image. 83 String path = generateFilepath(title); 84 writeFile(path, jpeg); 85 return addImage(resolver, title, date, location, orientation, 86 jpeg.length, path, width, height); 87 } 88 89 // Add the image to media store. addImage(ContentResolver resolver, String title, long date, Location location, int orientation, int jpegLength, String path, int width, int height)90 public static Uri addImage(ContentResolver resolver, String title, 91 long date, Location location, int orientation, int jpegLength, 92 String path, int width, int height) { 93 // Insert into MediaStore. 94 ContentValues values = new ContentValues(9); 95 values.put(ImageColumns.TITLE, title); 96 values.put(ImageColumns.DISPLAY_NAME, title + ".jpg"); 97 values.put(ImageColumns.DATE_TAKEN, date); 98 values.put(ImageColumns.MIME_TYPE, "image/jpeg"); 99 // Clockwise rotation in degrees. 0, 90, 180, or 270. 100 values.put(ImageColumns.ORIENTATION, orientation); 101 values.put(ImageColumns.DATA, path); 102 values.put(ImageColumns.SIZE, jpegLength); 103 104 setImageSize(values, width, height); 105 106 if (location != null) { 107 values.put(ImageColumns.LATITUDE, location.getLatitude()); 108 values.put(ImageColumns.LONGITUDE, location.getLongitude()); 109 } 110 111 Uri uri = null; 112 try { 113 uri = resolver.insert(Images.Media.EXTERNAL_CONTENT_URI, values); 114 } catch (Throwable th) { 115 // This can happen when the external volume is already mounted, but 116 // MediaScanner has not notify MediaProvider to add that volume. 117 // The picture is still safe and MediaScanner will find it and 118 // insert it into MediaProvider. The only problem is that the user 119 // cannot click the thumbnail to review the picture. 120 Log.e(TAG, "Failed to write MediaStore" + th); 121 } 122 return uri; 123 } 124 125 // newImage() and updateImage() together do the same work as 126 // addImage. newImage() is the first step, and it inserts the DATE_TAKEN and 127 // DATA fields into the database. 128 // 129 // We also insert hint values for the WIDTH and HEIGHT fields to give 130 // correct aspect ratio before the real values are updated in updateImage(). newImage(ContentResolver resolver, String title, long date, int width, int height)131 public static Uri newImage(ContentResolver resolver, String title, 132 long date, int width, int height) { 133 String path = generateFilepath(title); 134 135 // Insert into MediaStore. 136 ContentValues values = new ContentValues(4); 137 values.put(ImageColumns.DATE_TAKEN, date); 138 values.put(ImageColumns.DATA, path); 139 140 setImageSize(values, width, height); 141 142 Uri uri = null; 143 try { 144 uri = resolver.insert(Images.Media.EXTERNAL_CONTENT_URI, values); 145 } catch (Throwable th) { 146 // This can happen when the external volume is already mounted, but 147 // MediaScanner has not notify MediaProvider to add that volume. 148 // The picture is still safe and MediaScanner will find it and 149 // insert it into MediaProvider. The only problem is that the user 150 // cannot click the thumbnail to review the picture. 151 Log.e(TAG, "Failed to new image" + th); 152 } 153 return uri; 154 } 155 156 // This is the second step. It completes the partial data added by 157 // newImage. All columns other than DATE_TAKEN and DATA are inserted 158 // here. This method also save the image data into the file. 159 // 160 // Returns true if the update is successful. updateImage(ContentResolver resolver, Uri uri, String title, Location location, int orientation, byte[] jpeg, int width, int height)161 public static boolean updateImage(ContentResolver resolver, Uri uri, 162 String title, Location location, int orientation, byte[] jpeg, 163 int width, int height) { 164 // Save the image. 165 String path = generateFilepath(title); 166 String tmpPath = path + ".tmp"; 167 FileOutputStream out = null; 168 try { 169 // Write to a temporary file and rename it to the final name. This 170 // avoids other apps reading incomplete data. 171 out = new FileOutputStream(tmpPath); 172 out.write(jpeg); 173 out.close(); 174 new File(tmpPath).renameTo(new File(path)); 175 } catch (Exception e) { 176 Log.e(TAG, "Failed to write image", e); 177 return false; 178 } finally { 179 try { 180 out.close(); 181 } catch (Exception e) { 182 } 183 } 184 185 // Insert into MediaStore. 186 ContentValues values = new ContentValues(9); 187 values.put(ImageColumns.TITLE, title); 188 values.put(ImageColumns.DISPLAY_NAME, title + ".jpg"); 189 values.put(ImageColumns.MIME_TYPE, "image/jpeg"); 190 // Clockwise rotation in degrees. 0, 90, 180, or 270. 191 values.put(ImageColumns.ORIENTATION, orientation); 192 values.put(ImageColumns.SIZE, jpeg.length); 193 194 setImageSize(values, width, height); 195 196 if (location != null) { 197 values.put(ImageColumns.LATITUDE, location.getLatitude()); 198 values.put(ImageColumns.LONGITUDE, location.getLongitude()); 199 } 200 201 try { 202 resolver.update(uri, values, null, null); 203 } catch (Throwable th) { 204 Log.e(TAG, "Failed to update image" + th); 205 return false; 206 } 207 208 return true; 209 } 210 deleteImage(ContentResolver resolver, Uri uri)211 public static void deleteImage(ContentResolver resolver, Uri uri) { 212 try { 213 resolver.delete(uri, null, null); 214 } catch (Throwable th) { 215 Log.e(TAG, "Failed to delete image: " + uri); 216 } 217 } 218 generateFilepath(String title)219 public static String generateFilepath(String title) { 220 return DIRECTORY + '/' + title + ".jpg"; 221 } 222 getAvailableSpace()223 public static long getAvailableSpace() { 224 String state = Environment.getExternalStorageState(); 225 Log.d(TAG, "External storage state=" + state); 226 if (Environment.MEDIA_CHECKING.equals(state)) { 227 return PREPARING; 228 } 229 if (!Environment.MEDIA_MOUNTED.equals(state)) { 230 return UNAVAILABLE; 231 } 232 233 File dir = new File(DIRECTORY); 234 dir.mkdirs(); 235 if (!dir.isDirectory() || !dir.canWrite()) { 236 return UNAVAILABLE; 237 } 238 239 try { 240 StatFs stat = new StatFs(DIRECTORY); 241 return stat.getAvailableBlocks() * (long) stat.getBlockSize(); 242 } catch (Exception e) { 243 Log.i(TAG, "Fail to access external storage", e); 244 } 245 return UNKNOWN_SIZE; 246 } 247 248 /** 249 * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be 250 * imported. This is a temporary fix for bug#1655552. 251 */ ensureOSXCompatible()252 public static void ensureOSXCompatible() { 253 File nnnAAAAA = new File(DCIM, "100ANDRO"); 254 if (!(nnnAAAAA.exists() || nnnAAAAA.mkdirs())) { 255 Log.e(TAG, "Failed to create " + nnnAAAAA.getPath()); 256 } 257 } 258 } 259