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