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.cache; 18 19 import java.io.BufferedInputStream; 20 import java.io.BufferedOutputStream; 21 import java.io.ByteArrayInputStream; 22 import java.io.ByteArrayOutputStream; 23 import java.io.DataInputStream; 24 import java.io.DataOutputStream; 25 import java.io.IOException; 26 import java.net.URISyntaxException; 27 import java.nio.ByteBuffer; 28 import java.nio.LongBuffer; 29 import java.text.DateFormat; 30 import java.text.ParseException; 31 import java.text.SimpleDateFormat; 32 import java.util.ArrayList; 33 import java.util.Date; 34 import java.util.Locale; 35 import java.util.concurrent.atomic.AtomicReference; 36 37 import android.app.IntentService; 38 import android.content.ContentResolver; 39 import android.content.ContentValues; 40 import android.content.Context; 41 import android.content.Intent; 42 import android.database.Cursor; 43 import android.database.MergeCursor; 44 import android.graphics.Bitmap; 45 import android.graphics.Canvas; 46 import android.graphics.Paint; 47 import android.graphics.Rect; 48 import android.media.ExifInterface; 49 import android.net.Uri; 50 import android.os.Environment; 51 import android.os.Process; 52 import android.os.SystemClock; 53 import android.provider.MediaStore; 54 import android.provider.MediaStore.Images; 55 import android.provider.MediaStore.Video; 56 import android.util.Log; 57 import android.widget.Toast; 58 59 import com.cooliris.app.App; 60 import com.cooliris.app.Res; 61 import com.cooliris.media.DataSource; 62 import com.cooliris.media.DiskCache; 63 import com.cooliris.media.Gallery; 64 import com.cooliris.media.LongSparseArray; 65 import com.cooliris.media.MediaFeed; 66 import com.cooliris.media.MediaItem; 67 import com.cooliris.media.MediaSet; 68 import com.cooliris.media.Shared; 69 import com.cooliris.media.LocalDataSource; 70 import com.cooliris.media.SortCursor; 71 import com.cooliris.media.UriTexture; 72 import com.cooliris.media.Utils; 73 74 public final class CacheService extends IntentService { 75 public static final String ACTION_CACHE = "com.cooliris.cache.action.CACHE"; 76 public static final DiskCache sAlbumCache = new DiskCache("local-album-cache"); 77 public static final DiskCache sMetaAlbumCache = new DiskCache("local-meta-cache"); 78 public static final DiskCache sSkipThumbnailIds = new DiskCache("local-skip-cache"); 79 80 private static final String TAG = "CacheService"; 81 private static ImageList sList = null; 82 private static final boolean DEBUG = true; 83 84 // Wait 2 seconds to start the thumbnailer so that the application can load 85 // without any overheads. 86 private static final int THUMBNAILER_WAIT_IN_MS = 2000; 87 private static final int DEFAULT_THUMBNAIL_WIDTH = 128; 88 private static final int DEFAULT_THUMBNAIL_HEIGHT = 96; 89 90 public static final String DEFAULT_IMAGE_SORT_ORDER = Images.ImageColumns.DATE_TAKEN + " ASC"; 91 public static final String DEFAULT_VIDEO_SORT_ORDER = Video.VideoColumns.DATE_TAKEN + " ASC"; 92 public static final String DEFAULT_BUCKET_SORT_ORDER = "upper(" + Images.ImageColumns.BUCKET_DISPLAY_NAME + ") ASC"; 93 94 // Must preserve order between these indices and the order of the terms in 95 // BUCKET_PROJECTION_IMAGES, BUCKET_PROJECTION_VIDEOS. 96 // Not using SortedHashMap for efficieny reasons. 97 public static final int BUCKET_ID_INDEX = 0; 98 public static final int BUCKET_NAME_INDEX = 1; 99 public static final String[] BUCKET_PROJECTION_IMAGES = new String[] { Images.ImageColumns.BUCKET_ID, 100 Images.ImageColumns.BUCKET_DISPLAY_NAME }; 101 102 public static final String[] BUCKET_PROJECTION_VIDEOS = new String[] { Video.VideoColumns.BUCKET_ID, 103 Video.VideoColumns.BUCKET_DISPLAY_NAME }; 104 105 // Must preserve order between these indices and the order of the terms in 106 // THUMBNAIL_PROJECTION. 107 public static final int THUMBNAIL_ID_INDEX = 0; 108 public static final int THUMBNAIL_DATE_MODIFIED_INDEX = 1; 109 public static final int THUMBNAIL_DATA_INDEX = 2; 110 public static final int THUMBNAIL_ORIENTATION_INDEX = 3; 111 public static final String[] THUMBNAIL_PROJECTION = new String[] { Images.ImageColumns._ID, Images.ImageColumns.DATE_ADDED, 112 Images.ImageColumns.DATA, Images.ImageColumns.ORIENTATION }; 113 114 public static final String[] SENSE_PROJECTION = new String[] { Images.ImageColumns.BUCKET_ID, 115 "MAX(" + Images.ImageColumns.DATE_ADDED + "), COUNT(*)" }; 116 117 // Must preserve order between these indices and the order of the terms in 118 // INITIAL_PROJECTION_IMAGES and 119 // INITIAL_PROJECTION_VIDEOS. 120 public static final int MEDIA_ID_INDEX = 0; 121 public static final int MEDIA_CAPTION_INDEX = 1; 122 public static final int MEDIA_MIME_TYPE_INDEX = 2; 123 public static final int MEDIA_LATITUDE_INDEX = 3; 124 public static final int MEDIA_LONGITUDE_INDEX = 4; 125 public static final int MEDIA_DATE_TAKEN_INDEX = 5; 126 public static final int MEDIA_DATE_ADDED_INDEX = 6; 127 public static final int MEDIA_DATE_MODIFIED_INDEX = 7; 128 public static final int MEDIA_DATA_INDEX = 8; 129 public static final int MEDIA_ORIENTATION_OR_DURATION_INDEX = 9; 130 public static final int MEDIA_BUCKET_ID_INDEX = 10; 131 public static final String[] PROJECTION_IMAGES = new String[] { Images.ImageColumns._ID, Images.ImageColumns.TITLE, 132 Images.ImageColumns.MIME_TYPE, Images.ImageColumns.LATITUDE, Images.ImageColumns.LONGITUDE, 133 Images.ImageColumns.DATE_TAKEN, Images.ImageColumns.DATE_ADDED, Images.ImageColumns.DATE_MODIFIED, 134 Images.ImageColumns.DATA, Images.ImageColumns.ORIENTATION, Images.ImageColumns.BUCKET_ID }; 135 136 public static final String[] PROJECTION_VIDEOS = new String[] { Video.VideoColumns._ID, Video.VideoColumns.TITLE, 137 Video.VideoColumns.MIME_TYPE, Video.VideoColumns.LATITUDE, Video.VideoColumns.LONGITUDE, Video.VideoColumns.DATE_TAKEN, 138 Video.VideoColumns.DATE_ADDED, Video.VideoColumns.DATE_MODIFIED, Video.VideoColumns.DATA, Video.VideoColumns.DURATION, 139 Video.VideoColumns.BUCKET_ID }; 140 141 public static final String BASE_CONTENT_STRING_IMAGES = (Images.Media.EXTERNAL_CONTENT_URI).toString() + "/"; 142 public static final String BASE_CONTENT_STRING_VIDEOS = (Video.Media.EXTERNAL_CONTENT_URI).toString() + "/"; 143 private static final AtomicReference<Thread> THUMBNAIL_THREAD = new AtomicReference<Thread>(); 144 145 // Special indices in the Albumcache. 146 private static final int ALBUM_CACHE_METADATA_INDEX = -1; 147 private static final int ALBUM_CACHE_DIRTY_INDEX = -2; 148 private static final int ALBUM_CACHE_DIRTY_BUCKET_INDEX = -4; 149 private static final int ALBUM_CACHE_LOCALE_INDEX = -5; 150 151 private static final DateFormat mDateFormat = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss"); 152 private static final DateFormat mAltDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); 153 private static final byte[] sDummyData = new byte[] { 1 }; 154 private static final Object sCacheLock = new Object(); 155 156 public static final String getCachePath(final String subFolderName) { 157 return Environment.getExternalStorageDirectory() + "/Android/data/com.cooliris.media/cache/" + subFolderName; 158 } 159 160 public static final void startCache(final Context context, final boolean checkthumbnails) { 161 final Locale locale = getLocaleForAlbumCache(); 162 final Locale defaultLocale = Locale.getDefault(); 163 if (locale == null || !locale.equals(defaultLocale)) { 164 sAlbumCache.deleteAll(); 165 putLocaleForAlbumCache(defaultLocale); 166 } 167 final Intent intent = new Intent(ACTION_CACHE, null, context, CacheService.class); 168 intent.putExtra("checkthumbnails", checkthumbnails); 169 context.startService(intent); 170 } 171 172 public static final boolean isCacheReady(final boolean onlyMediaSets) { 173 if (onlyMediaSets) { 174 return (sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0) != null && sAlbumCache.get(ALBUM_CACHE_DIRTY_INDEX, 0) == null); 175 } else { 176 return (sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0) != null && sAlbumCache.get(ALBUM_CACHE_DIRTY_INDEX, 0) == null && sAlbumCache 177 .get(ALBUM_CACHE_DIRTY_BUCKET_INDEX, 0) == null); 178 } 179 } 180 181 public static final boolean isPresentInCache(final long setId) { 182 return sAlbumCache.get(setId, 0) != null; 183 } 184 185 public static final void markDirty() { 186 sList = null; 187 synchronized (sCacheLock) { 188 sAlbumCache.put(ALBUM_CACHE_DIRTY_INDEX, sDummyData, 0); 189 } 190 } 191 192 public static final void markDirty(final long id) { 193 if (id == Shared.INVALID) { 194 return; 195 } 196 sList = null; 197 synchronized (sCacheLock) { 198 byte[] data = longToByteArray(id); 199 final byte[] existingData = sAlbumCache.get(ALBUM_CACHE_DIRTY_BUCKET_INDEX, 0); 200 if (existingData != null && existingData.length > 0) { 201 final long[] ids = toLongArray(existingData); 202 final int numIds = ids.length; 203 for (int i = 0; i < numIds; ++i) { 204 if (ids[i] == id) { 205 return; 206 } 207 } 208 // Add this to the existing keys and concatenate the byte 209 // arrays. 210 data = concat(data, existingData); 211 } 212 sAlbumCache.put(ALBUM_CACHE_DIRTY_BUCKET_INDEX, data, 0); 213 } 214 } 215 216 public static final boolean setHasItems(final ContentResolver cr, final long setId) { 217 final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI; 218 final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI; 219 final StringBuffer whereString = new StringBuffer(Images.ImageColumns.BUCKET_ID + "=" + setId); 220 try { 221 final Cursor cursorImages = cr.query(uriImages, BUCKET_PROJECTION_IMAGES, whereString.toString(), null, null); 222 if (cursorImages != null && cursorImages.getCount() > 0) { 223 cursorImages.close(); 224 return true; 225 } 226 final Cursor cursorVideos = cr.query(uriVideos, BUCKET_PROJECTION_VIDEOS, whereString.toString(), null, null); 227 if (cursorVideos != null && cursorVideos.getCount() > 0) { 228 cursorVideos.close(); 229 return true; 230 } 231 } catch (Exception e) { 232 // If the database query failed for any reason 233 ; 234 } 235 return false; 236 } 237 238 public static final void loadMediaSets(final Context context, final MediaFeed feed, final DataSource source, 239 final boolean includeImages, final boolean includeVideos, final boolean moveCameraToFront) { 240 // We check to see if the Cache is ready. 241 syncCache(context); 242 final byte[] albumData = sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0); 243 if (albumData != null && albumData.length > 0) { 244 final DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(albumData), 256)); 245 try { 246 final int numAlbums = dis.readInt(); 247 for (int i = 0; i < numAlbums; ++i) { 248 final long setId = dis.readLong(); 249 final String name = Utils.readUTF(dis); 250 final boolean hasImages = dis.readBoolean(); 251 final boolean hasVideos = dis.readBoolean(); 252 MediaSet mediaSet = feed.getMediaSet(setId); 253 if (mediaSet == null) { 254 mediaSet = feed.addMediaSet(setId, source); 255 } else { 256 mediaSet.refresh(); 257 } 258 if (moveCameraToFront && mediaSet.mId == LocalDataSource.CAMERA_BUCKET_ID) { 259 feed.moveSetToFront(mediaSet); 260 } 261 if ((includeImages && hasImages) || (includeVideos && hasVideos)) { 262 mediaSet.mName = name; 263 mediaSet.mHasImages = hasImages; 264 mediaSet.mHasVideos = hasVideos; 265 mediaSet.mPicasaAlbumId = Shared.INVALID; 266 mediaSet.generateTitle(true); 267 } 268 } 269 } catch (IOException e) { 270 Log.e(TAG, "Error loading albums."); 271 sAlbumCache.deleteAll(); 272 putLocaleForAlbumCache(Locale.getDefault()); 273 } 274 } else { 275 if (DEBUG) 276 Log.d(TAG, "No albums found."); 277 } 278 } 279 280 public static final void loadMediaSet(final Context context, final MediaFeed feed, final DataSource source, final long bucketId) { 281 syncCache(context); 282 final byte[] albumData = sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0); 283 if (albumData != null && albumData.length > 0) { 284 DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(albumData), 256)); 285 try { 286 final int numAlbums = dis.readInt(); 287 for (int i = 0; i < numAlbums; ++i) { 288 final long setId = dis.readLong(); 289 MediaSet mediaSet = null; 290 if (setId == bucketId) { 291 mediaSet = feed.getMediaSet(setId); 292 if (mediaSet == null) { 293 mediaSet = feed.addMediaSet(setId, source); 294 } 295 } else { 296 mediaSet = new MediaSet(); 297 } 298 mediaSet.mName = Utils.readUTF(dis); 299 if (setId == bucketId) { 300 mediaSet.mPicasaAlbumId = Shared.INVALID; 301 mediaSet.generateTitle(true); 302 return; 303 } 304 } 305 } catch (IOException e) { 306 Log.e(TAG, "Error finding album " + bucketId); 307 sAlbumCache.deleteAll(); 308 putLocaleForAlbumCache(Locale.getDefault()); 309 } 310 } else { 311 if (DEBUG) 312 Log.d(TAG, "No album found for album id " + bucketId); 313 } 314 } 315 316 public static final void loadMediaItemsIntoMediaFeed(final Context context, final MediaFeed feed, final MediaSet set, 317 final int rangeStart, final int rangeEnd, final boolean includeImages, final boolean includeVideos) { 318 syncCache(context); 319 byte[] albumData = sAlbumCache.get(set.mId, 0); 320 if (albumData != null && set.mNumItemsLoaded < set.getNumExpectedItems()) { 321 final DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(albumData), 256)); 322 try { 323 final int numItems = dis.readInt(); 324 set.setNumExpectedItems(numItems); 325 set.mMinTimestamp = dis.readLong(); 326 set.mMaxTimestamp = dis.readLong(); 327 MediaItem reuseItem = null; 328 for (int i = 0; i < numItems; ++i) { 329 MediaItem item = (reuseItem == null) ? new MediaItem() : reuseItem; 330 // Must preserve order with method that writes to cache. 331 item.mId = dis.readLong(); 332 item.mCaption = Utils.readUTF(dis); 333 item.mMimeType = Utils.readUTF(dis); 334 item.setMediaType(dis.readInt()); 335 item.mLatitude = dis.readDouble(); 336 item.mLongitude = dis.readDouble(); 337 item.mDateTakenInMs = dis.readLong(); 338 item.mTriedRetrievingExifDateTaken = dis.readBoolean(); 339 item.mDateAddedInSec = dis.readLong(); 340 item.mDateModifiedInSec = dis.readLong(); 341 item.mDurationInSec = dis.readInt(); 342 item.mRotation = (float) dis.readInt(); 343 item.mFilePath = Utils.readUTF(dis); 344 345 // We are done reading. Now lets check to see if this item 346 // is already present in the set. 347 boolean setLookupContainsItem = set.lookupContainsItem(item); 348 if (setLookupContainsItem) { 349 reuseItem = item; 350 } else { 351 reuseItem = null; 352 } 353 int itemMediaType = item.getMediaType(); 354 if ((itemMediaType == MediaItem.MEDIA_TYPE_IMAGE && includeImages) 355 || (itemMediaType == MediaItem.MEDIA_TYPE_VIDEO && includeVideos)) { 356 String baseUri = (itemMediaType == MediaItem.MEDIA_TYPE_IMAGE) ? BASE_CONTENT_STRING_IMAGES 357 : BASE_CONTENT_STRING_VIDEOS; 358 item.mContentUri = baseUri + item.mId; 359 feed.addItemToMediaSet(item, set); 360 } 361 } 362 set.checkForDeletedItems(); 363 dis.close(); 364 } catch (IOException e) { 365 Log.e(TAG, "Error loading items for album " + set.mName); 366 sAlbumCache.deleteAll(); 367 putLocaleForAlbumCache(Locale.getDefault()); 368 } 369 } else { 370 if (DEBUG) 371 Log.d(TAG, "No items found for album " + set.mName); 372 } 373 set.updateNumExpectedItems(); 374 set.generateTitle(true); 375 } 376 377 private static void syncCache(Context context) { 378 if (!isCacheReady(true)) { 379 // In this case, we should try to show a toast 380 if (context instanceof Gallery) { 381 App.get(context).showToast(context.getResources().getString(Res.string.loading_new), Toast.LENGTH_LONG); 382 } 383 if (DEBUG) 384 Log.d(TAG, "Refreshing Cache for all items"); 385 refresh(context); 386 sAlbumCache.delete(ALBUM_CACHE_DIRTY_INDEX); 387 sAlbumCache.delete(ALBUM_CACHE_DIRTY_BUCKET_INDEX); 388 } else if (!isCacheReady(false)) { 389 if (DEBUG) 390 Log.d(TAG, "Refreshing Cache for dirty items"); 391 refreshDirtySets(context); 392 sAlbumCache.delete(ALBUM_CACHE_DIRTY_BUCKET_INDEX); 393 } 394 } 395 396 public static final void populateVideoItemFromCursor(final MediaItem item, final ContentResolver cr, final Cursor cursor, 397 final String baseUri) { 398 item.setMediaType(MediaItem.MEDIA_TYPE_VIDEO); 399 populateMediaItemFromCursor(item, cr, cursor, baseUri); 400 } 401 402 public static final void populateMediaItemFromCursor(final MediaItem item, final ContentResolver cr, final Cursor cursor, 403 final String baseUri) { 404 item.mId = cursor.getLong(CacheService.MEDIA_ID_INDEX); 405 item.mCaption = cursor.getString(CacheService.MEDIA_CAPTION_INDEX); 406 item.mMimeType = cursor.getString(CacheService.MEDIA_MIME_TYPE_INDEX); 407 item.mLatitude = cursor.getDouble(CacheService.MEDIA_LATITUDE_INDEX); 408 item.mLongitude = cursor.getDouble(CacheService.MEDIA_LONGITUDE_INDEX); 409 item.mDateTakenInMs = cursor.getLong(CacheService.MEDIA_DATE_TAKEN_INDEX); 410 item.mDateAddedInSec = cursor.getLong(CacheService.MEDIA_DATE_ADDED_INDEX); 411 item.mDateModifiedInSec = cursor.getLong(CacheService.MEDIA_DATE_MODIFIED_INDEX); 412 if (item.mDateTakenInMs == item.mDateModifiedInSec) { 413 item.mDateTakenInMs = item.mDateModifiedInSec * 1000; 414 } 415 item.mFilePath = cursor.getString(CacheService.MEDIA_DATA_INDEX); 416 if (baseUri != null) 417 item.mContentUri = baseUri + item.mId; 418 final int itemMediaType = item.getMediaType(); 419 final int orientationDurationValue = cursor.getInt(CacheService.MEDIA_ORIENTATION_OR_DURATION_INDEX); 420 if (itemMediaType == MediaItem.MEDIA_TYPE_IMAGE) { 421 item.mRotation = orientationDurationValue; 422 } else { 423 item.mDurationInSec = orientationDurationValue; 424 } 425 } 426 427 // Returns -1 if we failed to examine EXIF information or EXIF parsing 428 // failed. 429 public static final long fetchDateTaken(final MediaItem item) { 430 if (!item.isDateTakenValid() && !item.mTriedRetrievingExifDateTaken 431 && (item.mFilePath.endsWith(".jpg") || item.mFilePath.endsWith(".jpeg"))) { 432 try { 433 if (DEBUG) 434 Log.i(TAG, "Parsing date taken from exif"); 435 final ExifInterface exif = new ExifInterface(item.mFilePath); 436 final String dateTakenStr = exif.getAttribute(ExifInterface.TAG_DATETIME); 437 if (dateTakenStr != null) { 438 try { 439 final Date dateTaken = mDateFormat.parse(dateTakenStr); 440 return dateTaken.getTime(); 441 } catch (ParseException pe) { 442 try { 443 final Date dateTaken = mAltDateFormat.parse(dateTakenStr); 444 return dateTaken.getTime(); 445 } catch (ParseException pe2) { 446 if (DEBUG) 447 Log.i(TAG, "Unable to parse date out of string - " + dateTakenStr); 448 } 449 } 450 } 451 } catch (Exception e) { 452 if (DEBUG) 453 Log.i(TAG, "Error reading Exif information, probably not a jpeg."); 454 } 455 456 // Ensures that we only try retrieving EXIF date taken once. 457 item.mTriedRetrievingExifDateTaken = true; 458 } 459 return -1L; 460 } 461 462 public static final byte[] queryThumbnail(final Context context, final long thumbId, final long origId, final boolean isVideo, 463 final long timestamp) { 464 final DiskCache thumbnailCache = (isVideo) ? LocalDataSource.sThumbnailCacheVideo : LocalDataSource.sThumbnailCache; 465 return queryThumbnail(context, thumbId, origId, isVideo, thumbnailCache, timestamp); 466 } 467 468 public static final ImageList getImageList(final Context context) { 469 if (sList != null) 470 return sList; 471 ImageList list = new ImageList(); 472 final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI; 473 final ContentResolver cr = context.getContentResolver(); 474 try { 475 final Cursor cursorImages = cr.query(uriImages, THUMBNAIL_PROJECTION, null, null, null); 476 if (cursorImages != null && cursorImages.moveToFirst()) { 477 final int size = cursorImages.getCount(); 478 final long[] ids = new long[size]; 479 final long[] thumbnailIds = new long[size]; 480 final long[] timestamp = new long[size]; 481 final int[] orientation = new int[size]; 482 int ctr = 0; 483 do { 484 if (Thread.interrupted()) { 485 break; 486 } 487 ids[ctr] = cursorImages.getLong(THUMBNAIL_ID_INDEX); 488 timestamp[ctr] = cursorImages.getLong(THUMBNAIL_DATE_MODIFIED_INDEX); 489 thumbnailIds[ctr] = Utils.Crc64Long(cursorImages.getString(THUMBNAIL_DATA_INDEX)); 490 orientation[ctr] = cursorImages.getInt(THUMBNAIL_ORIENTATION_INDEX); 491 ++ctr; 492 } while (cursorImages.moveToNext()); 493 cursorImages.close(); 494 list.ids = ids; 495 list.thumbids = thumbnailIds; 496 list.timestamp = timestamp; 497 list.orientation = orientation; 498 } 499 } catch (Exception e) { 500 // If the database operation failed for any reason 501 ; 502 } 503 if (sList == null) { 504 sList = list; 505 } 506 return list; 507 } 508 509 private static final byte[] queryThumbnail(final Context context, final long thumbId, final long origId, final boolean isVideo, 510 final DiskCache thumbnailCache, final long timestamp) { 511 if (!App.get(context).isPaused()) { 512 final Thread thumbnailThread = THUMBNAIL_THREAD.getAndSet(null); 513 if (thumbnailThread != null) { 514 thumbnailThread.interrupt(); 515 } 516 } 517 byte[] bitmap = thumbnailCache.get(thumbId, timestamp); 518 if (bitmap == null) { 519 final long time = SystemClock.uptimeMillis(); 520 bitmap = buildThumbnailForId(context, thumbnailCache, thumbId, origId, isVideo, DEFAULT_THUMBNAIL_WIDTH, 521 DEFAULT_THUMBNAIL_HEIGHT, timestamp); 522 if (DEBUG) 523 Log.i(TAG, "Built thumbnail and screennail for " + origId + " in " + (SystemClock.uptimeMillis() - time)); 524 } 525 return bitmap; 526 } 527 528 private static final void buildThumbnails(final Context context) { 529 if (DEBUG) 530 Log.i(TAG, "Preparing DiskCache for all thumbnails."); 531 ImageList list = getImageList(context); 532 final int size = (list.ids == null) ? 0 : list.ids.length; 533 final long[] ids = list.ids; 534 final long[] timestamp = list.timestamp; 535 final long[] thumbnailIds = list.thumbids; 536 final DiskCache thumbnailCache = LocalDataSource.sThumbnailCache; 537 for (int i = 0; i < size; ++i) { 538 if (Thread.interrupted()) { 539 return; 540 } 541 final long id = ids[i]; 542 final long timeModifiedInSec = timestamp[i]; 543 final long thumbnailId = thumbnailIds[i]; 544 if (!isInThumbnailerSkipList(thumbnailId)) { 545 if (!thumbnailCache.isDataAvailable(thumbnailId, timeModifiedInSec * 1000)) { 546 byte[] retVal = buildThumbnailForId(context, thumbnailCache, thumbnailId, id, false, DEFAULT_THUMBNAIL_WIDTH, 547 DEFAULT_THUMBNAIL_HEIGHT, timeModifiedInSec * 1000); 548 if (retVal == null || retVal.length == 0) { 549 // There was an error in building the thumbnail. 550 // We record this thumbnail id 551 addToThumbnailerSkipList(thumbnailId); 552 } 553 } 554 } 555 } 556 thumbnailCache.flush(); 557 if (DEBUG) 558 Log.i(TAG, "DiskCache ready for all thumbnails."); 559 } 560 561 private static void addToThumbnailerSkipList(long thumbnailId) { 562 sSkipThumbnailIds.put(thumbnailId, sDummyData, 0); 563 sSkipThumbnailIds.flush(); 564 } 565 566 private static boolean isInThumbnailerSkipList(long thumbnailId) { 567 if (sSkipThumbnailIds != null && sSkipThumbnailIds.isDataAvailable(thumbnailId, 0)) { 568 byte[] data = sSkipThumbnailIds.get(thumbnailId, 0); 569 if (data != null && data.length > 0) { 570 return true; 571 } 572 } 573 return false; 574 } 575 576 private static final byte[] buildThumbnailForId(final Context context, final DiskCache thumbnailCache, final long thumbId, 577 final long origId, final boolean isVideo, final int thumbnailWidth, final int thumbnailHeight, final long timestamp) { 578 if (origId == Shared.INVALID) { 579 return null; 580 } 581 try { 582 Bitmap bitmap = null; 583 Thread.sleep(1); 584 new Thread() { 585 public void run() { 586 try { 587 Thread.sleep(5000); 588 } catch (InterruptedException e) { 589 ; 590 } 591 try { 592 if (isVideo) { 593 MediaStore.Video.Thumbnails.cancelThumbnailRequest(context.getContentResolver(), origId); 594 } else { 595 MediaStore.Images.Thumbnails.cancelThumbnailRequest(context.getContentResolver(), origId); 596 } 597 } catch (Exception e) { 598 ; 599 } 600 } 601 }.start(); 602 if (isVideo) { 603 bitmap = MediaStore.Video.Thumbnails.getThumbnail(context.getContentResolver(), origId, 604 MediaStore.Video.Thumbnails.MINI_KIND, null); 605 } else { 606 bitmap = MediaStore.Images.Thumbnails.getThumbnail(context.getContentResolver(), origId, 607 MediaStore.Images.Thumbnails.MINI_KIND, null); 608 } 609 if (bitmap == null) { 610 return null; 611 } 612 final byte[] retVal = writeBitmapToCache(thumbnailCache, thumbId, origId, bitmap, thumbnailWidth, thumbnailHeight, 613 timestamp); 614 return retVal; 615 } catch (InterruptedException e) { 616 return null; 617 } 618 } 619 620 public static final byte[] writeBitmapToCache(final DiskCache thumbnailCache, final long thumbId, final long origId, 621 final Bitmap bitmap, final int thumbnailWidth, final int thumbnailHeight, final long timestamp) { 622 final int width = bitmap.getWidth(); 623 final int height = bitmap.getHeight(); 624 // Detect faces to find the focal point, otherwise fall back to the 625 // image center. 626 int focusX = width / 2; 627 int focusY = height / 2; 628 // We have commented out face detection since it slows down the 629 // generation of the thumbnail and screennail. 630 631 // final FaceDetector faceDetector = new FaceDetector(width, height, 1); 632 // final FaceDetector.Face[] faces = new FaceDetector.Face[1]; 633 // final int numFaces = faceDetector.findFaces(bitmap, faces); 634 // if (numFaces > 0 && faces[0].confidence() >= 635 // FaceDetector.Face.CONFIDENCE_THRESHOLD) { 636 // final PointF midPoint = new PointF(); 637 // faces[0].getMidPoint(midPoint); 638 // focusX = (int) midPoint.x; 639 // focusY = (int) midPoint.y; 640 // } 641 642 // Crop to thumbnail aspect ratio biased towards the focus point. 643 int cropX; 644 int cropY; 645 int cropWidth; 646 int cropHeight; 647 float scaleFactor; 648 if (thumbnailWidth * height < thumbnailHeight * width) { 649 // Vertically constrained. 650 cropWidth = thumbnailWidth * height / thumbnailHeight; 651 cropX = Math.max(0, Math.min(focusX - cropWidth / 2, width - cropWidth)); 652 cropY = 0; 653 cropHeight = height; 654 scaleFactor = (float) thumbnailHeight / height; 655 } else { 656 // Horizontally constrained. 657 cropHeight = thumbnailHeight * width / thumbnailWidth; 658 cropY = Math.max(0, Math.min(focusY - cropHeight / 2, height - cropHeight)); 659 cropX = 0; 660 cropWidth = width; 661 scaleFactor = (float) thumbnailWidth / width; 662 } 663 final Bitmap finalBitmap = Bitmap.createBitmap(thumbnailWidth, thumbnailHeight, Bitmap.Config.RGB_565); 664 final Canvas canvas = new Canvas(finalBitmap); 665 final Paint paint = new Paint(); 666 paint.setDither(true); 667 paint.setFilterBitmap(true); 668 canvas.drawColor(0); 669 canvas.drawBitmap(bitmap, new Rect(cropX, cropY, cropX + cropWidth, cropY + cropHeight), new Rect(0, 0, thumbnailWidth, 670 thumbnailHeight), paint); 671 bitmap.recycle(); 672 673 // Store (long thumbnailId, short focusX, short focusY, JPEG data). 674 final ByteArrayOutputStream cacheOutput = new ByteArrayOutputStream(16384); 675 final DataOutputStream dataOutput = new DataOutputStream(cacheOutput); 676 byte[] retVal = null; 677 try { 678 dataOutput.writeLong(origId); 679 dataOutput.writeShort((int) ((focusX - cropX) * scaleFactor)); 680 dataOutput.writeShort((int) ((focusY - cropY) * scaleFactor)); 681 dataOutput.flush(); 682 finalBitmap.compress(Bitmap.CompressFormat.JPEG, 80, cacheOutput); 683 retVal = cacheOutput.toByteArray(); 684 synchronized (thumbnailCache) { 685 thumbnailCache.put(thumbId, retVal, timestamp); 686 } 687 cacheOutput.close(); 688 finalBitmap.recycle(); 689 } catch (Exception e) { 690 ; 691 } 692 return retVal; 693 } 694 695 public CacheService() { 696 super("CacheService"); 697 } 698 699 @Override 700 protected void onHandleIntent(final Intent intent) { 701 if (DEBUG) 702 Log.i(TAG, "Starting CacheService"); 703 if (Environment.getExternalStorageState() == Environment.MEDIA_BAD_REMOVAL) { 704 sAlbumCache.deleteAll(); 705 putLocaleForAlbumCache(Locale.getDefault()); 706 } 707 Locale locale = getLocaleForAlbumCache(); 708 if (locale != null && locale.equals(Locale.getDefault())) { 709 710 } else { 711 // The locale has changed, we need to regenerate the strings. 712 markDirty(); 713 } 714 if (intent.getBooleanExtra("checkthumbnails", false)) { 715 startNewThumbnailThread(this); 716 } else { 717 final Thread existingThread = THUMBNAIL_THREAD.getAndSet(null); 718 if (existingThread != null) { 719 existingThread.interrupt(); 720 } 721 } 722 } 723 724 private static final void putLocaleForAlbumCache(final Locale locale) { 725 final ByteArrayOutputStream bos = new ByteArrayOutputStream(); 726 final DataOutputStream dos = new DataOutputStream(bos); 727 try { 728 Utils.writeUTF(dos, locale.getCountry()); 729 Utils.writeUTF(dos, locale.getLanguage()); 730 Utils.writeUTF(dos, locale.getVariant()); 731 dos.flush(); 732 bos.flush(); 733 final byte[] data = bos.toByteArray(); 734 sAlbumCache.put(ALBUM_CACHE_LOCALE_INDEX, data, 0); 735 sAlbumCache.flush(); 736 dos.close(); 737 bos.close(); 738 } catch (IOException e) { 739 // Could not write locale to cache. 740 if (DEBUG) 741 Log.i(TAG, "Error writing locale to cache."); 742 ; 743 } 744 } 745 746 private static final Locale getLocaleForAlbumCache() { 747 final byte[] data = sAlbumCache.get(ALBUM_CACHE_LOCALE_INDEX, 0); 748 if (data != null && data.length > 0) { 749 ByteArrayInputStream bis = new ByteArrayInputStream(data); 750 DataInputStream dis = new DataInputStream(bis); 751 try { 752 String country = Utils.readUTF(dis); 753 if (country == null) 754 country = ""; 755 String language = Utils.readUTF(dis); 756 if (language == null) 757 language = ""; 758 String variant = Utils.readUTF(dis); 759 if (variant == null) 760 variant = ""; 761 final Locale locale = new Locale(language, country, variant); 762 dis.close(); 763 bis.close(); 764 return locale; 765 } catch (IOException e) { 766 // Could not read locale in cache. 767 if (DEBUG) 768 Log.i(TAG, "Error reading locale from cache."); 769 return null; 770 } 771 } 772 return null; 773 } 774 775 private static final void restartThread(final AtomicReference<Thread> threadRef, final String name, final Runnable action) { 776 // Create a new thread. 777 final Thread newThread = new Thread() { 778 public void run() { 779 try { 780 action.run(); 781 } finally { 782 threadRef.compareAndSet(this, null); 783 } 784 } 785 }; 786 newThread.setName(name); 787 newThread.start(); 788 789 // Interrupt any existing thread. 790 final Thread existingThread = threadRef.getAndSet(newThread); 791 if (existingThread != null) { 792 existingThread.interrupt(); 793 } 794 } 795 796 public static final void startNewThumbnailThread(final Context context) { 797 restartThread(THUMBNAIL_THREAD, "ThumbnailRefresh", new Runnable() { 798 public void run() { 799 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 800 try { 801 // It is an optimization to prevent the thumbnailer from 802 // running while the application loads 803 Thread.sleep(THUMBNAILER_WAIT_IN_MS); 804 } catch (InterruptedException e) { 805 return; 806 } 807 CacheService.buildThumbnails(context); 808 } 809 }); 810 } 811 812 private static final byte[] concat(final byte[] A, final byte[] B) { 813 final byte[] C = (byte[]) new byte[A.length + B.length]; 814 System.arraycopy(A, 0, C, 0, A.length); 815 System.arraycopy(B, 0, C, A.length, B.length); 816 return C; 817 } 818 819 private static final long[] toLongArray(final byte[] data) { 820 final ByteBuffer bBuffer = ByteBuffer.wrap(data); 821 final LongBuffer lBuffer = bBuffer.asLongBuffer(); 822 final int numLongs = lBuffer.capacity(); 823 final long[] retVal = new long[numLongs]; 824 for (int i = 0; i < numLongs; ++i) { 825 retVal[i] = lBuffer.get(i); 826 } 827 return retVal; 828 } 829 830 private static final byte[] longToByteArray(final long l) { 831 final byte[] bArray = new byte[8]; 832 final ByteBuffer bBuffer = ByteBuffer.wrap(bArray); 833 final LongBuffer lBuffer = bBuffer.asLongBuffer(); 834 lBuffer.put(0, l); 835 return bArray; 836 } 837 838 private static final byte[] longArrayToByteArray(final long[] l) { 839 final byte[] bArray = new byte[8 * l.length]; 840 final ByteBuffer bBuffer = ByteBuffer.wrap(bArray); 841 final LongBuffer lBuffer = bBuffer.asLongBuffer(); 842 int numLongs = l.length; 843 for (int i = 0; i < numLongs; ++i) { 844 lBuffer.put(i, l[i]); 845 } 846 return bArray; 847 } 848 849 private final static void refresh(final Context context) { 850 // First we build the album cache. 851 // This is the meta-data about the albums / buckets on the SD card. 852 if (DEBUG) 853 Log.i(TAG, "Refreshing cache."); 854 synchronized (sCacheLock) { 855 sAlbumCache.deleteAll(); 856 putLocaleForAlbumCache(Locale.getDefault()); 857 858 final ArrayList<MediaSet> sets = new ArrayList<MediaSet>(); 859 LongSparseArray<MediaSet> acceleratedSets = new LongSparseArray<MediaSet>(); 860 if (DEBUG) 861 Log.i(TAG, "Building albums."); 862 final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI.buildUpon().appendQueryParameter("distinct", "true").build(); 863 final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI.buildUpon().appendQueryParameter("distinct", "true").build(); 864 final ContentResolver cr = context.getContentResolver(); 865 try { 866 final Cursor cursorImages = cr.query(uriImages, BUCKET_PROJECTION_IMAGES, null, null, DEFAULT_BUCKET_SORT_ORDER); 867 final Cursor cursorVideos = cr.query(uriVideos, BUCKET_PROJECTION_VIDEOS, null, null, DEFAULT_BUCKET_SORT_ORDER); 868 Cursor[] cursors = new Cursor[2]; 869 cursors[0] = cursorImages; 870 cursors[1] = cursorVideos; 871 final SortCursor sortCursor = new SortCursor(cursors, Images.ImageColumns.BUCKET_DISPLAY_NAME, 872 SortCursor.TYPE_STRING, true); 873 try { 874 if (sortCursor != null && sortCursor.moveToFirst()) { 875 sets.ensureCapacity(sortCursor.getCount()); 876 acceleratedSets = new LongSparseArray<MediaSet>(sortCursor.getCount()); 877 MediaSet cameraSet = new MediaSet(); 878 cameraSet.mId = LocalDataSource.CAMERA_BUCKET_ID; 879 cameraSet.mName = context.getResources().getString(Res.string.camera); 880 sets.add(cameraSet); 881 acceleratedSets.put(cameraSet.mId, cameraSet); 882 do { 883 if (Thread.interrupted()) { 884 return; 885 } 886 long setId = sortCursor.getLong(BUCKET_ID_INDEX); 887 MediaSet mediaSet = findSet(setId, acceleratedSets); 888 if (mediaSet == null) { 889 mediaSet = new MediaSet(); 890 mediaSet.mId = setId; 891 mediaSet.mName = sortCursor.getString(BUCKET_NAME_INDEX); 892 sets.add(mediaSet); 893 acceleratedSets.put(setId, mediaSet); 894 } 895 mediaSet.mHasImages |= (sortCursor.getCurrentCursorIndex() == 0); 896 mediaSet.mHasVideos |= (sortCursor.getCurrentCursorIndex() == 1); 897 } while (sortCursor.moveToNext()); 898 sortCursor.close(); 899 } 900 } finally { 901 if (sortCursor != null) 902 sortCursor.close(); 903 } 904 writeSetsToCache(sets); 905 if (DEBUG) 906 Log.i(TAG, "Done building albums."); 907 // Now we must cache the items contained in every album / 908 // bucket. 909 populateMediaItemsForSets(context, sets, acceleratedSets, false); 910 } catch (Exception e) { 911 // If the database operation failed for any reason. 912 ; 913 } 914 } 915 } 916 917 private final static void refreshDirtySets(final Context context) { 918 synchronized (sCacheLock) { 919 final byte[] existingData = sAlbumCache.get(ALBUM_CACHE_DIRTY_BUCKET_INDEX, 0); 920 if (existingData != null && existingData.length > 0) { 921 final long[] ids = toLongArray(existingData); 922 final int numIds = ids.length; 923 if (numIds > 0) { 924 final ArrayList<MediaSet> sets = new ArrayList<MediaSet>(numIds); 925 final LongSparseArray<MediaSet> acceleratedSets = new LongSparseArray<MediaSet>(numIds); 926 for (int i = 0; i < numIds; ++i) { 927 final MediaSet set = new MediaSet(); 928 set.mId = ids[i]; 929 sets.add(set); 930 acceleratedSets.put(set.mId, set); 931 } 932 if (DEBUG) 933 Log.i(TAG, "Refreshing dirty albums"); 934 populateMediaItemsForSets(context, sets, acceleratedSets, true); 935 } 936 } 937 sAlbumCache.delete(ALBUM_CACHE_DIRTY_BUCKET_INDEX); 938 } 939 } 940 941 public static final long[] computeDirtySets(final Context context) { 942 final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI; 943 final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI; 944 final ContentResolver cr = context.getContentResolver(); 945 final String where = Images.ImageColumns.BUCKET_ID + "!=0) GROUP BY (" + Images.ImageColumns.BUCKET_ID + " "; 946 ArrayList<Long> retVal = new ArrayList<Long>(); 947 try { 948 final Cursor cursorImages = cr.query(uriImages, SENSE_PROJECTION, where, null, null); 949 final Cursor cursorVideos = cr.query(uriVideos, SENSE_PROJECTION, where, null, null); 950 Cursor[] cursors = new Cursor[2]; 951 cursors[0] = cursorImages; 952 cursors[1] = cursorVideos; 953 final MergeCursor cursor = new MergeCursor(cursors); 954 final ArrayList<Long> setIds = new ArrayList<Long>(); 955 final ArrayList<Long> maxAdded = new ArrayList<Long>(); 956 final ArrayList<Integer> counts = new ArrayList<Integer>(); 957 try { 958 if (cursor.moveToFirst()) { 959 do { 960 final long setId = cursor.getLong(0); 961 final long maxAdd = cursor.getLong(1); 962 final int count = cursor.getInt(2); 963 // We check to see if this id is already present. 964 if (setIds.contains(setId)) { 965 int index = setIds.indexOf(setId); 966 if (maxAdded.get(index) < maxAdd) { 967 maxAdded.set(index, maxAdd); 968 } 969 counts.set(index, counts.get(index) + count); 970 } else { 971 setIds.add(setId); 972 maxAdded.add(maxAdd); 973 counts.add(count); 974 } 975 } while (cursor.moveToNext()); 976 } 977 final int numSets = setIds.size(); 978 int ctr = 0; 979 if (numSets > 0) { 980 boolean allDirty = false; 981 do { 982 long setId = setIds.get(ctr); 983 if (allDirty) { 984 addNoDupe(retVal, setId); 985 } else { 986 boolean contains = sAlbumCache.isDataAvailable(setId, 0); 987 if (!contains) { 988 // We need to refresh everything. 989 markDirty(); 990 addNoDupe(retVal, setId); 991 allDirty = true; 992 } 993 if (!allDirty) { 994 long maxAdd = maxAdded.get(ctr); 995 int count = counts.get(ctr); 996 byte[] data = sMetaAlbumCache.get(setId, 0); 997 long[] dataLong = new long[2]; 998 if (data != null) { 999 dataLong = toLongArray(data); 1000 } 1001 long oldMaxAdded = dataLong[0]; 1002 long oldCount = dataLong[1]; 1003 if (maxAdd > oldMaxAdded || oldCount != count) { 1004 markDirty(setId); 1005 addNoDupe(retVal, setId); 1006 dataLong[0] = maxAdd; 1007 dataLong[1] = count; 1008 sMetaAlbumCache.put(setId, longArrayToByteArray(dataLong), 0); 1009 } 1010 } 1011 } 1012 ++ctr; 1013 } while (ctr < numSets); 1014 // We now check for any deleted sets. 1015 final byte[] albumData = sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0); 1016 if (albumData != null && albumData.length > 0) { 1017 final DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(albumData), 256)); 1018 try { 1019 final int numAlbums = dis.readInt(); 1020 for (int i = 0; i < numAlbums; ++i) { 1021 final long setId = dis.readLong(); 1022 Utils.readUTF(dis); 1023 dis.readBoolean(); 1024 dis.readBoolean(); 1025 if (!setIds.contains(setId)) { 1026 // This set was deleted, we need to recompute the cache. 1027 markDirty(); 1028 break; 1029 } 1030 } 1031 } catch (Exception e) { 1032 ; 1033 } 1034 } 1035 } 1036 } finally { 1037 cursor.close(); 1038 } 1039 sMetaAlbumCache.flush(); 1040 } catch (Exception e) { 1041 // If the database operation failed for any reason. 1042 ; 1043 } 1044 int numIds = retVal.size(); 1045 long retValIds[] = new long[numIds]; 1046 for (int i = 0; i < numIds; ++i) { 1047 retValIds[i] = retVal.get(i); 1048 } 1049 return retValIds; 1050 } 1051 1052 private static final void addNoDupe(ArrayList<Long> array, long value) { 1053 int size = array.size(); 1054 for (int i = 0; i < size; ++i) { 1055 if (array.get(i).longValue() == value) 1056 return; 1057 } 1058 array.add(value); 1059 } 1060 1061 private final static void populateMediaItemsForSets(final Context context, final ArrayList<MediaSet> sets, 1062 final LongSparseArray<MediaSet> acceleratedSets, boolean useWhere) { 1063 if (sets == null || sets.size() == 0 || Thread.interrupted()) { 1064 return; 1065 } 1066 if (DEBUG) 1067 Log.i(TAG, "Building items."); 1068 final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI; 1069 final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI; 1070 final ContentResolver cr = context.getContentResolver(); 1071 1072 String whereClause = null; 1073 if (useWhere) { 1074 int numSets = sets.size(); 1075 StringBuffer whereString = new StringBuffer(Images.ImageColumns.BUCKET_ID + " in ("); 1076 for (int i = 0; i < numSets; ++i) { 1077 whereString.append(sets.get(i).mId); 1078 if (i != numSets - 1) { 1079 whereString.append(","); 1080 } 1081 } 1082 whereString.append(")"); 1083 whereClause = whereString.toString(); 1084 if (DEBUG) 1085 Log.i(TAG, "Updating dirty albums where " + whereClause); 1086 } 1087 try { 1088 final Cursor cursorImages = cr.query(uriImages, PROJECTION_IMAGES, whereClause, null, DEFAULT_IMAGE_SORT_ORDER); 1089 final Cursor cursorVideos = cr.query(uriVideos, PROJECTION_VIDEOS, whereClause, null, DEFAULT_VIDEO_SORT_ORDER); 1090 final Cursor[] cursors = new Cursor[2]; 1091 cursors[0] = cursorImages; 1092 cursors[1] = cursorVideos; 1093 final SortCursor sortCursor = new SortCursor(cursors, Images.ImageColumns.DATE_TAKEN, SortCursor.TYPE_NUMERIC, true); 1094 if (Thread.interrupted()) { 1095 return; 1096 } 1097 try { 1098 if (sortCursor != null && sortCursor.moveToFirst()) { 1099 final int count = sortCursor.getCount(); 1100 final int numSets = sets.size(); 1101 final int approximateCountPerSet = count / numSets; 1102 for (int i = 0; i < numSets; ++i) { 1103 final MediaSet set = sets.get(i); 1104 set.setNumExpectedItems(approximateCountPerSet); 1105 } 1106 do { 1107 if (Thread.interrupted()) { 1108 return; 1109 } 1110 final MediaItem item = new MediaItem(); 1111 final boolean isVideo = (sortCursor.getCurrentCursorIndex() == 1); 1112 if (isVideo) { 1113 populateVideoItemFromCursor(item, cr, sortCursor, CacheService.BASE_CONTENT_STRING_VIDEOS); 1114 } else { 1115 populateMediaItemFromCursor(item, cr, sortCursor, CacheService.BASE_CONTENT_STRING_IMAGES); 1116 } 1117 final long setId = sortCursor.getLong(MEDIA_BUCKET_ID_INDEX); 1118 final MediaSet set = findSet(setId, acceleratedSets); 1119 if (set != null) { 1120 set.addItem(item); 1121 } 1122 } while (sortCursor.moveToNext()); 1123 } 1124 } finally { 1125 if (sortCursor != null) 1126 sortCursor.close(); 1127 } 1128 } catch (Exception e) { 1129 // If the database operation failed for any reason 1130 ; 1131 } 1132 if (sets.size() > 0) { 1133 writeItemsToCache(sets); 1134 if (DEBUG) 1135 Log.i(TAG, "Done building items."); 1136 } 1137 } 1138 1139 private static final void writeSetsToCache(final ArrayList<MediaSet> sets) { 1140 final ByteArrayOutputStream bos = new ByteArrayOutputStream(); 1141 final int numSets = sets.size(); 1142 final DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(bos, 256)); 1143 try { 1144 dos.writeInt(numSets); 1145 for (int i = 0; i < numSets; ++i) { 1146 if (Thread.interrupted()) { 1147 return; 1148 } 1149 final MediaSet set = sets.get(i); 1150 dos.writeLong(set.mId); 1151 Utils.writeUTF(dos, set.mName); 1152 dos.writeBoolean(set.mHasImages); 1153 dos.writeBoolean(set.mHasVideos); 1154 } 1155 dos.flush(); 1156 sAlbumCache.put(ALBUM_CACHE_METADATA_INDEX, bos.toByteArray(), 0); 1157 dos.close(); 1158 if (numSets == 0) { 1159 sAlbumCache.deleteAll(); 1160 putLocaleForAlbumCache(Locale.getDefault()); 1161 } 1162 sAlbumCache.flush(); 1163 } catch (IOException e) { 1164 Log.e(TAG, "Error writing albums to diskcache."); 1165 sAlbumCache.deleteAll(); 1166 putLocaleForAlbumCache(Locale.getDefault()); 1167 } 1168 } 1169 1170 private static final void writeItemsToCache(final ArrayList<MediaSet> sets) { 1171 final int numSets = sets.size(); 1172 for (int i = 0; i < numSets; ++i) { 1173 if (Thread.interrupted()) { 1174 return; 1175 } 1176 writeItemsForASet(sets.get(i)); 1177 } 1178 sAlbumCache.flush(); 1179 } 1180 1181 private static final void writeItemsForASet(final MediaSet set) { 1182 final ByteArrayOutputStream bos = new ByteArrayOutputStream(); 1183 final DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(bos, 256)); 1184 try { 1185 final ArrayList<MediaItem> items = set.getItems(); 1186 final int numItems = items.size(); 1187 dos.writeInt(numItems); 1188 dos.writeLong(set.mMinTimestamp); 1189 dos.writeLong(set.mMaxTimestamp); 1190 for (int i = 0; i < numItems; ++i) { 1191 MediaItem item = items.get(i); 1192 if (set.mId == LocalDataSource.CAMERA_BUCKET_ID || set.mId == LocalDataSource.DOWNLOAD_BUCKET_ID) { 1193 // Reverse the display order for the camera bucket - want 1194 // the latest first. 1195 item = items.get(numItems - i - 1); 1196 } 1197 dos.writeLong(item.mId); 1198 Utils.writeUTF(dos, item.mCaption); 1199 Utils.writeUTF(dos, item.mMimeType); 1200 dos.writeInt(item.getMediaType()); 1201 dos.writeDouble(item.mLatitude); 1202 dos.writeDouble(item.mLongitude); 1203 dos.writeLong(item.mDateTakenInMs); 1204 dos.writeBoolean(item.mTriedRetrievingExifDateTaken); 1205 dos.writeLong(item.mDateAddedInSec); 1206 dos.writeLong(item.mDateModifiedInSec); 1207 dos.writeInt(item.mDurationInSec); 1208 dos.writeInt((int) item.mRotation); 1209 Utils.writeUTF(dos, item.mFilePath); 1210 } 1211 dos.flush(); 1212 sAlbumCache.put(set.mId, bos.toByteArray(), 0); 1213 dos.close(); 1214 } catch (Exception e) { 1215 Log.e(TAG, "Error writing to diskcache for set " + set.mName); 1216 sAlbumCache.deleteAll(); 1217 putLocaleForAlbumCache(Locale.getDefault()); 1218 } 1219 } 1220 1221 private static final MediaSet findSet(final long id, final LongSparseArray<MediaSet> acceleratedTable) { 1222 // This is the accelerated lookup table for the MediaSet based on set 1223 // id. 1224 return acceleratedTable.get(id); 1225 } 1226 } 1227