Java tutorial
/* * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with the * License. You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law * or agreed to in writing, software distributed under the License is * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ package com.utils.image.cache; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.app.ActivityManager; import android.app.Fragment; import android.app.FragmentManager; import android.content.ComponentCallbacks2; import android.content.Context; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.StatFs; import android.support.v4.app.FragmentActivity; import android.util.Log; import com.utils.common.ApolloUtils; import com.utils.log.KeelLog; /** * This class holds the memory and disk bitmap caches. * ??? * * @author archko */ public final class ImageCache { private static final String TAG = ImageCache.class.getSimpleName(); /** * The {@link Uri} used to retrieve album art */ private static final Uri mArtworkUri; /** * Default memory cache size as a percent of device memory class */ private static final float MEM_CACHE_DIVIDER = 0.25f; /** * Default disk cache size 10MB */ private static final int DISK_CACHE_SIZE = 1024 * 1024 * 50; /** * Compression settings when writing images to disk cache */ private static final CompressFormat COMPRESS_FORMAT = CompressFormat.PNG; /** * Disk cache index to read from */ private static final int DISK_CACHE_INDEX = 0; /** * Image compression quality */ private static final int COMPRESS_QUALITY = 100; /** * LRU cache */ private MemoryCache mLruCache; /** * Disk LRU cache */ private DiskLruCache mDiskCache; private static ImageCache sInstance; /** * Used to temporarily pause the disk cache while scrolling */ public boolean mPauseDiskAccess = false; public static int IMAGE_MAX_WIDTH = 240; public static int IMAGE_MAX_HEIGHT = 400; public static final int BITMAP_SIZE = 1000 * 1000 * 4; static { mArtworkUri = Uri.parse("content://media/external/audio/albumart"); } /** * Constructor of <code>ImageCache</code> * * @param context The {@link Context} to use */ public ImageCache(final Context context) { init(context); } /** * Used to create a singleton of {@link ImageCache} * * @param context The {@link Context} to use * @return A new instance of this class. */ public final static ImageCache getInstance(final Context context) { if (sInstance == null) { sInstance = new ImageCache(context.getApplicationContext()); } return sInstance; } /** * Initialize the cache, providing all parameters. * * @param context The {@link Context} to use * @param cacheParams The cache parameters to initialize the cache */ private void init(final Context context) { ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(final Void... unused) { // Initialize the disk cahe in a background thread initDiskCache(context); return null; } }, (Void[]) null); // Set up the memory cache initLruCache(context); } /** * Initializes the disk cache. Note that this includes disk access so this * should not be executed on the main/UI thread. By default an ImageCache * does not initialize the disk cache when it is created, instead you should * call initDiskCache() to initialize it on a background thread. * * @param context The {@link Context} to use */ public void initDiskCache(final Context context) { // Set up disk cache if (mDiskCache == null || mDiskCache.isClosed()) { File diskCacheDir = getDiskCacheDir(context, TAG); if (diskCacheDir != null) { if (!diskCacheDir.exists()) { diskCacheDir.mkdirs(); } if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) { try { mDiskCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE); } catch (final IOException e) { diskCacheDir = null; } } } } } /** * Sets up the Lru cache * * @param context The {@link Context} to use */ @SuppressLint("NewApi") public void initLruCache(final Context context) { final ActivityManager activityManager = (ActivityManager) context .getSystemService(Context.ACTIVITY_SERVICE); final int lruCacheSize = Math.round(MEM_CACHE_DIVIDER * activityManager.getMemoryClass() * 1024 * 1024); KeelLog.d("lruCacheSize:" + lruCacheSize); mLruCache = new MemoryCache(lruCacheSize / 2); // Release some memory as needed if (ApolloUtils.hasICS()) { context.registerComponentCallbacks(new ComponentCallbacks2() { /** * {@inheritDoc} */ @Override public void onTrimMemory(final int level) { if (level >= TRIM_MEMORY_MODERATE) { evictAll(); } else if (level >= TRIM_MEMORY_BACKGROUND) { mLruCache.trimToSize(mLruCache.size() / 2); } } /** * {@inheritDoc} */ @Override public void onLowMemory() { // Nothing to do } /** * {@inheritDoc} */ @Override public void onConfigurationChanged(final Configuration newConfig) { // Nothing to do } }); } } /** * Find and return an existing ImageCache stored in a {@link RetainFragment} * , if not found a new one is created using the supplied params and saved * to a {@link RetainFragment} * * @param activity The calling {@link FragmentActivity} * @return An existing retained ImageCache object or a new one if one did * not exist */ public static final ImageCache findOrCreateCache(final Activity activity) { if (null == activity) { return null; } // Search for, or create an instance of the non-UI RetainFragment final RetainFragment retainFragment = findOrCreateRetainFragment(activity.getFragmentManager()); // See if we already have an ImageCache stored in RetainFragment ImageCache cache = (ImageCache) retainFragment.getObject(); // No existing ImageCache, create one and store it in RetainFragment if (cache == null) { cache = getInstance(activity); retainFragment.setObject(cache); } return cache; } /** * Locate an existing instance of this {@link Fragment} or if not found, * create and add it using {@link FragmentManager} * * @param fm The {@link FragmentManager} to use * @return The existing instance of the {@link Fragment} or the new instance * if just created */ public static final RetainFragment findOrCreateRetainFragment(final FragmentManager fm) { // Check to see if we have retained the worker fragment RetainFragment retainFragment = (RetainFragment) fm.findFragmentByTag(TAG); // If not retained, we need to create and add it if (retainFragment == null) { retainFragment = new RetainFragment(); fm.beginTransaction().add(retainFragment, TAG).commit(); } return retainFragment; } /** * Adds a new image to the memory and disk caches * * @param data The key used to store the image * @param bitmap The {@link Bitmap} to cache */ public void addBitmapToCache(final String data, final Bitmap bitmap) { if (data == null || bitmap == null) { return; } // Add to memory cache addBitmapToMemCache(data, bitmap); // Add to disk cache if (mDiskCache != null) { final String key = hashKeyForDisk(data); OutputStream out = null; try { final DiskLruCache.Snapshot snapshot = mDiskCache.get(key); if (snapshot == null) { final DiskLruCache.Editor editor = mDiskCache.edit(key); if (editor != null) { out = editor.newOutputStream(DISK_CACHE_INDEX); bitmap.compress(COMPRESS_FORMAT, COMPRESS_QUALITY, out); editor.commit(); out.close(); flush(); } } else { snapshot.getInputStream(DISK_CACHE_INDEX).close(); } } catch (final IOException e) { Log.e(TAG, "addBitmapToCache - " + e); } finally { try { if (out != null) { out.close(); out = null; } } catch (final IOException e) { Log.e(TAG, "addBitmapToCache - " + e); } catch (final IllegalStateException e) { Log.e(TAG, "addBitmapToCache - " + e); } } } } /** * Called to add a new image to the memory cache * * @param data The key identifier * @param bitmap The {@link Bitmap} to cache */ public void addBitmapToMemCache(final String data, final Bitmap bitmap) { if (data == null || bitmap == null) { return; } // Add to memory cache if (getBitmapFromMemCache(data) == null) { mLruCache.put(data, bitmap); } } /** * Fetches a cached image from the memory cache * * @param data Unique identifier for which item to get * @return The {@link Bitmap} if found in cache, null otherwise */ public final Bitmap getBitmapFromMemCache(final String data) { if (data == null) { return null; } if (mLruCache != null) { final Bitmap lruBitmap = mLruCache.get(data); if (lruBitmap != null) { return lruBitmap; } } return null; } /** * Fetches a cached image from the disk cache * * @param data Unique identifier for which item to get * @return The {@link Bitmap} if found in cache, null otherwise */ public final Bitmap getBitmapFromDiskCache(final String data, Bitmap.Config config) { if (data == null) { return null; } // Check in the memory cache here to avoid going to the disk cache less // often if (getBitmapFromMemCache(data) != null) { return getBitmapFromMemCache(data); } while (mPauseDiskAccess) { // Pause for moment } if (mDiskCache != null) { final String key = hashKeyForDisk(data); InputStream inputStream = null; try { final DiskLruCache.Snapshot snapshot = mDiskCache.get(key); if (snapshot != null) { inputStream = snapshot.getInputStream(DISK_CACHE_INDEX); if (inputStream != null) { BitmapFactory.Options options = new BitmapFactory.Options(); final Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options); if (bitmap != null) { return bitmap; } } } } catch (final IOException e) { Log.e(TAG, "getBitmapFromDiskCache - " + e); } finally { try { if (inputStream != null) { inputStream.close(); } } catch (final IOException e) { } } } return null; } /** * Fetches a cached image from the disk cache * * @param data Unique identifier for which item to get * @param ratio ,-1,>1,. * @return The {@link Bitmap} if found in cache, null otherwise */ public final Bitmap getBitmapFromDiskCache(final String data, int ratio, Bitmap.Config config) { if (data == null) { return null; } // Check in the memory cache here to avoid going to the disk cache less // often if (getBitmapFromMemCache(data) != null) { return getBitmapFromMemCache(data); } while (mPauseDiskAccess) { // Pause for moment } if (mDiskCache != null) { final String key = hashKeyForDisk(data); InputStream inputStream = null; try { final DiskLruCache.Snapshot snapshot = mDiskCache.get(key); if (snapshot != null) { inputStream = snapshot.getInputStream(DISK_CACHE_INDEX); if (inputStream != null) { return decodeBitmap(inputStream, ratio, config); } } } catch (final IOException e) { Log.e(TAG, "getBitmapFromDiskCache - " + e); } finally { try { if (inputStream != null) { inputStream.close(); } } catch (final IOException e) { } } } return null; } private Bitmap decodeBitmap(InputStream inputStream, int ratio, Bitmap.Config config) { Bitmap bitmap; int dw = IMAGE_MAX_WIDTH; int dh = IMAGE_MAX_HEIGHT; // Load up the image's dimensions not the image itself BitmapFactory.Options options = new BitmapFactory.Options(); if (ratio == -1) { options.inJustDecodeBounds = true; bitmap = BitmapFactory.decodeStream(inputStream, null, options); int heightRatio = (int) Math.ceil(options.outHeight / (float) dh); int widthRatio = (int) Math.ceil(options.outWidth / (float) dw); /*KeelLog.d(TAG, "widthRatio:"+widthRatio+" width:"+options.outWidth+ " height:"+options.outHeight+" dw:"+dw+" dh:"+dh+" heightRatio:"+heightRatio);*/ int fullSize = options.outHeight * options.outWidth; if (fullSize > BITMAP_SIZE) { if (fullSize >= BITMAP_SIZE * 2) { options.inSampleSize = 4; } else { options.inSampleSize = 2; } } if (options.inSampleSize < 2 && widthRatio > 2) { options.inSampleSize = widthRatio; } /*if (widthRatio>1||heightRatio>1) { if (heightRatio>widthRatio) { // Height ratio is larger, scale according to it options.inSampleSize=heightRatio; } } else if (heightRatio>1) { options.inSampleSize=heightRatio; if (widthRatio>1) { if (widthRatio>heightRatio) { // Wdith ratio is larger, scale according to it options.inSampleSize=widthRatio; } } }*/ } else { options.inSampleSize = ratio; } // Decode it for real //options.inPreferredConfig=Bitmap.Config.RGB_565; options.inPreferredConfig = config; options.inJustDecodeBounds = false; bitmap = BitmapFactory.decodeStream(inputStream, null, options); return bitmap; } /** * Tries to return a cached image from memory cache before fetching from the * disk cache * * @param data Unique identifier for which item to get * @return The {@link Bitmap} if found in cache, null otherwise */ public final Bitmap getCachedBitmap(final String data, Bitmap.Config config) { if (data == null) { return null; } Bitmap cachedImage = getBitmapFromMemCache(data); if (cachedImage == null) { cachedImage = getBitmapFromDiskCache(data, config); } if (cachedImage != null) { addBitmapToMemCache(data, cachedImage); return cachedImage; } return null; } /** * Tries to return the album art from memory cache and disk cache, before * calling {@code #getArtworkFromFile(Context, String)} again * * @param context The {@link Context} to use * @param data The name of the album art * @param id The ID of the album to find artwork for * @return The artwork for an album */ /*public final Bitmap getCachedArtwork(final Context context, final String data, final String id) { if (context == null || data == null) { return null; } Bitmap cachedImage = getCachedBitmap(data); if (cachedImage == null && id != null) { cachedImage = getArtworkFromFile(context, id); } if (cachedImage != null) { addBitmapToMemCache(data, cachedImage); return cachedImage; } return null; }*/ /** * Used to fetch the artwork for an album locally from the user's device * * @param context The {@link Context} to use * @param albumID The ID of the album to find artwork for * @return The artwork for an album */ /*public final Bitmap getArtworkFromFile(final Context context, final String albumId) { if (TextUtils.isEmpty(albumId)) { return null; } Bitmap artwork = null; while (mPauseDiskAccess) { // Pause for a moment } try { final Uri uri = ContentUris.withAppendedId(mArtworkUri, Long.valueOf(albumId)); final ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver() .openFileDescriptor(uri, "r"); if (parcelFileDescriptor != null) { final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); artwork = BitmapFactory.decodeFileDescriptor(fileDescriptor); } } catch (final IllegalStateException e) { // Log.e(TAG, "IllegalStateExcetpion - getArtworkFromFile - ", e); } catch (final FileNotFoundException e) { // Log.e(TAG, "FileNotFoundException - getArtworkFromFile - ", e); } catch (final OutOfMemoryError evict) { // Log.e(TAG, "OutOfMemoryError - getArtworkFromFile - ", evict); evictAll(); } return artwork; }*/ /** * flush() is called to synchronize up other methods that are accessing the * cache first */ public void flush() { ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(final Void... unused) { if (mDiskCache != null) { try { if (!mDiskCache.isClosed()) { mDiskCache.flush(); } } catch (final IOException e) { Log.e(TAG, "flush - " + e); } } return null; } }, (Void[]) null); } /** * Clears the disk and memory caches */ public void clearCaches() { ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(final Void... unused) { // Clear the disk cache try { if (mDiskCache != null) { mDiskCache.delete(); mDiskCache = null; } } catch (final IOException e) { Log.e(TAG, "clearCaches - " + e); } // Clear the memory cache evictAll(); return null; } }, (Void[]) null); } /** * Closes the disk cache associated with this ImageCache object. Note that * this includes disk access so this should not be executed on the main/UI * thread. */ public void close() { ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(final Void... unused) { if (mDiskCache != null) { try { if (!mDiskCache.isClosed()) { mDiskCache.close(); mDiskCache = null; } } catch (final IOException e) { Log.e(TAG, "close - " + e); } } return null; } }, (Void[]) null); } /** * Evicts all of the items from the memory cache and lets the system know * now would be a good time to garbage collect */ public void evictAll() { if (mLruCache != null) { mLruCache.evictAll(); } System.gc(); } /** * @param key The key used to identify which cache entries to delete. */ public void removeFromCache(final String key) { if (key == null) { return; } // Remove the Lru entry if (mLruCache != null) { mLruCache.remove(key); } try { // Remove the disk entry if (mDiskCache != null) { mDiskCache.remove(hashKeyForDisk(key)); } } catch (final IOException e) { Log.e(TAG, "remove - " + e); } flush(); } /** * Used to temporarily pause the disk cache while the user is scrolling to * improve scrolling. * * @param pause True to temporarily pause the disk cache, false otherwise. */ public void setPauseDiskCache(final boolean pause) { mPauseDiskAccess = pause; } /** * @return True if the user is scrolling, false otherwise. */ public boolean isScrolling() { return mPauseDiskAccess; } /** * Get a usable cache directory (external if available, internal otherwise) * * @param context The {@link Context} to use * @param uniqueName A unique directory name to append to the cache * directory * @return The cache directory */ public static final File getDiskCacheDir(final Context context, final String uniqueName) { // final String cachePath = Environment.MEDIA_MOUNTED.equals(Environment // .getExternalStorageState()) || !isExternalStorageRemovable() ? getExternalCacheDir( // context).getPath() : context.getCacheDir().getPath();//sd? final String cachePath = context.getCacheDir().getPath();// return new File(cachePath + File.separator + uniqueName); } /** * Check if external storage is built-in or removable * * @return True if external storage is removable (like an SD card), false * otherwise */ @TargetApi(Build.VERSION_CODES.GINGERBREAD) public static final boolean isExternalStorageRemovable() { if (ApolloUtils.hasGingerbread()) { return Environment.isExternalStorageRemovable(); } return true; } /** * Get the external app cache directory * * @param context The {@link Context} to use * @return The external cache directory */ public static final File getExternalCacheDir(final Context context) { if (ApolloUtils.hasFroyo()) { final File mCacheDir = context.getExternalCacheDir(); if (mCacheDir != null) { return mCacheDir; } } /* Before Froyo we need to construct the external cache dir ourselves */ final String mCacheDir = "/Android/data/" + context.getPackageName() + "/cache/"; return new File(Environment.getExternalStorageDirectory().getPath() + mCacheDir); } /** * Check how much usable space is available at a given path. * * @param path The path to check * @return The space available in bytes */ @TargetApi(Build.VERSION_CODES.GINGERBREAD) public static final long getUsableSpace(final File path) { if (ApolloUtils.hasGingerbread()) { return path.getUsableSpace(); } final StatFs stats = new StatFs(path.getPath()); return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks(); } /** * A hashing method that changes a string (like a URL) into a hash suitable * for using as a disk filename. * * @param key The key used to store the file */ public static final String hashKeyForDisk(final String key) { String cacheKey; try { final MessageDigest digest = MessageDigest.getInstance("MD5"); digest.update(key.getBytes()); cacheKey = bytesToHexString(digest.digest()); } catch (final NoSuchAlgorithmException e) { cacheKey = String.valueOf(key.hashCode()); } return cacheKey; } /** * http://stackoverflow.com/questions/332079 * * @param bytes The bytes to convert. * @return A {@link String} converted from the bytes of a hashable key used * to store a filename on the disk, to hex digits. */ private static final String bytesToHexString(final byte[] bytes) { final StringBuilder builder = new StringBuilder(); for (final byte b : bytes) { final String hex = Integer.toHexString(0xFF & b); if (hex.length() == 1) { builder.append('0'); } builder.append(hex); } return builder.toString(); } /** * A simple non-UI Fragment that stores a single Object and is retained over * configuration changes. In this sample it will be used to retain an * {@link ImageCache} object. */ public static final class RetainFragment extends Fragment { /** * The object to be stored */ private Object mObject; /** * Empty constructor as per the {@link Fragment} documentation */ public RetainFragment() { } /** * {@inheritDoc} */ @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Make sure this Fragment is retained over a configuration change setRetainInstance(true); } /** * Store a single object in this {@link Fragment} * * @param object The object to store */ public void setObject(final Object object) { mObject = object; } /** * Get the stored object * * @return The stored object */ public Object getObject() { return mObject; } } /** * Used to cache images via {@link LruCache}. */ public static final class MemoryCache extends LruCache<String, Bitmap> { /** * Constructor of <code>MemoryCache</code> * * @param maxSize The allowed size of the {@link LruCache} */ public MemoryCache(final int maxSize) { super(maxSize); } /** * Get the size in bytes of a bitmap. */ @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) public static final int getBitmapSize(final Bitmap bitmap) { if (ApolloUtils.hasHoneycombMR1()) { return bitmap.getByteCount(); } /* Pre HC-MR1 */ return bitmap.getRowBytes() * bitmap.getHeight(); } /** * {@inheritDoc} */ @Override protected int sizeOf(final String paramString, final Bitmap paramBitmap) { return getBitmapSize(paramBitmap); } } }