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.ContentResolver;
23 import android.content.ContentValues;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.database.Cursor;
27 import android.graphics.Bitmap;
28 import android.graphics.Canvas;
29 import android.graphics.Paint;
30 import android.graphics.Rect;
31 import android.media.ExifInterface;
32 import android.net.Uri;
33 import android.os.Environment;
34 import android.os.Process;
35 import android.os.SystemClock;
36 import android.provider.MediaStore;
37 import android.provider.MediaStore.Images;
38 import android.provider.MediaStore.Video;
39 import android.util.Log;
41 import com.cooliris.media.DataSource;
42 import com.cooliris.media.DiskCache;
43 import com.cooliris.media.Gallery;
44 import com.cooliris.media.LocalDataSource;
45 import com.cooliris.media.LongSparseArray;
46 import com.cooliris.media.MediaFeed;
47 import com.cooliris.media.MediaItem;
48 import com.cooliris.media.MediaSet;
49 import com.cooliris.media.R;
50 import com.cooliris.media.Shared;
51 import com.cooliris.media.SortCursor;
52 import com.cooliris.media.UriTexture;
53 import com.cooliris.media.Utils;
55 public final class CacheService extends IntentService {
56 public static final String ACTION_CACHE = "com.cooliris.cache.action.CACHE";
57 public static final DiskCache sAlbumCache = new DiskCache("local-album-cache");
59 private static final String TAG = "CacheService";
60 private static ImageList sList = null;
62 // Wait 2 seconds to start the thumbnailer so that the application can load without any overheads.
63 private static final int THUMBNAILER_WAIT_IN_MS = 2000;
64 private static final int DEFAULT_THUMBNAIL_WIDTH = 128;
65 private static final int DEFAULT_THUMBNAIL_HEIGHT = 96;
67 public static final String DEFAULT_IMAGE_SORT_ORDER = Images.ImageColumns.DATE_TAKEN + " ASC, "
68 + Images.ImageColumns.DATE_ADDED + " ASC";
69 public static final String DEFAULT_VIDEO_SORT_ORDER = Video.VideoColumns.DATE_TAKEN + " ASC, " + Video.VideoColumns.DATE_ADDED
71 public static final String DEFAULT_BUCKET_SORT_ORDER = "upper(" + Images.ImageColumns.BUCKET_DISPLAY_NAME + ") ASC";
73 // Must preserve order between these indices and the order of the terms in BUCKET_PROJECTION_IMAGES, BUCKET_PROJECTION_VIDEOS.
74 // Not using SortedHashMap for efficieny reasons.
75 public static final int BUCKET_ID_INDEX = 0;
76 public static final int BUCKET_NAME_INDEX = 1;
77 public static final String[] BUCKET_PROJECTION_IMAGES = new String[] { Images.ImageColumns.BUCKET_ID,
78 Images.ImageColumns.BUCKET_DISPLAY_NAME };
80 public static final String[] BUCKET_PROJECTION_VIDEOS = new String[] { Video.VideoColumns.BUCKET_ID,
81 Video.VideoColumns.BUCKET_DISPLAY_NAME };
83 // Must preserve order between these indices and the order of the terms in THUMBNAIL_PROJECTION.
84 public static final int THUMBNAIL_ID_INDEX = 0;
85 public static final int THUMBNAIL_DATE_MODIFIED_INDEX = 1;
86 public static final int THUMBNAIL_DATA_INDEX = 2;
87 public static final int THUMBNAIL_ORIENTATION_INDEX = 2;
88 public static final String[] THUMBNAIL_PROJECTION = new String[] { Images.ImageColumns._ID, Images.ImageColumns.DATE_MODIFIED,
89 Images.ImageColumns.DATA, Images.ImageColumns.ORIENTATION };
91 // Must preserve order between these indices and the order of the terms in INITIAL_PROJECTION_IMAGES and
92 // INITIAL_PROJECTION_VIDEOS.
93 public static final int MEDIA_ID_INDEX = 0;
94 public static final int MEDIA_CAPTION_INDEX = 1;
95 public static final int MEDIA_MIME_TYPE_INDEX = 2;
96 public static final int MEDIA_LATITUDE_INDEX = 3;
97 public static final int MEDIA_LONGITUDE_INDEX = 4;
98 public static final int MEDIA_DATE_TAKEN_INDEX = 5;
99 public static final int MEDIA_DATE_ADDED_INDEX = 6;
100 public static final int MEDIA_DATE_MODIFIED_INDEX = 7;
101 public static final int MEDIA_DATA_INDEX = 8;
102 public static final int MEDIA_ORIENTATION_OR_DURATION_INDEX = 9;
103 public static final int MEDIA_BUCKET_ID_INDEX = 10;
104 public static final String[] PROJECTION_IMAGES = new String[] { Images.ImageColumns._ID, Images.ImageColumns.TITLE,
105 Images.ImageColumns.MIME_TYPE, Images.ImageColumns.LATITUDE, Images.ImageColumns.LONGITUDE,
106 Images.ImageColumns.DATE_TAKEN, Images.ImageColumns.DATE_ADDED, Images.ImageColumns.DATE_MODIFIED,
107 Images.ImageColumns.DATA, Images.ImageColumns.ORIENTATION, Images.ImageColumns.BUCKET_ID };
109 private static final String[] PROJECTION_VIDEOS = new String[] { Video.VideoColumns._ID, Video.VideoColumns.TITLE,
110 Video.VideoColumns.MIME_TYPE, Video.VideoColumns.LATITUDE, Video.VideoColumns.LONGITUDE, Video.VideoColumns.DATE_TAKEN,
111 Video.VideoColumns.DATE_ADDED, Video.VideoColumns.DATE_MODIFIED, Video.VideoColumns.DATA, Video.VideoColumns.DURATION,
112 Video.VideoColumns.BUCKET_ID };
114 public static final String BASE_CONTENT_STRING_IMAGES = (Images.Media.EXTERNAL_CONTENT_URI).toString() + "/";
115 public static final String BASE_CONTENT_STRING_VIDEOS = (Video.Media.EXTERNAL_CONTENT_URI).toString() + "/";
116 private static final AtomicReference<Thread> CACHE_THREAD = new AtomicReference<Thread>();
117 private static final AtomicReference<Thread> THUMBNAIL_THREAD = new AtomicReference<Thread>();
119 // Special indices in the Albumcache.
120 private static final int ALBUM_CACHE_METADATA_INDEX = -1;
121 private static final int ALBUM_CACHE_DIRTY_INDEX = -2;
122 private static final int ALBUM_CACHE_INCOMPLETE_INDEX = -3;
123 private static final int ALBUM_CACHE_DIRTY_BUCKET_INDEX = -4;
124 private static final int ALBUM_CACHE_LOCALE_INDEX = -5;
126 private static final DateFormat mDateFormat = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
127 private static final DateFormat mAltDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
128 private static final byte[] sDummyData = new byte[] { 1 };
129 private static boolean QUEUE_DIRTY_SET;
130 private static boolean QUEUE_DIRTY_ALL;
132 public static final String getCachePath(final String subFolderName) {
133 return Environment.getExternalStorageDirectory() + "/Android/data/com.cooliris.media/cache/" + subFolderName;
136 public static final void startCache(final Context context, final boolean checkthumbnails) {
137 final Locale locale = getLocaleForAlbumCache();
138 final Locale defaultLocale = Locale.getDefault();
139 if (locale == null || !locale.equals(defaultLocale)) {
140 sAlbumCache.deleteAll();
141 putLocaleForAlbumCache(defaultLocale);
143 final Intent intent = new Intent(ACTION_CACHE, null, context, CacheService.class);
144 intent.putExtra("checkthumbnails", checkthumbnails);
145 context.startService(intent);
148 public static final boolean isCacheReady(final boolean onlyMediaSets) {
150 return (sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0) != null && sAlbumCache.get(ALBUM_CACHE_DIRTY_INDEX, 0) == null);
152 return (sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0) != null && sAlbumCache.get(ALBUM_CACHE_DIRTY_INDEX, 0) == null && sAlbumCache
153 .get(ALBUM_CACHE_INCOMPLETE_INDEX, 0) == null);
157 public static final boolean isCacheReady(final long setId) {
158 final boolean isReady = (sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0) != null
159 && sAlbumCache.get(ALBUM_CACHE_DIRTY_INDEX, 0) == null && sAlbumCache.get(ALBUM_CACHE_INCOMPLETE_INDEX, 0) == null);
163 // Also, we need to check if this setId is dirty.
164 final byte[] existingData = sAlbumCache.get(ALBUM_CACHE_DIRTY_BUCKET_INDEX, 0);
165 if (existingData != null && existingData.length > 0) {
166 final long[] ids = toLongArray(existingData);
167 final int numIds = ids.length;
168 for (int i = 0; i < numIds; ++i) {
169 if (ids[i] == setId) {
177 public static final boolean isPresentInCache(final long setId) {
178 return sAlbumCache.get(setId, 0) != null;
181 public static final void markDirty(final Context context) {
183 sAlbumCache.put(ALBUM_CACHE_DIRTY_INDEX, sDummyData);
184 if (CACHE_THREAD.get() == null) {
185 restartThread(CACHE_THREAD, "CacheRefresh", new Runnable() {
191 QUEUE_DIRTY_ALL = true;
195 public static final void markDirtyImmediate(final long id) {
197 byte[] data = longToByteArray(id);
198 final byte[] existingData = sAlbumCache.get(ALBUM_CACHE_DIRTY_BUCKET_INDEX, 0);
199 if (existingData != null && existingData.length > 0) {
200 final long[] ids = toLongArray(existingData);
201 final int numIds = ids.length;
202 for (int i = 0; i < numIds; ++i) {
207 // Add this to the existing keys and concatenate the byte arrays.
208 data = concat(data, existingData);
210 sAlbumCache.put(ALBUM_CACHE_DIRTY_BUCKET_INDEX, data);
213 public static final void markDirty(final Context context, final long id) {
214 markDirtyImmediate(id);
215 if (CACHE_THREAD.get() == null) {
216 restartThread(CACHE_THREAD, "CacheRefreshDirtySets", new Runnable() {
218 refreshDirtySets(context);
222 QUEUE_DIRTY_SET = true;
226 public static final boolean setHasItems(final ContentResolver cr, final long setId) {
227 final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
228 final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI;
229 final StringBuffer whereString = new StringBuffer(Images.ImageColumns.BUCKET_ID + "=" + setId);
230 final Cursor cursorImages = cr.query(uriImages, BUCKET_PROJECTION_IMAGES, whereString.toString(), null, null);
231 if (cursorImages != null && cursorImages.getCount() > 0) {
232 cursorImages.close();
235 final Cursor cursorVideos = cr.query(uriVideos, BUCKET_PROJECTION_VIDEOS, whereString.toString(), null, null);
236 if (cursorVideos != null && cursorVideos.getCount() > 0) {
237 cursorVideos.close();
243 public static final void loadMediaSets(final MediaFeed feed, final DataSource source, final boolean includeImages,
244 final boolean includeVideos) {
246 while (!isCacheReady(true) && timeElapsed < 10000) {
249 } catch (InterruptedException e) {
254 final byte[] albumData = sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0);
255 if (albumData != null && albumData.length > 0) {
256 final DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(albumData), 256));
258 final int numAlbums = dis.readInt();
259 Log.i(TAG, "Loading " + numAlbums + " albums.");
260 for (int i = 0; i < numAlbums; ++i) {
261 final long setId = dis.readLong();
262 final String name = Utils.readUTF(dis);
263 final boolean hasImages = dis.readBoolean();
264 final boolean hasVideos = dis.readBoolean();
265 MediaSet mediaSet = feed.getMediaSet(setId);
266 if (mediaSet == null) {
267 mediaSet = feed.addMediaSet(setId, source);
269 if ((includeImages && hasImages) || (includeVideos && hasVideos)) {
270 mediaSet.mName = name;
271 mediaSet.mHasImages = hasImages;
272 mediaSet.mHasVideos = hasVideos;
273 mediaSet.mPicasaAlbumId = Shared.INVALID;
274 mediaSet.generateTitle(true);
277 } catch (IOException e) {
278 Log.e(TAG, "Error loading albums.");
279 sAlbumCache.deleteAll();
280 putLocaleForAlbumCache(Locale.getDefault());
283 Log.d(TAG, "No albums found.");
287 public static final void loadMediaSet(final MediaFeed feed, final DataSource source, final long bucketId) {
289 while (!isCacheReady(false) && timeElapsed < 10000) {
292 } catch (InterruptedException e) {
297 final byte[] albumData = sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0);
298 if (albumData != null && albumData.length > 0) {
299 DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(albumData), 256));
301 final int numAlbums = dis.readInt();
302 for (int i = 0; i < numAlbums; ++i) {
303 final long setId = dis.readLong();
304 MediaSet mediaSet = null;
305 if (setId == bucketId) {
306 mediaSet = feed.getMediaSet(setId);
307 if (mediaSet == null) {
308 mediaSet = feed.addMediaSet(setId, source);
311 mediaSet = new MediaSet();
313 mediaSet.mName = Utils.readUTF(dis);
314 if (setId == bucketId) {
315 mediaSet.mPicasaAlbumId = Shared.INVALID;
316 mediaSet.generateTitle(true);
320 } catch (IOException e) {
321 Log.e(TAG, "Error finding album " + bucketId);
322 sAlbumCache.deleteAll();
323 putLocaleForAlbumCache(Locale.getDefault());
326 Log.d(TAG, "No album found for album id " + bucketId);
330 public static final void loadMediaItemsIntoMediaFeed(final MediaFeed feed, final MediaSet set, final int rangeStart,
331 final int rangeEnd, final boolean includeImages, final boolean includeVideos) {
333 byte[] albumData = null;
334 while (!isCacheReady(set.mId) && timeElapsed < 30000) {
337 } catch (InterruptedException e) {
342 albumData = sAlbumCache.get(set.mId, 0);
343 if (albumData != null && set.mNumItemsLoaded < set.getNumExpectedItems()) {
344 final DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(albumData), 256));
346 final int numItems = dis.readInt();
347 Log.i(TAG, "Loading Set Id " + set.mId + " with " + numItems + " items.");
348 set.setNumExpectedItems(numItems);
349 set.mMinTimestamp = dis.readLong();
350 set.mMaxTimestamp = dis.readLong();
351 for (int i = 0; i < numItems; ++i) {
352 final MediaItem item = new MediaItem();
353 // Must preserve order with method that writes to cache.
354 item.mId = dis.readLong();
355 item.mCaption = Utils.readUTF(dis);
356 item.mMimeType = Utils.readUTF(dis);
357 item.setMediaType(dis.readInt());
358 item.mLatitude = dis.readDouble();
359 item.mLongitude = dis.readDouble();
360 item.mDateTakenInMs = dis.readLong();
361 item.mTriedRetrievingExifDateTaken = dis.readBoolean();
362 item.mDateAddedInSec = dis.readLong();
363 item.mDateModifiedInSec = dis.readLong();
364 item.mDurationInSec = dis.readInt();
365 item.mRotation = (float) dis.readInt();
366 item.mFilePath = Utils.readUTF(dis);
367 int itemMediaType = item.getMediaType();
368 if ((itemMediaType == MediaItem.MEDIA_TYPE_IMAGE && includeImages)
369 || (itemMediaType == MediaItem.MEDIA_TYPE_VIDEO && includeVideos)) {
370 String baseUri = (itemMediaType == MediaItem.MEDIA_TYPE_IMAGE) ? BASE_CONTENT_STRING_IMAGES
371 : BASE_CONTENT_STRING_VIDEOS;
372 item.mContentUri = baseUri + item.mId;
373 feed.addItemToMediaSet(item, set);
377 } catch (IOException e) {
378 Log.e(TAG, "Error loading items for album " + set.mName);
379 sAlbumCache.deleteAll();
380 putLocaleForAlbumCache(Locale.getDefault());
383 Log.d(TAG, "No items found for album " + set.mName);
385 set.updateNumExpectedItems();
386 set.generateTitle(true);
389 public static final void populateVideoItemFromCursor(final MediaItem item, final ContentResolver cr, final Cursor cursor,
390 final String baseUri) {
391 item.setMediaType(MediaItem.MEDIA_TYPE_VIDEO);
392 populateMediaItemFromCursor(item, cr, cursor, baseUri);
395 public static final void populateMediaItemFromCursor(final MediaItem item, final ContentResolver cr, final Cursor cursor,
396 final String baseUri) {
397 item.mId = cursor.getLong(CacheService.MEDIA_ID_INDEX);
398 // item.mCaption = cursor.getString(CacheService.MEDIA_CAPTION_INDEX);
399 item.mMimeType = cursor.getString(CacheService.MEDIA_MIME_TYPE_INDEX);
400 item.mLatitude = cursor.getDouble(CacheService.MEDIA_LATITUDE_INDEX);
401 item.mLongitude = cursor.getDouble(CacheService.MEDIA_LONGITUDE_INDEX);
402 item.mDateTakenInMs = cursor.getLong(CacheService.MEDIA_DATE_TAKEN_INDEX);
403 item.mDateAddedInSec = cursor.getLong(CacheService.MEDIA_DATE_ADDED_INDEX);
404 item.mDateModifiedInSec = cursor.getLong(CacheService.MEDIA_DATE_MODIFIED_INDEX);
405 if (item.mDateTakenInMs == item.mDateModifiedInSec) {
406 item.mDateTakenInMs = item.mDateModifiedInSec * 1000;
408 item.mFilePath = cursor.getString(CacheService.MEDIA_DATA_INDEX);
410 item.mContentUri = baseUri + item.mId;
411 final int itemMediaType = item.getMediaType();
412 // Check to see if a new date taken is available.
413 final long dateTaken = fetchDateTaken(item);
414 if (dateTaken != -1L && item.mContentUri != null) {
415 item.mDateTakenInMs = dateTaken;
416 final ContentValues values = new ContentValues();
417 if (itemMediaType == MediaItem.MEDIA_TYPE_VIDEO) {
418 values.put(Video.VideoColumns.DATE_TAKEN, item.mDateTakenInMs);
420 values.put(Images.ImageColumns.DATE_TAKEN, item.mDateTakenInMs);
422 cr.update(Uri.parse(item.mContentUri), values, null, null);
425 final int orientationDurationValue = cursor.getInt(CacheService.MEDIA_ORIENTATION_OR_DURATION_INDEX);
426 if (itemMediaType == MediaItem.MEDIA_TYPE_IMAGE) {
427 item.mRotation = orientationDurationValue;
429 item.mDurationInSec = orientationDurationValue;
433 // Returns -1 if we failed to examine EXIF information or EXIF parsing failed.
434 public static final long fetchDateTaken(final MediaItem item) {
435 if (!item.isDateTakenValid() && !item.mTriedRetrievingExifDateTaken
436 && (item.mFilePath.endsWith(".jpg") || item.mFilePath.endsWith(".jpeg"))) {
438 Log.i(TAG, "Parsing date taken from exif");
439 final ExifInterface exif = new ExifInterface(item.mFilePath);
440 final String dateTakenStr = exif.getAttribute(ExifInterface.TAG_DATETIME);
441 if (dateTakenStr != null) {
443 final Date dateTaken = mDateFormat.parse(dateTakenStr);
444 return dateTaken.getTime();
445 } catch (ParseException pe) {
447 final Date dateTaken = mAltDateFormat.parse(dateTakenStr);
448 return dateTaken.getTime();
449 } catch (ParseException pe2) {
450 Log.i(TAG, "Unable to parse date out of string - " + dateTakenStr);
454 } catch (Exception e) {
455 Log.i(TAG, "Error reading Exif information, probably not a jpeg.");
458 // Ensures that we only try retrieving EXIF date taken once.
459 item.mTriedRetrievingExifDateTaken = true;
464 public static final byte[] queryThumbnail(final Context context, final long thumbId, final long origId, final boolean isVideo,
465 final long timestamp) {
466 final DiskCache thumbnailCache = (isVideo) ? LocalDataSource.sThumbnailCacheVideo : LocalDataSource.sThumbnailCache;
467 return queryThumbnail(context, thumbId, origId, isVideo, thumbnailCache, timestamp);
470 public static final ImageList getImageList(final Context context) {
473 ImageList list = new ImageList();
474 final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
475 final ContentResolver cr = context.getContentResolver();
476 final Cursor cursorImages = cr.query(uriImages, THUMBNAIL_PROJECTION, null, null, null);
477 if (cursorImages != null && cursorImages.moveToFirst()) {
478 final int size = cursorImages.getCount();
479 final long[] ids = new long[size];
480 final long[] thumbnailIds = new long[size];
481 final long[] timestamp = new long[size];
482 final int[] orientation = new int[size];
485 if (Thread.interrupted()) {
488 ids[ctr] = cursorImages.getLong(THUMBNAIL_ID_INDEX);
489 timestamp[ctr] = cursorImages.getLong(THUMBNAIL_DATE_MODIFIED_INDEX);
490 thumbnailIds[ctr] = Utils.Crc64Long(cursorImages.getString(THUMBNAIL_DATA_INDEX));
491 orientation[ctr] = cursorImages.getInt(THUMBNAIL_ORIENTATION_INDEX);
493 } while (cursorImages.moveToNext());
494 cursorImages.close();
496 list.thumbids = thumbnailIds;
497 list.timestamp = timestamp;
498 list.orientation = orientation;
506 private static final byte[] queryThumbnail(final Context context, final long thumbId, final long origId, final boolean isVideo,
507 final DiskCache thumbnailCache, final long timestamp) {
508 if (!((Gallery) context).isPaused()) {
509 final Thread thumbnailThread = THUMBNAIL_THREAD.getAndSet(null);
510 if (thumbnailThread != null) {
511 thumbnailThread.interrupt();
514 byte[] bitmap = thumbnailCache.get(thumbId, timestamp);
515 if (bitmap == null) {
516 Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
517 final long time = SystemClock.uptimeMillis();
518 bitmap = buildThumbnailForId(context, thumbnailCache, thumbId, origId, isVideo, DEFAULT_THUMBNAIL_WIDTH,
519 DEFAULT_THUMBNAIL_HEIGHT);
520 Log.i(TAG, "Built thumbnail and screennail for " + origId + " in " + (SystemClock.uptimeMillis() - time));
521 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
526 private static final void buildThumbnails(final Context context) {
527 Log.i(TAG, "Preparing DiskCache for all thumbnails.");
528 ImageList list = getImageList(context);
529 final int size = (list.ids == null) ? 0 : list.ids.length;
530 final long[] ids = list.ids;
531 final long[] timestamp = list.timestamp;
532 final long[] thumbnailIds = list.thumbids;
533 final DiskCache thumbnailCache = LocalDataSource.sThumbnailCache;
534 for (int i = 0; i < size; ++i) {
535 if (Thread.interrupted()) {
538 final long id = ids[i];
539 final long timeModifiedInSec = timestamp[i];
540 final long thumbnailId = thumbnailIds[i];
541 if (!thumbnailCache.isDataAvailable(thumbnailId, timeModifiedInSec * 1000)) {
542 buildThumbnailForId(context, thumbnailCache, thumbnailId, id, false, DEFAULT_THUMBNAIL_WIDTH,
543 DEFAULT_THUMBNAIL_HEIGHT);
546 Log.i(TAG, "DiskCache ready for all thumbnails.");
549 private static final byte[] buildThumbnailForId(final Context context, final DiskCache thumbnailCache, final long thumbId,
550 final long origId, final boolean isVideo, final int thumbnailWidth, final int thumbnailHeight) {
551 if (origId == Shared.INVALID) {
555 Bitmap bitmap = null;
558 final String uriString = BASE_CONTENT_STRING_IMAGES + origId;
559 UriTexture.invalidateCache(thumbId, 1024);
561 bitmap = UriTexture.createFromUri(context, uriString, 1024, 1024, thumbId, null);
562 } catch (IOException e) {
564 } catch (URISyntaxException e) {
568 Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
573 } catch (InterruptedException e) {
577 MediaStore.Video.Thumbnails.cancelThumbnailRequest(context.getContentResolver(), origId);
578 } catch (Exception e) {
583 bitmap = MediaStore.Video.Thumbnails.getThumbnail(context.getContentResolver(), origId,
584 MediaStore.Video.Thumbnails.MICRO_KIND, null);
586 if (bitmap == null) {
589 final byte[] retVal = writeBitmapToCache(thumbnailCache, thumbId, origId, bitmap, thumbnailWidth, thumbnailHeight);
591 } catch (InterruptedException e) {
596 public static final byte[] writeBitmapToCache(final DiskCache thumbnailCache, final long thumbId, final long origId,
597 final Bitmap bitmap, final int thumbnailWidth, final int thumbnailHeight) {
598 final int width = bitmap.getWidth();
599 final int height = bitmap.getHeight();
600 // Detect faces to find the focal point, otherwise fall back to the image center.
601 int focusX = width / 2;
602 int focusY = height / 2;
603 // We have commented out face detection since it slows down the generation of the thumbnail and screennail.
605 // final FaceDetector faceDetector = new FaceDetector(width, height, 1);
606 // final FaceDetector.Face[] faces = new FaceDetector.Face[1];
607 // final int numFaces = faceDetector.findFaces(bitmap, faces);
608 // if (numFaces > 0 && faces[0].confidence() >= FaceDetector.Face.CONFIDENCE_THRESHOLD) {
609 // final PointF midPoint = new PointF();
610 // faces[0].getMidPoint(midPoint);
611 // focusX = (int) midPoint.x;
612 // focusY = (int) midPoint.y;
615 // Crop to thumbnail aspect ratio biased towards the focus point.
621 if (thumbnailWidth * height < thumbnailHeight * width) {
622 // Vertically constrained.
623 cropWidth = thumbnailWidth * height / thumbnailHeight;
624 cropX = Math.max(0, Math.min(focusX - cropWidth / 2, width - cropWidth));
627 scaleFactor = (float) thumbnailHeight / height;
629 // Horizontally constrained.
630 cropHeight = thumbnailHeight * width / thumbnailWidth;
631 cropY = Math.max(0, Math.min(focusY - cropHeight / 2, height - cropHeight));
634 scaleFactor = (float) thumbnailWidth / width;
636 final Bitmap finalBitmap = Bitmap.createBitmap(thumbnailWidth, thumbnailHeight, Bitmap.Config.RGB_565);
637 final Canvas canvas = new Canvas(finalBitmap);
638 final Paint paint = new Paint();
639 paint.setFilterBitmap(true);
641 canvas.drawBitmap(bitmap, new Rect(cropX, cropY, cropX + cropWidth, cropY + cropHeight), new Rect(0, 0, thumbnailWidth,
642 thumbnailHeight), paint);
645 // Store (long thumbnailId, short focusX, short focusY, JPEG data).
646 final ByteArrayOutputStream cacheOutput = new ByteArrayOutputStream(16384);
647 final DataOutputStream dataOutput = new DataOutputStream(cacheOutput);
648 byte[] retVal = null;
650 dataOutput.writeLong(origId);
651 dataOutput.writeShort((int) ((focusX - cropX) * scaleFactor));
652 dataOutput.writeShort((int) ((focusY - cropY) * scaleFactor));
654 finalBitmap.compress(Bitmap.CompressFormat.JPEG, 80, cacheOutput);
655 retVal = cacheOutput.toByteArray();
656 synchronized (thumbnailCache) {
657 thumbnailCache.put(thumbId, retVal);
660 } catch (Exception e) {
666 public CacheService() {
667 super("CacheService");
671 protected void onHandleIntent(final Intent intent) {
672 Log.i(TAG, "Starting CacheService");
673 if (Environment.getExternalStorageState() == Environment.MEDIA_BAD_REMOVAL) {
674 sAlbumCache.deleteAll();
675 putLocaleForAlbumCache(Locale.getDefault());
677 Locale locale = getLocaleForAlbumCache();
678 if (locale != null && locale.equals(Locale.getDefault())) {
679 // The cache is in the same locale as the system locale.
680 if (!isCacheReady(false)) {
681 // The albums and their items have not yet been cached, we need to run the service.
682 startNewCacheThread();
684 startNewCacheThreadForDirtySets();
687 // The locale has changed, we need to regenerate the strings.
688 sAlbumCache.deleteAll();
689 putLocaleForAlbumCache(Locale.getDefault());
690 startNewCacheThread();
692 if (intent.getBooleanExtra("checkthumbnails", false)) {
693 startNewThumbnailThread(this);
695 final Thread existingThread = THUMBNAIL_THREAD.getAndSet(null);
696 if (existingThread != null) {
697 existingThread.interrupt();
702 private static final void putLocaleForAlbumCache(final Locale locale) {
703 final ByteArrayOutputStream bos = new ByteArrayOutputStream();
704 final DataOutputStream dos = new DataOutputStream(bos);
706 Utils.writeUTF(dos, locale.getCountry());
707 Utils.writeUTF(dos, locale.getLanguage());
708 Utils.writeUTF(dos, locale.getVariant());
711 final byte[] data = bos.toByteArray();
712 sAlbumCache.put(ALBUM_CACHE_LOCALE_INDEX, data);
716 } catch (IOException e) {
717 // Could not write locale to cache.
718 Log.i(TAG, "Error writing locale to cache.");
723 private static final Locale getLocaleForAlbumCache() {
724 final byte[] data = sAlbumCache.get(ALBUM_CACHE_LOCALE_INDEX, 0);
725 if (data != null && data.length > 0) {
726 ByteArrayInputStream bis = new ByteArrayInputStream(data);
727 DataInputStream dis = new DataInputStream(bis);
729 String country = Utils.readUTF(dis);
732 String language = Utils.readUTF(dis);
733 if (language == null)
735 String variant = Utils.readUTF(dis);
738 final Locale locale = new Locale(language, country, variant);
742 } catch (IOException e) {
743 // Could not read locale in cache.
744 Log.i(TAG, "Error reading locale from cache.");
751 private static final void restartThread(final AtomicReference<Thread> threadRef, final String name, final Runnable action) {
752 // Create a new thread.
753 final Thread newThread = new Thread() {
758 threadRef.compareAndSet(this, null);
762 newThread.setName(name);
765 // Interrupt any existing thread.
766 final Thread existingThread = threadRef.getAndSet(newThread);
767 if (existingThread != null) {
768 existingThread.interrupt();
772 public static final void startNewThumbnailThread(final Context context) {
773 restartThread(THUMBNAIL_THREAD, "ThumbnailRefresh", new Runnable() {
775 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
777 // It is an optimization to prevent the thumbnailer from running while the application loads
778 Thread.sleep(THUMBNAILER_WAIT_IN_MS);
779 } catch (InterruptedException e) {
782 CacheService.buildThumbnails(context);
787 private void startNewCacheThread() {
788 restartThread(CACHE_THREAD, "CacheRefresh", new Runnable() {
790 refresh(CacheService.this);
795 private void startNewCacheThreadForDirtySets() {
796 restartThread(CACHE_THREAD, "CacheRefreshDirtySets", new Runnable() {
798 refreshDirtySets(CacheService.this);
803 private static final byte[] concat(final byte[] A, final byte[] B) {
804 final byte[] C = (byte[]) new byte[A.length + B.length];
805 System.arraycopy(A, 0, C, 0, A.length);
806 System.arraycopy(B, 0, C, A.length, B.length);
810 private static final long[] toLongArray(final byte[] data) {
811 final ByteBuffer bBuffer = ByteBuffer.wrap(data);
812 final LongBuffer lBuffer = bBuffer.asLongBuffer();
813 final int numLongs = lBuffer.capacity();
814 final long[] retVal = new long[numLongs];
815 for (int i = 0; i < numLongs; ++i) {
816 retVal[i] = lBuffer.get(i);
821 private static final byte[] longToByteArray(final long l) {
822 final byte[] bArray = new byte[8];
823 final ByteBuffer bBuffer = ByteBuffer.wrap(bArray);
824 final LongBuffer lBuffer = bBuffer.asLongBuffer();
829 private final static void refresh(final Context context) {
830 // First we build the album cache.
831 // This is the meta-data about the albums / buckets on the SD card.
832 Log.i(TAG, "Refreshing cache.");
833 int priority = Process.getThreadPriority(Process.myTid());
834 Process.setThreadPriority(Process.THREAD_PRIORITY_MORE_FAVORABLE);
835 sAlbumCache.deleteAll();
836 putLocaleForAlbumCache(Locale.getDefault());
838 final ArrayList<MediaSet> sets = new ArrayList<MediaSet>();
839 LongSparseArray<MediaSet> acceleratedSets = new LongSparseArray<MediaSet>();
840 Log.i(TAG, "Building albums.");
841 final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI.buildUpon().appendQueryParameter("distinct", "true").build();
842 final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI.buildUpon().appendQueryParameter("distinct", "true").build();
843 final ContentResolver cr = context.getContentResolver();
845 final Cursor cursorImages = cr.query(uriImages, BUCKET_PROJECTION_IMAGES, null, null, DEFAULT_BUCKET_SORT_ORDER);
846 final Cursor cursorVideos = cr.query(uriVideos, BUCKET_PROJECTION_VIDEOS, null, null, DEFAULT_BUCKET_SORT_ORDER);
847 Cursor[] cursors = new Cursor[2];
848 cursors[0] = cursorImages;
849 cursors[1] = cursorVideos;
850 final SortCursor sortCursor = new SortCursor(cursors, Images.ImageColumns.BUCKET_DISPLAY_NAME, SortCursor.TYPE_STRING, true);
852 if (sortCursor != null && sortCursor.moveToFirst()) {
853 sets.ensureCapacity(sortCursor.getCount());
854 acceleratedSets = new LongSparseArray<MediaSet>(sortCursor.getCount());
855 MediaSet cameraSet = new MediaSet();
856 cameraSet.mId = LocalDataSource.CAMERA_BUCKET_ID;
857 cameraSet.mName = context.getResources().getString(R.string.camera);
859 acceleratedSets.put(cameraSet.mId, cameraSet);
861 if (Thread.interrupted()) {
864 long setId = sortCursor.getLong(BUCKET_ID_INDEX);
865 MediaSet mediaSet = findSet(setId, acceleratedSets);
866 if (mediaSet == null) {
867 mediaSet = new MediaSet();
868 mediaSet.mId = setId;
869 mediaSet.mName = sortCursor.getString(BUCKET_NAME_INDEX);
871 acceleratedSets.put(setId, mediaSet);
873 mediaSet.mHasImages |= (sortCursor.getCurrentCursorIndex() == 0);
874 mediaSet.mHasVideos |= (sortCursor.getCurrentCursorIndex() == 1);
875 } while (sortCursor.moveToNext());
879 if (sortCursor != null)
883 sAlbumCache.put(ALBUM_CACHE_INCOMPLETE_INDEX, sDummyData);
884 writeSetsToCache(sets);
885 Log.i(TAG, "Done building albums.");
886 // Now we must cache the items contained in every album / bucket.
887 populateMediaItemsForSets(context, sets, acceleratedSets, false);
888 sAlbumCache.delete(ALBUM_CACHE_INCOMPLETE_INDEX);
889 Process.setThreadPriority(priority);
890 if (QUEUE_DIRTY_ALL) {
891 QUEUE_DIRTY_ALL = false;
896 private final static void refreshDirtySets(final Context context) {
897 final byte[] existingData = sAlbumCache.get(ALBUM_CACHE_DIRTY_BUCKET_INDEX, 0);
898 if (existingData != null && existingData.length > 0) {
899 final long[] ids = toLongArray(existingData);
900 final int numIds = ids.length;
902 final ArrayList<MediaSet> sets = new ArrayList<MediaSet>(numIds);
903 final LongSparseArray<MediaSet> acceleratedSets = new LongSparseArray<MediaSet>(numIds);
904 for (int i = 0; i < numIds; ++i) {
905 final MediaSet set = new MediaSet();
908 acceleratedSets.put(set.mId, set);
910 Log.i(TAG, "Refreshing dirty albums");
911 populateMediaItemsForSets(context, sets, acceleratedSets, true);
914 if (QUEUE_DIRTY_SET) {
915 QUEUE_DIRTY_SET = false;
916 refreshDirtySets(context);
918 sAlbumCache.delete(ALBUM_CACHE_DIRTY_BUCKET_INDEX);
922 private final static void populateMediaItemsForSets(final Context context, final ArrayList<MediaSet> sets,
923 final LongSparseArray<MediaSet> acceleratedSets, boolean useWhere) {
924 if (sets == null || sets.size() == 0 || Thread.interrupted()) {
927 Log.i(TAG, "Building items.");
928 final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
929 final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI;
930 final ContentResolver cr = context.getContentResolver();
932 String whereClause = null;
934 int numSets = sets.size();
935 StringBuffer whereString = new StringBuffer(Images.ImageColumns.BUCKET_ID + " in (");
936 for (int i = 0; i < numSets; ++i) {
937 whereString.append(sets.get(i).mId);
938 if (i != numSets - 1) {
939 whereString.append(",");
942 whereString.append(")");
943 whereClause = whereString.toString();
944 Log.i(TAG, "Updating dirty albums where " + whereClause);
947 final Cursor cursorImages = cr.query(uriImages, PROJECTION_IMAGES, whereClause, null, DEFAULT_IMAGE_SORT_ORDER);
948 final Cursor cursorVideos = cr.query(uriVideos, PROJECTION_VIDEOS, whereClause, null, DEFAULT_VIDEO_SORT_ORDER);
949 final Cursor[] cursors = new Cursor[2];
950 cursors[0] = cursorImages;
951 cursors[1] = cursorVideos;
952 final SortCursor sortCursor = new SortCursor(cursors, Images.ImageColumns.DATE_TAKEN, SortCursor.TYPE_NUMERIC, true);
953 if (Thread.interrupted()) {
957 if (sortCursor != null && sortCursor.moveToFirst()) {
958 final int count = sortCursor.getCount();
959 final int numSets = sets.size();
960 final int approximateCountPerSet = count / numSets;
961 for (int i = 0; i < numSets; ++i) {
962 final MediaSet set = sets.get(i);
963 set.getItems().clear();
964 set.setNumExpectedItems(approximateCountPerSet);
967 if (Thread.interrupted()) {
970 final MediaItem item = new MediaItem();
971 final boolean isVideo = (sortCursor.getCurrentCursorIndex() == 1);
973 populateVideoItemFromCursor(item, cr, sortCursor, CacheService.BASE_CONTENT_STRING_VIDEOS);
975 populateMediaItemFromCursor(item, cr, sortCursor, CacheService.BASE_CONTENT_STRING_IMAGES);
977 final long setId = sortCursor.getLong(MEDIA_BUCKET_ID_INDEX);
978 final MediaSet set = findSet(setId, acceleratedSets);
980 set.getItems().add(item);
982 } while (sortCursor.moveToNext());
985 if (sortCursor != null) sortCursor.close();
987 if (sets.size() > 0) {
988 writeItemsToCache(sets);
989 Log.i(TAG, "Done building items.");
993 private static final void writeSetsToCache(final ArrayList<MediaSet> sets) {
994 final ByteArrayOutputStream bos = new ByteArrayOutputStream();
995 final int numSets = sets.size();
996 final DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(bos, 256));
998 dos.writeInt(numSets);
999 for (int i = 0; i < numSets; ++i) {
1000 if (Thread.interrupted()) {
1003 final MediaSet set = sets.get(i);
1004 dos.writeLong(set.mId);
1005 Utils.writeUTF(dos, set.mName);
1006 dos.writeBoolean(set.mHasImages);
1007 dos.writeBoolean(set.mHasVideos);
1010 sAlbumCache.put(ALBUM_CACHE_METADATA_INDEX, bos.toByteArray());
1013 sAlbumCache.deleteAll();
1014 putLocaleForAlbumCache(Locale.getDefault());
1016 sAlbumCache.flush();
1017 } catch (IOException e) {
1018 Log.e(TAG, "Error writing albums to diskcache.");
1019 sAlbumCache.deleteAll();
1020 putLocaleForAlbumCache(Locale.getDefault());
1024 private static final void writeItemsToCache(final ArrayList<MediaSet> sets) {
1025 final int numSets = sets.size();
1026 for (int i = 0; i < numSets; ++i) {
1027 if (Thread.interrupted()) {
1030 writeItemsForASet(sets.get(i));
1032 sAlbumCache.flush();
1035 private static final void writeItemsForASet(final MediaSet set) {
1036 final ByteArrayOutputStream bos = new ByteArrayOutputStream();
1037 final DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(bos, 256));
1039 final ArrayList<MediaItem> items = set.getItems();
1040 final int numItems = items.size();
1041 dos.writeInt(numItems);
1042 dos.writeLong(set.mMinTimestamp);
1043 dos.writeLong(set.mMaxTimestamp);
1044 for (int i = 0; i < numItems; ++i) {
1045 MediaItem item = items.get(i);
1046 if (set.mId == LocalDataSource.CAMERA_BUCKET_ID || set.mId == LocalDataSource.DOWNLOAD_BUCKET_ID) {
1047 // Reverse the display order for the camera bucket - want the latest first.
1048 item = items.get(numItems - i - 1);
1050 dos.writeLong(item.mId);
1051 Utils.writeUTF(dos, item.mCaption);
1052 Utils.writeUTF(dos, item.mMimeType);
1053 dos.writeInt(item.getMediaType());
1054 dos.writeDouble(item.mLatitude);
1055 dos.writeDouble(item.mLongitude);
1056 dos.writeLong(item.mDateTakenInMs);
1057 dos.writeBoolean(item.mTriedRetrievingExifDateTaken);
1058 dos.writeLong(item.mDateAddedInSec);
1059 dos.writeLong(item.mDateModifiedInSec);
1060 dos.writeInt(item.mDurationInSec);
1061 dos.writeInt((int) item.mRotation);
1062 Utils.writeUTF(dos, item.mFilePath);
1065 sAlbumCache.put(set.mId, bos.toByteArray());
1067 } catch (IOException e) {
1068 Log.e(TAG, "Error writing to diskcache for set " + set.mName);
1069 sAlbumCache.deleteAll();
1070 putLocaleForAlbumCache(Locale.getDefault());
1074 private static final MediaSet findSet(final long id, final LongSparseArray<MediaSet> acceleratedTable) {
1075 // This is the accelerated lookup table for the MediaSet based on set id.
1076 return acceleratedTable.get(id);