1 /* 2 * Copyright (C) 2007 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 com.android.camera.gallery.BaseImageList; 20 import com.android.camera.gallery.IImage; 21 import com.android.camera.gallery.IImageList; 22 import com.android.camera.gallery.ImageList; 23 import com.android.camera.gallery.ImageListUber; 24 import com.android.camera.gallery.VideoList; 25 26 import android.content.ContentResolver; 27 import android.content.ContentValues; 28 import android.database.Cursor; 29 import android.graphics.Bitmap; 30 import android.graphics.Bitmap.CompressFormat; 31 import android.location.Location; 32 import android.media.ExifInterface; 33 import android.net.Uri; 34 import android.os.Environment; 35 import android.os.Parcel; 36 import android.os.Parcelable; 37 import android.provider.MediaStore; 38 import android.provider.MediaStore.Images; 39 import android.util.Log; 40 import android.view.OrientationEventListener; 41 42 import java.io.File; 43 import java.io.FileNotFoundException; 44 import java.io.FileOutputStream; 45 import java.io.IOException; 46 import java.io.OutputStream; 47 import java.util.ArrayList; 48 import java.util.Iterator; 49 50 /** 51 * {@code ImageManager} is used to retrieve and store images 52 * in the media content provider. 53 */ 54 public class ImageManager { 55 private static final String TAG = "ImageManager"; 56 57 private static final Uri STORAGE_URI = Images.Media.EXTERNAL_CONTENT_URI; 58 private static final Uri VIDEO_STORAGE_URI = 59 Uri.parse("content://media/external/video/media"); 60 61 private ImageManager() { 62 } 63 64 /** 65 * {@code ImageListParam} specifies all the parameters we need to create an 66 * image list (we also need a ContentResolver). 67 */ 68 public static class ImageListParam implements Parcelable { 69 public DataLocation mLocation; 70 public int mInclusion; 71 public int mSort; 72 public String mBucketId; 73 74 // This is only used if we are creating an empty image list. 75 public boolean mIsEmptyImageList; 76 77 public ImageListParam() { 78 } 79 80 public void writeToParcel(Parcel out, int flags) { 81 out.writeInt(mLocation.ordinal()); 82 out.writeInt(mInclusion); 83 out.writeInt(mSort); 84 out.writeString(mBucketId); 85 out.writeInt(mIsEmptyImageList ? 1 : 0); 86 } 87 88 private ImageListParam(Parcel in) { 89 mLocation = DataLocation.values()[in.readInt()]; 90 mInclusion = in.readInt(); 91 mSort = in.readInt(); 92 mBucketId = in.readString(); 93 mIsEmptyImageList = (in.readInt() != 0); 94 } 95 96 @Override 97 public String toString() { 98 return String.format("ImageListParam{loc=%s,inc=%d,sort=%d," + 99 "bucket=%s,empty=%b}", mLocation, mInclusion, 100 mSort, mBucketId, mIsEmptyImageList); 101 } 102 103 public static final Parcelable.Creator<ImageListParam> CREATOR 104 = new Parcelable.Creator<ImageListParam>() { 105 public ImageListParam createFromParcel(Parcel in) { 106 return new ImageListParam(in); 107 } 108 109 public ImageListParam[] newArray(int size) { 110 return new ImageListParam[size]; 111 } 112 }; 113 114 public int describeContents() { 115 return 0; 116 } 117 } 118 119 // Location 120 public static enum DataLocation { NONE, INTERNAL, EXTERNAL, ALL } 121 122 // Inclusion 123 public static final int INCLUDE_IMAGES = (1 << 0); 124 public static final int INCLUDE_VIDEOS = (1 << 2); 125 126 // Sort 127 public static final int SORT_ASCENDING = 1; 128 public static final int SORT_DESCENDING = 2; 129 130 public static final String CAMERA_IMAGE_BUCKET_NAME = 131 Environment.getExternalStorageDirectory().toString() 132 + "/DCIM/Camera"; 133 public static final String CAMERA_IMAGE_BUCKET_ID = 134 getBucketId(CAMERA_IMAGE_BUCKET_NAME); 135 136 /** 137 * Matches code in MediaProvider.computeBucketValues. Should be a common 138 * function. 139 */ 140 public static String getBucketId(String path) { 141 return String.valueOf(path.toLowerCase().hashCode()); 142 } 143 144 /** 145 * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be 146 * imported. This is a temporary fix for bug#1655552. 147 */ 148 public static void ensureOSXCompatibleFolder() { 149 File nnnAAAAA = new File( 150 Environment.getExternalStorageDirectory().toString() 151 + "/DCIM/100ANDRO"); 152 if ((!nnnAAAAA.exists()) && (!nnnAAAAA.mkdir())) { 153 Log.e(TAG, "create NNNAAAAA file: " + nnnAAAAA.getPath() 154 + " failed"); 155 } 156 } 157 158 // 159 // Stores a bitmap or a jpeg byte array to a file (using the specified 160 // directory and filename). Also add an entry to the media store for 161 // this picture. The title, dateTaken, location are attributes for the 162 // picture. The degree is a one element array which returns the orientation 163 // of the picture. 164 // 165 public static Uri addImage(ContentResolver cr, String title, long dateTaken, 166 Location location, String directory, String filename, 167 Bitmap source, byte[] jpegData, int[] degree) { 168 // We should store image data earlier than insert it to ContentProvider, 169 // otherwise we may not be able to generate thumbnail in time. 170 OutputStream outputStream = null; 171 String filePath = directory + "/" + filename; 172 try { 173 File dir = new File(directory); 174 if (!dir.exists()) dir.mkdirs(); 175 File file = new File(directory, filename); 176 outputStream = new FileOutputStream(file); 177 if (source != null) { 178 source.compress(CompressFormat.JPEG, 75, outputStream); 179 degree[0] = 0; 180 } else { 181 outputStream.write(jpegData); 182 degree[0] = getExifOrientation(filePath); 183 } 184 } catch (FileNotFoundException ex) { 185 Log.w(TAG, ex); 186 return null; 187 } catch (IOException ex) { 188 Log.w(TAG, ex); 189 return null; 190 } finally { 191 Util.closeSilently(outputStream); 192 } 193 194 // Read back the compressed file size. 195 long size = new File(directory, filename).length(); 196 197 ContentValues values = new ContentValues(9); 198 values.put(Images.Media.TITLE, title); 199 200 // That filename is what will be handed to Gmail when a user shares a 201 // photo. Gmail gets the name of the picture attachment from the 202 // "DISPLAY_NAME" field. 203 values.put(Images.Media.DISPLAY_NAME, filename); 204 values.put(Images.Media.DATE_TAKEN, dateTaken); 205 values.put(Images.Media.MIME_TYPE, "image/jpeg"); 206 values.put(Images.Media.ORIENTATION, degree[0]); 207 values.put(Images.Media.DATA, filePath); 208 values.put(Images.Media.SIZE, size); 209 210 if (location != null) { 211 values.put(Images.Media.LATITUDE, location.getLatitude()); 212 values.put(Images.Media.LONGITUDE, location.getLongitude()); 213 } 214 215 return cr.insert(STORAGE_URI, values); 216 } 217 218 public static int getExifOrientation(String filepath) { 219 int degree = 0; 220 ExifInterface exif = null; 221 try { 222 exif = new ExifInterface(filepath); 223 } catch (IOException ex) { 224 Log.e(TAG, "cannot read exif", ex); 225 } 226 if (exif != null) { 227 int orientation = exif.getAttributeInt( 228 ExifInterface.TAG_ORIENTATION, -1); 229 if (orientation != -1) { 230 // We only recognize a subset of orientation tag values. 231 switch(orientation) { 232 case ExifInterface.ORIENTATION_ROTATE_90: 233 degree = 90; 234 break; 235 case ExifInterface.ORIENTATION_ROTATE_180: 236 degree = 180; 237 break; 238 case ExifInterface.ORIENTATION_ROTATE_270: 239 degree = 270; 240 break; 241 } 242 243 } 244 } 245 return degree; 246 } 247 248 // This is the factory function to create an image list. 249 public static IImageList makeImageList(ContentResolver cr, 250 ImageListParam param) { 251 DataLocation location = param.mLocation; 252 int inclusion = param.mInclusion; 253 int sort = param.mSort; 254 String bucketId = param.mBucketId; 255 boolean isEmptyImageList = param.mIsEmptyImageList; 256 257 if (isEmptyImageList || cr == null) { 258 return new EmptyImageList(); 259 } 260 261 // false ==> don't require write access 262 boolean haveSdCard = hasStorage(false); 263 264 // use this code to merge videos and stills into the same list 265 ArrayList<BaseImageList> l = new ArrayList<BaseImageList>(); 266 267 if (haveSdCard && location != DataLocation.INTERNAL) { 268 if ((inclusion & INCLUDE_IMAGES) != 0) { 269 l.add(new ImageList(cr, STORAGE_URI, sort, bucketId)); 270 } 271 if ((inclusion & INCLUDE_VIDEOS) != 0) { 272 l.add(new VideoList(cr, VIDEO_STORAGE_URI, sort, bucketId)); 273 } 274 } 275 if (location == DataLocation.INTERNAL || location == DataLocation.ALL) { 276 if ((inclusion & INCLUDE_IMAGES) != 0) { 277 l.add(new ImageList(cr, 278 Images.Media.INTERNAL_CONTENT_URI, sort, bucketId)); 279 } 280 } 281 282 // Optimization: If some of the lists are empty, remove them. 283 // If there is only one remaining list, return it directly. 284 Iterator<BaseImageList> iter = l.iterator(); 285 while (iter.hasNext()) { 286 BaseImageList sublist = iter.next(); 287 if (sublist.isEmpty()) { 288 sublist.close(); 289 iter.remove(); 290 } 291 } 292 293 if (l.size() == 1) { 294 BaseImageList list = l.get(0); 295 return list; 296 } 297 298 ImageListUber uber = new ImageListUber( 299 l.toArray(new IImageList[l.size()]), sort); 300 return uber; 301 } 302 303 private static class EmptyImageList implements IImageList { 304 public void close() { 305 } 306 307 public int getCount() { 308 return 0; 309 } 310 311 public IImage getImageAt(int i) { 312 return null; 313 } 314 } 315 316 public static ImageListParam getImageListParam(DataLocation location, 317 int inclusion, int sort, String bucketId) { 318 ImageListParam param = new ImageListParam(); 319 param.mLocation = location; 320 param.mInclusion = inclusion; 321 param.mSort = sort; 322 param.mBucketId = bucketId; 323 return param; 324 } 325 326 public static IImageList makeImageList(ContentResolver cr, 327 DataLocation location, int inclusion, int sort, String bucketId) { 328 ImageListParam param = getImageListParam(location, inclusion, sort, 329 bucketId); 330 return makeImageList(cr, param); 331 } 332 333 private static boolean checkFsWritable() { 334 // Create a temporary file to see whether a volume is really writeable. 335 // It's important not to put it in the root directory which may have a 336 // limit on the number of files. 337 String directoryName = 338 Environment.getExternalStorageDirectory().toString() + "/DCIM"; 339 File directory = new File(directoryName); 340 if (!directory.isDirectory()) { 341 if (!directory.mkdirs()) { 342 return false; 343 } 344 } 345 return directory.canWrite(); 346 } 347 348 public static boolean hasStorage() { 349 return hasStorage(true); 350 } 351 352 public static boolean hasStorage(boolean requireWriteAccess) { 353 String state = Environment.getExternalStorageState(); 354 355 if (Environment.MEDIA_MOUNTED.equals(state)) { 356 if (requireWriteAccess) { 357 boolean writable = checkFsWritable(); 358 return writable; 359 } else { 360 return true; 361 } 362 } else if (!requireWriteAccess 363 && Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { 364 return true; 365 } 366 return false; 367 } 368 369 private static Cursor query(ContentResolver resolver, Uri uri, 370 String[] projection, String selection, String[] selectionArgs, 371 String sortOrder) { 372 try { 373 if (resolver == null) { 374 return null; 375 } 376 return resolver.query( 377 uri, projection, selection, selectionArgs, sortOrder); 378 } catch (UnsupportedOperationException ex) { 379 return null; 380 } 381 382 } 383 384 public static boolean isMediaScannerScanning(ContentResolver cr) { 385 boolean result = false; 386 Cursor cursor = query(cr, MediaStore.getMediaScannerUri(), 387 new String [] {MediaStore.MEDIA_SCANNER_VOLUME}, 388 null, null, null); 389 if (cursor != null) { 390 if (cursor.getCount() == 1) { 391 cursor.moveToFirst(); 392 result = "external".equals(cursor.getString(0)); 393 } 394 cursor.close(); 395 } 396 397 return result; 398 } 399 400 public static String getLastImageThumbPath() { 401 return Environment.getExternalStorageDirectory().toString() + 402 "/DCIM/.thumbnails/image_last_thumb"; 403 } 404 405 public static String getLastVideoThumbPath() { 406 return Environment.getExternalStorageDirectory().toString() + 407 "/DCIM/.thumbnails/video_last_thumb"; 408 } 409 410 public static String getTempJpegPath() { 411 return Environment.getExternalStorageDirectory().toString() + 412 "/DCIM/.tempjpeg"; 413 } 414 } 415