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.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;
35 import org.lineageos.eleven.cache.disklrucache.DiskLruCache;
36 import org.lineageos.eleven.utils.ElevenUtils;
37 import org.lineageos.eleven.utils.IoUtils;
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 {
53 private static final String TAG = ImageCache.class.getSimpleName();
56 * The {@link Uri} used to retrieve album art
58 private static final Uri mArtworkUri;
61 * Default memory cache size as a percent of device memory class
63 private static final float MEM_CACHE_DIVIDER = 0.25f;
66 * Default disk cache size 10MB
68 private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10;
71 * Compression settings when writing images to disk cache
73 private static final CompressFormat COMPRESS_FORMAT = CompressFormat.JPEG;
76 * Disk cache index to read from
78 private static final int DISK_CACHE_INDEX = 0;
81 * Image compression quality
83 private static final int COMPRESS_QUALITY = 98;
88 private MemoryCache mLruCache;
93 private DiskLruCache mDiskCache;
96 * listeners to the cache state
98 private HashSet<ICacheListener> mListeners = new HashSet<>();
100 private static ImageCache sInstance;
103 * Used to temporarily pause the disk cache while scrolling
105 public boolean mPauseDiskAccess = false;
106 private final Object mPauseLock = new Object();
109 mArtworkUri = Uri.parse("content://media/external/audio/albumart");
113 * Constructor of <code>ImageCache</code>
115 * @param context The {@link Context} to use
117 public ImageCache(final Context context) {
122 * Used to create a singleton of {@link ImageCache}
124 * @param context The {@link Context} to use
125 * @return A new instance of this class.
127 public final static ImageCache getInstance(final Context context) {
128 if (sInstance == null) {
129 sInstance = new ImageCache(context.getApplicationContext());
135 * Initialize the cache, providing all parameters.
137 * @param context The {@link Context} to use
138 * @param cacheParams The cache parameters to initialize the cache
140 private void init(final Context context) {
141 ElevenUtils.execute(false, new AsyncTask<Void, Void, Void>() {
144 protected Void doInBackground(final Void... unused) {
145 // Initialize the disk cahe in a background thread
146 initDiskCache(context);
150 // Set up the memory cache
151 initLruCache(context);
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.
160 * @param context The {@link Context} to use
162 private synchronized void initDiskCache(final Context context) {
164 if (mDiskCache == null || mDiskCache.isClosed()) {
165 File diskCacheDir = getDiskCacheDir(context, TAG);
166 if (diskCacheDir != null) {
167 if (!diskCacheDir.exists()) {
168 diskCacheDir.mkdirs();
170 if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
172 mDiskCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
173 } catch (final IOException e) {
182 * Sets up the Lru cache
184 * @param context The {@link Context} to use
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()
191 mLruCache = new MemoryCache(lruCacheSize);
193 // Release some memory as needed
194 context.registerComponentCallbacks(new ComponentCallbacks2() {
197 public void onTrimMemory(final int level) {
198 if (level >= TRIM_MEMORY_MODERATE) {
200 } else if (level >= TRIM_MEMORY_BACKGROUND) {
201 mLruCache.trimToSize(mLruCache.size() / 2);
206 public void onLowMemory() {
211 public void onConfigurationChanged(final Configuration newConfig) {
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}
222 * @param activity The calling {@link FragmentActivity}
223 * @return An existing retained ImageCache object or a new one if one did
226 public static ImageCache findOrCreateCache(final Activity activity) {
228 // Search for, or create an instance of the non-UI RetainFragment
229 final RetainFragment retainFragment = findOrCreateRetainFragment(
230 activity.getFragmentManager());
232 // See if we already have an ImageCache stored in RetainFragment
233 ImageCache cache = (ImageCache)retainFragment.getObject();
235 // No existing ImageCache, create one and store it in RetainFragment
237 cache = getInstance(activity);
238 retainFragment.setObject(cache);
244 * Locate an existing instance of this {@link Fragment} or if not found,
245 * create and add it using {@link FragmentManager}
247 * @param fm The {@link FragmentManager} to use
248 * @return The existing instance of the {@link Fragment} or the new instance
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);
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();
260 return retainFragment;
264 * Adds a new image to the memory and disk caches
266 * @param data The key used to store the image
267 * @param bitmap The {@link Bitmap} to cache
269 public void addBitmapToCache(final String data, final Bitmap bitmap) {
270 addBitmapToCache(data, bitmap, false);
274 * Adds a new image to the memory and disk caches
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
280 public void addBitmapToCache(final String data, final Bitmap bitmap, final boolean replace) {
281 if (data == null || bitmap == null) {
285 // Add to memory cache
286 addBitmapToMemCache(data, bitmap, replace);
289 if (mDiskCache != null && !mDiskCache.isClosed()) {
290 final String key = hashKeyForDisk(data);
291 OutputStream out = null;
293 final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
294 if (snapshot != null) {
295 snapshot.getInputStream(DISK_CACHE_INDEX).close();
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);
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
314 Log.e(TAG, "addBitmapToCache", e);
316 IoUtils.closeQuietly(out);
322 * Called to add a new image to the memory cache
324 * @param data The key identifier
325 * @param bitmap The {@link Bitmap} to cache
327 public void addBitmapToMemCache(final String data, final Bitmap bitmap) {
328 addBitmapToMemCache(data, bitmap, false);
332 * Called to add a new image to the memory cache
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
338 public void addBitmapToMemCache(final String data, final Bitmap bitmap, final boolean replace) {
339 if (data == null || bitmap == null) {
342 // Add to memory cache
343 if (replace || getBitmapFromMemCache(data) == null) {
344 mLruCache.put(data, bitmap);
349 * Fetches a cached image from the memory cache
351 * @param data Unique identifier for which item to get
352 * @return The {@link Bitmap} if found in cache, null otherwise
354 public final Bitmap getBitmapFromMemCache(final String data) {
358 if (mLruCache != null) {
359 final Bitmap lruBitmap = mLruCache.get(data);
360 if (lruBitmap != null) {
368 * Fetches a cached image from the disk cache
370 * @param data Unique identifier for which item to get
371 * @return The {@link Bitmap} if found in cache, null otherwise
373 public final Bitmap getBitmapFromDiskCache(final String data) {
378 // Check in the memory cache here to avoid going to the disk cache less
380 if (getBitmapFromMemCache(data) != null) {
381 return getBitmapFromMemCache(data);
385 final String key = hashKeyForDisk(data);
386 if (mDiskCache != null) {
387 InputStream inputStream = null;
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) {
399 } catch (final IOException e) {
400 Log.e(TAG, "getBitmapFromDiskCache", e);
402 IoUtils.closeQuietly(inputStream);
409 * Tries to return a cached image from memory cache before fetching from the
412 * @param data Unique identifier for which item to get
413 * @return The {@link Bitmap} if found in cache, null otherwise
415 public final Bitmap getCachedBitmap(final String data) {
419 Bitmap cachedImage = getBitmapFromMemCache(data);
420 if (cachedImage == null) {
421 cachedImage = getBitmapFromDiskCache(data);
423 if (cachedImage != null) {
424 addBitmapToMemCache(data, cachedImage);
431 * Tries to return the album art from memory cache and disk cache, before
432 * calling {@code #getArtworkFromFile(Context, String)} again
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
439 public final Bitmap getCachedArtwork(final Context context, final String data, final long id) {
440 if (context == null || data == null) {
443 Bitmap cachedImage = getCachedBitmap(data);
444 if (cachedImage == null && id >= 0) {
445 cachedImage = getArtworkFromFile(context, id);
447 if (cachedImage != null) {
448 addBitmapToMemCache(data, cachedImage);
455 * Used to fetch the artwork for an album locally from the user's device
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
461 public final Bitmap getArtworkFromFile(final Context context, final long albumId) {
465 Bitmap artwork = null;
468 ParcelFileDescriptor parcelFileDescriptor = null;
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);
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);
484 IoUtils.closeQuietly(parcelFileDescriptor);
490 * flush() is called to synchronize up other methods that are accessing the
493 public void flush() {
494 ElevenUtils.execute(false, new AsyncTask<Void, Void, Void>() {
497 protected Void doInBackground(final Void... unused) {
498 if (mDiskCache != null) {
500 if (!mDiskCache.isClosed()) {
503 } catch (final IOException e) {
504 Log.e(TAG, "flush", e);
513 * Clears the disk and memory caches
515 public void clearCaches() {
516 ElevenUtils.execute(false, new AsyncTask<Void, Void, Void>() {
519 protected Void doInBackground(final Void... unused) {
520 // Clear the disk cache
522 if (mDiskCache != null) {
526 } catch (final IOException e) {
527 Log.e(TAG, "clearCaches", e);
529 // Clear the memory cache
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
541 public void close() {
542 ElevenUtils.execute(false, new AsyncTask<Void, Void, Void>() {
545 protected Void doInBackground(final Void... unused) {
546 if (mDiskCache != null) {
548 if (!mDiskCache.isClosed()) {
552 } catch (final IOException e) {
553 Log.e(TAG, "close", e);
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
565 public void evictAll() {
566 if (mLruCache != null) {
567 mLruCache.evictAll();
573 * @param key The key used to identify which cache entries to delete.
575 public void removeFromCache(final String key) {
579 // Remove the Lru entry
580 if (mLruCache != null) {
581 mLruCache.remove(key);
585 // Remove the disk entry
586 if (mDiskCache != null) {
587 mDiskCache.remove(hashKeyForDisk(key));
589 } catch (final IOException e) {
590 Log.e(TAG, "removeFromCache(" + key + ")", e);
596 * Used to temporarily pause the disk cache while the user is scrolling to
599 * @param pause True to temporarily pause the disk cache, false otherwise.
601 public void setPauseDiskCache(final boolean pause) {
602 synchronized (mPauseLock) {
603 if (mPauseDiskAccess != pause) {
604 mPauseDiskAccess = pause;
608 for (ICacheListener listener : mListeners) {
609 listener.onCacheUnpaused();
616 private void waitUntilUnpaused() {
617 synchronized (mPauseLock) {
618 if (Looper.myLooper() != Looper.getMainLooper()) {
619 while (mPauseDiskAccess) {
622 } catch (InterruptedException e) {
623 // ignored, we'll start waiting again
631 * @return True if the user is scrolling, false otherwise.
633 public boolean isDiskCachePaused() {
634 return mPauseDiskAccess;
637 public void addCacheListener(ICacheListener listener) {
638 mListeners.add(listener);
641 public void removeCacheListener(ICacheListener listener) {
642 mListeners.remove(listener);
646 * Get a usable cache directory (external if available, internal otherwise)
648 * @param context The {@link Context} to use
649 * @param uniqueName A unique directory name to append to the cache
651 * @return The cache directory
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);
662 * Check if external storage is built-in or removable
664 * @return True if external storage is removable (like an SD card), false
667 public static boolean isExternalStorageRemovable() {
668 return Environment.isExternalStorageRemovable();
672 * Get the external app cache directory
674 * @param context The {@link Context} to use
675 * @return The external cache directory
677 public static File getExternalCacheDir(final Context context) {
678 return context.getExternalCacheDir();
682 * Check how much usable space is available at a given path.
684 * @param path The path to check
685 * @return The space available in bytes
687 public static long getUsableSpace(final File path) {
688 return path.getUsableSpace();
692 * A hashing method that changes a string (like a URL) into a hash suitable
693 * for using as a disk filename.
695 * @param key The key used to store the file
697 public static String hashKeyForDisk(final String key) {
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());
710 * http://stackoverflow.com/questions/332079
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.
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) {
725 return builder.toString();
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.
733 public static final class RetainFragment extends Fragment {
736 * The object to be stored
738 private Object mObject;
741 * Empty constructor as per the {@link Fragment} documentation
743 public RetainFragment() {
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);
754 * Store a single object in this {@link Fragment}
756 * @param object The object to store
758 public void setObject(final Object object) {
763 * Get the stored object
765 * @return The stored object
767 public Object getObject() {
773 * Used to cache images via {@link LruCache}.
775 public static final class MemoryCache extends LruCache<String, Bitmap> {
778 * Constructor of <code>MemoryCache</code>
780 * @param maxSize The allowed size of the {@link LruCache}
782 public MemoryCache(final int maxSize) {
787 * Get the size in bytes of a bitmap.
789 public static int getBitmapSize(final Bitmap bitmap) {
790 return bitmap.getByteCount();
794 protected int sizeOf(final String paramString, final Bitmap paramBitmap) {
795 return getBitmapSize(paramBitmap);