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