FrameSequenceDrawable.java revision b19ecfd9cfa2c417034403adf7039edf9fe59327
1a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik/*
2a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik * Copyright (C) 2013 The Android Open Source Project
3a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik *
4a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik * Licensed under the Apache License, Version 2.0 (the "License");
5a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik * you may not use this file except in compliance with the License.
6a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik * You may obtain a copy of the License at
7a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik *
8a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik *      http://www.apache.org/licenses/LICENSE-2.0
9a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik *
10a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik * Unless required by applicable law or agreed to in writing, software
11a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik * distributed under the License is distributed on an "AS IS" BASIS,
12a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik * See the License for the specific language governing permissions and
14a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik * limitations under the License.
15a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik */
16a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
17a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craikpackage android.support.rastermill;
18a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
19a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craikimport android.graphics.Bitmap;
20a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craikimport android.graphics.Canvas;
21a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craikimport android.graphics.ColorFilter;
22a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craikimport android.graphics.Paint;
23a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craikimport android.graphics.PixelFormat;
24a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craikimport android.graphics.Rect;
25a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craikimport android.graphics.drawable.Animatable;
26a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craikimport android.graphics.drawable.Drawable;
27a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craikimport android.os.Handler;
28a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craikimport android.os.HandlerThread;
293105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craikimport android.os.Process;
30a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craikimport android.os.SystemClock;
31a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
32a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craikpublic class FrameSequenceDrawable extends Drawable implements Animatable, Runnable {
33a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private static final Object sLock = new Object();
34a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private static HandlerThread sDecodingThread;
35a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private static Handler sDecodingThreadHandler;
36a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private static void initializeDecodingThread() {
37a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        synchronized (sLock) {
38a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            if (sDecodingThread != null) return;
39a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
403105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik            sDecodingThread = new HandlerThread("FrameSequence decoding thread",
413105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik                    Process.THREAD_PRIORITY_BACKGROUND);
42a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            sDecodingThread.start();
43a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            sDecodingThreadHandler = new Handler(sDecodingThread.getLooper());
44a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        }
45a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    }
46a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
47e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik    public static interface OnFinishedListener {
48e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik        /**
49e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik         * Called when a FrameSequenceDrawable has finished looping.
50e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik         *
51e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik         * Note that this is will not be called if the drawable is explicitly
52e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik         * stopped, or marked invisible.
53e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik         */
54e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik        public abstract void onFinished(FrameSequenceDrawable drawable);
55e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik    }
56e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik
573105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik    public static interface BitmapProvider {
583105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        /**
593105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik         * Called by FrameSequenceDrawable to aquire an 8888 Bitmap with minimum dimensions.
603105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik         */
613105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        public abstract Bitmap acquireBitmap(int minWidth, int minHeight);
623105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik
633105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        /**
643105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik         * Called by FrameSequenceDrawable to release a Bitmap it no longer needs. The Bitmap
653105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik         * will no longer be used at all by the drawable, so it is safe to reuse elsewhere.
664eb541aff092a057b27b917f09d33aba226dffedChris Craik         *
674eb541aff092a057b27b917f09d33aba226dffedChris Craik         * This method may be called by FrameSequenceDrawable on any thread.
683105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik         */
693105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        public abstract void releaseBitmap(Bitmap bitmap);
703105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik    }
713105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik
723105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik    private static BitmapProvider sAllocatingBitmapProvider = new BitmapProvider() {
733105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        @Override
743105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        public Bitmap acquireBitmap(int minWidth, int minHeight) {
753105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik            return Bitmap.createBitmap(minWidth, minHeight, Bitmap.Config.ARGB_8888);
763105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        }
773105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik
783105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        @Override
79b19ecfd9cfa2c417034403adf7039edf9fe59327Chris Craik        public void releaseBitmap(Bitmap bitmap) {}
803105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik    };
813105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik
82e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik    /**
83e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik     * Register a callback to be invoked when a FrameSequenceDrawable finishes looping.
84e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik     *
853105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik     * @see #setLoopBehavior(int)
86e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik     */
87e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik    public void setOnFinishedListener(OnFinishedListener onFinishedListener) {
88e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik        mOnFinishedListener = onFinishedListener;
89e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik    }
90e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik
91e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik    /**
92e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik     * Loop only once.
93e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik     */
94e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik    public static final int LOOP_ONCE = 1;
95e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik
96e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik    /**
97e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik     * Loop continuously. The OnFinishedListener will never be called.
98e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik     */
99e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik    public static final int LOOP_INF = 2;
100e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik
101e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik    /**
102e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik     * Use loop count stored in source data, or LOOP_ONCE if not present.
103e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik     */
104e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik    public static final int LOOP_DEFAULT = 3;
105e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik
106e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik    /**
107e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik     * Define looping behavior of frame sequence.
108e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik     *
109e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik     * Must be one of LOOP_ONCE, LOOP_INF, or LOOP_DEFAULT
110e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik     */
111e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik    public void setLoopBehavior(int loopBehavior) {
112e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik        mLoopBehavior = loopBehavior;
113e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik    }
114e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik
115a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private final FrameSequence mFrameSequence;
116a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private final FrameSequence.State mFrameSequenceState;
117a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
118a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private final Paint mPaint;
119a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private final Rect mSrcRect;
120a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
121a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    //Protects the fields below
122a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private final Object mLock = new Object();
123a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
1244eb541aff092a057b27b917f09d33aba226dffedChris Craik    private final BitmapProvider mBitmapProvider;
1254eb541aff092a057b27b917f09d33aba226dffedChris Craik    private boolean mDestroyed = false;
126a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private Bitmap mFrontBitmap;
127a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private Bitmap mBackBitmap;
128a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
129a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private static final int STATE_SCHEDULED = 1;
130a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private static final int STATE_DECODING = 2;
131a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private static final int STATE_WAITING_TO_SWAP = 3;
132a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private static final int STATE_READY_TO_SWAP = 4;
133a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
134a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private int mState;
135e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik    private int mCurrentLoop;
136e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik    private int mLoopBehavior = LOOP_DEFAULT;
137a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
138a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private long mLastSwap;
1393105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik    private long mNextSwap;
140a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private int mNextFrameToDecode;
141e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik    private OnFinishedListener mOnFinishedListener;
142a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
143a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    /**
144a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik     * Runs on decoding thread, only modifies mBackBitmap's pixels
145a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik     */
146a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private Runnable mDecodeRunnable = new Runnable() {
147a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        @Override
148a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        public void run() {
149a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            int nextFrame;
150a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            Bitmap bitmap;
151a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            synchronized (mLock) {
1524eb541aff092a057b27b917f09d33aba226dffedChris Craik                if (mDestroyed) return;
1533105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik
154a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik                nextFrame = mNextFrameToDecode;
155a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik                if (nextFrame < 0) {
156a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik                    return;
157a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik                }
158a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik                bitmap = mBackBitmap;
159a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik                mState = STATE_DECODING;
160a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            }
161a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            int lastFrame = nextFrame - 2;
162a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            long invalidateTimeMs = mFrameSequenceState.getFrame(nextFrame, bitmap, lastFrame);
163a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
164a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            synchronized (mLock) {
165a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik                if (mNextFrameToDecode < 0 || mState != STATE_DECODING) return;
1663105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik                mNextSwap = invalidateTimeMs + mLastSwap;
167a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
168a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik                mState = STATE_WAITING_TO_SWAP;
169a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            }
1703105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik            scheduleSelf(FrameSequenceDrawable.this, mNextSwap);
171a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        }
172a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    };
173a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
174e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik    private Runnable mCallbackRunnable = new Runnable() {
175e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik        @Override
176e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik        public void run() {
177e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik            if (mOnFinishedListener != null) {
178e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik                mOnFinishedListener.onFinished(FrameSequenceDrawable.this);
179e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik            }
180e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik        }
181e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik    };
182a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
1833105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik    private static Bitmap acquireAndValidateBitmap(BitmapProvider bitmapProvider,
1843105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik            int minWidth, int minHeight) {
1853105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        Bitmap bitmap = bitmapProvider.acquireBitmap(minWidth, minHeight);
1863105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik
1873105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        if (bitmap.getWidth() < minWidth
1883105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik                || bitmap.getHeight() < minHeight
1893105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik                || bitmap.getConfig() != Bitmap.Config.ARGB_8888) {
1903105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik            throw new IllegalArgumentException("Invalid bitmap provided");
1913105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        }
1923105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik
1933105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        return bitmap;
1943105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik    }
1953105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik
196a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    public FrameSequenceDrawable(FrameSequence frameSequence) {
1973105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        this(frameSequence, sAllocatingBitmapProvider);
1983105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik    }
1993105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik
2003105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik    public FrameSequenceDrawable(FrameSequence frameSequence, BitmapProvider bitmapProvider) {
2014eb541aff092a057b27b917f09d33aba226dffedChris Craik        if (frameSequence == null || bitmapProvider == null) throw new IllegalArgumentException();
202a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
203a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        mFrameSequence = frameSequence;
204a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        mFrameSequenceState = frameSequence.createState();
205a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        final int width = frameSequence.getWidth();
206a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        final int height = frameSequence.getHeight();
207a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
2084eb541aff092a057b27b917f09d33aba226dffedChris Craik        mBitmapProvider = bitmapProvider;
2093105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        mFrontBitmap = acquireAndValidateBitmap(bitmapProvider, width, height);
2103105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        mBackBitmap = acquireAndValidateBitmap(bitmapProvider, width, height);
211a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        mSrcRect = new Rect(0, 0, width, height);
212a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        mPaint = new Paint();
213a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        mPaint.setFilterBitmap(true);
214a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
215a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        mLastSwap = 0;
216a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
217a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        mNextFrameToDecode = -1;
218a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        mFrameSequenceState.getFrame(0, mFrontBitmap, -1);
219a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        initializeDecodingThread();
220a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    }
221a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
2224eb541aff092a057b27b917f09d33aba226dffedChris Craik    private void checkDestroyedLocked() {
2234eb541aff092a057b27b917f09d33aba226dffedChris Craik        if (mDestroyed) {
2243105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik            throw new IllegalStateException("Cannot perform operation on recycled drawable");
2253105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        }
2263105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik    }
2273105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik
2284eb541aff092a057b27b917f09d33aba226dffedChris Craik    public boolean isDestroyed() {
2293105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        synchronized (mLock) {
2304eb541aff092a057b27b917f09d33aba226dffedChris Craik            return mDestroyed;
2313105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        }
2323105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik    }
2333105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik
2343105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik    /**
2353105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik     * Marks the drawable as permanently recycled (and thus unusable), and releases any owned
2364eb541aff092a057b27b917f09d33aba226dffedChris Craik     * Bitmaps drawable to its BitmapProvider, if attached.
2373105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik     *
2384eb541aff092a057b27b917f09d33aba226dffedChris Craik     * If no BitmapProvider is attached to the drawable, recycle() is called on the Bitmaps.
2393105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik     */
2404eb541aff092a057b27b917f09d33aba226dffedChris Craik    public void destroy() {
2414eb541aff092a057b27b917f09d33aba226dffedChris Craik        destroy(mBitmapProvider);
2424eb541aff092a057b27b917f09d33aba226dffedChris Craik    }
2434eb541aff092a057b27b917f09d33aba226dffedChris Craik
2444eb541aff092a057b27b917f09d33aba226dffedChris Craik    private void destroy(BitmapProvider bitmapProvider) {
2453105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        if (bitmapProvider == null) {
2463105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik            throw new IllegalStateException("BitmapProvider must be non-null");
2473105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        }
2483105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik
2493105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        Bitmap bitmapToReleaseA;
2503105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        Bitmap bitmapToReleaseB;
2513105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        synchronized (mLock) {
2524eb541aff092a057b27b917f09d33aba226dffedChris Craik            checkDestroyedLocked();
2533105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik
2543105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik            bitmapToReleaseA = mFrontBitmap;
2553105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik            bitmapToReleaseB = mBackBitmap;
2563105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik
2573105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik            mFrontBitmap = null;
2583105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik            mBackBitmap = null;
2594eb541aff092a057b27b917f09d33aba226dffedChris Craik            mDestroyed = true;
2603105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        }
2613105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik
2624eb541aff092a057b27b917f09d33aba226dffedChris Craik        // For simplicity and safety, we don't destroy the state object here
2633105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        bitmapProvider.releaseBitmap(bitmapToReleaseA);
2643105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik        bitmapProvider.releaseBitmap(bitmapToReleaseB);
2653105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik    }
2663105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik
267a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    @Override
268a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    protected void finalize() throws Throwable {
269a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        try {
2704eb541aff092a057b27b917f09d33aba226dffedChris Craik            mFrameSequenceState.destroy();
271a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        } finally {
272a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            super.finalize();
273a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        }
274a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    }
275a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
276a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    @Override
277a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    public void draw(Canvas canvas) {
278a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        synchronized (mLock) {
2794eb541aff092a057b27b917f09d33aba226dffedChris Craik            checkDestroyedLocked();
2803105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik            if (mState == STATE_WAITING_TO_SWAP) {
2813105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik                // may have failed to schedule mark ready runnable,
2823105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik                // so go ahead and swap if swapping is due
2833105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik                if (mNextSwap - SystemClock.uptimeMillis() <= 0) {
2843105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik                    mState = STATE_READY_TO_SWAP;
2853105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik                }
2863105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik            }
2873105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik
288a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            if (isRunning() && mState == STATE_READY_TO_SWAP) {
289a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik                // Because draw has occurred, the view system is guaranteed to no longer hold a
290a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik                // reference to the old mFrontBitmap, so we now use it to produce the next frame
291a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik                Bitmap tmp = mBackBitmap;
292a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik                mBackBitmap = mFrontBitmap;
293a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik                mFrontBitmap = tmp;
294a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
295a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik                mLastSwap = SystemClock.uptimeMillis();
296e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik
297e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik                boolean continueLooping = true;
298e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik                if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) {
299e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik                    mCurrentLoop++;
300e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik                    if ((mLoopBehavior == LOOP_ONCE && mCurrentLoop == 1) ||
3013105099a73d4fea3408ea0cf6b358fff77dc8b67Chris Craik                            (mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) {
302e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik                        continueLooping = false;
303e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik                    }
304e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik                }
305e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik
306e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik                if (continueLooping) {
307e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik                    scheduleDecodeLocked();
308e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik                } else {
309e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik                    scheduleSelf(mCallbackRunnable, 0);
310e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik                }
311a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            }
312a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        }
313a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
314a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        canvas.drawBitmap(mFrontBitmap, mSrcRect, getBounds(), mPaint);
315a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    }
316a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
317a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    private void scheduleDecodeLocked() {
318a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        mState = STATE_SCHEDULED;
319a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        mNextFrameToDecode = (mNextFrameToDecode + 1) % mFrameSequence.getFrameCount();
320a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        sDecodingThreadHandler.post(mDecodeRunnable);
321a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    }
322a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
323a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    @Override
324a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    public void run() {
325a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        // set ready to swap
326a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        synchronized (mLock) {
327a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            if (mState != STATE_WAITING_TO_SWAP || mNextFrameToDecode < 0) return;
328a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            mState = STATE_READY_TO_SWAP;
329a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        }
330a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        invalidateSelf();
331a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    }
332a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
333a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    @Override
334a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    public void start() {
335a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        if (!isRunning()) {
336a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            synchronized (mLock) {
3374eb541aff092a057b27b917f09d33aba226dffedChris Craik                checkDestroyedLocked();
338a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik                if (mState == STATE_SCHEDULED) return; // already scheduled
339e36c5d675c8c2f900ef186a55edf71ce36ca9fa0Chris Craik                mCurrentLoop = 0;
340a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik                scheduleDecodeLocked();
341a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            }
342a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        }
343a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    }
344a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
345a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    @Override
346a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    public void stop() {
347a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        if (isRunning()) {
348a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            unscheduleSelf(this);
349a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        }
350a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    }
351a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
352a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    @Override
353a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    public boolean isRunning() {
354a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        synchronized (mLock) {
3554eb541aff092a057b27b917f09d33aba226dffedChris Craik            return mNextFrameToDecode > -1 && !mDestroyed;
356a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        }
357a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    }
358a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
359a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    @Override
360a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    public void unscheduleSelf(Runnable what) {
361a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        synchronized (mLock) {
362a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            mNextFrameToDecode = -1;
363a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        }
364a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        super.unscheduleSelf(what);
365a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    }
366a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
367a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    @Override
368a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    public boolean setVisible(boolean visible, boolean restart) {
369a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        boolean changed = super.setVisible(visible, restart);
370a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
371a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        if (!visible) {
372a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            stop();
373a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        } else if (restart || changed) {
374a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            stop();
375a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik            start();
376a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        }
377a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
378a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        return changed;
379a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    }
380a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
381a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    // drawing properties
382a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
383a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    @Override
384a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    public void setFilterBitmap(boolean filter) {
385a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        mPaint.setFilterBitmap(filter);
386a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    }
387a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
388a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    @Override
389a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    public void setAlpha(int alpha) {
390a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        mPaint.setAlpha(alpha);
391a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    }
392a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
393a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    @Override
394a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    public void setColorFilter(ColorFilter colorFilter) {
395a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        mPaint.setColorFilter(colorFilter);
396a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    }
397a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
398a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    @Override
399a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    public int getIntrinsicWidth() {
400a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        return mFrameSequence.getWidth();
401a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    }
402a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
403a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    @Override
404a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    public int getIntrinsicHeight() {
405a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        return mFrameSequence.getHeight();
406a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    }
407a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik
408a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    @Override
409a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    public int getOpacity() {
410a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik        return mFrameSequence.isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSPARENT;
411a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik    }
412a3ac0a2df64dcfb8b0b01f1cf05e9afd1439e1f4Chris Craik}
413