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