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.content.Intent.ShortcutIconResource;
22import android.content.pm.PackageManager.NameNotFoundException;
23import android.graphics.Bitmap;
24import android.graphics.drawable.BitmapDrawable;
25import android.graphics.drawable.Drawable;
26import android.util.Log;
27import android.util.LruCache;
28import android.widget.ImageView;
29
30import com.android.tv.settings.util.UriUtils;
31import com.android.tv.settings.util.AccountImageChangeObserver;
32import com.android.tv.settings.R;
33
34import java.lang.ref.SoftReference;
35import java.util.ArrayList;
36import java.util.concurrent.Executor;
37import java.util.concurrent.Executors;
38
39/**
40 * Downloader class which loads a resource URI into an image view or triggers a callback
41 * <p>
42 * This class adds a LRU cache over DrawableLoader.
43 * <p>
44 * Calling getBitmap() or loadBitmap() will return a RefcountBitmapDrawable with initial refcount =
45 * 2 by the cache table and by caller.  You must call releaseRef() when you are done with the resource.
46 * The most common way is using RefcountImageView, and releaseRef() for you.  Once both RefcountImageView
47 * and LRUCache removes the refcount, the underlying bitmap will be used for decoding new bitmap.
48 * <p>
49 * If the URI does not point to a bitmap (e.g. point to a drawable xml, we won't cache it and we
50 * directly return a regular Drawable).
51 */
52public class DrawableDownloader {
53
54    private static final String TAG = "DrawableDownloader";
55
56    private static final boolean DEBUG = false;
57
58    private static final int CORE_POOL_SIZE = 5;
59
60    // thread pool for loading non android-resources such as http,  content
61    private static final Executor BITMAP_DOWNLOADER_THREAD_POOL_EXECUTOR =
62            Executors.newFixedThreadPool(CORE_POOL_SIZE);
63
64    private static final int CORE_RESOURCE_POOL_SIZE = 1;
65
66    // thread pool for loading android resources,  we use separate thread pool so
67    // that network loading will not block local android icons
68    private static final Executor BITMAP_RESOURCE_DOWNLOADER_THREAD_POOL_EXECUTOR =
69            Executors.newFixedThreadPool(CORE_RESOURCE_POOL_SIZE);
70
71    // 1/4 of max memory is used for bitmap mem cache
72    private static final int MEM_TO_CACHE = 4;
73
74    // hard limit for bitmap mem cache in MB
75    private static final int CACHE_HARD_LIMIT = 32;
76
77    /**
78     * bitmap cache item structure saved in LruCache
79     */
80    private static class BitmapItem {
81        int mOriginalWidth;
82        int mOriginalHeight;
83        ArrayList<BitmapDrawable> mBitmaps = new ArrayList<BitmapDrawable>(3);
84        int mByteCount;
85        public BitmapItem(int originalWidth, int originalHeight) {
86            mOriginalWidth = originalWidth;
87            mOriginalHeight = originalHeight;
88        }
89
90        // get bitmap from the list
91        BitmapDrawable findDrawable(BitmapWorkerOptions options) {
92            for (int i = 0, c = mBitmaps.size(); i < c; i++) {
93                BitmapDrawable d = mBitmaps.get(i);
94                // use drawable with original size
95                if (d.getIntrinsicWidth() == mOriginalWidth
96                        && d.getIntrinsicHeight() == mOriginalHeight) {
97                    return d;
98                }
99                // if specified width/height in options and is smaller than
100                // cached one, we can use this cached drawable
101                if (options.getHeight() != BitmapWorkerOptions.MAX_IMAGE_DIMENSION_PX) {
102                    if (options.getHeight() <= d.getIntrinsicHeight()) {
103                        return d;
104                    }
105                } else if (options.getWidth() != BitmapWorkerOptions.MAX_IMAGE_DIMENSION_PX) {
106                    if (options.getWidth() <= d.getIntrinsicWidth()) {
107                        return d;
108                    }
109                }
110            }
111            return null;
112        }
113
114        BitmapDrawable findLargestDrawable(BitmapWorkerOptions options) {
115            return mBitmaps.size() == 0 ? null : mBitmaps.get(0);
116        }
117
118        void addDrawable(BitmapDrawable d) {
119            int i = 0, c = mBitmaps.size();
120            for (; i < c; i++) {
121                BitmapDrawable drawable = mBitmaps.get(i);
122                if (drawable.getIntrinsicHeight() < d.getIntrinsicHeight()) {
123                    break;
124                }
125            }
126            mBitmaps.add(i, d);
127            mByteCount += RecycleBitmapPool.getSize(d.getBitmap());
128        }
129
130        void clear() {
131            for (int i = 0, c = mBitmaps.size(); i < c; i++) {
132                BitmapDrawable d = mBitmaps.get(i);
133                if (d instanceof RefcountBitmapDrawable) {
134                    ((RefcountBitmapDrawable) d).getRefcountObject().releaseRef();
135                }
136            }
137            mBitmaps.clear();
138            mByteCount = 0;
139        }
140    }
141
142    public static abstract class BitmapCallback {
143        SoftReference<DrawableLoader> mTask;
144
145        public abstract void onBitmapRetrieved(Drawable bitmap);
146    }
147
148    private Context mContext;
149    private LruCache<String, BitmapItem> mMemoryCache;
150    private RecycleBitmapPool mRecycledBitmaps;
151
152    private static DrawableDownloader sBitmapDownloader;
153
154    private static final Object sBitmapDownloaderLock = new Object();
155
156    /**
157     * get the singleton BitmapDownloader for the application
158     */
159    public final static DrawableDownloader getInstance(Context context) {
160        if (sBitmapDownloader == null) {
161            synchronized(sBitmapDownloaderLock) {
162                if (sBitmapDownloader == null) {
163                    sBitmapDownloader = new DrawableDownloader(context);
164                }
165            }
166        }
167        return sBitmapDownloader;
168    }
169
170    private static String getBucketKey(String baseKey, Bitmap.Config bitmapConfig) {
171        return new StringBuilder(baseKey.length() + 16).append(baseKey)
172                         .append(":").append(bitmapConfig == null ? "" : bitmapConfig.ordinal())
173                         .toString();
174     }
175
176    public static Drawable getDrawable(Context context, ShortcutIconResource iconResource)
177            throws NameNotFoundException {
178        return DrawableLoader.getDrawable(context, iconResource);
179    }
180
181    private DrawableDownloader(Context context) {
182        mContext = context;
183        int memClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE))
184                .getMemoryClass();
185        memClass = memClass / MEM_TO_CACHE;
186        if (memClass > CACHE_HARD_LIMIT) {
187            memClass = CACHE_HARD_LIMIT;
188        }
189        int cacheSize = 1024 * 1024 * memClass;
190        mMemoryCache = new LruCache<String, BitmapItem>(cacheSize) {
191            @Override
192            protected int sizeOf(String key, BitmapItem bitmap) {
193                return bitmap.mByteCount;
194            }
195            @Override
196            protected void entryRemoved(
197                    boolean evicted, String key, BitmapItem oldValue, BitmapItem newValue) {
198                if (evicted) {
199                    oldValue.clear();
200                }
201            }
202        };
203        mRecycledBitmaps = new RecycleBitmapPool();
204    }
205
206    /**
207     * trim memory cache to 0~1 * maxSize
208     */
209    public void trimTo(float amount) {
210        if (amount == 0f) {
211            mMemoryCache.evictAll();
212        } else {
213            mMemoryCache.trimToSize((int) (amount * mMemoryCache.maxSize()));
214        }
215    }
216
217    /**
218     * load bitmap in current thread, will *block* current thread.
219     * FIXME: Should avoid using this function at all cost.
220     * @deprecated
221     */
222    @Deprecated
223    public final Drawable loadBitmapBlocking(BitmapWorkerOptions options) {
224        final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
225        Drawable bitmap = null;
226        if (hasAccountImageUri) {
227            AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
228        } else {
229            bitmap = getBitmapFromMemCache(options);
230        }
231
232        if (bitmap == null) {
233            DrawableLoader task = new DrawableLoader(null, mRecycledBitmaps) {
234                @Override
235                protected Drawable doInBackground(BitmapWorkerOptions... params) {
236                    final Drawable bitmap = super.doInBackground(params);
237                    if (bitmap != null && !hasAccountImageUri) {
238                        addBitmapToMemoryCache(params[0], bitmap, this);
239                    }
240                    return bitmap;
241                }
242            };
243            return task.doInBackground(options);
244        }
245        return bitmap;
246    }
247
248    /**
249     * Loads the bitmap into the image view.
250     */
251    public void loadBitmap(BitmapWorkerOptions options, final ImageView imageView) {
252        cancelDownload(imageView);
253        final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
254        Drawable bitmap = null;
255        if (hasAccountImageUri) {
256            AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
257        } else {
258            bitmap = getBitmapFromMemCache(options);
259        }
260
261        if (bitmap != null) {
262            imageView.setImageDrawable(bitmap);
263        } else {
264            DrawableLoader task = new DrawableLoader(imageView, mRecycledBitmaps) {
265                @Override
266                protected Drawable doInBackground(BitmapWorkerOptions... params) {
267                    Drawable bitmap = super.doInBackground(params);
268                    if (bitmap != null && !hasAccountImageUri) {
269                        addBitmapToMemoryCache(params[0], bitmap, this);
270                    }
271                    return bitmap;
272                }
273            };
274            imageView.setTag(R.id.imageDownloadTask, new SoftReference<DrawableLoader>(task));
275            scheduleTask(task, options);
276        }
277    }
278
279    /**
280     * Loads the bitmap.
281     * <p>
282     * This will be sent back to the callback object.
283     */
284    public void getBitmap(BitmapWorkerOptions options, final BitmapCallback callback) {
285        cancelDownload(callback);
286        final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
287        final Drawable bitmap = hasAccountImageUri ? null : getBitmapFromMemCache(options);
288        if (hasAccountImageUri) {
289            AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
290        }
291
292        if (bitmap != null) {
293            callback.onBitmapRetrieved(bitmap);
294            return;
295        }
296        DrawableLoader task = new DrawableLoader(null, mRecycledBitmaps) {
297            @Override
298            protected Drawable doInBackground(BitmapWorkerOptions... params) {
299                final Drawable bitmap = super.doInBackground(params);
300                if (bitmap != null && !hasAccountImageUri) {
301                    addBitmapToMemoryCache(params[0], bitmap, this);
302                }
303                return bitmap;
304            }
305
306            @Override
307            protected void onPostExecute(Drawable bitmap) {
308                callback.onBitmapRetrieved(bitmap);
309            }
310        };
311        callback.mTask = new SoftReference<DrawableLoader>(task);
312        scheduleTask(task, options);
313    }
314
315    private static void scheduleTask(DrawableLoader task, BitmapWorkerOptions options) {
316        if (options.isFromResource()) {
317            task.executeOnExecutor(BITMAP_RESOURCE_DOWNLOADER_THREAD_POOL_EXECUTOR, options);
318        } else {
319            task.executeOnExecutor(BITMAP_DOWNLOADER_THREAD_POOL_EXECUTOR, options);
320        }
321    }
322
323    /**
324     * Cancel download<p>
325     * @param key {@link BitmapCallback} or {@link ImageView}
326     */
327    public boolean cancelDownload(Object key) {
328        DrawableLoader task = null;
329        if (key instanceof ImageView) {
330            ImageView imageView = (ImageView)key;
331            SoftReference<DrawableLoader> softReference =
332                    (SoftReference<DrawableLoader>) imageView.getTag(R.id.imageDownloadTask);
333            if (softReference != null) {
334                task = softReference.get();
335                softReference.clear();
336            }
337        } else if (key instanceof BitmapCallback) {
338            BitmapCallback callback = (BitmapCallback)key;
339            if (callback.mTask != null) {
340                task = callback.mTask.get();
341                callback.mTask = null;
342            }
343        }
344        if (task != null) {
345            return task.cancel(true);
346        }
347        return false;
348    }
349
350    private void addBitmapToMemoryCache(BitmapWorkerOptions key, Drawable bitmap,
351            DrawableLoader loader) {
352        if (!key.isMemCacheEnabled()) {
353            return;
354        }
355        if (!(bitmap instanceof BitmapDrawable)) {
356            return;
357        }
358        String bucketKey = getBucketKey(key.getCacheKey(), key.getBitmapConfig());
359        BitmapItem bitmapItem = mMemoryCache.get(bucketKey);
360        if (DEBUG) {
361            Log.d(TAG, "add cache "+bucketKey);
362        }
363        if (bitmapItem != null) {
364            // remove and re-add to update size
365            mMemoryCache.remove(bucketKey);
366        } else {
367            bitmapItem = new BitmapItem(loader.getOriginalWidth(), loader.getOriginalHeight());
368        }
369        if (bitmap instanceof RefcountBitmapDrawable) {
370            RefcountBitmapDrawable refcountDrawable = (RefcountBitmapDrawable) bitmap;
371            refcountDrawable.getRefcountObject().addRef();
372        }
373        bitmapItem.addDrawable((BitmapDrawable) bitmap);
374        mMemoryCache.put(bucketKey, bitmapItem);
375    }
376
377    private Drawable getBitmapFromMemCache(BitmapWorkerOptions key) {
378        String bucketKey =
379                getBucketKey(key.getCacheKey(), key.getBitmapConfig());
380        BitmapItem item = mMemoryCache.get(bucketKey);
381        if (item != null) {
382            return createRefCopy(item.findDrawable(key));
383        }
384        return null;
385    }
386
387    public BitmapDrawable getLargestBitmapFromMemCache(BitmapWorkerOptions key) {
388        String bucketKey =
389                getBucketKey(key.getCacheKey(), key.getBitmapConfig());
390        BitmapItem item = mMemoryCache.get(bucketKey);
391        if (item != null) {
392            return (BitmapDrawable) createRefCopy(item.findLargestDrawable(key));
393        }
394        return null;
395    }
396
397    private Drawable createRefCopy(Drawable d) {
398        if (d != null) {
399            if (d instanceof RefcountBitmapDrawable) {
400                RefcountBitmapDrawable refcountDrawable = (RefcountBitmapDrawable) d;
401                refcountDrawable.getRefcountObject().addRef();
402                d = new RefcountBitmapDrawable(mContext.getResources(),
403                        refcountDrawable);
404            }
405            return d;
406        }
407        return null;
408    }
409
410}
411