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 static Rect sRect;
62 
63     protected RequestKey mCurrKey;
64     protected RequestKey mPrevKey;
65     protected int mDecodeWidth;
66     protected int mDecodeHeight;
67 
68     protected final Paint mPaint = new Paint();
69     private final BitmapCache mCache;
70 
71     private final boolean mLimitDensity;
72     private final float mDensity;
73     private ReusableBitmap mBitmap;
74     private DecodeTask mTask;
75     private Cancelable mCreateFileDescriptorFactoryTask;
76 
77     // based on framework CL:I015d77
78     private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
79     private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
80     private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
81 
82     private static final Executor SMALL_POOL_EXECUTOR = new ThreadPoolExecutor(
83             CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, 1, TimeUnit.SECONDS,
84             new LinkedBlockingQueue<Runnable>(128), new NamedThreadFactory("decode"));
85     private static final Executor EXECUTOR = SMALL_POOL_EXECUTOR;
86 
87     private static final int MAX_BITMAP_DENSITY = DisplayMetrics.DENSITY_HIGH;
88     private static final float VERTICAL_CENTER = 1f / 2;
89     private static final float NO_MULTIPLIER = 1f;
90 
91     private static final String TAG = BasicBitmapDrawable.class.getSimpleName();
92     private static final boolean DEBUG = DecodeTask.DEBUG;
93 
BasicBitmapDrawable(final Resources res, final BitmapCache cache, final boolean limitDensity)94     public BasicBitmapDrawable(final Resources res, final BitmapCache cache,
95             final boolean limitDensity) {
96         mDensity = res.getDisplayMetrics().density;
97         mCache = cache;
98         mLimitDensity = limitDensity;
99         mPaint.setFilterBitmap(true);
100         mPaint.setAntiAlias(true);
101         mPaint.setDither(true);
102 
103         if (sRect == null) {
104             sRect = new Rect();
105         }
106     }
107 
getKey()108     public final RequestKey getKey() {
109         return mCurrKey;
110     }
111 
getPreviousKey()112     public final RequestKey getPreviousKey() {
113         return mPrevKey;
114     }
115 
getBitmap()116     protected ReusableBitmap getBitmap() {
117         return mBitmap;
118     }
119 
120     /**
121      * Set the dimensions to decode into. These dimensions should never change while the drawable is
122      * attached to the same cache, because caches can only contain bitmaps of one size for re-use.
123      *
124      * All UI operations should be called from the UI thread.
125      */
setDecodeDimensions(int width, int height)126     public void setDecodeDimensions(int width, int height) {
127         if (mDecodeWidth == 0 || mDecodeHeight == 0) {
128             mDecodeWidth = width;
129             mDecodeHeight = height;
130             setImage(mCurrKey);
131         }
132     }
133 
134     /**
135      * Binds to the given key and start the decode process. This will first look in the cache, then
136      * decode from the request key if not found.
137      *
138      * The key being replaced will be kept in {@link #mPrevKey}.
139      *
140      * All UI operations should be called from the UI thread.
141      */
bind(RequestKey key)142     public void bind(RequestKey key) {
143         Trace.beginSection("bind");
144         if (mCurrKey != null && mCurrKey.equals(key)) {
145             Trace.endSection();
146             return;
147         }
148         setImage(key);
149         Trace.endSection();
150     }
151 
152     /**
153      * Unbinds the current key and bitmap from the drawable. This will cause the bitmap to decrement
154      * its ref count.
155      *
156      * This will assume that you do not want to keep the unbound key in {@link #mPrevKey}.
157      *
158      * All UI operations should be called from the UI thread.
159      */
unbind()160     public void unbind() {
161         unbind(false);
162     }
163 
164     /**
165      * Unbinds the current key and bitmap from the drawable. This will cause the bitmap to decrement
166      * its ref count.
167      *
168      * If the temporary parameter is true, we will keep the unbound key in {@link #mPrevKey}.
169      *
170      * All UI operations should be called from the UI thread.
171      */
unbind(boolean temporary)172     public void unbind(boolean temporary) {
173         Trace.beginSection("unbind");
174         setImage(null);
175         if (!temporary) {
176             mPrevKey = null;
177         }
178         Trace.endSection();
179     }
180 
181     /**
182      * Should only be overriden, not called.
183      */
setImage(final RequestKey key)184     protected void setImage(final RequestKey key) {
185         Trace.beginSection("set image");
186         Trace.beginSection("release reference");
187         if (mBitmap != null) {
188             mBitmap.releaseReference();
189             mBitmap = null;
190         }
191         Trace.endSection();
192 
193         mPrevKey = mCurrKey;
194         mCurrKey = key;
195 
196         if (mTask != null) {
197             mTask.cancel();
198             mTask = null;
199         }
200         if (mCreateFileDescriptorFactoryTask != null) {
201             mCreateFileDescriptorFactoryTask.cancel();
202             mCreateFileDescriptorFactoryTask = null;
203         }
204 
205         if (key == null) {
206             invalidateSelf();
207             Trace.endSection();
208             return;
209         }
210 
211         // find cached entry here and skip decode if found.
212         final ReusableBitmap cached = mCache.get(key, true /* incrementRefCount */);
213         if (cached != null) {
214             setBitmap(cached);
215             if (DEBUG) {
216                 Log.d(TAG, String.format("CACHE HIT key=%s", mCurrKey));
217             }
218         } else {
219             loadFileDescriptorFactory();
220             if (DEBUG) {
221                 Log.d(TAG, String.format(
222                         "CACHE MISS key=%s\ncache=%s", mCurrKey, mCache.toDebugString()));
223             }
224         }
225         Trace.endSection();
226     }
227 
228     /**
229      * Should only be overriden, not called.
230      */
setBitmap(ReusableBitmap bmp)231     protected void setBitmap(ReusableBitmap bmp) {
232         if (hasBitmap()) {
233             mBitmap.releaseReference();
234         }
235         mBitmap = bmp;
236         invalidateSelf();
237     }
238 
239     /**
240      * Should only be overriden, not called.
241      */
loadFileDescriptorFactory()242     protected void loadFileDescriptorFactory() {
243         if (mCurrKey == null || mDecodeWidth == 0 || mDecodeHeight == 0) {
244             return;
245         }
246 
247         // Create file descriptor if request supports it.
248         mCreateFileDescriptorFactoryTask = mCurrKey
249                 .createFileDescriptorFactoryAsync(mCurrKey, this);
250         if (mCreateFileDescriptorFactoryTask == null) {
251             // Use input stream if request does not.
252             decode(null);
253         }
254     }
255 
256     @Override
fileDescriptorFactoryCreated(final RequestKey key, final FileDescriptorFactory factory)257     public void fileDescriptorFactoryCreated(final RequestKey key,
258             final FileDescriptorFactory factory) {
259         if (mCreateFileDescriptorFactoryTask == null) {
260             // Cancelled.
261             return;
262         }
263         mCreateFileDescriptorFactoryTask = null;
264 
265         if (key.equals(mCurrKey)) {
266             decode(factory);
267         }
268     }
269 
270     /**
271      * Should only be overriden, not called.
272      */
decode(final FileDescriptorFactory factory)273     protected void decode(final FileDescriptorFactory factory) {
274         Trace.beginSection("decode");
275         final int bufferW;
276         final int bufferH;
277         if (mLimitDensity) {
278             final float scale =
279                     Math.min(1f, (float) MAX_BITMAP_DENSITY / DisplayMetrics.DENSITY_DEFAULT
280                             / mDensity);
281             bufferW = (int) (mDecodeWidth * scale);
282             bufferH = (int) (mDecodeHeight * scale);
283         } else {
284             bufferW = mDecodeWidth;
285             bufferH = mDecodeHeight;
286         }
287 
288         if (mTask != null) {
289             mTask.cancel();
290         }
291         final DecodeOptions opts = new DecodeOptions(bufferW, bufferH, getDecodeVerticalCenter(),
292                 DecodeOptions.STRATEGY_ROUND_NEAREST);
293         mTask = new DecodeTask(mCurrKey, opts, factory, this, mCache);
294         mTask.executeOnExecutor(getExecutor());
295         Trace.endSection();
296     }
297 
getExecutor()298     protected Executor getExecutor() {
299         return EXECUTOR;
300     }
301 
getDrawVerticalCenter()302     protected float getDrawVerticalCenter() {
303         return VERTICAL_CENTER;
304     }
305 
getDrawVerticalOffsetMultiplier()306     protected float getDrawVerticalOffsetMultiplier() {
307         return NO_MULTIPLIER;
308     }
309 
310     /**
311      * Clients can override this to specify which section of the source image to decode from.
312      * Possible applications include using face detection to always decode around facial features.
313      */
getDecodeVerticalCenter()314     protected float getDecodeVerticalCenter() {
315         return VERTICAL_CENTER;
316     }
317 
318     @Override
draw(final Canvas canvas)319     public void draw(final Canvas canvas) {
320         final Rect bounds = getBounds();
321         if (bounds.isEmpty()) {
322             return;
323         }
324 
325         if (hasBitmap()) {
326             BitmapUtils.calculateCroppedSrcRect(
327                     mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(),
328                     bounds.width(), bounds.height(),
329                     bounds.height(), Integer.MAX_VALUE,
330                     getDrawVerticalCenter(), false /* absoluteFraction */,
331                     getDrawVerticalOffsetMultiplier(), sRect);
332 
333             final int orientation = mBitmap.getOrientation();
334             // calculateCroppedSrcRect() gave us the source rectangle "as if" the orientation has
335             // been corrected. We need to decode the uncorrected source rectangle. Calculate true
336             // coordinates.
337             RectUtils.rotateRectForOrientation(orientation,
338                     new Rect(0, 0, mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight()),
339                     sRect);
340 
341             // We may need to rotate the canvas, so we also have to rotate the bounds.
342             final Rect rotatedBounds = new Rect(bounds);
343             RectUtils.rotateRect(orientation, bounds.centerX(), bounds.centerY(), rotatedBounds);
344 
345             // Rotate the canvas.
346             canvas.save();
347             canvas.rotate(orientation, bounds.centerX(), bounds.centerY());
348             onDrawBitmap(canvas, sRect, rotatedBounds);
349             canvas.restore();
350         }
351     }
352 
hasBitmap()353     protected boolean hasBitmap() {
354         return mBitmap != null && mBitmap.bmp != null;
355     }
356 
357     /**
358      * Override this method to customize how to draw the bitmap to the canvas for the given bounds.
359      * The bitmap to be drawn can be found at {@link #getBitmap()}.
360      */
onDrawBitmap(final Canvas canvas, final Rect src, final Rect dst)361     protected void onDrawBitmap(final Canvas canvas, final Rect src, final Rect dst) {
362         if (hasBitmap()) {
363             canvas.drawBitmap(mBitmap.bmp, src, dst, mPaint);
364         }
365     }
366 
367     @Override
setAlpha(int alpha)368     public void setAlpha(int alpha) {
369         final int old = mPaint.getAlpha();
370         mPaint.setAlpha(alpha);
371         if (alpha != old) {
372             invalidateSelf();
373         }
374     }
375 
376     @Override
setColorFilter(ColorFilter cf)377     public void setColorFilter(ColorFilter cf) {
378         mPaint.setColorFilter(cf);
379         invalidateSelf();
380     }
381 
382     @Override
getOpacity()383     public int getOpacity() {
384         return (hasBitmap() && (mBitmap.bmp.hasAlpha() || mPaint.getAlpha() < 255)) ?
385                 PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
386     }
387 
388     @Override
onDecodeBegin(final RequestKey key)389     public void onDecodeBegin(final RequestKey key) { }
390 
391     @Override
onDecodeComplete(final RequestKey key, final ReusableBitmap result)392     public void onDecodeComplete(final RequestKey key, final ReusableBitmap result) {
393         if (key.equals(mCurrKey)) {
394             setBitmap(result);
395         } else {
396             // if the requests don't match (i.e. this request is stale), decrement the
397             // ref count to allow the bitmap to be pooled
398             if (result != null) {
399                 result.releaseReference();
400             }
401         }
402     }
403 
404     @Override
onDecodeCancel(final RequestKey key)405     public void onDecodeCancel(final RequestKey key) { }
406 
407     @Override
invalidateDrawable(Drawable who)408     public void invalidateDrawable(Drawable who) {
409         invalidateSelf();
410     }
411 
412     @Override
scheduleDrawable(Drawable who, Runnable what, long when)413     public void scheduleDrawable(Drawable who, Runnable what, long when) {
414         scheduleSelf(what, when);
415     }
416 
417     @Override
unscheduleDrawable(Drawable who, Runnable what)418     public void unscheduleDrawable(Drawable who, Runnable what) {
419         unscheduleSelf(what);
420     }
421 }
422