OSDN Git Service

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