1 /*
2  * Copyright (C) 2018 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 
17 package com.android.systemui.statusbar.phone;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.graphics.Region;
24 import android.util.Pools;
25 
26 import androidx.collection.ArraySet;
27 
28 import com.android.internal.annotations.VisibleForTesting;
29 import com.android.systemui.Dumpable;
30 import com.android.systemui.R;
31 import com.android.systemui.plugins.statusbar.StatusBarStateController;
32 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
33 import com.android.systemui.statusbar.StatusBarState;
34 import com.android.systemui.statusbar.notification.VisualStabilityManager;
35 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
36 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
37 import com.android.systemui.statusbar.policy.ConfigurationController;
38 import com.android.systemui.statusbar.policy.HeadsUpManager;
39 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
40 
41 import java.io.FileDescriptor;
42 import java.io.PrintWriter;
43 import java.util.ArrayList;
44 import java.util.HashSet;
45 import java.util.List;
46 import java.util.Stack;
47 
48 /**
49  * A implementation of HeadsUpManager for phone and car.
50  */
51 public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable,
52         VisualStabilityManager.Callback, OnHeadsUpChangedListener {
53     private static final String TAG = "HeadsUpManagerPhone";
54 
55     @VisibleForTesting
56     final int mExtensionTime;
57     private final KeyguardBypassController mBypassController;
58     private final NotificationGroupManager mGroupManager;
59     private final List<OnHeadsUpPhoneListenerChange> mHeadsUpPhoneListeners = new ArrayList<>();
60     private final int mAutoHeadsUpNotificationDecay;
61     private VisualStabilityManager mVisualStabilityManager;
62     private boolean mReleaseOnExpandFinish;
63 
64     private boolean mTrackingHeadsUp;
65     private HashSet<String> mSwipedOutKeys = new HashSet<>();
66     private HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>();
67     private HashSet<String> mKeysToRemoveWhenLeavingKeyguard = new HashSet<>();
68     private ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
69             = new ArraySet<>();
70     private boolean mIsExpanded;
71     private boolean mHeadsUpGoingAway;
72     private int mStatusBarState;
73     private AnimationStateHandler mAnimationStateHandler;
74     private int mHeadsUpInset;
75 
76     // Used for determining the region for touch interaction
77     private final Region mTouchableRegion = new Region();
78 
79     private final Pools.Pool<HeadsUpEntryPhone> mEntryPool = new Pools.Pool<HeadsUpEntryPhone>() {
80         private Stack<HeadsUpEntryPhone> mPoolObjects = new Stack<>();
81 
82         @Override
83         public HeadsUpEntryPhone acquire() {
84             if (!mPoolObjects.isEmpty()) {
85                 return mPoolObjects.pop();
86             }
87             return new HeadsUpEntryPhone();
88         }
89 
90         @Override
91         public boolean release(@NonNull HeadsUpEntryPhone instance) {
92             mPoolObjects.push(instance);
93             return true;
94         }
95     };
96 
97     ///////////////////////////////////////////////////////////////////////////////////////////////
98     //  Constructor:
99 
HeadsUpManagerPhone(@onNull final Context context, StatusBarStateController statusBarStateController, KeyguardBypassController bypassController, NotificationGroupManager groupManager, ConfigurationController configurationController)100     public HeadsUpManagerPhone(@NonNull final Context context,
101             StatusBarStateController statusBarStateController,
102             KeyguardBypassController bypassController,
103             NotificationGroupManager groupManager,
104             ConfigurationController configurationController) {
105         super(context);
106         Resources resources = mContext.getResources();
107         mExtensionTime = resources.getInteger(R.integer.ambient_notification_extension_time);
108         mAutoHeadsUpNotificationDecay = resources.getInteger(
109                 R.integer.auto_heads_up_notification_decay);
110         statusBarStateController.addCallback(mStatusBarStateListener);
111         mBypassController = bypassController;
112         mGroupManager = groupManager;
113 
114         updateResources();
115         configurationController.addCallback(new ConfigurationController.ConfigurationListener() {
116             @Override
117             public void onDensityOrFontScaleChanged() {
118                 updateResources();
119             }
120 
121             @Override
122             public void onOverlayChanged() {
123                 updateResources();
124             }
125         });
126     }
127 
setup(VisualStabilityManager visualStabilityManager)128     void setup(VisualStabilityManager visualStabilityManager) {
129         mVisualStabilityManager = visualStabilityManager;
130     }
131 
setAnimationStateHandler(AnimationStateHandler handler)132     public void setAnimationStateHandler(AnimationStateHandler handler) {
133         mAnimationStateHandler = handler;
134     }
135 
updateResources()136     private void updateResources() {
137         Resources resources = mContext.getResources();
138         mHeadsUpInset =
139                 resources.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height)
140                         + resources.getDimensionPixelSize(R.dimen.heads_up_status_bar_padding);
141     }
142 
143     ///////////////////////////////////////////////////////////////////////////////////////////////
144     //  Public methods:
145 
146     /**
147      * Add a listener to receive callbacks onHeadsUpGoingAway
148      */
addHeadsUpPhoneListener(OnHeadsUpPhoneListenerChange listener)149     void addHeadsUpPhoneListener(OnHeadsUpPhoneListenerChange listener) {
150         mHeadsUpPhoneListeners.add(listener);
151     }
152 
153     /**
154      * Gets the touchable region needed for heads up notifications. Returns null if no touchable
155      * region is required (ie: no heads up notification currently exists).
156      */
getTouchableRegion()157     @Nullable Region getTouchableRegion() {
158         NotificationEntry topEntry = getTopEntry();
159 
160         // This call could be made in an inconsistent state while the pinnedMode hasn't been
161         // updated yet, but callbacks leading out of the headsUp manager, querying it. Let's
162         // therefore also check if the topEntry is null.
163         if (!hasPinnedHeadsUp() || topEntry == null) {
164             return null;
165         } else {
166             if (topEntry.isChildInGroup()) {
167                 final NotificationEntry groupSummary =
168                         mGroupManager.getGroupSummary(topEntry.getSbn());
169                 if (groupSummary != null) {
170                     topEntry = groupSummary;
171                 }
172             }
173             ExpandableNotificationRow topRow = topEntry.getRow();
174             int[] tmpArray = new int[2];
175             topRow.getLocationOnScreen(tmpArray);
176             int minX = tmpArray[0];
177             int maxX = tmpArray[0] + topRow.getWidth();
178             int height = topRow.getIntrinsicHeight();
179             mTouchableRegion.set(minX, 0, maxX, mHeadsUpInset + height);
180             return mTouchableRegion;
181         }
182     }
183 
184     /**
185      * Decides whether a click is invalid for a notification, i.e it has not been shown long enough
186      * that a user might have consciously clicked on it.
187      *
188      * @param key the key of the touched notification
189      * @return whether the touch is invalid and should be discarded
190      */
shouldSwallowClick(@onNull String key)191     boolean shouldSwallowClick(@NonNull String key) {
192         HeadsUpManager.HeadsUpEntry entry = getHeadsUpEntry(key);
193         return entry != null && mClock.currentTimeMillis() < entry.mPostTime;
194     }
195 
onExpandingFinished()196     public void onExpandingFinished() {
197         if (mReleaseOnExpandFinish) {
198             releaseAllImmediately();
199             mReleaseOnExpandFinish = false;
200         } else {
201             for (NotificationEntry entry : mEntriesToRemoveAfterExpand) {
202                 if (isAlerting(entry.getKey())) {
203                     // Maybe the heads-up was removed already
204                     removeAlertEntry(entry.getKey());
205                 }
206             }
207         }
208         mEntriesToRemoveAfterExpand.clear();
209     }
210 
211     /**
212      * Sets the tracking-heads-up flag. If the flag is true, HeadsUpManager doesn't remove the entry
213      * from the list even after a Heads Up Notification is gone.
214      */
setTrackingHeadsUp(boolean trackingHeadsUp)215     public void setTrackingHeadsUp(boolean trackingHeadsUp) {
216         mTrackingHeadsUp = trackingHeadsUp;
217     }
218 
219     /**
220      * Notify that the status bar panel gets expanded or collapsed.
221      *
222      * @param isExpanded True to notify expanded, false to notify collapsed.
223      */
setIsPanelExpanded(boolean isExpanded)224     void setIsPanelExpanded(boolean isExpanded) {
225         if (isExpanded != mIsExpanded) {
226             mIsExpanded = isExpanded;
227             if (isExpanded) {
228                 mHeadsUpGoingAway = false;
229             }
230         }
231     }
232 
233     @Override
isEntryAutoHeadsUpped(String key)234     public boolean isEntryAutoHeadsUpped(String key) {
235         HeadsUpEntryPhone headsUpEntryPhone = getHeadsUpEntryPhone(key);
236         if (headsUpEntryPhone == null) {
237             return false;
238         }
239         return headsUpEntryPhone.isAutoHeadsUp();
240     }
241 
242     /**
243      * Set that we are exiting the headsUp pinned mode, but some notifications might still be
244      * animating out. This is used to keep the touchable regions in a sane state.
245      */
setHeadsUpGoingAway(boolean headsUpGoingAway)246     void setHeadsUpGoingAway(boolean headsUpGoingAway) {
247         if (headsUpGoingAway != mHeadsUpGoingAway) {
248             mHeadsUpGoingAway = headsUpGoingAway;
249             for (OnHeadsUpPhoneListenerChange listener : mHeadsUpPhoneListeners) {
250                 listener.onHeadsUpGoingAwayStateChanged(headsUpGoingAway);
251             }
252         }
253     }
254 
isHeadsUpGoingAway()255     boolean isHeadsUpGoingAway() {
256         return mHeadsUpGoingAway;
257     }
258 
259     /**
260      * Notifies that a remote input textbox in notification gets active or inactive.
261      *
262      * @param entry             The entry of the target notification.
263      * @param remoteInputActive True to notify active, False to notify inactive.
264      */
setRemoteInputActive( @onNull NotificationEntry entry, boolean remoteInputActive)265     public void setRemoteInputActive(
266             @NonNull NotificationEntry entry, boolean remoteInputActive) {
267         HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(entry.getKey());
268         if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) {
269             headsUpEntry.remoteInputActive = remoteInputActive;
270             if (remoteInputActive) {
271                 headsUpEntry.removeAutoRemovalCallbacks();
272             } else {
273                 headsUpEntry.updateEntry(false /* updatePostTime */);
274             }
275         }
276     }
277 
278     /**
279      * Sets whether an entry's menu row is exposed and therefore it should stick in the heads up
280      * area if it's pinned until it's hidden again.
281      */
setMenuShown(@onNull NotificationEntry entry, boolean menuShown)282     public void setMenuShown(@NonNull NotificationEntry entry, boolean menuShown) {
283         HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey());
284         if (headsUpEntry instanceof HeadsUpEntryPhone && entry.isRowPinned()) {
285             ((HeadsUpEntryPhone) headsUpEntry).setMenuShownPinned(menuShown);
286         }
287     }
288 
289     /**
290      * Extends the lifetime of the currently showing pulsing notification so that the pulse lasts
291      * longer.
292      */
extendHeadsUp()293     public void extendHeadsUp() {
294         HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone();
295         if (topEntry == null) {
296             return;
297         }
298         topEntry.extendPulse();
299     }
300 
301     ///////////////////////////////////////////////////////////////////////////////////////////////
302     //  HeadsUpManager public methods overrides:
303 
304     @Override
isTrackingHeadsUp()305     public boolean isTrackingHeadsUp() {
306         return mTrackingHeadsUp;
307     }
308 
309     @Override
snooze()310     public void snooze() {
311         super.snooze();
312         mReleaseOnExpandFinish = true;
313     }
314 
addSwipedOutNotification(@onNull String key)315     public void addSwipedOutNotification(@NonNull String key) {
316         mSwipedOutKeys.add(key);
317     }
318 
319     ///////////////////////////////////////////////////////////////////////////////////////////////
320     //  Dumpable overrides:
321 
322     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)323     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
324         pw.println("HeadsUpManagerPhone state:");
325         dumpInternal(fd, pw, args);
326     }
327 
328     @Override
shouldExtendLifetime(NotificationEntry entry)329     public boolean shouldExtendLifetime(NotificationEntry entry) {
330         // We should not defer the removal if reordering isn't allowed since otherwise
331         // these won't disappear until reordering is allowed again, which happens only once
332         // the notification panel is collapsed again.
333         return mVisualStabilityManager.isReorderingAllowed() && super.shouldExtendLifetime(entry);
334     }
335 
336     ///////////////////////////////////////////////////////////////////////////////////////////////
337     //  VisualStabilityManager.Callback overrides:
338 
339     @Override
onChangeAllowed()340     public void onChangeAllowed() {
341         mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false);
342         for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) {
343             if (isAlerting(entry.getKey())) {
344                 // Maybe the heads-up was removed already
345                 removeAlertEntry(entry.getKey());
346             }
347         }
348         mEntriesToRemoveWhenReorderingAllowed.clear();
349         mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true);
350     }
351 
352     ///////////////////////////////////////////////////////////////////////////////////////////////
353     //  HeadsUpManager utility (protected) methods overrides:
354 
355     @Override
createAlertEntry()356     protected HeadsUpEntry createAlertEntry() {
357         return mEntryPool.acquire();
358     }
359 
360     @Override
onAlertEntryRemoved(AlertEntry alertEntry)361     protected void onAlertEntryRemoved(AlertEntry alertEntry) {
362         mKeysToRemoveWhenLeavingKeyguard.remove(alertEntry.mEntry.getKey());
363         super.onAlertEntryRemoved(alertEntry);
364         mEntryPool.release((HeadsUpEntryPhone) alertEntry);
365     }
366 
367     @Override
shouldHeadsUpBecomePinned(NotificationEntry entry)368     protected boolean shouldHeadsUpBecomePinned(NotificationEntry entry) {
369         boolean pin = mStatusBarState == StatusBarState.SHADE && !mIsExpanded;
370         if (mBypassController.getBypassEnabled()) {
371             pin |= mStatusBarState == StatusBarState.KEYGUARD;
372         }
373         return pin || super.shouldHeadsUpBecomePinned(entry);
374     }
375 
376     @Override
dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args)377     protected void dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args) {
378         super.dumpInternal(fd, pw, args);
379         pw.print("  mBarState=");
380         pw.println(mStatusBarState);
381         pw.print("  mTouchableRegion=");
382         pw.println(mTouchableRegion);
383     }
384 
385     ///////////////////////////////////////////////////////////////////////////////////////////////
386     //  Private utility methods:
387 
388     @Nullable
getHeadsUpEntryPhone(@onNull String key)389     private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) {
390         return (HeadsUpEntryPhone) mAlertEntries.get(key);
391     }
392 
393     @Nullable
getTopHeadsUpEntryPhone()394     private HeadsUpEntryPhone getTopHeadsUpEntryPhone() {
395         return (HeadsUpEntryPhone) getTopHeadsUpEntry();
396     }
397 
398     @Override
canRemoveImmediately(@onNull String key)399     protected boolean canRemoveImmediately(@NonNull String key) {
400         if (mSwipedOutKeys.contains(key)) {
401             // We always instantly dismiss views being manually swiped out.
402             mSwipedOutKeys.remove(key);
403             return true;
404         }
405 
406         HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(key);
407         HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone();
408 
409         return headsUpEntry == null || headsUpEntry != topEntry || super.canRemoveImmediately(key);
410     }
411 
412     ///////////////////////////////////////////////////////////////////////////////////////////////
413     //  HeadsUpEntryPhone:
414 
415     protected class HeadsUpEntryPhone extends HeadsUpManager.HeadsUpEntry {
416 
417         private boolean mMenuShownPinned;
418 
419         /**
420          * If the time this entry has been on was extended
421          */
422         private boolean extended;
423 
424         /**
425          * Was this entry received while on keyguard
426          */
427         private boolean mIsAutoHeadsUp;
428 
429 
430         @Override
isSticky()431         public boolean isSticky() {
432             return super.isSticky() || mMenuShownPinned;
433         }
434 
setEntry(@onNull final NotificationEntry entry)435         public void setEntry(@NonNull final NotificationEntry entry) {
436             Runnable removeHeadsUpRunnable = () -> {
437                 if (!mVisualStabilityManager.isReorderingAllowed()
438                         // We don't want to allow reordering while pulsing, but headsup need to
439                         // time out anyway
440                         && !entry.showingPulsing()) {
441                     mEntriesToRemoveWhenReorderingAllowed.add(entry);
442                     mVisualStabilityManager.addReorderingAllowedCallback(HeadsUpManagerPhone.this,
443                             false  /* persistent */);
444                 } else if (mTrackingHeadsUp) {
445                     mEntriesToRemoveAfterExpand.add(entry);
446                 } else if (mIsAutoHeadsUp && mStatusBarState == StatusBarState.KEYGUARD) {
447                     mKeysToRemoveWhenLeavingKeyguard.add(entry.getKey());
448                 } else {
449                     removeAlertEntry(entry.getKey());
450                 }
451             };
452 
453             setEntry(entry, removeHeadsUpRunnable);
454         }
455 
456         @Override
updateEntry(boolean updatePostTime)457         public void updateEntry(boolean updatePostTime) {
458             mIsAutoHeadsUp = mEntry.isAutoHeadsUp();
459             super.updateEntry(updatePostTime);
460 
461             if (mEntriesToRemoveAfterExpand.contains(mEntry)) {
462                 mEntriesToRemoveAfterExpand.remove(mEntry);
463             }
464             if (mEntriesToRemoveWhenReorderingAllowed.contains(mEntry)) {
465                 mEntriesToRemoveWhenReorderingAllowed.remove(mEntry);
466             }
467             mKeysToRemoveWhenLeavingKeyguard.remove(mEntry.getKey());
468         }
469 
470         @Override
setExpanded(boolean expanded)471         public void setExpanded(boolean expanded) {
472             if (this.expanded == expanded) {
473                 return;
474             }
475 
476             this.expanded = expanded;
477             if (expanded) {
478                 removeAutoRemovalCallbacks();
479             } else {
480                 updateEntry(false /* updatePostTime */);
481             }
482         }
483 
setMenuShownPinned(boolean menuShownPinned)484         public void setMenuShownPinned(boolean menuShownPinned) {
485             if (mMenuShownPinned == menuShownPinned) {
486                 return;
487             }
488 
489             mMenuShownPinned = menuShownPinned;
490             if (menuShownPinned) {
491                 removeAutoRemovalCallbacks();
492             } else {
493                 updateEntry(false /* updatePostTime */);
494             }
495         }
496 
497         @Override
reset()498         public void reset() {
499             super.reset();
500             mMenuShownPinned = false;
501             extended = false;
502             mIsAutoHeadsUp = false;
503         }
504 
extendPulse()505         private void extendPulse() {
506             if (!extended) {
507                 extended = true;
508                 updateEntry(false);
509             }
510         }
511 
512         @Override
compareTo(AlertEntry alertEntry)513         public int compareTo(AlertEntry alertEntry) {
514             HeadsUpEntryPhone headsUpEntry = (HeadsUpEntryPhone) alertEntry;
515             boolean autoShown = isAutoHeadsUp();
516             boolean otherAutoShown = headsUpEntry.isAutoHeadsUp();
517             if (autoShown && !otherAutoShown) {
518                 return 1;
519             } else if (!autoShown && otherAutoShown) {
520                 return -1;
521             }
522             return super.compareTo(alertEntry);
523         }
524 
525         @Override
calculateFinishTime()526         protected long calculateFinishTime() {
527             return mPostTime + getDecayDuration() + (extended ? mExtensionTime : 0);
528         }
529 
getDecayDuration()530         private int getDecayDuration() {
531             if (isAutoHeadsUp()) {
532                 return getRecommendedHeadsUpTimeoutMs(mAutoHeadsUpNotificationDecay);
533             } else {
534                 return getRecommendedHeadsUpTimeoutMs(mAutoDismissNotificationDecay);
535             }
536         }
537 
isAutoHeadsUp()538         private boolean isAutoHeadsUp() {
539             return mIsAutoHeadsUp;
540         }
541     }
542 
543     public interface AnimationStateHandler {
setHeadsUpGoingAwayAnimationsAllowed(boolean allowed)544         void setHeadsUpGoingAwayAnimationsAllowed(boolean allowed);
545     }
546 
547     /**
548      * Listener to register for HeadsUpNotification Phone changes.
549      */
550     public interface OnHeadsUpPhoneListenerChange {
551         /**
552          * Called when a heads up notification is 'going away' or no longer 'going away'.
553          * See {@link HeadsUpManagerPhone#setHeadsUpGoingAway}.
554          */
onHeadsUpGoingAwayStateChanged(boolean headsUpGoingAway)555         void onHeadsUpGoingAwayStateChanged(boolean headsUpGoingAway);
556     }
557 
558     private final StateListener mStatusBarStateListener = new StateListener() {
559         @Override
560         public void onStateChanged(int newState) {
561             boolean wasKeyguard = mStatusBarState == StatusBarState.KEYGUARD;
562             boolean isKeyguard = newState == StatusBarState.KEYGUARD;
563             mStatusBarState = newState;
564             if (wasKeyguard && !isKeyguard && mKeysToRemoveWhenLeavingKeyguard.size() != 0) {
565                 String[] keys = mKeysToRemoveWhenLeavingKeyguard.toArray(new String[0]);
566                 for (String key : keys) {
567                     removeAlertEntry(key);
568                 }
569                 mKeysToRemoveWhenLeavingKeyguard.clear();
570             }
571             if (wasKeyguard && !isKeyguard && mBypassController.getBypassEnabled()) {
572                 ArrayList<String> keysToRemove = new ArrayList<>();
573                 for (AlertEntry entry : mAlertEntries.values()) {
574                     if (entry.mEntry != null && entry.mEntry.isBubble() && !entry.isSticky()) {
575                         keysToRemove.add(entry.mEntry.getKey());
576                     }
577                 }
578                 for (String key : keysToRemove) {
579                     removeAlertEntry(key);
580                 }
581             }
582         }
583 
584         @Override
585         public void onDozingChanged(boolean isDozing) {
586             if (!isDozing) {
587                 // Let's make sure all huns we got while dozing time out within the normal timeout
588                 // duration. Otherwise they could get stuck for a very long time
589                 for (AlertEntry entry : mAlertEntries.values()) {
590                     entry.updateEntry(true /* updatePostTime */);
591                 }
592             }
593         }
594     };
595 }
596