DisplayingBitmaps / src / com.example.android.displayingbitmaps / util /

ImageCache.java

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
}