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