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