OSDN Git Service

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