| 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); |
| } |
| } |