• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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             mTouchableRegion.set(minX, 0, maxX, mHeadsUpInset + height);
188             return mTouchableRegion;
189         }
190     }
191 
192     /**
193      * Decides whether a click is invalid for a notification, i.e it has not been shown long enough
194      * that a user might have consciously clicked on it.
195      *
196      * @param key the key of the touched notification
197      * @return whether the touch is invalid and should be discarded
198      */
shouldSwallowClick(@onNull String key)199     boolean shouldSwallowClick(@NonNull String key) {
200         HeadsUpManager.HeadsUpEntry entry = getHeadsUpEntry(key);
201         return entry != null && mClock.currentTimeMillis() < entry.mPostTime;
202     }
203 
onExpandingFinished()204     public void onExpandingFinished() {
205         if (mReleaseOnExpandFinish) {
206             releaseAllImmediately();
207             mReleaseOnExpandFinish = false;
208         } else {
209             for (NotificationEntry entry : mEntriesToRemoveAfterExpand) {
210                 if (isAlerting(entry.getKey())) {
211                     // Maybe the heads-up was removed already
212                     removeAlertEntry(entry.getKey());
213                 }
214             }
215         }
216         mEntriesToRemoveAfterExpand.clear();
217     }
218 
219     /**
220      * Sets the tracking-heads-up flag. If the flag is true, HeadsUpManager doesn't remove the entry
221      * from the list even after a Heads Up Notification is gone.
222      */
setTrackingHeadsUp(boolean trackingHeadsUp)223     public void setTrackingHeadsUp(boolean trackingHeadsUp) {
224         mTrackingHeadsUp = trackingHeadsUp;
225     }
226 
onShadeExpansionFullyChanged(Boolean isExpanded)227     private void onShadeExpansionFullyChanged(Boolean isExpanded) {
228         if (isExpanded != mIsExpanded) {
229             mIsExpanded = isExpanded;
230             if (isExpanded) {
231                 mHeadsUpGoingAway = false;
232             }
233         }
234     }
235 
236     /**
237      * Set that we are exiting the headsUp pinned mode, but some notifications might still be
238      * animating out. This is used to keep the touchable regions in a reasonable state.
239      */
setHeadsUpGoingAway(boolean headsUpGoingAway)240     void setHeadsUpGoingAway(boolean headsUpGoingAway) {
241         if (headsUpGoingAway != mHeadsUpGoingAway) {
242             mHeadsUpGoingAway = headsUpGoingAway;
243             for (OnHeadsUpPhoneListenerChange listener : mHeadsUpPhoneListeners) {
244                 listener.onHeadsUpGoingAwayStateChanged(headsUpGoingAway);
245             }
246         }
247     }
248 
isHeadsUpGoingAway()249     boolean isHeadsUpGoingAway() {
250         return mHeadsUpGoingAway;
251     }
252 
253     /**
254      * Notifies that a remote input textbox in notification gets active or inactive.
255      *
256      * @param entry             The entry of the target notification.
257      * @param remoteInputActive True to notify active, False to notify inactive.
258      */
setRemoteInputActive( @onNull NotificationEntry entry, boolean remoteInputActive)259     public void setRemoteInputActive(
260             @NonNull NotificationEntry entry, boolean remoteInputActive) {
261         HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(entry.getKey());
262         if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) {
263             headsUpEntry.remoteInputActive = remoteInputActive;
264             if (remoteInputActive) {
265                 headsUpEntry.removeAutoRemovalCallbacks();
266             } else {
267                 headsUpEntry.updateEntry(false /* updatePostTime */);
268             }
269         }
270     }
271 
272     /**
273      * Sets whether an entry's menu row is exposed and therefore it should stick in the heads up
274      * area if it's pinned until it's hidden again.
275      */
setMenuShown(@onNull NotificationEntry entry, boolean menuShown)276     public void setMenuShown(@NonNull NotificationEntry entry, boolean menuShown) {
277         HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey());
278         if (headsUpEntry instanceof HeadsUpEntryPhone && entry.isRowPinned()) {
279             ((HeadsUpEntryPhone) headsUpEntry).setMenuShownPinned(menuShown);
280         }
281     }
282 
283     /**
284      * Extends the lifetime of the currently showing pulsing notification so that the pulse lasts
285      * longer.
286      */
extendHeadsUp()287     public void extendHeadsUp() {
288         HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone();
289         if (topEntry == null) {
290             return;
291         }
292         topEntry.extendPulse();
293     }
294 
295     ///////////////////////////////////////////////////////////////////////////////////////////////
296     //  HeadsUpManager public methods overrides and overloads:
297 
298     @Override
isTrackingHeadsUp()299     public boolean isTrackingHeadsUp() {
300         return mTrackingHeadsUp;
301     }
302 
303     @Override
snooze()304     public void snooze() {
305         super.snooze();
306         mReleaseOnExpandFinish = true;
307     }
308 
addSwipedOutNotification(@onNull String key)309     public void addSwipedOutNotification(@NonNull String key) {
310         mSwipedOutKeys.add(key);
311     }
312 
removeNotification(@onNull String key, boolean releaseImmediately, boolean animate)313     public boolean removeNotification(@NonNull String key, boolean releaseImmediately,
314             boolean animate) {
315         if (animate) {
316             return removeNotification(key, releaseImmediately);
317         } else {
318             mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false);
319             boolean removed = removeNotification(key, releaseImmediately);
320             mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true);
321             return removed;
322         }
323     }
324 
325     ///////////////////////////////////////////////////////////////////////////////////////////////
326     //  Dumpable overrides:
327 
328     @Override
dump(PrintWriter pw, String[] args)329     public void dump(PrintWriter pw, String[] args) {
330         pw.println("HeadsUpManagerPhone state:");
331         dumpInternal(pw, args);
332     }
333 
334     ///////////////////////////////////////////////////////////////////////////////////////////////
335     //  OnReorderingAllowedListener:
336 
337     private final OnReorderingAllowedListener mOnReorderingAllowedListener = () -> {
338         mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false);
339         for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) {
340             if (isAlerting(entry.getKey())) {
341                 // Maybe the heads-up was removed already
342                 removeAlertEntry(entry.getKey());
343             }
344         }
345         mEntriesToRemoveWhenReorderingAllowed.clear();
346         mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true);
347     };
348 
349     ///////////////////////////////////////////////////////////////////////////////////////////////
350     //  HeadsUpManager utility (protected) methods overrides:
351 
352     @Override
createAlertEntry()353     protected HeadsUpEntry createAlertEntry() {
354         return mEntryPool.acquire();
355     }
356 
357     @Override
onAlertEntryRemoved(AlertEntry alertEntry)358     protected void onAlertEntryRemoved(AlertEntry alertEntry) {
359         super.onAlertEntryRemoved(alertEntry);
360         mEntryPool.release((HeadsUpEntryPhone) alertEntry);
361     }
362 
363     @Override
shouldHeadsUpBecomePinned(NotificationEntry entry)364     protected boolean shouldHeadsUpBecomePinned(NotificationEntry entry) {
365         boolean pin = mStatusBarState == StatusBarState.SHADE && !mIsExpanded;
366         if (mBypassController.getBypassEnabled()) {
367             pin |= mStatusBarState == StatusBarState.KEYGUARD;
368         }
369         return pin || super.shouldHeadsUpBecomePinned(entry);
370     }
371 
372     @Override
dumpInternal(PrintWriter pw, String[] args)373     protected void dumpInternal(PrintWriter pw, String[] args) {
374         super.dumpInternal(pw, args);
375         pw.print("  mBarState=");
376         pw.println(mStatusBarState);
377         pw.print("  mTouchableRegion=");
378         pw.println(mTouchableRegion);
379     }
380 
381     ///////////////////////////////////////////////////////////////////////////////////////////////
382     //  Private utility methods:
383 
384     @Nullable
getHeadsUpEntryPhone(@onNull String key)385     private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) {
386         return (HeadsUpEntryPhone) mAlertEntries.get(key);
387     }
388 
389     @Nullable
getTopHeadsUpEntryPhone()390     private HeadsUpEntryPhone getTopHeadsUpEntryPhone() {
391         return (HeadsUpEntryPhone) getTopHeadsUpEntry();
392     }
393 
394     @Override
canRemoveImmediately(@onNull String key)395     public boolean canRemoveImmediately(@NonNull String key) {
396         if (mSwipedOutKeys.contains(key)) {
397             // We always instantly dismiss views being manually swiped out.
398             mSwipedOutKeys.remove(key);
399             return true;
400         }
401 
402         HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(key);
403         HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone();
404 
405         return headsUpEntry == null || headsUpEntry != topEntry || super.canRemoveImmediately(key);
406     }
407 
408     ///////////////////////////////////////////////////////////////////////////////////////////////
409     //  HeadsUpEntryPhone:
410 
411     protected class HeadsUpEntryPhone extends HeadsUpManager.HeadsUpEntry {
412 
413         private boolean mMenuShownPinned;
414 
415         /**
416          * If the time this entry has been on was extended
417          */
418         private boolean extended;
419 
420 
421         @Override
isSticky()422         public boolean isSticky() {
423             return super.isSticky() || mMenuShownPinned;
424         }
425 
setEntry(@onNull final NotificationEntry entry)426         public void setEntry(@NonNull final NotificationEntry entry) {
427             Runnable removeHeadsUpRunnable = () -> {
428                 if (!mVisualStabilityProvider.isReorderingAllowed()
429                         // We don't want to allow reordering while pulsing, but headsup need to
430                         // time out anyway
431                         && !entry.showingPulsing()) {
432                     mEntriesToRemoveWhenReorderingAllowed.add(entry);
433                     mVisualStabilityProvider.addTemporaryReorderingAllowedListener(
434                             mOnReorderingAllowedListener);
435                 } else if (mTrackingHeadsUp) {
436                     mEntriesToRemoveAfterExpand.add(entry);
437                 } else {
438                     removeAlertEntry(entry.getKey());
439                 }
440             };
441 
442             setEntry(entry, removeHeadsUpRunnable);
443         }
444 
445         @Override
updateEntry(boolean updatePostTime)446         public void updateEntry(boolean updatePostTime) {
447             super.updateEntry(updatePostTime);
448 
449             if (mEntriesToRemoveAfterExpand.contains(mEntry)) {
450                 mEntriesToRemoveAfterExpand.remove(mEntry);
451             }
452             if (mEntriesToRemoveWhenReorderingAllowed.contains(mEntry)) {
453                 mEntriesToRemoveWhenReorderingAllowed.remove(mEntry);
454             }
455         }
456 
457         @Override
setExpanded(boolean expanded)458         public void setExpanded(boolean expanded) {
459             if (this.expanded == expanded) {
460                 return;
461             }
462 
463             this.expanded = expanded;
464             if (expanded) {
465                 removeAutoRemovalCallbacks();
466             } else {
467                 updateEntry(false /* updatePostTime */);
468             }
469         }
470 
setMenuShownPinned(boolean menuShownPinned)471         public void setMenuShownPinned(boolean menuShownPinned) {
472             if (mMenuShownPinned == menuShownPinned) {
473                 return;
474             }
475 
476             mMenuShownPinned = menuShownPinned;
477             if (menuShownPinned) {
478                 removeAutoRemovalCallbacks();
479             } else {
480                 updateEntry(false /* updatePostTime */);
481             }
482         }
483 
484         @Override
reset()485         public void reset() {
486             super.reset();
487             mMenuShownPinned = false;
488             extended = false;
489         }
490 
extendPulse()491         private void extendPulse() {
492             if (!extended) {
493                 extended = true;
494                 updateEntry(false);
495             }
496         }
497 
498         @Override
calculateFinishTime()499         protected long calculateFinishTime() {
500             return super.calculateFinishTime() + (extended ? mExtensionTime : 0);
501         }
502     }
503 
504     public interface AnimationStateHandler {
setHeadsUpGoingAwayAnimationsAllowed(boolean allowed)505         void setHeadsUpGoingAwayAnimationsAllowed(boolean allowed);
506     }
507 
508     /**
509      * Listener to register for HeadsUpNotification Phone changes.
510      */
511     public interface OnHeadsUpPhoneListenerChange {
512         /**
513          * Called when a heads up notification is 'going away' or no longer 'going away'.
514          * See {@link HeadsUpManagerPhone#setHeadsUpGoingAway}.
515          */
onHeadsUpGoingAwayStateChanged(boolean headsUpGoingAway)516         void onHeadsUpGoingAwayStateChanged(boolean headsUpGoingAway);
517     }
518 
519     private final StateListener mStatusBarStateListener = new StateListener() {
520         @Override
521         public void onStateChanged(int newState) {
522             boolean wasKeyguard = mStatusBarState == StatusBarState.KEYGUARD;
523             boolean isKeyguard = newState == StatusBarState.KEYGUARD;
524             mStatusBarState = newState;
525             if (wasKeyguard && !isKeyguard && mBypassController.getBypassEnabled()) {
526                 ArrayList<String> keysToRemove = new ArrayList<>();
527                 for (AlertEntry entry : mAlertEntries.values()) {
528                     if (entry.mEntry != null && entry.mEntry.isBubble() && !entry.isSticky()) {
529                         keysToRemove.add(entry.mEntry.getKey());
530                     }
531                 }
532                 for (String key : keysToRemove) {
533                     removeAlertEntry(key);
534                 }
535             }
536         }
537 
538         @Override
539         public void onDozingChanged(boolean isDozing) {
540             if (!isDozing) {
541                 // Let's make sure all huns we got while dozing time out within the normal timeout
542                 // duration. Otherwise they could get stuck for a very long time
543                 for (AlertEntry entry : mAlertEntries.values()) {
544                     entry.updateEntry(true /* updatePostTime */);
545                 }
546             }
547         }
548     };
549 }
550