1 /*
2  * Copyright (C) 2013 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 package com.android.bitmap.drawable;
17 
18 import android.content.res.Resources;
19 import android.graphics.Canvas;
20 import android.graphics.ColorFilter;
21 import android.graphics.Paint;
22 import android.graphics.PixelFormat;
23 import android.graphics.Rect;
24 import android.graphics.drawable.Drawable;
25 import android.util.DisplayMetrics;
26 import android.util.Log;
27 
28 import com.android.bitmap.BitmapCache;
29 import com.android.bitmap.DecodeTask;
30 import com.android.bitmap.DecodeTask.DecodeCallback;
31 import com.android.bitmap.DecodeTask.DecodeOptions;
32 import com.android.bitmap.NamedThreadFactory;
33 import com.android.bitmap.RequestKey;
34 import com.android.bitmap.RequestKey.Cancelable;
35 import com.android.bitmap.RequestKey.FileDescriptorFactory;
36 import com.android.bitmap.ReusableBitmap;
37 import com.android.bitmap.util.BitmapUtils;
38 import com.android.bitmap.util.RectUtils;
39 import com.android.bitmap.util.Trace;
40 
41 import java.util.concurrent.Executor;
42 import java.util.concurrent.LinkedBlockingQueue;
43 import java.util.concurrent.ThreadPoolExecutor;
44 import java.util.concurrent.TimeUnit;
45 
46 /**
47  * This class encapsulates the basic functionality needed to display a single image bitmap,
48  * including request creation/cancelling, and data unbinding and re-binding.
49  * <p>
50  * The actual bitmap decode work is handled by {@link DecodeTask}.
51  * <p>
52  * If being used with a long-lived cache (static cache, attached to the Application instead of the
53  * Activity, etc) then make sure to call {@link BasicBitmapDrawable#unbind()} at the appropriate
54  * times so the cache has accurate unref counts. The
55  * {@link com.android.bitmap.view.BitmapDrawableImageView} class has been created to do the
56  * appropriate unbind operation when the view is detached from the window.
57  */
58 public class BasicBitmapDrawable extends Drawable implements DecodeCallback,
59         Drawable.Callback, RequestKey.Callback {
60 
61     protected RequestKey mCurrKey;
62     protected RequestKey mPrevKey;
63     protected int mDecodeWidth;
64     protected int mDecodeHeight;
65 
66     protected final Paint mPaint = new Paint();
67     private final BitmapCache mCache;
68     private final Rect mRect = new Rect();
69 
70     private final boolean mLimitDensity;
71     private final float mDensity;
72     private ReusableBitmap mBitmap;
73     private DecodeTask mTask;
74     private Cancelable mCreateFileDescriptorFactoryTask;
75 
76     private int mLayoutDirection;
77 
78     // based on framework CL:I015d77
79     private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
80     private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
81     private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
82 
83     private static final Executor SMALL_POOL_EXECUTOR = new ThreadPoolExecutor(
84             CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, 1, TimeUnit.SECONDS,
85             new LinkedBlockingQueue<Runnable>(128), new NamedThreadFactory("decode"));
86     private static final Executor EXECUTOR = SMALL_POOL_EXECUTOR;
87 
88     private static final int MAX_BITMAP_DENSITY = DisplayMetrics.DENSITY_HIGH;
89     private static final float VERTICAL_CENTER = 1f / 2;
90     private static final float HORIZONTAL_CENTER = 1f / 2;
91     private static final float NO_MULTIPLIER = 1f;
92 
93     private static final String TAG = BasicBitmapDrawable.class.getSimpleName();
94     private static final boolean DEBUG = DecodeTask.DEBUG;
95 
BasicBitmapDrawable(final Resources res, final BitmapCache cache, final boolean limitDensity)96     public BasicBitmapDrawable(final Resources res, final BitmapCache cache,
97             final boolean limitDensity) {
98         mDensity = res.getDisplayMetrics().density;
99         mCache = cache;
100         mLimitDensity = limitDensity;
101         mPaint.setFilterBitmap(true);
102         mPaint.setAntiAlias(true);
103         mPaint.setDither(true);
104     }
105 
getKey()106     public final RequestKey getKey() {
107         return mCurrKey;
108     }
109 
getPreviousKey()110     public final RequestKey getPreviousKey() {
111         return mPrevKey;
112     }
113 
getBitmap()114     protected ReusableBitmap getBitmap() {
115         return mBitmap;
116     }
117 
118     /**
119      * Set the dimensions to decode into. These dimensions should never change while the drawable is
120      * attached to the same cache, because caches can only contain bitmaps of one size for re-use.
121      *
122      * All UI operations should be called from the UI thread.
123      */
setDecodeDimensions(int width, int height)124     public void setDecodeDimensions(int width, int height) {
125         if (mDecodeWidth == 0 || mDecodeHeight == 0) {
126             mDecodeWidth = width;
127             mDecodeHeight = height;
128             setImage(mCurrKey);
129         }
130     }
131 
132     /**
133      * Set layout direction.
134      * It ends with Local so as not conflict with hidden Drawable.setLayoutDirection.
135      * @param layoutDirection the resolved layout direction for the drawable,
136      *                        either {@link android.view.View#LAYOUT_DIRECTION_LTR}
137      *                        or {@link android.view.View#LAYOUT_DIRECTION_RTL}
138      */
setLayoutDirectionLocal(int layoutDirection)139     public void setLayoutDirectionLocal(int layoutDirection) {
140         if (mLayoutDirection != layoutDirection) {
141             mLayoutDirection = layoutDirection;
142             onLayoutDirectionChangeLocal(layoutDirection);
143         }
144     }
145 
146     /**
147      * Called when the drawable's resolved layout direction changes.
148      * It ends with Local so as not conflict with hidden Drawable.onLayoutDirectionChange.
149      *
150      * @param layoutDirection the new resolved layout direction
151      */
onLayoutDirectionChangeLocal(int layoutDirection)152     public void onLayoutDirectionChangeLocal(int layoutDirection) {}
153 
154     /**
155      * Returns the resolved layout direction for this Drawable.
156      * It ends with Local so as not conflict with hidden Drawable.getLayoutDirection.
157      *
158      * @return One of {@link android.view.View#LAYOUT_DIRECTION_LTR},
159      *         {@link android.view.View#LAYOUT_DIRECTION_RTL}
160      * @see #setLayoutDirectionLocal(int)
161      */
getLayoutDirectionLocal()162     public int getLayoutDirectionLocal() {
163         return mLayoutDirection;
164     }
165 
166     /**
167      * Binds to the given key and start the decode process. This will first look in the cache, then
168      * decode from the request key if not found.
169      *
170      * The key being replaced will be kept in {@link #mPrevKey}.
171      *
172      * All UI operations should be called from the UI thread.
173      */
bind(RequestKey key)174     public void bind(RequestKey key) {
175         Trace.beginSection("bind");
176         if (mCurrKey != null && mCurrKey.equals(key)) {
177             Trace.endSection();
178             return;
179         }
180         setImage(key);
181         Trace.endSection();
182     }
183 
184     /**
185      * Unbinds the current key and bitmap from the drawable. This will cause the bitmap to decrement
186      * its ref count.
187      *
188      * This will assume that you do not want to keep the unbound key in {@link #mPrevKey}.
189      *
190      * All UI operations should be called from the UI thread.
191      */
unbind()192     public void unbind() {
193         unbind(false);
194     }
195 
196     /**
197      * Unbinds the current key and bitmap from the drawable. This will cause the bitmap to decrement
198      * its ref count.
199      *
200      * If the temporary parameter is true, we will keep the unbound key in {@link #mPrevKey}.
201      *
202      * All UI operations should be called from the UI thread.
203      */
unbind(boolean temporary)204     public void unbind(boolean temporary) {
205         Trace.beginSection("unbind");
206         setImage(null);
207         if (!temporary) {
208             mPrevKey = null;
209         }
210         Trace.endSection();
211     }
212 
213     /**
214      * Should only be overriden, not called.
215      */
setImage(final RequestKey key)216     protected void setImage(final RequestKey key) {
217         Trace.beginSection("set image");
218         Trace.beginSection("release reference");
219         if (mBitmap != null) {
220             mBitmap.releaseReference();
221             mBitmap = null;
222         }
223         Trace.endSection();
224 
225         mPrevKey = mCurrKey;
226         mCurrKey = key;
227 
228         if (mTask != null) {
229             mTask.cancel();
230             mTask = null;
231         }
232         if (mCreateFileDescriptorFactoryTask != null) {
233             mCreateFileDescriptorFactoryTask.cancel();
234             mCreateFileDescriptorFactoryTask = null;
235         }
236 
237         if (key == null) {
238             onDecodeFailed();
239             Trace.endSection();
240             return;
241         }
242 
243         // find cached entry here and skip decode if found.
244         final ReusableBitmap cached = mCache.get(key, true /* incrementRefCount */);
245         if (cached != null) {
246             setBitmap(cached);
247             if (DEBUG) {
248                 Log.d(TAG, String.format("CACHE HIT key=%s", mCurrKey));
249             }
250         } else {
251             loadFileDescriptorFactory();
252             if (DEBUG) {
253                 Log.d(TAG, String.format(
254                         "CACHE MISS key=%s\ncache=%s", mCurrKey, mCache.toDebugString()));
255             }
256         }
257         Trace.endSection();
258     }
259 
260     /**
261      * Should only be overriden, not called.
262      */
setBitmap(ReusableBitmap bmp)263     protected void setBitmap(ReusableBitmap bmp) {
264         if (hasBitmap()) {
265             mBitmap.releaseReference();
266         }
267         mBitmap = bmp;
268         invalidateSelf();
269     }
270 
271     /**
272      * Should only be overriden, not called.
273      */
loadFileDescriptorFactory()274     protected void loadFileDescriptorFactory() {
275         if (mCurrKey == null || mDecodeWidth == 0 || mDecodeHeight == 0) {
276             onDecodeFailed();
277             return;
278         }
279 
280         // Create file descriptor if request supports it.
281         mCreateFileDescriptorFactoryTask = mCurrKey
282                 .createFileDescriptorFactoryAsync(mCurrKey, this);
283         if (mCreateFileDescriptorFactoryTask == null) {
284             // Use input stream if request does not.
285             decode(null);
286         }
287     }
288 
289     @Override
fileDescriptorFactoryCreated(final RequestKey key, final FileDescriptorFactory factory)290     public void fileDescriptorFactoryCreated(final RequestKey key,
291             final FileDescriptorFactory factory) {
292         if (mCreateFileDescriptorFactoryTask == null) {
293             // Cancelled.
294             onDecodeFailed();
295             return;
296         }
297         mCreateFileDescriptorFactoryTask = null;
298 
299         if (key.equals(mCurrKey)) {
300             decode(factory);
301         }
302     }
303 
304     /**
305      * Called when the decode process is cancelled at any time.
306      */
onDecodeFailed()307     protected void onDecodeFailed() {
308         invalidateSelf();
309     }
310 
311     /**
312      * Should only be overriden, not called.
313      */
decode(final FileDescriptorFactory factory)314     protected void decode(final FileDescriptorFactory factory) {
315         Trace.beginSection("decode");
316         final int bufferW;
317         final int bufferH;
318         if (mLimitDensity) {
319             final float scale =
320                     Math.min(1f, (float) MAX_BITMAP_DENSITY / DisplayMetrics.DENSITY_DEFAULT
321                             / mDensity);
322             bufferW = (int) (mDecodeWidth * scale);
323             bufferH = (int) (mDecodeHeight * scale);
324         } else {
325             bufferW = mDecodeWidth;
326             bufferH = mDecodeHeight;
327         }
328 
329         if (mTask != null) {
330             mTask.cancel();
331         }
332         final DecodeOptions opts = new DecodeOptions(bufferW, bufferH, getDecodeHorizontalCenter(),
333                 getDecodeVerticalCenter(), getDecodeStrategy());
334         mTask = new DecodeTask(mCurrKey, opts, factory, this, mCache);
335         mTask.executeOnExecutor(getExecutor());
336         Trace.endSection();
337     }
338 
339     /**
340      * Return one of the STRATEGY constants in {@link DecodeOptions}.
341      */
getDecodeStrategy()342     protected int getDecodeStrategy() {
343         return DecodeOptions.STRATEGY_ROUND_NEAREST;
344     }
345 
getExecutor()346     protected Executor getExecutor() {
347         return EXECUTOR;
348     }
349 
getDrawVerticalCenter()350     protected float getDrawVerticalCenter() {
351         return VERTICAL_CENTER;
352     }
353 
getDrawVerticalOffsetMultiplier()354     protected float getDrawVerticalOffsetMultiplier() {
355         return NO_MULTIPLIER;
356     }
357 
358     /**
359      * Clients can override this to specify which section of the source image to decode from.
360      * Possible applications include using face detection to always decode around facial features.
361      */
getDecodeHorizontalCenter()362     protected float getDecodeHorizontalCenter() {
363         return HORIZONTAL_CENTER;
364     }
365 
366     /**
367      * Clients can override this to specify which section of the source image to decode from.
368      * Possible applications include using face detection to always decode around facial features.
369      */
getDecodeVerticalCenter()370     protected float getDecodeVerticalCenter() {
371         return VERTICAL_CENTER;
372     }
373 
374     @Override
draw(final Canvas canvas)375     public void draw(final Canvas canvas) {
376         final Rect bounds = getBounds();
377         if (bounds.isEmpty()) {
378             return;
379         }
380 
381         if (hasBitmap()) {
382             BitmapUtils.calculateCroppedSrcRect(
383                     mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(),
384                     bounds.width(), bounds.height(),
385                     bounds.height(), Integer.MAX_VALUE, getDecodeHorizontalCenter(),
386                     getDrawVerticalCenter(), false /* absoluteFraction */,
387                     getDrawVerticalOffsetMultiplier(), mRect);
388 
389             final int orientation = mBitmap.getOrientation();
390             // calculateCroppedSrcRect() gave us the source rectangle "as if" the orientation has
391             // been corrected. We need to decode the uncorrected source rectangle. Calculate true
392             // coordinates.
393             RectUtils.rotateRectForOrientation(orientation,
394                     new Rect(0, 0, mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight()),
395                     mRect);
396 
397             // We may need to rotate the canvas, so we also have to rotate the bounds.
398             final Rect rotatedBounds = new Rect(bounds);
399             RectUtils.rotateRect(orientation, bounds.centerX(), bounds.centerY(), rotatedBounds);
400 
401             // Rotate the canvas.
402             canvas.save();
403             canvas.rotate(orientation, bounds.centerX(), bounds.centerY());
404             onDrawBitmap(canvas, mRect, rotatedBounds);
405             canvas.restore();
406         }
407     }
408 
hasBitmap()409     protected boolean hasBitmap() {
410         return mBitmap != null && mBitmap.bmp != null;
411     }
412 
413     /**
414      * Override this method to customize how to draw the bitmap to the canvas for the given bounds.
415      * The bitmap to be drawn can be found at {@link #getBitmap()}.
416      */
onDrawBitmap(final Canvas canvas, final Rect src, final Rect dst)417     protected void onDrawBitmap(final Canvas canvas, final Rect src, final Rect dst) {
418         if (hasBitmap()) {
419             canvas.drawBitmap(mBitmap.bmp, src, dst, mPaint);
420         }
421     }
422 
423     @Override
setAlpha(int alpha)424     public void setAlpha(int alpha) {
425         final int old = mPaint.getAlpha();
426         mPaint.setAlpha(alpha);
427         if (alpha != old) {
428             invalidateSelf();
429         }
430     }
431 
432     @Override
setColorFilter(ColorFilter cf)433     public void setColorFilter(ColorFilter cf) {
434         mPaint.setColorFilter(cf);
435         invalidateSelf();
436     }
437 
438     @Override
getOpacity()439     public int getOpacity() {
440         return (hasBitmap() && (mBitmap.bmp.hasAlpha() || mPaint.getAlpha() < 255)) ?
441                 PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
442     }
443 
444     @Override
onDecodeBegin(final RequestKey key)445     public void onDecodeBegin(final RequestKey key) { }
446 
447     @Override
onDecodeComplete(final RequestKey key, final ReusableBitmap result)448     public void onDecodeComplete(final RequestKey key, final ReusableBitmap result) {
449         if (key.equals(mCurrKey)) {
450             setBitmap(result);
451         } else {
452             // if the requests don't match (i.e. this request is stale), decrement the
453             // ref count to allow the bitmap to be pooled
454             if (result != null) {
455                 result.releaseReference();
456             }
457         }
458     }
459 
460     @Override
onDecodeCancel(final RequestKey key)461     public void onDecodeCancel(final RequestKey key) { }
462 
463     @Override
invalidateDrawable(Drawable who)464     public void invalidateDrawable(Drawable who) {
465         invalidateSelf();
466     }
467 
468     @Override
scheduleDrawable(Drawable who, Runnable what, long when)469     public void scheduleDrawable(Drawable who, Runnable what, long when) {
470         scheduleSelf(what, when);
471     }
472 
473     @Override
unscheduleDrawable(Drawable who, Runnable what)474     public void unscheduleDrawable(Drawable who, Runnable what) {
475         unscheduleSelf(what);
476     }
477 }
478