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.ArraySet;
27import android.util.Log;
28import android.util.Pools;
29import android.view.View;
30import android.view.ViewTreeObserver;
31import android.view.accessibility.AccessibilityEvent;
32
33import com.android.internal.logging.MetricsLogger;
34import com.android.systemui.R;
35import com.android.systemui.statusbar.ExpandableNotificationRow;
36import com.android.systemui.statusbar.NotificationData;
37import com.android.systemui.statusbar.StatusBarState;
38import com.android.systemui.statusbar.notification.VisualStabilityManager;
39import com.android.systemui.statusbar.phone.NotificationGroupManager;
40import com.android.systemui.statusbar.phone.PhoneStatusBar;
41
42import java.io.FileDescriptor;
43import java.io.PrintWriter;
44import java.util.ArrayList;
45import java.util.Collection;
46import java.util.HashMap;
47import java.util.HashSet;
48import java.util.Stack;
49
50/**
51 * A manager which handles heads up notifications which is a special mode where
52 * they simply peek from the top of the screen.
53 */
54public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsListener,
55        VisualStabilityManager.Callback {
56    private static final String TAG = "HeadsUpManager";
57    private static final boolean DEBUG = false;
58    private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms";
59    private static final int TAG_CLICKED_NOTIFICATION = R.id.is_clicked_heads_up_tag;
60
61    private final int mHeadsUpNotificationDecay;
62    private final int mMinimumDisplayTime;
63
64    private final int mTouchAcceptanceDelay;
65    private final ArrayMap<String, Long> mSnoozedPackages;
66    private final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>();
67    private final int mDefaultSnoozeLengthMs;
68    private final Handler mHandler = new Handler();
69    private final Pools.Pool<HeadsUpEntry> mEntryPool = new Pools.Pool<HeadsUpEntry>() {
70
71        private Stack<HeadsUpEntry> mPoolObjects = new Stack<>();
72
73        @Override
74        public HeadsUpEntry acquire() {
75            if (!mPoolObjects.isEmpty()) {
76                return mPoolObjects.pop();
77            }
78            return new HeadsUpEntry();
79        }
80
81        @Override
82        public boolean release(HeadsUpEntry instance) {
83            instance.reset();
84            mPoolObjects.push(instance);
85            return true;
86        }
87    };
88
89    private final View mStatusBarWindowView;
90    private final int mStatusBarHeight;
91    private final Context mContext;
92    private final NotificationGroupManager mGroupManager;
93    private PhoneStatusBar mBar;
94    private int mSnoozeLengthMs;
95    private ContentObserver mSettingsObserver;
96    private HashMap<String, HeadsUpEntry> mHeadsUpEntries = new HashMap<>();
97    private HashSet<String> mSwipedOutKeys = new HashSet<>();
98    private int mUser;
99    private Clock mClock;
100    private boolean mReleaseOnExpandFinish;
101    private boolean mTrackingHeadsUp;
102    private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>();
103    private ArraySet<NotificationData.Entry> mEntriesToRemoveWhenReorderingAllowed
104            = new ArraySet<>();
105    private boolean mIsExpanded;
106    private boolean mHasPinnedNotification;
107    private int[] mTmpTwoArray = new int[2];
108    private boolean mHeadsUpGoingAway;
109    private boolean mWaitingOnCollapseWhenGoingAway;
110    private boolean mIsObserving;
111    private boolean mRemoteInputActive;
112    private VisualStabilityManager mVisualStabilityManager;
113    private int mStatusBarState;
114
115    public HeadsUpManager(final Context context, View statusBarWindowView,
116                          NotificationGroupManager groupManager) {
117        mContext = context;
118        Resources resources = mContext.getResources();
119        mTouchAcceptanceDelay = resources.getInteger(R.integer.touch_acceptance_delay);
120        mSnoozedPackages = new ArrayMap<>();
121        mDefaultSnoozeLengthMs = resources.getInteger(R.integer.heads_up_default_snooze_length_ms);
122        mSnoozeLengthMs = mDefaultSnoozeLengthMs;
123        mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time);
124        mHeadsUpNotificationDecay = resources.getInteger(R.integer.heads_up_notification_decay);
125        mClock = new Clock();
126
127        mSnoozeLengthMs = Settings.Global.getInt(context.getContentResolver(),
128                SETTING_HEADS_UP_SNOOZE_LENGTH_MS, mDefaultSnoozeLengthMs);
129        mSettingsObserver = new ContentObserver(mHandler) {
130            @Override
131            public void onChange(boolean selfChange) {
132                final int packageSnoozeLengthMs = Settings.Global.getInt(
133                        context.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1);
134                if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) {
135                    mSnoozeLengthMs = packageSnoozeLengthMs;
136                    if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
137                }
138            }
139        };
140        context.getContentResolver().registerContentObserver(
141                Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false,
142                mSettingsObserver);
143        mStatusBarWindowView = statusBarWindowView;
144        mGroupManager = groupManager;
145        mStatusBarHeight = resources.getDimensionPixelSize(
146                com.android.internal.R.dimen.status_bar_height);
147    }
148
149    private void updateTouchableRegionListener() {
150        boolean shouldObserve = mHasPinnedNotification || mHeadsUpGoingAway
151                || mWaitingOnCollapseWhenGoingAway;
152        if (shouldObserve == mIsObserving) {
153            return;
154        }
155        if (shouldObserve) {
156            mStatusBarWindowView.getViewTreeObserver().addOnComputeInternalInsetsListener(this);
157            mStatusBarWindowView.requestLayout();
158        } else {
159            mStatusBarWindowView.getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
160        }
161        mIsObserving = shouldObserve;
162    }
163
164    public void setBar(PhoneStatusBar bar) {
165        mBar = bar;
166    }
167
168    public void addListener(OnHeadsUpChangedListener listener) {
169        mListeners.add(listener);
170    }
171
172    public PhoneStatusBar getBar() {
173        return mBar;
174    }
175
176    /**
177     * Called when posting a new notification to the heads up.
178     */
179    public void showNotification(NotificationData.Entry headsUp) {
180        if (DEBUG) Log.v(TAG, "showNotification");
181        addHeadsUpEntry(headsUp);
182        updateNotification(headsUp, true);
183        headsUp.setInterruption();
184    }
185
186    /**
187     * Called when updating or posting a notification to the heads up.
188     */
189    public void updateNotification(NotificationData.Entry headsUp, boolean alert) {
190        if (DEBUG) Log.v(TAG, "updateNotification");
191
192        headsUp.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
193
194        if (alert) {
195            HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(headsUp.key);
196            if (headsUpEntry == null) {
197                // the entry was released before this update (i.e by a listener) This can happen
198                // with the groupmanager
199                return;
200            }
201            headsUpEntry.updateEntry();
202            setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUp));
203        }
204    }
205
206    private void addHeadsUpEntry(NotificationData.Entry entry) {
207        HeadsUpEntry headsUpEntry = mEntryPool.acquire();
208
209        // This will also add the entry to the sortedList
210        headsUpEntry.setEntry(entry);
211        mHeadsUpEntries.put(entry.key, headsUpEntry);
212        entry.row.setHeadsUp(true);
213        setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(entry));
214        for (OnHeadsUpChangedListener listener : mListeners) {
215            listener.onHeadsUpStateChanged(entry, true);
216        }
217        entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
218    }
219
220    private boolean shouldHeadsUpBecomePinned(NotificationData.Entry entry) {
221        return mStatusBarState != StatusBarState.KEYGUARD
222                && !mIsExpanded || hasFullScreenIntent(entry);
223    }
224
225    private boolean hasFullScreenIntent(NotificationData.Entry entry) {
226        return entry.notification.getNotification().fullScreenIntent != null;
227    }
228
229    private void setEntryPinned(HeadsUpEntry headsUpEntry, boolean isPinned) {
230        ExpandableNotificationRow row = headsUpEntry.entry.row;
231        if (row.isPinned() != isPinned) {
232            row.setPinned(isPinned);
233            updatePinnedMode();
234            for (OnHeadsUpChangedListener listener : mListeners) {
235                if (isPinned) {
236                    listener.onHeadsUpPinned(row);
237                } else {
238                    listener.onHeadsUpUnPinned(row);
239                }
240            }
241        }
242    }
243
244    private void removeHeadsUpEntry(NotificationData.Entry entry) {
245        HeadsUpEntry remove = mHeadsUpEntries.remove(entry.key);
246        entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
247        entry.row.setHeadsUp(false);
248        setEntryPinned(remove, false /* isPinned */);
249        for (OnHeadsUpChangedListener listener : mListeners) {
250            listener.onHeadsUpStateChanged(entry, false);
251        }
252        mEntryPool.release(remove);
253    }
254
255    private void updatePinnedMode() {
256        boolean hasPinnedNotification = hasPinnedNotificationInternal();
257        if (hasPinnedNotification == mHasPinnedNotification) {
258            return;
259        }
260        mHasPinnedNotification = hasPinnedNotification;
261        if (mHasPinnedNotification) {
262            MetricsLogger.count(mContext, "note_peek", 1);
263        }
264        updateTouchableRegionListener();
265        for (OnHeadsUpChangedListener listener : mListeners) {
266            listener.onHeadsUpPinnedModeChanged(hasPinnedNotification);
267        }
268    }
269
270    /**
271     * React to the removal of the notification in the heads up.
272     *
273     * @return true if the notification was removed and false if it still needs to be kept around
274     * for a bit since it wasn't shown long enough
275     */
276    public boolean removeNotification(String key, boolean ignoreEarliestRemovalTime) {
277        if (DEBUG) Log.v(TAG, "remove");
278        if (wasShownLongEnough(key) || ignoreEarliestRemovalTime) {
279            releaseImmediately(key);
280            return true;
281        } else {
282            getHeadsUpEntry(key).removeAsSoonAsPossible();
283            return false;
284        }
285    }
286
287    private boolean wasShownLongEnough(String key) {
288        HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
289        HeadsUpEntry topEntry = getTopEntry();
290        if (mSwipedOutKeys.contains(key)) {
291            // We always instantly dismiss views being manually swiped out.
292            mSwipedOutKeys.remove(key);
293            return true;
294        }
295        if (headsUpEntry != topEntry) {
296            return true;
297        }
298        return headsUpEntry.wasShownLongEnough();
299    }
300
301    public boolean isHeadsUp(String key) {
302        return mHeadsUpEntries.containsKey(key);
303    }
304
305    /**
306     * Push any current Heads Up notification down into the shade.
307     */
308    public void releaseAllImmediately() {
309        if (DEBUG) Log.v(TAG, "releaseAllImmediately");
310        ArrayList<String> keys = new ArrayList<>(mHeadsUpEntries.keySet());
311        for (String key : keys) {
312            releaseImmediately(key);
313        }
314    }
315
316    public void releaseImmediately(String key) {
317        HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
318        if (headsUpEntry == null) {
319            return;
320        }
321        NotificationData.Entry shadeEntry = headsUpEntry.entry;
322        removeHeadsUpEntry(shadeEntry);
323    }
324
325    public boolean isSnoozed(String packageName) {
326        final String key = snoozeKey(packageName, mUser);
327        Long snoozedUntil = mSnoozedPackages.get(key);
328        if (snoozedUntil != null) {
329            if (snoozedUntil > SystemClock.elapsedRealtime()) {
330                if (DEBUG) Log.v(TAG, key + " snoozed");
331                return true;
332            }
333            mSnoozedPackages.remove(packageName);
334        }
335        return false;
336    }
337
338    public void snooze() {
339        for (String key : mHeadsUpEntries.keySet()) {
340            HeadsUpEntry entry = mHeadsUpEntries.get(key);
341            String packageName = entry.entry.notification.getPackageName();
342            mSnoozedPackages.put(snoozeKey(packageName, mUser),
343                    SystemClock.elapsedRealtime() + mSnoozeLengthMs);
344        }
345        mReleaseOnExpandFinish = true;
346    }
347
348    private static String snoozeKey(String packageName, int user) {
349        return user + "," + packageName;
350    }
351
352    private HeadsUpEntry getHeadsUpEntry(String key) {
353        return mHeadsUpEntries.get(key);
354    }
355
356    public NotificationData.Entry getEntry(String key) {
357        return mHeadsUpEntries.get(key).entry;
358    }
359
360    public Collection<HeadsUpEntry> getAllEntries() {
361        return mHeadsUpEntries.values();
362    }
363
364    public HeadsUpEntry getTopEntry() {
365        if (mHeadsUpEntries.isEmpty()) {
366            return null;
367        }
368        HeadsUpEntry topEntry = null;
369        for (HeadsUpEntry entry: mHeadsUpEntries.values()) {
370            if (topEntry == null || entry.compareTo(topEntry) == -1) {
371                topEntry = entry;
372            }
373        }
374        return topEntry;
375    }
376
377    /**
378     * Decides whether a click is invalid for a notification, i.e it has not been shown long enough
379     * that a user might have consciously clicked on it.
380     *
381     * @param key the key of the touched notification
382     * @return whether the touch is invalid and should be discarded
383     */
384    public boolean shouldSwallowClick(String key) {
385        HeadsUpEntry entry = mHeadsUpEntries.get(key);
386        if (entry != null && mClock.currentTimeMillis() < entry.postTime) {
387            return true;
388        }
389        return false;
390    }
391
392    public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
393        if (mIsExpanded || mBar.isBouncerShowing()) {
394            // The touchable region is always the full area when expanded
395            return;
396        }
397        if (mHasPinnedNotification) {
398            ExpandableNotificationRow topEntry = getTopEntry().entry.row;
399            if (topEntry.isChildInGroup()) {
400                final ExpandableNotificationRow groupSummary
401                        = mGroupManager.getGroupSummary(topEntry.getStatusBarNotification());
402                if (groupSummary != null) {
403                    topEntry = groupSummary;
404                }
405            }
406            topEntry.getLocationOnScreen(mTmpTwoArray);
407            int minX = mTmpTwoArray[0];
408            int maxX = mTmpTwoArray[0] + topEntry.getWidth();
409            int maxY = topEntry.getIntrinsicHeight();
410
411            info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
412            info.touchableRegion.set(minX, 0, maxX, maxY);
413        } else if (mHeadsUpGoingAway || mWaitingOnCollapseWhenGoingAway) {
414            info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
415            info.touchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight);
416        }
417    }
418
419    public void setUser(int user) {
420        mUser = user;
421    }
422
423    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
424        pw.println("HeadsUpManager state:");
425        pw.print("  mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay);
426        pw.print("  mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
427        pw.print("  now="); pw.println(SystemClock.elapsedRealtime());
428        pw.print("  mUser="); pw.println(mUser);
429        for (HeadsUpEntry entry: mHeadsUpEntries.values()) {
430            pw.print("  HeadsUpEntry="); pw.println(entry.entry);
431        }
432        int N = mSnoozedPackages.size();
433        pw.println("  snoozed packages: " + N);
434        for (int i = 0; i < N; i++) {
435            pw.print("    "); pw.print(mSnoozedPackages.valueAt(i));
436            pw.print(", "); pw.println(mSnoozedPackages.keyAt(i));
437        }
438    }
439
440    public boolean hasPinnedHeadsUp() {
441        return mHasPinnedNotification;
442    }
443
444    private boolean hasPinnedNotificationInternal() {
445        for (String key : mHeadsUpEntries.keySet()) {
446            HeadsUpEntry entry = mHeadsUpEntries.get(key);
447            if (entry.entry.row.isPinned()) {
448                return true;
449            }
450        }
451        return false;
452    }
453
454    /**
455     * Notifies that a notification was swiped out and will be removed.
456     *
457     * @param key the notification key
458     */
459    public void addSwipedOutNotification(String key) {
460        mSwipedOutKeys.add(key);
461    }
462
463    public void unpinAll() {
464        for (String key : mHeadsUpEntries.keySet()) {
465            HeadsUpEntry entry = mHeadsUpEntries.get(key);
466            setEntryPinned(entry, false /* isPinned */);
467            // maybe it got un sticky
468            entry.updateEntry(false /* updatePostTime */);
469        }
470    }
471
472    public void onExpandingFinished() {
473        if (mReleaseOnExpandFinish) {
474            releaseAllImmediately();
475            mReleaseOnExpandFinish = false;
476        } else {
477            for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) {
478                if (isHeadsUp(entry.key)) {
479                    // Maybe the heads-up was removed already
480                    removeHeadsUpEntry(entry);
481                }
482            }
483        }
484        mEntriesToRemoveAfterExpand.clear();
485    }
486
487    public void setTrackingHeadsUp(boolean trackingHeadsUp) {
488        mTrackingHeadsUp = trackingHeadsUp;
489    }
490
491    public boolean isTrackingHeadsUp() {
492        return mTrackingHeadsUp;
493    }
494
495    public void setIsExpanded(boolean isExpanded) {
496        if (isExpanded != mIsExpanded) {
497            mIsExpanded = isExpanded;
498            if (isExpanded) {
499                // make sure our state is sane
500                mWaitingOnCollapseWhenGoingAway = false;
501                mHeadsUpGoingAway = false;
502                updateTouchableRegionListener();
503            }
504        }
505    }
506
507    /**
508     * @return the height of the top heads up notification when pinned. This is different from the
509     *         intrinsic height, which also includes whether the notification is system expanded and
510     *         is mainly used when dragging down from a heads up notification.
511     */
512    public int getTopHeadsUpPinnedHeight() {
513        HeadsUpEntry topEntry = getTopEntry();
514        if (topEntry == null || topEntry.entry == null) {
515            return 0;
516        }
517        ExpandableNotificationRow row = topEntry.entry.row;
518        if (row.isChildInGroup()) {
519            final ExpandableNotificationRow groupSummary
520                    = mGroupManager.getGroupSummary(row.getStatusBarNotification());
521            if (groupSummary != null) {
522                row = groupSummary;
523            }
524        }
525        return row.getPinnedHeadsUpHeight(true /* atLeastMinHeight */);
526    }
527
528    /**
529     * Compare two entries and decide how they should be ranked.
530     *
531     * @return -1 if the first argument should be ranked higher than the second, 1 if the second
532     * one should be ranked higher and 0 if they are equal.
533     */
534    public int compare(NotificationData.Entry a, NotificationData.Entry b) {
535        HeadsUpEntry aEntry = getHeadsUpEntry(a.key);
536        HeadsUpEntry bEntry = getHeadsUpEntry(b.key);
537        if (aEntry == null || bEntry == null) {
538            return aEntry == null ? 1 : -1;
539        }
540        return aEntry.compareTo(bEntry);
541    }
542
543    /**
544     * Set that we are exiting the headsUp pinned mode, but some notifications might still be
545     * animating out. This is used to keep the touchable regions in a sane state.
546     */
547    public void setHeadsUpGoingAway(boolean headsUpGoingAway) {
548        if (headsUpGoingAway != mHeadsUpGoingAway) {
549            mHeadsUpGoingAway = headsUpGoingAway;
550            if (!headsUpGoingAway) {
551                waitForStatusBarLayout();
552            }
553            updateTouchableRegionListener();
554        }
555    }
556
557    /**
558     * We need to wait on the whole panel to collapse, before we can remove the touchable region
559     * listener.
560     */
561    private void waitForStatusBarLayout() {
562        mWaitingOnCollapseWhenGoingAway = true;
563        mStatusBarWindowView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
564            @Override
565            public void onLayoutChange(View v, int left, int top, int right, int bottom,
566                    int oldLeft,
567                    int oldTop, int oldRight, int oldBottom) {
568                if (mStatusBarWindowView.getHeight() <= mStatusBarHeight) {
569                    mStatusBarWindowView.removeOnLayoutChangeListener(this);
570                    mWaitingOnCollapseWhenGoingAway = false;
571                    updateTouchableRegionListener();
572                }
573            }
574        });
575    }
576
577    public static void setIsClickedNotification(View child, boolean clicked) {
578        child.setTag(TAG_CLICKED_NOTIFICATION, clicked ? true : null);
579    }
580
581    public static boolean isClickedHeadsUpNotification(View child) {
582        Boolean clicked = (Boolean) child.getTag(TAG_CLICKED_NOTIFICATION);
583        return clicked != null && clicked;
584    }
585
586    public void setRemoteInputActive(NotificationData.Entry entry, boolean remoteInputActive) {
587        HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(entry.key);
588        if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) {
589            headsUpEntry.remoteInputActive = remoteInputActive;
590            if (remoteInputActive) {
591                headsUpEntry.removeAutoRemovalCallbacks();
592            } else {
593                headsUpEntry.updateEntry(false /* updatePostTime */);
594            }
595        }
596    }
597
598    /**
599     * Set an entry to be expanded and therefore stick in the heads up area if it's pinned
600     * until it's collapsed again.
601     */
602    public void setExpanded(NotificationData.Entry entry, boolean expanded) {
603        HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(entry.key);
604        if (headsUpEntry != null && headsUpEntry.expanded != expanded) {
605            headsUpEntry.expanded = expanded;
606            if (expanded) {
607                headsUpEntry.removeAutoRemovalCallbacks();
608            } else {
609                headsUpEntry.updateEntry(false /* updatePostTime */);
610            }
611        }
612    }
613
614    @Override
615    public void onReorderingAllowed() {
616        for (NotificationData.Entry entry : mEntriesToRemoveWhenReorderingAllowed) {
617            if (isHeadsUp(entry.key)) {
618                // Maybe the heads-up was removed already
619                removeHeadsUpEntry(entry);
620            }
621        }
622        mEntriesToRemoveWhenReorderingAllowed.clear();
623    }
624
625    public void setVisualStabilityManager(VisualStabilityManager visualStabilityManager) {
626        mVisualStabilityManager = visualStabilityManager;
627    }
628
629    public void setStatusBarState(int statusBarState) {
630        mStatusBarState = statusBarState;
631    }
632
633    /**
634     * This represents a notification and how long it is in a heads up mode. It also manages its
635     * lifecycle automatically when created.
636     */
637    public class HeadsUpEntry implements Comparable<HeadsUpEntry> {
638        public NotificationData.Entry entry;
639        public long postTime;
640        public long earliestRemovaltime;
641        private Runnable mRemoveHeadsUpRunnable;
642        public boolean remoteInputActive;
643        public boolean expanded;
644
645        public void setEntry(final NotificationData.Entry entry) {
646            this.entry = entry;
647
648            // The actual post time will be just after the heads-up really slided in
649            postTime = mClock.currentTimeMillis() + mTouchAcceptanceDelay;
650            mRemoveHeadsUpRunnable = new Runnable() {
651                @Override
652                public void run() {
653                    if (!mVisualStabilityManager.isReorderingAllowed()) {
654                        mEntriesToRemoveWhenReorderingAllowed.add(entry);
655                        mVisualStabilityManager.addReorderingAllowedCallback(HeadsUpManager.this);
656                    } else if (!mTrackingHeadsUp) {
657                        removeHeadsUpEntry(entry);
658                    } else {
659                        mEntriesToRemoveAfterExpand.add(entry);
660                    }
661                }
662            };
663            updateEntry();
664        }
665
666        public void updateEntry() {
667            updateEntry(true);
668        }
669
670        public void updateEntry(boolean updatePostTime) {
671            long currentTime = mClock.currentTimeMillis();
672            earliestRemovaltime = currentTime + mMinimumDisplayTime;
673            if (updatePostTime) {
674                postTime = Math.max(postTime, currentTime);
675            }
676            removeAutoRemovalCallbacks();
677            if (mEntriesToRemoveAfterExpand.contains(entry)) {
678                mEntriesToRemoveAfterExpand.remove(entry);
679            }
680            if (mEntriesToRemoveWhenReorderingAllowed.contains(entry)) {
681                mEntriesToRemoveWhenReorderingAllowed.remove(entry);
682            }
683            if (!isSticky()) {
684                long finishTime = postTime + mHeadsUpNotificationDecay;
685                long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
686                mHandler.postDelayed(mRemoveHeadsUpRunnable, removeDelay);
687            }
688        }
689
690        private boolean isSticky() {
691            return (entry.row.isPinned() && expanded)
692                    || remoteInputActive || hasFullScreenIntent(entry);
693        }
694
695        @Override
696        public int compareTo(HeadsUpEntry o) {
697            boolean isPinned = entry.row.isPinned();
698            boolean otherPinned = o.entry.row.isPinned();
699            if (isPinned && !otherPinned) {
700                return -1;
701            } else if (!isPinned && otherPinned) {
702                return 1;
703            }
704            boolean selfFullscreen = hasFullScreenIntent(entry);
705            boolean otherFullscreen = hasFullScreenIntent(o.entry);
706            if (selfFullscreen && !otherFullscreen) {
707                return -1;
708            } else if (!selfFullscreen && otherFullscreen) {
709                return 1;
710            }
711
712            if (remoteInputActive && !o.remoteInputActive) {
713                return -1;
714            } else if (!remoteInputActive && o.remoteInputActive) {
715                return 1;
716            }
717
718            return postTime < o.postTime ? 1
719                    : postTime == o.postTime ? entry.key.compareTo(o.entry.key)
720                            : -1;
721        }
722
723        public void removeAutoRemovalCallbacks() {
724            mHandler.removeCallbacks(mRemoveHeadsUpRunnable);
725        }
726
727        public boolean wasShownLongEnough() {
728            return earliestRemovaltime < mClock.currentTimeMillis();
729        }
730
731        public void removeAsSoonAsPossible() {
732            removeAutoRemovalCallbacks();
733            mHandler.postDelayed(mRemoveHeadsUpRunnable,
734                    earliestRemovaltime - mClock.currentTimeMillis());
735        }
736
737        public void reset() {
738            removeAutoRemovalCallbacks();
739            entry = null;
740            mRemoveHeadsUpRunnable = null;
741            expanded = false;
742            remoteInputActive = false;
743        }
744    }
745
746    public static class Clock {
747        public long currentTimeMillis() {
748            return SystemClock.elapsedRealtime();
749        }
750    }
751
752}
753