1 package com.cooliris.cache;
3 import java.io.BufferedInputStream;
4 import java.io.BufferedOutputStream;
5 import java.io.ByteArrayInputStream;
6 import java.io.ByteArrayOutputStream;
7 import java.io.DataInputStream;
8 import java.io.DataOutputStream;
9 import java.io.IOException;
10 import java.net.URISyntaxException;
11 import java.nio.ByteBuffer;
12 import java.nio.LongBuffer;
13 import java.text.DateFormat;
14 import java.text.ParseException;
15 import java.text.SimpleDateFormat;
16 import java.util.ArrayList;
17 import java.util.Date;
18 import java.util.Locale;
19 import java.util.concurrent.atomic.AtomicReference;
21 import android.app.IntentService;
22 import android.content.ContentResolver;
23 import android.content.ContentValues;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.database.Cursor;
27 import android.database.MergeCursor;
28 import android.graphics.Bitmap;
29 import android.graphics.Canvas;
30 import android.graphics.Paint;
31 import android.graphics.Rect;
32 import android.media.ExifInterface;
33 import android.net.Uri;
34 import android.os.Environment;
35 import android.os.Process;
36 import android.os.SystemClock;
37 import android.provider.MediaStore;
38 import android.provider.MediaStore.Images;
39 import android.provider.MediaStore.Video;
40 import android.util.Log;
42 import com.cooliris.media.DataSource;
43 import com.cooliris.media.DiskCache;
44 import com.cooliris.media.Gallery;
45 import com.cooliris.media.LocalDataSource;
46 import com.cooliris.media.LongSparseArray;
47 import com.cooliris.media.MediaFeed;
48 import com.cooliris.media.MediaItem;
49 import com.cooliris.media.MediaSet;
50 import com.cooliris.media.R;
51 import com.cooliris.media.Shared;
52 import com.cooliris.media.SortCursor;
53 import com.cooliris.media.UriTexture;
54 import com.cooliris.media.Utils;
56 public final class CacheService extends IntentService {
57 public static final String ACTION_CACHE = "com.cooliris.cache.action.CACHE";
58 public static final DiskCache sAlbumCache = new DiskCache("local-album-cache");
59 public static final DiskCache sMetaAlbumCache = new DiskCache("local-meta-cache");
60 public static final DiskCache sSkipThumbnailIds = new DiskCache("local-skip-cache");
62 private static final String TAG = "CacheService";
63 private static ImageList sList = null;
65 // Wait 2 seconds to start the thumbnailer so that the application can load
66 // without any overheads.
67 private static final int THUMBNAILER_WAIT_IN_MS = 2000;
68 private static final int DEFAULT_THUMBNAIL_WIDTH = 128;
69 private static final int DEFAULT_THUMBNAIL_HEIGHT = 96;
71 public static final String DEFAULT_IMAGE_SORT_ORDER = Images.ImageColumns.DATE_TAKEN + " ASC";
72 public static final String DEFAULT_VIDEO_SORT_ORDER = Video.VideoColumns.DATE_TAKEN + " ASC";
73 public static final String DEFAULT_BUCKET_SORT_ORDER = "upper(" + Images.ImageColumns.BUCKET_DISPLAY_NAME + ") ASC";
75 // Must preserve order between these indices and the order of the terms in
76 // BUCKET_PROJECTION_IMAGES, BUCKET_PROJECTION_VIDEOS.
77 // Not using SortedHashMap for efficieny reasons.
78 public static final int BUCKET_ID_INDEX = 0;
79 public static final int BUCKET_NAME_INDEX = 1;
80 public static final String[] BUCKET_PROJECTION_IMAGES = new String[] { Images.ImageColumns.BUCKET_ID,
81 Images.ImageColumns.BUCKET_DISPLAY_NAME };
83 public static final String[] BUCKET_PROJECTION_VIDEOS = new String[] { Video.VideoColumns.BUCKET_ID,
84 Video.VideoColumns.BUCKET_DISPLAY_NAME };
86 // Must preserve order between these indices and the order of the terms in
87 // THUMBNAIL_PROJECTION.
88 public static final int THUMBNAIL_ID_INDEX = 0;
89 public static final int THUMBNAIL_DATE_MODIFIED_INDEX = 1;
90 public static final int THUMBNAIL_DATA_INDEX = 2;
91 public static final int THUMBNAIL_ORIENTATION_INDEX = 2;
92 public static final String[] THUMBNAIL_PROJECTION = new String[] { Images.ImageColumns._ID, Images.ImageColumns.DATE_MODIFIED,
93 Images.ImageColumns.DATA, Images.ImageColumns.ORIENTATION };
95 public static final String[] SENSE_PROJECTION = new String[] { Images.ImageColumns.BUCKET_ID,
96 "MAX(" + Images.ImageColumns.DATE_ADDED + "), COUNT(*)" };
98 // Must preserve order between these indices and the order of the terms in
99 // INITIAL_PROJECTION_IMAGES and
100 // INITIAL_PROJECTION_VIDEOS.
101 public static final int MEDIA_ID_INDEX = 0;
102 public static final int MEDIA_CAPTION_INDEX = 1;
103 public static final int MEDIA_MIME_TYPE_INDEX = 2;
104 public static final int MEDIA_LATITUDE_INDEX = 3;
105 public static final int MEDIA_LONGITUDE_INDEX = 4;
106 public static final int MEDIA_DATE_TAKEN_INDEX = 5;
107 public static final int MEDIA_DATE_ADDED_INDEX = 6;
108 public static final int MEDIA_DATE_MODIFIED_INDEX = 7;
109 public static final int MEDIA_DATA_INDEX = 8;
110 public static final int MEDIA_ORIENTATION_OR_DURATION_INDEX = 9;
111 public static final int MEDIA_BUCKET_ID_INDEX = 10;
112 public static final String[] PROJECTION_IMAGES = new String[] { Images.ImageColumns._ID, Images.ImageColumns.TITLE,
113 Images.ImageColumns.MIME_TYPE, Images.ImageColumns.LATITUDE, Images.ImageColumns.LONGITUDE,
114 Images.ImageColumns.DATE_TAKEN, Images.ImageColumns.DATE_ADDED, Images.ImageColumns.DATE_MODIFIED,
115 Images.ImageColumns.DATA, Images.ImageColumns.ORIENTATION, Images.ImageColumns.BUCKET_ID };
117 private static final String[] PROJECTION_VIDEOS = new String[] { Video.VideoColumns._ID, Video.VideoColumns.TITLE,
118 Video.VideoColumns.MIME_TYPE, Video.VideoColumns.LATITUDE, Video.VideoColumns.LONGITUDE, Video.VideoColumns.DATE_TAKEN,
119 Video.VideoColumns.DATE_ADDED, Video.VideoColumns.DATE_MODIFIED, Video.VideoColumns.DATA, Video.VideoColumns.DURATION,
120 Video.VideoColumns.BUCKET_ID };
122 public static final String BASE_CONTENT_STRING_IMAGES = (Images.Media.EXTERNAL_CONTENT_URI).toString() + "/";
123 public static final String BASE_CONTENT_STRING_VIDEOS = (Video.Media.EXTERNAL_CONTENT_URI).toString() + "/";
124 private static final AtomicReference<Thread> CACHE_THREAD = new AtomicReference<Thread>();
125 private static final AtomicReference<Thread> THUMBNAIL_THREAD = new AtomicReference<Thread>();
127 // Special indices in the Albumcache.
128 private static final int ALBUM_CACHE_METADATA_INDEX = -1;
129 private static final int ALBUM_CACHE_DIRTY_INDEX = -2;
130 private static final int ALBUM_CACHE_INCOMPLETE_INDEX = -3;
131 private static final int ALBUM_CACHE_DIRTY_BUCKET_INDEX = -4;
132 private static final int ALBUM_CACHE_LOCALE_INDEX = -5;
134 private static final DateFormat mDateFormat = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
135 private static final DateFormat mAltDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
136 private static final byte[] sDummyData = new byte[] { 1 };
137 private static boolean QUEUE_DIRTY_SET;
138 private static boolean QUEUE_DIRTY_ALL;
139 private static boolean QUEUE_DIRTY_SENSE;
141 public interface Observer {
142 void onChange(long[] bucketIds);
145 public static final String getCachePath(final String subFolderName) {
146 return Environment.getExternalStorageDirectory() + "/Android/data/com.cooliris.media/cache/" + subFolderName;
149 public static final void startCache(final Context context, final boolean checkthumbnails) {
150 final Locale locale = getLocaleForAlbumCache();
151 final Locale defaultLocale = Locale.getDefault();
152 if (locale == null || !locale.equals(defaultLocale)) {
153 sAlbumCache.deleteAll();
154 putLocaleForAlbumCache(defaultLocale);
156 final Intent intent = new Intent(ACTION_CACHE, null, context, CacheService.class);
157 intent.putExtra("checkthumbnails", checkthumbnails);
158 context.startService(intent);
161 public static final boolean isCacheReady(final boolean onlyMediaSets) {
163 return (sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0) != null && sAlbumCache.get(ALBUM_CACHE_DIRTY_INDEX, 0) == null);
165 return (sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0) != null && sAlbumCache.get(ALBUM_CACHE_DIRTY_INDEX, 0) == null && sAlbumCache
166 .get(ALBUM_CACHE_INCOMPLETE_INDEX, 0) == null);
170 public static final boolean isCacheReady(final long setId) {
171 final boolean isReady = (sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0) != null
172 && sAlbumCache.get(ALBUM_CACHE_DIRTY_INDEX, 0) == null && sAlbumCache.get(ALBUM_CACHE_INCOMPLETE_INDEX, 0) == null);
176 // Also, we need to check if this setId is dirty.
177 final byte[] existingData = sAlbumCache.get(ALBUM_CACHE_DIRTY_BUCKET_INDEX, 0);
178 if (existingData != null && existingData.length > 0) {
179 final long[] ids = toLongArray(existingData);
180 final int numIds = ids.length;
181 for (int i = 0; i < numIds; ++i) {
182 if (ids[i] == setId) {
190 public static final boolean isPresentInCache(final long setId) {
191 return sAlbumCache.get(setId, 0) != null;
194 public static final void senseDirty(final Context context, final Observer observer) {
195 if (CACHE_THREAD.get() == null) {
196 QUEUE_DIRTY_SENSE = false;
197 QUEUE_DIRTY_ALL = false;
198 QUEUE_DIRTY_SET = false;
199 restartThread(CACHE_THREAD, "CacheRefresh", new Runnable() {
202 // We sleep for a bit here waiting for the provider
203 // database to insert the row(s).
204 // This should be unnecessary, and to be fixed in future
207 } catch (InterruptedException e) {
209 Log.i(TAG, "Computing dirty sets.");
210 long ids[] = computeDirtySets(context);
211 processQueuedDirty(context);
212 if (ids != null && observer != null) {
213 observer.onChange(ids);
215 if (ids != null && ids.length > 0) {
217 Log.i(TAG, "Done computing dirty sets for num " + ids.length);
222 QUEUE_DIRTY_SENSE = true;
226 public static final void markDirty(final Context context) {
228 sAlbumCache.put(ALBUM_CACHE_DIRTY_INDEX, sDummyData, 0);
229 if (CACHE_THREAD.get() == null) {
230 QUEUE_DIRTY_SENSE = false;
231 QUEUE_DIRTY_ALL = false;
232 QUEUE_DIRTY_SET = false;
233 restartThread(CACHE_THREAD, "CacheRefresh", new Runnable() {
236 processQueuedDirty(context);
240 QUEUE_DIRTY_ALL = true;
244 public static final void markDirtyImmediate(final long id) {
245 if (id == Shared.INVALID) {
249 byte[] data = longToByteArray(id);
250 final byte[] existingData = sAlbumCache.get(ALBUM_CACHE_DIRTY_BUCKET_INDEX, 0);
251 if (existingData != null && existingData.length > 0) {
252 final long[] ids = toLongArray(existingData);
253 final int numIds = ids.length;
254 for (int i = 0; i < numIds; ++i) {
259 // Add this to the existing keys and concatenate the byte arrays.
260 data = concat(data, existingData);
262 sAlbumCache.put(ALBUM_CACHE_DIRTY_BUCKET_INDEX, data, 0);
265 public static final void markDirty(final Context context, final long id) {
266 markDirtyImmediate(id);
267 if (CACHE_THREAD.get() == null) {
268 QUEUE_DIRTY_SET = false;
269 restartThread(CACHE_THREAD, "CacheRefreshDirtySets", new Runnable() {
271 refreshDirtySets(context);
272 processQueuedDirty(context);
276 QUEUE_DIRTY_SET = true;
280 public static final boolean setHasItems(final ContentResolver cr, final long setId) {
281 final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
282 final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI;
283 final StringBuffer whereString = new StringBuffer(Images.ImageColumns.BUCKET_ID + "=" + setId);
285 final Cursor cursorImages = cr.query(uriImages, BUCKET_PROJECTION_IMAGES, whereString.toString(), null, null);
286 if (cursorImages != null && cursorImages.getCount() > 0) {
287 cursorImages.close();
290 final Cursor cursorVideos = cr.query(uriVideos, BUCKET_PROJECTION_VIDEOS, whereString.toString(), null, null);
291 if (cursorVideos != null && cursorVideos.getCount() > 0) {
292 cursorVideos.close();
295 } catch (Exception e) {
296 // If the database query failed for any reason
302 public static final void loadMediaSets(final MediaFeed feed, final DataSource source, final boolean includeImages,
303 final boolean includeVideos) {
305 while (!isCacheReady(true) && timeElapsed < 10000) {
308 } catch (InterruptedException e) {
313 final byte[] albumData = sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0);
314 if (albumData != null && albumData.length > 0) {
315 final DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(albumData), 256));
317 final int numAlbums = dis.readInt();
318 for (int i = 0; i < numAlbums; ++i) {
319 final long setId = dis.readLong();
320 final String name = Utils.readUTF(dis);
321 final boolean hasImages = dis.readBoolean();
322 final boolean hasVideos = dis.readBoolean();
323 MediaSet mediaSet = feed.getMediaSet(setId);
324 if (mediaSet == null) {
325 mediaSet = feed.addMediaSet(setId, source);
327 if ((includeImages && hasImages) || (includeVideos && hasVideos)) {
328 mediaSet.mName = name;
329 mediaSet.mHasImages = hasImages;
330 mediaSet.mHasVideos = hasVideos;
331 mediaSet.mPicasaAlbumId = Shared.INVALID;
332 mediaSet.generateTitle(true);
335 } catch (IOException e) {
336 Log.e(TAG, "Error loading albums.");
337 sAlbumCache.deleteAll();
338 putLocaleForAlbumCache(Locale.getDefault());
341 Log.d(TAG, "No albums found.");
345 public static final void loadMediaSet(final MediaFeed feed, final DataSource source, final long bucketId) {
347 while (!isCacheReady(false) && timeElapsed < 10000) {
350 } catch (InterruptedException e) {
355 final byte[] albumData = sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0);
356 if (albumData != null && albumData.length > 0) {
357 DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(albumData), 256));
359 final int numAlbums = dis.readInt();
360 for (int i = 0; i < numAlbums; ++i) {
361 final long setId = dis.readLong();
362 MediaSet mediaSet = null;
363 if (setId == bucketId) {
364 mediaSet = feed.getMediaSet(setId);
365 if (mediaSet == null) {
366 mediaSet = feed.addMediaSet(setId, source);
369 mediaSet = new MediaSet();
371 mediaSet.mName = Utils.readUTF(dis);
372 if (setId == bucketId) {
373 mediaSet.mPicasaAlbumId = Shared.INVALID;
374 mediaSet.generateTitle(true);
378 } catch (IOException e) {
379 Log.e(TAG, "Error finding album " + bucketId);
380 sAlbumCache.deleteAll();
381 putLocaleForAlbumCache(Locale.getDefault());
384 Log.d(TAG, "No album found for album id " + bucketId);
388 public static final void loadMediaItemsIntoMediaFeed(final MediaFeed feed, final MediaSet set, final int rangeStart,
389 final int rangeEnd, final boolean includeImages, final boolean includeVideos) {
391 byte[] albumData = null;
392 while (!isCacheReady(set.mId) && timeElapsed < 30000) {
395 } catch (InterruptedException e) {
400 albumData = sAlbumCache.get(set.mId, 0);
401 if (albumData != null && set.mNumItemsLoaded < set.getNumExpectedItems()) {
402 final DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(albumData), 256));
404 final int numItems = dis.readInt();
405 set.setNumExpectedItems(numItems);
406 set.mMinTimestamp = dis.readLong();
407 set.mMaxTimestamp = dis.readLong();
408 for (int i = 0; i < numItems; ++i) {
409 final MediaItem item = new MediaItem();
410 // Must preserve order with method that writes to cache.
411 item.mId = dis.readLong();
412 item.mCaption = Utils.readUTF(dis);
413 item.mMimeType = Utils.readUTF(dis);
414 item.setMediaType(dis.readInt());
415 item.mLatitude = dis.readDouble();
416 item.mLongitude = dis.readDouble();
417 item.mDateTakenInMs = dis.readLong();
418 item.mTriedRetrievingExifDateTaken = dis.readBoolean();
419 item.mDateAddedInSec = dis.readLong();
420 item.mDateModifiedInSec = dis.readLong();
421 item.mDurationInSec = dis.readInt();
422 item.mRotation = (float) dis.readInt();
423 item.mFilePath = Utils.readUTF(dis);
424 int itemMediaType = item.getMediaType();
425 if ((itemMediaType == MediaItem.MEDIA_TYPE_IMAGE && includeImages)
426 || (itemMediaType == MediaItem.MEDIA_TYPE_VIDEO && includeVideos)) {
427 String baseUri = (itemMediaType == MediaItem.MEDIA_TYPE_IMAGE) ? BASE_CONTENT_STRING_IMAGES
428 : BASE_CONTENT_STRING_VIDEOS;
429 item.mContentUri = baseUri + item.mId;
430 feed.addItemToMediaSet(item, set);
434 } catch (IOException e) {
435 Log.e(TAG, "Error loading items for album " + set.mName);
436 sAlbumCache.deleteAll();
437 putLocaleForAlbumCache(Locale.getDefault());
440 Log.d(TAG, "No items found for album " + set.mName);
442 set.updateNumExpectedItems();
443 set.generateTitle(true);
446 public static final void populateVideoItemFromCursor(final MediaItem item, final ContentResolver cr, final Cursor cursor,
447 final String baseUri) {
448 item.setMediaType(MediaItem.MEDIA_TYPE_VIDEO);
449 populateMediaItemFromCursor(item, cr, cursor, baseUri);
452 public static final void populateMediaItemFromCursor(final MediaItem item, final ContentResolver cr, final Cursor cursor,
453 final String baseUri) {
454 item.mId = cursor.getLong(CacheService.MEDIA_ID_INDEX);
455 item.mCaption = cursor.getString(CacheService.MEDIA_CAPTION_INDEX);
456 item.mMimeType = cursor.getString(CacheService.MEDIA_MIME_TYPE_INDEX);
457 item.mLatitude = cursor.getDouble(CacheService.MEDIA_LATITUDE_INDEX);
458 item.mLongitude = cursor.getDouble(CacheService.MEDIA_LONGITUDE_INDEX);
459 item.mDateTakenInMs = cursor.getLong(CacheService.MEDIA_DATE_TAKEN_INDEX);
460 item.mDateAddedInSec = cursor.getLong(CacheService.MEDIA_DATE_ADDED_INDEX);
461 item.mDateModifiedInSec = cursor.getLong(CacheService.MEDIA_DATE_MODIFIED_INDEX);
462 if (item.mDateTakenInMs == item.mDateModifiedInSec) {
463 item.mDateTakenInMs = item.mDateModifiedInSec * 1000;
465 item.mFilePath = cursor.getString(CacheService.MEDIA_DATA_INDEX);
467 item.mContentUri = baseUri + item.mId;
468 final int itemMediaType = item.getMediaType();
469 // Check to see if a new date taken is available.
470 final long dateTaken = fetchDateTaken(item);
471 if (dateTaken != -1L && item.mContentUri != null) {
472 item.mDateTakenInMs = dateTaken;
473 final ContentValues values = new ContentValues();
474 if (itemMediaType == MediaItem.MEDIA_TYPE_VIDEO) {
475 values.put(Video.VideoColumns.DATE_TAKEN, item.mDateTakenInMs);
477 values.put(Images.ImageColumns.DATE_TAKEN, item.mDateTakenInMs);
480 cr.update(Uri.parse(item.mContentUri), values, null, null);
481 } catch (Exception e) {
482 // If the database operation fails for any reason.
487 final int orientationDurationValue = cursor.getInt(CacheService.MEDIA_ORIENTATION_OR_DURATION_INDEX);
488 if (itemMediaType == MediaItem.MEDIA_TYPE_IMAGE) {
489 item.mRotation = orientationDurationValue;
491 item.mDurationInSec = orientationDurationValue;
495 // Returns -1 if we failed to examine EXIF information or EXIF parsing
497 public static final long fetchDateTaken(final MediaItem item) {
498 if (!item.isDateTakenValid() && !item.mTriedRetrievingExifDateTaken
499 && (item.mFilePath.endsWith(".jpg") || item.mFilePath.endsWith(".jpeg"))) {
501 Log.i(TAG, "Parsing date taken from exif");
502 final ExifInterface exif = new ExifInterface(item.mFilePath);
503 final String dateTakenStr = exif.getAttribute(ExifInterface.TAG_DATETIME);
504 if (dateTakenStr != null) {
506 final Date dateTaken = mDateFormat.parse(dateTakenStr);
507 return dateTaken.getTime();
508 } catch (ParseException pe) {
510 final Date dateTaken = mAltDateFormat.parse(dateTakenStr);
511 return dateTaken.getTime();
512 } catch (ParseException pe2) {
513 Log.i(TAG, "Unable to parse date out of string - " + dateTakenStr);
517 } catch (Exception e) {
518 Log.i(TAG, "Error reading Exif information, probably not a jpeg.");
521 // Ensures that we only try retrieving EXIF date taken once.
522 item.mTriedRetrievingExifDateTaken = true;
527 public static final byte[] queryThumbnail(final Context context, final long thumbId, final long origId, final boolean isVideo,
528 final long timestamp) {
529 final DiskCache thumbnailCache = (isVideo) ? LocalDataSource.sThumbnailCacheVideo : LocalDataSource.sThumbnailCache;
530 return queryThumbnail(context, thumbId, origId, isVideo, thumbnailCache, timestamp);
533 public static final ImageList getImageList(final Context context) {
536 ImageList list = new ImageList();
537 final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
538 final ContentResolver cr = context.getContentResolver();
540 final Cursor cursorImages = cr.query(uriImages, THUMBNAIL_PROJECTION, null, null, null);
541 if (cursorImages != null && cursorImages.moveToFirst()) {
542 final int size = cursorImages.getCount();
543 final long[] ids = new long[size];
544 final long[] thumbnailIds = new long[size];
545 final long[] timestamp = new long[size];
546 final int[] orientation = new int[size];
549 if (Thread.interrupted()) {
552 ids[ctr] = cursorImages.getLong(THUMBNAIL_ID_INDEX);
553 timestamp[ctr] = cursorImages.getLong(THUMBNAIL_DATE_MODIFIED_INDEX);
554 thumbnailIds[ctr] = Utils.Crc64Long(cursorImages.getString(THUMBNAIL_DATA_INDEX));
555 orientation[ctr] = cursorImages.getInt(THUMBNAIL_ORIENTATION_INDEX);
557 } while (cursorImages.moveToNext());
558 cursorImages.close();
560 list.thumbids = thumbnailIds;
561 list.timestamp = timestamp;
562 list.orientation = orientation;
564 } catch (Exception e) {
565 // If the database operation failed for any reason
574 private static final byte[] queryThumbnail(final Context context, final long thumbId, final long origId, final boolean isVideo,
575 final DiskCache thumbnailCache, final long timestamp) {
576 if (!((Gallery) context).isPaused()) {
577 final Thread thumbnailThread = THUMBNAIL_THREAD.getAndSet(null);
578 if (thumbnailThread != null) {
579 thumbnailThread.interrupt();
582 byte[] bitmap = thumbnailCache.get(thumbId, timestamp);
583 if (bitmap == null) {
584 final long time = SystemClock.uptimeMillis();
585 bitmap = buildThumbnailForId(context, thumbnailCache, thumbId, origId, isVideo, DEFAULT_THUMBNAIL_WIDTH,
586 DEFAULT_THUMBNAIL_HEIGHT, timestamp);
587 Log.i(TAG, "Built thumbnail and screennail for " + origId + " in " + (SystemClock.uptimeMillis() - time));
592 private static final void buildThumbnails(final Context context) {
593 Log.i(TAG, "Preparing DiskCache for all thumbnails.");
594 ImageList list = getImageList(context);
595 final int size = (list.ids == null) ? 0 : list.ids.length;
596 final long[] ids = list.ids;
597 final long[] timestamp = list.timestamp;
598 final long[] thumbnailIds = list.thumbids;
599 final DiskCache thumbnailCache = LocalDataSource.sThumbnailCache;
600 for (int i = 0; i < size; ++i) {
601 if (Thread.interrupted()) {
604 final long id = ids[i];
605 final long timeModifiedInSec = timestamp[i];
606 final long thumbnailId = thumbnailIds[i];
607 if (!isInThumbnailerSkipList(thumbnailId)) {
608 if (!thumbnailCache.isDataAvailable(thumbnailId, timeModifiedInSec * 1000)) {
609 byte[] retVal = buildThumbnailForId(context, thumbnailCache, thumbnailId, id, false, DEFAULT_THUMBNAIL_WIDTH,
610 DEFAULT_THUMBNAIL_HEIGHT, timeModifiedInSec * 1000);
611 if (retVal == null || retVal.length == 0) {
612 // There was an error in building the thumbnail.
613 // We record this thumbnail id
614 addToThumbnailerSkipList(thumbnailId);
619 Log.i(TAG, "DiskCache ready for all thumbnails.");
622 private static void addToThumbnailerSkipList(long thumbnailId) {
623 sSkipThumbnailIds.put(thumbnailId, sDummyData, 0);
624 sSkipThumbnailIds.flush();
627 private static boolean isInThumbnailerSkipList(long thumbnailId) {
628 if (sSkipThumbnailIds.isDataAvailable(thumbnailId, 0)) {
629 byte[] data = sSkipThumbnailIds.get(thumbnailId, 0);
630 if (data.length > 0) {
637 private static final byte[] buildThumbnailForId(final Context context, final DiskCache thumbnailCache, final long thumbId,
638 final long origId, final boolean isVideo, final int thumbnailWidth, final int thumbnailHeight, final long timestamp) {
639 if (origId == Shared.INVALID) {
643 Bitmap bitmap = null;
646 final String uriString = BASE_CONTENT_STRING_IMAGES + origId;
647 UriTexture.invalidateCache(thumbId, 1024);
649 bitmap = UriTexture.createFromUri(context, uriString, 1024, 1024, thumbId, null);
650 } catch (IOException e) {
652 } catch (URISyntaxException e) {
660 } catch (InterruptedException e) {
664 MediaStore.Video.Thumbnails.cancelThumbnailRequest(context.getContentResolver(), origId);
665 } catch (Exception e) {
670 bitmap = MediaStore.Video.Thumbnails.getThumbnail(context.getContentResolver(), origId,
671 MediaStore.Video.Thumbnails.MICRO_KIND, null);
673 if (bitmap == null) {
676 final byte[] retVal = writeBitmapToCache(thumbnailCache, thumbId, origId, bitmap, thumbnailWidth, thumbnailHeight,
679 } catch (InterruptedException e) {
684 public static final byte[] writeBitmapToCache(final DiskCache thumbnailCache, final long thumbId, final long origId,
685 final Bitmap bitmap, final int thumbnailWidth, final int thumbnailHeight, final long timestamp) {
686 final int width = bitmap.getWidth();
687 final int height = bitmap.getHeight();
688 // Detect faces to find the focal point, otherwise fall back to the
690 int focusX = width / 2;
691 int focusY = height / 2;
692 // We have commented out face detection since it slows down the
693 // generation of the thumbnail and screennail.
695 // final FaceDetector faceDetector = new FaceDetector(width, height, 1);
696 // final FaceDetector.Face[] faces = new FaceDetector.Face[1];
697 // final int numFaces = faceDetector.findFaces(bitmap, faces);
698 // if (numFaces > 0 && faces[0].confidence() >=
699 // FaceDetector.Face.CONFIDENCE_THRESHOLD) {
700 // final PointF midPoint = new PointF();
701 // faces[0].getMidPoint(midPoint);
702 // focusX = (int) midPoint.x;
703 // focusY = (int) midPoint.y;
706 // Crop to thumbnail aspect ratio biased towards the focus point.
712 if (thumbnailWidth * height < thumbnailHeight * width) {
713 // Vertically constrained.
714 cropWidth = thumbnailWidth * height / thumbnailHeight;
715 cropX = Math.max(0, Math.min(focusX - cropWidth / 2, width - cropWidth));
718 scaleFactor = (float) thumbnailHeight / height;
720 // Horizontally constrained.
721 cropHeight = thumbnailHeight * width / thumbnailWidth;
722 cropY = Math.max(0, Math.min(focusY - cropHeight / 2, height - cropHeight));
725 scaleFactor = (float) thumbnailWidth / width;
727 final Bitmap finalBitmap = Bitmap.createBitmap(thumbnailWidth, thumbnailHeight, Bitmap.Config.RGB_565);
728 final Canvas canvas = new Canvas(finalBitmap);
729 final Paint paint = new Paint();
730 paint.setFilterBitmap(true);
732 canvas.drawBitmap(bitmap, new Rect(cropX, cropY, cropX + cropWidth, cropY + cropHeight), new Rect(0, 0, thumbnailWidth,
733 thumbnailHeight), paint);
736 // Store (long thumbnailId, short focusX, short focusY, JPEG data).
737 final ByteArrayOutputStream cacheOutput = new ByteArrayOutputStream(16384);
738 final DataOutputStream dataOutput = new DataOutputStream(cacheOutput);
739 byte[] retVal = null;
741 dataOutput.writeLong(origId);
742 dataOutput.writeShort((int) ((focusX - cropX) * scaleFactor));
743 dataOutput.writeShort((int) ((focusY - cropY) * scaleFactor));
745 finalBitmap.compress(Bitmap.CompressFormat.JPEG, 80, cacheOutput);
746 retVal = cacheOutput.toByteArray();
747 synchronized (thumbnailCache) {
748 thumbnailCache.put(thumbId, retVal, timestamp);
751 finalBitmap.recycle();
752 } catch (Exception e) {
758 public CacheService() {
759 super("CacheService");
763 protected void onHandleIntent(final Intent intent) {
764 Log.i(TAG, "Starting CacheService");
765 if (Environment.getExternalStorageState() == Environment.MEDIA_BAD_REMOVAL) {
766 sAlbumCache.deleteAll();
767 putLocaleForAlbumCache(Locale.getDefault());
769 Locale locale = getLocaleForAlbumCache();
770 if (locale != null && locale.equals(Locale.getDefault())) {
771 // The cache is in the same locale as the system locale.
772 if (!isCacheReady(false)) {
773 // The albums and their items have not yet been cached, we need
774 // to run the service.
775 startNewCacheThread();
777 startNewCacheThreadForDirtySets();
780 // The locale has changed, we need to regenerate the strings.
781 sAlbumCache.deleteAll();
782 putLocaleForAlbumCache(Locale.getDefault());
783 startNewCacheThread();
785 if (intent.getBooleanExtra("checkthumbnails", false)) {
786 startNewThumbnailThread(this);
788 final Thread existingThread = THUMBNAIL_THREAD.getAndSet(null);
789 if (existingThread != null) {
790 existingThread.interrupt();
795 private static final void putLocaleForAlbumCache(final Locale locale) {
796 final ByteArrayOutputStream bos = new ByteArrayOutputStream();
797 final DataOutputStream dos = new DataOutputStream(bos);
799 Utils.writeUTF(dos, locale.getCountry());
800 Utils.writeUTF(dos, locale.getLanguage());
801 Utils.writeUTF(dos, locale.getVariant());
804 final byte[] data = bos.toByteArray();
805 sAlbumCache.put(ALBUM_CACHE_LOCALE_INDEX, data, 0);
809 } catch (IOException e) {
810 // Could not write locale to cache.
811 Log.i(TAG, "Error writing locale to cache.");
816 private static final Locale getLocaleForAlbumCache() {
817 final byte[] data = sAlbumCache.get(ALBUM_CACHE_LOCALE_INDEX, 0);
818 if (data != null && data.length > 0) {
819 ByteArrayInputStream bis = new ByteArrayInputStream(data);
820 DataInputStream dis = new DataInputStream(bis);
822 String country = Utils.readUTF(dis);
825 String language = Utils.readUTF(dis);
826 if (language == null)
828 String variant = Utils.readUTF(dis);
831 final Locale locale = new Locale(language, country, variant);
835 } catch (IOException e) {
836 // Could not read locale in cache.
837 Log.i(TAG, "Error reading locale from cache.");
844 private static final void restartThread(final AtomicReference<Thread> threadRef, final String name, final Runnable action) {
845 // Create a new thread.
846 final Thread newThread = new Thread() {
851 threadRef.compareAndSet(this, null);
855 newThread.setName(name);
858 // Interrupt any existing thread.
859 final Thread existingThread = threadRef.getAndSet(newThread);
860 if (existingThread != null) {
861 existingThread.interrupt();
865 public static final void startNewThumbnailThread(final Context context) {
866 restartThread(THUMBNAIL_THREAD, "ThumbnailRefresh", new Runnable() {
868 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
870 // It is an optimization to prevent the thumbnailer from
871 // running while the application loads
872 Thread.sleep(THUMBNAILER_WAIT_IN_MS);
873 } catch (InterruptedException e) {
876 CacheService.buildThumbnails(context);
881 private void startNewCacheThread() {
882 restartThread(CACHE_THREAD, "CacheRefresh", new Runnable() {
884 refresh(CacheService.this);
889 private void startNewCacheThreadForDirtySets() {
890 restartThread(CACHE_THREAD, "CacheRefreshDirtySets", new Runnable() {
892 refreshDirtySets(CacheService.this);
897 private static final byte[] concat(final byte[] A, final byte[] B) {
898 final byte[] C = (byte[]) new byte[A.length + B.length];
899 System.arraycopy(A, 0, C, 0, A.length);
900 System.arraycopy(B, 0, C, A.length, B.length);
904 private static final long[] toLongArray(final byte[] data) {
905 final ByteBuffer bBuffer = ByteBuffer.wrap(data);
906 final LongBuffer lBuffer = bBuffer.asLongBuffer();
907 final int numLongs = lBuffer.capacity();
908 final long[] retVal = new long[numLongs];
909 for (int i = 0; i < numLongs; ++i) {
910 retVal[i] = lBuffer.get(i);
915 private static final byte[] longToByteArray(final long l) {
916 final byte[] bArray = new byte[8];
917 final ByteBuffer bBuffer = ByteBuffer.wrap(bArray);
918 final LongBuffer lBuffer = bBuffer.asLongBuffer();
923 private static final byte[] longArrayToByteArray(final long[] l) {
924 final byte[] bArray = new byte[8 * l.length];
925 final ByteBuffer bBuffer = ByteBuffer.wrap(bArray);
926 final LongBuffer lBuffer = bBuffer.asLongBuffer();
927 int numLongs = l.length;
928 for (int i = 0; i < numLongs; ++i) {
929 lBuffer.put(i, l[i]);
934 private final static void refresh(final Context context) {
935 // First we build the album cache.
936 // This is the meta-data about the albums / buckets on the SD card.
937 Log.i(TAG, "Refreshing cache.");
938 sAlbumCache.deleteAll();
939 putLocaleForAlbumCache(Locale.getDefault());
941 final ArrayList<MediaSet> sets = new ArrayList<MediaSet>();
942 LongSparseArray<MediaSet> acceleratedSets = new LongSparseArray<MediaSet>();
943 Log.i(TAG, "Building albums.");
944 final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI.buildUpon().appendQueryParameter("distinct", "true").build();
945 final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI.buildUpon().appendQueryParameter("distinct", "true").build();
946 final ContentResolver cr = context.getContentResolver();
948 final Cursor cursorImages = cr.query(uriImages, BUCKET_PROJECTION_IMAGES, null, null, DEFAULT_BUCKET_SORT_ORDER);
949 final Cursor cursorVideos = cr.query(uriVideos, BUCKET_PROJECTION_VIDEOS, null, null, DEFAULT_BUCKET_SORT_ORDER);
950 Cursor[] cursors = new Cursor[2];
951 cursors[0] = cursorImages;
952 cursors[1] = cursorVideos;
953 final SortCursor sortCursor = new SortCursor(cursors, Images.ImageColumns.BUCKET_DISPLAY_NAME, SortCursor.TYPE_STRING,
956 if (sortCursor != null && sortCursor.moveToFirst()) {
957 sets.ensureCapacity(sortCursor.getCount());
958 acceleratedSets = new LongSparseArray<MediaSet>(sortCursor.getCount());
959 MediaSet cameraSet = new MediaSet();
960 cameraSet.mId = LocalDataSource.CAMERA_BUCKET_ID;
961 cameraSet.mName = context.getResources().getString(R.string.camera);
963 acceleratedSets.put(cameraSet.mId, cameraSet);
965 if (Thread.interrupted()) {
968 long setId = sortCursor.getLong(BUCKET_ID_INDEX);
969 MediaSet mediaSet = findSet(setId, acceleratedSets);
970 if (mediaSet == null) {
971 mediaSet = new MediaSet();
972 mediaSet.mId = setId;
973 mediaSet.mName = sortCursor.getString(BUCKET_NAME_INDEX);
975 acceleratedSets.put(setId, mediaSet);
977 mediaSet.mHasImages |= (sortCursor.getCurrentCursorIndex() == 0);
978 mediaSet.mHasVideos |= (sortCursor.getCurrentCursorIndex() == 1);
979 } while (sortCursor.moveToNext());
983 if (sortCursor != null)
987 sAlbumCache.put(ALBUM_CACHE_INCOMPLETE_INDEX, sDummyData, 0);
988 writeSetsToCache(sets);
989 Log.i(TAG, "Done building albums.");
990 // Now we must cache the items contained in every album / bucket.
991 populateMediaItemsForSets(context, sets, acceleratedSets, false);
992 } catch (Exception e) {
993 // If the database operation failed for any reason.
996 sAlbumCache.delete(ALBUM_CACHE_INCOMPLETE_INDEX);
999 private final static void refreshDirtySets(final Context context) {
1000 final byte[] existingData = sAlbumCache.get(ALBUM_CACHE_DIRTY_BUCKET_INDEX, 0);
1001 if (existingData != null && existingData.length > 0) {
1002 final long[] ids = toLongArray(existingData);
1003 final int numIds = ids.length;
1005 final ArrayList<MediaSet> sets = new ArrayList<MediaSet>(numIds);
1006 final LongSparseArray<MediaSet> acceleratedSets = new LongSparseArray<MediaSet>(numIds);
1007 for (int i = 0; i < numIds; ++i) {
1008 final MediaSet set = new MediaSet();
1011 acceleratedSets.put(set.mId, set);
1013 Log.i(TAG, "Refreshing dirty albums");
1014 populateMediaItemsForSets(context, sets, acceleratedSets, true);
1017 sAlbumCache.delete(ALBUM_CACHE_DIRTY_BUCKET_INDEX);
1020 public static final long[] computeDirtySets(final Context context) {
1021 final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
1022 final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI;
1023 final ContentResolver cr = context.getContentResolver();
1024 final String where = Images.ImageColumns.BUCKET_ID + "!=0) GROUP BY (" + Images.ImageColumns.BUCKET_ID + " ";
1025 ArrayList<Long> retVal = new ArrayList<Long>();
1027 final Cursor cursorImages = cr.query(uriImages, SENSE_PROJECTION, where, null, null);
1028 final Cursor cursorVideos = cr.query(uriVideos, SENSE_PROJECTION, where, null, null);
1029 Cursor[] cursors = new Cursor[2];
1030 cursors[0] = cursorImages;
1031 cursors[1] = cursorVideos;
1032 final MergeCursor cursor = new MergeCursor(cursors);
1034 if (cursor.moveToFirst()) {
1035 boolean allDirty = false;
1037 long setId = cursor.getLong(0);
1039 addNoDupe(retVal, setId);
1041 boolean contains = sAlbumCache.isDataAvailable(setId, 0);
1043 // We need to refresh everything.
1045 addNoDupe(retVal, setId);
1049 long maxAdded = cursor.getLong(1);
1050 int count = cursor.getInt(2);
1051 byte[] data = sMetaAlbumCache.get(setId, 0);
1052 long[] dataLong = new long[2];
1054 dataLong = toLongArray(data);
1056 long oldMaxAdded = dataLong[0];
1057 long oldCount = dataLong[1];
1058 if (maxAdded > oldMaxAdded || oldCount != count) {
1059 markDirty(context, setId);
1060 addNoDupe(retVal, setId);
1061 dataLong[0] = maxAdded;
1062 dataLong[1] = count;
1063 sMetaAlbumCache.put(setId, longArrayToByteArray(dataLong), 0);
1067 } while (cursor.moveToNext());
1072 sMetaAlbumCache.flush();
1073 } catch (Exception e) {
1074 // If the database operation failed for any reason.
1077 int numIds = retVal.size();
1078 long retValIds[] = new long[numIds];
1079 for (int i = 0; i < numIds; ++i) {
1080 retValIds[i] = retVal.get(i);
1085 private static final void addNoDupe(ArrayList<Long> array, long value) {
1086 int size = array.size();
1087 for (int i = 0; i < size; ++i) {
1088 if (array.get(i).longValue() == value)
1094 private static final void processQueuedDirty(final Context context) {
1096 if (QUEUE_DIRTY_SENSE) {
1097 QUEUE_DIRTY_SENSE = false;
1098 QUEUE_DIRTY_ALL = false;
1099 QUEUE_DIRTY_SET = false;
1100 computeDirtySets(context);
1101 } else if (QUEUE_DIRTY_ALL) {
1102 QUEUE_DIRTY_ALL = false;
1103 QUEUE_DIRTY_SET = false;
1104 QUEUE_DIRTY_SENSE = false;
1106 } else if (QUEUE_DIRTY_SET) {
1107 QUEUE_DIRTY_SET = false;
1108 // We don't mark QUEUE_DIRTY_SENSE because a set outside the
1110 // sets might have gotten modified.
1111 refreshDirtySets(context);
1113 } while (QUEUE_DIRTY_SENSE || QUEUE_DIRTY_SET || QUEUE_DIRTY_ALL);
1116 private final static void populateMediaItemsForSets(final Context context, final ArrayList<MediaSet> sets,
1117 final LongSparseArray<MediaSet> acceleratedSets, boolean useWhere) {
1118 if (sets == null || sets.size() == 0 || Thread.interrupted()) {
1121 Log.i(TAG, "Building items.");
1122 final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
1123 final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI;
1124 final ContentResolver cr = context.getContentResolver();
1126 String whereClause = null;
1128 int numSets = sets.size();
1129 StringBuffer whereString = new StringBuffer(Images.ImageColumns.BUCKET_ID + " in (");
1130 for (int i = 0; i < numSets; ++i) {
1131 whereString.append(sets.get(i).mId);
1132 if (i != numSets - 1) {
1133 whereString.append(",");
1136 whereString.append(")");
1137 whereClause = whereString.toString();
1138 Log.i(TAG, "Updating dirty albums where " + whereClause);
1141 final Cursor cursorImages = cr.query(uriImages, PROJECTION_IMAGES, whereClause, null, DEFAULT_IMAGE_SORT_ORDER);
1142 final Cursor cursorVideos = cr.query(uriVideos, PROJECTION_VIDEOS, whereClause, null, DEFAULT_VIDEO_SORT_ORDER);
1143 final Cursor[] cursors = new Cursor[2];
1144 cursors[0] = cursorImages;
1145 cursors[1] = cursorVideos;
1146 final SortCursor sortCursor = new SortCursor(cursors, Images.ImageColumns.DATE_TAKEN, SortCursor.TYPE_NUMERIC, true);
1147 if (Thread.interrupted()) {
1151 if (sortCursor != null && sortCursor.moveToFirst()) {
1152 final int count = sortCursor.getCount();
1153 final int numSets = sets.size();
1154 final int approximateCountPerSet = count / numSets;
1155 for (int i = 0; i < numSets; ++i) {
1156 final MediaSet set = sets.get(i);
1157 set.setNumExpectedItems(approximateCountPerSet);
1160 if (Thread.interrupted()) {
1163 final MediaItem item = new MediaItem();
1164 final boolean isVideo = (sortCursor.getCurrentCursorIndex() == 1);
1166 populateVideoItemFromCursor(item, cr, sortCursor, CacheService.BASE_CONTENT_STRING_VIDEOS);
1168 populateMediaItemFromCursor(item, cr, sortCursor, CacheService.BASE_CONTENT_STRING_IMAGES);
1170 final long setId = sortCursor.getLong(MEDIA_BUCKET_ID_INDEX);
1171 final MediaSet set = findSet(setId, acceleratedSets);
1175 } while (sortCursor.moveToNext());
1178 if (sortCursor != null)
1181 } catch (Exception e) {
1182 // If the database operation failed for any reason
1185 if (sets.size() > 0) {
1186 writeItemsToCache(sets);
1187 Log.i(TAG, "Done building items.");
1191 private static final void writeSetsToCache(final ArrayList<MediaSet> sets) {
1192 final ByteArrayOutputStream bos = new ByteArrayOutputStream();
1193 final int numSets = sets.size();
1194 final DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(bos, 256));
1196 dos.writeInt(numSets);
1197 for (int i = 0; i < numSets; ++i) {
1198 if (Thread.interrupted()) {
1201 final MediaSet set = sets.get(i);
1202 dos.writeLong(set.mId);
1203 Utils.writeUTF(dos, set.mName);
1204 dos.writeBoolean(set.mHasImages);
1205 dos.writeBoolean(set.mHasVideos);
1208 sAlbumCache.put(ALBUM_CACHE_METADATA_INDEX, bos.toByteArray(), 0);
1211 sAlbumCache.deleteAll();
1212 putLocaleForAlbumCache(Locale.getDefault());
1214 sAlbumCache.flush();
1215 } catch (IOException e) {
1216 Log.e(TAG, "Error writing albums to diskcache.");
1217 sAlbumCache.deleteAll();
1218 putLocaleForAlbumCache(Locale.getDefault());
1222 private static final void writeItemsToCache(final ArrayList<MediaSet> sets) {
1223 final int numSets = sets.size();
1224 for (int i = 0; i < numSets; ++i) {
1225 if (Thread.interrupted()) {
1228 writeItemsForASet(sets.get(i));
1230 sAlbumCache.flush();
1233 private static final void writeItemsForASet(final MediaSet set) {
1234 final ByteArrayOutputStream bos = new ByteArrayOutputStream();
1235 final DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(bos, 256));
1237 final ArrayList<MediaItem> items = set.getItems();
1238 final int numItems = items.size();
1239 dos.writeInt(numItems);
1240 dos.writeLong(set.mMinTimestamp);
1241 dos.writeLong(set.mMaxTimestamp);
1242 for (int i = 0; i < numItems; ++i) {
1243 MediaItem item = items.get(i);
1244 if (set.mId == LocalDataSource.CAMERA_BUCKET_ID || set.mId == LocalDataSource.DOWNLOAD_BUCKET_ID) {
1245 // Reverse the display order for the camera bucket - want
1246 // the latest first.
1247 item = items.get(numItems - i - 1);
1249 dos.writeLong(item.mId);
1250 Utils.writeUTF(dos, item.mCaption);
1251 Utils.writeUTF(dos, item.mMimeType);
1252 dos.writeInt(item.getMediaType());
1253 dos.writeDouble(item.mLatitude);
1254 dos.writeDouble(item.mLongitude);
1255 dos.writeLong(item.mDateTakenInMs);
1256 dos.writeBoolean(item.mTriedRetrievingExifDateTaken);
1257 dos.writeLong(item.mDateAddedInSec);
1258 dos.writeLong(item.mDateModifiedInSec);
1259 dos.writeInt(item.mDurationInSec);
1260 dos.writeInt((int) item.mRotation);
1261 Utils.writeUTF(dos, item.mFilePath);
1264 sAlbumCache.put(set.mId, bos.toByteArray(), 0);
1266 } catch (IOException e) {
1267 Log.e(TAG, "Error writing to diskcache for set " + set.mName);
1268 sAlbumCache.deleteAll();
1269 putLocaleForAlbumCache(Locale.getDefault());
1273 private static final MediaSet findSet(final long id, final LongSparseArray<MediaSet> acceleratedTable) {
1274 // This is the accelerated lookup table for the MediaSet based on set
1276 return acceleratedTable.get(id);