Home | History | Annotate | Download | only in util
      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.displayingbitmaps.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.Bitmap.Config;
     24 import android.graphics.BitmapFactory;
     25 import android.graphics.drawable.BitmapDrawable;
     26 import android.os.Build.VERSION_CODES;
     27 import android.os.Bundle;
     28 import android.os.Environment;
     29 import android.os.StatFs;
     30 import android.support.v4.app.Fragment;
     31 import android.support.v4.app.FragmentManager;
     32 import android.support.v4.util.LruCache;
     33 
     34 import com.example.android.common.logger.Log;
     35 import com.example.android.displayingbitmaps.BuildConfig;
     36 
     37 import java.io.File;
     38 import java.io.FileDescriptor;
     39 import java.io.FileInputStream;
     40 import java.io.IOException;
     41 import java.io.InputStream;
     42 import java.io.OutputStream;
     43 import java.lang.ref.SoftReference;
     44 import java.security.MessageDigest;
     45 import java.security.NoSuchAlgorithmException;
     46 import java.util.Collections;
     47 import java.util.HashSet;
     48 import java.util.Iterator;
     49 import java.util.Set;
     50 
     51 /**
     52  * This class handles disk and memory caching of bitmaps in conjunction with the
     53  * {@link ImageWorker} class and its subclasses. Use
     54  * {@link ImageCache#getInstance(android.support.v4.app.FragmentManager, ImageCacheParams)} to get an instance of this
     55  * class, although usually a cache should be added directly to an {@link ImageWorker} by calling
     56  * {@link ImageWorker#addImageCache(android.support.v4.app.FragmentManager, ImageCacheParams)}.
     57  */
     58 public class ImageCache {
     59     private static final String TAG = "ImageCache";
     60 
     61     // Default memory cache size in kilobytes
     62     private static final int DEFAULT_MEM_CACHE_SIZE = 1024 * 5; // 5MB
     63 
     64     // Default disk cache size in bytes
     65     private static final int DEFAULT_DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
     66 
     67     // Compression settings when writing images to disk cache
     68     private static final CompressFormat DEFAULT_COMPRESS_FORMAT = CompressFormat.JPEG;
     69     private static final int DEFAULT_COMPRESS_QUALITY = 70;
     70     private static final int DISK_CACHE_INDEX = 0;
     71 
     72     // Constants to easily toggle various caches
     73     private static final boolean DEFAULT_MEM_CACHE_ENABLED = true;
     74     private static final boolean DEFAULT_DISK_CACHE_ENABLED = true;
     75     private static final boolean DEFAULT_INIT_DISK_CACHE_ON_CREATE = false;
     76 
     77     private DiskLruCache mDiskLruCache;
     78     private LruCache<String, BitmapDrawable> mMemoryCache;
     79     private ImageCacheParams mCacheParams;
     80     private final Object mDiskCacheLock = new Object();
     81     private boolean mDiskCacheStarting = true;
     82 
     83     private Set<SoftReference<Bitmap>> mReusableBitmaps;
     84 
     85     /**
     86      * Create a new ImageCache object using the specified parameters. This should not be
     87      * called directly by other classes, instead use
     88      * {@link ImageCache#getInstance(android.support.v4.app.FragmentManager, ImageCacheParams)} to fetch an ImageCache
     89      * instance.
     90      *
     91      * @param cacheParams The cache parameters to use to initialize the cache
     92      */
     93     private ImageCache(ImageCacheParams cacheParams) {
     94         init(cacheParams);
     95     }
     96 
     97     /**
     98      * Return an {@link ImageCache} instance. A {@link RetainFragment} is used to retain the
     99      * ImageCache object across configuration changes such as a change in device orientation.
    100      *
    101      * @param fragmentManager The fragment manager to use when dealing with the retained fragment.
    102      * @param cacheParams The cache parameters to use if the ImageCache needs instantiation.
    103      * @return An existing retained ImageCache object or a new one if one did not exist
    104      */
    105     public static ImageCache getInstance(
    106             FragmentManager fragmentManager, ImageCacheParams cacheParams) {
    107 
    108         // Search for, or create an instance of the non-UI RetainFragment
    109         final RetainFragment mRetainFragment = findOrCreateRetainFragment(fragmentManager);
    110 
    111         // See if we already have an ImageCache stored in RetainFragment
    112         ImageCache imageCache = (ImageCache) mRetainFragment.getObject();
    113 
    114         // No existing ImageCache, create one and store it in RetainFragment
    115         if (imageCache == null) {
    116             imageCache = new ImageCache(cacheParams);
    117             mRetainFragment.setObject(imageCache);
    118         }
    119 
    120         return imageCache;
    121     }
    122 
    123     /**
    124      * Initialize the cache, providing all parameters.
    125      *
    126      * @param cacheParams The cache parameters to initialize the cache
    127      */
    128     private void init(ImageCacheParams cacheParams) {
    129         mCacheParams = cacheParams;
    130 
    131         //BEGIN_INCLUDE(init_memory_cache)
    132         // Set up memory cache
    133         if (mCacheParams.memoryCacheEnabled) {
    134             if (BuildConfig.DEBUG) {
    135                 Log.d(TAG, "Memory cache created (size = " + mCacheParams.memCacheSize + ")");
    136             }
    137 
    138             // If we're running on Honeycomb or newer, create a set of reusable bitmaps that can be
    139             // populated into the inBitmap field of BitmapFactory.Options. Note that the set is
    140             // of SoftReferences which will actually not be very effective due to the garbage
    141             // collector being aggressive clearing Soft/WeakReferences. A better approach
    142             // would be to use a strongly references bitmaps, however this would require some
    143             // balancing of memory usage between this set and the bitmap LruCache. It would also
    144             // require knowledge of the expected size of the bitmaps. From Honeycomb to JellyBean
    145             // the size would need to be precise, from KitKat onward the size would just need to
    146             // be the upper bound (due to changes in how inBitmap can re-use bitmaps).
    147             if (Utils.hasHoneycomb()) {
    148                 mReusableBitmaps =
    149                         Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
    150             }
    151 
    152             mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {
    153 
    154                 /**
    155                  * Notify the removed entry that is no longer being cached
    156                  */
    157                 @Override
    158                 protected void entryRemoved(boolean evicted, String key,
    159                         BitmapDrawable oldValue, BitmapDrawable newValue) {
    160                     if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
    161                         // The removed entry is a recycling drawable, so notify it
    162                         // that it has been removed from the memory cache
    163                         ((RecyclingBitmapDrawable) oldValue).setIsCached(false);
    164                     } else {
    165                         // The removed entry is a standard BitmapDrawable
    166 
    167                         if (Utils.hasHoneycomb()) {
    168                             // We're running on Honeycomb or later, so add the bitmap
    169                             // to a SoftReference set for possible use with inBitmap later
    170                             mReusableBitmaps.add(new SoftReference<Bitmap>(oldValue.getBitmap()));
    171                         }
    172                     }
    173                 }
    174 
    175                 /**
    176                  * Measure item size in kilobytes rather than units which is more practical
    177                  * for a bitmap cache
    178                  */
    179                 @Override
    180                 protected int sizeOf(String key, BitmapDrawable value) {
    181                     final int bitmapSize = getBitmapSize(value) / 1024;
    182                     return bitmapSize == 0 ? 1 : bitmapSize;
    183                 }
    184             };
    185         }
    186         //END_INCLUDE(init_memory_cache)
    187 
    188         // By default the disk cache is not initialized here as it should be initialized
    189         // on a separate thread due to disk access.
    190         if (cacheParams.initDiskCacheOnCreate) {
    191             // Set up disk cache
    192             initDiskCache();
    193         }
    194     }
    195 
    196     /**
    197      * Initializes the disk cache.  Note that this includes disk access so this should not be
    198      * executed on the main/UI thread. By default an ImageCache does not initialize the disk
    199      * cache when it is created, instead you should call initDiskCache() to initialize it on a
    200      * background thread.
    201      */
    202     public void initDiskCache() {
    203         // Set up disk cache
    204         synchronized (mDiskCacheLock) {
    205             if (mDiskLruCache == null || mDiskLruCache.isClosed()) {
    206                 File diskCacheDir = mCacheParams.diskCacheDir;
    207                 if (mCacheParams.diskCacheEnabled && diskCacheDir != null) {
    208                     if (!diskCacheDir.exists()) {
    209                         diskCacheDir.mkdirs();
    210                     }
    211                     if (getUsableSpace(diskCacheDir) > mCacheParams.diskCacheSize) {
    212                         try {
    213                             mDiskLruCache = DiskLruCache.open(
    214                                     diskCacheDir, 1, 1, mCacheParams.diskCacheSize);
    215                             if (BuildConfig.DEBUG) {
    216                                 Log.d(TAG, "Disk cache initialized");
    217                             }
    218                         } catch (final IOException e) {
    219                             mCacheParams.diskCacheDir = null;
    220                             Log.e(TAG, "initDiskCache - " + e);
    221                         }
    222                     }
    223                 }
    224             }
    225             mDiskCacheStarting = false;
    226             mDiskCacheLock.notifyAll();
    227         }
    228     }
    229 
    230     /**
    231      * Adds a bitmap to both memory and disk cache.
    232      * @param data Unique identifier for the bitmap to store
    233      * @param value The bitmap drawable to store
    234      */
    235     public void addBitmapToCache(String data, BitmapDrawable value) {
    236         //BEGIN_INCLUDE(add_bitmap_to_cache)
    237         if (data == null || value == null) {
    238             return;
    239         }
    240 
    241         // Add to memory cache
    242         if (mMemoryCache != null) {
    243             if (RecyclingBitmapDrawable.class.isInstance(value)) {
    244                 // The removed entry is a recycling drawable, so notify it
    245                 // that it has been added into the memory cache
    246                 ((RecyclingBitmapDrawable) value).setIsCached(true);
    247             }
    248             mMemoryCache.put(data, value);
    249         }
    250 
    251         synchronized (mDiskCacheLock) {
    252             // Add to disk cache
    253             if (mDiskLruCache != null) {
    254                 final String key = hashKeyForDisk(data);
    255                 OutputStream out = null;
    256                 try {
    257                     DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
    258                     if (snapshot == null) {
    259                         final DiskLruCache.Editor editor = mDiskLruCache.edit(key);
    260                         if (editor != null) {
    261                             out = editor.newOutputStream(DISK_CACHE_INDEX);
    262                             value.getBitmap().compress(
    263                                     mCacheParams.compressFormat, mCacheParams.compressQuality, out);
    264                             editor.commit();
    265                             out.close();
    266                         }
    267                     } else {
    268                         snapshot.getInputStream(DISK_CACHE_INDEX).close();
    269                     }
    270                 } catch (final IOException e) {
    271                     Log.e(TAG, "addBitmapToCache - " + e);
    272                 } catch (Exception e) {
    273                     Log.e(TAG, "addBitmapToCache - " + e);
    274                 } finally {
    275                     try {
    276                         if (out != null) {
    277                             out.close();
    278                         }
    279                     } catch (IOException e) {}
    280                 }
    281             }
    282         }
    283         //END_INCLUDE(add_bitmap_to_cache)
    284     }
    285 
    286     /**
    287      * Get from memory cache.
    288      *
    289      * @param data Unique identifier for which item to get
    290      * @return The bitmap drawable if found in cache, null otherwise
    291      */
    292     public BitmapDrawable getBitmapFromMemCache(String data) {
    293         //BEGIN_INCLUDE(get_bitmap_from_mem_cache)
    294         BitmapDrawable memValue = null;
    295 
    296         if (mMemoryCache != null) {
    297             memValue = mMemoryCache.get(data);
    298         }
    299 
    300         if (BuildConfig.DEBUG && memValue != null) {
    301             Log.d(TAG, "Memory cache hit");
    302         }
    303 
    304         return memValue;
    305         //END_INCLUDE(get_bitmap_from_mem_cache)
    306     }
    307 
    308     /**
    309      * Get from disk cache.
    310      *
    311      * @param data Unique identifier for which item to get
    312      * @return The bitmap if found in cache, null otherwise
    313      */
    314     public Bitmap getBitmapFromDiskCache(String data) {
    315         //BEGIN_INCLUDE(get_bitmap_from_disk_cache)
    316         final String key = hashKeyForDisk(data);
    317         Bitmap bitmap = null;
    318 
    319         synchronized (mDiskCacheLock) {
    320             while (mDiskCacheStarting) {
    321                 try {
    322                     mDiskCacheLock.wait();
    323                 } catch (InterruptedException e) {}
    324             }
    325             if (mDiskLruCache != null) {
    326                 InputStream inputStream = null;
    327                 try {
    328                     final DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
    329                     if (snapshot != null) {
    330                         if (BuildConfig.DEBUG) {
    331                             Log.d(TAG, "Disk cache hit");
    332                         }
    333                         inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
    334                         if (inputStream != null) {
    335                             FileDescriptor fd = ((FileInputStream) inputStream).getFD();
    336 
    337                             // Decode bitmap, but we don't want to sample so give
    338                             // MAX_VALUE as the target dimensions
    339                             bitmap = ImageResizer.decodeSampledBitmapFromDescriptor(
    340                                     fd, Integer.MAX_VALUE, Integer.MAX_VALUE, this);
    341                         }
    342                     }
    343                 } catch (final IOException e) {
    344                     Log.e(TAG, "getBitmapFromDiskCache - " + e);
    345                 } finally {
    346                     try {
    347                         if (inputStream != null) {
    348                             inputStream.close();
    349                         }
    350                     } catch (IOException e) {}
    351                 }
    352             }
    353             return bitmap;
    354         }
    355         //END_INCLUDE(get_bitmap_from_disk_cache)
    356     }
    357 
    358     /**
    359      * @param options - BitmapFactory.Options with out* options populated
    360      * @return Bitmap that case be used for inBitmap
    361      */
    362     protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
    363         //BEGIN_INCLUDE(get_bitmap_from_reusable_set)
    364         Bitmap bitmap = null;
    365 
    366         if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
    367             synchronized (mReusableBitmaps) {
    368                 final Iterator<SoftReference<Bitmap>> iterator = mReusableBitmaps.iterator();
    369                 Bitmap item;
    370 
    371                 while (iterator.hasNext()) {
    372                     item = iterator.next().get();
    373 
    374                     if (null != item && item.isMutable()) {
    375                         // Check to see it the item can be used for inBitmap
    376                         if (canUseForInBitmap(item, options)) {
    377                             bitmap = item;
    378 
    379                             // Remove from reusable set so it can't be used again
    380                             iterator.remove();
    381                             break;
    382                         }
    383                     } else {
    384                         // Remove from the set if the reference has been cleared.
    385                         iterator.remove();
    386                     }
    387                 }
    388             }
    389         }
    390 
    391         return bitmap;
    392         //END_INCLUDE(get_bitmap_from_reusable_set)
    393     }
    394 
    395     /**
    396      * Clears both the memory and disk cache associated with this ImageCache object. Note that
    397      * this includes disk access so this should not be executed on the main/UI thread.
    398      */
    399     public void clearCache() {
    400         if (mMemoryCache != null) {
    401             mMemoryCache.evictAll();
    402             if (BuildConfig.DEBUG) {
    403                 Log.d(TAG, "Memory cache cleared");
    404             }
    405         }
    406 
    407         synchronized (mDiskCacheLock) {
    408             mDiskCacheStarting = true;
    409             if (mDiskLruCache != null && !mDiskLruCache.isClosed()) {
    410                 try {
    411                     mDiskLruCache.delete();
    412                     if (BuildConfig.DEBUG) {
    413                         Log.d(TAG, "Disk cache cleared");
    414                     }
    415                 } catch (IOException e) {
    416                     Log.e(TAG, "clearCache - " + e);
    417                 }
    418                 mDiskLruCache = null;
    419                 initDiskCache();
    420             }
    421         }
    422     }
    423 
    424     /**
    425      * Flushes the disk cache associated with this ImageCache object. Note that this includes
    426      * disk access so this should not be executed on the main/UI thread.
    427      */
    428     public void flush() {
    429         synchronized (mDiskCacheLock) {
    430             if (mDiskLruCache != null) {
    431                 try {
    432                     mDiskLruCache.flush();
    433                     if (BuildConfig.DEBUG) {
    434                         Log.d(TAG, "Disk cache flushed");
    435                     }
    436                 } catch (IOException e) {
    437                     Log.e(TAG, "flush - " + e);
    438                 }
    439             }
    440         }
    441     }
    442 
    443     /**
    444      * Closes the disk cache associated with this ImageCache object. Note that this includes
    445      * disk access so this should not be executed on the main/UI thread.
    446      */
    447     public void close() {
    448         synchronized (mDiskCacheLock) {
    449             if (mDiskLruCache != null) {
    450                 try {
    451                     if (!mDiskLruCache.isClosed()) {
    452                         mDiskLruCache.close();
    453                         mDiskLruCache = null;
    454                         if (BuildConfig.DEBUG) {
    455                             Log.d(TAG, "Disk cache closed");
    456                         }
    457                     }
    458                 } catch (IOException e) {
    459                     Log.e(TAG, "close - " + e);
    460                 }
    461             }
    462         }
    463     }
    464 
    465     /**
    466      * A holder class that contains cache parameters.
    467      */
    468     public static class ImageCacheParams {
    469         public int memCacheSize = DEFAULT_MEM_CACHE_SIZE;
    470         public int diskCacheSize = DEFAULT_DISK_CACHE_SIZE;
    471         public File diskCacheDir;
    472         public CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT;
    473         public int compressQuality = DEFAULT_COMPRESS_QUALITY;
    474         public boolean memoryCacheEnabled = DEFAULT_MEM_CACHE_ENABLED;
    475         public boolean diskCacheEnabled = DEFAULT_DISK_CACHE_ENABLED;
    476         public boolean initDiskCacheOnCreate = DEFAULT_INIT_DISK_CACHE_ON_CREATE;
    477 
    478         /**
    479          * Create a set of image cache parameters that can be provided to
    480          * {@link ImageCache#getInstance(android.support.v4.app.FragmentManager, ImageCacheParams)} or
    481          * {@link ImageWorker#addImageCache(android.support.v4.app.FragmentManager, ImageCacheParams)}.
    482          * @param context A context to use.
    483          * @param diskCacheDirectoryName A unique subdirectory name that will be appended to the
    484          *                               application cache directory. Usually "cache" or "images"
    485          *                               is sufficient.
    486          */
    487         public ImageCacheParams(Context context, String diskCacheDirectoryName) {
    488             diskCacheDir = getDiskCacheDir(context, diskCacheDirectoryName);
    489         }
    490 
    491         /**
    492          * Sets the memory cache size based on a percentage of the max available VM memory.
    493          * Eg. setting percent to 0.2 would set the memory cache to one fifth of the available
    494          * memory. Throws {@link IllegalArgumentException} if percent is < 0.01 or > .8.
    495          * memCacheSize is stored in kilobytes instead of bytes as this will eventually be passed
    496          * to construct a LruCache which takes an int in its constructor.
    497          *
    498          * This value should be chosen carefully based on a number of factors
    499          * Refer to the corresponding Android Training class for more discussion:
    500          * http://developer.android.com/training/displaying-bitmaps/
    501          *
    502          * @param percent Percent of available app memory to use to size memory cache
    503          */
    504         public void setMemCacheSizePercent(float percent) {
    505             if (percent < 0.01f || percent > 0.8f) {
    506                 throw new IllegalArgumentException("setMemCacheSizePercent - percent must be "
    507                         + "between 0.01 and 0.8 (inclusive)");
    508             }
    509             memCacheSize = Math.round(percent * Runtime.getRuntime().maxMemory() / 1024);
    510         }
    511     }
    512 
    513     /**
    514      * @param candidate - Bitmap to check
    515      * @param targetOptions - Options that have the out* value populated
    516      * @return true if <code>candidate</code> can be used for inBitmap re-use with
    517      *      <code>targetOptions</code>
    518      */
    519     @TargetApi(VERSION_CODES.KITKAT)
    520     private static boolean canUseForInBitmap(
    521             Bitmap candidate, BitmapFactory.Options targetOptions) {
    522         //BEGIN_INCLUDE(can_use_for_inbitmap)
    523         if (!Utils.hasKitKat()) {
    524             // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
    525             return candidate.getWidth() == targetOptions.outWidth
    526                     && candidate.getHeight() == targetOptions.outHeight
    527                     && targetOptions.inSampleSize == 1;
    528         }
    529 
    530         // From Android 4.4 (KitKat) onward we can re-use if the byte size of the new bitmap
    531         // is smaller than the reusable bitmap candidate allocation byte count.
    532         int width = targetOptions.outWidth / targetOptions.inSampleSize;
    533         int height = targetOptions.outHeight / targetOptions.inSampleSize;
    534         int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
    535         return byteCount <= candidate.getAllocationByteCount();
    536         //END_INCLUDE(can_use_for_inbitmap)
    537     }
    538 
    539     /**
    540      * Return the byte usage per pixel of a bitmap based on its configuration.
    541      * @param config The bitmap configuration.
    542      * @return The byte usage per pixel.
    543      */
    544     private static int getBytesPerPixel(Config config) {
    545         if (config == Config.ARGB_8888) {
    546             return 4;
    547         } else if (config == Config.RGB_565) {
    548             return 2;
    549         } else if (config == Config.ARGB_4444) {
    550             return 2;
    551         } else if (config == Config.ALPHA_8) {
    552             return 1;
    553         }
    554         return 1;
    555     }
    556 
    557     /**
    558      * Get a usable cache directory (external if available, internal otherwise).
    559      *
    560      * @param context The context to use
    561      * @param uniqueName A unique directory name to append to the cache dir
    562      * @return The cache dir
    563      */
    564     public static File getDiskCacheDir(Context context, String uniqueName) {
    565         // Check if media is mounted or storage is built-in, if so, try and use external cache dir
    566         // otherwise use internal cache dir
    567         final String cachePath =
    568                 Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
    569                         !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
    570                                 context.getCacheDir().getPath();
    571 
    572         return new File(cachePath + File.separator + uniqueName);
    573     }
    574 
    575     /**
    576      * A hashing method that changes a string (like a URL) into a hash suitable for using as a
    577      * disk filename.
    578      */
    579     public static String hashKeyForDisk(String key) {
    580         String cacheKey;
    581         try {
    582             final MessageDigest mDigest = MessageDigest.getInstance("MD5");
    583             mDigest.update(key.getBytes());
    584             cacheKey = bytesToHexString(mDigest.digest());
    585         } catch (NoSuchAlgorithmException e) {
    586             cacheKey = String.valueOf(key.hashCode());
    587         }
    588         return cacheKey;
    589     }
    590 
    591     private static String bytesToHexString(byte[] bytes) {
    592         // http://stackoverflow.com/questions/332079
    593         StringBuilder sb = new StringBuilder();
    594         for (int i = 0; i < bytes.length; i++) {
    595             String hex = Integer.toHexString(0xFF & bytes[i]);
    596             if (hex.length() == 1) {
    597                 sb.append('0');
    598             }
    599             sb.append(hex);
    600         }
    601         return sb.toString();
    602     }
    603 
    604     /**
    605      * Get the size in bytes of a bitmap in a BitmapDrawable. Note that from Android 4.4 (KitKat)
    606      * onward this returns the allocated memory size of the bitmap which can be larger than the
    607      * actual bitmap data byte count (in the case it was re-used).
    608      *
    609      * @param value
    610      * @return size in bytes
    611      */
    612     @TargetApi(VERSION_CODES.KITKAT)
    613     public static int getBitmapSize(BitmapDrawable value) {
    614         Bitmap bitmap = value.getBitmap();
    615 
    616         // From KitKat onward use getAllocationByteCount() as allocated bytes can potentially be
    617         // larger than bitmap byte count.
    618         if (Utils.hasKitKat()) {
    619             return bitmap.getAllocationByteCount();
    620         }
    621 
    622         if (Utils.hasHoneycombMR1()) {
    623             return bitmap.getByteCount();
    624         }
    625 
    626         // Pre HC-MR1
    627         return bitmap.getRowBytes() * bitmap.getHeight();
    628     }
    629 
    630     /**
    631      * Check if external storage is built-in or removable.
    632      *
    633      * @return True if external storage is removable (like an SD card), false
    634      *         otherwise.
    635      */
    636     @TargetApi(VERSION_CODES.GINGERBREAD)
    637     public static boolean isExternalStorageRemovable() {
    638         if (Utils.hasGingerbread()) {
    639             return Environment.isExternalStorageRemovable();
    640         }
    641         return true;
    642     }
    643 
    644     /**
    645      * Get the external app cache directory.
    646      *
    647      * @param context The context to use
    648      * @return The external cache dir
    649      */
    650     @TargetApi(VERSION_CODES.FROYO)
    651     public static File getExternalCacheDir(Context context) {
    652         if (Utils.hasFroyo()) {
    653             return context.getExternalCacheDir();
    654         }
    655 
    656         // Before Froyo we need to construct the external cache dir ourselves
    657         final String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/";
    658         return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir);
    659     }
    660 
    661     /**
    662      * Check how much usable space is available at a given path.
    663      *
    664      * @param path The path to check
    665      * @return The space available in bytes
    666      */
    667     @TargetApi(VERSION_CODES.GINGERBREAD)
    668     public static long getUsableSpace(File path) {
    669         if (Utils.hasGingerbread()) {
    670             return path.getUsableSpace();
    671         }
    672         final StatFs stats = new StatFs(path.getPath());
    673         return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
    674     }
    675 
    676     /**
    677      * Locate an existing instance of this Fragment or if not found, create and
    678      * add it using FragmentManager.
    679      *
    680      * @param fm The FragmentManager manager to use.
    681      * @return The existing instance of the Fragment or the new instance if just
    682      *         created.
    683      */
    684     private static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
    685         //BEGIN_INCLUDE(find_create_retain_fragment)
    686         // Check to see if we have retained the worker fragment.
    687         RetainFragment mRetainFragment = (RetainFragment) fm.findFragmentByTag(TAG);
    688 
    689         // If not retained (or first time running), we need to create and add it.
    690         if (mRetainFragment == null) {
    691             mRetainFragment = new RetainFragment();
    692             fm.beginTransaction().add(mRetainFragment, TAG).commitAllowingStateLoss();
    693         }
    694 
    695         return mRetainFragment;
    696         //END_INCLUDE(find_create_retain_fragment)
    697     }
    698 
    699     /**
    700      * A simple non-UI Fragment that stores a single Object and is retained over configuration
    701      * changes. It will be used to retain the ImageCache object.
    702      */
    703     public static class RetainFragment extends Fragment {
    704         private Object mObject;
    705 
    706         /**
    707          * Empty constructor as per the Fragment documentation
    708          */
    709         public RetainFragment() {}
    710 
    711         @Override
    712         public void onCreate(Bundle savedInstanceState) {
    713             super.onCreate(savedInstanceState);
    714 
    715             // Make sure this Fragment is retained over a configuration change
    716             setRetainInstance(true);
    717         }
    718 
    719         /**
    720          * Store a single object in this Fragment.
    721          *
    722          * @param object The object to store
    723          */
    724         public void setObject(Object object) {
    725             mObject = object;
    726         }
    727 
    728         /**
    729          * Get the stored object.
    730          *
    731          * @return The stored object
    732          */
    733         public Object getObject() {
    734             return mObject;
    735         }
    736     }
    737 
    738 }
    739