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