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