Home | History | Annotate | Download | only in data
      1 /*
      2  * Copyright (C) 2013 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 package com.android.photos.data;
     17 
     18 import android.content.Context;
     19 import android.database.sqlite.SQLiteDatabase;
     20 import android.net.Uri;
     21 import android.os.Environment;
     22 import android.util.Log;
     23 
     24 import com.android.photos.data.MediaCacheDatabase.Action;
     25 import com.android.photos.data.MediaRetriever.MediaSize;
     26 
     27 import java.io.ByteArrayInputStream;
     28 import java.io.File;
     29 import java.io.FileInputStream;
     30 import java.io.FileNotFoundException;
     31 import java.io.InputStream;
     32 import java.util.ArrayList;
     33 import java.util.HashMap;
     34 import java.util.LinkedList;
     35 import java.util.List;
     36 import java.util.Map;
     37 import java.util.Queue;
     38 
     39 /**
     40  * MediaCache keeps a cache of images, videos, thumbnails and previews. Calls to
     41  * retrieve a specific media item are executed asynchronously. The caller has an
     42  * option to receive a notification for lower resolution images that happen to
     43  * be available prior to the one requested.
     44  * <p>
     45  * When an media item has been retrieved, the notification for it is called on a
     46  * separate notifier thread. This thread should not be held for a long time so
     47  * that other notifications may happen.
     48  * </p>
     49  * <p>
     50  * Media items are uniquely identified by their content URIs. Each
     51  * scheme/authority can offer its own MediaRetriever, running in its own thread.
     52  * </p>
     53  * <p>
     54  * The MediaCache is an LRU cache, but does not allow the thumbnail cache to
     55  * drop below a minimum size. This prevents browsing through original images to
     56  * wipe out the thumbnails.
     57  * </p>
     58  */
     59 public class MediaCache {
     60     static final String TAG = MediaCache.class.getSimpleName();
     61     /** Subdirectory containing the image cache. */
     62     static final String IMAGE_CACHE_SUBDIR = "image_cache";
     63     /** File name extension to use for cached images. */
     64     static final String IMAGE_EXTENSION = ".cache";
     65     /** File name extension to use for temporary cached images while retrieving. */
     66     static final String TEMP_IMAGE_EXTENSION = ".temp";
     67 
     68     public static interface ImageReady {
     69         void imageReady(InputStream bitmapInputStream);
     70     }
     71 
     72     public static interface OriginalReady {
     73         void originalReady(File originalFile);
     74     }
     75 
     76     /** A Thread for each MediaRetriever */
     77     private class ProcessQueue extends Thread {
     78         private Queue<ProcessingJob> mQueue;
     79 
     80         public ProcessQueue(Queue<ProcessingJob> queue) {
     81             mQueue = queue;
     82         }
     83 
     84         @Override
     85         public void run() {
     86             while (mRunning) {
     87                 ProcessingJob status;
     88                 synchronized (mQueue) {
     89                     while (mQueue.isEmpty()) {
     90                         try {
     91                             mQueue.wait();
     92                         } catch (InterruptedException e) {
     93                             if (!mRunning) {
     94                                 return;
     95                             }
     96                             Log.w(TAG, "Unexpected interruption", e);
     97                         }
     98                     }
     99                     status = mQueue.remove();
    100                 }
    101                 processTask(status);
    102             }
    103         }
    104     };
    105 
    106     private interface NotifyReady {
    107         void notifyReady();
    108 
    109         void setFile(File file) throws FileNotFoundException;
    110 
    111         boolean isPrefetch();
    112     }
    113 
    114     private static class NotifyOriginalReady implements NotifyReady {
    115         private final OriginalReady mCallback;
    116         private File mFile;
    117 
    118         public NotifyOriginalReady(OriginalReady callback) {
    119             mCallback = callback;
    120         }
    121 
    122         @Override
    123         public void notifyReady() {
    124             if (mCallback != null) {
    125                 mCallback.originalReady(mFile);
    126             }
    127         }
    128 
    129         @Override
    130         public void setFile(File file) {
    131             mFile = file;
    132         }
    133 
    134         @Override
    135         public boolean isPrefetch() {
    136             return mCallback == null;
    137         }
    138     }
    139 
    140     private static class NotifyImageReady implements NotifyReady {
    141         private final ImageReady mCallback;
    142         private InputStream mInputStream;
    143 
    144         public NotifyImageReady(ImageReady callback) {
    145             mCallback = callback;
    146         }
    147 
    148         @Override
    149         public void notifyReady() {
    150             if (mCallback != null) {
    151                 mCallback.imageReady(mInputStream);
    152             }
    153         }
    154 
    155         @Override
    156         public void setFile(File file) throws FileNotFoundException {
    157             mInputStream = new FileInputStream(file);
    158         }
    159 
    160         public void setBytes(byte[] bytes) {
    161             mInputStream = new ByteArrayInputStream(bytes);
    162         }
    163 
    164         @Override
    165         public boolean isPrefetch() {
    166             return mCallback == null;
    167         }
    168     }
    169 
    170     /** A media item to be retrieved and its notifications. */
    171     private static class ProcessingJob {
    172         public ProcessingJob(Uri uri, MediaSize size, NotifyReady complete,
    173                 NotifyImageReady lowResolution) {
    174             this.contentUri = uri;
    175             this.size = size;
    176             this.complete = complete;
    177             this.lowResolution = lowResolution;
    178         }
    179         public Uri contentUri;
    180         public MediaSize size;
    181         public NotifyImageReady lowResolution;
    182         public NotifyReady complete;
    183     }
    184 
    185     private boolean mRunning = true;
    186     private static MediaCache sInstance;
    187     private File mCacheDir;
    188     private Context mContext;
    189     private Queue<NotifyReady> mCallbacks = new LinkedList<NotifyReady>();
    190     private Map<String, MediaRetriever> mRetrievers = new HashMap<String, MediaRetriever>();
    191     private Map<String, List<ProcessingJob>> mTasks = new HashMap<String, List<ProcessingJob>>();
    192     private List<ProcessQueue> mProcessingThreads = new ArrayList<ProcessQueue>();
    193     private MediaCacheDatabase mDatabaseHelper;
    194     private long mTempImageNumber = 1;
    195     private Object mTempImageNumberLock = new Object();
    196 
    197     private long mMaxCacheSize = 40 * 1024 * 1024; // 40 MB
    198     private long mMinThumbCacheSize = 4 * 1024 * 1024; // 4 MB
    199     private long mCacheSize = -1;
    200     private long mThumbCacheSize = -1;
    201     private Object mCacheSizeLock = new Object();
    202 
    203     private Action mNotifyCachedLowResolution = new Action() {
    204         @Override
    205         public void execute(Uri uri, long id, MediaSize size, Object parameter) {
    206             ProcessingJob job = (ProcessingJob) parameter;
    207             File file = createCacheImagePath(id);
    208             addNotification(job.lowResolution, file);
    209         }
    210     };
    211 
    212     private Action mMoveTempToCache = new Action() {
    213         @Override
    214         public void execute(Uri uri, long id, MediaSize size, Object parameter) {
    215             File tempFile = (File) parameter;
    216             File cacheFile = createCacheImagePath(id);
    217             tempFile.renameTo(cacheFile);
    218         }
    219     };
    220 
    221     private Action mDeleteFile = new Action() {
    222         @Override
    223         public void execute(Uri uri, long id, MediaSize size, Object parameter) {
    224             File file = createCacheImagePath(id);
    225             file.delete();
    226             synchronized (mCacheSizeLock) {
    227                 if (mCacheSize != -1) {
    228                     long length = (Long) parameter;
    229                     mCacheSize -= length;
    230                     if (size == MediaSize.Thumbnail) {
    231                         mThumbCacheSize -= length;
    232                     }
    233                 }
    234             }
    235         }
    236     };
    237 
    238     /** The thread used to make ImageReady and OriginalReady callbacks. */
    239     private Thread mProcessNotifications = new Thread() {
    240         @Override
    241         public void run() {
    242             while (mRunning) {
    243                 NotifyReady notifyImage;
    244                 synchronized (mCallbacks) {
    245                     while (mCallbacks.isEmpty()) {
    246                         try {
    247                             mCallbacks.wait();
    248                         } catch (InterruptedException e) {
    249                             if (!mRunning) {
    250                                 return;
    251                             }
    252                             Log.w(TAG, "Unexpected Interruption, continuing");
    253                         }
    254                     }
    255                     notifyImage = mCallbacks.remove();
    256                 }
    257 
    258                 notifyImage.notifyReady();
    259             }
    260         }
    261     };
    262 
    263     public static synchronized void initialize(Context context) {
    264         if (sInstance == null) {
    265             sInstance = new MediaCache(context);
    266             MediaCacheUtils.initialize(context);
    267         }
    268     }
    269 
    270     public static MediaCache getInstance() {
    271         return sInstance;
    272     }
    273 
    274     public static synchronized void shutdown() {
    275         sInstance.mRunning = false;
    276         sInstance.mProcessNotifications.interrupt();
    277         for (ProcessQueue processingThread : sInstance.mProcessingThreads) {
    278             processingThread.interrupt();
    279         }
    280         sInstance = null;
    281     }
    282 
    283     private MediaCache(Context context) {
    284         mDatabaseHelper = new MediaCacheDatabase(context);
    285         mProcessNotifications.start();
    286         mContext = context;
    287     }
    288 
    289     // This is used for testing.
    290     public void setCacheDir(File cacheDir) {
    291         cacheDir.mkdirs();
    292         mCacheDir = cacheDir;
    293     }
    294 
    295     public File getCacheDir() {
    296         synchronized (mContext) {
    297             if (mCacheDir == null) {
    298                 String state = Environment.getExternalStorageState();
    299                 File baseDir;
    300                 if (Environment.MEDIA_MOUNTED.equals(state)) {
    301                     baseDir = mContext.getExternalCacheDir();
    302                 } else {
    303                     // Stored in internal cache
    304                     baseDir = mContext.getCacheDir();
    305                 }
    306                 mCacheDir = new File(baseDir, IMAGE_CACHE_SUBDIR);
    307                 mCacheDir.mkdirs();
    308             }
    309             return mCacheDir;
    310         }
    311     }
    312 
    313     /**
    314      * Invalidates all cached images related to a given contentUri. This call
    315      * doesn't complete until the images have been removed from the cache.
    316      */
    317     public void invalidate(Uri contentUri) {
    318         mDatabaseHelper.delete(contentUri, mDeleteFile);
    319     }
    320 
    321     public void clearCacheDir() {
    322         File[] cachedFiles = getCacheDir().listFiles();
    323         if (cachedFiles != null) {
    324             for (File cachedFile : cachedFiles) {
    325                 cachedFile.delete();
    326             }
    327         }
    328     }
    329 
    330     /**
    331      * Add a MediaRetriever for a Uri scheme and authority. This MediaRetriever
    332      * will be granted its own thread for retrieving images.
    333      */
    334     public void addRetriever(String scheme, String authority, MediaRetriever retriever) {
    335         String differentiator = getDifferentiator(scheme, authority);
    336         synchronized (mRetrievers) {
    337             mRetrievers.put(differentiator, retriever);
    338         }
    339         synchronized (mTasks) {
    340             LinkedList<ProcessingJob> queue = new LinkedList<ProcessingJob>();
    341             mTasks.put(differentiator, queue);
    342             new ProcessQueue(queue).start();
    343         }
    344     }
    345 
    346     /**
    347      * Retrieves a thumbnail. complete will be called when the thumbnail is
    348      * available. If lowResolution is not null and a lower resolution thumbnail
    349      * is available before the thumbnail, lowResolution will be called prior to
    350      * complete. All callbacks will be made on a thread other than the calling
    351      * thread.
    352      *
    353      * @param contentUri The URI for the full resolution image to search for.
    354      * @param complete Callback for when the image has been retrieved.
    355      * @param lowResolution If not null and a lower resolution image is
    356      *            available prior to retrieving the thumbnail, this will be
    357      *            called with the low resolution bitmap.
    358      */
    359     public void retrieveThumbnail(Uri contentUri, ImageReady complete, ImageReady lowResolution) {
    360         addTask(contentUri, complete, lowResolution, MediaSize.Thumbnail);
    361     }
    362 
    363     /**
    364      * Retrieves a preview. complete will be called when the preview is
    365      * available. If lowResolution is not null and a lower resolution preview is
    366      * available before the preview, lowResolution will be called prior to
    367      * complete. All callbacks will be made on a thread other than the calling
    368      * thread.
    369      *
    370      * @param contentUri The URI for the full resolution image to search for.
    371      * @param complete Callback for when the image has been retrieved.
    372      * @param lowResolution If not null and a lower resolution image is
    373      *            available prior to retrieving the preview, this will be called
    374      *            with the low resolution bitmap.
    375      */
    376     public void retrievePreview(Uri contentUri, ImageReady complete, ImageReady lowResolution) {
    377         addTask(contentUri, complete, lowResolution, MediaSize.Preview);
    378     }
    379 
    380     /**
    381      * Retrieves the original image or video. complete will be called when the
    382      * media is available on the local file system. If lowResolution is not null
    383      * and a lower resolution preview is available before the original,
    384      * lowResolution will be called prior to complete. All callbacks will be
    385      * made on a thread other than the calling thread.
    386      *
    387      * @param contentUri The URI for the full resolution image to search for.
    388      * @param complete Callback for when the image has been retrieved.
    389      * @param lowResolution If not null and a lower resolution image is
    390      *            available prior to retrieving the preview, this will be called
    391      *            with the low resolution bitmap.
    392      */
    393     public void retrieveOriginal(Uri contentUri, OriginalReady complete, ImageReady lowResolution) {
    394         File localFile = getLocalFile(contentUri);
    395         if (localFile != null) {
    396             addNotification(new NotifyOriginalReady(complete), localFile);
    397         } else {
    398             NotifyImageReady notifyLowResolution = (lowResolution == null) ? null
    399                     : new NotifyImageReady(lowResolution);
    400             addTask(contentUri, new NotifyOriginalReady(complete), notifyLowResolution,
    401                     MediaSize.Original);
    402         }
    403     }
    404 
    405     /**
    406      * Looks for an already cached media at a specific size.
    407      *
    408      * @param contentUri The original media item content URI
    409      * @param size The target size to search for in the cache
    410      * @return The cached file location or null if it is not cached.
    411      */
    412     public File getCachedFile(Uri contentUri, MediaSize size) {
    413         Long cachedId = mDatabaseHelper.getCached(contentUri, size);
    414         File file = null;
    415         if (cachedId != null) {
    416             file = createCacheImagePath(cachedId);
    417             if (!file.exists()) {
    418                 mDatabaseHelper.delete(contentUri, size, mDeleteFile);
    419                 file = null;
    420             }
    421         }
    422         return file;
    423     }
    424 
    425     /**
    426      * Inserts a media item into the cache.
    427      *
    428      * @param contentUri The original media item URI.
    429      * @param size The size of the media item to store in the cache.
    430      * @param tempFile The temporary file where the image is stored. This file
    431      *            will no longer exist after executing this method.
    432      * @return The new location, in the cache, of the media item or null if it
    433      *         wasn't possible to move into the cache.
    434      */
    435     public File insertIntoCache(Uri contentUri, MediaSize size, File tempFile) {
    436         long fileSize = tempFile.length();
    437         if (fileSize == 0) {
    438             return null;
    439         }
    440         File cacheFile = null;
    441         SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
    442         // Ensure that this step is atomic
    443         db.beginTransaction();
    444         try {
    445             Long id = mDatabaseHelper.getCached(contentUri, size);
    446             if (id != null) {
    447                 cacheFile = createCacheImagePath(id);
    448                 if (tempFile.renameTo(cacheFile)) {
    449                     mDatabaseHelper.updateLength(id, fileSize);
    450                 } else {
    451                     Log.w(TAG, "Could not update cached file with " + tempFile);
    452                     tempFile.delete();
    453                     cacheFile = null;
    454                 }
    455             } else {
    456                 ensureFreeCacheSpace(tempFile.length(), size);
    457                 id = mDatabaseHelper.insert(contentUri, size, mMoveTempToCache, tempFile);
    458                 cacheFile = createCacheImagePath(id);
    459             }
    460             db.setTransactionSuccessful();
    461         } finally {
    462             db.endTransaction();
    463         }
    464         return cacheFile;
    465     }
    466 
    467     /**
    468      * For testing purposes.
    469      */
    470     public void setMaxCacheSize(long maxCacheSize) {
    471         synchronized (mCacheSizeLock) {
    472             mMaxCacheSize = maxCacheSize;
    473             mMinThumbCacheSize = mMaxCacheSize / 10;
    474             mCacheSize = -1;
    475             mThumbCacheSize = -1;
    476         }
    477     }
    478 
    479     private File createCacheImagePath(long id) {
    480         return new File(getCacheDir(), String.valueOf(id) + IMAGE_EXTENSION);
    481     }
    482 
    483     private void addTask(Uri contentUri, ImageReady complete, ImageReady lowResolution,
    484             MediaSize size) {
    485         NotifyReady notifyComplete = new NotifyImageReady(complete);
    486         NotifyImageReady notifyLowResolution = null;
    487         if (lowResolution != null) {
    488             notifyLowResolution = new NotifyImageReady(lowResolution);
    489         }
    490         addTask(contentUri, notifyComplete, notifyLowResolution, size);
    491     }
    492 
    493     private void addTask(Uri contentUri, NotifyReady complete, NotifyImageReady lowResolution,
    494             MediaSize size) {
    495         MediaRetriever retriever = getMediaRetriever(contentUri);
    496         Uri uri = retriever.normalizeUri(contentUri, size);
    497         if (uri == null) {
    498             throw new IllegalArgumentException("No MediaRetriever for " + contentUri);
    499         }
    500         size = retriever.normalizeMediaSize(uri, size);
    501 
    502         File cachedFile = getCachedFile(uri, size);
    503         if (cachedFile != null) {
    504             addNotification(complete, cachedFile);
    505             return;
    506         }
    507         String differentiator = getDifferentiator(uri.getScheme(), uri.getAuthority());
    508         synchronized (mTasks) {
    509             List<ProcessingJob> tasks = mTasks.get(differentiator);
    510             if (tasks == null) {
    511                 throw new IllegalArgumentException("Cannot find retriever for: " + uri);
    512             }
    513             synchronized (tasks) {
    514                 ProcessingJob job = new ProcessingJob(uri, size, complete, lowResolution);
    515                 if (complete.isPrefetch()) {
    516                     tasks.add(job);
    517                 } else {
    518                     int index = tasks.size() - 1;
    519                     while (index >= 0 && tasks.get(index).complete.isPrefetch()) {
    520                         index--;
    521                     }
    522                     tasks.add(index + 1, job);
    523                 }
    524                 tasks.notifyAll();
    525             }
    526         }
    527     }
    528 
    529     private MediaRetriever getMediaRetriever(Uri uri) {
    530         String differentiator = getDifferentiator(uri.getScheme(), uri.getAuthority());
    531         MediaRetriever retriever;
    532         synchronized (mRetrievers) {
    533             retriever = mRetrievers.get(differentiator);
    534         }
    535         if (retriever == null) {
    536             throw new IllegalArgumentException("No MediaRetriever for " + uri);
    537         }
    538         return retriever;
    539     }
    540 
    541     private File getLocalFile(Uri uri) {
    542         MediaRetriever retriever = getMediaRetriever(uri);
    543         File localFile = null;
    544         if (retriever != null) {
    545             localFile = retriever.getLocalFile(uri);
    546         }
    547         return localFile;
    548     }
    549 
    550     private MediaSize getFastImageSize(Uri uri, MediaSize size) {
    551         MediaRetriever retriever = getMediaRetriever(uri);
    552         return retriever.getFastImageSize(uri, size);
    553     }
    554 
    555     private boolean isFastImageBetter(MediaSize fastImageType, MediaSize size) {
    556         if (fastImageType == null) {
    557             return false;
    558         }
    559         if (size == null) {
    560             return true;
    561         }
    562         return fastImageType.isBetterThan(size);
    563     }
    564 
    565     private byte[] getTemporaryImage(Uri uri, MediaSize fastImageType) {
    566         MediaRetriever retriever = getMediaRetriever(uri);
    567         return retriever.getTemporaryImage(uri, fastImageType);
    568     }
    569 
    570     private void processTask(ProcessingJob job) {
    571         File cachedFile = getCachedFile(job.contentUri, job.size);
    572         if (cachedFile != null) {
    573             addNotification(job.complete, cachedFile);
    574             return;
    575         }
    576 
    577         boolean hasLowResolution = job.lowResolution != null;
    578         if (hasLowResolution) {
    579             MediaSize cachedSize = mDatabaseHelper.executeOnBestCached(job.contentUri, job.size,
    580                     mNotifyCachedLowResolution);
    581             MediaSize fastImageSize = getFastImageSize(job.contentUri, job.size);
    582             if (isFastImageBetter(fastImageSize, cachedSize)) {
    583                 if (fastImageSize.isTemporary()) {
    584                     byte[] bytes = getTemporaryImage(job.contentUri, fastImageSize);
    585                     if (bytes != null) {
    586                         addNotification(job.lowResolution, bytes);
    587                     }
    588                 } else {
    589                     File lowFile = getMedia(job.contentUri, fastImageSize);
    590                     if (lowFile != null) {
    591                         addNotification(job.lowResolution, lowFile);
    592                     }
    593                 }
    594             }
    595         }
    596 
    597         // Now get the full size desired
    598         File fullSizeFile = getMedia(job.contentUri, job.size);
    599         if (fullSizeFile != null) {
    600             addNotification(job.complete, fullSizeFile);
    601         }
    602     }
    603 
    604     private void addNotification(NotifyReady callback, File file) {
    605         try {
    606             callback.setFile(file);
    607             synchronized (mCallbacks) {
    608                 mCallbacks.add(callback);
    609                 mCallbacks.notifyAll();
    610             }
    611         } catch (FileNotFoundException e) {
    612             Log.e(TAG, "Unable to read file " + file, e);
    613         }
    614     }
    615 
    616     private void addNotification(NotifyImageReady callback, byte[] bytes) {
    617         callback.setBytes(bytes);
    618         synchronized (mCallbacks) {
    619             mCallbacks.add(callback);
    620             mCallbacks.notifyAll();
    621         }
    622     }
    623 
    624     private File getMedia(Uri uri, MediaSize size) {
    625         long imageNumber;
    626         synchronized (mTempImageNumberLock) {
    627             imageNumber = mTempImageNumber++;
    628         }
    629         File tempFile = new File(getCacheDir(), String.valueOf(imageNumber) + TEMP_IMAGE_EXTENSION);
    630         MediaRetriever retriever = getMediaRetriever(uri);
    631         boolean retrieved = retriever.getMedia(uri, size, tempFile);
    632         File cachedFile = null;
    633         if (retrieved) {
    634             ensureFreeCacheSpace(tempFile.length(), size);
    635             long id = mDatabaseHelper.insert(uri, size, mMoveTempToCache, tempFile);
    636             cachedFile = createCacheImagePath(id);
    637         }
    638         return cachedFile;
    639     }
    640 
    641     private static String getDifferentiator(String scheme, String authority) {
    642         if (authority == null) {
    643             return scheme;
    644         }
    645         StringBuilder differentiator = new StringBuilder(scheme);
    646         differentiator.append(':');
    647         differentiator.append(authority);
    648         return differentiator.toString();
    649     }
    650 
    651     private void ensureFreeCacheSpace(long size, MediaSize mediaSize) {
    652         synchronized (mCacheSizeLock) {
    653             if (mCacheSize == -1 || mThumbCacheSize == -1) {
    654                 mCacheSize = mDatabaseHelper.getCacheSize();
    655                 mThumbCacheSize = mDatabaseHelper.getThumbnailCacheSize();
    656                 if (mCacheSize == -1 || mThumbCacheSize == -1) {
    657                     Log.e(TAG, "Can't determine size of the image cache");
    658                     return;
    659                 }
    660             }
    661             mCacheSize += size;
    662             if (mediaSize == MediaSize.Thumbnail) {
    663                 mThumbCacheSize += size;
    664             }
    665             if (mCacheSize > mMaxCacheSize) {
    666                 shrinkCacheLocked();
    667             }
    668         }
    669     }
    670 
    671     private void shrinkCacheLocked() {
    672         long deleteSize = mMinThumbCacheSize;
    673         boolean includeThumbnails = (mThumbCacheSize - deleteSize) > mMinThumbCacheSize;
    674         mDatabaseHelper.deleteOldCached(includeThumbnails, deleteSize, mDeleteFile);
    675     }
    676 }
    677