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