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 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 } 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) { 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 } 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) { 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; 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) { 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 } 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) { 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; 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) { 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(); 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) { 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; 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 }