• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.android.bitmapfun.util;
18 
19 import android.annotation.TargetApi;
20 import android.content.Context;
21 import android.graphics.Bitmap;
22 import android.graphics.Bitmap.CompressFormat;
23 import android.graphics.drawable.BitmapDrawable;
24 import android.graphics.BitmapFactory;
25 import android.os.Bundle;
26 import android.os.Environment;
27 import android.os.StatFs;
28 import android.support.v4.app.Fragment;
29 import android.support.v4.app.FragmentManager;
30 import android.support.v4.util.LruCache;
31 import android.util.Log;
32 
33 import com.example.android.bitmapfun.BuildConfig;
34 
35 import java.io.File;
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.io.OutputStream;
39 import java.security.MessageDigest;
40 import java.security.NoSuchAlgorithmException;
41 
42 /**
43  * This class holds our bitmap caches (memory and disk).
44  */
45 public class ImageCache {
46     private static final String TAG = "ImageCache";
47 
48     // Default memory cache size in kilobytes
49     private static final int DEFAULT_MEM_CACHE_SIZE = 1024 * 5; // 5MB
50 
51     // Default disk cache size in bytes
52     private static final int DEFAULT_DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
53 
54     // Compression settings when writing images to disk cache
55     private static final CompressFormat DEFAULT_COMPRESS_FORMAT = CompressFormat.JPEG;
56     private static final int DEFAULT_COMPRESS_QUALITY = 70;
57     private static final int DISK_CACHE_INDEX = 0;
58 
59     // Constants to easily toggle various caches
60     private static final boolean DEFAULT_MEM_CACHE_ENABLED = true;
61     private static final boolean DEFAULT_DISK_CACHE_ENABLED = true;
62     private static final boolean DEFAULT_CLEAR_DISK_CACHE_ON_START = false;
63     private static final boolean DEFAULT_INIT_DISK_CACHE_ON_CREATE = false;
64 
65     private DiskLruCache mDiskLruCache;
66     private LruCache<String, BitmapDrawable> mMemoryCache;
67     private ImageCacheParams mCacheParams;
68     private final Object mDiskCacheLock = new Object();
69     private boolean mDiskCacheStarting = true;
70 
71     /**
72      * Creating a new ImageCache object using the specified parameters.
73      *
74      * @param cacheParams The cache parameters to use to initialize the cache
75      */
ImageCache(ImageCacheParams cacheParams)76     public ImageCache(ImageCacheParams cacheParams) {
77         init(cacheParams);
78     }
79 
80     /**
81      * Creating a new ImageCache object using the default parameters.
82      *
83      * @param context The context to use
84      * @param uniqueName A unique name that will be appended to the cache directory
85      */
ImageCache(Context context, String uniqueName)86     public ImageCache(Context context, String uniqueName) {
87         init(new ImageCacheParams(context, uniqueName));
88     }
89 
90     /**
91      * Find and return an existing ImageCache stored in a {@link RetainFragment}, if not found a new
92      * one is created using the supplied params and saved to a {@link RetainFragment}.
93      *
94      * @param fragmentManager The fragment manager to use when dealing with the retained fragment.
95      * @param cacheParams The cache parameters to use if creating the ImageCache
96      * @return An existing retained ImageCache object or a new one if one did not exist
97      */
findOrCreateCache( FragmentManager fragmentManager, ImageCacheParams cacheParams)98     public static ImageCache findOrCreateCache(
99             FragmentManager fragmentManager, ImageCacheParams cacheParams) {
100 
101         // Search for, or create an instance of the non-UI RetainFragment
102         final RetainFragment mRetainFragment = findOrCreateRetainFragment(fragmentManager);
103 
104         // See if we already have an ImageCache stored in RetainFragment
105         ImageCache imageCache = (ImageCache) mRetainFragment.getObject();
106 
107         // No existing ImageCache, create one and store it in RetainFragment
108         if (imageCache == null) {
109             imageCache = new ImageCache(cacheParams);
110             mRetainFragment.setObject(imageCache);
111         }
112 
113         return imageCache;
114     }
115 
116     /**
117      * Initialize the cache, providing all parameters.
118      *
119      * @param cacheParams The cache parameters to initialize the cache
120      */
init(ImageCacheParams cacheParams)121     private void init(ImageCacheParams cacheParams) {
122         mCacheParams = cacheParams;
123 
124         // Set up memory cache
125         if (mCacheParams.memoryCacheEnabled) {
126             if (BuildConfig.DEBUG) {
127                 Log.d(TAG, "Memory cache created (size = " + mCacheParams.memCacheSize + ")");
128             }
129             mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {
130 
131                 /**
132                  * Notify the removed entry that is no longer being cached
133                  */
134                 @Override
135                 protected void entryRemoved(boolean evicted, String key,
136                         BitmapDrawable oldValue, BitmapDrawable newValue) {
137                     if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
138                         // The removed entry is a recycling drawable, so notify it
139                         // that it has been removed from the memory cache
140                         ((RecyclingBitmapDrawable) oldValue).setIsCached(false);
141                     }
142                 }
143 
144                 /**
145                  * Measure item size in kilobytes rather than units which is more practical
146                  * for a bitmap cache
147                  */
148                 @Override
149                 protected int sizeOf(String key, BitmapDrawable value) {
150                     final int bitmapSize = getBitmapSize(value) / 1024;
151                     return bitmapSize == 0 ? 1 : bitmapSize;
152                 }
153             };
154         }
155 
156         // By default the disk cache is not initialized here as it should be initialized
157         // on a separate thread due to disk access.
158         if (cacheParams.initDiskCacheOnCreate) {
159             // Set up disk cache
160             initDiskCache();
161         }
162     }
163 
164     /**
165      * Initializes the disk cache.  Note that this includes disk access so this should not be
166      * executed on the main/UI thread. By default an ImageCache does not initialize the disk
167      * cache when it is created, instead you should call initDiskCache() to initialize it on a
168      * background thread.
169      */
initDiskCache()170     public void initDiskCache() {
171         // Set up disk cache
172         synchronized (mDiskCacheLock) {
173             if (mDiskLruCache == null || mDiskLruCache.isClosed()) {
174                 File diskCacheDir = mCacheParams.diskCacheDir;
175                 if (mCacheParams.diskCacheEnabled && diskCacheDir != null) {
176                     if (!diskCacheDir.exists()) {
177                         diskCacheDir.mkdirs();
178                     }
179                     if (getUsableSpace(diskCacheDir) > mCacheParams.diskCacheSize) {
180                         try {
181                             mDiskLruCache = DiskLruCache.open(
182                                     diskCacheDir, 1, 1, mCacheParams.diskCacheSize);
183                             if (BuildConfig.DEBUG) {
184                                 Log.d(TAG, "Disk cache initialized");
185                             }
186                         } catch (final IOException e) {
187                             mCacheParams.diskCacheDir = null;
188                             Log.e(TAG, "initDiskCache - " + e);
189                         }
190                     }
191                 }
192             }
193             mDiskCacheStarting = false;
194             mDiskCacheLock.notifyAll();
195         }
196     }
197 
198     /**
199      * Adds a bitmap to both memory and disk cache.
200      * @param data Unique identifier for the bitmap to store
201      * @param value The bitmap drawable to store
202      */
addBitmapToCache(String data, BitmapDrawable value)203     public void addBitmapToCache(String data, BitmapDrawable value) {
204         if (data == null || value == null) {
205             return;
206         }
207 
208         // Add to memory cache
209         if (mMemoryCache != null) {
210             if (RecyclingBitmapDrawable.class.isInstance(value)) {
211                 // The removed entry is a recycling drawable, so notify it
212                 // that it has been added into the memory cache
213                 ((RecyclingBitmapDrawable) value).setIsCached(true);
214             }
215             mMemoryCache.put(data, value);
216         }
217 
218         synchronized (mDiskCacheLock) {
219             // Add to disk cache
220             if (mDiskLruCache != null) {
221                 final String key = hashKeyForDisk(data);
222                 OutputStream out = null;
223                 try {
224                     DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
225                     if (snapshot == null) {
226                         final DiskLruCache.Editor editor = mDiskLruCache.edit(key);
227                         if (editor != null) {
228                             out = editor.newOutputStream(DISK_CACHE_INDEX);
229                             value.getBitmap().compress(
230                                     mCacheParams.compressFormat, mCacheParams.compressQuality, out);
231                             editor.commit();
232                             out.close();
233                         }
234                     } else {
235                         snapshot.getInputStream(DISK_CACHE_INDEX).close();
236                     }
237                 } catch (final IOException e) {
238                     Log.e(TAG, "addBitmapToCache - " + e);
239                 } catch (Exception e) {
240                     Log.e(TAG, "addBitmapToCache - " + e);
241                 } finally {
242                     try {
243                         if (out != null) {
244                             out.close();
245                         }
246                     } catch (IOException e) {}
247                 }
248             }
249         }
250     }
251 
252     /**
253      * Get from memory cache.
254      *
255      * @param data Unique identifier for which item to get
256      * @return The bitmap drawable if found in cache, null otherwise
257      */
getBitmapFromMemCache(String data)258     public BitmapDrawable getBitmapFromMemCache(String data) {
259         BitmapDrawable memValue = null;
260 
261         if (mMemoryCache != null) {
262             memValue = mMemoryCache.get(data);
263         }
264 
265         if (BuildConfig.DEBUG && memValue != null) {
266             Log.d(TAG, "Memory cache hit");
267         }
268 
269         return memValue;
270     }
271 
272     /**
273      * Get from disk cache.
274      *
275      * @param data Unique identifier for which item to get
276      * @return The bitmap if found in cache, null otherwise
277      */
getBitmapFromDiskCache(String data)278     public Bitmap getBitmapFromDiskCache(String data) {
279         final String key = hashKeyForDisk(data);
280         synchronized (mDiskCacheLock) {
281             while (mDiskCacheStarting) {
282                 try {
283                     mDiskCacheLock.wait();
284                 } catch (InterruptedException e) {}
285             }
286             if (mDiskLruCache != null) {
287                 InputStream inputStream = null;
288                 try {
289                     final DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
290                     if (snapshot != null) {
291                         if (BuildConfig.DEBUG) {
292                             Log.d(TAG, "Disk cache hit");
293                         }
294                         inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
295                         if (inputStream != null) {
296                             final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
297                             return bitmap;
298                         }
299                     }
300                 } catch (final IOException e) {
301                     Log.e(TAG, "getBitmapFromDiskCache - " + e);
302                 } finally {
303                     try {
304                         if (inputStream != null) {
305                             inputStream.close();
306                         }
307                     } catch (IOException e) {}
308                 }
309             }
310             return null;
311         }
312     }
313 
314     /**
315      * Clears both the memory and disk cache associated with this ImageCache object. Note that
316      * this includes disk access so this should not be executed on the main/UI thread.
317      */
clearCache()318     public void clearCache() {
319         if (mMemoryCache != null) {
320             mMemoryCache.evictAll();
321             if (BuildConfig.DEBUG) {
322                 Log.d(TAG, "Memory cache cleared");
323             }
324         }
325 
326         synchronized (mDiskCacheLock) {
327             mDiskCacheStarting = true;
328             if (mDiskLruCache != null && !mDiskLruCache.isClosed()) {
329                 try {
330                     mDiskLruCache.delete();
331                     if (BuildConfig.DEBUG) {
332                         Log.d(TAG, "Disk cache cleared");
333                     }
334                 } catch (IOException e) {
335                     Log.e(TAG, "clearCache - " + e);
336                 }
337                 mDiskLruCache = null;
338                 initDiskCache();
339             }
340         }
341     }
342 
343     /**
344      * Flushes the disk cache associated with this ImageCache object. Note that this includes
345      * disk access so this should not be executed on the main/UI thread.
346      */
flush()347     public void flush() {
348         synchronized (mDiskCacheLock) {
349             if (mDiskLruCache != null) {
350                 try {
351                     mDiskLruCache.flush();
352                     if (BuildConfig.DEBUG) {
353                         Log.d(TAG, "Disk cache flushed");
354                     }
355                 } catch (IOException e) {
356                     Log.e(TAG, "flush - " + e);
357                 }
358             }
359         }
360     }
361 
362     /**
363      * Closes the disk cache associated with this ImageCache object. Note that this includes
364      * disk access so this should not be executed on the main/UI thread.
365      */
close()366     public void close() {
367         synchronized (mDiskCacheLock) {
368             if (mDiskLruCache != null) {
369                 try {
370                     if (!mDiskLruCache.isClosed()) {
371                         mDiskLruCache.close();
372                         mDiskLruCache = null;
373                         if (BuildConfig.DEBUG) {
374                             Log.d(TAG, "Disk cache closed");
375                         }
376                     }
377                 } catch (IOException e) {
378                     Log.e(TAG, "close - " + e);
379                 }
380             }
381         }
382     }
383 
384     /**
385      * A holder class that contains cache parameters.
386      */
387     public static class ImageCacheParams {
388         public int memCacheSize = DEFAULT_MEM_CACHE_SIZE;
389         public int diskCacheSize = DEFAULT_DISK_CACHE_SIZE;
390         public File diskCacheDir;
391         public CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT;
392         public int compressQuality = DEFAULT_COMPRESS_QUALITY;
393         public boolean memoryCacheEnabled = DEFAULT_MEM_CACHE_ENABLED;
394         public boolean diskCacheEnabled = DEFAULT_DISK_CACHE_ENABLED;
395         public boolean clearDiskCacheOnStart = DEFAULT_CLEAR_DISK_CACHE_ON_START;
396         public boolean initDiskCacheOnCreate = DEFAULT_INIT_DISK_CACHE_ON_CREATE;
397 
ImageCacheParams(Context context, String uniqueName)398         public ImageCacheParams(Context context, String uniqueName) {
399             diskCacheDir = getDiskCacheDir(context, uniqueName);
400         }
401 
ImageCacheParams(File diskCacheDir)402         public ImageCacheParams(File diskCacheDir) {
403             this.diskCacheDir = diskCacheDir;
404         }
405 
406         /**
407          * Sets the memory cache size based on a percentage of the max available VM memory.
408          * Eg. setting percent to 0.2 would set the memory cache to one fifth of the available
409          * memory. Throws {@link IllegalArgumentException} if percent is < 0.05 or > .8.
410          * memCacheSize is stored in kilobytes instead of bytes as this will eventually be passed
411          * to construct a LruCache which takes an int in its constructor.
412          *
413          * This value should be chosen carefully based on a number of factors
414          * Refer to the corresponding Android Training class for more discussion:
415          * http://developer.android.com/training/displaying-bitmaps/
416          *
417          * @param percent Percent of available app memory to use to size memory cache
418          */
setMemCacheSizePercent(float percent)419         public void setMemCacheSizePercent(float percent) {
420             if (percent < 0.05f || percent > 0.8f) {
421                 throw new IllegalArgumentException("setMemCacheSizePercent - percent must be "
422                         + "between 0.05 and 0.8 (inclusive)");
423             }
424             memCacheSize = Math.round(percent * Runtime.getRuntime().maxMemory() / 1024);
425         }
426     }
427 
428     /**
429      * Get a usable cache directory (external if available, internal otherwise).
430      *
431      * @param context The context to use
432      * @param uniqueName A unique directory name to append to the cache dir
433      * @return The cache dir
434      */
getDiskCacheDir(Context context, String uniqueName)435     public static File getDiskCacheDir(Context context, String uniqueName) {
436         // Check if media is mounted or storage is built-in, if so, try and use external cache dir
437         // otherwise use internal cache dir
438         final String cachePath =
439                 Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
440                         !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
441                                 context.getCacheDir().getPath();
442 
443         return new File(cachePath + File.separator + uniqueName);
444     }
445 
446     /**
447      * A hashing method that changes a string (like a URL) into a hash suitable for using as a
448      * disk filename.
449      */
hashKeyForDisk(String key)450     public static String hashKeyForDisk(String key) {
451         String cacheKey;
452         try {
453             final MessageDigest mDigest = MessageDigest.getInstance("MD5");
454             mDigest.update(key.getBytes());
455             cacheKey = bytesToHexString(mDigest.digest());
456         } catch (NoSuchAlgorithmException e) {
457             cacheKey = String.valueOf(key.hashCode());
458         }
459         return cacheKey;
460     }
461 
bytesToHexString(byte[] bytes)462     private static String bytesToHexString(byte[] bytes) {
463         // http://stackoverflow.com/questions/332079
464         StringBuilder sb = new StringBuilder();
465         for (int i = 0; i < bytes.length; i++) {
466             String hex = Integer.toHexString(0xFF & bytes[i]);
467             if (hex.length() == 1) {
468                 sb.append('0');
469             }
470             sb.append(hex);
471         }
472         return sb.toString();
473     }
474 
475     /**
476      * Get the size in bytes of a bitmap in a BitmapDrawable.
477      * @param value
478      * @return size in bytes
479      */
480     @TargetApi(12)
getBitmapSize(BitmapDrawable value)481     public static int getBitmapSize(BitmapDrawable value) {
482         Bitmap bitmap = value.getBitmap();
483 
484         if (Utils.hasHoneycombMR1()) {
485             return bitmap.getByteCount();
486         }
487         // Pre HC-MR1
488         return bitmap.getRowBytes() * bitmap.getHeight();
489     }
490 
491     /**
492      * Check if external storage is built-in or removable.
493      *
494      * @return True if external storage is removable (like an SD card), false
495      *         otherwise.
496      */
497     @TargetApi(9)
isExternalStorageRemovable()498     public static boolean isExternalStorageRemovable() {
499         if (Utils.hasGingerbread()) {
500             return Environment.isExternalStorageRemovable();
501         }
502         return true;
503     }
504 
505     /**
506      * Get the external app cache directory.
507      *
508      * @param context The context to use
509      * @return The external cache dir
510      */
511     @TargetApi(8)
getExternalCacheDir(Context context)512     public static File getExternalCacheDir(Context context) {
513         if (Utils.hasFroyo()) {
514             return context.getExternalCacheDir();
515         }
516 
517         // Before Froyo we need to construct the external cache dir ourselves
518         final String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/";
519         return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir);
520     }
521 
522     /**
523      * Check how much usable space is available at a given path.
524      *
525      * @param path The path to check
526      * @return The space available in bytes
527      */
528     @TargetApi(9)
getUsableSpace(File path)529     public static long getUsableSpace(File path) {
530         if (Utils.hasGingerbread()) {
531             return path.getUsableSpace();
532         }
533         final StatFs stats = new StatFs(path.getPath());
534         return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
535     }
536 
537     /**
538      * Locate an existing instance of this Fragment or if not found, create and
539      * add it using FragmentManager.
540      *
541      * @param fm The FragmentManager manager to use.
542      * @return The existing instance of the Fragment or the new instance if just
543      *         created.
544      */
findOrCreateRetainFragment(FragmentManager fm)545     public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
546         // Check to see if we have retained the worker fragment.
547         RetainFragment mRetainFragment = (RetainFragment) fm.findFragmentByTag(TAG);
548 
549         // If not retained (or first time running), we need to create and add it.
550         if (mRetainFragment == null) {
551             mRetainFragment = new RetainFragment();
552             fm.beginTransaction().add(mRetainFragment, TAG).commitAllowingStateLoss();
553         }
554 
555         return mRetainFragment;
556     }
557 
558     /**
559      * A simple non-UI Fragment that stores a single Object and is retained over configuration
560      * changes. It will be used to retain the ImageCache object.
561      */
562     public static class RetainFragment extends Fragment {
563         private Object mObject;
564 
565         /**
566          * Empty constructor as per the Fragment documentation
567          */
RetainFragment()568         public RetainFragment() {}
569 
570         @Override
onCreate(Bundle savedInstanceState)571         public void onCreate(Bundle savedInstanceState) {
572             super.onCreate(savedInstanceState);
573 
574             // Make sure this Fragment is retained over a configuration change
575             setRetainInstance(true);
576         }
577 
578         /**
579          * Store a single object in this Fragment.
580          *
581          * @param object The object to store
582          */
setObject(Object object)583         public void setObject(Object object) {
584             mObject = object;
585         }
586 
587         /**
588          * Get the stored object.
589          *
590          * @return The stored object
591          */
getObject()592         public Object getObject() {
593             return mObject;
594         }
595     }
596 
597 }
598