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