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
17package android.support.rastermill;
18
19import android.graphics.Bitmap;
20import android.graphics.BitmapShader;
21import android.graphics.Canvas;
22import android.graphics.ColorFilter;
23import android.graphics.Paint;
24import android.graphics.PixelFormat;
25import android.graphics.Rect;
26import android.graphics.RectF;
27import android.graphics.Shader;
28import android.graphics.drawable.Animatable;
29import android.graphics.drawable.Drawable;
30import android.os.Handler;
31import android.os.HandlerThread;
32import android.os.Process;
33import android.os.SystemClock;
34import android.util.Log;
35
36public class FrameSequenceDrawable extends Drawable implements Animatable, Runnable {
37    private static final String TAG = "FrameSequence";
38    /**
39     * These constants are chosen to imitate common browser behavior for WebP/GIF.
40     * If other decoders are added, this behavior should be moved into the WebP/GIF decoders.
41     *
42     * Note that 0 delay is undefined behavior in the GIF standard.
43     */
44    private static final long MIN_DELAY_MS = 20;
45    private static final long DEFAULT_DELAY_MS = 100;
46
47    private static final Object sLock = new Object();
48    private static HandlerThread sDecodingThread;
49    private static Handler sDecodingThreadHandler;
50    private static void initializeDecodingThread() {
51        synchronized (sLock) {
52            if (sDecodingThread != null) return;
53
54            sDecodingThread = new HandlerThread("FrameSequence decoding thread",
55                    Process.THREAD_PRIORITY_BACKGROUND);
56            sDecodingThread.start();
57            sDecodingThreadHandler = new Handler(sDecodingThread.getLooper());
58        }
59    }
60
61    public static interface OnFinishedListener {
62        /**
63         * Called when a FrameSequenceDrawable has finished looping.
64         *
65         * Note that this is will not be called if the drawable is explicitly
66         * stopped, or marked invisible.
67         */
68        public abstract void onFinished(FrameSequenceDrawable drawable);
69    }
70
71    public static interface BitmapProvider {
72        /**
73         * Called by FrameSequenceDrawable to aquire an 8888 Bitmap with minimum dimensions.
74         */
75        public abstract Bitmap acquireBitmap(int minWidth, int minHeight);
76
77        /**
78         * Called by FrameSequenceDrawable to release a Bitmap it no longer needs. The Bitmap
79         * will no longer be used at all by the drawable, so it is safe to reuse elsewhere.
80         *
81         * This method may be called by FrameSequenceDrawable on any thread.
82         */
83        public abstract void releaseBitmap(Bitmap bitmap);
84    }
85
86    private static BitmapProvider sAllocatingBitmapProvider = new BitmapProvider() {
87        @Override
88        public Bitmap acquireBitmap(int minWidth, int minHeight) {
89            return Bitmap.createBitmap(minWidth, minHeight, Bitmap.Config.ARGB_8888);
90        }
91
92        @Override
93        public void releaseBitmap(Bitmap bitmap) {}
94    };
95
96    /**
97     * Register a callback to be invoked when a FrameSequenceDrawable finishes looping.
98     *
99     * @see #setLoopBehavior(int)
100     */
101    public void setOnFinishedListener(OnFinishedListener onFinishedListener) {
102        mOnFinishedListener = onFinishedListener;
103    }
104
105    /**
106     * Loop a finite number of times, which can be set using setLoopCount. Default to loop once.
107     */
108    public static final int LOOP_FINITE = 1;
109
110    /**
111     * Loop continuously. The OnFinishedListener will never be called.
112     */
113    public static final int LOOP_INF = 2;
114
115    /**
116     * Use loop count stored in source data, or LOOP_ONCE if not present.
117     */
118    public static final int LOOP_DEFAULT = 3;
119
120    /**
121     * Loop only once.
122     *
123     * @deprecated Use LOOP_FINITE instead.
124     */
125    @Deprecated
126    public static final int LOOP_ONCE = LOOP_FINITE;
127
128    /**
129     * Define looping behavior of frame sequence.
130     *
131     * Must be one of LOOP_ONCE, LOOP_INF, LOOP_DEFAULT, or LOOP_FINITE.
132     */
133    public void setLoopBehavior(int loopBehavior) {
134        mLoopBehavior = loopBehavior;
135    }
136
137    /**
138     * Set the number of loops in LOOP_FINITE mode. The number must be a postive integer.
139     */
140    public void setLoopCount(int loopCount) {
141        mLoopCount = loopCount;
142    }
143
144    private final FrameSequence mFrameSequence;
145    private final FrameSequence.State mFrameSequenceState;
146
147    private final Paint mPaint;
148    private BitmapShader mFrontBitmapShader;
149    private BitmapShader mBackBitmapShader;
150    private final Rect mSrcRect;
151    private boolean mCircleMaskEnabled;
152
153    //Protects the fields below
154    private final Object mLock = new Object();
155
156    private final BitmapProvider mBitmapProvider;
157    private boolean mDestroyed = false;
158    private Bitmap mFrontBitmap;
159    private Bitmap mBackBitmap;
160
161    private static final int STATE_SCHEDULED = 1;
162    private static final int STATE_DECODING = 2;
163    private static final int STATE_WAITING_TO_SWAP = 3;
164    private static final int STATE_READY_TO_SWAP = 4;
165
166    private int mState;
167    private int mCurrentLoop;
168    private int mLoopBehavior = LOOP_DEFAULT;
169    private int mLoopCount = 1;
170
171    private long mLastSwap;
172    private long mNextSwap;
173    private int mNextFrameToDecode;
174    private OnFinishedListener mOnFinishedListener;
175
176    private RectF mTempRectF = new RectF();
177
178    /**
179     * Runs on decoding thread, only modifies mBackBitmap's pixels
180     */
181    private Runnable mDecodeRunnable = new Runnable() {
182        @Override
183        public void run() {
184            int nextFrame;
185            Bitmap bitmap;
186            synchronized (mLock) {
187                if (mDestroyed) return;
188
189                nextFrame = mNextFrameToDecode;
190                if (nextFrame < 0) {
191                    return;
192                }
193                bitmap = mBackBitmap;
194                mState = STATE_DECODING;
195            }
196            int lastFrame = nextFrame - 2;
197            boolean exceptionDuringDecode = false;
198            long invalidateTimeMs = 0;
199            try {
200                invalidateTimeMs = mFrameSequenceState.getFrame(nextFrame, bitmap, lastFrame);
201            } catch(Exception e) {
202                // Exception during decode: continue, but delay next frame indefinitely.
203                Log.e(TAG, "exception during decode: " + e);
204                exceptionDuringDecode = true;
205            }
206
207            if (invalidateTimeMs < MIN_DELAY_MS) {
208                invalidateTimeMs = DEFAULT_DELAY_MS;
209            }
210
211            boolean schedule = false;
212            Bitmap bitmapToRelease = null;
213            synchronized (mLock) {
214                if (mDestroyed) {
215                    bitmapToRelease = mBackBitmap;
216                    mBackBitmap = null;
217                } else if (mNextFrameToDecode >= 0 && mState == STATE_DECODING) {
218                    schedule = true;
219                    mNextSwap = exceptionDuringDecode ? Long.MAX_VALUE : invalidateTimeMs + mLastSwap;
220                    mState = STATE_WAITING_TO_SWAP;
221                }
222            }
223            if (schedule) {
224                scheduleSelf(FrameSequenceDrawable.this, mNextSwap);
225            }
226            if (bitmapToRelease != null) {
227                // destroy the bitmap here, since there's no safe way to get back to
228                // drawable thread - drawable is likely detached, so schedule is noop.
229                mBitmapProvider.releaseBitmap(bitmapToRelease);
230            }
231        }
232    };
233
234    private Runnable mFinishedCallbackRunnable = new Runnable() {
235        @Override
236        public void run() {
237            synchronized (mLock) {
238                mNextFrameToDecode = -1;
239                mState = 0;
240            }
241            if (mOnFinishedListener != null) {
242                mOnFinishedListener.onFinished(FrameSequenceDrawable.this);
243            }
244        }
245    };
246
247    private static Bitmap acquireAndValidateBitmap(BitmapProvider bitmapProvider,
248            int minWidth, int minHeight) {
249        Bitmap bitmap = bitmapProvider.acquireBitmap(minWidth, minHeight);
250
251        if (bitmap.getWidth() < minWidth
252                || bitmap.getHeight() < minHeight
253                || bitmap.getConfig() != Bitmap.Config.ARGB_8888) {
254            throw new IllegalArgumentException("Invalid bitmap provided");
255        }
256
257        return bitmap;
258    }
259
260    public FrameSequenceDrawable(FrameSequence frameSequence) {
261        this(frameSequence, sAllocatingBitmapProvider);
262    }
263
264    public FrameSequenceDrawable(FrameSequence frameSequence, BitmapProvider bitmapProvider) {
265        if (frameSequence == null || bitmapProvider == null) throw new IllegalArgumentException();
266
267        mFrameSequence = frameSequence;
268        mFrameSequenceState = frameSequence.createState();
269        final int width = frameSequence.getWidth();
270        final int height = frameSequence.getHeight();
271
272        mBitmapProvider = bitmapProvider;
273        mFrontBitmap = acquireAndValidateBitmap(bitmapProvider, width, height);
274        mBackBitmap = acquireAndValidateBitmap(bitmapProvider, width, height);
275        mSrcRect = new Rect(0, 0, width, height);
276        mPaint = new Paint();
277        mPaint.setFilterBitmap(true);
278
279        mFrontBitmapShader
280            = new BitmapShader(mFrontBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
281        mBackBitmapShader
282            = new BitmapShader(mBackBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
283
284        mLastSwap = 0;
285
286        mNextFrameToDecode = -1;
287        mFrameSequenceState.getFrame(0, mFrontBitmap, -1);
288        initializeDecodingThread();
289    }
290
291    /**
292     * Pass true to mask the shape of the animated drawing content to a circle.
293     *
294     * <p> The masking circle will be the largest circle contained in the Drawable's bounds.
295     * Masking is done with BitmapShader, incurring minimal additional draw cost.
296     */
297    public final void setCircleMaskEnabled(boolean circleMaskEnabled) {
298        if (mCircleMaskEnabled != circleMaskEnabled) {
299            mCircleMaskEnabled = circleMaskEnabled;
300            // Anti alias only necessary when using circular mask
301            mPaint.setAntiAlias(circleMaskEnabled);
302            invalidateSelf();
303        }
304    }
305
306    public final boolean getCircleMaskEnabled() {
307        return mCircleMaskEnabled;
308    }
309
310    private void checkDestroyedLocked() {
311        if (mDestroyed) {
312            throw new IllegalStateException("Cannot perform operation on recycled drawable");
313        }
314    }
315
316    public boolean isDestroyed() {
317        synchronized (mLock) {
318            return mDestroyed;
319        }
320    }
321
322    /**
323     * Marks the drawable as permanently recycled (and thus unusable), and releases any owned
324     * Bitmaps drawable to its BitmapProvider, if attached.
325     *
326     * If no BitmapProvider is attached to the drawable, recycle() is called on the Bitmaps.
327     */
328    public void destroy() {
329        if (mBitmapProvider == null) {
330            throw new IllegalStateException("BitmapProvider must be non-null");
331        }
332
333        Bitmap bitmapToReleaseA;
334        Bitmap bitmapToReleaseB = null;
335        synchronized (mLock) {
336            checkDestroyedLocked();
337
338            bitmapToReleaseA = mFrontBitmap;
339            mFrontBitmap = null;
340
341            if (mState != STATE_DECODING) {
342                bitmapToReleaseB = mBackBitmap;
343                mBackBitmap = null;
344            }
345
346            mDestroyed = true;
347        }
348
349        // For simplicity and safety, we don't destroy the state object here
350        mBitmapProvider.releaseBitmap(bitmapToReleaseA);
351        if (bitmapToReleaseB != null) {
352            mBitmapProvider.releaseBitmap(bitmapToReleaseB);
353        }
354    }
355
356    @Override
357    protected void finalize() throws Throwable {
358        try {
359            mFrameSequenceState.destroy();
360        } finally {
361            super.finalize();
362        }
363    }
364
365    @Override
366    public void draw(Canvas canvas) {
367        synchronized (mLock) {
368            checkDestroyedLocked();
369            if (mState == STATE_WAITING_TO_SWAP) {
370                // may have failed to schedule mark ready runnable,
371                // so go ahead and swap if swapping is due
372                if (mNextSwap - SystemClock.uptimeMillis() <= 0) {
373                    mState = STATE_READY_TO_SWAP;
374                }
375            }
376
377            if (isRunning() && mState == STATE_READY_TO_SWAP) {
378                // Because draw has occurred, the view system is guaranteed to no longer hold a
379                // reference to the old mFrontBitmap, so we now use it to produce the next frame
380                Bitmap tmp = mBackBitmap;
381                mBackBitmap = mFrontBitmap;
382                mFrontBitmap = tmp;
383
384                BitmapShader tmpShader = mBackBitmapShader;
385                mBackBitmapShader = mFrontBitmapShader;
386                mFrontBitmapShader = tmpShader;
387
388                mLastSwap = SystemClock.uptimeMillis();
389
390                boolean continueLooping = true;
391                if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) {
392                    mCurrentLoop++;
393                    if ((mLoopBehavior == LOOP_FINITE && mCurrentLoop == mLoopCount) ||
394                            (mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) {
395                        continueLooping = false;
396                    }
397                }
398
399                if (continueLooping) {
400                    scheduleDecodeLocked();
401                } else {
402                    scheduleSelf(mFinishedCallbackRunnable, 0);
403                }
404            }
405        }
406
407        if (mCircleMaskEnabled) {
408            final Rect bounds = getBounds();
409            final int bitmapWidth = getIntrinsicWidth();
410            final int bitmapHeight = getIntrinsicHeight();
411            final float scaleX = 1.0f * bounds.width() / bitmapWidth;
412            final float scaleY = 1.0f * bounds.height() / bitmapHeight;
413
414            canvas.save();
415            // scale and translate to account for bounds, so we can operate in intrinsic
416            // width/height (so it's valid to use an unscaled bitmap shader)
417            canvas.translate(bounds.left, bounds.top);
418            canvas.scale(scaleX, scaleY);
419
420            final float unscaledCircleDiameter = Math.min(bounds.width(), bounds.height());
421            final float scaledDiameterX = unscaledCircleDiameter / scaleX;
422            final float scaledDiameterY = unscaledCircleDiameter / scaleY;
423
424            // Want to draw a circle, but we have to compensate for canvas scale
425            mTempRectF.set(
426                    (bitmapWidth - scaledDiameterX) / 2.0f,
427                    (bitmapHeight - scaledDiameterY) / 2.0f,
428                    (bitmapWidth + scaledDiameterX) / 2.0f,
429                    (bitmapHeight + scaledDiameterY) / 2.0f);
430            mPaint.setShader(mFrontBitmapShader);
431            canvas.drawOval(mTempRectF, mPaint);
432            canvas.restore();
433        } else {
434            mPaint.setShader(null);
435            canvas.drawBitmap(mFrontBitmap, mSrcRect, getBounds(), mPaint);
436        }
437    }
438
439    private void scheduleDecodeLocked() {
440        mState = STATE_SCHEDULED;
441        mNextFrameToDecode = (mNextFrameToDecode + 1) % mFrameSequence.getFrameCount();
442        sDecodingThreadHandler.post(mDecodeRunnable);
443    }
444
445    @Override
446    public void run() {
447        // set ready to swap as necessary
448        boolean invalidate = false;
449        synchronized (mLock) {
450            if (mNextFrameToDecode >= 0 && mState == STATE_WAITING_TO_SWAP) {
451                mState = STATE_READY_TO_SWAP;
452                invalidate = true;
453            }
454        }
455        if (invalidate) {
456            invalidateSelf();
457        }
458    }
459
460    @Override
461    public void start() {
462        if (!isRunning()) {
463            synchronized (mLock) {
464                checkDestroyedLocked();
465                if (mState == STATE_SCHEDULED) return; // already scheduled
466                mCurrentLoop = 0;
467                scheduleDecodeLocked();
468            }
469        }
470    }
471
472    @Override
473    public void stop() {
474        if (isRunning()) {
475            unscheduleSelf(this);
476        }
477    }
478
479    @Override
480    public boolean isRunning() {
481        synchronized (mLock) {
482            return mNextFrameToDecode > -1 && !mDestroyed;
483        }
484    }
485
486    @Override
487    public void unscheduleSelf(Runnable what) {
488        synchronized (mLock) {
489            mNextFrameToDecode = -1;
490            mState = 0;
491        }
492        super.unscheduleSelf(what);
493    }
494
495    @Override
496    public boolean setVisible(boolean visible, boolean restart) {
497        boolean changed = super.setVisible(visible, restart);
498
499        if (!visible) {
500            stop();
501        } else if (restart || changed) {
502            stop();
503            start();
504        }
505
506        return changed;
507    }
508
509    // drawing properties
510
511    @Override
512    public void setFilterBitmap(boolean filter) {
513        mPaint.setFilterBitmap(filter);
514    }
515
516    @Override
517    public void setAlpha(int alpha) {
518        mPaint.setAlpha(alpha);
519    }
520
521    @Override
522    public void setColorFilter(ColorFilter colorFilter) {
523        mPaint.setColorFilter(colorFilter);
524    }
525
526    @Override
527    public int getIntrinsicWidth() {
528        return mFrameSequence.getWidth();
529    }
530
531    @Override
532    public int getIntrinsicHeight() {
533        return mFrameSequence.getHeight();
534    }
535
536    @Override
537    public int getOpacity() {
538        return mFrameSequence.isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSPARENT;
539    }
540}
541