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