1 /* 2 * Copyright (C) 2017 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; 18 19 import static com.android.systemui.statusbar.RemoteInputController.processForRemoteInput; 20 21 import android.annotation.NonNull; 22 import android.annotation.SuppressLint; 23 import android.app.NotificationChannel; 24 import android.app.NotificationManager; 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.os.RemoteException; 28 import android.os.UserHandle; 29 import android.service.notification.StatusBarNotification; 30 import android.util.Log; 31 32 import com.android.systemui.dagger.SysUISingleton; 33 import com.android.systemui.dagger.qualifiers.Main; 34 import com.android.systemui.plugins.PluginManager; 35 import com.android.systemui.statusbar.dagger.CentralSurfacesModule; 36 import com.android.systemui.statusbar.notification.collection.NotifCollection; 37 import com.android.systemui.statusbar.notification.collection.PipelineDumpable; 38 import com.android.systemui.statusbar.notification.collection.PipelineDumper; 39 import com.android.systemui.statusbar.phone.CentralSurfaces; 40 import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins; 41 import com.android.systemui.util.time.SystemClock; 42 43 import java.util.ArrayList; 44 import java.util.Deque; 45 import java.util.List; 46 import java.util.concurrent.ConcurrentLinkedDeque; 47 import java.util.concurrent.Executor; 48 49 import javax.inject.Inject; 50 51 /** 52 * This class handles listening to notification updates and passing them along to 53 * NotificationPresenter to be displayed to the user. 54 */ 55 @SysUISingleton 56 @SuppressLint("OverrideAbstract") 57 public class NotificationListener extends NotificationListenerWithPlugins implements 58 PipelineDumpable { 59 private static final String TAG = "NotificationListener"; 60 private static final boolean DEBUG = CentralSurfaces.DEBUG; 61 private static final long MAX_RANKING_DELAY_MILLIS = 500L; 62 63 private final Context mContext; 64 private final NotificationManager mNotificationManager; 65 private final SystemClock mSystemClock; 66 private final Executor mMainExecutor; 67 private final List<NotificationHandler> mNotificationHandlers = new ArrayList<>(); 68 private final ArrayList<NotificationSettingsListener> mSettingsListeners = new ArrayList<>(); 69 70 private final Deque<RankingMap> mRankingMapQueue = new ConcurrentLinkedDeque<>(); 71 private final Runnable mDispatchRankingUpdateRunnable = this::dispatchRankingUpdate; 72 private long mSkippingRankingUpdatesSince = -1; 73 74 /** 75 * Injected constructor. See {@link CentralSurfacesModule}. 76 */ 77 @Inject NotificationListener( Context context, NotificationManager notificationManager, SystemClock systemClock, @Main Executor mainExecutor, PluginManager pluginManager)78 public NotificationListener( 79 Context context, 80 NotificationManager notificationManager, 81 SystemClock systemClock, 82 @Main Executor mainExecutor, 83 PluginManager pluginManager) { 84 super(pluginManager); 85 mContext = context; 86 mNotificationManager = notificationManager; 87 mSystemClock = systemClock; 88 mMainExecutor = mainExecutor; 89 } 90 91 /** Registers a listener that's notified when notifications are added/removed/etc. */ addNotificationHandler(NotificationHandler handler)92 public void addNotificationHandler(NotificationHandler handler) { 93 if (mNotificationHandlers.contains(handler)) { 94 throw new IllegalArgumentException("Listener is already added"); 95 } 96 mNotificationHandlers.add(handler); 97 } 98 99 /** Registers a listener that's notified when any notification-related settings change. */ addNotificationSettingsListener(NotificationSettingsListener listener)100 public void addNotificationSettingsListener(NotificationSettingsListener listener) { 101 mSettingsListeners.add(listener); 102 } 103 104 @Override onListenerConnected()105 public void onListenerConnected() { 106 if (DEBUG) Log.d(TAG, "onListenerConnected"); 107 onPluginConnected(); 108 final StatusBarNotification[] notifications = getActiveNotifications(); 109 if (notifications == null) { 110 Log.w(TAG, "onListenerConnected unable to get active notifications."); 111 return; 112 } 113 final RankingMap currentRanking = getCurrentRanking(); 114 mMainExecutor.execute(() -> { 115 // There's currently a race condition between the calls to getActiveNotifications() and 116 // getCurrentRanking(). It's possible for the ranking that we store here to not contain 117 // entries for every notification in getActiveNotifications(). To prevent downstream 118 // crashes, we temporarily fill in these missing rankings with stubs. 119 // See b/146011844 for long-term fix 120 final List<Ranking> newRankings = new ArrayList<>(); 121 for (StatusBarNotification sbn : notifications) { 122 newRankings.add(getRankingOrTemporaryStandIn(currentRanking, sbn.getKey())); 123 } 124 final RankingMap completeMap = new RankingMap(newRankings.toArray(new Ranking[0])); 125 126 for (StatusBarNotification sbn : notifications) { 127 for (NotificationHandler listener : mNotificationHandlers) { 128 listener.onNotificationPosted(sbn, completeMap); 129 } 130 } 131 for (NotificationHandler listener : mNotificationHandlers) { 132 listener.onNotificationsInitialized(); 133 } 134 }); 135 onSilentStatusBarIconsVisibilityChanged( 136 mNotificationManager.shouldHideSilentStatusBarIcons()); 137 } 138 139 @Override onNotificationPosted(final StatusBarNotification sbn, final RankingMap rankingMap)140 public void onNotificationPosted(final StatusBarNotification sbn, 141 final RankingMap rankingMap) { 142 if (DEBUG) Log.d(TAG, "onNotificationPosted: " + sbn); 143 if (sbn != null && !onPluginNotificationPosted(sbn, rankingMap)) { 144 mMainExecutor.execute(() -> { 145 processForRemoteInput(sbn.getNotification(), mContext); 146 147 for (NotificationHandler handler : mNotificationHandlers) { 148 handler.onNotificationPosted(sbn, rankingMap); 149 } 150 }); 151 } 152 } 153 154 @Override onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, int reason)155 public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, 156 int reason) { 157 if (DEBUG) Log.d(TAG, "onNotificationRemoved: " + sbn + " reason: " + reason); 158 if (sbn != null && !onPluginNotificationRemoved(sbn, rankingMap)) { 159 mMainExecutor.execute(() -> { 160 for (NotificationHandler handler : mNotificationHandlers) { 161 handler.onNotificationRemoved(sbn, rankingMap, reason); 162 } 163 }); 164 } 165 } 166 167 @Override onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap)168 public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) { 169 onNotificationRemoved(sbn, rankingMap, NotifCollection.REASON_UNKNOWN); 170 } 171 172 @Override onNotificationRankingUpdate(final RankingMap rankingMap)173 public void onNotificationRankingUpdate(final RankingMap rankingMap) { 174 if (DEBUG) Log.d(TAG, "onRankingUpdate"); 175 if (rankingMap != null) { 176 // Add the ranking to the queue, then run dispatchRankingUpdate() on the main thread 177 RankingMap r = onPluginRankingUpdate(rankingMap); 178 mRankingMapQueue.addLast(r); 179 // Maintaining our own queue and always posting the runnable allows us to guarantee the 180 // relative ordering of all events which are dispatched, which is important so that the 181 // RankingMap always has exactly the same elements that are current, per add/remove 182 // events. 183 mMainExecutor.execute(mDispatchRankingUpdateRunnable); 184 } 185 } 186 187 /** 188 * This method is (and must be) the sole consumer of the RankingMap queue. After pulling an 189 * object off the queue, it checks if the queue is empty, and only dispatches the ranking update 190 * if the queue is still empty. 191 */ dispatchRankingUpdate()192 private void dispatchRankingUpdate() { 193 if (DEBUG) Log.d(TAG, "dispatchRankingUpdate"); 194 RankingMap r = mRankingMapQueue.pollFirst(); 195 if (r == null) { 196 Log.wtf(TAG, "mRankingMapQueue was empty!"); 197 } 198 if (!mRankingMapQueue.isEmpty()) { 199 final long now = mSystemClock.elapsedRealtime(); 200 if (mSkippingRankingUpdatesSince == -1) { 201 mSkippingRankingUpdatesSince = now; 202 } 203 final long timeSkippingRankingUpdates = now - mSkippingRankingUpdatesSince; 204 if (timeSkippingRankingUpdates < MAX_RANKING_DELAY_MILLIS) { 205 if (DEBUG) { 206 Log.d(TAG, "Skipping dispatch of onNotificationRankingUpdate() -- " 207 + mRankingMapQueue.size() + " more updates already in the queue."); 208 } 209 return; 210 } 211 if (DEBUG) { 212 Log.d(TAG, "Proceeding with dispatch of onNotificationRankingUpdate() -- " 213 + mRankingMapQueue.size() + " more updates already in the queue."); 214 } 215 } 216 mSkippingRankingUpdatesSince = -1; 217 for (NotificationHandler handler : mNotificationHandlers) { 218 handler.onNotificationRankingUpdate(r); 219 } 220 } 221 222 @Override onNotificationChannelModified( String pkgName, UserHandle user, NotificationChannel channel, int modificationType)223 public void onNotificationChannelModified( 224 String pkgName, UserHandle user, NotificationChannel channel, int modificationType) { 225 if (DEBUG) Log.d(TAG, "onNotificationChannelModified"); 226 if (!onPluginNotificationChannelModified(pkgName, user, channel, modificationType)) { 227 mMainExecutor.execute(() -> { 228 for (NotificationHandler handler : mNotificationHandlers) { 229 handler.onNotificationChannelModified(pkgName, user, channel, modificationType); 230 } 231 }); 232 } 233 } 234 235 @Override onSilentStatusBarIconsVisibilityChanged(boolean hideSilentStatusIcons)236 public void onSilentStatusBarIconsVisibilityChanged(boolean hideSilentStatusIcons) { 237 for (NotificationSettingsListener listener : mSettingsListeners) { 238 listener.onStatusBarIconsBehaviorChanged(hideSilentStatusIcons); 239 } 240 } 241 unsnoozeNotification(@onNull String key)242 public final void unsnoozeNotification(@NonNull String key) { 243 if (!isBound()) return; 244 try { 245 getNotificationInterface().unsnoozeNotificationFromSystemListener(mWrapper, key); 246 } catch (android.os.RemoteException ex) { 247 Log.v(TAG, "Unable to contact notification manager", ex); 248 } 249 } 250 registerAsSystemService()251 public void registerAsSystemService() { 252 try { 253 registerAsSystemService(mContext, 254 new ComponentName(mContext.getPackageName(), getClass().getCanonicalName()), 255 UserHandle.USER_ALL); 256 } catch (RemoteException e) { 257 Log.e(TAG, "Unable to register notification listener", e); 258 } 259 } 260 261 @Override dumpPipeline(@onNull PipelineDumper d)262 public void dumpPipeline(@NonNull PipelineDumper d) { 263 d.dump("notificationHandlers", mNotificationHandlers); 264 } 265 getRankingOrTemporaryStandIn(RankingMap rankingMap, String key)266 private static Ranking getRankingOrTemporaryStandIn(RankingMap rankingMap, String key) { 267 Ranking ranking = new Ranking(); 268 if (!rankingMap.getRanking(key, ranking)) { 269 ranking.populate( 270 key, 271 /* rank= */ 0, 272 /* matchesInterruptionFilter= */ false, 273 /* visibilityOverride= */ 0, 274 /* suppressedVisualEffects= */ 0, 275 /* importance= */ 0, 276 /* explanation= */ null, 277 /* overrideGroupKey= */ null, 278 /* channel= */ null, 279 /* overridePeople= */ new ArrayList<>(), 280 /* snoozeCriteria= */ new ArrayList<>(), 281 /* showBadge= */ false, 282 /* userSentiment= */ 0, 283 /* hidden= */ false, 284 /* lastAudiblyAlertedMs= */ 0, 285 /* noisy= */ false, 286 /* smartActions= */ new ArrayList<>(), 287 /* smartReplies= */ new ArrayList<>(), 288 /* canBubble= */ false, 289 /* isTextChanged= */ false, 290 /* isConversation= */ false, 291 /* shortcutInfo= */ null, 292 /* rankingAdjustment= */ 0, 293 /* isBubble= */ false, 294 /* proposedImportance= */ 0, 295 /* sensitiveContent= */ false 296 ); 297 } 298 return ranking; 299 } 300 301 public interface NotificationSettingsListener { 302 onStatusBarIconsBehaviorChanged(boolean hideSilentStatusIcons)303 default void onStatusBarIconsBehaviorChanged(boolean hideSilentStatusIcons) { } 304 } 305 306 /** Interface for listening to add/remove events that we receive from NotificationManager. */ 307 public interface NotificationHandler { onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap)308 void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap); onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap)309 void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap); onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, int reason)310 void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, int reason); onNotificationRankingUpdate(RankingMap rankingMap)311 void onNotificationRankingUpdate(RankingMap rankingMap); 312 313 /** Called after a notification channel is modified. */ onNotificationChannelModified( String pkgName, UserHandle user, NotificationChannel channel, int modificationType)314 default void onNotificationChannelModified( 315 String pkgName, 316 UserHandle user, 317 NotificationChannel channel, 318 int modificationType) { 319 } 320 321 /** 322 * Called after the listener has connected to NoMan and posted any current notifications. 323 */ onNotificationsInitialized()324 void onNotificationsInitialized(); 325 } 326 } 327