1 /*
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 
17 package com.android.systemui.statusbar.notification.collection.coordinator;
18 
19 import static com.android.systemui.statusbar.NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY;
20 import static com.android.systemui.statusbar.notification.interruption.HeadsUpController.alertAgain;
21 
22 import androidx.annotation.NonNull;
23 import androidx.annotation.Nullable;
24 
25 import com.android.systemui.statusbar.NotificationRemoteInputManager;
26 import com.android.systemui.statusbar.notification.collection.ListEntry;
27 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
28 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
29 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope;
30 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
31 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
32 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
33 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
34 import com.android.systemui.statusbar.notification.collection.render.NodeController;
35 import com.android.systemui.statusbar.notification.dagger.IncomingHeader;
36 import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder;
37 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
38 import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt;
39 import com.android.systemui.statusbar.policy.HeadsUpManager;
40 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
41 
42 import java.util.Objects;
43 
44 import javax.inject.Inject;
45 
46 /**
47  * Coordinates heads up notification (HUN) interactions with the notification pipeline based on
48  * the HUN state reported by the {@link HeadsUpManager}. In this class we only consider one
49  * notification, in particular the {@link HeadsUpManager#getTopEntry()}, to be HeadsUpping at a
50  * time even though other notifications may be queued to heads up next.
51  *
52  * The current HUN, but not HUNs that are queued to heads up, will be:
53  * - Lifetime extended until it's no longer heads upping.
54  * - Promoted out of its group if it's a child of a group.
55  * - In the HeadsUpCoordinatorSection. Ordering is configured in {@link NotifCoordinators}.
56  * - Removed from HeadsUpManager if it's removed from the NotificationCollection.
57  *
58  * Note: The inflation callback in {@link PreparationCoordinator} handles showing HUNs.
59  */
60 @CoordinatorScope
61 public class HeadsUpCoordinator implements Coordinator {
62     private static final String TAG = "HeadsUpCoordinator";
63 
64     private final HeadsUpManager mHeadsUpManager;
65     private final HeadsUpViewBinder mHeadsUpViewBinder;
66     private final NotificationInterruptStateProvider mNotificationInterruptStateProvider;
67     private final NotificationRemoteInputManager mRemoteInputManager;
68     private final NodeController mIncomingHeaderController;
69 
70     // tracks the current HeadUpNotification reported by HeadsUpManager
71     private @Nullable NotificationEntry mCurrentHun;
72 
73     private NotifLifetimeExtender.OnEndLifetimeExtensionCallback mEndLifetimeExtension;
74     private NotificationEntry mNotifExtendingLifetime; // notif we've extended the lifetime for
75 
76     @Inject
HeadsUpCoordinator( HeadsUpManager headsUpManager, HeadsUpViewBinder headsUpViewBinder, NotificationInterruptStateProvider notificationInterruptStateProvider, NotificationRemoteInputManager remoteInputManager, @IncomingHeader NodeController incomingHeaderController)77     public HeadsUpCoordinator(
78             HeadsUpManager headsUpManager,
79             HeadsUpViewBinder headsUpViewBinder,
80             NotificationInterruptStateProvider notificationInterruptStateProvider,
81             NotificationRemoteInputManager remoteInputManager,
82             @IncomingHeader NodeController incomingHeaderController) {
83         mHeadsUpManager = headsUpManager;
84         mHeadsUpViewBinder = headsUpViewBinder;
85         mNotificationInterruptStateProvider = notificationInterruptStateProvider;
86         mRemoteInputManager = remoteInputManager;
87         mIncomingHeaderController = incomingHeaderController;
88     }
89 
90     @Override
attach(NotifPipeline pipeline)91     public void attach(NotifPipeline pipeline) {
92         mHeadsUpManager.addListener(mOnHeadsUpChangedListener);
93         pipeline.addCollectionListener(mNotifCollectionListener);
94         pipeline.addPromoter(mNotifPromoter);
95         pipeline.addNotificationLifetimeExtender(mLifetimeExtender);
96     }
97 
getSectioner()98     public NotifSectioner getSectioner() {
99         return mNotifSectioner;
100     }
101 
onHeadsUpViewBound(NotificationEntry entry)102     private void onHeadsUpViewBound(NotificationEntry entry) {
103         mHeadsUpManager.showNotification(entry);
104     }
105 
106     private final NotifCollectionListener mNotifCollectionListener = new NotifCollectionListener() {
107         /**
108          * Notification was just added and if it should heads up, bind the view and then show it.
109          */
110         @Override
111         public void onEntryAdded(NotificationEntry entry) {
112             if (mNotificationInterruptStateProvider.shouldHeadsUp(entry)) {
113                 mHeadsUpViewBinder.bindHeadsUpView(
114                         entry,
115                         HeadsUpCoordinator.this::onHeadsUpViewBound);
116             }
117         }
118 
119         /**
120          * Notification could've updated to be heads up or not heads up. Even if it did update to
121          * heads up, if the notification specified that it only wants to alert once, don't heads
122          * up again.
123          */
124         @Override
125         public void onEntryUpdated(NotificationEntry entry) {
126             boolean hunAgain = alertAgain(entry, entry.getSbn().getNotification());
127             // includes check for whether this notification should be filtered:
128             boolean shouldHeadsUp = mNotificationInterruptStateProvider.shouldHeadsUp(entry);
129             final boolean wasHeadsUp = mHeadsUpManager.isAlerting(entry.getKey());
130             if (wasHeadsUp) {
131                 if (shouldHeadsUp) {
132                     mHeadsUpManager.updateNotification(entry.getKey(), hunAgain);
133                 } else if (!mHeadsUpManager.isEntryAutoHeadsUpped(entry.getKey())) {
134                     // We don't want this to be interrupting anymore, let's remove it
135                     mHeadsUpManager.removeNotification(
136                             entry.getKey(), false /* removeImmediately */);
137                 }
138             } else if (shouldHeadsUp && hunAgain) {
139                 // This notification was updated to be heads up, show it!
140                 mHeadsUpViewBinder.bindHeadsUpView(
141                         entry,
142                         HeadsUpCoordinator.this::onHeadsUpViewBound);
143             }
144         }
145 
146         /**
147          * Stop alerting HUNs that are removed from the notification collection
148          */
149         @Override
150         public void onEntryRemoved(NotificationEntry entry, int reason) {
151             final String entryKey = entry.getKey();
152             if (mHeadsUpManager.isAlerting(entryKey)) {
153                 boolean removeImmediatelyForRemoteInput =
154                         mRemoteInputManager.isSpinning(entryKey)
155                                 && !FORCE_REMOTE_INPUT_HISTORY;
156                 mHeadsUpManager.removeNotification(entry.getKey(), removeImmediatelyForRemoteInput);
157             }
158         }
159 
160         @Override
161         public void onEntryCleanUp(NotificationEntry entry) {
162             mHeadsUpViewBinder.abortBindCallback(entry);
163         }
164     };
165 
166     private final NotifLifetimeExtender mLifetimeExtender = new NotifLifetimeExtender() {
167         @Override
168         public @NonNull String getName() {
169             return TAG;
170         }
171 
172         @Override
173         public void setCallback(@NonNull OnEndLifetimeExtensionCallback callback) {
174             mEndLifetimeExtension = callback;
175         }
176 
177         @Override
178         public boolean shouldExtendLifetime(@NonNull NotificationEntry entry, int reason) {
179             boolean isShowingHun = isCurrentlyShowingHun(entry);
180             if (isShowingHun) {
181                 mNotifExtendingLifetime = entry;
182             }
183             return isShowingHun;
184         }
185 
186         @Override
187         public void cancelLifetimeExtension(@NonNull NotificationEntry entry) {
188             if (Objects.equals(mNotifExtendingLifetime, entry)) {
189                 mNotifExtendingLifetime = null;
190             }
191         }
192     };
193 
194     private final NotifPromoter mNotifPromoter = new NotifPromoter(TAG) {
195         @Override
196         public boolean shouldPromoteToTopLevel(NotificationEntry entry) {
197             return isCurrentlyShowingHun(entry);
198         }
199     };
200 
201     private final NotifSectioner mNotifSectioner = new NotifSectioner("HeadsUp",
202             NotificationPriorityBucketKt.BUCKET_HEADS_UP) {
203         @Override
204         public boolean isInSection(ListEntry entry) {
205             return isCurrentlyShowingHun(entry);
206         }
207 
208         @Nullable
209         @Override
210         public NodeController getHeaderNodeController() {
211             // TODO: remove SHOW_ALL_SECTIONS, this redundant method, and mIncomingHeaderController
212             if (RankingCoordinator.SHOW_ALL_SECTIONS) {
213                 return mIncomingHeaderController;
214             }
215             return null;
216         }
217     };
218 
219     private final OnHeadsUpChangedListener mOnHeadsUpChangedListener =
220             new OnHeadsUpChangedListener() {
221         @Override
222         public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) {
223             NotificationEntry newHUN = mHeadsUpManager.getTopEntry();
224             if (!Objects.equals(mCurrentHun, newHUN)) {
225                 mCurrentHun = newHUN;
226                 endNotifLifetimeExtension();
227             }
228             if (!isHeadsUp) {
229                 mHeadsUpViewBinder.unbindHeadsUpView(entry);
230             }
231         }
232     };
233 
isCurrentlyShowingHun(ListEntry entry)234     private boolean isCurrentlyShowingHun(ListEntry entry) {
235         return mCurrentHun == entry.getRepresentativeEntry();
236     }
237 
endNotifLifetimeExtension()238     private void endNotifLifetimeExtension() {
239         if (mNotifExtendingLifetime != null) {
240             mEndLifetimeExtension.onEndLifetimeExtension(
241                     mLifetimeExtender,
242                     mNotifExtendingLifetime);
243             mNotifExtendingLifetime = null;
244         }
245     }
246 }
247