OSDN Git Service

reconcile main tree with open-source eclair
[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.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;
41
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;
55
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");
61
62     private static final String TAG = "CacheService";
63     private static ImageList sList = null;
64
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;
70
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";
74
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 };
82
83     public static final String[] BUCKET_PROJECTION_VIDEOS = new String[] { Video.VideoColumns.BUCKET_ID,
84             Video.VideoColumns.BUCKET_DISPLAY_NAME };
85
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 };
94
95     public static final String[] SENSE_PROJECTION = new String[] { Images.ImageColumns.BUCKET_ID,
96             "MAX(" + Images.ImageColumns.DATE_ADDED + "), COUNT(*)" };
97
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 };
116
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 };
121
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>();
126
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;
133
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;
140
141     public interface Observer {
142         void onChange(long[] bucketIds);
143     }
144
145     public static final String getCachePath(final String subFolderName) {
146         return Environment.getExternalStorageDirectory() + "/Android/data/com.cooliris.media/cache/" + subFolderName;
147     }
148
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);
155         }
156         final Intent intent = new Intent(ACTION_CACHE, null, context, CacheService.class);
157         intent.putExtra("checkthumbnails", checkthumbnails);
158         context.startService(intent);
159     }
160
161     public static final boolean isCacheReady(final boolean onlyMediaSets) {
162         if (onlyMediaSets) {
163             return (sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0) != null && sAlbumCache.get(ALBUM_CACHE_DIRTY_INDEX, 0) == null);
164         } else {
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);
167         }
168     }
169
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);
173         if (!isReady) {
174             return isReady;
175         }
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) {
183                     return false;
184                 }
185             }
186         }
187         return true;
188     }
189
190     public static final boolean isPresentInCache(final long setId) {
191         return sAlbumCache.get(setId, 0) != null;
192     }
193
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() {
200                 public void run() {
201                     try {
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
205                         // versions.
206                         Thread.sleep(100);
207                     } catch (InterruptedException e) {
208                     }
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);
214                     }
215                     if (ids != null && ids.length > 0) {
216                         sList = null;
217                         Log.i(TAG, "Done computing dirty sets for num " + ids.length);
218                     }
219                 }
220             });
221         } else {
222             QUEUE_DIRTY_SENSE = true;
223         }
224     }
225
226     public static final void markDirty(final Context context) {
227         sList = null;
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() {
234                 public void run() {
235                     refresh(context);
236                     processQueuedDirty(context);
237                 }
238             });
239         } else {
240             QUEUE_DIRTY_ALL = true;
241         }
242     }
243
244     public static final void markDirtyImmediate(final long id) {
245         if (id == Shared.INVALID) {
246             return;
247         }
248         sList = null;
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) {
255                 if (ids[i] == id) {
256                     return;
257                 }
258             }
259             // Add this to the existing keys and concatenate the byte arrays.
260             data = concat(data, existingData);
261         }
262         sAlbumCache.put(ALBUM_CACHE_DIRTY_BUCKET_INDEX, data, 0);
263     }
264
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() {
270                 public void run() {
271                     refreshDirtySets(context);
272                     processQueuedDirty(context);
273                 }
274             });
275         } else {
276             QUEUE_DIRTY_SET = true;
277         }
278     }
279
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);
284         try {
285             final Cursor cursorImages = cr.query(uriImages, BUCKET_PROJECTION_IMAGES, whereString.toString(), null, null);
286             if (cursorImages != null && cursorImages.getCount() > 0) {
287                 cursorImages.close();
288                 return true;
289             }
290             final Cursor cursorVideos = cr.query(uriVideos, BUCKET_PROJECTION_VIDEOS, whereString.toString(), null, null);
291             if (cursorVideos != null && cursorVideos.getCount() > 0) {
292                 cursorVideos.close();
293                 return true;
294             }
295         } catch (Exception e) {
296             // If the database query failed for any reason
297             ;
298         }
299         return false;
300     }
301
302     public static final void loadMediaSets(final MediaFeed feed, final DataSource source, final boolean includeImages,
303             final boolean includeVideos) {
304         int timeElapsed = 0;
305         while (!isCacheReady(true) && timeElapsed < 10000) {
306             try {
307                 Thread.sleep(300);
308             } catch (InterruptedException e) {
309                 return;
310             }
311             timeElapsed += 300;
312         }
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));
316             try {
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);
326                     }
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);
333                     }
334                 }
335             } catch (IOException e) {
336                 Log.e(TAG, "Error loading albums.");
337                 sAlbumCache.deleteAll();
338                 putLocaleForAlbumCache(Locale.getDefault());
339             }
340         } else {
341             Log.d(TAG, "No albums found.");
342         }
343     }
344
345     public static final void loadMediaSet(final MediaFeed feed, final DataSource source, final long bucketId) {
346         int timeElapsed = 0;
347         while (!isCacheReady(false) && timeElapsed < 10000) {
348             try {
349                 Thread.sleep(300);
350             } catch (InterruptedException e) {
351                 return;
352             }
353             timeElapsed += 300;
354         }
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));
358             try {
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);
367                         }
368                     } else {
369                         mediaSet = new MediaSet();
370                     }
371                     mediaSet.mName = Utils.readUTF(dis);
372                     if (setId == bucketId) {
373                         mediaSet.mPicasaAlbumId = Shared.INVALID;
374                         mediaSet.generateTitle(true);
375                         return;
376                     }
377                 }
378             } catch (IOException e) {
379                 Log.e(TAG, "Error finding album " + bucketId);
380                 sAlbumCache.deleteAll();
381                 putLocaleForAlbumCache(Locale.getDefault());
382             }
383         } else {
384             Log.d(TAG, "No album found for album id " + bucketId);
385         }
386     }
387
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) {
390         int timeElapsed = 0;
391         byte[] albumData = null;
392         while (!isCacheReady(set.mId) && timeElapsed < 30000) {
393             try {
394                 Thread.sleep(300);
395             } catch (InterruptedException e) {
396                 return;
397             }
398             timeElapsed += 300;
399         }
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));
403             try {
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);
431                     }
432                 }
433                 dis.close();
434             } catch (IOException e) {
435                 Log.e(TAG, "Error loading items for album " + set.mName);
436                 sAlbumCache.deleteAll();
437                 putLocaleForAlbumCache(Locale.getDefault());
438             }
439         } else {
440             Log.d(TAG, "No items found for album " + set.mName);
441         }
442         set.updateNumExpectedItems();
443         set.generateTitle(true);
444     }
445
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);
450     }
451
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;
464         }
465         item.mFilePath = cursor.getString(CacheService.MEDIA_DATA_INDEX);
466         if (baseUri != null)
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);
476             } else {
477                 values.put(Images.ImageColumns.DATE_TAKEN, item.mDateTakenInMs);
478             }
479             try {
480                 cr.update(Uri.parse(item.mContentUri), values, null, null);
481             } catch (Exception e) {
482                 // If the database operation fails for any reason.
483                 ;
484             }
485         }
486
487         final int orientationDurationValue = cursor.getInt(CacheService.MEDIA_ORIENTATION_OR_DURATION_INDEX);
488         if (itemMediaType == MediaItem.MEDIA_TYPE_IMAGE) {
489             item.mRotation = orientationDurationValue;
490         } else {
491             item.mDurationInSec = orientationDurationValue;
492         }
493     }
494
495     // Returns -1 if we failed to examine EXIF information or EXIF parsing
496     // failed.
497     public static final long fetchDateTaken(final MediaItem item) {
498         if (!item.isDateTakenValid() && !item.mTriedRetrievingExifDateTaken
499                 && (item.mFilePath.endsWith(".jpg") || item.mFilePath.endsWith(".jpeg"))) {
500             try {
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) {
505                     try {
506                         final Date dateTaken = mDateFormat.parse(dateTakenStr);
507                         return dateTaken.getTime();
508                     } catch (ParseException pe) {
509                         try {
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);
514                         }
515                     }
516                 }
517             } catch (Exception e) {
518                 Log.i(TAG, "Error reading Exif information, probably not a jpeg.");
519             }
520
521             // Ensures that we only try retrieving EXIF date taken once.
522             item.mTriedRetrievingExifDateTaken = true;
523         }
524         return -1L;
525     }
526
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);
531     }
532
533     public static final ImageList getImageList(final Context context) {
534         if (sList != null)
535             return sList;
536         ImageList list = new ImageList();
537         final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
538         final ContentResolver cr = context.getContentResolver();
539         try {
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];
547                 int ctr = 0;
548                 do {
549                     if (Thread.interrupted()) {
550                         break;
551                     }
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);
556                     ++ctr;
557                 } while (cursorImages.moveToNext());
558                 cursorImages.close();
559                 list.ids = ids;
560                 list.thumbids = thumbnailIds;
561                 list.timestamp = timestamp;
562                 list.orientation = orientation;
563             }
564         } catch (Exception e) {
565             // If the database operation failed for any reason
566             ;
567         }
568         if (sList == null) {
569             sList = list;
570         }
571         return list;
572     }
573
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();
580             }
581         }
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));
588         }
589         return bitmap;
590     }
591
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()) {
602                 return;
603             }
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);
615                     }
616                 }
617             }
618         }
619         Log.i(TAG, "DiskCache ready for all thumbnails.");
620     }
621
622     private static void addToThumbnailerSkipList(long thumbnailId) {
623         sSkipThumbnailIds.put(thumbnailId, sDummyData, 0);
624         sSkipThumbnailIds.flush();
625     }
626
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) {
631                 return true;
632             }
633         }
634         return false;
635     }
636
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) {
640             return null;
641         }
642         try {
643             Bitmap bitmap = null;
644             Thread.sleep(1);
645             if (!isVideo) {
646                 final String uriString = BASE_CONTENT_STRING_IMAGES + origId;
647                 UriTexture.invalidateCache(thumbId, 1024);
648                 try {
649                     bitmap = UriTexture.createFromUri(context, uriString, 1024, 1024, thumbId, null);
650                 } catch (IOException e) {
651                     return null;
652                 } catch (URISyntaxException e) {
653                     return null;
654                 }
655             } else {
656                 new Thread() {
657                     public void run() {
658                         try {
659                             Thread.sleep(5000);
660                         } catch (InterruptedException e) {
661                             ;
662                         }
663                         try {
664                             MediaStore.Video.Thumbnails.cancelThumbnailRequest(context.getContentResolver(), origId);
665                         } catch (Exception e) {
666                             ;
667                         }
668                     }
669                 }.start();
670                 bitmap = MediaStore.Video.Thumbnails.getThumbnail(context.getContentResolver(), origId,
671                         MediaStore.Video.Thumbnails.MICRO_KIND, null);
672             }
673             if (bitmap == null) {
674                 return null;
675             }
676             final byte[] retVal = writeBitmapToCache(thumbnailCache, thumbId, origId, bitmap, thumbnailWidth, thumbnailHeight,
677                     timestamp);
678             return retVal;
679         } catch (InterruptedException e) {
680             return null;
681         }
682     }
683
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
689         // image center.
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.
694
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;
704         // }
705
706         // Crop to thumbnail aspect ratio biased towards the focus point.
707         int cropX;
708         int cropY;
709         int cropWidth;
710         int cropHeight;
711         float scaleFactor;
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));
716             cropY = 0;
717             cropHeight = height;
718             scaleFactor = (float) thumbnailHeight / height;
719         } else {
720             // Horizontally constrained.
721             cropHeight = thumbnailHeight * width / thumbnailWidth;
722             cropY = Math.max(0, Math.min(focusY - cropHeight / 2, height - cropHeight));
723             cropX = 0;
724             cropWidth = width;
725             scaleFactor = (float) thumbnailWidth / width;
726         }
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);
731         canvas.drawColor(0);
732         canvas.drawBitmap(bitmap, new Rect(cropX, cropY, cropX + cropWidth, cropY + cropHeight), new Rect(0, 0, thumbnailWidth,
733                 thumbnailHeight), paint);
734         bitmap.recycle();
735
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;
740         try {
741             dataOutput.writeLong(origId);
742             dataOutput.writeShort((int) ((focusX - cropX) * scaleFactor));
743             dataOutput.writeShort((int) ((focusY - cropY) * scaleFactor));
744             dataOutput.flush();
745             finalBitmap.compress(Bitmap.CompressFormat.JPEG, 80, cacheOutput);
746             retVal = cacheOutput.toByteArray();
747             synchronized (thumbnailCache) {
748                 thumbnailCache.put(thumbId, retVal, timestamp);
749             }
750             cacheOutput.close();
751             finalBitmap.recycle();
752         } catch (Exception e) {
753             ;
754         }
755         return retVal;
756     }
757
758     public CacheService() {
759         super("CacheService");
760     }
761
762     @Override
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());
768         }
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();
776             } else {
777                 startNewCacheThreadForDirtySets();
778             }
779         } else {
780             // The locale has changed, we need to regenerate the strings.
781             sAlbumCache.deleteAll();
782             putLocaleForAlbumCache(Locale.getDefault());
783             startNewCacheThread();
784         }
785         if (intent.getBooleanExtra("checkthumbnails", false)) {
786             startNewThumbnailThread(this);
787         } else {
788             final Thread existingThread = THUMBNAIL_THREAD.getAndSet(null);
789             if (existingThread != null) {
790                 existingThread.interrupt();
791             }
792         }
793     }
794
795     private static final void putLocaleForAlbumCache(final Locale locale) {
796         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
797         final DataOutputStream dos = new DataOutputStream(bos);
798         try {
799             Utils.writeUTF(dos, locale.getCountry());
800             Utils.writeUTF(dos, locale.getLanguage());
801             Utils.writeUTF(dos, locale.getVariant());
802             dos.flush();
803             bos.flush();
804             final byte[] data = bos.toByteArray();
805             sAlbumCache.put(ALBUM_CACHE_LOCALE_INDEX, data, 0);
806             sAlbumCache.flush();
807             dos.close();
808             bos.close();
809         } catch (IOException e) {
810             // Could not write locale to cache.
811             Log.i(TAG, "Error writing locale to cache.");
812             ;
813         }
814     }
815
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);
821             try {
822                 String country = Utils.readUTF(dis);
823                 if (country == null)
824                     country = "";
825                 String language = Utils.readUTF(dis);
826                 if (language == null)
827                     language = "";
828                 String variant = Utils.readUTF(dis);
829                 if (variant == null)
830                     variant = "";
831                 final Locale locale = new Locale(language, country, variant);
832                 dis.close();
833                 bis.close();
834                 return locale;
835             } catch (IOException e) {
836                 // Could not read locale in cache.
837                 Log.i(TAG, "Error reading locale from cache.");
838                 return null;
839             }
840         }
841         return null;
842     }
843
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() {
847             public void run() {
848                 try {
849                     action.run();
850                 } finally {
851                     threadRef.compareAndSet(this, null);
852                 }
853             }
854         };
855         newThread.setName(name);
856         newThread.start();
857
858         // Interrupt any existing thread.
859         final Thread existingThread = threadRef.getAndSet(newThread);
860         if (existingThread != null) {
861             existingThread.interrupt();
862         }
863     }
864
865     public static final void startNewThumbnailThread(final Context context) {
866         restartThread(THUMBNAIL_THREAD, "ThumbnailRefresh", new Runnable() {
867             public void run() {
868                 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
869                 try {
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) {
874                     return;
875                 }
876                 CacheService.buildThumbnails(context);
877             }
878         });
879     }
880
881     private void startNewCacheThread() {
882         restartThread(CACHE_THREAD, "CacheRefresh", new Runnable() {
883             public void run() {
884                 refresh(CacheService.this);
885             }
886         });
887     }
888
889     private void startNewCacheThreadForDirtySets() {
890         restartThread(CACHE_THREAD, "CacheRefreshDirtySets", new Runnable() {
891             public void run() {
892                 refreshDirtySets(CacheService.this);
893             }
894         });
895     }
896
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);
901         return C;
902     }
903
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);
911         }
912         return retVal;
913     }
914
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();
919         lBuffer.put(0, l);
920         return bArray;
921     }
922
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]);
930         }
931         return bArray;
932     }
933
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());
940
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();
947         try {
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,
954                     true);
955             try {
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);
962                     sets.add(cameraSet);
963                     acceleratedSets.put(cameraSet.mId, cameraSet);
964                     do {
965                         if (Thread.interrupted()) {
966                             return;
967                         }
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);
974                             sets.add(mediaSet);
975                             acceleratedSets.put(setId, mediaSet);
976                         }
977                         mediaSet.mHasImages |= (sortCursor.getCurrentCursorIndex() == 0);
978                         mediaSet.mHasVideos |= (sortCursor.getCurrentCursorIndex() == 1);
979                     } while (sortCursor.moveToNext());
980                     sortCursor.close();
981                 }
982             } finally {
983                 if (sortCursor != null)
984                     sortCursor.close();
985             }
986
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.
994             ;
995         }
996         sAlbumCache.delete(ALBUM_CACHE_INCOMPLETE_INDEX);
997     }
998
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;
1004             if (numIds > 0) {
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();
1009                     set.mId = ids[i];
1010                     sets.add(set);
1011                     acceleratedSets.put(set.mId, set);
1012                 }
1013                 Log.i(TAG, "Refreshing dirty albums");
1014                 populateMediaItemsForSets(context, sets, acceleratedSets, true);
1015             }
1016         }
1017         sAlbumCache.delete(ALBUM_CACHE_DIRTY_BUCKET_INDEX);
1018     }
1019
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>();
1026         try {
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);
1033             try {
1034                 if (cursor.moveToFirst()) {
1035                     boolean allDirty = false;
1036                     do {
1037                         long setId = cursor.getLong(0);
1038                         if (allDirty) {
1039                             addNoDupe(retVal, setId);
1040                         } else {
1041                             boolean contains = sAlbumCache.isDataAvailable(setId, 0);
1042                             if (!contains) {
1043                                 // We need to refresh everything.
1044                                 markDirty(context);
1045                                 addNoDupe(retVal, setId);
1046                                 allDirty = true;
1047                             }
1048                             if (!allDirty) {
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];
1053                                 if (data != null) {
1054                                     dataLong = toLongArray(data);
1055                                 }
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);
1064                                 }
1065                             }
1066                         }
1067                     } while (cursor.moveToNext());
1068                 }
1069             } finally {
1070                 cursor.close();
1071             }
1072             sMetaAlbumCache.flush();
1073         } catch (Exception e) {
1074             // If the database operation failed for any reason.
1075             ;
1076         }
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);
1081         }
1082         return retValIds;
1083     }
1084
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)
1089                 return;
1090         }
1091         array.add(value);
1092     }
1093
1094     private static final void processQueuedDirty(final Context context) {
1095         do {
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;
1105                 refresh(context);
1106             } else if (QUEUE_DIRTY_SET) {
1107                 QUEUE_DIRTY_SET = false;
1108                 // We don't mark QUEUE_DIRTY_SENSE because a set outside the
1109                 // dirty
1110                 // sets might have gotten modified.
1111                 refreshDirtySets(context);
1112             }
1113         } while (QUEUE_DIRTY_SENSE || QUEUE_DIRTY_SET || QUEUE_DIRTY_ALL);
1114     }
1115
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()) {
1119             return;
1120         }
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();
1125
1126         String whereClause = null;
1127         if (useWhere) {
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(",");
1134                 }
1135             }
1136             whereString.append(")");
1137             whereClause = whereString.toString();
1138             Log.i(TAG, "Updating dirty albums where " + whereClause);
1139         }
1140         try {
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()) {
1148                 return;
1149             }
1150             try {
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);
1158                     }
1159                     do {
1160                         if (Thread.interrupted()) {
1161                             return;
1162                         }
1163                         final MediaItem item = new MediaItem();
1164                         final boolean isVideo = (sortCursor.getCurrentCursorIndex() == 1);
1165                         if (isVideo) {
1166                             populateVideoItemFromCursor(item, cr, sortCursor, CacheService.BASE_CONTENT_STRING_VIDEOS);
1167                         } else {
1168                             populateMediaItemFromCursor(item, cr, sortCursor, CacheService.BASE_CONTENT_STRING_IMAGES);
1169                         }
1170                         final long setId = sortCursor.getLong(MEDIA_BUCKET_ID_INDEX);
1171                         final MediaSet set = findSet(setId, acceleratedSets);
1172                         if (set != null) {
1173                             set.addItem(item);
1174                         }
1175                     } while (sortCursor.moveToNext());
1176                 }
1177             } finally {
1178                 if (sortCursor != null)
1179                     sortCursor.close();
1180             }
1181         } catch (Exception e) {
1182             // If the database operation failed for any reason
1183             ;
1184         }
1185         if (sets.size() > 0) {
1186             writeItemsToCache(sets);
1187             Log.i(TAG, "Done building items.");
1188         }
1189     }
1190
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));
1195         try {
1196             dos.writeInt(numSets);
1197             for (int i = 0; i < numSets; ++i) {
1198                 if (Thread.interrupted()) {
1199                     return;
1200                 }
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);
1206             }
1207             dos.flush();
1208             sAlbumCache.put(ALBUM_CACHE_METADATA_INDEX, bos.toByteArray(), 0);
1209             dos.close();
1210             if (numSets == 0) {
1211                 sAlbumCache.deleteAll();
1212                 putLocaleForAlbumCache(Locale.getDefault());
1213             }
1214             sAlbumCache.flush();
1215         } catch (IOException e) {
1216             Log.e(TAG, "Error writing albums to diskcache.");
1217             sAlbumCache.deleteAll();
1218             putLocaleForAlbumCache(Locale.getDefault());
1219         }
1220     }
1221
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()) {
1226                 return;
1227             }
1228             writeItemsForASet(sets.get(i));
1229         }
1230         sAlbumCache.flush();
1231     }
1232
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));
1236         try {
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);
1248                 }
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);
1262             }
1263             dos.flush();
1264             sAlbumCache.put(set.mId, bos.toByteArray(), 0);
1265             dos.close();
1266         } catch (IOException e) {
1267             Log.e(TAG, "Error writing to diskcache for set " + set.mName);
1268             sAlbumCache.deleteAll();
1269             putLocaleForAlbumCache(Locale.getDefault());
1270         }
1271     }
1272
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
1275         // id.
1276         return acceleratedTable.get(id);
1277     }
1278 }