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