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