OSDN Git Service

ImageCache: add missing close call and cleanup
[android-x86/packages-apps-Eleven.git] / src / org / lineageos / eleven / cache / ImageCache.java
1 /*
2  * Copyright (C) 2012 Andrew Neal
3  * Copyright (C) 2014 The CyanogenMod Project
4  * Licensed under the Apache License, Version 2.0
5  * (the "License"); you may not use this file except in compliance with the
6  * License. You may obtain a copy of the License at
7  * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
8  * or agreed to in writing, software distributed under the License is
9  * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
10  * KIND, either express or implied. See the License for the specific language
11  * governing permissions and limitations under the License.
12  */
13
14 package org.lineageos.eleven.cache;
15
16 import android.app.Activity;
17 import android.app.ActivityManager;
18 import android.app.Fragment;
19 import android.app.FragmentManager;
20 import android.content.ComponentCallbacks2;
21 import android.content.ContentUris;
22 import android.content.Context;
23 import android.content.res.Configuration;
24 import android.graphics.Bitmap;
25 import android.graphics.Bitmap.CompressFormat;
26 import android.graphics.BitmapFactory;
27 import android.net.Uri;
28 import android.os.AsyncTask;
29 import android.os.Bundle;
30 import android.os.Environment;
31 import android.os.Looper;
32 import android.os.ParcelFileDescriptor;
33 import android.util.Log;
34
35 import org.lineageos.eleven.cache.disklrucache.DiskLruCache;
36 import org.lineageos.eleven.utils.ElevenUtils;
37 import org.lineageos.eleven.utils.IoUtils;
38
39 import java.io.File;
40 import java.io.FileDescriptor;
41 import java.io.FileNotFoundException;
42 import java.io.IOException;
43 import java.io.InputStream;
44 import java.io.OutputStream;
45 import java.security.MessageDigest;
46 import java.security.NoSuchAlgorithmException;
47 import java.util.HashSet;
48
49 /**
50  * This class holds the memory and disk bitmap caches.
51  */
52 public final class ImageCache {
53     private static final String TAG = ImageCache.class.getSimpleName();
54
55     /**
56      * The {@link Uri} used to retrieve album art
57      */
58     private static final Uri mArtworkUri;
59
60     /**
61      * Default memory cache size as a percent of device memory class
62      */
63     private static final float MEM_CACHE_DIVIDER = 0.25f;
64
65     /**
66      * Default disk cache size 10MB
67      */
68     private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10;
69
70     /**
71      * Compression settings when writing images to disk cache
72      */
73     private static final CompressFormat COMPRESS_FORMAT = CompressFormat.JPEG;
74
75     /**
76      * Disk cache index to read from
77      */
78     private static final int DISK_CACHE_INDEX = 0;
79
80     /**
81      * Image compression quality
82      */
83     private static final int COMPRESS_QUALITY = 98;
84
85     /**
86      * LRU cache
87      */
88     private MemoryCache mLruCache;
89
90     /**
91      * Disk LRU cache
92      */
93     private DiskLruCache mDiskCache;
94
95     /**
96      * listeners to the cache state
97      */
98     private HashSet<ICacheListener> mListeners = new HashSet<>();
99
100     private static ImageCache sInstance;
101
102     /**
103      * Used to temporarily pause the disk cache while scrolling
104      */
105     public boolean mPauseDiskAccess = false;
106     private final Object mPauseLock = new Object();
107
108     static {
109         mArtworkUri = Uri.parse("content://media/external/audio/albumart");
110     }
111
112     /**
113      * Constructor of <code>ImageCache</code>
114      *
115      * @param context The {@link Context} to use
116      */
117     public ImageCache(final Context context) {
118         init(context);
119     }
120
121     /**
122      * Used to create a singleton of {@link ImageCache}
123      *
124      * @param context The {@link Context} to use
125      * @return A new instance of this class.
126      */
127     public final static ImageCache getInstance(final Context context) {
128         if (sInstance == null) {
129             sInstance = new ImageCache(context.getApplicationContext());
130         }
131         return sInstance;
132     }
133
134     /**
135      * Initialize the cache, providing all parameters.
136      *
137      * @param context The {@link Context} to use
138      * @param cacheParams The cache parameters to initialize the cache
139      */
140     private void init(final Context context) {
141         ElevenUtils.execute(false, new AsyncTask<Void, Void, Void>() {
142
143             @Override
144             protected Void doInBackground(final Void... unused) {
145                 // Initialize the disk cahe in a background thread
146                 initDiskCache(context);
147                 return null;
148             }
149         }, (Void[])null);
150         // Set up the memory cache
151         initLruCache(context);
152     }
153
154     /**
155      * Initializes the disk cache. Note that this includes disk access so this
156      * should not be executed on the main/UI thread. By default an ImageCache
157      * does not initialize the disk cache when it is created, instead you should
158      * call initDiskCache() to initialize it on a background thread.
159      *
160      * @param context The {@link Context} to use
161      */
162     private synchronized void initDiskCache(final Context context) {
163         // Set up disk cache
164         if (mDiskCache == null || mDiskCache.isClosed()) {
165             File diskCacheDir = getDiskCacheDir(context, TAG);
166             if (diskCacheDir != null) {
167                 if (!diskCacheDir.exists()) {
168                     diskCacheDir.mkdirs();
169                 }
170                 if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
171                     try {
172                         mDiskCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
173                     } catch (final IOException e) {
174                         diskCacheDir = null;
175                     }
176                 }
177             }
178         }
179     }
180
181     /**
182      * Sets up the Lru cache
183      *
184      * @param context The {@link Context} to use
185      */
186     public void initLruCache(final Context context) {
187         final ActivityManager activityManager = (ActivityManager)context
188                 .getSystemService(Context.ACTIVITY_SERVICE);
189         final int lruCacheSize = Math.round(MEM_CACHE_DIVIDER * activityManager.getMemoryClass()
190                 * 1024 * 1024);
191         mLruCache = new MemoryCache(lruCacheSize);
192
193         // Release some memory as needed
194         context.registerComponentCallbacks(new ComponentCallbacks2() {
195
196             @Override
197             public void onTrimMemory(final int level) {
198                 if (level >= TRIM_MEMORY_MODERATE) {
199                     evictAll();
200                 } else if (level >= TRIM_MEMORY_BACKGROUND) {
201                     mLruCache.trimToSize(mLruCache.size() / 2);
202                 }
203             }
204
205             @Override
206             public void onLowMemory() {
207                 // Nothing to do
208             }
209
210             @Override
211             public void onConfigurationChanged(final Configuration newConfig) {
212                 // Nothing to do
213             }
214         });
215     }
216
217     /**
218      * Find and return an existing ImageCache stored in a {@link RetainFragment}
219      * , if not found a new one is created using the supplied params and saved
220      * to a {@link RetainFragment}
221      *
222      * @param activity The calling {@link FragmentActivity}
223      * @return An existing retained ImageCache object or a new one if one did
224      *         not exist
225      */
226     public static ImageCache findOrCreateCache(final Activity activity) {
227
228         // Search for, or create an instance of the non-UI RetainFragment
229         final RetainFragment retainFragment = findOrCreateRetainFragment(
230                 activity.getFragmentManager());
231
232         // See if we already have an ImageCache stored in RetainFragment
233         ImageCache cache = (ImageCache)retainFragment.getObject();
234
235         // No existing ImageCache, create one and store it in RetainFragment
236         if (cache == null) {
237             cache = getInstance(activity);
238             retainFragment.setObject(cache);
239         }
240         return cache;
241     }
242
243     /**
244      * Locate an existing instance of this {@link Fragment} or if not found,
245      * create and add it using {@link FragmentManager}
246      *
247      * @param fm The {@link FragmentManager} to use
248      * @return The existing instance of the {@link Fragment} or the new instance
249      *         if just created
250      */
251     public static RetainFragment findOrCreateRetainFragment(final FragmentManager fm) {
252         // Check to see if we have retained the worker fragment
253         RetainFragment retainFragment = (RetainFragment)fm.findFragmentByTag(TAG);
254
255         // If not retained, we need to create and add it
256         if (retainFragment == null) {
257             retainFragment = new RetainFragment();
258             fm.beginTransaction().add(retainFragment, TAG).commit();
259         }
260         return retainFragment;
261     }
262
263     /**
264      * Adds a new image to the memory and disk caches
265      *
266      * @param data The key used to store the image
267      * @param bitmap The {@link Bitmap} to cache
268      */
269     public void addBitmapToCache(final String data, final Bitmap bitmap) {
270         addBitmapToCache(data, bitmap, false);
271     }
272
273     /**
274      * Adds a new image to the memory and disk caches
275      *
276      * @param data The key used to store the image
277      * @param bitmap The {@link Bitmap} to cache
278      * @param replace force a replace even if the bitmap exists in the cache
279      */
280     public void addBitmapToCache(final String data, final Bitmap bitmap, final boolean replace) {
281         if (data == null || bitmap == null) {
282             return;
283         }
284
285         // Add to memory cache
286         addBitmapToMemCache(data, bitmap, replace);
287
288         // Add to disk cache
289         if (mDiskCache != null && !mDiskCache.isClosed()) {
290             final String key = hashKeyForDisk(data);
291             OutputStream out = null;
292             try {
293                 final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
294                 if (snapshot != null) {
295                     snapshot.getInputStream(DISK_CACHE_INDEX).close();
296                 }
297
298                 if (snapshot == null || replace) {
299                     final DiskLruCache.Editor editor = mDiskCache.edit(key);
300                     if (editor != null) {
301                         out = editor.newOutputStream(DISK_CACHE_INDEX);
302                         bitmap.compress(COMPRESS_FORMAT, COMPRESS_QUALITY, out);
303                         editor.commit();
304                         out.close();
305                         flush();
306                     }
307                 }
308             } catch (final IOException e) {
309                 Log.e(TAG, "addBitmapToCache", e);
310             } catch (final IllegalStateException e) {
311                 // if the user clears the cache while we have an async task going we could try
312                 // writing to the disk cache while it isn't ready. Catching here will silently
313                 // fail instead
314                 Log.e(TAG, "addBitmapToCache", e);
315             } finally {
316                 IoUtils.closeQuietly(out);
317             }
318         }
319     }
320
321     /**
322      * Called to add a new image to the memory cache
323      *
324      * @param data The key identifier
325      * @param bitmap The {@link Bitmap} to cache
326      */
327     public void addBitmapToMemCache(final String data, final Bitmap bitmap) {
328         addBitmapToMemCache(data, bitmap, false);
329     }
330
331     /**
332      * Called to add a new image to the memory cache
333      *
334      * @param data The key identifier
335      * @param bitmap The {@link Bitmap} to cache
336      * @param replace whether to force a replace if it already exists
337      */
338     public void addBitmapToMemCache(final String data, final Bitmap bitmap, final boolean replace) {
339         if (data == null || bitmap == null) {
340             return;
341         }
342         // Add to memory cache
343         if (replace || getBitmapFromMemCache(data) == null) {
344             mLruCache.put(data, bitmap);
345         }
346     }
347
348     /**
349      * Fetches a cached image from the memory cache
350      *
351      * @param data Unique identifier for which item to get
352      * @return The {@link Bitmap} if found in cache, null otherwise
353      */
354     public final Bitmap getBitmapFromMemCache(final String data) {
355         if (data == null) {
356             return null;
357         }
358         if (mLruCache != null) {
359             final Bitmap lruBitmap = mLruCache.get(data);
360             if (lruBitmap != null) {
361                 return lruBitmap;
362             }
363         }
364         return null;
365     }
366
367     /**
368      * Fetches a cached image from the disk cache
369      *
370      * @param data Unique identifier for which item to get
371      * @return The {@link Bitmap} if found in cache, null otherwise
372      */
373     public final Bitmap getBitmapFromDiskCache(final String data) {
374         if (data == null) {
375             return null;
376         }
377
378         // Check in the memory cache here to avoid going to the disk cache less
379         // often
380         if (getBitmapFromMemCache(data) != null) {
381             return getBitmapFromMemCache(data);
382         }
383
384         waitUntilUnpaused();
385         final String key = hashKeyForDisk(data);
386         if (mDiskCache != null) {
387             InputStream inputStream = null;
388             try {
389                 final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
390                 if (snapshot != null) {
391                     inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
392                     if (inputStream != null) {
393                         final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
394                         if (bitmap != null) {
395                             return bitmap;
396                         }
397                     }
398                 }
399             } catch (final IOException e) {
400                 Log.e(TAG, "getBitmapFromDiskCache", e);
401             } finally {
402                 IoUtils.closeQuietly(inputStream);
403             }
404         }
405         return null;
406     }
407
408     /**
409      * Tries to return a cached image from memory cache before fetching from the
410      * disk cache
411      *
412      * @param data Unique identifier for which item to get
413      * @return The {@link Bitmap} if found in cache, null otherwise
414      */
415     public final Bitmap getCachedBitmap(final String data) {
416         if (data == null) {
417             return null;
418         }
419         Bitmap cachedImage = getBitmapFromMemCache(data);
420         if (cachedImage == null) {
421             cachedImage = getBitmapFromDiskCache(data);
422         }
423         if (cachedImage != null) {
424             addBitmapToMemCache(data, cachedImage);
425             return cachedImage;
426         }
427         return null;
428     }
429
430     /**
431      * Tries to return the album art from memory cache and disk cache, before
432      * calling {@code #getArtworkFromFile(Context, String)} again
433      *
434      * @param context The {@link Context} to use
435      * @param data The name of the album art
436      * @param id The ID of the album to find artwork for
437      * @return The artwork for an album
438      */
439     public final Bitmap getCachedArtwork(final Context context, final String data, final long id) {
440         if (context == null || data == null) {
441             return null;
442         }
443         Bitmap cachedImage = getCachedBitmap(data);
444         if (cachedImage == null && id >= 0) {
445             cachedImage = getArtworkFromFile(context, id);
446         }
447         if (cachedImage != null) {
448             addBitmapToMemCache(data, cachedImage);
449             return cachedImage;
450         }
451         return null;
452     }
453
454     /**
455      * Used to fetch the artwork for an album locally from the user's device
456      *
457      * @param context The {@link Context} to use
458      * @param albumID The ID of the album to find artwork for
459      * @return The artwork for an album
460      */
461     public final Bitmap getArtworkFromFile(final Context context, final long albumId) {
462         if (albumId < 0) {
463             return null;
464         }
465         Bitmap artwork = null;
466         waitUntilUnpaused();
467
468         ParcelFileDescriptor parcelFileDescriptor = null;
469         try {
470             final Uri uri = ContentUris.withAppendedId(mArtworkUri, albumId);
471             parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r");
472             if (parcelFileDescriptor != null) {
473                 final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
474                 artwork = BitmapFactory.decodeFileDescriptor(fileDescriptor);
475             }
476         } catch (final IllegalStateException e) {
477             // Log.e(TAG, "IllegalStateExcetpion - getArtworkFromFile - ", e);
478         } catch (final FileNotFoundException e) {
479             // Log.e(TAG, "FileNotFoundException - getArtworkFromFile - ", e);
480         } catch (final OutOfMemoryError evict) {
481             // Log.e(TAG, "OutOfMemoryError - getArtworkFromFile - ", evict);
482             evictAll();
483         } finally {
484             IoUtils.closeQuietly(parcelFileDescriptor);
485         }
486         return artwork;
487     }
488
489     /**
490      * flush() is called to synchronize up other methods that are accessing the
491      * cache first
492      */
493     public void flush() {
494         ElevenUtils.execute(false, new AsyncTask<Void, Void, Void>() {
495
496             @Override
497             protected Void doInBackground(final Void... unused) {
498                 if (mDiskCache != null) {
499                     try {
500                         if (!mDiskCache.isClosed()) {
501                             mDiskCache.flush();
502                         }
503                     } catch (final IOException e) {
504                         Log.e(TAG, "flush", e);
505                     }
506                 }
507                 return null;
508             }
509         });
510     }
511
512     /**
513      * Clears the disk and memory caches
514      */
515     public void clearCaches() {
516         ElevenUtils.execute(false, new AsyncTask<Void, Void, Void>() {
517
518             @Override
519             protected Void doInBackground(final Void... unused) {
520                 // Clear the disk cache
521                 try {
522                     if (mDiskCache != null) {
523                         mDiskCache.delete();
524                         mDiskCache = null;
525                     }
526                 } catch (final IOException e) {
527                     Log.e(TAG, "clearCaches", e);
528                 }
529                 // Clear the memory cache
530                 evictAll();
531                 return null;
532             }
533         });
534     }
535
536     /**
537      * Closes the disk cache associated with this ImageCache object. Note that
538      * this includes disk access so this should not be executed on the main/UI
539      * thread.
540      */
541     public void close() {
542         ElevenUtils.execute(false, new AsyncTask<Void, Void, Void>() {
543
544             @Override
545             protected Void doInBackground(final Void... unused) {
546                 if (mDiskCache != null) {
547                     try {
548                         if (!mDiskCache.isClosed()) {
549                             mDiskCache.close();
550                             mDiskCache = null;
551                         }
552                     } catch (final IOException e) {
553                         Log.e(TAG, "close", e);
554                     }
555                 }
556                 return null;
557             }
558         });
559     }
560
561     /**
562      * Evicts all of the items from the memory cache and lets the system know
563      * now would be a good time to garbage collect
564      */
565     public void evictAll() {
566         if (mLruCache != null) {
567             mLruCache.evictAll();
568         }
569         System.gc();
570     }
571
572     /**
573      * @param key The key used to identify which cache entries to delete.
574      */
575     public void removeFromCache(final String key) {
576         if (key == null) {
577             return;
578         }
579         // Remove the Lru entry
580         if (mLruCache != null) {
581             mLruCache.remove(key);
582         }
583
584         try {
585             // Remove the disk entry
586             if (mDiskCache != null) {
587                 mDiskCache.remove(hashKeyForDisk(key));
588             }
589         } catch (final IOException e) {
590             Log.e(TAG, "removeFromCache(" + key + ")", e);
591         }
592         flush();
593     }
594
595     /**
596      * Used to temporarily pause the disk cache while the user is scrolling to
597      * improve scrolling.
598      *
599      * @param pause True to temporarily pause the disk cache, false otherwise.
600      */
601     public void setPauseDiskCache(final boolean pause) {
602         synchronized (mPauseLock) {
603             if (mPauseDiskAccess != pause) {
604                 mPauseDiskAccess = pause;
605                 if (!pause) {
606                     mPauseLock.notify();
607
608                     for (ICacheListener listener : mListeners) {
609                         listener.onCacheUnpaused();
610                     }
611                 }
612             }
613         }
614     }
615
616     private void waitUntilUnpaused() {
617         synchronized (mPauseLock) {
618             if (Looper.myLooper() != Looper.getMainLooper()) {
619                 while (mPauseDiskAccess) {
620                     try {
621                         mPauseLock.wait();
622                     } catch (InterruptedException e) {
623                         // ignored, we'll start waiting again
624                     }
625                 }
626             }
627         }
628     }
629
630     /**
631      * @return True if the user is scrolling, false otherwise.
632      */
633     public boolean isDiskCachePaused() {
634         return mPauseDiskAccess;
635     }
636
637     public void addCacheListener(ICacheListener listener) {
638         mListeners.add(listener);
639     }
640
641     public void removeCacheListener(ICacheListener listener) {
642         mListeners.remove(listener);
643     }
644
645     /**
646      * Get a usable cache directory (external if available, internal otherwise)
647      *
648      * @param context The {@link Context} to use
649      * @param uniqueName A unique directory name to append to the cache
650      *            directory
651      * @return The cache directory
652      */
653     public static File getDiskCacheDir(final Context context, final String uniqueName) {
654         // getExternalCacheDir(context) returns null if external storage is not ready
655         final String cachePath = getExternalCacheDir(context) != null
656                                     ? getExternalCacheDir(context).getPath()
657                                     : context.getCacheDir().getPath();
658         return new File(cachePath, uniqueName);
659     }
660
661     /**
662      * Check if external storage is built-in or removable
663      *
664      * @return True if external storage is removable (like an SD card), false
665      *         otherwise
666      */
667     public static boolean isExternalStorageRemovable() {
668         return Environment.isExternalStorageRemovable();
669     }
670
671     /**
672      * Get the external app cache directory
673      *
674      * @param context The {@link Context} to use
675      * @return The external cache directory
676      */
677     public static File getExternalCacheDir(final Context context) {
678         return context.getExternalCacheDir();
679     }
680
681     /**
682      * Check how much usable space is available at a given path.
683      *
684      * @param path The path to check
685      * @return The space available in bytes
686      */
687     public static long getUsableSpace(final File path) {
688         return path.getUsableSpace();
689     }
690
691     /**
692      * A hashing method that changes a string (like a URL) into a hash suitable
693      * for using as a disk filename.
694      *
695      * @param key The key used to store the file
696      */
697     public static String hashKeyForDisk(final String key) {
698         String cacheKey;
699         try {
700             final MessageDigest digest = MessageDigest.getInstance("MD5");
701             digest.update(key.getBytes());
702             cacheKey = bytesToHexString(digest.digest());
703         } catch (final NoSuchAlgorithmException e) {
704             cacheKey = String.valueOf(key.hashCode());
705         }
706         return cacheKey;
707     }
708
709     /**
710      * http://stackoverflow.com/questions/332079
711      *
712      * @param bytes The bytes to convert.
713      * @return A {@link String} converted from the bytes of a hashable key used
714      *         to store a filename on the disk, to hex digits.
715      */
716     private static String bytesToHexString(final byte[] bytes) {
717         final StringBuilder builder = new StringBuilder();
718         for (final byte b : bytes) {
719             final String hex = Integer.toHexString(0xFF & b);
720             if (hex.length() == 1) {
721                 builder.append('0');
722             }
723             builder.append(hex);
724         }
725         return builder.toString();
726     }
727
728     /**
729      * A simple non-UI Fragment that stores a single Object and is retained over
730      * configuration changes. In this sample it will be used to retain an
731      * {@link ImageCache} object.
732      */
733     public static final class RetainFragment extends Fragment {
734
735         /**
736          * The object to be stored
737          */
738         private Object mObject;
739
740         /**
741          * Empty constructor as per the {@link Fragment} documentation
742          */
743         public RetainFragment() {
744         }
745
746         @Override
747         public void onCreate(final Bundle savedInstanceState) {
748             super.onCreate(savedInstanceState);
749             // Make sure this Fragment is retained over a configuration change
750             setRetainInstance(true);
751         }
752
753         /**
754          * Store a single object in this {@link Fragment}
755          *
756          * @param object The object to store
757          */
758         public void setObject(final Object object) {
759             mObject = object;
760         }
761
762         /**
763          * Get the stored object
764          *
765          * @return The stored object
766          */
767         public Object getObject() {
768             return mObject;
769         }
770     }
771
772     /**
773      * Used to cache images via {@link LruCache}.
774      */
775     public static final class MemoryCache extends LruCache<String, Bitmap> {
776
777         /**
778          * Constructor of <code>MemoryCache</code>
779          *
780          * @param maxSize The allowed size of the {@link LruCache}
781          */
782         public MemoryCache(final int maxSize) {
783             super(maxSize);
784         }
785
786         /**
787          * Get the size in bytes of a bitmap.
788          */
789         public static int getBitmapSize(final Bitmap bitmap) {
790             return bitmap.getByteCount();
791         }
792
793         @Override
794         protected int sizeOf(final String paramString, final Bitmap paramBitmap) {
795             return getBitmapSize(paramBitmap);
796         }
797     }
798 }