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) {
72b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            if (isCurrentSnackbar(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;
81b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            } else if (isNextSnackbar(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) {
104b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            if (isCurrentSnackbar(callback)) {
105e51e533995e445f031fc8efcce6ff9a61e7066ccChris Banes                cancelSnackbarLocked(mCurrentSnackbar, event);
106e51e533995e445f031fc8efcce6ff9a61e7066ccChris Banes            } else if (isNextSnackbar(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) {
118b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            if (isCurrentSnackbar(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) {
134b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            if (isCurrentSnackbar(callback)) {
135b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                scheduleTimeoutLocked(mCurrentSnackbar);
136b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            }
137b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        }
138b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
139b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
140b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    public void cancelTimeout(Callback callback) {
141b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        synchronized (mLock) {
142b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            if (isCurrentSnackbar(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) {
150b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            if (isCurrentSnackbar(callback)) {
151b7f9224b1495db47eb8fd813b5912250e900770aChris Banes                scheduleTimeoutLocked(mCurrentSnackbar);
152b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            }
153b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        }
154b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
155b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
156b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    private static class SnackbarRecord {
157c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes        private final WeakReference<Callback> callback;
158b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        private int duration;
159b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
160b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        SnackbarRecord(int duration, Callback callback) {
161c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes            this.callback = new WeakReference<>(callback);
162b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            this.duration = duration;
163b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        }
164c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes
165c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes        boolean isSnackbar(Callback callback) {
166c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes            return callback != null && this.callback.get() == callback;
167c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes        }
168b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
169b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
170b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    private void showNextSnackbarLocked() {
171b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        if (mNextSnackbar != null) {
172b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            mCurrentSnackbar = mNextSnackbar;
173b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            mNextSnackbar = null;
174c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes
175c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes            final Callback callback = mCurrentSnackbar.callback.get();
176c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes            if (callback != null) {
177c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes                callback.show();
178c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes            } else {
179c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes                // The callback doesn't exist any more, clear out the Snackbar
180c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes                mCurrentSnackbar = null;
181c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes            }
182b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        }
183b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
184b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
185e51e533995e445f031fc8efcce6ff9a61e7066ccChris Banes    private boolean cancelSnackbarLocked(SnackbarRecord record, int event) {
186c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes        final Callback callback = record.callback.get();
187c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes        if (callback != null) {
188e51e533995e445f031fc8efcce6ff9a61e7066ccChris Banes            callback.dismiss(event);
189c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes            return true;
190c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes        }
191c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes        return false;
192b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
193b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
194b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    private boolean isCurrentSnackbar(Callback callback) {
195c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes        return mCurrentSnackbar != null && mCurrentSnackbar.isSnackbar(callback);
196b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
197b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
198b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    private boolean isNextSnackbar(Callback callback) {
199c482f89070ee5032081e394f77a9a1e63c3cd7a8Chris Banes        return mNextSnackbar != null && mNextSnackbar.isSnackbar(callback);
200b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
201b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
202b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    private void scheduleTimeoutLocked(SnackbarRecord r) {
2030bfb0e034ed6b4f7bbf58a111d2fc893e0553350Chris Banes        if (r.duration == Snackbar.LENGTH_INDEFINITE) {
2040bfb0e034ed6b4f7bbf58a111d2fc893e0553350Chris Banes            // If we're set to indefinite, we don't want to set a timeout
2050bfb0e034ed6b4f7bbf58a111d2fc893e0553350Chris Banes            return;
2060bfb0e034ed6b4f7bbf58a111d2fc893e0553350Chris Banes        }
2070bfb0e034ed6b4f7bbf58a111d2fc893e0553350Chris Banes
2082361e349a59da4d3791e5d2a0d842a84dba91b41Chris Banes        int durationMs = LONG_DURATION_MS;
2092361e349a59da4d3791e5d2a0d842a84dba91b41Chris Banes        if (r.duration > 0) {
2102361e349a59da4d3791e5d2a0d842a84dba91b41Chris Banes            durationMs = r.duration;
2112361e349a59da4d3791e5d2a0d842a84dba91b41Chris Banes        } else if (r.duration == Snackbar.LENGTH_SHORT) {
2122361e349a59da4d3791e5d2a0d842a84dba91b41Chris Banes            durationMs = SHORT_DURATION_MS;
2132361e349a59da4d3791e5d2a0d842a84dba91b41Chris Banes        }
214b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        mHandler.removeCallbacksAndMessages(r);
2152361e349a59da4d3791e5d2a0d842a84dba91b41Chris Banes        mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_TIMEOUT, r), durationMs);
216b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
217b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
218b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    private void handleTimeout(SnackbarRecord record) {
219b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        synchronized (mLock) {
220b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            if (mCurrentSnackbar == record || mNextSnackbar == record) {
221e51e533995e445f031fc8efcce6ff9a61e7066ccChris Banes                cancelSnackbarLocked(record, Snackbar.Callback.DISMISS_EVENT_TIMEOUT);
222b7f9224b1495db47eb8fd813b5912250e900770aChris Banes            }
223b7f9224b1495db47eb8fd813b5912250e900770aChris Banes        }
224b7f9224b1495db47eb8fd813b5912250e900770aChris Banes    }
225b7f9224b1495db47eb8fd813b5912250e900770aChris Banes
226b7f9224b1495db47eb8fd813b5912250e900770aChris Banes}
227