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 package com.android.systemui.statusbar.notification.logging; 17 18 import android.content.Context; 19 import android.os.Handler; 20 import android.os.RemoteException; 21 import android.os.ServiceManager; 22 import android.os.SystemClock; 23 import android.os.Trace; 24 import android.service.notification.NotificationListenerService; 25 import android.service.notification.StatusBarNotification; 26 import android.util.ArrayMap; 27 import android.util.ArraySet; 28 import android.util.Log; 29 30 import androidx.annotation.Nullable; 31 32 import com.android.internal.annotations.GuardedBy; 33 import com.android.internal.annotations.VisibleForTesting; 34 import com.android.internal.statusbar.IStatusBarService; 35 import com.android.internal.statusbar.NotificationVisibility; 36 import com.android.systemui.dagger.qualifiers.UiBackground; 37 import com.android.systemui.plugins.statusbar.StatusBarStateController; 38 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; 39 import com.android.systemui.statusbar.NotificationListener; 40 import com.android.systemui.statusbar.StatusBarState; 41 import com.android.systemui.statusbar.notification.NotificationEntryListener; 42 import com.android.systemui.statusbar.notification.NotificationEntryManager; 43 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 44 import com.android.systemui.statusbar.notification.dagger.NotificationsModule; 45 import com.android.systemui.statusbar.notification.stack.ExpandableViewState; 46 import com.android.systemui.statusbar.notification.stack.NotificationListContainer; 47 48 import java.util.Collection; 49 import java.util.Collections; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.concurrent.Executor; 53 54 import javax.inject.Inject; 55 56 /** 57 * Handles notification logging, in particular, logging which notifications are visible and which 58 * are not. 59 */ 60 public class NotificationLogger implements StateListener { 61 private static final String TAG = "NotificationLogger"; 62 private static final boolean DEBUG = false; 63 64 /** The minimum delay in ms between reports of notification visibility. */ 65 private static final int VISIBILITY_REPORT_MIN_DELAY_MS = 500; 66 67 /** Keys of notifications currently visible to the user. */ 68 private final ArraySet<NotificationVisibility> mCurrentlyVisibleNotifications = 69 new ArraySet<>(); 70 71 // Dependencies: 72 private final NotificationListenerService mNotificationListener; 73 private final Executor mUiBgExecutor; 74 private final NotificationEntryManager mEntryManager; 75 private final NotificationPanelLogger mNotificationPanelLogger; 76 private final ExpansionStateLogger mExpansionStateLogger; 77 78 protected Handler mHandler = new Handler(); 79 protected IStatusBarService mBarService; 80 private long mLastVisibilityReportUptimeMs; 81 private NotificationListContainer mListContainer; 82 private final Object mDozingLock = new Object(); 83 @GuardedBy("mDozingLock") 84 private Boolean mDozing = null; // Use null to indicate state is not yet known 85 @GuardedBy("mDozingLock") 86 private Boolean mLockscreen = null; // Use null to indicate state is not yet known 87 private Boolean mPanelExpanded = null; // Use null to indicate state is not yet known 88 private boolean mLogging = false; 89 90 protected final OnChildLocationsChangedListener mNotificationLocationsChangedListener = 91 new OnChildLocationsChangedListener() { 92 @Override 93 public void onChildLocationsChanged() { 94 if (mHandler.hasCallbacks(mVisibilityReporter)) { 95 // Visibilities will be reported when the existing 96 // callback is executed. 97 return; 98 } 99 // Calculate when we're allowed to run the visibility 100 // reporter. Note that this timestamp might already have 101 // passed. That's OK, the callback will just be executed 102 // ASAP. 103 long nextReportUptimeMs = 104 mLastVisibilityReportUptimeMs + VISIBILITY_REPORT_MIN_DELAY_MS; 105 mHandler.postAtTime(mVisibilityReporter, nextReportUptimeMs); 106 } 107 }; 108 109 // Tracks notifications currently visible in mNotificationStackScroller and 110 // emits visibility events via NoMan on changes. 111 protected Runnable mVisibilityReporter = new Runnable() { 112 private final ArraySet<NotificationVisibility> mTmpNewlyVisibleNotifications = 113 new ArraySet<>(); 114 private final ArraySet<NotificationVisibility> mTmpCurrentlyVisibleNotifications = 115 new ArraySet<>(); 116 private final ArraySet<NotificationVisibility> mTmpNoLongerVisibleNotifications = 117 new ArraySet<>(); 118 119 @Override 120 public void run() { 121 mLastVisibilityReportUptimeMs = SystemClock.uptimeMillis(); 122 123 // 1. Loop over active entries: 124 // A. Keep list of visible notifications. 125 // B. Keep list of previously hidden, now visible notifications. 126 // 2. Compute no-longer visible notifications by removing currently 127 // visible notifications from the set of previously visible 128 // notifications. 129 // 3. Report newly visible and no-longer visible notifications. 130 // 4. Keep currently visible notifications for next report. 131 List<NotificationEntry> activeNotifications = mEntryManager.getVisibleNotifications(); 132 int N = activeNotifications.size(); 133 for (int i = 0; i < N; i++) { 134 NotificationEntry entry = activeNotifications.get(i); 135 String key = entry.getSbn().getKey(); 136 boolean isVisible = mListContainer.isInVisibleLocation(entry); 137 NotificationVisibility visObj = NotificationVisibility.obtain(key, i, N, isVisible, 138 getNotificationLocation(entry)); 139 boolean previouslyVisible = mCurrentlyVisibleNotifications.contains(visObj); 140 if (isVisible) { 141 // Build new set of visible notifications. 142 mTmpCurrentlyVisibleNotifications.add(visObj); 143 if (!previouslyVisible) { 144 mTmpNewlyVisibleNotifications.add(visObj); 145 } 146 } else { 147 // release object 148 visObj.recycle(); 149 } 150 } 151 mTmpNoLongerVisibleNotifications.addAll(mCurrentlyVisibleNotifications); 152 mTmpNoLongerVisibleNotifications.removeAll(mTmpCurrentlyVisibleNotifications); 153 154 logNotificationVisibilityChanges( 155 mTmpNewlyVisibleNotifications, mTmpNoLongerVisibleNotifications); 156 157 recycleAllVisibilityObjects(mCurrentlyVisibleNotifications); 158 mCurrentlyVisibleNotifications.addAll(mTmpCurrentlyVisibleNotifications); 159 160 mExpansionStateLogger.onVisibilityChanged( 161 mTmpCurrentlyVisibleNotifications, mTmpCurrentlyVisibleNotifications); 162 Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Active]", N); 163 Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Visible]", 164 mCurrentlyVisibleNotifications.size()); 165 166 recycleAllVisibilityObjects(mTmpNoLongerVisibleNotifications); 167 mTmpCurrentlyVisibleNotifications.clear(); 168 mTmpNewlyVisibleNotifications.clear(); 169 mTmpNoLongerVisibleNotifications.clear(); 170 } 171 }; 172 173 /** 174 * Returns the location of the notification referenced by the given {@link NotificationEntry}. 175 */ getNotificationLocation( NotificationEntry entry)176 public static NotificationVisibility.NotificationLocation getNotificationLocation( 177 NotificationEntry entry) { 178 if (entry == null || entry.getRow() == null || entry.getRow().getViewState() == null) { 179 return NotificationVisibility.NotificationLocation.LOCATION_UNKNOWN; 180 } 181 return convertNotificationLocation(entry.getRow().getViewState().location); 182 } 183 convertNotificationLocation( int location)184 private static NotificationVisibility.NotificationLocation convertNotificationLocation( 185 int location) { 186 switch (location) { 187 case ExpandableViewState.LOCATION_FIRST_HUN: 188 return NotificationVisibility.NotificationLocation.LOCATION_FIRST_HEADS_UP; 189 case ExpandableViewState.LOCATION_HIDDEN_TOP: 190 return NotificationVisibility.NotificationLocation.LOCATION_HIDDEN_TOP; 191 case ExpandableViewState.LOCATION_MAIN_AREA: 192 return NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA; 193 case ExpandableViewState.LOCATION_BOTTOM_STACK_PEEKING: 194 return NotificationVisibility.NotificationLocation.LOCATION_BOTTOM_STACK_PEEKING; 195 case ExpandableViewState.LOCATION_BOTTOM_STACK_HIDDEN: 196 return NotificationVisibility.NotificationLocation.LOCATION_BOTTOM_STACK_HIDDEN; 197 case ExpandableViewState.LOCATION_GONE: 198 return NotificationVisibility.NotificationLocation.LOCATION_GONE; 199 default: 200 return NotificationVisibility.NotificationLocation.LOCATION_UNKNOWN; 201 } 202 } 203 204 /** 205 * Injected constructor. See {@link NotificationsModule}. 206 */ NotificationLogger(NotificationListener notificationListener, @UiBackground Executor uiBgExecutor, NotificationEntryManager entryManager, StatusBarStateController statusBarStateController, ExpansionStateLogger expansionStateLogger, NotificationPanelLogger notificationPanelLogger)207 public NotificationLogger(NotificationListener notificationListener, 208 @UiBackground Executor uiBgExecutor, 209 NotificationEntryManager entryManager, 210 StatusBarStateController statusBarStateController, 211 ExpansionStateLogger expansionStateLogger, 212 NotificationPanelLogger notificationPanelLogger) { 213 mNotificationListener = notificationListener; 214 mUiBgExecutor = uiBgExecutor; 215 mEntryManager = entryManager; 216 mBarService = IStatusBarService.Stub.asInterface( 217 ServiceManager.getService(Context.STATUS_BAR_SERVICE)); 218 mExpansionStateLogger = expansionStateLogger; 219 mNotificationPanelLogger = notificationPanelLogger; 220 // Not expected to be destroyed, don't need to unsubscribe 221 statusBarStateController.addCallback(this); 222 223 entryManager.addNotificationEntryListener(new NotificationEntryListener() { 224 @Override 225 public void onEntryRemoved( 226 NotificationEntry entry, 227 NotificationVisibility visibility, 228 boolean removedByUser, 229 int reason) { 230 mExpansionStateLogger.onEntryRemoved(entry.getKey()); 231 } 232 233 @Override 234 public void onPreEntryUpdated(NotificationEntry entry) { 235 mExpansionStateLogger.onEntryUpdated(entry.getKey()); 236 } 237 238 @Override 239 public void onInflationError( 240 StatusBarNotification notification, 241 Exception exception) { 242 logNotificationError(notification, exception); 243 } 244 }); 245 } 246 setUpWithContainer(NotificationListContainer listContainer)247 public void setUpWithContainer(NotificationListContainer listContainer) { 248 mListContainer = listContainer; 249 } 250 stopNotificationLogging()251 public void stopNotificationLogging() { 252 if (mLogging) { 253 mLogging = false; 254 if (DEBUG) { 255 Log.i(TAG, "stopNotificationLogging: log notifications invisible"); 256 } 257 // Report all notifications as invisible and turn down the 258 // reporter. 259 if (!mCurrentlyVisibleNotifications.isEmpty()) { 260 logNotificationVisibilityChanges( 261 Collections.emptyList(), mCurrentlyVisibleNotifications); 262 recycleAllVisibilityObjects(mCurrentlyVisibleNotifications); 263 } 264 mHandler.removeCallbacks(mVisibilityReporter); 265 mListContainer.setChildLocationsChangedListener(null); 266 } 267 } 268 startNotificationLogging()269 public void startNotificationLogging() { 270 if (!mLogging) { 271 mLogging = true; 272 if (DEBUG) { 273 Log.i(TAG, "startNotificationLogging"); 274 } 275 mListContainer.setChildLocationsChangedListener(mNotificationLocationsChangedListener); 276 // Some transitions like mVisibleToUser=false -> mVisibleToUser=true don't 277 // cause the scroller to emit child location events. Hence generate 278 // one ourselves to guarantee that we're reporting visible 279 // notifications. 280 // (Note that in cases where the scroller does emit events, this 281 // additional event doesn't break anything.) 282 mNotificationLocationsChangedListener.onChildLocationsChanged(); 283 } 284 } 285 setDozing(boolean dozing)286 private void setDozing(boolean dozing) { 287 synchronized (mDozingLock) { 288 mDozing = dozing; 289 maybeUpdateLoggingStatus(); 290 } 291 } 292 293 /** 294 * Logs Notification inflation error 295 */ logNotificationError( StatusBarNotification notification, Exception exception)296 private void logNotificationError( 297 StatusBarNotification notification, 298 Exception exception) { 299 try { 300 mBarService.onNotificationError( 301 notification.getPackageName(), 302 notification.getTag(), 303 notification.getId(), 304 notification.getUid(), 305 notification.getInitialPid(), 306 exception.getMessage(), 307 notification.getUserId()); 308 } catch (RemoteException ex) { 309 // The end is nigh. 310 } 311 } 312 logNotificationVisibilityChanges( Collection<NotificationVisibility> newlyVisible, Collection<NotificationVisibility> noLongerVisible)313 private void logNotificationVisibilityChanges( 314 Collection<NotificationVisibility> newlyVisible, 315 Collection<NotificationVisibility> noLongerVisible) { 316 if (newlyVisible.isEmpty() && noLongerVisible.isEmpty()) { 317 return; 318 } 319 final NotificationVisibility[] newlyVisibleAr = cloneVisibilitiesAsArr(newlyVisible); 320 final NotificationVisibility[] noLongerVisibleAr = cloneVisibilitiesAsArr(noLongerVisible); 321 322 mUiBgExecutor.execute(() -> { 323 try { 324 mBarService.onNotificationVisibilityChanged(newlyVisibleAr, noLongerVisibleAr); 325 } catch (RemoteException e) { 326 // Ignore. 327 } 328 329 final int N = newlyVisibleAr.length; 330 if (N > 0) { 331 String[] newlyVisibleKeyAr = new String[N]; 332 for (int i = 0; i < N; i++) { 333 newlyVisibleKeyAr[i] = newlyVisibleAr[i].key; 334 } 335 // TODO: Call NotificationEntryManager to do this, once it exists. 336 // TODO: Consider not catching all runtime exceptions here. 337 try { 338 mNotificationListener.setNotificationsShown(newlyVisibleKeyAr); 339 } catch (RuntimeException e) { 340 Log.d(TAG, "failed setNotificationsShown: ", e); 341 } 342 } 343 recycleAllVisibilityObjects(newlyVisibleAr); 344 recycleAllVisibilityObjects(noLongerVisibleAr); 345 }); 346 } 347 recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array)348 private void recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array) { 349 final int N = array.size(); 350 for (int i = 0 ; i < N; i++) { 351 array.valueAt(i).recycle(); 352 } 353 array.clear(); 354 } 355 recycleAllVisibilityObjects(NotificationVisibility[] array)356 private void recycleAllVisibilityObjects(NotificationVisibility[] array) { 357 final int N = array.length; 358 for (int i = 0 ; i < N; i++) { 359 if (array[i] != null) { 360 array[i].recycle(); 361 } 362 } 363 } 364 cloneVisibilitiesAsArr( Collection<NotificationVisibility> c)365 private static NotificationVisibility[] cloneVisibilitiesAsArr( 366 Collection<NotificationVisibility> c) { 367 final NotificationVisibility[] array = new NotificationVisibility[c.size()]; 368 int i = 0; 369 for(NotificationVisibility nv: c) { 370 if (nv != null) { 371 array[i] = nv.clone(); 372 } 373 i++; 374 } 375 return array; 376 } 377 378 @VisibleForTesting getVisibilityReporter()379 public Runnable getVisibilityReporter() { 380 return mVisibilityReporter; 381 } 382 383 @Override onStateChanged(int newState)384 public void onStateChanged(int newState) { 385 if (DEBUG) { 386 Log.i(TAG, "onStateChanged: new=" + newState); 387 } 388 synchronized (mDozingLock) { 389 mLockscreen = (newState == StatusBarState.KEYGUARD 390 || newState == StatusBarState.SHADE_LOCKED); 391 } 392 } 393 394 @Override onDozingChanged(boolean isDozing)395 public void onDozingChanged(boolean isDozing) { 396 if (DEBUG) { 397 Log.i(TAG, "onDozingChanged: new=" + isDozing); 398 } 399 setDozing(isDozing); 400 } 401 402 @GuardedBy("mDozingLock") maybeUpdateLoggingStatus()403 private void maybeUpdateLoggingStatus() { 404 if (mPanelExpanded == null || mDozing == null) { 405 if (DEBUG) { 406 Log.i(TAG, "Panel status unclear: panelExpandedKnown=" 407 + (mPanelExpanded == null) + " dozingKnown=" + (mDozing == null)); 408 } 409 return; 410 } 411 // Once we know panelExpanded and Dozing, turn logging on & off when appropriate 412 boolean lockscreen = mLockscreen == null ? false : mLockscreen; 413 if (mPanelExpanded && !mDozing) { 414 mNotificationPanelLogger.logPanelShown(lockscreen, 415 mEntryManager.getVisibleNotifications()); 416 if (DEBUG) { 417 Log.i(TAG, "Notification panel shown, lockscreen=" + lockscreen); 418 } 419 startNotificationLogging(); 420 } else { 421 if (DEBUG) { 422 Log.i(TAG, "Notification panel hidden, lockscreen=" + lockscreen); 423 } 424 stopNotificationLogging(); 425 } 426 } 427 428 /** 429 * Called by StatusBar to notify the logger that the panel expansion has changed. 430 * The panel may be showing any of the normal notification panel, the AOD, or the bouncer. 431 * @param isExpanded True if the panel is expanded. 432 */ onPanelExpandedChanged(boolean isExpanded)433 public void onPanelExpandedChanged(boolean isExpanded) { 434 if (DEBUG) { 435 Log.i(TAG, "onPanelExpandedChanged: new=" + isExpanded); 436 } 437 mPanelExpanded = isExpanded; 438 synchronized (mDozingLock) { 439 maybeUpdateLoggingStatus(); 440 } 441 } 442 443 /** 444 * Called when the notification is expanded / collapsed. 445 */ onExpansionChanged(String key, boolean isUserAction, boolean isExpanded)446 public void onExpansionChanged(String key, boolean isUserAction, boolean isExpanded) { 447 NotificationVisibility.NotificationLocation location = 448 getNotificationLocation(mEntryManager.getActiveNotificationUnfiltered(key)); 449 mExpansionStateLogger.onExpansionChanged(key, isUserAction, isExpanded, location); 450 } 451 452 @VisibleForTesting setVisibilityReporter(Runnable visibilityReporter)453 public void setVisibilityReporter(Runnable visibilityReporter) { 454 mVisibilityReporter = visibilityReporter; 455 } 456 457 /** 458 * A listener that is notified when some child locations might have changed. 459 */ 460 public interface OnChildLocationsChangedListener { onChildLocationsChanged()461 void onChildLocationsChanged(); 462 } 463 464 /** 465 * Logs the expansion state change when the notification is visible. 466 */ 467 public static class ExpansionStateLogger { 468 /** Notification key -> state, should be accessed in UI offload thread only. */ 469 private final Map<String, State> mExpansionStates = new ArrayMap<>(); 470 471 /** 472 * Notification key -> last logged expansion state, should be accessed in UI thread only. 473 */ 474 private final Map<String, Boolean> mLoggedExpansionState = new ArrayMap<>(); 475 private final Executor mUiBgExecutor; 476 @VisibleForTesting 477 IStatusBarService mBarService; 478 479 @Inject ExpansionStateLogger(@iBackground Executor uiBgExecutor)480 public ExpansionStateLogger(@UiBackground Executor uiBgExecutor) { 481 mUiBgExecutor = uiBgExecutor; 482 mBarService = 483 IStatusBarService.Stub.asInterface( 484 ServiceManager.getService(Context.STATUS_BAR_SERVICE)); 485 } 486 487 @VisibleForTesting onExpansionChanged(String key, boolean isUserAction, boolean isExpanded, NotificationVisibility.NotificationLocation location)488 void onExpansionChanged(String key, boolean isUserAction, boolean isExpanded, 489 NotificationVisibility.NotificationLocation location) { 490 State state = getState(key); 491 state.mIsUserAction = isUserAction; 492 state.mIsExpanded = isExpanded; 493 state.mLocation = location; 494 maybeNotifyOnNotificationExpansionChanged(key, state); 495 } 496 497 @VisibleForTesting onVisibilityChanged( Collection<NotificationVisibility> newlyVisible, Collection<NotificationVisibility> noLongerVisible)498 void onVisibilityChanged( 499 Collection<NotificationVisibility> newlyVisible, 500 Collection<NotificationVisibility> noLongerVisible) { 501 final NotificationVisibility[] newlyVisibleAr = 502 cloneVisibilitiesAsArr(newlyVisible); 503 final NotificationVisibility[] noLongerVisibleAr = 504 cloneVisibilitiesAsArr(noLongerVisible); 505 506 for (NotificationVisibility nv : newlyVisibleAr) { 507 State state = getState(nv.key); 508 state.mIsVisible = true; 509 state.mLocation = nv.location; 510 maybeNotifyOnNotificationExpansionChanged(nv.key, state); 511 } 512 for (NotificationVisibility nv : noLongerVisibleAr) { 513 State state = getState(nv.key); 514 state.mIsVisible = false; 515 } 516 } 517 518 @VisibleForTesting onEntryRemoved(String key)519 void onEntryRemoved(String key) { 520 mExpansionStates.remove(key); 521 mLoggedExpansionState.remove(key); 522 } 523 524 @VisibleForTesting onEntryUpdated(String key)525 void onEntryUpdated(String key) { 526 // When the notification is updated, we should consider the notification as not 527 // yet logged. 528 mLoggedExpansionState.remove(key); 529 } 530 getState(String key)531 private State getState(String key) { 532 State state = mExpansionStates.get(key); 533 if (state == null) { 534 state = new State(); 535 mExpansionStates.put(key, state); 536 } 537 return state; 538 } 539 maybeNotifyOnNotificationExpansionChanged(final String key, State state)540 private void maybeNotifyOnNotificationExpansionChanged(final String key, State state) { 541 if (!state.isFullySet()) { 542 return; 543 } 544 if (!state.mIsVisible) { 545 return; 546 } 547 Boolean loggedExpansionState = mLoggedExpansionState.get(key); 548 // Consider notification is initially collapsed, so only expanded is logged in the 549 // first time. 550 if (loggedExpansionState == null && !state.mIsExpanded) { 551 return; 552 } 553 if (loggedExpansionState != null 554 && state.mIsExpanded == loggedExpansionState) { 555 return; 556 } 557 mLoggedExpansionState.put(key, state.mIsExpanded); 558 final State stateToBeLogged = new State(state); 559 mUiBgExecutor.execute(() -> { 560 try { 561 mBarService.onNotificationExpansionChanged(key, stateToBeLogged.mIsUserAction, 562 stateToBeLogged.mIsExpanded, stateToBeLogged.mLocation.ordinal()); 563 } catch (RemoteException e) { 564 Log.e(TAG, "Failed to call onNotificationExpansionChanged: ", e); 565 } 566 }); 567 } 568 569 private static class State { 570 @Nullable 571 Boolean mIsUserAction; 572 @Nullable 573 Boolean mIsExpanded; 574 @Nullable 575 Boolean mIsVisible; 576 @Nullable 577 NotificationVisibility.NotificationLocation mLocation; 578 State()579 private State() {} 580 State(State state)581 private State(State state) { 582 this.mIsUserAction = state.mIsUserAction; 583 this.mIsExpanded = state.mIsExpanded; 584 this.mIsVisible = state.mIsVisible; 585 this.mLocation = state.mLocation; 586 } 587 isFullySet()588 private boolean isFullySet() { 589 return mIsUserAction != null && mIsExpanded != null && mIsVisible != null 590 && mLocation != null; 591 } 592 } 593 } 594 } 595