HeadsUpManager.java revision 31d9ef7a402b58b10758da1d71ff5e2181abe8a4
1/*
2 * Copyright (C) 2011 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 com.android.systemui.statusbar.policy;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.database.ContentObserver;
22import android.os.Handler;
23import android.os.SystemClock;
24import android.provider.Settings;
25import android.util.ArrayMap;
26import android.util.Log;
27import android.util.Pools;
28import android.view.ViewTreeObserver;
29import android.view.accessibility.AccessibilityEvent;
30
31import com.android.systemui.R;
32import com.android.systemui.statusbar.ExpandableNotificationRow;
33import com.android.systemui.statusbar.NotificationData;
34import com.android.systemui.statusbar.phone.PhoneStatusBar;
35
36import java.io.FileDescriptor;
37import java.io.PrintWriter;
38import java.util.HashMap;
39import java.util.HashSet;
40import java.util.Stack;
41import java.util.TreeSet;
42
43public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsListener {
44    private static final String TAG = "HeadsUpManager";
45    private static final boolean DEBUG = false;
46    private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms";
47
48    private final int mHeadsUpNotificationDecay;
49    private final int mMinimumDisplayTime;
50
51    private final int mTouchSensitivityDelay;
52    private final ArrayMap<String, Long> mSnoozedPackages;
53    private final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>();
54    private final int mDefaultSnoozeLengthMs;
55    private final Handler mHandler = new Handler();
56    private final Pools.Pool<HeadsUpEntry> mEntryPool = new Pools.Pool<HeadsUpEntry>() {
57
58        private Stack<HeadsUpEntry> mPoolObjects = new Stack<>();
59
60        @Override
61        public HeadsUpEntry acquire() {
62            if (!mPoolObjects.isEmpty()) {
63                return mPoolObjects.pop();
64            }
65            return new HeadsUpEntry();
66        }
67
68        @Override
69        public boolean release(HeadsUpEntry instance) {
70            instance.removeAutoCancelCallbacks();
71            mPoolObjects.push(instance);
72            return true;
73        }
74    };
75
76
77    private PhoneStatusBar mBar;
78    private int mSnoozeLengthMs;
79    private ContentObserver mSettingsObserver;
80    private HashMap<String, HeadsUpEntry> mHeadsUpEntries = new HashMap<>();
81    private TreeSet<HeadsUpEntry> mSortedEntries = new TreeSet<>();
82    private HashSet<String> mSwipedOutKeys = new HashSet<>();
83    private int mUser;
84    private Clock mClock;
85    private boolean mReleaseOnExpandFinish;
86    private boolean mTrackingHeadsUp;
87    private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>();
88    private boolean mIsExpanded;
89    private boolean mHasPinnedHeadsUp;
90    private int[] mTmpTwoArray = new int[2];
91
92    public HeadsUpManager(final Context context, ViewTreeObserver observer) {
93        Resources resources = context.getResources();
94        mTouchSensitivityDelay = resources.getInteger(R.integer.heads_up_sensitivity_delay);
95        if (DEBUG) Log.v(TAG, "create() " + mTouchSensitivityDelay);
96        mSnoozedPackages = new ArrayMap<>();
97        mDefaultSnoozeLengthMs = resources.getInteger(R.integer.heads_up_default_snooze_length_ms);
98        mSnoozeLengthMs = mDefaultSnoozeLengthMs;
99        mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time);
100        mHeadsUpNotificationDecay = resources.getInteger(R.integer.heads_up_notification_decay);
101        mClock = new Clock();
102
103        mSnoozeLengthMs = Settings.Global.getInt(context.getContentResolver(),
104                SETTING_HEADS_UP_SNOOZE_LENGTH_MS, mDefaultSnoozeLengthMs);
105        mSettingsObserver = new ContentObserver(mHandler) {
106            @Override
107            public void onChange(boolean selfChange) {
108                final int packageSnoozeLengthMs = Settings.Global.getInt(
109                        context.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1);
110                if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) {
111                    mSnoozeLengthMs = packageSnoozeLengthMs;
112                    if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
113                }
114            }
115        };
116        context.getContentResolver().registerContentObserver(
117                Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false,
118                mSettingsObserver);
119        if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
120        observer.addOnComputeInternalInsetsListener(this);
121    }
122
123    public void setBar(PhoneStatusBar bar) {
124        mBar = bar;
125    }
126
127    public void addListener(OnHeadsUpChangedListener listener) {
128        mListeners.add(listener);
129    }
130
131    public PhoneStatusBar getBar() {
132        return mBar;
133    }
134
135    /**
136     * Called when posting a new notification to the heads up.
137     */
138    public void showNotification(NotificationData.Entry headsUp) {
139        if (DEBUG) Log.v(TAG, "showNotification");
140        addHeadsUpEntry(headsUp);
141        updateNotification(headsUp, true);
142        headsUp.setInterruption();
143    }
144
145    /**
146     * Called when updating or posting a notification to the heads up.
147     */
148    public void updateNotification(NotificationData.Entry headsUp, boolean alert) {
149        if (DEBUG) Log.v(TAG, "updateNotification");
150
151        headsUp.row.setChildrenExpanded(false /* expanded */, false /* animated */);
152        headsUp.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
153
154        if (alert) {
155            HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(headsUp.key);
156            headsUpEntry.updateEntry();
157            setEntryToShade(headsUpEntry, mIsExpanded, false /* justAdded */, false);
158        }
159    }
160
161    private void addHeadsUpEntry(NotificationData.Entry entry) {
162        HeadsUpEntry headsUpEntry = mEntryPool.acquire();
163
164        // This will also add the entry to the sortedList
165        headsUpEntry.setEntry(entry);
166        mHeadsUpEntries.put(entry.key, headsUpEntry);
167        entry.row.setHeadsUp(true);
168        setEntryToShade(headsUpEntry, mIsExpanded /* inShade */, true /* justAdded */, false);
169        for (OnHeadsUpChangedListener listener : mListeners) {
170            listener.OnHeadsUpStateChanged(entry, true);
171        }
172        entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
173    }
174
175    private void setEntryToShade(HeadsUpEntry headsUpEntry, boolean inShade, boolean justAdded,
176            boolean forceImmediate) {
177        ExpandableNotificationRow row = headsUpEntry.entry.row;
178        if (row.isInShade() != inShade || justAdded) {
179            row.setInShade(inShade);
180            if (!justAdded || !inShade) {
181                updatePinnedHeadsUpState(forceImmediate);
182                for (OnHeadsUpChangedListener listener : mListeners) {
183                    listener.OnHeadsUpPinnedChanged(row, !inShade);
184                }
185            }
186        }
187    }
188
189    private void removeHeadsUpEntry(NotificationData.Entry entry) {
190        HeadsUpEntry remove = mHeadsUpEntries.remove(entry.key);
191        mSortedEntries.remove(remove);
192        mEntryPool.release(remove);
193        entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
194        entry.row.setHeadsUp(false);
195        setEntryToShade(remove, true /* inShade */, false /* justAdded */,
196                false /* forceImmediate */);
197        for (OnHeadsUpChangedListener listener : mListeners) {
198            listener.OnHeadsUpStateChanged(entry, false);
199        }
200    }
201
202    private void updatePinnedHeadsUpState(boolean forceImmediate) {
203        boolean hasPinnedHeadsUp = hasPinnedHeadsUpInternal();
204        if (hasPinnedHeadsUp == mHasPinnedHeadsUp) {
205            return;
206        }
207        mHasPinnedHeadsUp = hasPinnedHeadsUp;
208        for (OnHeadsUpChangedListener listener :mListeners) {
209            listener.OnPinnedHeadsUpExistChanged(hasPinnedHeadsUp, forceImmediate);
210        }
211    }
212
213    /**
214     * React to the removal of the notification in the heads up.
215     *
216     * @return true if the notification was removed and false if it still needs to be kept around
217     * for a bit since it wasn't shown long enough
218     */
219    public boolean removeNotification(String key) {
220        if (DEBUG) Log.v(TAG, "remove");
221        if (wasShownLongEnough(key)) {
222            releaseImmediately(key);
223            return true;
224        } else {
225            getHeadsUpEntry(key).hideAsSoonAsPossible();
226            return false;
227        }
228    }
229
230    private boolean wasShownLongEnough(String key) {
231        HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
232        HeadsUpEntry topEntry = getTopEntry();
233        if (mSwipedOutKeys.contains(key)) {
234            // We always instantly dismiss views being manually swiped out.
235            mSwipedOutKeys.remove(key);
236            return true;
237        }
238        if (headsUpEntry != topEntry) {
239            return true;
240        }
241        return headsUpEntry.wasShownLongEnough();
242    }
243
244    public boolean isHeadsUp(String key) {
245        return mHeadsUpEntries.containsKey(key);
246    }
247
248
249    /**
250     * Push any current Heads Up notification down into the shade.
251     */
252    public void releaseAllImmediately() {
253        if (DEBUG) Log.v(TAG, "releaseAllImmediately");
254        HashSet<String> keys = new HashSet<>(mHeadsUpEntries.keySet());
255        for (String key: keys) {
256            releaseImmediately(key);
257        }
258    }
259
260    public void releaseImmediately(String key) {
261        HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
262        if (headsUpEntry == null) {
263            return;
264        }
265        NotificationData.Entry shadeEntry = headsUpEntry.entry;
266        removeHeadsUpEntry(shadeEntry);
267    }
268
269    public boolean isSnoozed(String packageName) {
270        final String key = snoozeKey(packageName, mUser);
271        Long snoozedUntil = mSnoozedPackages.get(key);
272        if (snoozedUntil != null) {
273            if (snoozedUntil > SystemClock.elapsedRealtime()) {
274                if (DEBUG) Log.v(TAG, key + " snoozed");
275                return true;
276            }
277            mSnoozedPackages.remove(packageName);
278        }
279        return false;
280    }
281
282    public void snooze() {
283        for (String key: mHeadsUpEntries.keySet()) {
284            HeadsUpEntry entry = mHeadsUpEntries.get(key);
285            String packageName = entry.entry.notification.getPackageName();
286            mSnoozedPackages.put(snoozeKey(packageName, mUser),
287                    SystemClock.elapsedRealtime() + mSnoozeLengthMs);
288        }
289        mReleaseOnExpandFinish = true;
290    }
291
292    private static String snoozeKey(String packageName, int user) {
293        return user + "," + packageName;
294    }
295
296    private HeadsUpEntry getHeadsUpEntry(String key) {
297        return mHeadsUpEntries.get(key);
298    }
299
300    public NotificationData.Entry getEntry(String key) {
301        return mHeadsUpEntries.get(key).entry;
302    }
303
304    public TreeSet<HeadsUpEntry> getSortedEntries() {
305        return mSortedEntries;
306    }
307
308    public HeadsUpEntry getTopEntry() {
309        return mSortedEntries.isEmpty() ? null : mSortedEntries.first();
310    }
311
312    /**
313     * @param key the key of the touched notification
314     * @return whether the touch is valid and should not be discarded
315     */
316    public boolean shouldSwallowClick(String key) {
317        if (mClock.currentTimeMillis() < mHeadsUpEntries.get(key).postTime) {
318            return true;
319        }
320        return false;
321    }
322
323    public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
324        if (!mIsExpanded && mHasPinnedHeadsUp) {
325            int minX = Integer.MAX_VALUE;
326            int maxX = 0;
327            int minY = Integer.MAX_VALUE;
328            int maxY = 0;
329            for (HeadsUpEntry entry: mSortedEntries) {
330                ExpandableNotificationRow row = entry.entry.row;
331                if (!row.isInShade()) {
332                    row.getLocationOnScreen(mTmpTwoArray);
333                    minX = Math.min(minX, mTmpTwoArray[0]);
334                    minY = Math.min(minY, 0);
335                    maxX = Math.max(maxX, mTmpTwoArray[0] + row.getWidth());
336                    maxY = Math.max(maxY, row.getHeadsUpHeight());
337                }
338            }
339
340            info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
341            info.touchableRegion.set(minX, minY, maxX, maxY);
342        }
343    }
344
345    public void setUser(int user) {
346        mUser = user;
347    }
348
349    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
350        pw.println("HeadsUpManager state:");
351        pw.print("  mTouchSensitivityDelay="); pw.println(mTouchSensitivityDelay);
352        pw.print("  mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
353        pw.print("  now="); pw.println(SystemClock.elapsedRealtime());
354        pw.print("  mUser="); pw.println(mUser);
355        for (HeadsUpEntry entry: mSortedEntries) {
356            pw.print("  HeadsUpEntry="); pw.println(entry.entry);
357        }
358        int N = mSnoozedPackages.size();
359        pw.println("  snoozed packages: " + N);
360        for (int i = 0; i < N; i++) {
361            pw.print("    "); pw.print(mSnoozedPackages.valueAt(i));
362            pw.print(", "); pw.println(mSnoozedPackages.keyAt(i));
363        }
364    }
365
366    public boolean hasPinnedHeadsUp() {
367        return mHasPinnedHeadsUp;
368    }
369
370    private boolean hasPinnedHeadsUpInternal() {
371        for (String key: mHeadsUpEntries.keySet()) {
372            HeadsUpEntry entry = mHeadsUpEntries.get(key);
373            if (!entry.entry.row.isInShade()) {
374                return true;
375            }
376        }
377        return false;
378    }
379
380    public void addSwipedOutKey(String key) {
381        mSwipedOutKeys.add(key);
382    }
383
384    public float getHighestPinnedHeadsUp() {
385        float max = 0;
386        for (HeadsUpEntry entry: mSortedEntries) {
387            if (!entry.entry.row.isInShade()) {
388                max = Math.max(max, entry.entry.row.getActualHeight());
389            }
390        }
391        return max;
392    }
393
394    public void releaseAllToShade() {
395        for (String key: mHeadsUpEntries.keySet()) {
396            HeadsUpEntry entry = mHeadsUpEntries.get(key);
397            setEntryToShade(entry, true /* toShade */, false /* justAdded */,
398                    true /* forceImmediate */);
399        }
400    }
401
402    public void onExpandingFinished() {
403        if (mReleaseOnExpandFinish) {
404            releaseAllImmediately();
405            mReleaseOnExpandFinish = false;
406        } else {
407            for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) {
408                removeHeadsUpEntry(entry);
409            }
410            mEntriesToRemoveAfterExpand.clear();
411        }
412    }
413
414    public void setTrackingHeadsUp(boolean trackingHeadsUp) {
415        mTrackingHeadsUp = trackingHeadsUp;
416    }
417
418    public void setIsExpanded(boolean isExpanded) {
419        if (isExpanded != mIsExpanded) {
420            mIsExpanded = isExpanded;
421            if (isExpanded) {
422                releaseAllToShade();
423            }
424        }
425    }
426
427    public int getTopHeadsUpHeight() {
428        HeadsUpEntry topEntry = getTopEntry();
429        return topEntry != null ? topEntry.entry.row.getHeadsUpHeight() : 0;
430    }
431
432    public int compare(NotificationData.Entry a, NotificationData.Entry b) {
433        HeadsUpEntry aEntry = getHeadsUpEntry(a.key);
434        HeadsUpEntry bEntry = getHeadsUpEntry(b.key);
435        if (aEntry == null || bEntry == null) {
436            return aEntry == null ? 1 : -1;
437        }
438        return aEntry.compareTo(bEntry);
439    }
440
441    public class HeadsUpEntry implements Comparable<HeadsUpEntry> {
442        public NotificationData.Entry entry;
443        public long postTime;
444        public long earliestRemovaltime;
445        private Runnable mRemoveHeadsUpRunnable;
446
447        public void setEntry(final NotificationData.Entry entry) {
448            this.entry = entry;
449
450            // The actual post time will be just after the heads-up really slided in
451            postTime = mClock.currentTimeMillis() + mTouchSensitivityDelay;
452            mRemoveHeadsUpRunnable = new Runnable() {
453                @Override
454                public void run() {
455                    if (!mTrackingHeadsUp) {
456                        removeHeadsUpEntry(entry);
457                    } else {
458                        mEntriesToRemoveAfterExpand.add(entry);
459                    }
460                }
461            };
462            updateEntry();
463        }
464
465        public void updateEntry() {
466            long currentTime = mClock.currentTimeMillis();
467            earliestRemovaltime = currentTime + mMinimumDisplayTime;
468            postTime = Math.max(postTime, currentTime);
469            removeAutoCancelCallbacks();
470            if (canEntryDecay()) {
471                long finishTime = postTime + mHeadsUpNotificationDecay;
472                long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
473                mHandler.postDelayed(mRemoveHeadsUpRunnable, removeDelay);
474            }
475            updateSortOrder(HeadsUpEntry.this);
476        }
477
478        private boolean canEntryDecay() {
479            return entry.notification.getNotification().fullScreenIntent == null;
480        }
481
482        @Override
483        public int compareTo(HeadsUpEntry o) {
484            return postTime < o.postTime ? 1
485                    : postTime == o.postTime ? 0
486                            : -1;
487        }
488
489        public void removeAutoCancelCallbacks() {
490            mHandler.removeCallbacks(mRemoveHeadsUpRunnable);
491        }
492
493        public boolean wasShownLongEnough() {
494            return earliestRemovaltime < mClock.currentTimeMillis();
495        }
496
497        public void hideAsSoonAsPossible() {
498            removeAutoCancelCallbacks();
499            mHandler.postDelayed(mRemoveHeadsUpRunnable,
500                    earliestRemovaltime - mClock.currentTimeMillis());
501        }
502    }
503
504    /**
505     * Update the sorted heads up order.
506     *
507     * @param headsUpEntry the headsUp that changed
508     */
509    private void updateSortOrder(HeadsUpEntry headsUpEntry) {
510        mSortedEntries.remove(headsUpEntry);
511        mSortedEntries.add(headsUpEntry);
512    }
513
514    public static class Clock {
515        public long currentTimeMillis() {
516            return SystemClock.elapsedRealtime();
517        }
518    }
519
520    public interface OnHeadsUpChangedListener {
521        void OnPinnedHeadsUpExistChanged(boolean exist, boolean changeImmediatly);
522        void OnHeadsUpPinnedChanged(ExpandableNotificationRow headsUp, boolean isHeadsUp);
523        void OnHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp);
524    }
525}
526