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