1/*
2 * Copyright (C) 2015 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.design.widget;
18
19import android.os.Handler;
20import android.os.Looper;
21import android.os.Message;
22
23import java.lang.ref.WeakReference;
24
25/**
26 * Manages {@link Snackbar}s.
27 */
28class SnackbarManager {
29
30    static final int MSG_TIMEOUT = 0;
31
32    private static final int SHORT_DURATION_MS = 1500;
33    private static final int LONG_DURATION_MS = 2750;
34
35    private static SnackbarManager sSnackbarManager;
36
37    static SnackbarManager getInstance() {
38        if (sSnackbarManager == null) {
39            sSnackbarManager = new SnackbarManager();
40        }
41        return sSnackbarManager;
42    }
43
44    private final Object mLock;
45    private final Handler mHandler;
46
47    private SnackbarRecord mCurrentSnackbar;
48    private SnackbarRecord mNextSnackbar;
49
50    private SnackbarManager() {
51        mLock = new Object();
52        mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
53            @Override
54            public boolean handleMessage(Message message) {
55                switch (message.what) {
56                    case MSG_TIMEOUT:
57                        handleTimeout((SnackbarRecord) message.obj);
58                        return true;
59                }
60                return false;
61            }
62        });
63    }
64
65    interface Callback {
66        void show();
67        void dismiss(int event);
68    }
69
70    public void show(int duration, Callback callback) {
71        synchronized (mLock) {
72            if (isCurrentSnackbarLocked(callback)) {
73                // Means that the callback is already in the queue. We'll just update the duration
74                mCurrentSnackbar.duration = duration;
75
76                // If this is the Snackbar currently being shown, call re-schedule it's
77                // timeout
78                mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
79                scheduleTimeoutLocked(mCurrentSnackbar);
80                return;
81            } else if (isNextSnackbarLocked(callback)) {
82                // We'll just update the duration
83                mNextSnackbar.duration = duration;
84            } else {
85                // Else, we need to create a new record and queue it
86                mNextSnackbar = new SnackbarRecord(duration, callback);
87            }
88
89            if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar,
90                    Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) {
91                // If we currently have a Snackbar, try and cancel it and wait in line
92                return;
93            } else {
94                // Clear out the current snackbar
95                mCurrentSnackbar = null;
96                // Otherwise, just show it now
97                showNextSnackbarLocked();
98            }
99        }
100    }
101
102    public void dismiss(Callback callback, int event) {
103        synchronized (mLock) {
104            if (isCurrentSnackbarLocked(callback)) {
105                cancelSnackbarLocked(mCurrentSnackbar, event);
106            } else if (isNextSnackbarLocked(callback)) {
107                cancelSnackbarLocked(mNextSnackbar, event);
108            }
109        }
110    }
111
112    /**
113     * Should be called when a Snackbar is no longer displayed. This is after any exit
114     * animation has finished.
115     */
116    public void onDismissed(Callback callback) {
117        synchronized (mLock) {
118            if (isCurrentSnackbarLocked(callback)) {
119                // If the callback is from a Snackbar currently show, remove it and show a new one
120                mCurrentSnackbar = null;
121                if (mNextSnackbar != null) {
122                    showNextSnackbarLocked();
123                }
124            }
125        }
126    }
127
128    /**
129     * Should be called when a Snackbar is being shown. This is after any entrance animation has
130     * finished.
131     */
132    public void onShown(Callback callback) {
133        synchronized (mLock) {
134            if (isCurrentSnackbarLocked(callback)) {
135                scheduleTimeoutLocked(mCurrentSnackbar);
136            }
137        }
138    }
139
140    public void cancelTimeout(Callback callback) {
141        synchronized (mLock) {
142            if (isCurrentSnackbarLocked(callback)) {
143                mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
144            }
145        }
146    }
147
148    public void restoreTimeout(Callback callback) {
149        synchronized (mLock) {
150            if (isCurrentSnackbarLocked(callback)) {
151                scheduleTimeoutLocked(mCurrentSnackbar);
152            }
153        }
154    }
155
156    public boolean isCurrent(Callback callback) {
157        synchronized (mLock) {
158            return isCurrentSnackbarLocked(callback);
159        }
160    }
161
162    public boolean isCurrentOrNext(Callback callback) {
163        synchronized (mLock) {
164            return isCurrentSnackbarLocked(callback) || isNextSnackbarLocked(callback);
165        }
166    }
167
168    private static class SnackbarRecord {
169        final WeakReference<Callback> callback;
170        int duration;
171
172        SnackbarRecord(int duration, Callback callback) {
173            this.callback = new WeakReference<>(callback);
174            this.duration = duration;
175        }
176
177        boolean isSnackbar(Callback callback) {
178            return callback != null && this.callback.get() == callback;
179        }
180    }
181
182    private void showNextSnackbarLocked() {
183        if (mNextSnackbar != null) {
184            mCurrentSnackbar = mNextSnackbar;
185            mNextSnackbar = null;
186
187            final Callback callback = mCurrentSnackbar.callback.get();
188            if (callback != null) {
189                callback.show();
190            } else {
191                // The callback doesn't exist any more, clear out the Snackbar
192                mCurrentSnackbar = null;
193            }
194        }
195    }
196
197    private boolean cancelSnackbarLocked(SnackbarRecord record, int event) {
198        final Callback callback = record.callback.get();
199        if (callback != null) {
200            // Make sure we remove any timeouts for the SnackbarRecord
201            mHandler.removeCallbacksAndMessages(record);
202            callback.dismiss(event);
203            return true;
204        }
205        return false;
206    }
207
208    private boolean isCurrentSnackbarLocked(Callback callback) {
209        return mCurrentSnackbar != null && mCurrentSnackbar.isSnackbar(callback);
210    }
211
212    private boolean isNextSnackbarLocked(Callback callback) {
213        return mNextSnackbar != null && mNextSnackbar.isSnackbar(callback);
214    }
215
216    private void scheduleTimeoutLocked(SnackbarRecord r) {
217        if (r.duration == Snackbar.LENGTH_INDEFINITE) {
218            // If we're set to indefinite, we don't want to set a timeout
219            return;
220        }
221
222        int durationMs = LONG_DURATION_MS;
223        if (r.duration > 0) {
224            durationMs = r.duration;
225        } else if (r.duration == Snackbar.LENGTH_SHORT) {
226            durationMs = SHORT_DURATION_MS;
227        }
228        mHandler.removeCallbacksAndMessages(r);
229        mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_TIMEOUT, r), durationMs);
230    }
231
232    void handleTimeout(SnackbarRecord record) {
233        synchronized (mLock) {
234            if (mCurrentSnackbar == record || mNextSnackbar == record) {
235                cancelSnackbarLocked(record, Snackbar.Callback.DISMISS_EVENT_TIMEOUT);
236            }
237        }
238    }
239
240}
241