1b7f9224b1495db47eb8fd813b5912250e900770aChris Banes/*
2b7f9224b1495db47eb8fd813b5912250e900770aChris Banes * Copyright (C) 2015 The Android Open Source Project
3b7f9224b1495db47eb8fd813b5912250e900770aChris Banes *
4b7f9224b1495db47eb8fd813b5912250e900770aChris Banes * Licensed under the Apache License, Version 2.0 (the "License");
5b7f9224b1495db47eb8fd813b5912250e900770aChris Banes * you may not use this file except in compliance with the License.
6b7f9224b1495db47eb8fd813b5912250e900770aChris Banes * You may obtain a copy of the License at
7b7f9224b1495db47eb8fd813b5912250e900770aChris Banes *
8b7f9224b1495db47eb8fd813b5912250e900770aChris Banes *      http://www.apache.org/licenses/LICENSE-2.0
9b7f9224b1495db47eb8fd813b5912250e900770aChris Banes *
10b7f9224b1495db47eb8fd813b5912250e900770aChris Banes * Unless required by applicable law or agreed to in writing, software
11b7f9224b1495db47eb8fd813b5912250e900770aChris Banes * distributed under the License is distributed on an "AS IS" BASIS,
12b7f9224b1495db47eb8fd813b5912250e900770aChris Banes * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13b7f9224b1495db47eb8fd813b5912250e900770aChris Banes * See the License for the specific language governing permissions and
14b7f9224b1495db47eb8fd813b5912250e900770aChris Banes * limitations under the License.
15b7f9224b1495db47eb8fd813b5912250e900770aChris Banes */
16b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
17b7f9224b1495db47eb8fd813b5912250e900770aChris Banespackage android.support.design.widget;
18b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
19b7f9224b1495db47eb8fd813b5912250e900770aChris Banesimport android.os.Handler;
20b7f9224b1495db47eb8fd813b5912250e900770aChris Banesimport android.os.Looper;
21b7f9224b1495db47eb8fd813b5912250e900770aChris Banesimport android.os.Message;
22b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
23c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banesimport java.lang.ref.WeakReference;
24c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes
25b7f9224b1495db47eb8fd813b5912250e900770aChris Banes/**
26b7f9224b1495db47eb8fd813b5912250e900770aChris Banes * Manages {@link Snackbar}s.
27b7f9224b1495db47eb8fd813b5912250e900770aChris Banes */
28b7f9224b1495db47eb8fd813b5912250e900770aChris Banesclass SnackbarManager {
29b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
30b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    private static final int MSG_TIMEOUT = 0;
31b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
32b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    private static final int SHORT_DURATION_MS = 1500;
33b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    private static final int LONG_DURATION_MS = 2750;
34b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
35b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    private static SnackbarManager sSnackbarManager;
36b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
37b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    static SnackbarManager getInstance() {
38b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        if (sSnackbarManager == null) {
39b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            sSnackbarManager = new SnackbarManager();
40b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        }
41b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        return sSnackbarManager;
42b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
43b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
44b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    private final Object mLock;
45b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    private final Handler mHandler;
46b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
47b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    private SnackbarRecord mCurrentSnackbar;
48b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    private SnackbarRecord mNextSnackbar;
49b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
50b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    private SnackbarManager() {
51b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        mLock = new Object();
52b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
53b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            @Override
54b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            public boolean handleMessage(Message message) {
55b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                switch (message.what) {
56b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                    case MSG_TIMEOUT:
57b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                        handleTimeout((SnackbarRecord) message.obj);
58b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                        return true;
59b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                }
60b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                return false;
61b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            }
62b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        });
63b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
64b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
65b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    interface Callback {
66b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        void show();
67e51e533995e445f031fc8efcce6ff9a61e7066ccChris Banes        void dismiss(int event);
68b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
69b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
70b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    public void show(int duration, Callback callback) {
71b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        synchronized (mLock) {
725462d3e588481416a38e893bdb0f1073f82f8dccChris Banes            if (isCurrentSnackbarLocked(callback)) {
73b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                // Means that the callback is already in the queue. We'll just update the duration
74b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                mCurrentSnackbar.duration = duration;
75b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
76b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                // If this is the Snackbar currently being shown, call re-schedule it's
77b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                // timeout
78b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
79b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                scheduleTimeoutLocked(mCurrentSnackbar);
80b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                return;
815462d3e588481416a38e893bdb0f1073f82f8dccChris Banes            } else if (isNextSnackbarLocked(callback)) {
82b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                // We'll just update the duration
83b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                mNextSnackbar.duration = duration;
84b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            } else {
85b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                // Else, we need to create a new record and queue it
86b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                mNextSnackbar = new SnackbarRecord(duration, callback);
87b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            }
88b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
89e51e533995e445f031fc8efcce6ff9a61e7066ccChris Banes            if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar,
90e51e533995e445f031fc8efcce6ff9a61e7066ccChris Banes                    Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) {
91c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes                // If we currently have a Snackbar, try and cancel it and wait in line
92c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes                return;
93b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            } else {
94c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes                // Clear out the current snackbar
95c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes                mCurrentSnackbar = null;
96b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                // Otherwise, just show it now
97b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                showNextSnackbarLocked();
98b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            }
99b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        }
100b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
101b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
102e51e533995e445f031fc8efcce6ff9a61e7066ccChris Banes    public void dismiss(Callback callback, int event) {
103b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        synchronized (mLock) {
1045462d3e588481416a38e893bdb0f1073f82f8dccChris Banes            if (isCurrentSnackbarLocked(callback)) {
105e51e533995e445f031fc8efcce6ff9a61e7066ccChris Banes                cancelSnackbarLocked(mCurrentSnackbar, event);
1065462d3e588481416a38e893bdb0f1073f82f8dccChris Banes            } else if (isNextSnackbarLocked(callback)) {
107e51e533995e445f031fc8efcce6ff9a61e7066ccChris Banes                cancelSnackbarLocked(mNextSnackbar, event);
108b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            }
109b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        }
110b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
111b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
112b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    /**
113b7f9224b1495db47eb8fd813b5912250e900770aChris Banes     * Should be called when a Snackbar is no longer displayed. This is after any exit
114b7f9224b1495db47eb8fd813b5912250e900770aChris Banes     * animation has finished.
115b7f9224b1495db47eb8fd813b5912250e900770aChris Banes     */
116b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    public void onDismissed(Callback callback) {
117b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        synchronized (mLock) {
1185462d3e588481416a38e893bdb0f1073f82f8dccChris Banes            if (isCurrentSnackbarLocked(callback)) {
119b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                // If the callback is from a Snackbar currently show, remove it and show a new one
120b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                mCurrentSnackbar = null;
121b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                if (mNextSnackbar != null) {
122b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                    showNextSnackbarLocked();
123b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                }
124b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            }
125b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        }
126b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
127b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
128b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    /**
129b7f9224b1495db47eb8fd813b5912250e900770aChris Banes     * Should be called when a Snackbar is being shown. This is after any entrance animation has
130b7f9224b1495db47eb8fd813b5912250e900770aChris Banes     * finished.
131b7f9224b1495db47eb8fd813b5912250e900770aChris Banes     */
132b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    public void onShown(Callback callback) {
133b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        synchronized (mLock) {
1345462d3e588481416a38e893bdb0f1073f82f8dccChris Banes            if (isCurrentSnackbarLocked(callback)) {
135b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                scheduleTimeoutLocked(mCurrentSnackbar);
136b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            }
137b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        }
138b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
139b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
140b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    public void cancelTimeout(Callback callback) {
141b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        synchronized (mLock) {
1425462d3e588481416a38e893bdb0f1073f82f8dccChris Banes            if (isCurrentSnackbarLocked(callback)) {
143b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
144b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            }
145b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        }
146b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
147b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
148b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    public void restoreTimeout(Callback callback) {
149b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        synchronized (mLock) {
1505462d3e588481416a38e893bdb0f1073f82f8dccChris Banes            if (isCurrentSnackbarLocked(callback)) {
151b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                scheduleTimeoutLocked(mCurrentSnackbar);
152b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            }
153b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        }
154b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
155b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
1565462d3e588481416a38e893bdb0f1073f82f8dccChris Banes    public boolean isCurrent(Callback callback) {
1575462d3e588481416a38e893bdb0f1073f82f8dccChris Banes        synchronized (mLock) {
1585462d3e588481416a38e893bdb0f1073f82f8dccChris Banes            return isCurrentSnackbarLocked(callback);
1595462d3e588481416a38e893bdb0f1073f82f8dccChris Banes        }
1605462d3e588481416a38e893bdb0f1073f82f8dccChris Banes    }
1615462d3e588481416a38e893bdb0f1073f82f8dccChris Banes
1625462d3e588481416a38e893bdb0f1073f82f8dccChris Banes    public boolean isCurrentOrNext(Callback callback) {
1635462d3e588481416a38e893bdb0f1073f82f8dccChris Banes        synchronized (mLock) {
1645462d3e588481416a38e893bdb0f1073f82f8dccChris Banes            return isCurrentSnackbarLocked(callback) || isNextSnackbarLocked(callback);
1655462d3e588481416a38e893bdb0f1073f82f8dccChris Banes        }
1665462d3e588481416a38e893bdb0f1073f82f8dccChris Banes    }
1675462d3e588481416a38e893bdb0f1073f82f8dccChris Banes
168b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    private static class SnackbarRecord {
169c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes        private final WeakReference<Callback> callback;
170b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        private int duration;
171b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
172b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        SnackbarRecord(int duration, Callback callback) {
173c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes            this.callback = new WeakReference<>(callback);
174b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            this.duration = duration;
175b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        }
176c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes
177c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes        boolean isSnackbar(Callback callback) {
178c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes            return callback != null && this.callback.get() == callback;
179c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes        }
180b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
181b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
182b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    private void showNextSnackbarLocked() {
183b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        if (mNextSnackbar != null) {
184b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            mCurrentSnackbar = mNextSnackbar;
185b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            mNextSnackbar = null;
186c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes
187c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes            final Callback callback = mCurrentSnackbar.callback.get();
188c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes            if (callback != null) {
189c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes                callback.show();
190c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes            } else {
191c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes                // The callback doesn't exist any more, clear out the Snackbar
192c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes                mCurrentSnackbar = null;
193c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes            }
194b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        }
195b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
196b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
197e51e533995e445f031fc8efcce6ff9a61e7066ccChris Banes    private boolean cancelSnackbarLocked(SnackbarRecord record, int event) {
198c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes        final Callback callback = record.callback.get();
199c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes        if (callback != null) {
200bfd48d0521963754e04e407499ee9e278fe06c0fChris Banes            // Make sure we remove any timeouts for the SnackbarRecord
201bfd48d0521963754e04e407499ee9e278fe06c0fChris Banes            mHandler.removeCallbacksAndMessages(record);
202e51e533995e445f031fc8efcce6ff9a61e7066ccChris Banes            callback.dismiss(event);
203c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes            return true;
204c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes        }
205c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes        return false;
206b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
207b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
2085462d3e588481416a38e893bdb0f1073f82f8dccChris Banes    private boolean isCurrentSnackbarLocked(Callback callback) {
209c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes        return mCurrentSnackbar != null && mCurrentSnackbar.isSnackbar(callback);
210b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
211b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
2125462d3e588481416a38e893bdb0f1073f82f8dccChris Banes    private boolean isNextSnackbarLocked(Callback callback) {
213c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes        return mNextSnackbar != null && mNextSnackbar.isSnackbar(callback);
214b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
215b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
216b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    private void scheduleTimeoutLocked(SnackbarRecord r) {
2170bfb0e034ed6b4f7bbf58a111d2fc893e0553350Chris Banes        if (r.duration == Snackbar.LENGTH_INDEFINITE) {
2180bfb0e034ed6b4f7bbf58a111d2fc893e0553350Chris Banes            // If we're set to indefinite, we don't want to set a timeout
2190bfb0e034ed6b4f7bbf58a111d2fc893e0553350Chris Banes            return;
2200bfb0e034ed6b4f7bbf58a111d2fc893e0553350Chris Banes        }
2210bfb0e034ed6b4f7bbf58a111d2fc893e0553350Chris Banes
2222361e349a59da4d3791e5d2a0d842a84dba91b41Chris Banes        int durationMs = LONG_DURATION_MS;
2232361e349a59da4d3791e5d2a0d842a84dba91b41Chris Banes        if (r.duration > 0) {
2242361e349a59da4d3791e5d2a0d842a84dba91b41Chris Banes            durationMs = r.duration;
2252361e349a59da4d3791e5d2a0d842a84dba91b41Chris Banes        } else if (r.duration == Snackbar.LENGTH_SHORT) {
2262361e349a59da4d3791e5d2a0d842a84dba91b41Chris Banes            durationMs = SHORT_DURATION_MS;
2272361e349a59da4d3791e5d2a0d842a84dba91b41Chris Banes        }
228b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        mHandler.removeCallbacksAndMessages(r);
2292361e349a59da4d3791e5d2a0d842a84dba91b41Chris Banes        mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_TIMEOUT, r), durationMs);
230b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
231b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
232b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    private void handleTimeout(SnackbarRecord record) {
233b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        synchronized (mLock) {
234b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            if (mCurrentSnackbar == record || mNextSnackbar == record) {
235e51e533995e445f031fc8efcce6ff9a61e7066ccChris Banes                cancelSnackbarLocked(record, Snackbar.Callback.DISMISS_EVENT_TIMEOUT);
236b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            }
237b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        }
238b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
239b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
240b7f9224b1495db47eb8fd813b5912250e900770aChris Banes}
241