1 /* 2 * Copyright (C) 2009 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.cooliris.media; 18 19 import java.io.File; 20 import java.io.IOException; 21 import java.net.URI; 22 import java.util.ArrayList; 23 import java.util.List; 24 25 import android.content.ContentResolver; 26 import android.content.ContentUris; 27 import android.content.ContentValues; 28 import android.content.Context; 29 import android.database.Cursor; 30 import android.media.ExifInterface; 31 import android.net.Uri; 32 import android.os.Environment; 33 import android.provider.MediaStore; 34 import android.provider.MediaStore.Images; 35 import android.provider.MediaStore.Video; 36 import android.util.Log; 37 38 import com.cooliris.cache.CacheService; 39 40 public class LocalDataSource implements DataSource { 41 private static final String TAG = "LocalDataSource"; 42 public static final String URI_ALL_MEDIA = MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString(); 43 public static final DiskCache sThumbnailCache = new DiskCache("local-image-thumbs"); 44 public static final DiskCache sThumbnailCacheVideo = new DiskCache("local-video-thumbs"); 45 46 public static final String CAMERA_STRING = "Camera"; 47 public static final String DOWNLOAD_STRING = "download"; 48 public static final String CAMERA_BUCKET_NAME = Environment.getExternalStorageDirectory().toString() + "/DCIM/" + CAMERA_STRING; 49 public static final String DOWNLOAD_BUCKET_NAME = Environment.getExternalStorageDirectory().toString() + "/" + DOWNLOAD_STRING; 50 public static final int CAMERA_BUCKET_ID = getBucketId(CAMERA_BUCKET_NAME); 51 public static final int DOWNLOAD_BUCKET_ID = getBucketId(DOWNLOAD_BUCKET_NAME); 52 53 /** 54 * Matches code in MediaProvider.computeBucketValues. Should be a common 55 * function. 56 */ 57 public static int getBucketId(String path) { 58 return (path.toLowerCase().hashCode()); 59 } 60 61 private final String mUri; 62 private final String mBucketId; 63 private boolean mDone;; 64 private final boolean mSingleUri; 65 private final boolean mAllItems; 66 private final boolean mFlattenAllItems; 67 private final DiskCache mDiskCache; 68 private boolean mIncludeImages; 69 private boolean mIncludeVideos; 70 private Context mContext; 71 72 public LocalDataSource(final Context context, final String uri, final boolean flattenAllItems) { 73 this.mUri = uri; 74 mContext = context; 75 mIncludeImages = true; 76 mIncludeVideos = false; 77 String bucketId = Uri.parse(uri).getQueryParameter("bucketId"); 78 if (bucketId != null && bucketId.length() > 0) { 79 mBucketId = bucketId; 80 } else { 81 mBucketId = null; 82 } 83 mFlattenAllItems = flattenAllItems; 84 if (mBucketId == null) { 85 if (uri.equals(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())) { 86 mAllItems = true; 87 } else { 88 mAllItems = false; 89 } 90 } else { 91 mAllItems = false; 92 } 93 mSingleUri = isSingleImageMode(uri) && mBucketId == null; 94 mDone = false; 95 mDiskCache = mUri.startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString()) 96 || mUri.startsWith(MediaStore.Video.Media.EXTERNAL_CONTENT_URI.toString()) 97 || mUri.startsWith("file://") ? sThumbnailCache 98 : null; 99 } 100 101 public void setMimeFilter(boolean includeImages, boolean includeVideos) { 102 mIncludeImages = includeImages; 103 mIncludeVideos = includeVideos; 104 } 105 106 public void shutdown() { 107 108 } 109 110 public boolean isSingleImage() { 111 return mSingleUri; 112 } 113 114 private static boolean isSingleImageMode(String uriString) { 115 return !uriString.equals(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString()) 116 && !uriString.equals(MediaStore.Images.Media.INTERNAL_CONTENT_URI.toString()); 117 } 118 119 public DiskCache getThumbnailCache() { 120 return mDiskCache; 121 } 122 123 public void loadItemsForSet(MediaFeed feed, MediaSet parentSet, int rangeStart, int rangeEnd) { 124 if (parentSet.mNumItemsLoaded > 0 && mDone) { 125 return; 126 } 127 if (mSingleUri && !mDone) { 128 MediaItem item = new MediaItem(); 129 item.mId = 0; 130 item.mFilePath = ""; 131 item.setMediaType((isImage(mUri)) ? MediaItem.MEDIA_TYPE_IMAGE : MediaItem.MEDIA_TYPE_VIDEO); 132 if (mUri.startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString()) 133 || mUri.startsWith(MediaStore.Video.Media.EXTERNAL_CONTENT_URI.toString())) { 134 MediaItem newItem = createMediaItemFromUri(mContext, Uri.parse(mUri), item.getMediaType()); 135 if (newItem != null) { 136 item = newItem; 137 String fileUri = new File(item.mFilePath).toURI().toString(); 138 parentSet.mName = Utils.getBucketNameFromUri(mContext.getContentResolver(), Uri.parse(fileUri)); 139 parentSet.mId = Utils.getBucketIdFromUri(mContext.getContentResolver(), Uri.parse(fileUri)); 140 parentSet.generateTitle(true); 141 } 142 } else if (mUri.startsWith("file://")) { 143 MediaItem newItem = null; 144 int numRetries = 15; 145 do { 146 newItem = createMediaItemFromFileUri(mContext, mUri); 147 if (newItem == null) { 148 --numRetries; 149 try { 150 Thread.sleep(500); 151 } catch (InterruptedException e) { 152 ; 153 } 154 } 155 } while (newItem == null && numRetries >= 0); 156 if (newItem != null) { 157 item = newItem; 158 } else { 159 item.mContentUri = mUri; 160 item.mThumbnailUri = mUri; 161 item.mScreennailUri = mUri; 162 feed.setSingleImageMode(true); 163 } 164 } else { 165 item.mContentUri = mUri; 166 item.mThumbnailUri = mUri; 167 item.mScreennailUri = mUri; 168 feed.setSingleImageMode(true); 169 } 170 if (item != null) { 171 feed.addItemToMediaSet(item, parentSet); 172 // Parse EXIF orientation if a local file. 173 if (mUri.startsWith("file://")) { 174 try { 175 ExifInterface exif = new ExifInterface(Uri.parse(mUri).getPath()); 176 item.mRotation = Shared.exifOrientationToDegrees(exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 177 ExifInterface.ORIENTATION_NORMAL)); 178 } catch (IOException e) { 179 Log.i(TAG, "Error reading Exif information, probably not a jpeg."); 180 } 181 } 182 // Try and get the date taken for this item. 183 long dateTaken = CacheService.fetchDateTaken(item); 184 if (dateTaken != -1L) { 185 item.mDateTakenInMs = dateTaken; 186 } 187 CacheService.loadMediaItemsIntoMediaFeed(mContext, feed, parentSet, rangeStart, rangeEnd, mIncludeImages, mIncludeVideos); 188 ArrayList<MediaItem> items = parentSet.getItems(); 189 int numItems = items.size(); 190 if (numItems == 1 && parentSet.mNumItemsLoaded > 1) { 191 parentSet.mNumItemsLoaded = 1; 192 } 193 parentSet.removeDuplicate(item); 194 } 195 parentSet.updateNumExpectedItems(); 196 parentSet.generateTitle(true); 197 } else if (mUri.equals(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString()) & mFlattenAllItems) { 198 final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI; 199 final ContentResolver cr = mContext.getContentResolver(); 200 String where = null; 201 try { 202 Cursor cursor = cr.query(uriImages, CacheService.PROJECTION_IMAGES, where, null, null); 203 if (cursor != null && cursor.moveToFirst()) { 204 parentSet.setNumExpectedItems(cursor.getCount()); 205 do { 206 if (Thread.interrupted()) { 207 return; 208 } 209 final MediaItem item = new MediaItem(); 210 CacheService.populateMediaItemFromCursor(item, cr, cursor, CacheService.BASE_CONTENT_STRING_IMAGES); 211 feed.addItemToMediaSet(item, parentSet); 212 } while (cursor.moveToNext()); 213 if (cursor != null) { 214 cursor.close(); 215 cursor = null; 216 } 217 parentSet.updateNumExpectedItems(); 218 parentSet.generateTitle(true); 219 } 220 } catch (Exception e) { 221 // If the database operation failed for any reason. 222 ; 223 } 224 } else { 225 CacheService.loadMediaItemsIntoMediaFeed(mContext, feed, parentSet, rangeStart, rangeEnd, mIncludeImages, mIncludeVideos); 226 } 227 mDone = true; 228 } 229 230 private static boolean isImage(String uriString) { 231 return !uriString.startsWith(MediaStore.Video.Media.EXTERNAL_CONTENT_URI.toString()); 232 } 233 234 public void loadMediaSets(final MediaFeed feed) { 235 MediaSet set = null; // Dummy set. 236 boolean loadOtherSets = true; 237 if (mSingleUri) { 238 String name = Utils.getBucketNameFromUri(mContext.getContentResolver(), Uri.parse(mUri)); 239 long id = Utils.getBucketIdFromUri(mContext.getContentResolver(), Uri.parse(mUri)); 240 set = feed.addMediaSet(id, this); 241 set.mName = name; 242 set.mId = id; 243 set.setNumExpectedItems(2); 244 set.generateTitle(true); 245 set.mPicasaAlbumId = Shared.INVALID; 246 if (this.getThumbnailCache() != sThumbnailCache) { 247 loadOtherSets = false; 248 } 249 } else if (mBucketId == null) { 250 // All the buckets. 251 if (mFlattenAllItems) { 252 set = feed.addMediaSet(0, this); // Create dummy set. 253 set.mName = Utils.getBucketNameFromUri(mContext.getContentResolver(), Uri.parse(mUri)); 254 set.mId = getBucketId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString() + "/" + set.mName); 255 set.setNumExpectedItems(1); 256 set.generateTitle(true); 257 set.mPicasaAlbumId = Shared.INVALID; 258 } else { 259 CacheService.loadMediaSets(mContext, feed, this, mIncludeImages, mIncludeVideos, true); 260 } 261 } else { 262 CacheService.loadMediaSet(mContext, feed, this, Long.parseLong(mBucketId)); 263 ArrayList<MediaSet> sets = feed.getMediaSets(); 264 if (sets.size() > 0) 265 set = sets.get(0); 266 } 267 // We also load the other MediaSets 268 if (!mAllItems && set != null && loadOtherSets) { 269 final long setId = set.mId; 270 if (!CacheService.isPresentInCache(setId)) { 271 CacheService.markDirty(); 272 } 273 CacheService.loadMediaSets(mContext, feed, this, mIncludeImages, mIncludeVideos, false); 274 275 // not re-ordering media sets in the case of displaying a single image 276 if (!mSingleUri) { 277 feed.moveSetToFront(set); 278 } 279 } 280 } 281 282 public boolean performOperation(int operation, ArrayList<MediaBucket> mediaBuckets, Object data) { 283 int numBuckets = mediaBuckets.size(); 284 ContentResolver cr = mContext.getContentResolver(); 285 switch (operation) { 286 case MediaFeed.OPERATION_DELETE: 287 for (int i = 0; i < numBuckets; ++i) { 288 MediaBucket bucket = mediaBuckets.get(i); 289 MediaSet set = bucket.mediaSet; 290 ArrayList<MediaItem> items = bucket.mediaItems; 291 if (set != null && items == null) { 292 // TODO bulk delete 293 // remove the entire bucket 294 final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI; 295 final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI; 296 final String whereImages = Images.ImageColumns.BUCKET_ID + "=" + Long.toString(set.mId); 297 final String whereVideos = Video.VideoColumns.BUCKET_ID + "=" + Long.toString(set.mId); 298 cr.delete(uriImages, whereImages, null); 299 cr.delete(uriVideos, whereVideos, null); 300 //CacheService.markDirty(); 301 } 302 if (set != null && items != null) { 303 // We need to remove these items from the set. 304 int numItems = items.size(); 305 try { 306 for (int j = 0; j < numItems; ++j) { 307 MediaItem item = items.get(j); 308 cr.delete(Uri.parse(item.mContentUri), null, null); 309 } 310 } catch (Exception e) { 311 // If the database operation failed for any reason. 312 ; 313 } 314 set.updateNumExpectedItems(); 315 set.generateTitle(true); 316 //CacheService.markDirty(set.mId); 317 } 318 } 319 break; 320 case MediaFeed.OPERATION_ROTATE: 321 for (int i = 0; i < numBuckets; ++i) { 322 MediaBucket bucket = mediaBuckets.get(i); 323 ArrayList<MediaItem> items = bucket.mediaItems; 324 if (items == null) { 325 continue; 326 } 327 float angleToRotate = ((Float) data).floatValue(); 328 if (angleToRotate == 0) { 329 return true; 330 } 331 int numItems = items.size(); 332 for (int j = 0; j < numItems; ++j) { 333 rotateItem(items.get(j), angleToRotate); 334 } 335 } 336 break; 337 } 338 return true; 339 } 340 341 private void rotateItem(final MediaItem item, float angleToRotate) { 342 ContentResolver cr = mContext.getContentResolver(); 343 try { 344 int currentOrientation = (int) item.mRotation; 345 angleToRotate += currentOrientation; 346 float rotation = Shared.normalizePositive(angleToRotate); 347 String rotationString = Integer.toString((int) rotation); 348 349 // Update the database entry. 350 ContentValues values = new ContentValues(); 351 values.put(Images.ImageColumns.ORIENTATION, rotationString); 352 try { 353 cr.update(Uri.parse(item.mContentUri), values, null, null); 354 } catch (Exception e) { 355 // If the database operation fails for any reason. 356 ; 357 } 358 359 // Update the file EXIF information. 360 Uri uri = Uri.parse(item.mContentUri); 361 String uriScheme = uri.getScheme(); 362 if (uriScheme.equals("file") || uriScheme.equals("content")) { 363 final String path = (uriScheme.equals("file")) ? uri.getPath() : item.mFilePath; 364 ExifInterface exif = new ExifInterface(path); 365 exif.setAttribute(ExifInterface.TAG_ORIENTATION, Integer.toString(Shared.degreesToExifOrientation(rotation))); 366 exif.saveAttributes(); 367 } 368 369 // Invalidate the cache entry. 370 CacheService.markDirty(item.mParentMediaSet.mId); 371 372 // Update the object representation of the item. 373 item.mRotation = rotation; 374 } catch (Exception e) { 375 // System.out.println("Apparently not a JPEG"); 376 } 377 } 378 379 public static MediaItem createMediaItemFromUri(Context context, Uri target, int mediaType) { 380 MediaItem item = null; 381 long id = ContentUris.parseId(target); 382 ContentResolver cr = context.getContentResolver(); 383 String whereClause = Images.ImageColumns._ID + "=" + Long.toString(id); 384 try { 385 final Uri uri = (mediaType == MediaItem.MEDIA_TYPE_IMAGE) 386 ? Images.Media.EXTERNAL_CONTENT_URI 387 : Video.Media.EXTERNAL_CONTENT_URI; 388 final String[] projection = (mediaType == MediaItem.MEDIA_TYPE_IMAGE) 389 ? CacheService.PROJECTION_IMAGES 390 : CacheService.PROJECTION_VIDEOS; 391 Cursor cursor = cr.query(uri, projection, whereClause, null, null); 392 if (cursor != null) { 393 if (cursor.moveToFirst()) { 394 item = new MediaItem(); 395 CacheService.populateMediaItemFromCursor(item, cr, cursor, uri.toString() + "/"); 396 } 397 cursor.close(); 398 cursor = null; 399 } 400 } catch (Exception e) { 401 // If the database operation failed for any reason. 402 ; 403 } 404 item.mId = id; 405 return item; 406 } 407 408 public static MediaItem createMediaItemFromFileUri(Context context, String fileUri) { 409 MediaItem item = null; 410 String filepath = new File(URI.create(fileUri)).toString(); 411 ContentResolver cr = context.getContentResolver(); 412 long bucketId = Utils.getBucketIdFromUri(context.getContentResolver(), Uri.parse(fileUri)); 413 String whereClause = Images.ImageColumns.BUCKET_ID + "=" + bucketId + " AND " + Images.ImageColumns.DATA + "='" + filepath 414 + "'"; 415 try { 416 Cursor cursor = cr.query(Images.Media.EXTERNAL_CONTENT_URI, CacheService.PROJECTION_IMAGES, whereClause, null, null); 417 if (cursor != null) { 418 if (cursor.moveToFirst()) { 419 item = new MediaItem(); 420 CacheService.populateMediaItemFromCursor(item, cr, cursor, Images.Media.EXTERNAL_CONTENT_URI.toString() + "/"); 421 } 422 cursor.close(); 423 cursor = null; 424 } 425 } catch (Exception e) { 426 // If the database operation failed for any reason. 427 ; 428 } 429 return item; 430 } 431 432 public String[] getDatabaseUris() { 433 return new String[] {Images.Media.EXTERNAL_CONTENT_URI.toString(), Video.Media.EXTERNAL_CONTENT_URI.toString()}; 434 } 435 436 public void refresh(final MediaFeed feed, final String[] databaseUris) { 437 // We check to see what has changed. 438 long[] ids = CacheService.computeDirtySets(mContext); 439 int numDirtySets = ids.length; 440 for (int i = 0; i < numDirtySets; ++i) { 441 long setId = ids[i]; 442 if (feed.getMediaSet(setId) != null) { 443 MediaSet newSet = feed.replaceMediaSet(setId, this); 444 newSet.generateTitle(true); 445 } else { 446 MediaSet mediaSet = feed.addMediaSet(setId, this); 447 if (setId == CAMERA_BUCKET_ID) { 448 mediaSet.mName = CAMERA_STRING; 449 } else if (setId == DOWNLOAD_BUCKET_ID) { 450 mediaSet.mName = DOWNLOAD_STRING; 451 } 452 mediaSet.generateTitle(true); 453 } 454 } 455 } 456 457 } 458