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.coalescer; 18 19 import static java.util.Objects.requireNonNull; 20 21 import android.annotation.MainThread; 22 import android.app.NotificationChannel; 23 import android.os.UserHandle; 24 import android.service.notification.NotificationListenerService.Ranking; 25 import android.service.notification.NotificationListenerService.RankingMap; 26 import android.service.notification.StatusBarNotification; 27 import android.util.ArrayMap; 28 29 import androidx.annotation.NonNull; 30 31 import com.android.systemui.Dumpable; 32 import com.android.systemui.dagger.qualifiers.Main; 33 import com.android.systemui.statusbar.NotificationListener; 34 import com.android.systemui.statusbar.NotificationListener.NotificationHandler; 35 import com.android.systemui.util.concurrency.DelayableExecutor; 36 import com.android.systemui.util.time.SystemClock; 37 38 import java.io.FileDescriptor; 39 import java.io.PrintWriter; 40 import java.util.ArrayList; 41 import java.util.Comparator; 42 import java.util.List; 43 import java.util.Map; 44 45 import javax.inject.Inject; 46 47 /** 48 * An attempt to make posting notification groups an atomic process 49 * 50 * Due to the nature of the groups API, individual members of a group are posted to system server 51 * one at a time. This means that whenever a group member is posted, we don't know if there are any 52 * more members soon to be posted. 53 * 54 * The Coalescer sits between the NotificationListenerService and the NotifCollection. It clusters 55 * new notifications that are members of groups and delays their posting until any of the following 56 * criteria are met: 57 * 58 * - A few milliseconds pass (see groupLingerDuration on the constructor) 59 * - Any notification in the delayed group is updated 60 * - Any notification in the delayed group is retracted 61 * 62 * Once we cross this threshold, all members of the group in question are posted atomically to the 63 * NotifCollection. If this process was triggered by an update or removal, then that event is then 64 * passed along to the NotifCollection. 65 */ 66 @MainThread 67 public class GroupCoalescer implements Dumpable { 68 private final DelayableExecutor mMainExecutor; 69 private final SystemClock mClock; 70 private final GroupCoalescerLogger mLogger; 71 private final long mMinGroupLingerDuration; 72 private final long mMaxGroupLingerDuration; 73 74 private BatchableNotificationHandler mHandler; 75 76 private final Map<String, CoalescedEvent> mCoalescedEvents = new ArrayMap<>(); 77 private final Map<String, EventBatch> mBatches = new ArrayMap<>(); 78 79 @Inject GroupCoalescer( @ain DelayableExecutor mainExecutor, SystemClock clock, GroupCoalescerLogger logger)80 public GroupCoalescer( 81 @Main DelayableExecutor mainExecutor, 82 SystemClock clock, 83 GroupCoalescerLogger logger) { 84 this(mainExecutor, clock, logger, MIN_GROUP_LINGER_DURATION, MAX_GROUP_LINGER_DURATION); 85 } 86 87 /** 88 * @param minGroupLingerDuration How long, in ms, to wait for another notification from the same 89 * group to arrive before emitting all pending events for that 90 * group. Each subsequent arrival of a group member resets the 91 * timer for that group. 92 * @param maxGroupLingerDuration The maximum time, in ms, that a group can linger in the 93 * coalescer before it's force-emitted. 94 */ GroupCoalescer( @ain DelayableExecutor mainExecutor, SystemClock clock, GroupCoalescerLogger logger, long minGroupLingerDuration, long maxGroupLingerDuration)95 GroupCoalescer( 96 @Main DelayableExecutor mainExecutor, 97 SystemClock clock, 98 GroupCoalescerLogger logger, 99 long minGroupLingerDuration, 100 long maxGroupLingerDuration) { 101 mMainExecutor = mainExecutor; 102 mClock = clock; 103 mLogger = logger; 104 mMinGroupLingerDuration = minGroupLingerDuration; 105 mMaxGroupLingerDuration = maxGroupLingerDuration; 106 } 107 108 /** 109 * Attaches the coalescer to the pipeline, making it ready to receive events. Should only be 110 * called once. 111 */ attach(NotificationListener listenerService)112 public void attach(NotificationListener listenerService) { 113 listenerService.addNotificationHandler(mListener); 114 } 115 setNotificationHandler(BatchableNotificationHandler handler)116 public void setNotificationHandler(BatchableNotificationHandler handler) { 117 mHandler = handler; 118 } 119 120 private final NotificationHandler mListener = new NotificationHandler() { 121 @Override 122 public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { 123 maybeEmitBatch(sbn); 124 applyRanking(rankingMap); 125 126 final boolean shouldCoalesce = handleNotificationPosted(sbn, rankingMap); 127 128 if (shouldCoalesce) { 129 mLogger.logEventCoalesced(sbn.getKey()); 130 mHandler.onNotificationRankingUpdate(rankingMap); 131 } else { 132 mHandler.onNotificationPosted(sbn, rankingMap); 133 } 134 } 135 136 @Override 137 public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) { 138 maybeEmitBatch(sbn); 139 applyRanking(rankingMap); 140 mHandler.onNotificationRemoved(sbn, rankingMap); 141 } 142 143 @Override 144 public void onNotificationRemoved( 145 StatusBarNotification sbn, 146 RankingMap rankingMap, 147 int reason) { 148 maybeEmitBatch(sbn); 149 applyRanking(rankingMap); 150 mHandler.onNotificationRemoved(sbn, rankingMap, reason); 151 } 152 153 @Override 154 public void onNotificationRankingUpdate(RankingMap rankingMap) { 155 applyRanking(rankingMap); 156 mHandler.onNotificationRankingUpdate(rankingMap); 157 } 158 159 @Override 160 public void onNotificationsInitialized() { 161 mHandler.onNotificationsInitialized(); 162 } 163 164 @Override 165 public void onNotificationChannelModified( 166 String pkgName, 167 UserHandle user, 168 NotificationChannel channel, 169 int modificationType) { 170 mHandler.onNotificationChannelModified(pkgName, user, channel, modificationType); 171 } 172 }; 173 maybeEmitBatch(StatusBarNotification sbn)174 private void maybeEmitBatch(StatusBarNotification sbn) { 175 final CoalescedEvent event = mCoalescedEvents.get(sbn.getKey()); 176 final EventBatch batch = mBatches.get(sbn.getGroupKey()); 177 if (event != null) { 178 mLogger.logEarlyEmit(sbn.getKey(), requireNonNull(event.getBatch()).mGroupKey); 179 emitBatch(requireNonNull(event.getBatch())); 180 } else if (batch != null 181 && mClock.uptimeMillis() - batch.mCreatedTimestamp >= mMaxGroupLingerDuration) { 182 mLogger.logMaxBatchTimeout(sbn.getKey(), batch.mGroupKey); 183 emitBatch(batch); 184 } 185 } 186 187 /** 188 * @return True if the notification was coalesced and false otherwise. 189 */ handleNotificationPosted( StatusBarNotification sbn, RankingMap rankingMap)190 private boolean handleNotificationPosted( 191 StatusBarNotification sbn, 192 RankingMap rankingMap) { 193 194 if (mCoalescedEvents.containsKey(sbn.getKey())) { 195 throw new IllegalStateException( 196 "Notification has already been coalesced: " + sbn.getKey()); 197 } 198 199 if (sbn.isGroup()) { 200 final EventBatch batch = getOrBuildBatch(sbn.getGroupKey()); 201 202 CoalescedEvent event = 203 new CoalescedEvent( 204 sbn.getKey(), 205 batch.mMembers.size(), 206 sbn, 207 requireRanking(rankingMap, sbn.getKey()), 208 batch); 209 mCoalescedEvents.put(event.getKey(), event); 210 211 batch.mMembers.add(event); 212 resetShortTimeout(batch); 213 214 return true; 215 } else { 216 return false; 217 } 218 } 219 getOrBuildBatch(final String groupKey)220 private EventBatch getOrBuildBatch(final String groupKey) { 221 EventBatch batch = mBatches.get(groupKey); 222 if (batch == null) { 223 batch = new EventBatch(mClock.uptimeMillis(), groupKey); 224 mBatches.put(groupKey, batch); 225 } 226 return batch; 227 } 228 resetShortTimeout(EventBatch batch)229 private void resetShortTimeout(EventBatch batch) { 230 if (batch.mCancelShortTimeout != null) { 231 batch.mCancelShortTimeout.run(); 232 } 233 batch.mCancelShortTimeout = 234 mMainExecutor.executeDelayed( 235 () -> { 236 batch.mCancelShortTimeout = null; 237 emitBatch(batch); 238 }, 239 mMinGroupLingerDuration); 240 } 241 emitBatch(EventBatch batch)242 private void emitBatch(EventBatch batch) { 243 if (batch != mBatches.get(batch.mGroupKey)) { 244 throw new IllegalStateException("Cannot emit out-of-date batch " + batch.mGroupKey); 245 } 246 if (batch.mMembers.isEmpty()) { 247 throw new IllegalStateException("Batch " + batch.mGroupKey + " cannot be empty"); 248 } 249 if (batch.mCancelShortTimeout != null) { 250 batch.mCancelShortTimeout.run(); 251 batch.mCancelShortTimeout = null; 252 } 253 254 mBatches.remove(batch.mGroupKey); 255 256 final List<CoalescedEvent> events = new ArrayList<>(batch.mMembers); 257 for (CoalescedEvent event : events) { 258 mCoalescedEvents.remove(event.getKey()); 259 event.setBatch(null); 260 } 261 events.sort(mEventComparator); 262 263 mLogger.logEmitBatch(batch.mGroupKey); 264 265 mHandler.onNotificationBatchPosted(events); 266 } 267 requireRanking(RankingMap rankingMap, String key)268 private Ranking requireRanking(RankingMap rankingMap, String key) { 269 Ranking ranking = new Ranking(); 270 if (!rankingMap.getRanking(key, ranking)) { 271 throw new IllegalArgumentException("Ranking map does not contain key " + key); 272 } 273 return ranking; 274 } 275 applyRanking(RankingMap rankingMap)276 private void applyRanking(RankingMap rankingMap) { 277 for (CoalescedEvent event : mCoalescedEvents.values()) { 278 Ranking ranking = new Ranking(); 279 if (rankingMap.getRanking(event.getKey(), ranking)) { 280 event.setRanking(ranking); 281 } else { 282 // TODO: (b/148791039) We should crash if we are ever handed a ranking with 283 // incomplete entries. Right now, there's a race condition in NotificationListener 284 // that means this might occur when SystemUI is starting up. 285 mLogger.logMissingRanking(event.getKey()); 286 } 287 } 288 } 289 290 @Override dump(@onNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args)291 public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { 292 long now = mClock.uptimeMillis(); 293 294 int eventCount = 0; 295 296 pw.println(); 297 pw.println("Coalesced notifications:"); 298 for (EventBatch batch : mBatches.values()) { 299 pw.println(" Batch " + batch.mGroupKey + ":"); 300 pw.println(" Created " + (now - batch.mCreatedTimestamp) + "ms ago"); 301 for (CoalescedEvent event : batch.mMembers) { 302 pw.println(" " + event.getKey()); 303 eventCount++; 304 } 305 } 306 307 if (eventCount != mCoalescedEvents.size()) { 308 pw.println(" ERROR: batches contain " + mCoalescedEvents.size() + " events but" 309 + " am tracking " + mCoalescedEvents.size() + " total events"); 310 pw.println(" All tracked events:"); 311 for (CoalescedEvent event : mCoalescedEvents.values()) { 312 pw.println(" " + event.getKey()); 313 } 314 } 315 } 316 317 private final Comparator<CoalescedEvent> mEventComparator = (o1, o2) -> { 318 int cmp = Boolean.compare( 319 o2.getSbn().getNotification().isGroupSummary(), 320 o1.getSbn().getNotification().isGroupSummary()); 321 if (cmp == 0) { 322 cmp = o1.getPosition() - o2.getPosition(); 323 } 324 return cmp; 325 }; 326 327 /** 328 * Extension of {@link NotificationListener.NotificationHandler} to include notification 329 * groups. 330 */ 331 public interface BatchableNotificationHandler extends NotificationHandler { 332 /** 333 * Fired whenever the coalescer needs to emit a batch of multiple post events. This is 334 * usually the addition of a new group, but can contain just a single event, or just an 335 * update to a subset of an existing group. 336 */ onNotificationBatchPosted(List<CoalescedEvent> events)337 void onNotificationBatchPosted(List<CoalescedEvent> events); 338 } 339 340 private static final int MIN_GROUP_LINGER_DURATION = 50; 341 private static final int MAX_GROUP_LINGER_DURATION = 500; 342 } 343