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