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 */
16package com.android.bitmap.drawable;
17
18import android.content.res.Resources;
19import android.graphics.Canvas;
20import android.graphics.ColorFilter;
21import android.graphics.Paint;
22import android.graphics.PixelFormat;
23import android.graphics.Rect;
24import android.graphics.drawable.Drawable;
25import android.util.DisplayMetrics;
26import android.util.Log;
27
28import com.android.bitmap.BitmapCache;
29import com.android.bitmap.DecodeTask;
30import com.android.bitmap.DecodeTask.DecodeCallback;
31import com.android.bitmap.DecodeTask.DecodeOptions;
32import com.android.bitmap.NamedThreadFactory;
33import com.android.bitmap.RequestKey;
34import com.android.bitmap.RequestKey.Cancelable;
35import com.android.bitmap.RequestKey.FileDescriptorFactory;
36import com.android.bitmap.ReusableBitmap;
37import com.android.bitmap.util.BitmapUtils;
38import com.android.bitmap.util.RectUtils;
39import com.android.bitmap.util.Trace;
40
41import java.util.concurrent.Executor;
42import java.util.concurrent.LinkedBlockingQueue;
43import java.util.concurrent.ThreadPoolExecutor;
44import 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 */
58public 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
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
106    public final RequestKey getKey() {
107        return mCurrKey;
108    }
109
110    public final RequestKey getPreviousKey() {
111        return mPrevKey;
112    }
113
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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     */
307    protected void onDecodeFailed() {
308        invalidateSelf();
309    }
310
311    /**
312     * Should only be overriden, not called.
313     */
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     */
342    protected int getDecodeStrategy() {
343        return DecodeOptions.STRATEGY_ROUND_NEAREST;
344    }
345
346    protected Executor getExecutor() {
347        return EXECUTOR;
348    }
349
350    protected float getDrawVerticalCenter() {
351        return VERTICAL_CENTER;
352    }
353
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     */
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     */
370    protected float getDecodeVerticalCenter() {
371        return VERTICAL_CENTER;
372    }
373
374    @Override
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
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     */
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
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
433    public void setColorFilter(ColorFilter cf) {
434        mPaint.setColorFilter(cf);
435        invalidateSelf();
436    }
437
438    @Override
439    public int getOpacity() {
440        return (hasBitmap() && (mBitmap.bmp.hasAlpha() || mPaint.getAlpha() < 255)) ?
441                PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
442    }
443
444    @Override
445    public void onDecodeBegin(final RequestKey key) { }
446
447    @Override
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
461    public void onDecodeCancel(final RequestKey key) { }
462
463    @Override
464    public void invalidateDrawable(Drawable who) {
465        invalidateSelf();
466    }
467
468    @Override
469    public void scheduleDrawable(Drawable who, Runnable what, long when) {
470        scheduleSelf(what, when);
471    }
472
473    @Override
474    public void unscheduleDrawable(Drawable who, Runnable what) {
475        unscheduleSelf(what);
476    }
477}
478