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.
14 package org.lineageos.eleven.cache;
16 import android.annotation.SuppressLint;
17 import android.app.Activity;
18 import android.app.ActivityManager;
19 import android.app.Fragment;
20 import android.app.FragmentManager;
21 import android.content.ComponentCallbacks2;
22 import android.content.ContentUris;
23 import android.content.Context;
24 import android.content.res.Configuration;
25 import android.graphics.Bitmap;
26 import android.graphics.Bitmap.CompressFormat;
27 import android.graphics.BitmapFactory;
28 import android.net.Uri;
29 import android.os.AsyncTask;
30 import android.os.Bundle;
31 import android.os.Environment;
32 import android.os.Looper;
33 import android.os.ParcelFileDescriptor;
34 import android.util.Log;
36 import org.lineageos.eleven.cache.disklrucache.DiskLruCache;
37 import org.lineageos.eleven.utils.ElevenUtils;
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;
50 * This class holds the memory and disk bitmap caches.
52 public final class ImageCache {
54 private static final String TAG = ImageCache.class.getSimpleName();
57 * The {@link Uri} used to retrieve album art
59 private static final Uri mArtworkUri;
62 * Default memory cache size as a percent of device memory class
64 private static final float MEM_CACHE_DIVIDER = 0.25f;
67 * Default disk cache size 10MB
69 private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10;
72 * Compression settings when writing images to disk cache
74 private static final CompressFormat COMPRESS_FORMAT = CompressFormat.JPEG;
77 * Disk cache index to read from
79 private static final int DISK_CACHE_INDEX = 0;
82 * Image compression quality
84 private static final int COMPRESS_QUALITY = 98;
89 private MemoryCache mLruCache;
94 private DiskLruCache mDiskCache;
97 * listeners to the cache state
99 private HashSet<ICacheListener> mListeners = new HashSet<>();
101 private static ImageCache sInstance;
104 * Used to temporarily pause the disk cache while scrolling
106 public boolean mPauseDiskAccess = false;
107 private final Object mPauseLock = new Object();
110 mArtworkUri = Uri.parse("content://media/external/audio/albumart");
114 * Constructor of <code>ImageCache</code>
116 * @param context The {@link Context} to use
118 public ImageCache(final Context context) {
123 * Used to create a singleton of {@link ImageCache}
125 * @param context The {@link Context} to use
126 * @return A new instance of this class.
128 public final static ImageCache getInstance(final Context context) {
129 if (sInstance == null) {
130 sInstance = new ImageCache(context.getApplicationContext());
136 * Initialize the cache, providing all parameters.
138 * @param context The {@link Context} to use
139 * @param cacheParams The cache parameters to initialize the cache
141 private void init(final Context context) {
142 ElevenUtils.execute(false, new AsyncTask<Void, Void, Void>() {
145 protected Void doInBackground(final Void... unused) {
146 // Initialize the disk cahe in a background thread
147 initDiskCache(context);
151 // Set up the memory cache
152 initLruCache(context);
156 * Initializes the disk cache. Note that this includes disk access so this
157 * should not be executed on the main/UI thread. By default an ImageCache
158 * does not initialize the disk cache when it is created, instead you should
159 * call initDiskCache() to initialize it on a background thread.
161 * @param context The {@link Context} to use
163 private synchronized void initDiskCache(final Context context) {
165 if (mDiskCache == null || mDiskCache.isClosed()) {
166 File diskCacheDir = getDiskCacheDir(context, TAG);
167 if (diskCacheDir != null) {
168 if (!diskCacheDir.exists()) {
169 diskCacheDir.mkdirs();
171 if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
173 mDiskCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
174 } catch (final IOException e) {
183 * Sets up the Lru cache
185 * @param context The {@link Context} to use
187 @SuppressLint("NewApi")
188 public void initLruCache(final Context context) {
189 final ActivityManager activityManager = (ActivityManager)context
190 .getSystemService(Context.ACTIVITY_SERVICE);
191 final int lruCacheSize = Math.round(MEM_CACHE_DIVIDER * activityManager.getMemoryClass()
193 mLruCache = new MemoryCache(lruCacheSize);
195 // Release some memory as needed
196 context.registerComponentCallbacks(new ComponentCallbacks2() {
202 public void onTrimMemory(final int level) {
203 if (level >= TRIM_MEMORY_MODERATE) {
205 } else if (level >= TRIM_MEMORY_BACKGROUND) {
206 mLruCache.trimToSize(mLruCache.size() / 2);
214 public void onLowMemory() {
222 public void onConfigurationChanged(final Configuration newConfig) {
229 * Find and return an existing ImageCache stored in a {@link RetainFragment}
230 * , if not found a new one is created using the supplied params and saved
231 * to a {@link RetainFragment}
233 * @param activity The calling {@link FragmentActivity}
234 * @return An existing retained ImageCache object or a new one if one did
237 public static final ImageCache findOrCreateCache(final Activity activity) {
239 // Search for, or create an instance of the non-UI RetainFragment
240 final RetainFragment retainFragment = findOrCreateRetainFragment(
241 activity.getFragmentManager());
243 // See if we already have an ImageCache stored in RetainFragment
244 ImageCache cache = (ImageCache)retainFragment.getObject();
246 // No existing ImageCache, create one and store it in RetainFragment
248 cache = getInstance(activity);
249 retainFragment.setObject(cache);
255 * Locate an existing instance of this {@link Fragment} or if not found,
256 * create and add it using {@link FragmentManager}
258 * @param fm The {@link FragmentManager} to use
259 * @return The existing instance of the {@link Fragment} or the new instance
262 public static final RetainFragment findOrCreateRetainFragment(final FragmentManager fm) {
263 // Check to see if we have retained the worker fragment
264 RetainFragment retainFragment = (RetainFragment)fm.findFragmentByTag(TAG);
266 // If not retained, we need to create and add it
267 if (retainFragment == null) {
268 retainFragment = new RetainFragment();
269 fm.beginTransaction().add(retainFragment, TAG).commit();
271 return retainFragment;
275 * Adds a new image to the memory and disk caches
277 * @param data The key used to store the image
278 * @param bitmap The {@link Bitmap} to cache
280 public void addBitmapToCache(final String data, final Bitmap bitmap) {
281 addBitmapToCache(data, bitmap, false);
285 * Adds a new image to the memory and disk caches
287 * @param data The key used to store the image
288 * @param bitmap The {@link Bitmap} to cache
289 * @param replace force a replace even if the bitmap exists in the cache
291 public void addBitmapToCache(final String data, final Bitmap bitmap, final boolean replace) {
292 if (data == null || bitmap == null) {
296 // Add to memory cache
297 addBitmapToMemCache(data, bitmap, replace);
300 if (mDiskCache != null && !mDiskCache.isClosed()) {
301 final String key = hashKeyForDisk(data);
302 OutputStream out = null;
304 final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
305 if (snapshot != null) {
306 snapshot.getInputStream(DISK_CACHE_INDEX).close();
309 if (snapshot == null || replace) {
310 final DiskLruCache.Editor editor = mDiskCache.edit(key);
311 if (editor != null) {
312 out = editor.newOutputStream(DISK_CACHE_INDEX);
313 bitmap.compress(COMPRESS_FORMAT, COMPRESS_QUALITY, out);
319 } catch (final IOException e) {
320 Log.e(TAG, "addBitmapToCache - " + e);
321 } catch (final IllegalStateException e) {
322 // if the user clears the cache while we have an async task going we could try
323 // writing to the disk cache while it isn't ready. Catching here will silently
325 Log.e(TAG, "addBitmapToCache - " + e);
332 } catch (final IOException e) {
333 Log.e(TAG, "addBitmapToCache - " + e);
334 } catch (final IllegalStateException e) {
335 Log.e(TAG, "addBitmapToCache - " + e);
342 * Called to add a new image to the memory cache
344 * @param data The key identifier
345 * @param bitmap The {@link Bitmap} to cache
347 public void addBitmapToMemCache(final String data, final Bitmap bitmap) {
348 addBitmapToMemCache(data, bitmap, false);
352 * Called to add a new image to the memory cache
354 * @param data The key identifier
355 * @param bitmap The {@link Bitmap} to cache
356 * @param replace whether to force a replace if it already exists
358 public void addBitmapToMemCache(final String data, final Bitmap bitmap, final boolean replace) {
359 if (data == null || bitmap == null) {
362 // Add to memory cache
363 if (replace || getBitmapFromMemCache(data) == null) {
364 mLruCache.put(data, bitmap);
369 * Fetches a cached image from the memory cache
371 * @param data Unique identifier for which item to get
372 * @return The {@link Bitmap} if found in cache, null otherwise
374 public final Bitmap getBitmapFromMemCache(final String data) {
378 if (mLruCache != null) {
379 final Bitmap lruBitmap = mLruCache.get(data);
380 if (lruBitmap != null) {
388 * Fetches a cached image from the disk cache
390 * @param data Unique identifier for which item to get
391 * @return The {@link Bitmap} if found in cache, null otherwise
393 public final Bitmap getBitmapFromDiskCache(final String data) {
398 // Check in the memory cache here to avoid going to the disk cache less
400 if (getBitmapFromMemCache(data) != null) {
401 return getBitmapFromMemCache(data);
405 final String key = hashKeyForDisk(data);
406 if (mDiskCache != null) {
407 InputStream inputStream = null;
409 final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
410 if (snapshot != null) {
411 inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
412 if (inputStream != null) {
413 final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
414 if (bitmap != null) {
419 } catch (final IOException e) {
420 Log.e(TAG, "getBitmapFromDiskCache - " + e);
423 if (inputStream != null) {
426 } catch (final IOException e) {
434 * Tries to return a cached image from memory cache before fetching from the
437 * @param data Unique identifier for which item to get
438 * @return The {@link Bitmap} if found in cache, null otherwise
440 public final Bitmap getCachedBitmap(final String data) {
444 Bitmap cachedImage = getBitmapFromMemCache(data);
445 if (cachedImage == null) {
446 cachedImage = getBitmapFromDiskCache(data);
448 if (cachedImage != null) {
449 addBitmapToMemCache(data, cachedImage);
456 * Tries to return the album art from memory cache and disk cache, before
457 * calling {@code #getArtworkFromFile(Context, String)} again
459 * @param context The {@link Context} to use
460 * @param data The name of the album art
461 * @param id The ID of the album to find artwork for
462 * @return The artwork for an album
464 public final Bitmap getCachedArtwork(final Context context, final String data, final long id) {
465 if (context == null || data == null) {
468 Bitmap cachedImage = getCachedBitmap(data);
469 if (cachedImage == null && id >= 0) {
470 cachedImage = getArtworkFromFile(context, id);
472 if (cachedImage != null) {
473 addBitmapToMemCache(data, cachedImage);
480 * Used to fetch the artwork for an album locally from the user's device
482 * @param context The {@link Context} to use
483 * @param albumID The ID of the album to find artwork for
484 * @return The artwork for an album
486 public final Bitmap getArtworkFromFile(final Context context, final long albumId) {
490 Bitmap artwork = null;
493 final Uri uri = ContentUris.withAppendedId(mArtworkUri, albumId);
494 final ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver()
495 .openFileDescriptor(uri, "r");
496 if (parcelFileDescriptor != null) {
497 final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
498 artwork = BitmapFactory.decodeFileDescriptor(fileDescriptor);
500 } catch (final IllegalStateException e) {
501 // Log.e(TAG, "IllegalStateExcetpion - getArtworkFromFile - ", e);
502 } catch (final FileNotFoundException e) {
503 // Log.e(TAG, "FileNotFoundException - getArtworkFromFile - ", e);
504 } catch (final OutOfMemoryError evict) {
505 // Log.e(TAG, "OutOfMemoryError - getArtworkFromFile - ", evict);
512 * flush() is called to synchronize up other methods that are accessing the
515 public void flush() {
516 ElevenUtils.execute(false, new AsyncTask<Void, Void, Void>() {
519 protected Void doInBackground(final Void... unused) {
520 if (mDiskCache != null) {
522 if (!mDiskCache.isClosed()) {
525 } catch (final IOException e) {
526 Log.e(TAG, "flush - " + e);
535 * Clears the disk and memory caches
537 public void clearCaches() {
538 ElevenUtils.execute(false, new AsyncTask<Void, Void, Void>() {
541 protected Void doInBackground(final Void... unused) {
542 // Clear the disk cache
544 if (mDiskCache != null) {
548 } catch (final IOException e) {
549 Log.e(TAG, "clearCaches - " + e);
551 // Clear the memory cache
559 * Closes the disk cache associated with this ImageCache object. Note that
560 * this includes disk access so this should not be executed on the main/UI
563 public void close() {
564 ElevenUtils.execute(false, new AsyncTask<Void, Void, Void>() {
567 protected Void doInBackground(final Void... unused) {
568 if (mDiskCache != null) {
570 if (!mDiskCache.isClosed()) {
574 } catch (final IOException e) {
575 Log.e(TAG, "close - " + e);
584 * Evicts all of the items from the memory cache and lets the system know
585 * now would be a good time to garbage collect
587 public void evictAll() {
588 if (mLruCache != null) {
589 mLruCache.evictAll();
595 * @param key The key used to identify which cache entries to delete.
597 public void removeFromCache(final String key) {
601 // Remove the Lru entry
602 if (mLruCache != null) {
603 mLruCache.remove(key);
607 // Remove the disk entry
608 if (mDiskCache != null) {
609 mDiskCache.remove(hashKeyForDisk(key));
611 } catch (final IOException e) {
612 Log.e(TAG, "remove - " + e);
618 * Used to temporarily pause the disk cache while the user is scrolling to
621 * @param pause True to temporarily pause the disk cache, false otherwise.
623 public void setPauseDiskCache(final boolean pause) {
624 synchronized (mPauseLock) {
625 if (mPauseDiskAccess != pause) {
626 mPauseDiskAccess = pause;
630 for (ICacheListener listener : mListeners) {
631 listener.onCacheUnpaused();
638 private void waitUntilUnpaused() {
639 synchronized (mPauseLock) {
640 if (Looper.myLooper() != Looper.getMainLooper()) {
641 while (mPauseDiskAccess) {
644 } catch (InterruptedException e) {
645 // ignored, we'll start waiting again
653 * @return True if the user is scrolling, false otherwise.
655 public boolean isDiskCachePaused() {
656 return mPauseDiskAccess;
659 public void addCacheListener(ICacheListener listener) {
660 mListeners.add(listener);
663 public void removeCacheListener(ICacheListener listener) {
664 mListeners.remove(listener);
668 * Get a usable cache directory (external if available, internal otherwise)
670 * @param context The {@link Context} to use
671 * @param uniqueName A unique directory name to append to the cache
673 * @return The cache directory
675 public static final File getDiskCacheDir(final Context context, final String uniqueName) {
676 // getExternalCacheDir(context) returns null if external storage is not ready
677 final String cachePath = getExternalCacheDir(context) != null
678 ? getExternalCacheDir(context).getPath()
679 : context.getCacheDir().getPath();
680 return new File(cachePath, uniqueName);
684 * Check if external storage is built-in or removable
686 * @return True if external storage is removable (like an SD card), false
689 public static final boolean isExternalStorageRemovable() {
690 return Environment.isExternalStorageRemovable();
694 * Get the external app cache directory
696 * @param context The {@link Context} to use
697 * @return The external cache directory
699 public static final File getExternalCacheDir(final Context context) {
700 return context.getExternalCacheDir();
704 * Check how much usable space is available at a given path.
706 * @param path The path to check
707 * @return The space available in bytes
709 public static final long getUsableSpace(final File path) {
710 return path.getUsableSpace();
714 * A hashing method that changes a string (like a URL) into a hash suitable
715 * for using as a disk filename.
717 * @param key The key used to store the file
719 public static final String hashKeyForDisk(final String key) {
722 final MessageDigest digest = MessageDigest.getInstance("MD5");
723 digest.update(key.getBytes());
724 cacheKey = bytesToHexString(digest.digest());
725 } catch (final NoSuchAlgorithmException e) {
726 cacheKey = String.valueOf(key.hashCode());
732 * http://stackoverflow.com/questions/332079
734 * @param bytes The bytes to convert.
735 * @return A {@link String} converted from the bytes of a hashable key used
736 * to store a filename on the disk, to hex digits.
738 private static final String bytesToHexString(final byte[] bytes) {
739 final StringBuilder builder = new StringBuilder();
740 for (final byte b : bytes) {
741 final String hex = Integer.toHexString(0xFF & b);
742 if (hex.length() == 1) {
747 return builder.toString();
751 * A simple non-UI Fragment that stores a single Object and is retained over
752 * configuration changes. In this sample it will be used to retain an
753 * {@link ImageCache} object.
755 public static final class RetainFragment extends Fragment {
758 * The object to be stored
760 private Object mObject;
763 * Empty constructor as per the {@link Fragment} documentation
765 public RetainFragment() {
772 public void onCreate(final Bundle savedInstanceState) {
773 super.onCreate(savedInstanceState);
774 // Make sure this Fragment is retained over a configuration change
775 setRetainInstance(true);
779 * Store a single object in this {@link Fragment}
781 * @param object The object to store
783 public void setObject(final Object object) {
788 * Get the stored object
790 * @return The stored object
792 public Object getObject() {
798 * Used to cache images via {@link LruCache}.
800 public static final class MemoryCache extends LruCache<String, Bitmap> {
803 * Constructor of <code>MemoryCache</code>
805 * @param maxSize The allowed size of the {@link LruCache}
807 public MemoryCache(final int maxSize) {
812 * Get the size in bytes of a bitmap.
814 public static final int getBitmapSize(final Bitmap bitmap) {
815 return bitmap.getByteCount();
822 protected int sizeOf(final String paramString, final Bitmap paramBitmap) {
823 return getBitmapSize(paramBitmap);