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