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