OSDN Git Service

Automatic translation import
[android-x86/packages-apps-Eleven.git] / src / com / cyngn / eleven / cache / ImageCache.java
1 /*
2  * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
3  * (the "License"); you may not use this file except in compliance with the
4  * License. You may obtain a copy of the License at
5  * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
6  * or agreed to in writing, software distributed under the License is
7  * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
8  * KIND, either express or implied. See the License for the specific language
9  * governing permissions and limitations under the License.
10  */
11
12 package com.cyngn.eleven.cache;
13
14 import android.annotation.SuppressLint;
15 import android.app.Activity;
16 import android.app.ActivityManager;
17 import android.app.Fragment;
18 import android.app.FragmentManager;
19 import android.content.ComponentCallbacks2;
20 import android.content.ContentUris;
21 import android.content.Context;
22 import android.content.res.Configuration;
23 import android.graphics.Bitmap;
24 import android.graphics.Bitmap.CompressFormat;
25 import android.graphics.BitmapFactory;
26 import android.net.Uri;
27 import android.os.AsyncTask;
28 import android.os.Build;
29 import android.os.Bundle;
30 import android.os.Environment;
31 import android.os.Looper;
32 import android.os.ParcelFileDescriptor;
33 import android.text.TextUtils;
34 import android.util.Log;
35
36 import com.cyngn.eleven.utils.ApolloUtils;
37
38 import java.io.File;
39 import java.io.FileDescriptor;
40 import java.io.FileNotFoundException;
41 import java.io.IOException;
42 import java.io.InputStream;
43 import java.io.OutputStream;
44 import java.security.MessageDigest;
45 import java.security.NoSuchAlgorithmException;
46
47 /**
48  * This class holds the memory and disk bitmap caches.
49  */
50 public final class ImageCache {
51
52     private static final String TAG = ImageCache.class.getSimpleName();
53
54     /**
55      * The {@link Uri} used to retrieve album art
56      */
57     private static final Uri mArtworkUri;
58
59     /**
60      * Default memory cache size as a percent of device memory class
61      */
62     private static final float MEM_CACHE_DIVIDER = 0.25f;
63
64     /**
65      * Default disk cache size 10MB
66      */
67     private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10;
68
69     /**
70      * Compression settings when writing images to disk cache
71      */
72     private static final CompressFormat COMPRESS_FORMAT = CompressFormat.JPEG;
73
74     /**
75      * Disk cache index to read from
76      */
77     private static final int DISK_CACHE_INDEX = 0;
78
79     /**
80      * Image compression quality
81      */
82     private static final int COMPRESS_QUALITY = 98;
83
84     /**
85      * LRU cache
86      */
87     private MemoryCache mLruCache;
88
89     /**
90      * Disk LRU cache
91      */
92     private DiskLruCache mDiskCache;
93
94     private static ImageCache sInstance;
95
96     /**
97      * Used to temporarily pause the disk cache while scrolling
98      */
99     public boolean mPauseDiskAccess = false;
100     private Object mPauseLock = new Object();
101
102     static {
103         mArtworkUri = Uri.parse("content://media/external/audio/albumart");
104     }
105
106     /**
107      * Constructor of <code>ImageCache</code>
108      *
109      * @param context The {@link Context} to use
110      */
111     public ImageCache(final Context context) {
112         init(context);
113     }
114
115     /**
116      * Used to create a singleton of {@link ImageCache}
117      *
118      * @param context The {@link Context} to use
119      * @return A new instance of this class.
120      */
121     public final static ImageCache getInstance(final Context context) {
122         if (sInstance == null) {
123             sInstance = new ImageCache(context.getApplicationContext());
124         }
125         return sInstance;
126     }
127
128     /**
129      * Initialize the cache, providing all parameters.
130      *
131      * @param context The {@link Context} to use
132      * @param cacheParams The cache parameters to initialize the cache
133      */
134     private void init(final Context context) {
135         ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
136
137             @Override
138             protected Void doInBackground(final Void... unused) {
139                 // Initialize the disk cahe in a background thread
140                 initDiskCache(context);
141                 return null;
142             }
143         }, (Void[])null);
144         // Set up the memory cache
145         initLruCache(context);
146     }
147
148     /**
149      * Initializes the disk cache. Note that this includes disk access so this
150      * should not be executed on the main/UI thread. By default an ImageCache
151      * does not initialize the disk cache when it is created, instead you should
152      * call initDiskCache() to initialize it on a background thread.
153      *
154      * @param context The {@link Context} to use
155      */
156     private synchronized void initDiskCache(final Context context) {
157         // Set up disk cache
158         if (mDiskCache == null || mDiskCache.isClosed()) {
159             File diskCacheDir = getDiskCacheDir(context, TAG);
160             if (diskCacheDir != null) {
161                 if (!diskCacheDir.exists()) {
162                     diskCacheDir.mkdirs();
163                 }
164                 if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
165                     try {
166                         mDiskCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
167                     } catch (final IOException e) {
168                         diskCacheDir = null;
169                     }
170                 }
171             }
172         }
173     }
174
175     /**
176      * Sets up the Lru cache
177      *
178      * @param context The {@link Context} to use
179      */
180     @SuppressLint("NewApi")
181     public void initLruCache(final Context context) {
182         final ActivityManager activityManager = (ActivityManager)context
183                 .getSystemService(Context.ACTIVITY_SERVICE);
184         final int lruCacheSize = Math.round(MEM_CACHE_DIVIDER * activityManager.getMemoryClass()
185                 * 1024 * 1024);
186         mLruCache = new MemoryCache(lruCacheSize);
187
188         // Release some memory as needed
189         context.registerComponentCallbacks(new ComponentCallbacks2() {
190
191             /**
192              * {@inheritDoc}
193              */
194             @Override
195             public void onTrimMemory(final int level) {
196                 if (level >= TRIM_MEMORY_MODERATE) {
197                     evictAll();
198                 } else if (level >= TRIM_MEMORY_BACKGROUND) {
199                     mLruCache.trimToSize(mLruCache.size() / 2);
200                 }
201             }
202
203             /**
204              * {@inheritDoc}
205              */
206             @Override
207             public void onLowMemory() {
208                 // Nothing to do
209             }
210
211             /**
212              * {@inheritDoc}
213              */
214             @Override
215             public void onConfigurationChanged(final Configuration newConfig) {
216                 // Nothing to do
217             }
218         });
219     }
220
221     /**
222      * Find and return an existing ImageCache stored in a {@link RetainFragment}
223      * , if not found a new one is created using the supplied params and saved
224      * to a {@link RetainFragment}
225      *
226      * @param activity The calling {@link FragmentActivity}
227      * @return An existing retained ImageCache object or a new one if one did
228      *         not exist
229      */
230     public static final ImageCache findOrCreateCache(final Activity activity) {
231
232         // Search for, or create an instance of the non-UI RetainFragment
233         final RetainFragment retainFragment = findOrCreateRetainFragment(
234                 activity.getFragmentManager());
235
236         // See if we already have an ImageCache stored in RetainFragment
237         ImageCache cache = (ImageCache)retainFragment.getObject();
238
239         // No existing ImageCache, create one and store it in RetainFragment
240         if (cache == null) {
241             cache = getInstance(activity);
242             retainFragment.setObject(cache);
243         }
244         return cache;
245     }
246
247     /**
248      * Locate an existing instance of this {@link Fragment} or if not found,
249      * create and add it using {@link FragmentManager}
250      *
251      * @param fm The {@link FragmentManager} to use
252      * @return The existing instance of the {@link Fragment} or the new instance
253      *         if just created
254      */
255     public static final RetainFragment findOrCreateRetainFragment(final FragmentManager fm) {
256         // Check to see if we have retained the worker fragment
257         RetainFragment retainFragment = (RetainFragment)fm.findFragmentByTag(TAG);
258
259         // If not retained, we need to create and add it
260         if (retainFragment == null) {
261             retainFragment = new RetainFragment();
262             fm.beginTransaction().add(retainFragment, TAG).commit();
263         }
264         return retainFragment;
265     }
266
267     /**
268      * Adds a new image to the memory and disk caches
269      *
270      * @param data The key used to store the image
271      * @param bitmap The {@link Bitmap} to cache
272      */
273     public void addBitmapToCache(final String data, final Bitmap bitmap) {
274         if (data == null || bitmap == null) {
275             return;
276         }
277
278         // Add to memory cache
279         addBitmapToMemCache(data, bitmap);
280
281         // Add to disk cache
282         if (mDiskCache != null) {
283             final String key = hashKeyForDisk(data);
284             OutputStream out = null;
285             try {
286                 final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
287                 if (snapshot == null) {
288                     final DiskLruCache.Editor editor = mDiskCache.edit(key);
289                     if (editor != null) {
290                         out = editor.newOutputStream(DISK_CACHE_INDEX);
291                         bitmap.compress(COMPRESS_FORMAT, COMPRESS_QUALITY, out);
292                         editor.commit();
293                         out.close();
294                         flush();
295                     }
296                 } else {
297                     snapshot.getInputStream(DISK_CACHE_INDEX).close();
298                 }
299             } catch (final IOException e) {
300                 Log.e(TAG, "addBitmapToCache - " + e);
301             } finally {
302                 try {
303                     if (out != null) {
304                         out.close();
305                         out = null;
306                     }
307                 } catch (final IOException e) {
308                     Log.e(TAG, "addBitmapToCache - " + e);
309                 } catch (final IllegalStateException e) {
310                     Log.e(TAG, "addBitmapToCache - " + e);
311                 }
312             }
313         }
314     }
315
316     /**
317      * Called to add a new image to the memory cache
318      *
319      * @param data The key identifier
320      * @param bitmap The {@link Bitmap} to cache
321      */
322     public void addBitmapToMemCache(final String data, final Bitmap bitmap) {
323         if (data == null || bitmap == null) {
324             return;
325         }
326         // Add to memory cache
327         if (getBitmapFromMemCache(data) == null) {
328             mLruCache.put(data, bitmap);
329         }
330     }
331
332     /**
333      * Fetches a cached image from the memory cache
334      *
335      * @param data Unique identifier for which item to get
336      * @return The {@link Bitmap} if found in cache, null otherwise
337      */
338     public final Bitmap getBitmapFromMemCache(final String data) {
339         if (data == null) {
340             return null;
341         }
342         if (mLruCache != null) {
343             final Bitmap lruBitmap = mLruCache.get(data);
344             if (lruBitmap != null) {
345                 return lruBitmap;
346             }
347         }
348         return null;
349     }
350
351     /**
352      * Fetches a cached image from the disk cache
353      *
354      * @param data Unique identifier for which item to get
355      * @return The {@link Bitmap} if found in cache, null otherwise
356      */
357     public final Bitmap getBitmapFromDiskCache(final String data) {
358         if (data == null) {
359             return null;
360         }
361
362         // Check in the memory cache here to avoid going to the disk cache less
363         // often
364         if (getBitmapFromMemCache(data) != null) {
365             return getBitmapFromMemCache(data);
366         }
367
368         waitUntilUnpaused();
369         final String key = hashKeyForDisk(data);
370         if (mDiskCache != null) {
371             InputStream inputStream = null;
372             try {
373                 final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
374                 if (snapshot != null) {
375                     inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
376                     if (inputStream != null) {
377                         final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
378                         if (bitmap != null) {
379                             return bitmap;
380                         }
381                     }
382                 }
383             } catch (final IOException e) {
384                 Log.e(TAG, "getBitmapFromDiskCache - " + e);
385             } finally {
386                 try {
387                     if (inputStream != null) {
388                         inputStream.close();
389                     }
390                 } catch (final IOException e) {
391                 }
392             }
393         }
394         return null;
395     }
396
397     /**
398      * Tries to return a cached image from memory cache before fetching from the
399      * disk cache
400      *
401      * @param data Unique identifier for which item to get
402      * @return The {@link Bitmap} if found in cache, null otherwise
403      */
404     public final Bitmap getCachedBitmap(final String data) {
405         if (data == null) {
406             return null;
407         }
408         Bitmap cachedImage = getBitmapFromMemCache(data);
409         if (cachedImage == null) {
410             cachedImage = getBitmapFromDiskCache(data);
411         }
412         if (cachedImage != null) {
413             addBitmapToMemCache(data, cachedImage);
414             return cachedImage;
415         }
416         return null;
417     }
418
419     /**
420      * Tries to return the album art from memory cache and disk cache, before
421      * calling {@code #getArtworkFromFile(Context, String)} again
422      *
423      * @param context The {@link Context} to use
424      * @param data The name of the album art
425      * @param id The ID of the album to find artwork for
426      * @return The artwork for an album
427      */
428     public final Bitmap getCachedArtwork(final Context context, final String data, final long id) {
429         if (context == null || data == null) {
430             return null;
431         }
432         Bitmap cachedImage = getCachedBitmap(data);
433         if (cachedImage == null && id >= 0) {
434             cachedImage = getArtworkFromFile(context, id);
435         }
436         if (cachedImage != null) {
437             addBitmapToMemCache(data, cachedImage);
438             return cachedImage;
439         }
440         return null;
441     }
442
443     /**
444      * Used to fetch the artwork for an album locally from the user's device
445      *
446      * @param context The {@link Context} to use
447      * @param albumID The ID of the album to find artwork for
448      * @return The artwork for an album
449      */
450     public final Bitmap getArtworkFromFile(final Context context, final long albumId) {
451         if (albumId < 0) {
452             return null;
453         }
454         Bitmap artwork = null;
455         waitUntilUnpaused();
456         try {
457             final Uri uri = ContentUris.withAppendedId(mArtworkUri, albumId);
458             final ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver()
459                     .openFileDescriptor(uri, "r");
460             if (parcelFileDescriptor != null) {
461                 final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
462                 artwork = BitmapFactory.decodeFileDescriptor(fileDescriptor);
463             }
464         } catch (final IllegalStateException e) {
465             // Log.e(TAG, "IllegalStateExcetpion - getArtworkFromFile - ", e);
466         } catch (final FileNotFoundException e) {
467             // Log.e(TAG, "FileNotFoundException - getArtworkFromFile - ", e);
468         } catch (final OutOfMemoryError evict) {
469             // Log.e(TAG, "OutOfMemoryError - getArtworkFromFile - ", evict);
470             evictAll();
471         }
472         return artwork;
473     }
474
475     /**
476      * flush() is called to synchronize up other methods that are accessing the
477      * cache first
478      */
479     public void flush() {
480         ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
481
482             @Override
483             protected Void doInBackground(final Void... unused) {
484                 if (mDiskCache != null) {
485                     try {
486                         if (!mDiskCache.isClosed()) {
487                             mDiskCache.flush();
488                         }
489                     } catch (final IOException e) {
490                         Log.e(TAG, "flush - " + e);
491                     }
492                 }
493                 return null;
494             }
495         }, (Void[])null);
496     }
497
498     /**
499      * Clears the disk and memory caches
500      */
501     public void clearCaches() {
502         ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
503
504             @Override
505             protected Void doInBackground(final Void... unused) {
506                 // Clear the disk cache
507                 try {
508                     if (mDiskCache != null) {
509                         mDiskCache.delete();
510                         mDiskCache = null;
511                     }
512                 } catch (final IOException e) {
513                     Log.e(TAG, "clearCaches - " + e);
514                 }
515                 // Clear the memory cache
516                 evictAll();
517                 return null;
518             }
519         }, (Void[])null);
520     }
521
522     /**
523      * Closes the disk cache associated with this ImageCache object. Note that
524      * this includes disk access so this should not be executed on the main/UI
525      * thread.
526      */
527     public void close() {
528         ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
529
530             @Override
531             protected Void doInBackground(final Void... unused) {
532                 if (mDiskCache != null) {
533                     try {
534                         if (!mDiskCache.isClosed()) {
535                             mDiskCache.close();
536                             mDiskCache = null;
537                         }
538                     } catch (final IOException e) {
539                         Log.e(TAG, "close - " + e);
540                     }
541                 }
542                 return null;
543             }
544         }, (Void[])null);
545     }
546
547     /**
548      * Evicts all of the items from the memory cache and lets the system know
549      * now would be a good time to garbage collect
550      */
551     public void evictAll() {
552         if (mLruCache != null) {
553             mLruCache.evictAll();
554         }
555         System.gc();
556     }
557
558     /**
559      * @param key The key used to identify which cache entries to delete.
560      */
561     public void removeFromCache(final String key) {
562         if (key == null) {
563             return;
564         }
565         // Remove the Lru entry
566         if (mLruCache != null) {
567             mLruCache.remove(key);
568         }
569
570         try {
571             // Remove the disk entry
572             if (mDiskCache != null) {
573                 mDiskCache.remove(hashKeyForDisk(key));
574             }
575         } catch (final IOException e) {
576             Log.e(TAG, "remove - " + e);
577         }
578         flush();
579     }
580
581     /**
582      * Used to temporarily pause the disk cache while the user is scrolling to
583      * improve scrolling.
584      *
585      * @param pause True to temporarily pause the disk cache, false otherwise.
586      */
587     public void setPauseDiskCache(final boolean pause) {
588         synchronized (mPauseLock) {
589             if (mPauseDiskAccess != pause) {
590                 mPauseDiskAccess = pause;
591                 if (!pause) {
592                     mPauseLock.notify();
593                 }
594             }
595         }
596     }
597
598     private void waitUntilUnpaused() {
599         synchronized (mPauseLock) {
600             if (Looper.myLooper() != Looper.getMainLooper()) {
601                 while (mPauseDiskAccess) {
602                     try {
603                         mPauseLock.wait();
604                     } catch (InterruptedException e) {
605                         // ignored, we'll start waiting again
606                     }
607                 }
608             }
609         }
610     }
611
612     /**
613      * @return True if the user is scrolling, false otherwise.
614      */
615     public boolean isDiskCachePaused() {
616         return mPauseDiskAccess;
617     }
618
619     /**
620      * Get a usable cache directory (external if available, internal otherwise)
621      *
622      * @param context The {@link Context} to use
623      * @param uniqueName A unique directory name to append to the cache
624      *            directory
625      * @return The cache directory
626      */
627     public static final File getDiskCacheDir(final Context context, final String uniqueName) {
628         // getExternalCacheDir(context) returns null if external storage is not ready
629         final String cachePath = getExternalCacheDir(context) != null
630                                     ? getExternalCacheDir(context).getPath()
631                                     : context.getCacheDir().getPath();
632         return new File(cachePath, uniqueName);
633     }
634
635     /**
636      * Check if external storage is built-in or removable
637      *
638      * @return True if external storage is removable (like an SD card), false
639      *         otherwise
640      */
641     public static final boolean isExternalStorageRemovable() {
642         return Environment.isExternalStorageRemovable();
643     }
644
645     /**
646      * Get the external app cache directory
647      *
648      * @param context The {@link Context} to use
649      * @return The external cache directory
650      */
651     public static final File getExternalCacheDir(final Context context) {
652         return context.getExternalCacheDir();
653     }
654
655     /**
656      * Check how much usable space is available at a given path.
657      *
658      * @param path The path to check
659      * @return The space available in bytes
660      */
661     public static final long getUsableSpace(final File path) {
662         return path.getUsableSpace();
663     }
664
665     /**
666      * A hashing method that changes a string (like a URL) into a hash suitable
667      * for using as a disk filename.
668      *
669      * @param key The key used to store the file
670      */
671     public static final String hashKeyForDisk(final String key) {
672         String cacheKey;
673         try {
674             final MessageDigest digest = MessageDigest.getInstance("MD5");
675             digest.update(key.getBytes());
676             cacheKey = bytesToHexString(digest.digest());
677         } catch (final NoSuchAlgorithmException e) {
678             cacheKey = String.valueOf(key.hashCode());
679         }
680         return cacheKey;
681     }
682
683     /**
684      * http://stackoverflow.com/questions/332079
685      *
686      * @param bytes The bytes to convert.
687      * @return A {@link String} converted from the bytes of a hashable key used
688      *         to store a filename on the disk, to hex digits.
689      */
690     private static final String bytesToHexString(final byte[] bytes) {
691         final StringBuilder builder = new StringBuilder();
692         for (final byte b : bytes) {
693             final String hex = Integer.toHexString(0xFF & b);
694             if (hex.length() == 1) {
695                 builder.append('0');
696             }
697             builder.append(hex);
698         }
699         return builder.toString();
700     }
701
702     /**
703      * A simple non-UI Fragment that stores a single Object and is retained over
704      * configuration changes. In this sample it will be used to retain an
705      * {@link ImageCache} object.
706      */
707     public static final class RetainFragment extends Fragment {
708
709         /**
710          * The object to be stored
711          */
712         private Object mObject;
713
714         /**
715          * Empty constructor as per the {@link Fragment} documentation
716          */
717         public RetainFragment() {
718         }
719
720         /**
721          * {@inheritDoc}
722          */
723         @Override
724         public void onCreate(final Bundle savedInstanceState) {
725             super.onCreate(savedInstanceState);
726             // Make sure this Fragment is retained over a configuration change
727             setRetainInstance(true);
728         }
729
730         /**
731          * Store a single object in this {@link Fragment}
732          *
733          * @param object The object to store
734          */
735         public void setObject(final Object object) {
736             mObject = object;
737         }
738
739         /**
740          * Get the stored object
741          *
742          * @return The stored object
743          */
744         public Object getObject() {
745             return mObject;
746         }
747     }
748
749     /**
750      * Used to cache images via {@link LruCache}.
751      */
752     public static final class MemoryCache extends LruCache<String, Bitmap> {
753
754         /**
755          * Constructor of <code>MemoryCache</code>
756          *
757          * @param maxSize The allowed size of the {@link LruCache}
758          */
759         public MemoryCache(final int maxSize) {
760             super(maxSize);
761         }
762
763         /**
764          * Get the size in bytes of a bitmap.
765          */
766         public static final int getBitmapSize(final Bitmap bitmap) {
767             return bitmap.getByteCount();
768         }
769
770         /**
771          * {@inheritDoc}
772          */
773         @Override
774         protected int sizeOf(final String paramString, final Bitmap paramBitmap) {
775             return getBitmapSize(paramBitmap);
776         }
777
778     }
779
780 }