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