1 package com.cooliris.cache;
3 import java.io.BufferedInputStream;
4 import java.io.BufferedOutputStream;
5 import java.io.ByteArrayInputStream;
6 import java.io.ByteArrayOutputStream;
7 import java.io.DataInputStream;
8 import java.io.DataOutputStream;
9 import java.io.IOException;
10 import java.net.URISyntaxException;
11 import java.nio.ByteBuffer;
12 import java.nio.LongBuffer;
13 import java.text.DateFormat;
14 import java.text.ParseException;
15 import java.text.SimpleDateFormat;
16 import java.util.ArrayList;
17 import java.util.Date;
18 import java.util.Locale;
19 import java.util.concurrent.atomic.AtomicReference;
21 import android.app.IntentService;
22 import android.content.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;
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;
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");
63 private static final String TAG = "CacheService";
64 private static ImageList sList = null;
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;
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";
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 };
84 public static final String[] BUCKET_PROJECTION_VIDEOS = new String[] { Video.VideoColumns.BUCKET_ID,
85 Video.VideoColumns.BUCKET_DISPLAY_NAME };
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 };
96 public static final String[] SENSE_PROJECTION = new String[] { Images.ImageColumns.BUCKET_ID,
97 "MAX(" + Images.ImageColumns.DATE_ADDED + "), COUNT(*)" };
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 };
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 };
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>();
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;
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;
142 public interface Observer {
143 void onChange(long[] bucketIds);
146 public static final String getCachePath(final String subFolderName) {
147 return Environment.getExternalStorageDirectory() + "/Android/data/com.cooliris.media/cache/" + subFolderName;
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);
157 final Intent intent = new Intent(ACTION_CACHE, null, context, CacheService.class);
158 intent.putExtra("checkthumbnails", checkthumbnails);
159 context.startService(intent);
162 public static final boolean isCacheReady(final boolean onlyMediaSets) {
164 return (sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0) != null && sAlbumCache.get(ALBUM_CACHE_DIRTY_INDEX, 0) == null);
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);
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);
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) {
191 public static final boolean isPresentInCache(final long setId) {
192 return sAlbumCache.get(setId, 0) != null;
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() {
202 Log.i(TAG, "Computing dirty sets.");
203 long ids[] = computeDirtySets(context);
204 if (ids != null && observer != null) {
205 observer.onChange(ids);
207 if (ids != null && ids.length > 0) {
209 Log.i(TAG, "Done computing dirty sets for num " + ids.length);
214 QUEUE_DIRTY_SENSE = true;
218 public static final void markDirty(final Context context) {
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() {
231 QUEUE_DIRTY_ALL = true;
235 public static final void markDirtyImmediate(final long id) {
236 if (id == Shared.INVALID) {
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) {
250 // Add this to the existing keys and concatenate the byte arrays.
251 data = concat(data, existingData);
253 sAlbumCache.put(ALBUM_CACHE_DIRTY_BUCKET_INDEX, data);
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() {
262 refreshDirtySets(context);
266 QUEUE_DIRTY_SET = true;
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();
279 final Cursor cursorVideos = cr.query(uriVideos, BUCKET_PROJECTION_VIDEOS, whereString.toString(), null, null);
280 if (cursorVideos != null && cursorVideos.getCount() > 0) {
281 cursorVideos.close();
287 public static final void loadMediaSets(final MediaFeed feed, final DataSource source, final boolean includeImages,
288 final boolean includeVideos) {
290 while (!isCacheReady(true) && timeElapsed < 10000) {
293 } catch (InterruptedException e) {
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));
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);
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);
320 } catch (IOException e) {
321 Log.e(TAG, "Error loading albums.");
322 sAlbumCache.deleteAll();
323 putLocaleForAlbumCache(Locale.getDefault());
326 Log.d(TAG, "No albums found.");
330 public static final void loadMediaSet(final MediaFeed feed, final DataSource source, final long bucketId) {
332 while (!isCacheReady(false) && timeElapsed < 10000) {
335 } catch (InterruptedException e) {
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));
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);
354 mediaSet = new MediaSet();
356 mediaSet.mName = Utils.readUTF(dis);
357 if (setId == bucketId) {
358 mediaSet.mPicasaAlbumId = Shared.INVALID;
359 mediaSet.generateTitle(true);
363 } catch (IOException e) {
364 Log.e(TAG, "Error finding album " + bucketId);
365 sAlbumCache.deleteAll();
366 putLocaleForAlbumCache(Locale.getDefault());
369 Log.d(TAG, "No album found for album id " + bucketId);
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) {
376 byte[] albumData = null;
377 while (!isCacheReady(set.mId) && timeElapsed < 30000) {
380 } catch (InterruptedException e) {
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));
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);
419 } catch (IOException e) {
420 Log.e(TAG, "Error loading items for album " + set.mName);
421 sAlbumCache.deleteAll();
422 putLocaleForAlbumCache(Locale.getDefault());
425 Log.d(TAG, "No items found for album " + set.mName);
427 set.updateNumExpectedItems();
428 set.generateTitle(true);
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);
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;
450 item.mFilePath = cursor.getString(CacheService.MEDIA_DATA_INDEX);
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);
462 values.put(Images.ImageColumns.DATE_TAKEN, item.mDateTakenInMs);
464 cr.update(Uri.parse(item.mContentUri), values, null, null);
467 final int orientationDurationValue = cursor.getInt(CacheService.MEDIA_ORIENTATION_OR_DURATION_INDEX);
468 if (itemMediaType == MediaItem.MEDIA_TYPE_IMAGE) {
469 item.mRotation = orientationDurationValue;
471 item.mDurationInSec = orientationDurationValue;
475 // Returns -1 if we failed to examine EXIF information or EXIF parsing
477 public static final long fetchDateTaken(final MediaItem item) {
478 if (!item.isDateTakenValid() && !item.mTriedRetrievingExifDateTaken
479 && (item.mFilePath.endsWith(".jpg") || item.mFilePath.endsWith(".jpeg"))) {
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) {
486 final Date dateTaken = mDateFormat.parse(dateTakenStr);
487 return dateTaken.getTime();
488 } catch (ParseException pe) {
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);
497 } catch (Exception e) {
498 Log.i(TAG, "Error reading Exif information, probably not a jpeg.");
501 // Ensures that we only try retrieving EXIF date taken once.
502 item.mTriedRetrievingExifDateTaken = true;
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);
513 public static final ImageList getImageList(final Context context) {
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];
528 if (Thread.interrupted()) {
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);
536 } while (cursorImages.moveToNext());
537 cursorImages.close();
539 list.thumbids = thumbnailIds;
540 list.timestamp = timestamp;
541 list.orientation = orientation;
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();
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));
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()) {
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);
587 Log.i(TAG, "DiskCache ready for all thumbnails.");
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) {
596 Bitmap bitmap = null;
599 final String uriString = BASE_CONTENT_STRING_IMAGES + origId;
600 UriTexture.invalidateCache(thumbId, 1024);
602 bitmap = UriTexture.createFromUri(context, uriString, 1024, 1024, thumbId, null);
603 } catch (IOException e) {
605 } catch (URISyntaxException e) {
613 } catch (InterruptedException e) {
617 MediaStore.Video.Thumbnails.cancelThumbnailRequest(context.getContentResolver(), origId);
618 } catch (Exception e) {
623 bitmap = MediaStore.Video.Thumbnails.getThumbnail(context.getContentResolver(), origId,
624 MediaStore.Video.Thumbnails.MICRO_KIND, null);
626 if (bitmap == null) {
629 final byte[] retVal = writeBitmapToCache(thumbnailCache, thumbId, origId, bitmap, thumbnailWidth, thumbnailHeight);
631 } catch (InterruptedException e) {
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
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.
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;
658 // Crop to thumbnail aspect ratio biased towards the focus point.
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));
670 scaleFactor = (float) thumbnailHeight / height;
672 // Horizontally constrained.
673 cropHeight = thumbnailHeight * width / thumbnailWidth;
674 cropY = Math.max(0, Math.min(focusY - cropHeight / 2, height - cropHeight));
677 scaleFactor = (float) thumbnailWidth / width;
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);
684 canvas.drawBitmap(bitmap, new Rect(cropX, cropY, cropX + cropWidth, cropY + cropHeight), new Rect(0, 0, thumbnailWidth,
685 thumbnailHeight), paint);
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;
693 dataOutput.writeLong(origId);
694 dataOutput.writeShort((int) ((focusX - cropX) * scaleFactor));
695 dataOutput.writeShort((int) ((focusY - cropY) * scaleFactor));
697 finalBitmap.compress(Bitmap.CompressFormat.JPEG, 80, cacheOutput);
698 retVal = cacheOutput.toByteArray();
699 synchronized (thumbnailCache) {
700 thumbnailCache.put(thumbId, retVal);
703 } catch (Exception e) {
709 public CacheService() {
710 super("CacheService");
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());
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();
728 startNewCacheThreadForDirtySets();
731 // The locale has changed, we need to regenerate the strings.
732 sAlbumCache.deleteAll();
733 putLocaleForAlbumCache(Locale.getDefault());
734 startNewCacheThread();
736 if (intent.getBooleanExtra("checkthumbnails", false)) {
737 startNewThumbnailThread(this);
739 final Thread existingThread = THUMBNAIL_THREAD.getAndSet(null);
740 if (existingThread != null) {
741 existingThread.interrupt();
746 private static final void putLocaleForAlbumCache(final Locale locale) {
747 final ByteArrayOutputStream bos = new ByteArrayOutputStream();
748 final DataOutputStream dos = new DataOutputStream(bos);
750 Utils.writeUTF(dos, locale.getCountry());
751 Utils.writeUTF(dos, locale.getLanguage());
752 Utils.writeUTF(dos, locale.getVariant());
755 final byte[] data = bos.toByteArray();
756 sAlbumCache.put(ALBUM_CACHE_LOCALE_INDEX, data);
760 } catch (IOException e) {
761 // Could not write locale to cache.
762 Log.i(TAG, "Error writing locale to cache.");
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);
773 String country = Utils.readUTF(dis);
776 String language = Utils.readUTF(dis);
777 if (language == null)
779 String variant = Utils.readUTF(dis);
782 final Locale locale = new Locale(language, country, variant);
786 } catch (IOException e) {
787 // Could not read locale in cache.
788 Log.i(TAG, "Error reading locale from cache.");
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() {
802 threadRef.compareAndSet(this, null);
806 newThread.setName(name);
809 // Interrupt any existing thread.
810 final Thread existingThread = threadRef.getAndSet(newThread);
811 if (existingThread != null) {
812 existingThread.interrupt();
816 public static final void startNewThumbnailThread(final Context context) {
817 restartThread(THUMBNAIL_THREAD, "ThumbnailRefresh", new Runnable() {
819 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
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) {
827 CacheService.buildThumbnails(context);
832 private void startNewCacheThread() {
833 restartThread(CACHE_THREAD, "CacheRefresh", new Runnable() {
835 refresh(CacheService.this);
840 private void startNewCacheThreadForDirtySets() {
841 restartThread(CACHE_THREAD, "CacheRefreshDirtySets", new Runnable() {
843 refreshDirtySets(CacheService.this);
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);
855 private static final long toLong(final byte[] data) {
856 // 8 bytes for a long
857 if (data == null || data.length < 8)
859 final ByteBuffer bBuffer = ByteBuffer.wrap(data);
860 final LongBuffer lBuffer = bBuffer.asLongBuffer();
861 final int numLongs = lBuffer.capacity();
862 return lBuffer.get(0);
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);
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();
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]);
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());
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();
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);
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);
923 acceleratedSets.put(cameraSet.mId, cameraSet);
925 if (Thread.interrupted()) {
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);
935 acceleratedSets.put(setId, mediaSet);
937 mediaSet.mHasImages |= (sortCursor.getCurrentCursorIndex() == 0);
938 mediaSet.mHasVideos |= (sortCursor.getCurrentCursorIndex() == 1);
939 } while (sortCursor.moveToNext());
943 if (sortCursor != null)
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);
954 // Complete any queued dirty requests
955 processQueuedDirty(context);
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;
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();
970 acceleratedSets.put(set.mId, set);
972 Log.i(TAG, "Refreshing dirty albums");
973 populateMediaItemsForSets(context, sets, acceleratedSets, true);
976 processQueuedDirty(context);
977 sAlbumCache.delete(ALBUM_CACHE_DIRTY_BUCKET_INDEX);
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;
994 if (cursor.moveToFirst()) {
995 retVal = new long[cursor.getCount()];
996 boolean allDirty = false;
998 long setId = cursor.getLong(0);
1000 retVal[ctr++] = setId;
1002 boolean contains = sAlbumCache.isDataAvailable(setId, 0);
1004 // We need to refresh everything.
1006 retVal[ctr++] = setId;
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];
1015 dataLong = toLongArray(data);
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));
1029 } while (cursor.moveToNext());
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];
1040 return retValCompact;
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;
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);
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()) {
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();
1072 String whereClause = null;
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(",");
1082 whereString.append(")");
1083 whereClause = whereString.toString();
1084 Log.i(TAG, "Updating dirty albums where " + whereClause);
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()) {
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);
1106 if (Thread.interrupted()) {
1109 final MediaItem item = new MediaItem();
1110 final boolean isVideo = (sortCursor.getCurrentCursorIndex() == 1);
1112 populateVideoItemFromCursor(item, cr, sortCursor, CacheService.BASE_CONTENT_STRING_VIDEOS);
1114 populateMediaItemFromCursor(item, cr, sortCursor, CacheService.BASE_CONTENT_STRING_IMAGES);
1116 final long setId = sortCursor.getLong(MEDIA_BUCKET_ID_INDEX);
1117 final MediaSet set = findSet(setId, acceleratedSets);
1121 } while (sortCursor.moveToNext());
1124 if (sortCursor != null)
1127 if (sets.size() > 0) {
1128 writeItemsToCache(sets);
1129 Log.i(TAG, "Done building items.");
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));
1138 dos.writeInt(numSets);
1139 for (int i = 0; i < numSets; ++i) {
1140 if (Thread.interrupted()) {
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);
1150 sAlbumCache.put(ALBUM_CACHE_METADATA_INDEX, bos.toByteArray());
1153 sAlbumCache.deleteAll();
1154 putLocaleForAlbumCache(Locale.getDefault());
1156 sAlbumCache.flush();
1157 } catch (IOException e) {
1158 Log.e(TAG, "Error writing albums to diskcache.");
1159 sAlbumCache.deleteAll();
1160 putLocaleForAlbumCache(Locale.getDefault());
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()) {
1170 writeItemsForASet(sets.get(i));
1172 sAlbumCache.flush();
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));
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);
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);
1206 sAlbumCache.put(set.mId, bos.toByteArray());
1208 } catch (IOException e) {
1209 Log.e(TAG, "Error writing to diskcache for set " + set.mName);
1210 sAlbumCache.deleteAll();
1211 putLocaleForAlbumCache(Locale.getDefault());
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
1218 return acceleratedTable.get(id);