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
17package com.android.tv.settings.widget;
18
19import android.app.ActivityManager;
20import android.content.Context;
21import android.graphics.Bitmap;
22import android.util.Log;
23import android.util.LruCache;
24import android.widget.ImageView;
25
26import com.android.tv.settings.util.AccountImageChangeObserver;
27import com.android.tv.settings.util.UriUtils;
28import com.android.tv.settings.R;
29
30import java.lang.ref.SoftReference;
31import java.util.concurrent.Executor;
32import java.util.concurrent.Executors;
33
34/**
35 * Downloader class which loads a resource URI into an image view.
36 * <p>
37 * This class adds a cache over BitmapWorkerTask.
38 */
39public class BitmapDownloader {
40
41    private static final String TAG = "BitmapDownloader";
42
43    private static final boolean DEBUG = false;
44
45    private static final int CORE_POOL_SIZE = 5;
46
47    private static final Executor BITMAP_DOWNLOADER_THREAD_POOL_EXECUTOR =
48            Executors.newFixedThreadPool(CORE_POOL_SIZE);
49
50    // 1/4 of max memory is used for bitmap mem cache
51    private static final int MEM_TO_CACHE = 4;
52
53    // hard limit for bitmap mem cache in MB
54    private static final int CACHE_HARD_LIMIT = 32;
55
56    /**
57     * bitmap cache item structure saved in LruCache
58     */
59    private static class BitmapItem {
60        /**
61         * cached bitmap
62         */
63        Bitmap mBitmap;
64        /**
65         * indicate if the bitmap is scaled down from original source (never scale up)
66         */
67        boolean mScaled;
68
69        public BitmapItem(Bitmap bitmap, boolean scaled) {
70            mBitmap = bitmap;
71            mScaled = scaled;
72        }
73    }
74
75    private LruCache<String, BitmapItem> mMemoryCache;
76
77    private static BitmapDownloader sBitmapDownloader;
78
79    private static final Object sBitmapDownloaderLock = new Object();
80
81    // Bitmap cache also uses size of Bitmap as part of key.
82    // Bitmap cache is divided into following buckets by height:
83    // TODO: we currently care more about height, what about width in key?
84    // height <= 128, 128 < height <= 512, height > 512
85    // Different bitmap cache buckets save different bitmap cache items.
86    // Bitmaps within same bucket share the largest cache item.
87    private static final int[] SIZE_BUCKET = new int[]{128, 512, Integer.MAX_VALUE};
88
89    public static abstract class BitmapCallback {
90        SoftReference<BitmapWorkerTask> mTask;
91
92        public abstract void onBitmapRetrieved(Bitmap bitmap);
93    }
94
95    /**
96     * get the singleton BitmapDownloader for the application
97     */
98    public final static BitmapDownloader getInstance(Context context) {
99        if (sBitmapDownloader == null) {
100            synchronized(sBitmapDownloaderLock) {
101                if (sBitmapDownloader == null) {
102                    sBitmapDownloader = new BitmapDownloader(context);
103                }
104            }
105        }
106        return sBitmapDownloader;
107    }
108
109    public BitmapDownloader(Context context) {
110        int memClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE))
111                .getMemoryClass();
112        memClass = memClass / MEM_TO_CACHE;
113        if (memClass > CACHE_HARD_LIMIT) {
114            memClass = CACHE_HARD_LIMIT;
115        }
116        int cacheSize = 1024 * 1024 * memClass;
117        mMemoryCache = new LruCache<String, BitmapItem>(cacheSize) {
118            @Override
119            protected int sizeOf(String key, BitmapItem bitmap) {
120                return bitmap.mBitmap.getByteCount();
121            }
122        };
123    }
124
125    /**
126     * load bitmap in current thread, will *block* current thread.
127     * FIXME: Should avoid using this function at all cost.
128     * @deprecated
129     */
130    @Deprecated
131    public final Bitmap loadBitmapBlocking(BitmapWorkerOptions options) {
132        final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
133        Bitmap bitmap = null;
134        if (hasAccountImageUri) {
135            AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
136        } else {
137            bitmap = getBitmapFromMemCache(options);
138        }
139
140        if (bitmap == null) {
141            BitmapWorkerTask task = new BitmapWorkerTask(null) {
142                @Override
143                protected Bitmap doInBackground(BitmapWorkerOptions... params) {
144                    final Bitmap bitmap = super.doInBackground(params);
145                    if (bitmap != null && !hasAccountImageUri) {
146                        addBitmapToMemoryCache(params[0], bitmap, isScaled());
147                    }
148                    return bitmap;
149                }
150            };
151
152            return task.doInBackground(options);
153        }
154        return bitmap;
155    }
156
157    /**
158     * Loads the bitmap into the image view.
159     */
160    public void loadBitmap(BitmapWorkerOptions options, final ImageView imageView) {
161        cancelDownload(imageView);
162        final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
163        Bitmap bitmap = null;
164        if (hasAccountImageUri) {
165            AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
166        } else {
167            bitmap = getBitmapFromMemCache(options);
168        }
169
170        if (bitmap != null) {
171            imageView.setImageBitmap(bitmap);
172        } else {
173            BitmapWorkerTask task = new BitmapWorkerTask(imageView) {
174                @Override
175                protected Bitmap doInBackground(BitmapWorkerOptions... params) {
176                    Bitmap bitmap = super.doInBackground(params);
177                    if (bitmap != null && !hasAccountImageUri) {
178                        addBitmapToMemoryCache(params[0], bitmap, isScaled());
179                    }
180                    return bitmap;
181                }
182            };
183            imageView.setTag(R.id.imageDownloadTask, new SoftReference<BitmapWorkerTask>(task));
184            task.execute(options);
185        }
186    }
187
188    /**
189     * Loads the bitmap.
190     * <p>
191     * This will be sent back to the callback object.
192     */
193    public void getBitmap(BitmapWorkerOptions options, final BitmapCallback callback) {
194        cancelDownload(callback);
195        final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
196        final Bitmap bitmap = hasAccountImageUri ? null : getBitmapFromMemCache(options);
197        if (hasAccountImageUri) {
198            AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
199        }
200
201        BitmapWorkerTask task = new BitmapWorkerTask(null) {
202            @Override
203            protected Bitmap doInBackground(BitmapWorkerOptions... params) {
204                if (bitmap != null) {
205                    return bitmap;
206                }
207                final Bitmap bitmap = super.doInBackground(params);
208                if (bitmap != null && !hasAccountImageUri) {
209                    addBitmapToMemoryCache(params[0], bitmap, isScaled());
210                }
211                return bitmap;
212            }
213
214            @Override
215            protected void onPostExecute(Bitmap bitmap) {
216                callback.onBitmapRetrieved(bitmap);
217            }
218        };
219        callback.mTask = new SoftReference<BitmapWorkerTask>(task);
220        task.executeOnExecutor(BITMAP_DOWNLOADER_THREAD_POOL_EXECUTOR, options);
221    }
222
223    /**
224     * Cancel download<p>
225     * @param key {@link BitmapCallback} or {@link ImageView}
226     */
227    public boolean cancelDownload(Object key) {
228        BitmapWorkerTask task = null;
229        if (key instanceof ImageView) {
230            ImageView imageView = (ImageView)key;
231            SoftReference<BitmapWorkerTask> softReference =
232                    (SoftReference<BitmapWorkerTask>) imageView.getTag(R.id.imageDownloadTask);
233            if (softReference != null) {
234                task = softReference.get();
235                softReference.clear();
236            }
237        } else if (key instanceof BitmapCallback) {
238            BitmapCallback callback = (BitmapCallback)key;
239            if (callback.mTask != null) {
240                task = callback.mTask.get();
241                callback.mTask = null;
242            }
243        }
244        if (task != null) {
245            return task.cancel(true);
246        }
247        return false;
248    }
249
250    private static String getBucketKey(String baseKey, Bitmap.Config bitmapConfig, int width) {
251        for (int i = 0; i < SIZE_BUCKET.length; i++) {
252            if (width <= SIZE_BUCKET[i]) {
253                return new StringBuilder(baseKey.length() + 16).append(baseKey)
254                        .append(":").append(bitmapConfig == null ? "" : bitmapConfig.ordinal())
255                        .append(":").append(SIZE_BUCKET[i]).toString();
256            }
257        }
258        // should never happen because last bucket is Integer.MAX_VALUE
259        throw new RuntimeException();
260    }
261
262    private void addBitmapToMemoryCache(BitmapWorkerOptions key, Bitmap bitmap, boolean isScaled) {
263        if (!key.isMemCacheEnabled()) {
264            return;
265        }
266        String bucketKey = getBucketKey(
267                key.getCacheKey(), key.getBitmapConfig(), bitmap.getHeight());
268        BitmapItem bitmapItem = mMemoryCache.get(bucketKey);
269        if (bitmapItem != null) {
270            Bitmap currentBitmap = bitmapItem.mBitmap;
271            // If somebody else happened to get a larger one in the bucket, discard our bitmap.
272            // TODO: need a better way to prevent current downloading for the same Bitmap
273            if (currentBitmap.getWidth() >= bitmap.getWidth() && currentBitmap.getHeight()
274                    >= bitmap.getHeight()) {
275                return;
276            }
277        }
278        if (DEBUG) {
279            Log.d(TAG, "add cache "+bucketKey+" isScaled = "+isScaled);
280        }
281        bitmapItem = new BitmapItem(bitmap, isScaled);
282        mMemoryCache.put(bucketKey, bitmapItem);
283    }
284
285    private Bitmap getBitmapFromMemCache(BitmapWorkerOptions key) {
286        if (key.getHeight() != BitmapWorkerOptions.MAX_IMAGE_DIMENSION_PX) {
287            // 1. find the bitmap in the size bucket
288            String bucketKey =
289                    getBucketKey(key.getCacheKey(), key.getBitmapConfig(), key.getHeight());
290            BitmapItem bitmapItem = mMemoryCache.get(bucketKey);
291            if (bitmapItem != null) {
292                Bitmap bitmap = bitmapItem.mBitmap;
293                // now we have the bitmap in the bucket, use it when the bitmap is not scaled or
294                // if the size is larger than or equals to the output size
295                if (!bitmapItem.mScaled) {
296                    return bitmap;
297                }
298                if (bitmap.getHeight() >= key.getHeight()) {
299                    return bitmap;
300                }
301            }
302            // 2. find un-scaled bitmap in smaller buckets.  If the un-scaled bitmap exists
303            // in higher buckets,  we still need to scale it down.  Right now we just
304            // return null and let the BitmapWorkerTask to do the same job again.
305            // TODO: use the existing unscaled bitmap and we don't need to load it from resource
306            // or network again.
307            for (int i = SIZE_BUCKET.length - 1; i >= 0; i--) {
308                if (SIZE_BUCKET[i] >= key.getHeight()) {
309                    continue;
310                }
311                bucketKey = getBucketKey(key.getCacheKey(), key.getBitmapConfig(), SIZE_BUCKET[i]);
312                bitmapItem = mMemoryCache.get(bucketKey);
313                if (bitmapItem != null && !bitmapItem.mScaled) {
314                    return bitmapItem.mBitmap;
315                }
316            }
317            return null;
318        }
319        // 3. find un-scaled bitmap if size is not specified
320        for (int i = SIZE_BUCKET.length - 1; i >= 0; i--) {
321            String bucketKey =
322                    getBucketKey(key.getCacheKey(), key.getBitmapConfig(), SIZE_BUCKET[i]);
323            BitmapItem bitmapItem = mMemoryCache.get(bucketKey);
324            if (bitmapItem != null && !bitmapItem.mScaled) {
325                return bitmapItem.mBitmap;
326            }
327        }
328        return null;
329    }
330
331    public Bitmap getLargestBitmapFromMemCache(BitmapWorkerOptions key) {
332        // find largest bitmap matching the key
333        for (int i = SIZE_BUCKET.length - 1; i >= 0; i--) {
334            String bucketKey =
335                    getBucketKey(key.getCacheKey(), key.getBitmapConfig(), SIZE_BUCKET[i]);
336            BitmapItem bitmapItem = mMemoryCache.get(bucketKey);
337            if (bitmapItem != null) {
338                return bitmapItem.mBitmap;
339            }
340        }
341        return null;
342    }
343}
344