OSDN Git Service

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