1 /*
2  * Copyright (C) 2014 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.mediabrowserservice;
18 
19 import android.graphics.Bitmap;
20 import android.os.AsyncTask;
21 import android.util.LruCache;
22 
23 import com.example.android.mediabrowserservice.utils.BitmapHelper;
24 import com.example.android.mediabrowserservice.utils.LogHelper;
25 
26 import java.io.IOException;
27 
28 /**
29  * Implements a basic cache of album arts, with async loading support.
30  */
31 public final class AlbumArtCache {
32     private static final String TAG = LogHelper.makeLogTag(AlbumArtCache.class);
33 
34     private static final int MAX_ALBUM_ART_CACHE_SIZE = 12*1024*1024;  // 12 MB
35     private static final int MAX_ART_WIDTH = 800;  // pixels
36     private static final int MAX_ART_HEIGHT = 480;  // pixels
37 
38     // Resolution reasonable for carrying around as an icon (generally in
39     // MediaDescription.getIconBitmap). This should not be bigger than necessary, because
40     // the MediaDescription object should be lightweight. If you set it too high and try to
41     // serialize the MediaDescription, you may get FAILED BINDER TRANSACTION errors.
42     private static final int MAX_ART_WIDTH_ICON = 128;  // pixels
43     private static final int MAX_ART_HEIGHT_ICON = 128;  // pixels
44 
45     private static final int BIG_BITMAP_INDEX = 0;
46     private static final int ICON_BITMAP_INDEX = 1;
47 
48     private final LruCache<String, Bitmap[]> mCache;
49 
50     private static final AlbumArtCache sInstance = new AlbumArtCache();
51 
getInstance()52     public static AlbumArtCache getInstance() {
53         return sInstance;
54     }
55 
AlbumArtCache()56     private AlbumArtCache() {
57         // Holds no more than MAX_ALBUM_ART_CACHE_SIZE bytes, bounded by maxmemory/4 and
58         // Integer.MAX_VALUE:
59         int maxSize = Math.min(MAX_ALBUM_ART_CACHE_SIZE,
60             (int) (Math.min(Integer.MAX_VALUE, Runtime.getRuntime().maxMemory()/4)));
61         mCache = new LruCache<String, Bitmap[]>(maxSize) {
62             @Override
63             protected int sizeOf(String key, Bitmap[] value) {
64                 return value[BIG_BITMAP_INDEX].getByteCount()
65                     + value[ICON_BITMAP_INDEX].getByteCount();
66             }
67         };
68     }
69 
getBigImage(String artUrl)70     public Bitmap getBigImage(String artUrl) {
71         Bitmap[] result = mCache.get(artUrl);
72         return result == null ? null : result[BIG_BITMAP_INDEX];
73     }
74 
getIconImage(String artUrl)75     public Bitmap getIconImage(String artUrl) {
76         Bitmap[] result = mCache.get(artUrl);
77         return result == null ? null : result[ICON_BITMAP_INDEX];
78     }
79 
fetch(final String artUrl, final FetchListener listener)80     public void fetch(final String artUrl, final FetchListener listener) {
81         // WARNING: for the sake of simplicity, simultaneous multi-thread fetch requests
82         // are not handled properly: they may cause redundant costly operations, like HTTP
83         // requests and bitmap rescales. For production-level apps, we recommend you use
84         // a proper image loading library, like Glide.
85         Bitmap[] bitmap = mCache.get(artUrl);
86         if (bitmap != null) {
87             LogHelper.d(TAG, "getOrFetch: album art is in cache, using it", artUrl);
88             listener.onFetched(artUrl, bitmap[BIG_BITMAP_INDEX], bitmap[ICON_BITMAP_INDEX]);
89             return;
90         }
91         LogHelper.d(TAG, "getOrFetch: starting asynctask to fetch ", artUrl);
92 
93         new AsyncTask<Void, Void, Bitmap[]>() {
94             @Override
95             protected Bitmap[] doInBackground(Void[] objects) {
96                 Bitmap[] bitmaps;
97                 try {
98                     Bitmap bitmap = BitmapHelper.fetchAndRescaleBitmap(artUrl,
99                         MAX_ART_WIDTH, MAX_ART_HEIGHT);
100                     Bitmap icon = BitmapHelper.scaleBitmap(bitmap,
101                         MAX_ART_WIDTH_ICON, MAX_ART_HEIGHT_ICON);
102                     bitmaps = new Bitmap[] {bitmap, icon};
103                     mCache.put(artUrl, bitmaps);
104                 } catch (IOException e) {
105                     return null;
106                 }
107                 LogHelper.d(TAG, "doInBackground: putting bitmap in cache. cache size=" +
108                     mCache.size());
109                 return bitmaps;
110             }
111 
112             @Override
113             protected void onPostExecute(Bitmap[] bitmaps) {
114                 if (bitmaps == null) {
115                     listener.onError(artUrl, new IllegalArgumentException("got null bitmaps"));
116                 } else {
117                     listener.onFetched(artUrl,
118                         bitmaps[BIG_BITMAP_INDEX], bitmaps[ICON_BITMAP_INDEX]);
119                 }
120             }
121         }.execute();
122     }
123 
124     public static abstract class FetchListener {
onFetched(String artUrl, Bitmap bigImage, Bitmap iconImage)125         public abstract void onFetched(String artUrl, Bitmap bigImage, Bitmap iconImage);
onError(String artUrl, Exception e)126         public void onError(String artUrl, Exception e) {
127             LogHelper.e(TAG, e, "AlbumArtFetchListener: error while downloading " + artUrl);
128         }
129     }
130 }
131