• 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.app.Notification;
22 import android.os.SystemClock;
23 import android.service.notification.StatusBarNotification;
24 import android.util.ArrayMap;
25 
26 import com.android.internal.statusbar.NotificationVisibility;
27 import com.android.systemui.Dependency;
28 import com.android.systemui.plugins.statusbar.StatusBarStateController;
29 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
30 import com.android.systemui.statusbar.AlertingNotificationManager;
31 import com.android.systemui.statusbar.AmbientPulseManager;
32 import com.android.systemui.statusbar.AmbientPulseManager.OnAmbientChangedListener;
33 import com.android.systemui.statusbar.InflationTask;
34 import com.android.systemui.statusbar.notification.NotificationEntryListener;
35 import com.android.systemui.statusbar.notification.NotificationEntryManager;
36 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
37 import com.android.systemui.statusbar.notification.row.NotificationContentInflater.AsyncInflationTask;
38 import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag;
39 import com.android.systemui.statusbar.phone.NotificationGroupManager.NotificationGroup;
40 import com.android.systemui.statusbar.phone.NotificationGroupManager.OnGroupChangeListener;
41 import com.android.systemui.statusbar.policy.HeadsUpManager;
42 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
43 
44 import java.util.ArrayList;
45 import java.util.Objects;
46 
47 import javax.inject.Inject;
48 import javax.inject.Singleton;
49 
50 /**
51  * A helper class dealing with the alert interactions between {@link NotificationGroupManager},
52  * {@link HeadsUpManager}, {@link AmbientPulseManager}. In particular, this class deals with keeping
53  * the correct notification in a group alerting based off the group suppression.
54  */
55 @Singleton
56 public class NotificationGroupAlertTransferHelper implements OnHeadsUpChangedListener,
57         OnAmbientChangedListener, StateListener {
58 
59     private static final long ALERT_TRANSFER_TIMEOUT = 300;
60 
61     /**
62      * The list of entries containing group alert metadata for each group. Keyed by group key.
63      */
64     private final ArrayMap<String, GroupAlertEntry> mGroupAlertEntries = new ArrayMap<>();
65 
66     /**
67      * The list of entries currently inflating that should alert after inflation. Keyed by
68      * notification key.
69      */
70     private final ArrayMap<String, PendingAlertInfo> mPendingAlerts = new ArrayMap<>();
71 
72     private HeadsUpManager mHeadsUpManager;
73     private final AmbientPulseManager mAmbientPulseManager =
74             Dependency.get(AmbientPulseManager.class);
75     private final NotificationGroupManager mGroupManager =
76             Dependency.get(NotificationGroupManager.class);
77 
78     private NotificationEntryManager mEntryManager;
79 
80     private boolean mIsDozing;
81 
82     @Inject
NotificationGroupAlertTransferHelper()83     public NotificationGroupAlertTransferHelper() {
84         Dependency.get(StatusBarStateController.class).addCallback(this);
85     }
86 
87     /** Causes the TransferHelper to register itself as a listener to the appropriate classes. */
bind(NotificationEntryManager entryManager, NotificationGroupManager groupManager)88     public void bind(NotificationEntryManager entryManager,
89             NotificationGroupManager groupManager) {
90         if (mEntryManager != null) {
91             throw new IllegalStateException("Already bound.");
92         }
93 
94         // TODO(b/119637830): It would be good if GroupManager already had all pending notifications
95         // as normal children (i.e. add notifications to GroupManager before inflation) so that we
96         // don't have to have this dependency. We'd also have to worry less about the suppression
97         // not being up to date.
98         mEntryManager = entryManager;
99 
100         mEntryManager.addNotificationEntryListener(mNotificationEntryListener);
101         groupManager.addOnGroupChangeListener(mOnGroupChangeListener);
102     }
103 
104     /**
105      * Whether or not a notification has transferred its alert state to the notification and
106      * the notification should alert after inflating.
107      *
108      * @param entry notification to check
109      * @return true if the entry was transferred to and should inflate + alert
110      */
isAlertTransferPending(@onNull NotificationEntry entry)111     public boolean isAlertTransferPending(@NonNull NotificationEntry entry) {
112         PendingAlertInfo alertInfo = mPendingAlerts.get(entry.key);
113         return alertInfo != null && alertInfo.isStillValid();
114     }
115 
setHeadsUpManager(HeadsUpManager headsUpManager)116     public void setHeadsUpManager(HeadsUpManager headsUpManager) {
117         mHeadsUpManager = headsUpManager;
118     }
119 
120     @Override
onStateChanged(int newState)121     public void onStateChanged(int newState) {}
122 
123     @Override
onDozingChanged(boolean isDozing)124     public void onDozingChanged(boolean isDozing) {
125         if (mIsDozing != isDozing) {
126             for (GroupAlertEntry groupAlertEntry : mGroupAlertEntries.values()) {
127                 groupAlertEntry.mLastAlertTransferTime = 0;
128                 groupAlertEntry.mAlertSummaryOnNextAddition = false;
129             }
130         }
131         mIsDozing = isDozing;
132     }
133 
134     private final OnGroupChangeListener mOnGroupChangeListener = new OnGroupChangeListener() {
135         @Override
136         public void onGroupCreated(NotificationGroup group, String groupKey) {
137             mGroupAlertEntries.put(groupKey, new GroupAlertEntry(group));
138         }
139 
140         @Override
141         public void onGroupRemoved(NotificationGroup group, String groupKey) {
142             mGroupAlertEntries.remove(groupKey);
143         }
144 
145         @Override
146         public void onGroupSuppressionChanged(NotificationGroup group, boolean suppressed) {
147             AlertingNotificationManager alertManager = getActiveAlertManager();
148             if (suppressed) {
149                 if (alertManager.isAlerting(group.summary.key)) {
150                     handleSuppressedSummaryAlerted(group.summary, alertManager);
151                 }
152             } else {
153                 // Group summary can be null if we are no longer suppressed because the summary was
154                 // removed. In that case, we don't need to alert the summary.
155                 if (group.summary == null) {
156                     return;
157                 }
158                 GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(mGroupManager.getGroupKey(
159                         group.summary.notification));
160                 // Group is no longer suppressed. We should check if we need to transfer the alert
161                 // back to the summary now that it's no longer suppressed.
162                 if (groupAlertEntry.mAlertSummaryOnNextAddition) {
163                     if (!alertManager.isAlerting(group.summary.key)) {
164                         alertNotificationWhenPossible(group.summary, alertManager);
165                     }
166                     groupAlertEntry.mAlertSummaryOnNextAddition = false;
167                 } else {
168                     checkShouldTransferBack(groupAlertEntry);
169                 }
170             }
171         }
172     };
173 
174     @Override
onAmbientStateChanged(NotificationEntry entry, boolean isAmbient)175     public void onAmbientStateChanged(NotificationEntry entry, boolean isAmbient) {
176         onAlertStateChanged(entry, isAmbient, mAmbientPulseManager);
177     }
178 
179     @Override
onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp)180     public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) {
181         onAlertStateChanged(entry, isHeadsUp, mHeadsUpManager);
182     }
183 
onAlertStateChanged(NotificationEntry entry, boolean isAlerting, AlertingNotificationManager alertManager)184     private void onAlertStateChanged(NotificationEntry entry, boolean isAlerting,
185             AlertingNotificationManager alertManager) {
186         if (isAlerting && mGroupManager.isSummaryOfSuppressedGroup(entry.notification)) {
187             handleSuppressedSummaryAlerted(entry, alertManager);
188         }
189     }
190 
191     private final NotificationEntryListener mNotificationEntryListener =
192             new NotificationEntryListener() {
193         // Called when a new notification has been posted but is not inflated yet. We use this to
194         // see as early as we can if we need to abort a transfer.
195         @Override
196         public void onPendingEntryAdded(NotificationEntry entry) {
197             String groupKey = mGroupManager.getGroupKey(entry.notification);
198             GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(groupKey);
199             if (groupAlertEntry != null) {
200                 checkShouldTransferBack(groupAlertEntry);
201             }
202         }
203 
204         // Called when the entry's reinflation has finished. If there is an alert pending, we
205         // then show the alert.
206         @Override
207         public void onEntryReinflated(NotificationEntry entry) {
208             PendingAlertInfo alertInfo = mPendingAlerts.remove(entry.key);
209             if (alertInfo != null) {
210                 if (alertInfo.isStillValid()) {
211                     alertNotificationWhenPossible(entry, getActiveAlertManager());
212                 } else {
213                     // The transfer is no longer valid. Free the content.
214                     entry.getRow().freeContentViewWhenSafe(
215                             alertInfo.mAlertManager.getContentFlag());
216                 }
217             }
218         }
219 
220         @Override
221         public void onEntryRemoved(
222                 @Nullable NotificationEntry entry,
223                 NotificationVisibility visibility,
224                 boolean removedByUser) {
225             // Removes any alerts pending on this entry. Note that this will not stop any inflation
226             // tasks started by a transfer, so this should only be used as clean-up for when
227             // inflation is stopped and the pending alert no longer needs to happen.
228             mPendingAlerts.remove(entry.key);
229         }
230     };
231 
232     /**
233      * Gets the number of new notifications pending inflation that will be added to the group
234      * but currently aren't and should not alert.
235      *
236      * @param group group to check
237      * @return the number of new notifications that will be added to the group
238      */
getPendingChildrenNotAlerting(@onNull NotificationGroup group)239     private int getPendingChildrenNotAlerting(@NonNull NotificationGroup group) {
240         if (mEntryManager == null) {
241             return 0;
242         }
243         int number = 0;
244         Iterable<NotificationEntry> values = mEntryManager.getPendingNotificationsIterator();
245         for (NotificationEntry entry : values) {
246             if (isPendingNotificationInGroup(entry, group) && onlySummaryAlerts(entry)) {
247                 number++;
248             }
249         }
250         return number;
251     }
252 
253     /**
254      * Checks if the pending inflations will add children to this group.
255      *
256      * @param group group to check
257      * @return true if a pending notification will add to this group
258      */
pendingInflationsWillAddChildren(@onNull NotificationGroup group)259     private boolean pendingInflationsWillAddChildren(@NonNull NotificationGroup group) {
260         if (mEntryManager == null) {
261             return false;
262         }
263         Iterable<NotificationEntry> values = mEntryManager.getPendingNotificationsIterator();
264         for (NotificationEntry entry : values) {
265             if (isPendingNotificationInGroup(entry, group)) {
266                 return true;
267             }
268         }
269         return false;
270     }
271 
272     /**
273      * Checks if a new pending notification will be added to the group.
274      *
275      * @param entry pending notification
276      * @param group group to check
277      * @return true if the notification will add to the group, false o/w
278      */
isPendingNotificationInGroup(@onNull NotificationEntry entry, @NonNull NotificationGroup group)279     private boolean isPendingNotificationInGroup(@NonNull NotificationEntry entry,
280             @NonNull NotificationGroup group) {
281         String groupKey = mGroupManager.getGroupKey(group.summary.notification);
282         return mGroupManager.isGroupChild(entry.notification)
283                 && Objects.equals(mGroupManager.getGroupKey(entry.notification), groupKey)
284                 && !group.children.containsKey(entry.key);
285     }
286 
287     /**
288      * Handles the scenario where a summary that has been suppressed is alerted.  A suppressed
289      * summary should for all intents and purposes be invisible to the user and as a result should
290      * not alert.  When this is the case, it is our responsibility to pass the alert to the
291      * appropriate child which will be the representative notification alerting for the group.
292      *
293      * @param summary the summary that is suppressed and alerting
294      * @param alertManager the alert manager that manages the alerting summary
295      */
handleSuppressedSummaryAlerted(@onNull NotificationEntry summary, @NonNull AlertingNotificationManager alertManager)296     private void handleSuppressedSummaryAlerted(@NonNull NotificationEntry summary,
297             @NonNull AlertingNotificationManager alertManager) {
298         StatusBarNotification sbn = summary.notification;
299         GroupAlertEntry groupAlertEntry =
300                 mGroupAlertEntries.get(mGroupManager.getGroupKey(sbn));
301         if (!mGroupManager.isSummaryOfSuppressedGroup(summary.notification)
302                 || !alertManager.isAlerting(sbn.getKey())
303                 || groupAlertEntry == null) {
304             return;
305         }
306 
307         if (pendingInflationsWillAddChildren(groupAlertEntry.mGroup)) {
308             // New children will actually be added to this group, let's not transfer the alert.
309             return;
310         }
311 
312         NotificationEntry child = mGroupManager.getLogicalChildren(summary.notification).iterator().next();
313         if (child != null) {
314             if (child.getRow().keepInParent()
315                     || child.isRowRemoved()
316                     || child.isRowDismissed()) {
317                 // The notification is actually already removed. No need to alert it.
318                 return;
319             }
320             if (!alertManager.isAlerting(child.key) && onlySummaryAlerts(summary)) {
321                 groupAlertEntry.mLastAlertTransferTime = SystemClock.elapsedRealtime();
322             }
323             transferAlertState(summary, child, alertManager);
324         }
325     }
326 
327     /**
328      * Transfers the alert state one entry to another. We remove the alert from the first entry
329      * immediately to have the incorrect one up as short as possible. The second should alert
330      * when possible.
331      *
332      * @param fromEntry entry to transfer alert from
333      * @param toEntry entry to transfer to
334      * @param alertManager alert manager for the alert type
335      */
transferAlertState(@onNull NotificationEntry fromEntry, @NonNull NotificationEntry toEntry, @NonNull AlertingNotificationManager alertManager)336     private void transferAlertState(@NonNull NotificationEntry fromEntry, @NonNull NotificationEntry toEntry,
337             @NonNull AlertingNotificationManager alertManager) {
338         alertManager.removeNotification(fromEntry.key, true /* releaseImmediately */);
339         alertNotificationWhenPossible(toEntry, alertManager);
340     }
341 
342     /**
343      * Determines if we need to transfer the alert back to the summary from the child and does
344      * so if needed.
345      *
346      * This can happen since notification groups are not delivered as a whole unit and it is
347      * possible we erroneously transfer the alert from the summary to the child even though
348      * more children are coming. Thus, if a child is added within a certain timeframe after we
349      * transfer, we back out and alert the summary again.
350      *
351      * @param groupAlertEntry group alert entry to check
352      */
checkShouldTransferBack(@onNull GroupAlertEntry groupAlertEntry)353     private void checkShouldTransferBack(@NonNull GroupAlertEntry groupAlertEntry) {
354         if (SystemClock.elapsedRealtime() - groupAlertEntry.mLastAlertTransferTime
355                 < ALERT_TRANSFER_TIMEOUT) {
356             NotificationEntry summary = groupAlertEntry.mGroup.summary;
357             AlertingNotificationManager alertManager = getActiveAlertManager();
358 
359             if (!onlySummaryAlerts(summary)) {
360                 return;
361             }
362             ArrayList<NotificationEntry> children = mGroupManager.getLogicalChildren(summary.notification);
363             int numChildren = children.size();
364             int numPendingChildren = getPendingChildrenNotAlerting(groupAlertEntry.mGroup);
365             numChildren += numPendingChildren;
366             if (numChildren <= 1) {
367                 return;
368             }
369             boolean releasedChild = false;
370             for (int i = 0; i < children.size(); i++) {
371                 NotificationEntry entry = children.get(i);
372                 if (onlySummaryAlerts(entry) && alertManager.isAlerting(entry.key)) {
373                     releasedChild = true;
374                     alertManager.removeNotification(entry.key, true /* releaseImmediately */);
375                 }
376                 if (mPendingAlerts.containsKey(entry.key)) {
377                     // This is the child that would've been removed if it was inflated.
378                     releasedChild = true;
379                     mPendingAlerts.get(entry.key).mAbortOnInflation = true;
380                 }
381             }
382             if (releasedChild && !alertManager.isAlerting(summary.key)) {
383                 boolean notifyImmediately = (numChildren - numPendingChildren) > 1;
384                 if (notifyImmediately) {
385                     alertNotificationWhenPossible(summary, alertManager);
386                 } else {
387                     // Should wait until the pending child inflates before alerting.
388                     groupAlertEntry.mAlertSummaryOnNextAddition = true;
389                 }
390                 groupAlertEntry.mLastAlertTransferTime = 0;
391             }
392         }
393     }
394 
395     /**
396      * Tries to alert the notification. If its content view is not inflated, we inflate and continue
397      * when the entry finishes inflating the view.
398      *
399      * @param entry entry to show
400      * @param alertManager alert manager for the alert type
401      */
alertNotificationWhenPossible(@onNull NotificationEntry entry, @NonNull AlertingNotificationManager alertManager)402     private void alertNotificationWhenPossible(@NonNull NotificationEntry entry,
403             @NonNull AlertingNotificationManager alertManager) {
404         @InflationFlag int contentFlag = alertManager.getContentFlag();
405         if (!entry.getRow().isInflationFlagSet(contentFlag)) {
406             mPendingAlerts.put(entry.key, new PendingAlertInfo(entry, alertManager));
407             entry.getRow().updateInflationFlag(contentFlag, true /* shouldInflate */);
408             entry.getRow().inflateViews();
409             return;
410         }
411         if (alertManager.isAlerting(entry.key)) {
412             alertManager.updateNotification(entry.key, true /* alert */);
413         } else {
414             alertManager.showNotification(entry);
415         }
416     }
417 
getActiveAlertManager()418     private AlertingNotificationManager getActiveAlertManager() {
419         return mIsDozing ? mAmbientPulseManager : mHeadsUpManager;
420     }
421 
onlySummaryAlerts(NotificationEntry entry)422     private boolean onlySummaryAlerts(NotificationEntry entry) {
423         return entry.notification.getNotification().getGroupAlertBehavior()
424                 == Notification.GROUP_ALERT_SUMMARY;
425     }
426 
427     /**
428      * Information about a pending alert used to determine if the alert is still needed when
429      * inflation completes.
430      */
431     private class PendingAlertInfo {
432         /**
433          * The alert manager when the transfer is initiated.
434          */
435         final AlertingNotificationManager mAlertManager;
436 
437         /**
438          * The original notification when the transfer is initiated. This is used to determine if
439          * the transfer is still valid if the notification is updated.
440          */
441         final StatusBarNotification mOriginalNotification;
442         final NotificationEntry mEntry;
443 
444         /**
445          * The notification is still pending inflation but we've decided that we no longer need
446          * the content view (e.g. suppression might have changed and we decided we need to transfer
447          * back). However, there is no way to abort just this inflation if other inflation requests
448          * have started (see {@link AsyncInflationTask#supersedeTask(InflationTask)}). So instead
449          * we just flag it as aborted and free when it's inflated.
450          */
451         boolean mAbortOnInflation;
452 
PendingAlertInfo(NotificationEntry entry, AlertingNotificationManager alertManager)453         PendingAlertInfo(NotificationEntry entry, AlertingNotificationManager alertManager) {
454             mOriginalNotification = entry.notification;
455             mEntry = entry;
456             mAlertManager = alertManager;
457         }
458 
459         /**
460          * Whether or not the pending alert is still valid and should still alert after inflation.
461          *
462          * @return true if the pending alert should still occur, false o/w
463          */
isStillValid()464         private boolean isStillValid() {
465             if (mAbortOnInflation) {
466                 // Notification is aborted due to the transfer being explicitly cancelled
467                 return false;
468             }
469             if (mAlertManager != getActiveAlertManager()) {
470                 // Alert manager has changed
471                 return false;
472             }
473             if (mEntry.notification.getGroupKey() != mOriginalNotification.getGroupKey()) {
474                 // Groups have changed
475                 return false;
476             }
477             if (mEntry.notification.getNotification().isGroupSummary()
478                     != mOriginalNotification.getNotification().isGroupSummary()) {
479                 // Notification has changed from group summary to not or vice versa
480                 return false;
481             }
482             return true;
483         }
484     }
485 
486     /**
487      * Contains alert metadata for the notification group used to determine when/how the alert
488      * should be transferred.
489      */
490     private static class GroupAlertEntry {
491         /**
492          * The time when the last alert transfer from summary to child happened.
493          */
494         long mLastAlertTransferTime;
495         boolean mAlertSummaryOnNextAddition;
496         final NotificationGroup mGroup;
497 
GroupAlertEntry(NotificationGroup group)498         GroupAlertEntry(NotificationGroup group) {
499             this.mGroup = group;
500         }
501     }
502 }
503