2 * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
3 * (the "License"); you may not use this file except in compliance with the
4 * License. You may obtain a copy of the License at
5 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
6 * or agreed to in writing, software distributed under the License is
7 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
8 * KIND, either express or implied. See the License for the specific language
9 * governing permissions and limitations under the License.
12 package com.cyngn.eleven.cache;
14 import android.annotation.SuppressLint;
15 import android.app.Activity;
16 import android.app.ActivityManager;
17 import android.app.Fragment;
18 import android.app.FragmentManager;
19 import android.content.ComponentCallbacks2;
20 import android.content.ContentUris;
21 import android.content.Context;
22 import android.content.res.Configuration;
23 import android.graphics.Bitmap;
24 import android.graphics.Bitmap.CompressFormat;
25 import android.graphics.BitmapFactory;
26 import android.net.Uri;
27 import android.os.AsyncTask;
28 import android.os.Build;
29 import android.os.Bundle;
30 import android.os.Environment;
31 import android.os.Looper;
32 import android.os.ParcelFileDescriptor;
33 import android.text.TextUtils;
34 import android.util.Log;
36 import com.cyngn.eleven.utils.ApolloUtils;
39 import java.io.FileDescriptor;
40 import java.io.FileNotFoundException;
41 import java.io.IOException;
42 import java.io.InputStream;
43 import java.io.OutputStream;
44 import java.security.MessageDigest;
45 import java.security.NoSuchAlgorithmException;
48 * This class holds the memory and disk bitmap caches.
50 public final class ImageCache {
52 private static final String TAG = ImageCache.class.getSimpleName();
55 * The {@link Uri} used to retrieve album art
57 private static final Uri mArtworkUri;
60 * Default memory cache size as a percent of device memory class
62 private static final float MEM_CACHE_DIVIDER = 0.25f;
65 * Default disk cache size 10MB
67 private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10;
70 * Compression settings when writing images to disk cache
72 private static final CompressFormat COMPRESS_FORMAT = CompressFormat.JPEG;
75 * Disk cache index to read from
77 private static final int DISK_CACHE_INDEX = 0;
80 * Image compression quality
82 private static final int COMPRESS_QUALITY = 98;
87 private MemoryCache mLruCache;
92 private DiskLruCache mDiskCache;
94 private static ImageCache sInstance;
97 * Used to temporarily pause the disk cache while scrolling
99 public boolean mPauseDiskAccess = false;
100 private Object mPauseLock = new Object();
103 mArtworkUri = Uri.parse("content://media/external/audio/albumart");
107 * Constructor of <code>ImageCache</code>
109 * @param context The {@link Context} to use
111 public ImageCache(final Context context) {
116 * Used to create a singleton of {@link ImageCache}
118 * @param context The {@link Context} to use
119 * @return A new instance of this class.
121 public final static ImageCache getInstance(final Context context) {
122 if (sInstance == null) {
123 sInstance = new ImageCache(context.getApplicationContext());
129 * Initialize the cache, providing all parameters.
131 * @param context The {@link Context} to use
132 * @param cacheParams The cache parameters to initialize the cache
134 private void init(final Context context) {
135 ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
138 protected Void doInBackground(final Void... unused) {
139 // Initialize the disk cahe in a background thread
140 initDiskCache(context);
144 // Set up the memory cache
145 initLruCache(context);
149 * Initializes the disk cache. Note that this includes disk access so this
150 * should not be executed on the main/UI thread. By default an ImageCache
151 * does not initialize the disk cache when it is created, instead you should
152 * call initDiskCache() to initialize it on a background thread.
154 * @param context The {@link Context} to use
156 private synchronized void initDiskCache(final Context context) {
158 if (mDiskCache == null || mDiskCache.isClosed()) {
159 File diskCacheDir = getDiskCacheDir(context, TAG);
160 if (diskCacheDir != null) {
161 if (!diskCacheDir.exists()) {
162 diskCacheDir.mkdirs();
164 if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
166 mDiskCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
167 } catch (final IOException e) {
176 * Sets up the Lru cache
178 * @param context The {@link Context} to use
180 @SuppressLint("NewApi")
181 public void initLruCache(final Context context) {
182 final ActivityManager activityManager = (ActivityManager)context
183 .getSystemService(Context.ACTIVITY_SERVICE);
184 final int lruCacheSize = Math.round(MEM_CACHE_DIVIDER * activityManager.getMemoryClass()
186 mLruCache = new MemoryCache(lruCacheSize);
188 // Release some memory as needed
189 context.registerComponentCallbacks(new ComponentCallbacks2() {
195 public void onTrimMemory(final int level) {
196 if (level >= TRIM_MEMORY_MODERATE) {
198 } else if (level >= TRIM_MEMORY_BACKGROUND) {
199 mLruCache.trimToSize(mLruCache.size() / 2);
207 public void onLowMemory() {
215 public void onConfigurationChanged(final Configuration newConfig) {
222 * Find and return an existing ImageCache stored in a {@link RetainFragment}
223 * , if not found a new one is created using the supplied params and saved
224 * to a {@link RetainFragment}
226 * @param activity The calling {@link FragmentActivity}
227 * @return An existing retained ImageCache object or a new one if one did
230 public static final ImageCache findOrCreateCache(final Activity activity) {
232 // Search for, or create an instance of the non-UI RetainFragment
233 final RetainFragment retainFragment = findOrCreateRetainFragment(
234 activity.getFragmentManager());
236 // See if we already have an ImageCache stored in RetainFragment
237 ImageCache cache = (ImageCache)retainFragment.getObject();
239 // No existing ImageCache, create one and store it in RetainFragment
241 cache = getInstance(activity);
242 retainFragment.setObject(cache);
248 * Locate an existing instance of this {@link Fragment} or if not found,
249 * create and add it using {@link FragmentManager}
251 * @param fm The {@link FragmentManager} to use
252 * @return The existing instance of the {@link Fragment} or the new instance
255 public static final RetainFragment findOrCreateRetainFragment(final FragmentManager fm) {
256 // Check to see if we have retained the worker fragment
257 RetainFragment retainFragment = (RetainFragment)fm.findFragmentByTag(TAG);
259 // If not retained, we need to create and add it
260 if (retainFragment == null) {
261 retainFragment = new RetainFragment();
262 fm.beginTransaction().add(retainFragment, TAG).commit();
264 return retainFragment;
268 * Adds a new image to the memory and disk caches
270 * @param data The key used to store the image
271 * @param bitmap The {@link Bitmap} to cache
273 public void addBitmapToCache(final String data, final Bitmap bitmap) {
274 if (data == null || bitmap == null) {
278 // Add to memory cache
279 addBitmapToMemCache(data, bitmap);
282 if (mDiskCache != null) {
283 final String key = hashKeyForDisk(data);
284 OutputStream out = null;
286 final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
287 if (snapshot == null) {
288 final DiskLruCache.Editor editor = mDiskCache.edit(key);
289 if (editor != null) {
290 out = editor.newOutputStream(DISK_CACHE_INDEX);
291 bitmap.compress(COMPRESS_FORMAT, COMPRESS_QUALITY, out);
297 snapshot.getInputStream(DISK_CACHE_INDEX).close();
299 } catch (final IOException e) {
300 Log.e(TAG, "addBitmapToCache - " + e);
307 } catch (final IOException e) {
308 Log.e(TAG, "addBitmapToCache - " + e);
309 } catch (final IllegalStateException e) {
310 Log.e(TAG, "addBitmapToCache - " + e);
317 * Called to add a new image to the memory cache
319 * @param data The key identifier
320 * @param bitmap The {@link Bitmap} to cache
322 public void addBitmapToMemCache(final String data, final Bitmap bitmap) {
323 if (data == null || bitmap == null) {
326 // Add to memory cache
327 if (getBitmapFromMemCache(data) == null) {
328 mLruCache.put(data, bitmap);
333 * Fetches a cached image from the memory cache
335 * @param data Unique identifier for which item to get
336 * @return The {@link Bitmap} if found in cache, null otherwise
338 public final Bitmap getBitmapFromMemCache(final String data) {
342 if (mLruCache != null) {
343 final Bitmap lruBitmap = mLruCache.get(data);
344 if (lruBitmap != null) {
352 * Fetches a cached image from the disk cache
354 * @param data Unique identifier for which item to get
355 * @return The {@link Bitmap} if found in cache, null otherwise
357 public final Bitmap getBitmapFromDiskCache(final String data) {
362 // Check in the memory cache here to avoid going to the disk cache less
364 if (getBitmapFromMemCache(data) != null) {
365 return getBitmapFromMemCache(data);
369 final String key = hashKeyForDisk(data);
370 if (mDiskCache != null) {
371 InputStream inputStream = null;
373 final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
374 if (snapshot != null) {
375 inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
376 if (inputStream != null) {
377 final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
378 if (bitmap != null) {
383 } catch (final IOException e) {
384 Log.e(TAG, "getBitmapFromDiskCache - " + e);
387 if (inputStream != null) {
390 } catch (final IOException e) {
398 * Tries to return a cached image from memory cache before fetching from the
401 * @param data Unique identifier for which item to get
402 * @return The {@link Bitmap} if found in cache, null otherwise
404 public final Bitmap getCachedBitmap(final String data) {
408 Bitmap cachedImage = getBitmapFromMemCache(data);
409 if (cachedImage == null) {
410 cachedImage = getBitmapFromDiskCache(data);
412 if (cachedImage != null) {
413 addBitmapToMemCache(data, cachedImage);
420 * Tries to return the album art from memory cache and disk cache, before
421 * calling {@code #getArtworkFromFile(Context, String)} again
423 * @param context The {@link Context} to use
424 * @param data The name of the album art
425 * @param id The ID of the album to find artwork for
426 * @return The artwork for an album
428 public final Bitmap getCachedArtwork(final Context context, final String data, final long id) {
429 if (context == null || data == null) {
432 Bitmap cachedImage = getCachedBitmap(data);
433 if (cachedImage == null && id >= 0) {
434 cachedImage = getArtworkFromFile(context, id);
436 if (cachedImage != null) {
437 addBitmapToMemCache(data, cachedImage);
444 * Used to fetch the artwork for an album locally from the user's device
446 * @param context The {@link Context} to use
447 * @param albumID The ID of the album to find artwork for
448 * @return The artwork for an album
450 public final Bitmap getArtworkFromFile(final Context context, final long albumId) {
454 Bitmap artwork = null;
457 final Uri uri = ContentUris.withAppendedId(mArtworkUri, albumId);
458 final ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver()
459 .openFileDescriptor(uri, "r");
460 if (parcelFileDescriptor != null) {
461 final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
462 artwork = BitmapFactory.decodeFileDescriptor(fileDescriptor);
464 } catch (final IllegalStateException e) {
465 // Log.e(TAG, "IllegalStateExcetpion - getArtworkFromFile - ", e);
466 } catch (final FileNotFoundException e) {
467 // Log.e(TAG, "FileNotFoundException - getArtworkFromFile - ", e);
468 } catch (final OutOfMemoryError evict) {
469 // Log.e(TAG, "OutOfMemoryError - getArtworkFromFile - ", evict);
476 * flush() is called to synchronize up other methods that are accessing the
479 public void flush() {
480 ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
483 protected Void doInBackground(final Void... unused) {
484 if (mDiskCache != null) {
486 if (!mDiskCache.isClosed()) {
489 } catch (final IOException e) {
490 Log.e(TAG, "flush - " + e);
499 * Clears the disk and memory caches
501 public void clearCaches() {
502 ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
505 protected Void doInBackground(final Void... unused) {
506 // Clear the disk cache
508 if (mDiskCache != null) {
512 } catch (final IOException e) {
513 Log.e(TAG, "clearCaches - " + e);
515 // Clear the memory cache
523 * Closes the disk cache associated with this ImageCache object. Note that
524 * this includes disk access so this should not be executed on the main/UI
527 public void close() {
528 ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
531 protected Void doInBackground(final Void... unused) {
532 if (mDiskCache != null) {
534 if (!mDiskCache.isClosed()) {
538 } catch (final IOException e) {
539 Log.e(TAG, "close - " + e);
548 * Evicts all of the items from the memory cache and lets the system know
549 * now would be a good time to garbage collect
551 public void evictAll() {
552 if (mLruCache != null) {
553 mLruCache.evictAll();
559 * @param key The key used to identify which cache entries to delete.
561 public void removeFromCache(final String key) {
565 // Remove the Lru entry
566 if (mLruCache != null) {
567 mLruCache.remove(key);
571 // Remove the disk entry
572 if (mDiskCache != null) {
573 mDiskCache.remove(hashKeyForDisk(key));
575 } catch (final IOException e) {
576 Log.e(TAG, "remove - " + e);
582 * Used to temporarily pause the disk cache while the user is scrolling to
585 * @param pause True to temporarily pause the disk cache, false otherwise.
587 public void setPauseDiskCache(final boolean pause) {
588 synchronized (mPauseLock) {
589 if (mPauseDiskAccess != pause) {
590 mPauseDiskAccess = pause;
598 private void waitUntilUnpaused() {
599 synchronized (mPauseLock) {
600 if (Looper.myLooper() != Looper.getMainLooper()) {
601 while (mPauseDiskAccess) {
604 } catch (InterruptedException e) {
605 // ignored, we'll start waiting again
613 * @return True if the user is scrolling, false otherwise.
615 public boolean isDiskCachePaused() {
616 return mPauseDiskAccess;
620 * Get a usable cache directory (external if available, internal otherwise)
622 * @param context The {@link Context} to use
623 * @param uniqueName A unique directory name to append to the cache
625 * @return The cache directory
627 public static final File getDiskCacheDir(final Context context, final String uniqueName) {
628 // getExternalCacheDir(context) returns null if external storage is not ready
629 final String cachePath = getExternalCacheDir(context) != null
630 ? getExternalCacheDir(context).getPath()
631 : context.getCacheDir().getPath();
632 return new File(cachePath, uniqueName);
636 * Check if external storage is built-in or removable
638 * @return True if external storage is removable (like an SD card), false
641 public static final boolean isExternalStorageRemovable() {
642 return Environment.isExternalStorageRemovable();
646 * Get the external app cache directory
648 * @param context The {@link Context} to use
649 * @return The external cache directory
651 public static final File getExternalCacheDir(final Context context) {
652 return context.getExternalCacheDir();
656 * Check how much usable space is available at a given path.
658 * @param path The path to check
659 * @return The space available in bytes
661 public static final long getUsableSpace(final File path) {
662 return path.getUsableSpace();
666 * A hashing method that changes a string (like a URL) into a hash suitable
667 * for using as a disk filename.
669 * @param key The key used to store the file
671 public static final String hashKeyForDisk(final String key) {
674 final MessageDigest digest = MessageDigest.getInstance("MD5");
675 digest.update(key.getBytes());
676 cacheKey = bytesToHexString(digest.digest());
677 } catch (final NoSuchAlgorithmException e) {
678 cacheKey = String.valueOf(key.hashCode());
684 * http://stackoverflow.com/questions/332079
686 * @param bytes The bytes to convert.
687 * @return A {@link String} converted from the bytes of a hashable key used
688 * to store a filename on the disk, to hex digits.
690 private static final String bytesToHexString(final byte[] bytes) {
691 final StringBuilder builder = new StringBuilder();
692 for (final byte b : bytes) {
693 final String hex = Integer.toHexString(0xFF & b);
694 if (hex.length() == 1) {
699 return builder.toString();
703 * A simple non-UI Fragment that stores a single Object and is retained over
704 * configuration changes. In this sample it will be used to retain an
705 * {@link ImageCache} object.
707 public static final class RetainFragment extends Fragment {
710 * The object to be stored
712 private Object mObject;
715 * Empty constructor as per the {@link Fragment} documentation
717 public RetainFragment() {
724 public void onCreate(final Bundle savedInstanceState) {
725 super.onCreate(savedInstanceState);
726 // Make sure this Fragment is retained over a configuration change
727 setRetainInstance(true);
731 * Store a single object in this {@link Fragment}
733 * @param object The object to store
735 public void setObject(final Object object) {
740 * Get the stored object
742 * @return The stored object
744 public Object getObject() {
750 * Used to cache images via {@link LruCache}.
752 public static final class MemoryCache extends LruCache<String, Bitmap> {
755 * Constructor of <code>MemoryCache</code>
757 * @param maxSize The allowed size of the {@link LruCache}
759 public MemoryCache(final int maxSize) {
764 * Get the size in bytes of a bitmap.
766 public static final int getBitmapSize(final Bitmap bitmap) {
767 return bitmap.getByteCount();
774 protected int sizeOf(final String paramString, final Bitmap paramBitmap) {
775 return getBitmapSize(paramBitmap);