• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
imageReady(InputStream bitmapInputStream)69         void imageReady(InputStream bitmapInputStream);
70     }
71 
72     public static interface OriginalReady {
originalReady(File originalFile)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 
ProcessQueue(Queue<ProcessingJob> queue)80         public ProcessQueue(Queue<ProcessingJob> queue) {
81             mQueue = queue;
82         }
83 
84         @Override
run()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 {
notifyReady()107         void notifyReady();
108 
setFile(File file)109         void setFile(File file) throws FileNotFoundException;
110 
isPrefetch()111         boolean isPrefetch();
112     }
113 
114     private static class NotifyOriginalReady implements NotifyReady {
115         private final OriginalReady mCallback;
116         private File mFile;
117 
NotifyOriginalReady(OriginalReady callback)118         public NotifyOriginalReady(OriginalReady callback) {
119             mCallback = callback;
120         }
121 
122         @Override
notifyReady()123         public void notifyReady() {
124             if (mCallback != null) {
125                 mCallback.originalReady(mFile);
126             }
127         }
128 
129         @Override
setFile(File file)130         public void setFile(File file) {
131             mFile = file;
132         }
133 
134         @Override
isPrefetch()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 
NotifyImageReady(ImageReady callback)144         public NotifyImageReady(ImageReady callback) {
145             mCallback = callback;
146         }
147 
148         @Override
notifyReady()149         public void notifyReady() {
150             if (mCallback != null) {
151                 mCallback.imageReady(mInputStream);
152             }
153         }
154 
155         @Override
setFile(File file)156         public void setFile(File file) throws FileNotFoundException {
157             mInputStream = new FileInputStream(file);
158         }
159 
setBytes(byte[] bytes)160         public void setBytes(byte[] bytes) {
161             mInputStream = new ByteArrayInputStream(bytes);
162         }
163 
164         @Override
isPrefetch()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 {
ProcessingJob(Uri uri, MediaSize size, NotifyReady complete, NotifyImageReady lowResolution)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 
initialize(Context context)263     public static synchronized void initialize(Context context) {
264         if (sInstance == null) {
265             sInstance = new MediaCache(context);
266             MediaCacheUtils.initialize(context);
267         }
268     }
269 
getInstance()270     public static MediaCache getInstance() {
271         return sInstance;
272     }
273 
shutdown()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 
MediaCache(Context context)283     private MediaCache(Context context) {
284         mDatabaseHelper = new MediaCacheDatabase(context);
285         mProcessNotifications.start();
286         mContext = context;
287     }
288 
289     // This is used for testing.
setCacheDir(File cacheDir)290     public void setCacheDir(File cacheDir) {
291         cacheDir.mkdirs();
292         mCacheDir = cacheDir;
293     }
294 
getCacheDir()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      */
invalidate(Uri contentUri)317     public void invalidate(Uri contentUri) {
318         mDatabaseHelper.delete(contentUri, mDeleteFile);
319     }
320 
clearCacheDir()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      */
addRetriever(String scheme, String authority, MediaRetriever retriever)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      */
retrieveThumbnail(Uri contentUri, ImageReady complete, ImageReady lowResolution)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      */
retrievePreview(Uri contentUri, ImageReady complete, ImageReady lowResolution)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      */
retrieveOriginal(Uri contentUri, OriginalReady complete, ImageReady lowResolution)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      */
getCachedFile(Uri contentUri, MediaSize size)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      */
insertIntoCache(Uri contentUri, MediaSize size, File tempFile)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      */
setMaxCacheSize(long maxCacheSize)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 
createCacheImagePath(long id)479     private File createCacheImagePath(long id) {
480         return new File(getCacheDir(), String.valueOf(id) + IMAGE_EXTENSION);
481     }
482 
addTask(Uri contentUri, ImageReady complete, ImageReady lowResolution, MediaSize size)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 
addTask(Uri contentUri, NotifyReady complete, NotifyImageReady lowResolution, MediaSize size)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 
getMediaRetriever(Uri uri)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 
getLocalFile(Uri uri)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 
getFastImageSize(Uri uri, MediaSize size)550     private MediaSize getFastImageSize(Uri uri, MediaSize size) {
551         MediaRetriever retriever = getMediaRetriever(uri);
552         return retriever.getFastImageSize(uri, size);
553     }
554 
isFastImageBetter(MediaSize fastImageType, MediaSize size)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 
getTemporaryImage(Uri uri, MediaSize fastImageType)565     private byte[] getTemporaryImage(Uri uri, MediaSize fastImageType) {
566         MediaRetriever retriever = getMediaRetriever(uri);
567         return retriever.getTemporaryImage(uri, fastImageType);
568     }
569 
processTask(ProcessingJob job)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 
addNotification(NotifyReady callback, File file)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 
addNotification(NotifyImageReady callback, byte[] bytes)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 
getMedia(Uri uri, MediaSize size)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 
getDifferentiator(String scheme, String authority)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 
ensureFreeCacheSpace(long size, MediaSize mediaSize)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 
shrinkCacheLocked()671     private void shrinkCacheLocked() {
672         long deleteSize = mMinThumbCacheSize;
673         boolean includeThumbnails = (mThumbCacheSize - deleteSize) > mMinThumbCacheSize;
674         mDatabaseHelper.deleteOldCached(includeThumbnails, deleteSize, mDeleteFile);
675     }
676 }
677