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