blob: 92426e54ec91ba000c30237d83e4e662eb6af7ce [file] [log] [blame]
Beverlya53fb0d2020-01-29 15:26:13 -05001/*
2 * Copyright (C) 2020 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.notification.collection.coordinator;
18
19import static android.service.notification.NotificationStats.DISMISSAL_OTHER;
Beverly7e0d6492020-02-07 16:22:14 -050020import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
Beverlya53fb0d2020-01-29 15:26:13 -050021
22import com.android.internal.statusbar.NotificationVisibility;
23import com.android.systemui.bubbles.BubbleController;
24import com.android.systemui.statusbar.notification.collection.NotifCollection;
25import com.android.systemui.statusbar.notification.collection.NotifPipeline;
26import com.android.systemui.statusbar.notification.collection.NotificationEntry;
27import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
28import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats;
29import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
30import com.android.systemui.statusbar.notification.logging.NotificationLogger;
31
32import java.util.HashSet;
33import java.util.Set;
34
35import javax.inject.Inject;
36import javax.inject.Singleton;
37
38/**
39 * Coordinates hiding, intercepting (the dismissal), and deletion of bubbled notifications.
40 *
41 * The typical "start state" for a bubbled notification is when a bubble-able notification is
42 * posted. It is visible as a bubble AND as a notification in the shade. From here, we can get
43 * into a few hidden-from-shade states described below:
44 *
45 * Start State -> Hidden from shade
46 * User expands the bubble so we hide its notification from the shade.
47 * OR
48 * User dismisses a group summary with a bubbled child. All bubbled children are now hidden from
49 * the shade. And the group summary's dismissal is intercepted + hidden from the shade (see below).
50 *
51 * Start State -> Dismissal intercepted + hidden from shade
52 * User dismisses the notification from the shade. We now hide the notification from the shade
53 * and intercept its dismissal (the removal signal is never sent to system server). We
54 * keep the notification alive in system server so that {@link BubbleController} can still
55 * respond to app-cancellations (ie: remove the bubble if the app cancels the notification).
56 *
57 */
58@Singleton
59public class BubbleCoordinator implements Coordinator {
60 private static final String TAG = "BubbleCoordinator";
61
62 private final BubbleController mBubbleController;
63 private final NotifCollection mNotifCollection;
64 private final Set<String> mInterceptedDismissalEntries = new HashSet<>();
65 private NotifPipeline mNotifPipeline;
66 private NotifDismissInterceptor.OnEndDismissInterception mOnEndDismissInterception;
67
68 @Inject
69 public BubbleCoordinator(
70 BubbleController bubbleController,
71 NotifCollection notifCollection) {
72 mBubbleController = bubbleController;
73 mNotifCollection = notifCollection;
74 }
75
76 @Override
77 public void attach(NotifPipeline pipeline) {
78 mNotifPipeline = pipeline;
79 mNotifPipeline.addNotificationDismissInterceptor(mDismissInterceptor);
Kevin Han22653012020-01-22 12:56:25 -080080 mNotifPipeline.addFinalizeFilter(mNotifFilter);
Beverlya53fb0d2020-01-29 15:26:13 -050081 mBubbleController.addNotifCallback(mNotifCallback);
82 }
83
84 private final NotifFilter mNotifFilter = new NotifFilter(TAG) {
85 @Override
86 public boolean shouldFilterOut(NotificationEntry entry, long now) {
87 return mBubbleController.isBubbleNotificationSuppressedFromShade(entry);
88 }
89 };
90
91 private final NotifDismissInterceptor mDismissInterceptor = new NotifDismissInterceptor() {
92 @Override
93 public String getName() {
94 return TAG;
95 }
96
97 @Override
98 public void setCallback(OnEndDismissInterception callback) {
99 mOnEndDismissInterception = callback;
100 }
101
102 @Override
103 public boolean shouldInterceptDismissal(NotificationEntry entry) {
104 // TODO: b/149041810 add support for intercepting app-cancelled bubble notifications
105 // for experimental bubbles
106 if (mBubbleController.handleDismissalInterception(entry)) {
107 mInterceptedDismissalEntries.add(entry.getKey());
108 return true;
109 } else {
110 mInterceptedDismissalEntries.remove(entry.getKey());
111 return false;
112 }
113 }
114
115 @Override
116 public void cancelDismissInterception(NotificationEntry entry) {
117 mInterceptedDismissalEntries.remove(entry.getKey());
118 }
119 };
120
121 private final BubbleController.NotifCallback mNotifCallback =
122 new BubbleController.NotifCallback() {
123 @Override
124 public void removeNotification(NotificationEntry entry, int reason) {
125 if (isInterceptingDismissal(entry)) {
126 mInterceptedDismissalEntries.remove(entry.getKey());
127 mOnEndDismissInterception.onEndDismissInterception(mDismissInterceptor, entry,
128 createDismissedByUserStats(entry));
Ned Burns88aeaf72020-03-13 20:45:19 -0400129 } else if (mNotifPipeline.getAllNotifs().contains(entry)) {
Beverlya53fb0d2020-01-29 15:26:13 -0500130 // Bubbles are hiding the notifications from the shade, but the bubble was
131 // deleted; therefore, the notification should be cancelled as if it were a user
132 // dismissal (this won't re-enter handleInterceptDimissal because Bubbles
133 // will have already marked it as no longer a bubble)
134 mNotifCollection.dismissNotification(entry, createDismissedByUserStats(entry));
135 }
136 }
137
138 @Override
139 public void invalidateNotifications(String reason) {
140 mNotifFilter.invalidateList();
141 }
142
143 @Override
144 public void maybeCancelSummary(NotificationEntry entry) {
145 // no-op
146 }
147 };
148
149 private boolean isInterceptingDismissal(NotificationEntry entry) {
150 return mInterceptedDismissalEntries.contains(entry.getKey());
151 }
152
153 private DismissedByUserStats createDismissedByUserStats(NotificationEntry entry) {
154 return new DismissedByUserStats(
155 DISMISSAL_OTHER,
Beverly7e0d6492020-02-07 16:22:14 -0500156 DISMISS_SENTIMENT_NEUTRAL,
Beverlya53fb0d2020-01-29 15:26:13 -0500157 NotificationVisibility.obtain(entry.getKey(),
158 entry.getRanking().getRank(),
Beverly7e0d6492020-02-07 16:22:14 -0500159 mNotifPipeline.getShadeListCount(),
Beverlya53fb0d2020-01-29 15:26:13 -0500160 true, // was visible as a bubble
161 NotificationLogger.getNotificationLocation(entry))
162 );
163 }
164}