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