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