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