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