blob: d38d30ef9eae141db73140c54c2140b6353fa941 [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
88 public void setHeadsUpManager(HeadsUpManager headsUpManager) {
89 mHeadsUpManager = headsUpManager;
90 }
91
92 public void setPendingEntries(HashMap<String, Entry> pendingNotifications) {
93 mPendingNotifications = pendingNotifications;
94 }
95
96 @Override
97 public void onStateChanged(int newState) {}
98
99 @Override
100 public void onDozingChanged(boolean isDozing) {
101 if (mIsDozing != isDozing) {
102 for (GroupAlertEntry groupAlertEntry : mGroupAlertEntries.values()) {
103 groupAlertEntry.mLastAlertTransferTime = 0;
104 groupAlertEntry.mAlertSummaryOnNextAddition = false;
105 }
106 }
107 mIsDozing = isDozing;
108 }
109
110 @Override
111 public void onGroupCreated(NotificationGroup group, String groupKey) {
112 mGroupAlertEntries.put(groupKey, new GroupAlertEntry(group));
113 }
114
115 @Override
116 public void onGroupRemoved(NotificationGroup group, String groupKey) {
117 mGroupAlertEntries.remove(groupKey);
118 }
119
120 @Override
121 public void onGroupSuppressionChanged(NotificationGroup group, boolean suppressed) {
122 AlertingNotificationManager alertManager = getActiveAlertManager();
123 if (suppressed) {
124 if (alertManager.isAlerting(group.summary.key)) {
125 handleSuppressedSummaryAlerted(group.summary, alertManager);
126 }
127 } else {
128 // Group summary can be null if we are no longer suppressed because the summary was
129 // removed. In that case, we don't need to alert the summary.
130 if (group.summary == null) {
131 return;
132 }
133 GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(mGroupManager.getGroupKey(
134 group.summary.notification));
135 // Group is no longer suppressed. We should check if we need to transfer the alert
136 // back to the summary now that it's no longer suppressed.
137 if (groupAlertEntry.mAlertSummaryOnNextAddition) {
138 if (!alertManager.isAlerting(group.summary.key)) {
139 alertNotificationWhenPossible(group.summary, alertManager);
140 }
141 groupAlertEntry.mAlertSummaryOnNextAddition = false;
142 } else {
143 checkShouldTransferBack(groupAlertEntry);
144 }
145 }
146 }
147
148 @Override
149 public void onAmbientStateChanged(Entry entry, boolean isAmbient) {
150 onAlertStateChanged(entry, isAmbient, mAmbientPulseManager);
151 }
152
153 @Override
154 public void onHeadsUpStateChanged(Entry entry, boolean isHeadsUp) {
155 onAlertStateChanged(entry, isHeadsUp, mHeadsUpManager);
156 }
157
158 private void onAlertStateChanged(Entry entry, boolean isAlerting,
159 AlertingNotificationManager alertManager) {
160 if (isAlerting && mGroupManager.isSummaryOfSuppressedGroup(entry.notification)) {
161 handleSuppressedSummaryAlerted(entry, alertManager);
162 }
163 }
164
165 /**
166 * Called when the entry's reinflation has finished. If there is an alert pending, we then
167 * show the alert.
168 *
169 * @param entry entry whose inflation has finished
170 */
171 public void onInflationFinished(@NonNull Entry entry) {
172 PendingAlertInfo alertInfo = mPendingAlerts.remove(entry.key);
173 if (alertInfo != null) {
174 if (alertInfo.isStillValid()) {
175 alertNotificationWhenPossible(entry, getActiveAlertManager());
176 } else {
177 // The transfer is no longer valid. Free the content.
178 entry.row.freeContentViewWhenSafe(alertInfo.mAlertManager.getContentFlag());
179 }
180 }
181 }
182
183 /**
184 * Called when the entry's reinflation has been aborted.
185 *
186 * @param entry entry whose inflation has been aborted
187 */
188 public void onInflationAborted(@NonNull Entry entry) {
189 GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(
190 mGroupManager.getGroupKey(entry.notification));
191 if (groupAlertEntry == null) {
192 return;
193 }
194 mPendingAlerts.remove(entry.key);
195 }
196
197 /**
198 * Called when a new notification has been posted but is not inflated yet. We use this to see
199 * as early as we can if we need to abort a transfer.
200 *
201 * @param entry entry that has been added
202 */
203 public void onPendingEntryAdded(@NonNull Entry entry) {
204 String groupKey = mGroupManager.getGroupKey(entry.notification);
205 GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(groupKey);
206 if (groupAlertEntry != null) {
207 checkShouldTransferBack(groupAlertEntry);
208 }
209 }
210
211 /**
212 * Gets the number of new notifications pending inflation that will be added to the group
213 * but currently aren't and should not alert.
214 *
215 * @param group group to check
216 * @return the number of new notifications that will be added to the group
217 */
218 private int getPendingChildrenNotAlerting(@NonNull NotificationGroup group) {
219 if (mPendingNotifications == null) {
220 return 0;
221 }
222 int number = 0;
223 Collection<Entry> values = mPendingNotifications.values();
224 for (Entry entry : values) {
225 if (isPendingNotificationInGroup(entry, group) && onlySummaryAlerts(entry)) {
226 number++;
227 }
228 }
229 return number;
230 }
231
232 /**
233 * Checks if the pending inflations will add children to this group.
234 *
235 * @param group group to check
236 * @return true if a pending notification will add to this group
237 */
238 private boolean pendingInflationsWillAddChildren(@NonNull NotificationGroup group) {
239 if (mPendingNotifications == null) {
240 return false;
241 }
242 Collection<Entry> values = mPendingNotifications.values();
243 for (Entry entry : values) {
244 if (isPendingNotificationInGroup(entry, group)) {
245 return true;
246 }
247 }
248 return false;
249 }
250
251 /**
252 * Checks if a new pending notification will be added to the group.
253 *
254 * @param entry pending notification
255 * @param group group to check
256 * @return true if the notification will add to the group, false o/w
257 */
258 private boolean isPendingNotificationInGroup(@NonNull Entry entry,
259 @NonNull NotificationGroup group) {
260 String groupKey = mGroupManager.getGroupKey(group.summary.notification);
261 return mGroupManager.isGroupChild(entry.notification)
262 && Objects.equals(mGroupManager.getGroupKey(entry.notification), groupKey)
263 && !group.children.containsKey(entry.key);
264 }
265
266 /**
267 * Handles the scenario where a summary that has been suppressed is alerted. A suppressed
268 * summary should for all intents and purposes be invisible to the user and as a result should
269 * not alert. When this is the case, it is our responsibility to pass the alert to the
270 * appropriate child which will be the representative notification alerting for the group.
271 *
272 * @param summary the summary that is suppressed and alerting
273 * @param alertManager the alert manager that manages the alerting summary
274 */
275 private void handleSuppressedSummaryAlerted(@NonNull Entry summary,
276 @NonNull AlertingNotificationManager alertManager) {
277 StatusBarNotification sbn = summary.notification;
278 GroupAlertEntry groupAlertEntry =
279 mGroupAlertEntries.get(mGroupManager.getGroupKey(sbn));
280 if (!mGroupManager.isSummaryOfSuppressedGroup(summary.notification)
281 || !alertManager.isAlerting(sbn.getKey())
282 || groupAlertEntry == null) {
283 return;
284 }
285
286 if (pendingInflationsWillAddChildren(groupAlertEntry.mGroup)) {
287 // New children will actually be added to this group, let's not transfer the alert.
288 return;
289 }
290
291 Entry child = mGroupManager.getLogicalChildren(summary.notification).iterator().next();
292 if (child != null) {
293 if (child.row.keepInParent()
294 || child.row.isRemoved()
295 || child.row.isDismissed()) {
296 // The notification is actually already removed. No need to alert it.
297 return;
298 }
299 if (!alertManager.isAlerting(child.key) && onlySummaryAlerts(summary)) {
300 groupAlertEntry.mLastAlertTransferTime = SystemClock.elapsedRealtime();
301 }
302 transferAlertState(summary, child, alertManager);
303 }
304 }
305
306 /**
307 * Transfers the alert state one entry to another. We remove the alert from the first entry
308 * immediately to have the incorrect one up as short as possible. The second should alert
309 * when possible.
310 *
311 * @param fromEntry entry to transfer alert from
312 * @param toEntry entry to transfer to
313 * @param alertManager alert manager for the alert type
314 */
315 private void transferAlertState(@NonNull Entry fromEntry, @NonNull Entry toEntry,
316 @NonNull AlertingNotificationManager alertManager) {
317 alertManager.removeNotification(fromEntry.key, true /* releaseImmediately */);
318 alertNotificationWhenPossible(toEntry, alertManager);
319 }
320
321 /**
322 * Determines if we need to transfer the alert back to the summary from the child and does
323 * so if needed.
324 *
325 * This can happen since notification groups are not delivered as a whole unit and it is
326 * possible we erroneously transfer the alert from the summary to the child even though
327 * more children are coming. Thus, if a child is added within a certain timeframe after we
328 * transfer, we back out and alert the summary again.
329 *
330 * @param groupAlertEntry group alert entry to check
331 */
332 private void checkShouldTransferBack(@NonNull GroupAlertEntry groupAlertEntry) {
333 if (SystemClock.elapsedRealtime() - groupAlertEntry.mLastAlertTransferTime
334 < ALERT_TRANSFER_TIMEOUT) {
335 Entry summary = groupAlertEntry.mGroup.summary;
336 AlertingNotificationManager alertManager = getActiveAlertManager();
337
338 if (!onlySummaryAlerts(summary)) {
339 return;
340 }
341 ArrayList<Entry> children = mGroupManager.getLogicalChildren(summary.notification);
342 int numChildren = children.size();
343 int numPendingChildren = getPendingChildrenNotAlerting(groupAlertEntry.mGroup);
344 numChildren += numPendingChildren;
345 if (numChildren <= 1) {
346 return;
347 }
348 boolean releasedChild = false;
349 for (int i = 0; i < children.size(); i++) {
350 Entry entry = children.get(i);
351 if (onlySummaryAlerts(entry) && alertManager.isAlerting(entry.key)) {
352 releasedChild = true;
353 alertManager.removeNotification(entry.key, true /* releaseImmediately */);
354 }
355 if (mPendingAlerts.containsKey(entry.key)) {
356 // This is the child that would've been removed if it was inflated.
357 releasedChild = true;
358 mPendingAlerts.get(entry.key).mAbortOnInflation = true;
359 }
360 }
361 if (releasedChild && !alertManager.isAlerting(summary.key)) {
362 boolean notifyImmediately = (numChildren - numPendingChildren) > 1;
363 if (notifyImmediately) {
364 alertNotificationWhenPossible(summary, alertManager);
365 } else {
366 // Should wait until the pending child inflates before alerting.
367 groupAlertEntry.mAlertSummaryOnNextAddition = true;
368 }
369 groupAlertEntry.mLastAlertTransferTime = 0;
370 }
371 }
372 }
373
374 /**
375 * Tries to alert the notification. If its content view is not inflated, we inflate and continue
376 * when the entry finishes inflating the view.
377 *
378 * @param entry entry to show
379 * @param alertManager alert manager for the alert type
380 */
381 private void alertNotificationWhenPossible(@NonNull Entry entry,
382 @NonNull AlertingNotificationManager alertManager) {
383 @InflationFlag int contentFlag = alertManager.getContentFlag();
384 if (!entry.row.isInflationFlagSet(contentFlag)) {
385 // Take in the current alert manager in case it changes.
386 mPendingAlerts.put(entry.key, new PendingAlertInfo(alertManager));
387 entry.row.updateInflationFlag(contentFlag, true /* shouldInflate */);
388 entry.row.inflateViews();
389 return;
390 }
391 if (alertManager.isAlerting(entry.key)) {
392 alertManager.updateNotification(entry.key, true /* alert */);
393 } else {
394 alertManager.showNotification(entry);
395 }
396 }
397
398 private AlertingNotificationManager getActiveAlertManager() {
399 return mIsDozing ? mAmbientPulseManager : mHeadsUpManager;
400 }
401
402 private boolean onlySummaryAlerts(Entry entry) {
403 return entry.notification.getNotification().getGroupAlertBehavior()
404 == Notification.GROUP_ALERT_SUMMARY;
405 }
406
407 /**
408 * Information about a pending alert used to determine if the alert is still needed when
409 * inflation completes.
410 */
411 private class PendingAlertInfo {
412 final AlertingNotificationManager mAlertManager;
413 /**
414 * The notification is still pending inflation but we've decided that we no longer need
415 * the content view (e.g. suppression might have changed and we decided we need to transfer
416 * back). However, there is no way to abort just this inflation if other inflation requests
417 * have started (see {@link AsyncInflationTask#supersedeTask(InflationTask)}). So instead
418 * we just flag it as aborted and free when it's inflated.
419 */
420 boolean mAbortOnInflation;
421
422 PendingAlertInfo(AlertingNotificationManager alertManager) {
423 mAlertManager = alertManager;
424 }
425
426 /**
427 * Whether or not the pending alert is still valid and should still alert after inflation.
428 *
429 * @return true if the pending alert should still occur, false o/w
430 */
431 private boolean isStillValid() {
432 if (mAbortOnInflation) {
433 // Notification is aborted due to the transfer being explicitly cancelled
434 return false;
435 }
436 if (mAlertManager != getActiveAlertManager()) {
437 // Alert manager has changed
438 return false;
439 }
440 return true;
441 }
442 }
443
444 /**
445 * Contains alert metadata for the notification group used to determine when/how the alert
446 * should be transferred.
447 */
448 private static class GroupAlertEntry {
449 /**
450 * The time when the last alert transfer from summary to child happened.
451 */
452 long mLastAlertTransferTime;
453 boolean mAlertSummaryOnNextAddition;
454 final NotificationGroup mGroup;
455
456 GroupAlertEntry(NotificationGroup group) {
457 this.mGroup = group;
458 }
459 }
460}