blob: d54f9fa3fb01d6b5465c7b52cdc335028d6cecae [file] [log] [blame]
package com.cooliris.cache;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.LongBuffer;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicReference;
import android.app.IntentService;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.MergeCursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Environment;
import android.os.Process;
import android.os.SystemClock;
import android.provider.MediaStore;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Video;
import android.util.Log;
import com.cooliris.media.DataSource;
import com.cooliris.media.DiskCache;
import com.cooliris.media.Gallery;
import com.cooliris.media.LocalDataSource;
import com.cooliris.media.LongSparseArray;
import com.cooliris.media.MediaFeed;
import com.cooliris.media.MediaItem;
import com.cooliris.media.MediaSet;
import com.cooliris.media.R;
import com.cooliris.media.Shared;
import com.cooliris.media.SortCursor;
import com.cooliris.media.UriTexture;
import com.cooliris.media.Utils;
public final class CacheService extends IntentService {
public static final String ACTION_CACHE = "com.cooliris.cache.action.CACHE";
public static final DiskCache sAlbumCache = new DiskCache("local-album-cache");
public static final DiskCache sMetaAlbumCache = new DiskCache("local-meta-album-cache");
private static final String TAG = "CacheService";
private static ImageList sList = null;
// Wait 2 seconds to start the thumbnailer so that the application can load
// without any overheads.
private static final int THUMBNAILER_WAIT_IN_MS = 2000;
private static final int DEFAULT_THUMBNAIL_WIDTH = 128;
private static final int DEFAULT_THUMBNAIL_HEIGHT = 96;
public static final String DEFAULT_IMAGE_SORT_ORDER = Images.ImageColumns.DATE_TAKEN + " ASC, "
+ Images.ImageColumns.DATE_ADDED + " ASC";
public static final String DEFAULT_VIDEO_SORT_ORDER = Video.VideoColumns.DATE_TAKEN + " ASC, " + Video.VideoColumns.DATE_ADDED
+ " ASC";
public static final String DEFAULT_BUCKET_SORT_ORDER = "upper(" + Images.ImageColumns.BUCKET_DISPLAY_NAME + ") ASC";
// Must preserve order between these indices and the order of the terms in
// BUCKET_PROJECTION_IMAGES, BUCKET_PROJECTION_VIDEOS.
// Not using SortedHashMap for efficieny reasons.
public static final int BUCKET_ID_INDEX = 0;
public static final int BUCKET_NAME_INDEX = 1;
public static final String[] BUCKET_PROJECTION_IMAGES = new String[] { Images.ImageColumns.BUCKET_ID,
Images.ImageColumns.BUCKET_DISPLAY_NAME };
public static final String[] BUCKET_PROJECTION_VIDEOS = new String[] { Video.VideoColumns.BUCKET_ID,
Video.VideoColumns.BUCKET_DISPLAY_NAME };
// Must preserve order between these indices and the order of the terms in
// THUMBNAIL_PROJECTION.
public static final int THUMBNAIL_ID_INDEX = 0;
public static final int THUMBNAIL_DATE_MODIFIED_INDEX = 1;
public static final int THUMBNAIL_DATA_INDEX = 2;
public static final int THUMBNAIL_ORIENTATION_INDEX = 2;
public static final String[] THUMBNAIL_PROJECTION = new String[] { Images.ImageColumns._ID, Images.ImageColumns.DATE_MODIFIED,
Images.ImageColumns.DATA, Images.ImageColumns.ORIENTATION };
public static final String[] SENSE_PROJECTION = new String[] { Images.ImageColumns.BUCKET_ID,
"MAX(" + Images.ImageColumns.DATE_ADDED + ")" };
// Must preserve order between these indices and the order of the terms in
// INITIAL_PROJECTION_IMAGES and
// INITIAL_PROJECTION_VIDEOS.
public static final int MEDIA_ID_INDEX = 0;
public static final int MEDIA_CAPTION_INDEX = 1;
public static final int MEDIA_MIME_TYPE_INDEX = 2;
public static final int MEDIA_LATITUDE_INDEX = 3;
public static final int MEDIA_LONGITUDE_INDEX = 4;
public static final int MEDIA_DATE_TAKEN_INDEX = 5;
public static final int MEDIA_DATE_ADDED_INDEX = 6;
public static final int MEDIA_DATE_MODIFIED_INDEX = 7;
public static final int MEDIA_DATA_INDEX = 8;
public static final int MEDIA_ORIENTATION_OR_DURATION_INDEX = 9;
public static final int MEDIA_BUCKET_ID_INDEX = 10;
public static final String[] PROJECTION_IMAGES = new String[] { Images.ImageColumns._ID, Images.ImageColumns.TITLE,
Images.ImageColumns.MIME_TYPE, Images.ImageColumns.LATITUDE, Images.ImageColumns.LONGITUDE,
Images.ImageColumns.DATE_TAKEN, Images.ImageColumns.DATE_ADDED, Images.ImageColumns.DATE_MODIFIED,
Images.ImageColumns.DATA, Images.ImageColumns.ORIENTATION, Images.ImageColumns.BUCKET_ID };
private static final String[] PROJECTION_VIDEOS = new String[] { Video.VideoColumns._ID, Video.VideoColumns.TITLE,
Video.VideoColumns.MIME_TYPE, Video.VideoColumns.LATITUDE, Video.VideoColumns.LONGITUDE, Video.VideoColumns.DATE_TAKEN,
Video.VideoColumns.DATE_ADDED, Video.VideoColumns.DATE_MODIFIED, Video.VideoColumns.DATA, Video.VideoColumns.DURATION,
Video.VideoColumns.BUCKET_ID };
public static final String BASE_CONTENT_STRING_IMAGES = (Images.Media.EXTERNAL_CONTENT_URI).toString() + "/";
public static final String BASE_CONTENT_STRING_VIDEOS = (Video.Media.EXTERNAL_CONTENT_URI).toString() + "/";
private static final AtomicReference<Thread> CACHE_THREAD = new AtomicReference<Thread>();
private static final AtomicReference<Thread> THUMBNAIL_THREAD = new AtomicReference<Thread>();
// Special indices in the Albumcache.
private static final int ALBUM_CACHE_METADATA_INDEX = -1;
private static final int ALBUM_CACHE_DIRTY_INDEX = -2;
private static final int ALBUM_CACHE_INCOMPLETE_INDEX = -3;
private static final int ALBUM_CACHE_DIRTY_BUCKET_INDEX = -4;
private static final int ALBUM_CACHE_LOCALE_INDEX = -5;
private static final DateFormat mDateFormat = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
private static final DateFormat mAltDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
private static final byte[] sDummyData = new byte[] { 1 };
private static boolean QUEUE_DIRTY_SET;
private static boolean QUEUE_DIRTY_ALL;
private static boolean QUEUE_DIRTY_SENSE;
public interface Observer {
void onChange(long[] bucketIds);
}
public static final String getCachePath(final String subFolderName) {
return Environment.getExternalStorageDirectory() + "/Android/data/com.cooliris.media/cache/" + subFolderName;
}
public static final void startCache(final Context context, final boolean checkthumbnails) {
final Locale locale = getLocaleForAlbumCache();
final Locale defaultLocale = Locale.getDefault();
if (locale == null || !locale.equals(defaultLocale)) {
sAlbumCache.deleteAll();
putLocaleForAlbumCache(defaultLocale);
}
final Intent intent = new Intent(ACTION_CACHE, null, context, CacheService.class);
intent.putExtra("checkthumbnails", checkthumbnails);
context.startService(intent);
}
public static final boolean isCacheReady(final boolean onlyMediaSets) {
if (onlyMediaSets) {
return (sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0) != null && sAlbumCache.get(ALBUM_CACHE_DIRTY_INDEX, 0) == null);
} else {
return (sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0) != null && sAlbumCache.get(ALBUM_CACHE_DIRTY_INDEX, 0) == null && sAlbumCache
.get(ALBUM_CACHE_INCOMPLETE_INDEX, 0) == null);
}
}
public static final boolean isCacheReady(final long setId) {
final boolean isReady = (sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0) != null
&& sAlbumCache.get(ALBUM_CACHE_DIRTY_INDEX, 0) == null && sAlbumCache.get(ALBUM_CACHE_INCOMPLETE_INDEX, 0) == null);
if (!isReady) {
return isReady;
}
// Also, we need to check if this setId is dirty.
final byte[] existingData = sAlbumCache.get(ALBUM_CACHE_DIRTY_BUCKET_INDEX, 0);
if (existingData != null && existingData.length > 0) {
final long[] ids = toLongArray(existingData);
final int numIds = ids.length;
for (int i = 0; i < numIds; ++i) {
if (ids[i] == setId) {
return false;
}
}
}
return true;
}
public static final boolean isPresentInCache(final long setId) {
return sAlbumCache.get(setId, 0) != null;
}
public static final void senseDirty(final Context context, final Observer observer) {
if (CACHE_THREAD.get() == null) {
QUEUE_DIRTY_SENSE = false;
QUEUE_DIRTY_ALL = false;
QUEUE_DIRTY_SET = false;
restartThread(CACHE_THREAD, "CacheRefresh", new Runnable() {
public void run() {
Log.i(TAG, "Computing dirty sets.");
long ids[] = computeDirtySets(context);
if (ids != null && observer != null) {
observer.onChange(ids);
}
if (ids.length > 0) {
sList = null;
}
Log.i(TAG, "Done computing dirty sets for num " + ids.length);
}
});
} else {
QUEUE_DIRTY_SENSE = true;
}
}
public static final void markDirty(final Context context) {
sList = null;
sAlbumCache.put(ALBUM_CACHE_DIRTY_INDEX, sDummyData);
if (CACHE_THREAD.get() == null) {
QUEUE_DIRTY_SENSE = false;
QUEUE_DIRTY_ALL = false;
QUEUE_DIRTY_SET = false;
restartThread(CACHE_THREAD, "CacheRefresh", new Runnable() {
public void run() {
refresh(context);
}
});
} else {
QUEUE_DIRTY_ALL = true;
}
}
public static final void markDirtyImmediate(final long id) {
if (id == Shared.INVALID) {
return;
}
sList = null;
byte[] data = longToByteArray(id);
final byte[] existingData = sAlbumCache.get(ALBUM_CACHE_DIRTY_BUCKET_INDEX, 0);
if (existingData != null && existingData.length > 0) {
final long[] ids = toLongArray(existingData);
final int numIds = ids.length;
for (int i = 0; i < numIds; ++i) {
if (ids[i] == id) {
return;
}
}
// Add this to the existing keys and concatenate the byte arrays.
data = concat(data, existingData);
}
sAlbumCache.put(ALBUM_CACHE_DIRTY_BUCKET_INDEX, data);
}
public static final void markDirty(final Context context, final long id) {
markDirtyImmediate(id);
if (CACHE_THREAD.get() == null) {
QUEUE_DIRTY_SET = false;
restartThread(CACHE_THREAD, "CacheRefreshDirtySets", new Runnable() {
public void run() {
refreshDirtySets(context);
}
});
} else {
QUEUE_DIRTY_SET = true;
}
}
public static final boolean setHasItems(final ContentResolver cr, final long setId) {
final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI;
final StringBuffer whereString = new StringBuffer(Images.ImageColumns.BUCKET_ID + "=" + setId);
final Cursor cursorImages = cr.query(uriImages, BUCKET_PROJECTION_IMAGES, whereString.toString(), null, null);
if (cursorImages != null && cursorImages.getCount() > 0) {
cursorImages.close();
return true;
}
final Cursor cursorVideos = cr.query(uriVideos, BUCKET_PROJECTION_VIDEOS, whereString.toString(), null, null);
if (cursorVideos != null && cursorVideos.getCount() > 0) {
cursorVideos.close();
return true;
}
return false;
}
public static final void loadMediaSets(final MediaFeed feed, final DataSource source, final boolean includeImages,
final boolean includeVideos) {
int timeElapsed = 0;
while (!isCacheReady(true) && timeElapsed < 10000) {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
return;
}
timeElapsed += 300;
}
final byte[] albumData = sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0);
if (albumData != null && albumData.length > 0) {
final DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(albumData), 256));
try {
final int numAlbums = dis.readInt();
for (int i = 0; i < numAlbums; ++i) {
final long setId = dis.readLong();
final String name = Utils.readUTF(dis);
final boolean hasImages = dis.readBoolean();
final boolean hasVideos = dis.readBoolean();
MediaSet mediaSet = feed.getMediaSet(setId);
if (mediaSet == null) {
mediaSet = feed.addMediaSet(setId, source);
}
if ((includeImages && hasImages) || (includeVideos && hasVideos)) {
mediaSet.mName = name;
mediaSet.mHasImages = hasImages;
mediaSet.mHasVideos = hasVideos;
mediaSet.mPicasaAlbumId = Shared.INVALID;
mediaSet.generateTitle(true);
}
}
} catch (IOException e) {
Log.e(TAG, "Error loading albums.");
sAlbumCache.deleteAll();
putLocaleForAlbumCache(Locale.getDefault());
}
} else {
Log.d(TAG, "No albums found.");
}
}
public static final void loadMediaSet(final MediaFeed feed, final DataSource source, final long bucketId) {
int timeElapsed = 0;
while (!isCacheReady(false) && timeElapsed < 10000) {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
return;
}
timeElapsed += 300;
}
final byte[] albumData = sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0);
if (albumData != null && albumData.length > 0) {
DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(albumData), 256));
try {
final int numAlbums = dis.readInt();
for (int i = 0; i < numAlbums; ++i) {
final long setId = dis.readLong();
MediaSet mediaSet = null;
if (setId == bucketId) {
mediaSet = feed.getMediaSet(setId);
if (mediaSet == null) {
mediaSet = feed.addMediaSet(setId, source);
}
} else {
mediaSet = new MediaSet();
}
mediaSet.mName = Utils.readUTF(dis);
if (setId == bucketId) {
mediaSet.mPicasaAlbumId = Shared.INVALID;
mediaSet.generateTitle(true);
return;
}
}
} catch (IOException e) {
Log.e(TAG, "Error finding album " + bucketId);
sAlbumCache.deleteAll();
putLocaleForAlbumCache(Locale.getDefault());
}
} else {
Log.d(TAG, "No album found for album id " + bucketId);
}
}
public static final void loadMediaItemsIntoMediaFeed(final MediaFeed feed, final MediaSet set, final int rangeStart,
final int rangeEnd, final boolean includeImages, final boolean includeVideos) {
int timeElapsed = 0;
byte[] albumData = null;
while (!isCacheReady(set.mId) && timeElapsed < 30000) {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
return;
}
timeElapsed += 300;
}
albumData = sAlbumCache.get(set.mId, 0);
if (albumData != null && set.mNumItemsLoaded < set.getNumExpectedItems()) {
final DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(albumData), 256));
try {
final int numItems = dis.readInt();
set.setNumExpectedItems(numItems);
set.mMinTimestamp = dis.readLong();
set.mMaxTimestamp = dis.readLong();
for (int i = 0; i < numItems; ++i) {
final MediaItem item = new MediaItem();
// Must preserve order with method that writes to cache.
item.mId = dis.readLong();
item.mCaption = Utils.readUTF(dis);
item.mMimeType = Utils.readUTF(dis);
item.setMediaType(dis.readInt());
item.mLatitude = dis.readDouble();
item.mLongitude = dis.readDouble();
item.mDateTakenInMs = dis.readLong();
item.mTriedRetrievingExifDateTaken = dis.readBoolean();
item.mDateAddedInSec = dis.readLong();
item.mDateModifiedInSec = dis.readLong();
item.mDurationInSec = dis.readInt();
item.mRotation = (float) dis.readInt();
item.mFilePath = Utils.readUTF(dis);
int itemMediaType = item.getMediaType();
if ((itemMediaType == MediaItem.MEDIA_TYPE_IMAGE && includeImages)
|| (itemMediaType == MediaItem.MEDIA_TYPE_VIDEO && includeVideos)) {
String baseUri = (itemMediaType == MediaItem.MEDIA_TYPE_IMAGE) ? BASE_CONTENT_STRING_IMAGES
: BASE_CONTENT_STRING_VIDEOS;
item.mContentUri = baseUri + item.mId;
feed.addItemToMediaSet(item, set);
}
}
dis.close();
} catch (IOException e) {
Log.e(TAG, "Error loading items for album " + set.mName);
sAlbumCache.deleteAll();
putLocaleForAlbumCache(Locale.getDefault());
}
} else {
Log.d(TAG, "No items found for album " + set.mName);
}
set.updateNumExpectedItems();
set.generateTitle(true);
}
public static final void populateVideoItemFromCursor(final MediaItem item, final ContentResolver cr, final Cursor cursor,
final String baseUri) {
item.setMediaType(MediaItem.MEDIA_TYPE_VIDEO);
populateMediaItemFromCursor(item, cr, cursor, baseUri);
}
public static final void populateMediaItemFromCursor(final MediaItem item, final ContentResolver cr, final Cursor cursor,
final String baseUri) {
item.mId = cursor.getLong(CacheService.MEDIA_ID_INDEX);
item.mCaption = cursor.getString(CacheService.MEDIA_CAPTION_INDEX);
item.mMimeType = cursor.getString(CacheService.MEDIA_MIME_TYPE_INDEX);
item.mLatitude = cursor.getDouble(CacheService.MEDIA_LATITUDE_INDEX);
item.mLongitude = cursor.getDouble(CacheService.MEDIA_LONGITUDE_INDEX);
item.mDateTakenInMs = cursor.getLong(CacheService.MEDIA_DATE_TAKEN_INDEX);
item.mDateAddedInSec = cursor.getLong(CacheService.MEDIA_DATE_ADDED_INDEX);
item.mDateModifiedInSec = cursor.getLong(CacheService.MEDIA_DATE_MODIFIED_INDEX);
if (item.mDateTakenInMs == item.mDateModifiedInSec) {
item.mDateTakenInMs = item.mDateModifiedInSec * 1000;
}
item.mFilePath = cursor.getString(CacheService.MEDIA_DATA_INDEX);
if (baseUri != null)
item.mContentUri = baseUri + item.mId;
final int itemMediaType = item.getMediaType();
// Check to see if a new date taken is available.
final long dateTaken = fetchDateTaken(item);
if (dateTaken != -1L && item.mContentUri != null) {
item.mDateTakenInMs = dateTaken;
final ContentValues values = new ContentValues();
if (itemMediaType == MediaItem.MEDIA_TYPE_VIDEO) {
values.put(Video.VideoColumns.DATE_TAKEN, item.mDateTakenInMs);
} else {
values.put(Images.ImageColumns.DATE_TAKEN, item.mDateTakenInMs);
}
cr.update(Uri.parse(item.mContentUri), values, null, null);
}
final int orientationDurationValue = cursor.getInt(CacheService.MEDIA_ORIENTATION_OR_DURATION_INDEX);
if (itemMediaType == MediaItem.MEDIA_TYPE_IMAGE) {
item.mRotation = orientationDurationValue;
} else {
item.mDurationInSec = orientationDurationValue;
}
}
// Returns -1 if we failed to examine EXIF information or EXIF parsing
// failed.
public static final long fetchDateTaken(final MediaItem item) {
if (!item.isDateTakenValid() && !item.mTriedRetrievingExifDateTaken
&& (item.mFilePath.endsWith(".jpg") || item.mFilePath.endsWith(".jpeg"))) {
try {
Log.i(TAG, "Parsing date taken from exif");
final ExifInterface exif = new ExifInterface(item.mFilePath);
final String dateTakenStr = exif.getAttribute(ExifInterface.TAG_DATETIME);
if (dateTakenStr != null) {
try {
final Date dateTaken = mDateFormat.parse(dateTakenStr);
return dateTaken.getTime();
} catch (ParseException pe) {
try {
final Date dateTaken = mAltDateFormat.parse(dateTakenStr);
return dateTaken.getTime();
} catch (ParseException pe2) {
Log.i(TAG, "Unable to parse date out of string - " + dateTakenStr);
}
}
}
} catch (Exception e) {
Log.i(TAG, "Error reading Exif information, probably not a jpeg.");
}
// Ensures that we only try retrieving EXIF date taken once.
item.mTriedRetrievingExifDateTaken = true;
}
return -1L;
}
public static final byte[] queryThumbnail(final Context context, final long thumbId, final long origId, final boolean isVideo,
final long timestamp) {
final DiskCache thumbnailCache = (isVideo) ? LocalDataSource.sThumbnailCacheVideo : LocalDataSource.sThumbnailCache;
return queryThumbnail(context, thumbId, origId, isVideo, thumbnailCache, timestamp);
}
public static final ImageList getImageList(final Context context) {
if (sList != null)
return sList;
ImageList list = new ImageList();
final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
final ContentResolver cr = context.getContentResolver();
final Cursor cursorImages = cr.query(uriImages, THUMBNAIL_PROJECTION, null, null, null);
if (cursorImages != null && cursorImages.moveToFirst()) {
final int size = cursorImages.getCount();
final long[] ids = new long[size];
final long[] thumbnailIds = new long[size];
final long[] timestamp = new long[size];
final int[] orientation = new int[size];
int ctr = 0;
do {
if (Thread.interrupted()) {
break;
}
ids[ctr] = cursorImages.getLong(THUMBNAIL_ID_INDEX);
timestamp[ctr] = cursorImages.getLong(THUMBNAIL_DATE_MODIFIED_INDEX);
thumbnailIds[ctr] = Utils.Crc64Long(cursorImages.getString(THUMBNAIL_DATA_INDEX));
orientation[ctr] = cursorImages.getInt(THUMBNAIL_ORIENTATION_INDEX);
++ctr;
} while (cursorImages.moveToNext());
cursorImages.close();
list.ids = ids;
list.thumbids = thumbnailIds;
list.timestamp = timestamp;
list.orientation = orientation;
}
if (sList == null) {
sList = list;
}
return list;
}
private static final byte[] queryThumbnail(final Context context, final long thumbId, final long origId, final boolean isVideo,
final DiskCache thumbnailCache, final long timestamp) {
if (!((Gallery) context).isPaused()) {
final Thread thumbnailThread = THUMBNAIL_THREAD.getAndSet(null);
if (thumbnailThread != null) {
thumbnailThread.interrupt();
}
}
byte[] bitmap = thumbnailCache.get(thumbId, timestamp);
if (bitmap == null) {
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
final long time = SystemClock.uptimeMillis();
bitmap = buildThumbnailForId(context, thumbnailCache, thumbId, origId, isVideo, DEFAULT_THUMBNAIL_WIDTH,
DEFAULT_THUMBNAIL_HEIGHT);
Log.i(TAG, "Built thumbnail and screennail for " + origId + " in " + (SystemClock.uptimeMillis() - time));
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
}
return bitmap;
}
private static final void buildThumbnails(final Context context) {
Log.i(TAG, "Preparing DiskCache for all thumbnails.");
ImageList list = getImageList(context);
final int size = (list.ids == null) ? 0 : list.ids.length;
final long[] ids = list.ids;
final long[] timestamp = list.timestamp;
final long[] thumbnailIds = list.thumbids;
final DiskCache thumbnailCache = LocalDataSource.sThumbnailCache;
for (int i = 0; i < size; ++i) {
if (Thread.interrupted()) {
return;
}
final long id = ids[i];
final long timeModifiedInSec = timestamp[i];
final long thumbnailId = thumbnailIds[i];
if (!thumbnailCache.isDataAvailable(thumbnailId, timeModifiedInSec * 1000)) {
buildThumbnailForId(context, thumbnailCache, thumbnailId, id, false, DEFAULT_THUMBNAIL_WIDTH,
DEFAULT_THUMBNAIL_HEIGHT);
}
}
Log.i(TAG, "DiskCache ready for all thumbnails.");
}
private static final byte[] buildThumbnailForId(final Context context, final DiskCache thumbnailCache, final long thumbId,
final long origId, final boolean isVideo, final int thumbnailWidth, final int thumbnailHeight) {
if (origId == Shared.INVALID) {
return null;
}
try {
Bitmap bitmap = null;
Thread.sleep(1);
if (!isVideo) {
final String uriString = BASE_CONTENT_STRING_IMAGES + origId;
UriTexture.invalidateCache(thumbId, 1024);
try {
bitmap = UriTexture.createFromUri(context, uriString, 1024, 1024, thumbId, null);
} catch (IOException e) {
return null;
} catch (URISyntaxException e) {
return null;
}
} else {
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
new Thread() {
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
;
}
try {
MediaStore.Video.Thumbnails.cancelThumbnailRequest(context.getContentResolver(), origId);
} catch (Exception e) {
;
}
}
}.start();
bitmap = MediaStore.Video.Thumbnails.getThumbnail(context.getContentResolver(), origId,
MediaStore.Video.Thumbnails.MICRO_KIND, null);
}
if (bitmap == null) {
return null;
}
final byte[] retVal = writeBitmapToCache(thumbnailCache, thumbId, origId, bitmap, thumbnailWidth, thumbnailHeight);
return retVal;
} catch (InterruptedException e) {
return null;
}
}
public static final byte[] writeBitmapToCache(final DiskCache thumbnailCache, final long thumbId, final long origId,
final Bitmap bitmap, final int thumbnailWidth, final int thumbnailHeight) {
final int width = bitmap.getWidth();
final int height = bitmap.getHeight();
// Detect faces to find the focal point, otherwise fall back to the
// image center.
int focusX = width / 2;
int focusY = height / 2;
// We have commented out face detection since it slows down the
// generation of the thumbnail and screennail.
// final FaceDetector faceDetector = new FaceDetector(width, height, 1);
// final FaceDetector.Face[] faces = new FaceDetector.Face[1];
// final int numFaces = faceDetector.findFaces(bitmap, faces);
// if (numFaces > 0 && faces[0].confidence() >=
// FaceDetector.Face.CONFIDENCE_THRESHOLD) {
// final PointF midPoint = new PointF();
// faces[0].getMidPoint(midPoint);
// focusX = (int) midPoint.x;
// focusY = (int) midPoint.y;
// }
// Crop to thumbnail aspect ratio biased towards the focus point.
int cropX;
int cropY;
int cropWidth;
int cropHeight;
float scaleFactor;
if (thumbnailWidth * height < thumbnailHeight * width) {
// Vertically constrained.
cropWidth = thumbnailWidth * height / thumbnailHeight;
cropX = Math.max(0, Math.min(focusX - cropWidth / 2, width - cropWidth));
cropY = 0;
cropHeight = height;
scaleFactor = (float) thumbnailHeight / height;
} else {
// Horizontally constrained.
cropHeight = thumbnailHeight * width / thumbnailWidth;
cropY = Math.max(0, Math.min(focusY - cropHeight / 2, height - cropHeight));
cropX = 0;
cropWidth = width;
scaleFactor = (float) thumbnailWidth / width;
}
final Bitmap finalBitmap = Bitmap.createBitmap(thumbnailWidth, thumbnailHeight, Bitmap.Config.RGB_565);
final Canvas canvas = new Canvas(finalBitmap);
final Paint paint = new Paint();
paint.setFilterBitmap(true);
canvas.drawColor(0);
canvas.drawBitmap(bitmap, new Rect(cropX, cropY, cropX + cropWidth, cropY + cropHeight), new Rect(0, 0, thumbnailWidth,
thumbnailHeight), paint);
bitmap.recycle();
// Store (long thumbnailId, short focusX, short focusY, JPEG data).
final ByteArrayOutputStream cacheOutput = new ByteArrayOutputStream(16384);
final DataOutputStream dataOutput = new DataOutputStream(cacheOutput);
byte[] retVal = null;
try {
dataOutput.writeLong(origId);
dataOutput.writeShort((int) ((focusX - cropX) * scaleFactor));
dataOutput.writeShort((int) ((focusY - cropY) * scaleFactor));
dataOutput.flush();
finalBitmap.compress(Bitmap.CompressFormat.JPEG, 80, cacheOutput);
retVal = cacheOutput.toByteArray();
synchronized (thumbnailCache) {
thumbnailCache.put(thumbId, retVal);
}
cacheOutput.close();
} catch (Exception e) {
;
}
return retVal;
}
public CacheService() {
super("CacheService");
}
@Override
protected void onHandleIntent(final Intent intent) {
Log.i(TAG, "Starting CacheService");
if (Environment.getExternalStorageState() == Environment.MEDIA_BAD_REMOVAL) {
sAlbumCache.deleteAll();
putLocaleForAlbumCache(Locale.getDefault());
}
Locale locale = getLocaleForAlbumCache();
if (locale != null && locale.equals(Locale.getDefault())) {
// The cache is in the same locale as the system locale.
if (!isCacheReady(false)) {
// The albums and their items have not yet been cached, we need
// to run the service.
startNewCacheThread();
} else {
startNewCacheThreadForDirtySets();
}
} else {
// The locale has changed, we need to regenerate the strings.
sAlbumCache.deleteAll();
putLocaleForAlbumCache(Locale.getDefault());
startNewCacheThread();
}
if (intent.getBooleanExtra("checkthumbnails", false)) {
startNewThumbnailThread(this);
} else {
final Thread existingThread = THUMBNAIL_THREAD.getAndSet(null);
if (existingThread != null) {
existingThread.interrupt();
}
}
}
private static final void putLocaleForAlbumCache(final Locale locale) {
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
final DataOutputStream dos = new DataOutputStream(bos);
try {
Utils.writeUTF(dos, locale.getCountry());
Utils.writeUTF(dos, locale.getLanguage());
Utils.writeUTF(dos, locale.getVariant());
dos.flush();
bos.flush();
final byte[] data = bos.toByteArray();
sAlbumCache.put(ALBUM_CACHE_LOCALE_INDEX, data);
sAlbumCache.flush();
dos.close();
bos.close();
} catch (IOException e) {
// Could not write locale to cache.
Log.i(TAG, "Error writing locale to cache.");
;
}
}
private static final Locale getLocaleForAlbumCache() {
final byte[] data = sAlbumCache.get(ALBUM_CACHE_LOCALE_INDEX, 0);
if (data != null && data.length > 0) {
ByteArrayInputStream bis = new ByteArrayInputStream(data);
DataInputStream dis = new DataInputStream(bis);
try {
String country = Utils.readUTF(dis);
if (country == null)
country = "";
String language = Utils.readUTF(dis);
if (language == null)
language = "";
String variant = Utils.readUTF(dis);
if (variant == null)
variant = "";
final Locale locale = new Locale(language, country, variant);
dis.close();
bis.close();
return locale;
} catch (IOException e) {
// Could not read locale in cache.
Log.i(TAG, "Error reading locale from cache.");
return null;
}
}
return null;
}
private static final void restartThread(final AtomicReference<Thread> threadRef, final String name, final Runnable action) {
// Create a new thread.
final Thread newThread = new Thread() {
public void run() {
try {
action.run();
} finally {
threadRef.compareAndSet(this, null);
}
}
};
newThread.setName(name);
newThread.start();
// Interrupt any existing thread.
final Thread existingThread = threadRef.getAndSet(newThread);
if (existingThread != null) {
existingThread.interrupt();
}
}
public static final void startNewThumbnailThread(final Context context) {
restartThread(THUMBNAIL_THREAD, "ThumbnailRefresh", new Runnable() {
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
try {
// It is an optimization to prevent the thumbnailer from
// running while the application loads
Thread.sleep(THUMBNAILER_WAIT_IN_MS);
} catch (InterruptedException e) {
return;
}
CacheService.buildThumbnails(context);
}
});
}
private void startNewCacheThread() {
restartThread(CACHE_THREAD, "CacheRefresh", new Runnable() {
public void run() {
refresh(CacheService.this);
}
});
}
private void startNewCacheThreadForDirtySets() {
restartThread(CACHE_THREAD, "CacheRefreshDirtySets", new Runnable() {
public void run() {
refreshDirtySets(CacheService.this);
}
});
}
private static final byte[] concat(final byte[] A, final byte[] B) {
final byte[] C = (byte[]) new byte[A.length + B.length];
System.arraycopy(A, 0, C, 0, A.length);
System.arraycopy(B, 0, C, A.length, B.length);
return C;
}
private static final long toLong(final byte[] data) {
// 8 bytes for a long
if (data == null || data.length < 8)
return 0;
final ByteBuffer bBuffer = ByteBuffer.wrap(data);
final LongBuffer lBuffer = bBuffer.asLongBuffer();
final int numLongs = lBuffer.capacity();
return lBuffer.get(0);
}
private static final long[] toLongArray(final byte[] data) {
final ByteBuffer bBuffer = ByteBuffer.wrap(data);
final LongBuffer lBuffer = bBuffer.asLongBuffer();
final int numLongs = lBuffer.capacity();
final long[] retVal = new long[numLongs];
for (int i = 0; i < numLongs; ++i) {
retVal[i] = lBuffer.get(i);
}
return retVal;
}
private static final byte[] longToByteArray(final long l) {
final byte[] bArray = new byte[8];
final ByteBuffer bBuffer = ByteBuffer.wrap(bArray);
final LongBuffer lBuffer = bBuffer.asLongBuffer();
lBuffer.put(0, l);
return bArray;
}
private final static void refresh(final Context context) {
// First we build the album cache.
// This is the meta-data about the albums / buckets on the SD card.
Log.i(TAG, "Refreshing cache.");
int priority = Process.getThreadPriority(Process.myTid());
Process.setThreadPriority(Process.THREAD_PRIORITY_MORE_FAVORABLE);
sAlbumCache.deleteAll();
putLocaleForAlbumCache(Locale.getDefault());
final ArrayList<MediaSet> sets = new ArrayList<MediaSet>();
LongSparseArray<MediaSet> acceleratedSets = new LongSparseArray<MediaSet>();
Log.i(TAG, "Building albums.");
final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI.buildUpon().appendQueryParameter("distinct", "true").build();
final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI.buildUpon().appendQueryParameter("distinct", "true").build();
final ContentResolver cr = context.getContentResolver();
final Cursor cursorImages = cr.query(uriImages, BUCKET_PROJECTION_IMAGES, null, null, DEFAULT_BUCKET_SORT_ORDER);
final Cursor cursorVideos = cr.query(uriVideos, BUCKET_PROJECTION_VIDEOS, null, null, DEFAULT_BUCKET_SORT_ORDER);
Cursor[] cursors = new Cursor[2];
cursors[0] = cursorImages;
cursors[1] = cursorVideos;
final SortCursor sortCursor = new SortCursor(cursors, Images.ImageColumns.BUCKET_DISPLAY_NAME, SortCursor.TYPE_STRING, true);
try {
if (sortCursor != null && sortCursor.moveToFirst()) {
sets.ensureCapacity(sortCursor.getCount());
acceleratedSets = new LongSparseArray<MediaSet>(sortCursor.getCount());
MediaSet cameraSet = new MediaSet();
cameraSet.mId = LocalDataSource.CAMERA_BUCKET_ID;
cameraSet.mName = context.getResources().getString(R.string.camera);
sets.add(cameraSet);
acceleratedSets.put(cameraSet.mId, cameraSet);
do {
if (Thread.interrupted()) {
return;
}
long setId = sortCursor.getLong(BUCKET_ID_INDEX);
MediaSet mediaSet = findSet(setId, acceleratedSets);
if (mediaSet == null) {
mediaSet = new MediaSet();
mediaSet.mId = setId;
mediaSet.mName = sortCursor.getString(BUCKET_NAME_INDEX);
sets.add(mediaSet);
acceleratedSets.put(setId, mediaSet);
}
mediaSet.mHasImages |= (sortCursor.getCurrentCursorIndex() == 0);
mediaSet.mHasVideos |= (sortCursor.getCurrentCursorIndex() == 1);
} while (sortCursor.moveToNext());
sortCursor.close();
}
} finally {
if (sortCursor != null)
sortCursor.close();
}
sAlbumCache.put(ALBUM_CACHE_INCOMPLETE_INDEX, sDummyData);
writeSetsToCache(sets);
Log.i(TAG, "Done building albums.");
// Now we must cache the items contained in every album / bucket.
populateMediaItemsForSets(context, sets, acceleratedSets, false);
sAlbumCache.delete(ALBUM_CACHE_INCOMPLETE_INDEX);
Process.setThreadPriority(priority);
// Complete any queued dirty requests
processQueuedDirty(context);
}
private final static void refreshDirtySets(final Context context) {
final byte[] existingData = sAlbumCache.get(ALBUM_CACHE_DIRTY_BUCKET_INDEX, 0);
if (existingData != null && existingData.length > 0) {
final long[] ids = toLongArray(existingData);
final int numIds = ids.length;
if (numIds > 0) {
final ArrayList<MediaSet> sets = new ArrayList<MediaSet>(numIds);
final LongSparseArray<MediaSet> acceleratedSets = new LongSparseArray<MediaSet>(numIds);
for (int i = 0; i < numIds; ++i) {
final MediaSet set = new MediaSet();
set.mId = ids[i];
sets.add(set);
acceleratedSets.put(set.mId, set);
}
Log.i(TAG, "Refreshing dirty albums");
populateMediaItemsForSets(context, sets, acceleratedSets, true);
}
}
processQueuedDirty(context);
sAlbumCache.delete(ALBUM_CACHE_DIRTY_BUCKET_INDEX);
}
private static final long[] computeDirtySets(final Context context) {
final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI.buildUpon().appendQueryParameter("distinct", "true").build();
final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI.buildUpon().appendQueryParameter("distinct", "true").build();
final ContentResolver cr = context.getContentResolver();
final Cursor cursorImages = cr.query(uriImages, SENSE_PROJECTION, null, null, null);
final Cursor cursorVideos = cr.query(uriVideos, SENSE_PROJECTION, null, null, null);
Cursor[] cursors = new Cursor[2];
cursors[0] = cursorImages;
cursors[1] = cursorVideos;
final MergeCursor cursor = new MergeCursor(cursors);
long[] retVal = null;
try {
if (cursor.moveToFirst()) {
retVal = new long[cursor.getCount()];
int ctr = 0;
boolean allDirty = false;
do {
long setId = cursor.getLong(0);
retVal[ctr++] = setId;
byte[] data = sMetaAlbumCache.get(setId, 0);
if (data == null) {
// We need to refresh everything.
markDirty(context);
allDirty = true;
}
if (!allDirty) {
long maxAdded = cursor.getLong(1);
long oldMaxAdded = toLong(data);
if (maxAdded > oldMaxAdded) {
markDirty(context, setId);
}
}
} while (cursor.moveToNext());
}
} finally {
cursor.close();
}
processQueuedDirty(context);
return retVal;
}
private static final void processQueuedDirty(final Context context) {
if (QUEUE_DIRTY_SENSE) {
QUEUE_DIRTY_SENSE = false;
QUEUE_DIRTY_ALL = false;
QUEUE_DIRTY_SET = false;
computeDirtySets(context);
} else if (QUEUE_DIRTY_ALL) {
QUEUE_DIRTY_ALL = false;
QUEUE_DIRTY_SET = false;
QUEUE_DIRTY_SENSE = false;
refresh(context);
} else if (QUEUE_DIRTY_SET) {
QUEUE_DIRTY_SET = false;
// We don't mark QUEUE_DIRTY_SENSE because a set outside the dirty
// sets might have gotten modified.
refreshDirtySets(context);
}
}
private final static void populateMediaItemsForSets(final Context context, final ArrayList<MediaSet> sets,
final LongSparseArray<MediaSet> acceleratedSets, boolean useWhere) {
if (sets == null || sets.size() == 0 || Thread.interrupted()) {
return;
}
Log.i(TAG, "Building items.");
final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI;
final ContentResolver cr = context.getContentResolver();
String whereClause = null;
if (useWhere) {
int numSets = sets.size();
StringBuffer whereString = new StringBuffer(Images.ImageColumns.BUCKET_ID + " in (");
for (int i = 0; i < numSets; ++i) {
whereString.append(sets.get(i).mId);
if (i != numSets - 1) {
whereString.append(",");
}
}
whereString.append(")");
whereClause = whereString.toString();
Log.i(TAG, "Updating dirty albums where " + whereClause);
}
final Cursor cursorImages = cr.query(uriImages, PROJECTION_IMAGES, whereClause, null, DEFAULT_IMAGE_SORT_ORDER);
final Cursor cursorVideos = cr.query(uriVideos, PROJECTION_VIDEOS, whereClause, null, DEFAULT_VIDEO_SORT_ORDER);
final Cursor[] cursors = new Cursor[2];
cursors[0] = cursorImages;
cursors[1] = cursorVideos;
final SortCursor sortCursor = new SortCursor(cursors, Images.ImageColumns.DATE_TAKEN, SortCursor.TYPE_NUMERIC, true);
if (Thread.interrupted()) {
return;
}
try {
if (sortCursor != null && sortCursor.moveToFirst()) {
final int count = sortCursor.getCount();
final int numSets = sets.size();
final int approximateCountPerSet = count / numSets;
for (int i = 0; i < numSets; ++i) {
final MediaSet set = sets.get(i);
set.getItems().clear();
set.setNumExpectedItems(approximateCountPerSet);
}
do {
if (Thread.interrupted()) {
return;
}
final MediaItem item = new MediaItem();
final boolean isVideo = (sortCursor.getCurrentCursorIndex() == 1);
if (isVideo) {
populateVideoItemFromCursor(item, cr, sortCursor, CacheService.BASE_CONTENT_STRING_VIDEOS);
} else {
populateMediaItemFromCursor(item, cr, sortCursor, CacheService.BASE_CONTENT_STRING_IMAGES);
}
final long setId = sortCursor.getLong(MEDIA_BUCKET_ID_INDEX);
final MediaSet set = findSet(setId, acceleratedSets);
if (set != null) {
set.getItems().add(item);
}
} while (sortCursor.moveToNext());
}
} finally {
if (sortCursor != null)
sortCursor.close();
}
if (sets.size() > 0) {
writeItemsToCache(sets);
Log.i(TAG, "Done building items.");
}
}
private static final void writeSetsToCache(final ArrayList<MediaSet> sets) {
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
final int numSets = sets.size();
final DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(bos, 256));
try {
dos.writeInt(numSets);
for (int i = 0; i < numSets; ++i) {
if (Thread.interrupted()) {
return;
}
final MediaSet set = sets.get(i);
dos.writeLong(set.mId);
Utils.writeUTF(dos, set.mName);
dos.writeBoolean(set.mHasImages);
dos.writeBoolean(set.mHasVideos);
}
dos.flush();
sAlbumCache.put(ALBUM_CACHE_METADATA_INDEX, bos.toByteArray());
dos.close();
if (numSets == 0) {
sAlbumCache.deleteAll();
putLocaleForAlbumCache(Locale.getDefault());
}
sAlbumCache.flush();
} catch (IOException e) {
Log.e(TAG, "Error writing albums to diskcache.");
sAlbumCache.deleteAll();
putLocaleForAlbumCache(Locale.getDefault());
}
}
private static final void writeItemsToCache(final ArrayList<MediaSet> sets) {
final int numSets = sets.size();
for (int i = 0; i < numSets; ++i) {
if (Thread.interrupted()) {
return;
}
writeItemsForASet(sets.get(i));
}
writeMetaAlbumCache(sets);
sAlbumCache.flush();
}
private static final void writeMetaAlbumCache(ArrayList<MediaSet> sets) {
final int numSets = sets.size();
for (int i = 0; i < numSets; ++i) {
final MediaSet set = sets.get(i);
byte[] data = longToByteArray(set.mMaxAddedTimestamp);
sMetaAlbumCache.put(set.mId, data);
}
sMetaAlbumCache.flush();
}
private static final void writeItemsForASet(final MediaSet set) {
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
final DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(bos, 256));
try {
final ArrayList<MediaItem> items = set.getItems();
final int numItems = items.size();
dos.writeInt(numItems);
dos.writeLong(set.mMinTimestamp);
dos.writeLong(set.mMaxTimestamp);
for (int i = 0; i < numItems; ++i) {
MediaItem item = items.get(i);
if (set.mId == LocalDataSource.CAMERA_BUCKET_ID || set.mId == LocalDataSource.DOWNLOAD_BUCKET_ID) {
// Reverse the display order for the camera bucket - want
// the latest first.
item = items.get(numItems - i - 1);
}
dos.writeLong(item.mId);
Utils.writeUTF(dos, item.mCaption);
Utils.writeUTF(dos, item.mMimeType);
dos.writeInt(item.getMediaType());
dos.writeDouble(item.mLatitude);
dos.writeDouble(item.mLongitude);
dos.writeLong(item.mDateTakenInMs);
dos.writeBoolean(item.mTriedRetrievingExifDateTaken);
dos.writeLong(item.mDateAddedInSec);
dos.writeLong(item.mDateModifiedInSec);
dos.writeInt(item.mDurationInSec);
dos.writeInt((int) item.mRotation);
Utils.writeUTF(dos, item.mFilePath);
}
dos.flush();
sAlbumCache.put(set.mId, bos.toByteArray());
dos.close();
} catch (IOException e) {
Log.e(TAG, "Error writing to diskcache for set " + set.mName);
sAlbumCache.deleteAll();
putLocaleForAlbumCache(Locale.getDefault());
}
}
private static final MediaSet findSet(final long id, final LongSparseArray<MediaSet> acceleratedTable) {
// This is the accelerated lookup table for the MediaSet based on set
// id.
return acceleratedTable.get(id);
}
}