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