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