1 /* 2 * Copyright (C) 2019 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 17 package com.android.systemui.statusbar; 18 19 import static android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS; 20 21 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_FROM_AOD; 22 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_TO_AOD; 23 24 import android.animation.Animator; 25 import android.animation.AnimatorListenerAdapter; 26 import android.animation.ObjectAnimator; 27 import android.animation.ValueAnimator; 28 import android.os.SystemProperties; 29 import android.os.Trace; 30 import android.text.format.DateFormat; 31 import android.util.FloatProperty; 32 import android.util.Log; 33 import android.view.Choreographer; 34 import android.view.InsetsFlags; 35 import android.view.View; 36 import android.view.ViewDebug; 37 import android.view.WindowInsets; 38 import android.view.WindowInsets.Type.InsetsType; 39 import android.view.WindowInsetsController.Appearance; 40 import android.view.WindowInsetsController.Behavior; 41 import android.view.animation.Interpolator; 42 43 import androidx.annotation.NonNull; 44 45 import com.android.app.animation.Interpolators; 46 import com.android.internal.annotations.GuardedBy; 47 import com.android.internal.annotations.VisibleForTesting; 48 import com.android.internal.jank.InteractionJankMonitor; 49 import com.android.internal.jank.InteractionJankMonitor.Configuration; 50 import com.android.internal.logging.UiEventLogger; 51 import com.android.keyguard.KeyguardClockSwitch; 52 import com.android.systemui.DejankUtils; 53 import com.android.systemui.Dumpable; 54 import com.android.systemui.R; 55 import com.android.systemui.dagger.SysUISingleton; 56 import com.android.systemui.dump.DumpManager; 57 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; 58 import com.android.systemui.shade.ShadeExpansionStateManager; 59 import com.android.systemui.statusbar.notification.stack.StackStateAnimator; 60 import com.android.systemui.statusbar.policy.CallbackController; 61 import com.android.systemui.util.Compile; 62 63 import java.io.PrintWriter; 64 import java.util.ArrayList; 65 import java.util.Comparator; 66 67 import javax.inject.Inject; 68 69 /** 70 * Tracks and reports on {@link StatusBarState}. 71 */ 72 @SysUISingleton 73 public class StatusBarStateControllerImpl implements 74 SysuiStatusBarStateController, 75 CallbackController<StateListener>, 76 Dumpable { 77 private static final String TAG = "SbStateController"; 78 private static final boolean DEBUG_IMMERSIVE_APPS = 79 SystemProperties.getBoolean("persist.debug.immersive_apps", false); 80 81 // Must be a power of 2 82 private static final int HISTORY_SIZE = 32; 83 84 private static final int MAX_STATE = StatusBarState.SHADE_LOCKED; 85 private static final int MIN_STATE = StatusBarState.SHADE; 86 87 private static final Comparator<RankedListener> sComparator = 88 Comparator.comparingInt(o -> o.mRank); 89 private static final FloatProperty<StatusBarStateControllerImpl> SET_DARK_AMOUNT_PROPERTY = 90 new FloatProperty<StatusBarStateControllerImpl>("mDozeAmount") { 91 92 @Override 93 public void setValue(StatusBarStateControllerImpl object, float value) { 94 object.setDozeAmountInternal(value); 95 } 96 97 @Override 98 public Float get(StatusBarStateControllerImpl object) { 99 return object.mDozeAmount; 100 } 101 }; 102 103 private final ArrayList<RankedListener> mListeners = new ArrayList<>(); 104 private final UiEventLogger mUiEventLogger; 105 private final InteractionJankMonitor mInteractionJankMonitor; 106 private int mState; 107 private int mLastState; 108 private int mUpcomingState; 109 private boolean mLeaveOpenOnKeyguardHide; 110 private boolean mKeyguardRequested; 111 112 // Record the HISTORY_SIZE most recent states 113 private int mHistoryIndex = 0; 114 private HistoricalState[] mHistoricalRecords = new HistoricalState[HISTORY_SIZE]; 115 // This is used by InteractionJankMonitor to get callback from HWUI. 116 private View mView; 117 118 /** 119 * If any of the system bars is hidden. 120 */ 121 private boolean mIsFullscreen = false; 122 123 /** 124 * If the device is currently pulsing (AOD2). 125 */ 126 private boolean mPulsing; 127 128 /** 129 * If the device is currently dozing or not. 130 */ 131 private boolean mIsDozing; 132 133 /** 134 * If the device is currently dreaming or not. 135 */ 136 private boolean mIsDreaming; 137 138 /** 139 * If the status bar is currently expanded or not. 140 */ 141 private boolean mIsExpanded; 142 143 /** 144 * Current {@link #mDozeAmount} animator. 145 */ 146 private ValueAnimator mDarkAnimator; 147 148 /** 149 * Current doze amount in this frame. 150 */ 151 private float mDozeAmount; 152 153 /** 154 * Where the animator will stop. 155 */ 156 private float mDozeAmountTarget; 157 158 /** 159 * The type of interpolator that should be used to the doze animation. 160 */ 161 private Interpolator mDozeInterpolator = Interpolators.FAST_OUT_SLOW_IN; 162 163 @Inject StatusBarStateControllerImpl( UiEventLogger uiEventLogger, DumpManager dumpManager, InteractionJankMonitor interactionJankMonitor, ShadeExpansionStateManager shadeExpansionStateManager )164 public StatusBarStateControllerImpl( 165 UiEventLogger uiEventLogger, 166 DumpManager dumpManager, 167 InteractionJankMonitor interactionJankMonitor, 168 ShadeExpansionStateManager shadeExpansionStateManager 169 ) { 170 mUiEventLogger = uiEventLogger; 171 mInteractionJankMonitor = interactionJankMonitor; 172 for (int i = 0; i < HISTORY_SIZE; i++) { 173 mHistoricalRecords[i] = new HistoricalState(); 174 } 175 shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged); 176 177 dumpManager.registerDumpable(this); 178 } 179 180 @Override getState()181 public int getState() { 182 return mState; 183 } 184 185 @Override setState(int state, boolean force)186 public boolean setState(int state, boolean force) { 187 if (state > MAX_STATE || state < MIN_STATE) { 188 throw new IllegalArgumentException("Invalid state " + state); 189 } 190 191 // Unless we're explicitly asked to force the state change, don't apply the new state if 192 // it's identical to both the current and upcoming states, since that should not be 193 // necessary. 194 if (!force && state == mState && state == mUpcomingState) { 195 return false; 196 } 197 198 if (state != mUpcomingState) { 199 Log.d(TAG, "setState: requested state " + StatusBarState.toString(state) 200 + "!= upcomingState: " + StatusBarState.toString(mUpcomingState) + ". " 201 + "This usually means the status bar state transition was interrupted before " 202 + "the upcoming state could be applied."); 203 } 204 205 // Record the to-be mState and mLastState 206 recordHistoricalState(state /* newState */, mState /* lastState */, false); 207 208 // b/139259891 209 if (mState == StatusBarState.SHADE && state == StatusBarState.SHADE_LOCKED) { 210 Log.e(TAG, "Invalid state transition: SHADE -> SHADE_LOCKED", new Throwable()); 211 } 212 213 synchronized (mListeners) { 214 String tag = getClass().getSimpleName() + "#setState(" + state + ")"; 215 DejankUtils.startDetectingBlockingIpcs(tag); 216 for (RankedListener rl : new ArrayList<>(mListeners)) { 217 rl.mListener.onStatePreChange(mState, state); 218 } 219 mLastState = mState; 220 mState = state; 221 updateUpcomingState(mState); 222 mUiEventLogger.log(StatusBarStateEvent.fromState(mState)); 223 Trace.instantForTrack(Trace.TRACE_TAG_APP, "UI Events", "StatusBarState " + tag); 224 for (RankedListener rl : new ArrayList<>(mListeners)) { 225 rl.mListener.onStateChanged(mState); 226 } 227 228 for (RankedListener rl : new ArrayList<>(mListeners)) { 229 rl.mListener.onStatePostChange(); 230 } 231 DejankUtils.stopDetectingBlockingIpcs(tag); 232 } 233 234 return true; 235 } 236 237 @Override setUpcomingState(int nextState)238 public void setUpcomingState(int nextState) { 239 recordHistoricalState(nextState /* newState */, mState /* lastState */, true); 240 updateUpcomingState(nextState); 241 242 } 243 updateUpcomingState(int upcomingState)244 private void updateUpcomingState(int upcomingState) { 245 if (mUpcomingState != upcomingState) { 246 mUpcomingState = upcomingState; 247 for (RankedListener rl : new ArrayList<>(mListeners)) { 248 rl.mListener.onUpcomingStateChanged(mUpcomingState); 249 } 250 } 251 } 252 253 @Override getCurrentOrUpcomingState()254 public int getCurrentOrUpcomingState() { 255 return mUpcomingState; 256 } 257 258 @Override isDozing()259 public boolean isDozing() { 260 return mIsDozing; 261 } 262 263 @Override isPulsing()264 public boolean isPulsing() { 265 return mPulsing; 266 } 267 268 @Override getDozeAmount()269 public float getDozeAmount() { 270 return mDozeAmount; 271 } 272 273 @Override isExpanded()274 public boolean isExpanded() { 275 return mIsExpanded; 276 } 277 278 @Override getInterpolatedDozeAmount()279 public float getInterpolatedDozeAmount() { 280 return mDozeInterpolator.getInterpolation(mDozeAmount); 281 } 282 283 @Override setIsDozing(boolean isDozing)284 public boolean setIsDozing(boolean isDozing) { 285 if (mIsDozing == isDozing) { 286 return false; 287 } 288 289 mIsDozing = isDozing; 290 291 synchronized (mListeners) { 292 String tag = getClass().getSimpleName() + "#setIsDozing"; 293 DejankUtils.startDetectingBlockingIpcs(tag); 294 for (RankedListener rl : new ArrayList<>(mListeners)) { 295 rl.mListener.onDozingChanged(isDozing); 296 } 297 DejankUtils.stopDetectingBlockingIpcs(tag); 298 } 299 300 return true; 301 } 302 303 @Override setIsDreaming(boolean isDreaming)304 public boolean setIsDreaming(boolean isDreaming) { 305 if (Log.isLoggable(TAG, Log.DEBUG) || Compile.IS_DEBUG) { 306 Log.d(TAG, "setIsDreaming:" + isDreaming); 307 } 308 if (mIsDreaming == isDreaming) { 309 return false; 310 } 311 312 mIsDreaming = isDreaming; 313 314 synchronized (mListeners) { 315 String tag = getClass().getSimpleName() + "#setIsDreaming"; 316 DejankUtils.startDetectingBlockingIpcs(tag); 317 for (RankedListener rl : new ArrayList<>(mListeners)) { 318 rl.mListener.onDreamingChanged(isDreaming); 319 } 320 DejankUtils.stopDetectingBlockingIpcs(tag); 321 } 322 323 return true; 324 } 325 326 @Override isDreaming()327 public boolean isDreaming() { 328 return mIsDreaming; 329 } 330 331 @Override setAndInstrumentDozeAmount(View view, float dozeAmount, boolean animated)332 public void setAndInstrumentDozeAmount(View view, float dozeAmount, boolean animated) { 333 if (mDarkAnimator != null && mDarkAnimator.isRunning()) { 334 if (animated && mDozeAmountTarget == dozeAmount) { 335 return; 336 } else { 337 mDarkAnimator.cancel(); 338 } 339 } 340 341 // We don't need a new attached view if we already have one. 342 if ((mView == null || !mView.isAttachedToWindow()) 343 && (view != null && view.isAttachedToWindow())) { 344 mView = view; 345 } 346 mDozeAmountTarget = dozeAmount; 347 if (animated) { 348 startDozeAnimation(); 349 } else { 350 setDozeAmountInternal(dozeAmount); 351 } 352 } 353 onShadeExpansionFullyChanged(Boolean isExpanded)354 private void onShadeExpansionFullyChanged(Boolean isExpanded) { 355 if (mIsExpanded != isExpanded) { 356 mIsExpanded = isExpanded; 357 String tag = getClass().getSimpleName() + "#setIsExpanded"; 358 DejankUtils.startDetectingBlockingIpcs(tag); 359 for (RankedListener rl : new ArrayList<>(mListeners)) { 360 rl.mListener.onExpandedChanged(mIsExpanded); 361 } 362 DejankUtils.stopDetectingBlockingIpcs(tag); 363 } 364 } 365 startDozeAnimation()366 private void startDozeAnimation() { 367 if (mDozeAmount == 0f || mDozeAmount == 1f) { 368 mDozeInterpolator = mIsDozing 369 ? Interpolators.FAST_OUT_SLOW_IN 370 : Interpolators.TOUCH_RESPONSE_REVERSE; 371 } 372 if (mDozeAmount == 1f && !mIsDozing) { 373 // Workaround to force relayoutWindow to be called a frame earlier. Otherwise, if 374 // mDozeAmount = 1f, then neither start() nor the first frame of the animation will 375 // cause the scrim opacity to change, which ultimately results in an extra relayout and 376 // causes us to miss a frame. By settings the doze amount to be <1f a frame earlier, 377 // we can batch the relayout with the one in NotificationShadeWindowControllerImpl. 378 setDozeAmountInternal(0.99f); 379 } 380 mDarkAnimator = createDarkAnimator(); 381 } 382 383 @VisibleForTesting createDarkAnimator()384 protected ObjectAnimator createDarkAnimator() { 385 ObjectAnimator darkAnimator = ObjectAnimator.ofFloat( 386 this, SET_DARK_AMOUNT_PROPERTY, mDozeAmountTarget); 387 darkAnimator.setInterpolator(Interpolators.LINEAR); 388 darkAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_WAKEUP); 389 darkAnimator.addListener(new AnimatorListenerAdapter() { 390 @Override 391 public void onAnimationCancel(Animator animation) { 392 cancelInteractionJankMonitor(); 393 } 394 395 @Override 396 public void onAnimationEnd(Animator animation) { 397 endInteractionJankMonitor(); 398 } 399 400 @Override 401 public void onAnimationStart(Animator animation) { 402 beginInteractionJankMonitor(); 403 } 404 }); 405 darkAnimator.start(); 406 return darkAnimator; 407 } 408 setDozeAmountInternal(float dozeAmount)409 private void setDozeAmountInternal(float dozeAmount) { 410 if (Float.compare(dozeAmount, mDozeAmount) == 0) { 411 return; 412 } 413 mDozeAmount = dozeAmount; 414 float interpolatedAmount = mDozeInterpolator.getInterpolation(dozeAmount); 415 synchronized (mListeners) { 416 String tag = getClass().getSimpleName() + "#setDozeAmount"; 417 DejankUtils.startDetectingBlockingIpcs(tag); 418 for (RankedListener rl : new ArrayList<>(mListeners)) { 419 rl.mListener.onDozeAmountChanged(mDozeAmount, interpolatedAmount); 420 } 421 DejankUtils.stopDetectingBlockingIpcs(tag); 422 } 423 } 424 425 /** Returns the id of the currently rendering clock */ getClockId()426 public String getClockId() { 427 if (mView == null) { 428 return KeyguardClockSwitch.MISSING_CLOCK_ID; 429 } 430 431 View clockSwitch = mView.findViewById(R.id.keyguard_clock_container); 432 if (clockSwitch == null) { 433 Log.e(TAG, "Clock container was missing"); 434 return KeyguardClockSwitch.MISSING_CLOCK_ID; 435 } 436 if (!(clockSwitch instanceof KeyguardClockSwitch)) { 437 Log.e(TAG, "Clock container was incorrect type: " + clockSwitch); 438 return KeyguardClockSwitch.MISSING_CLOCK_ID; 439 } 440 441 return ((KeyguardClockSwitch) clockSwitch).getClockId(); 442 } 443 beginInteractionJankMonitor()444 private void beginInteractionJankMonitor() { 445 final boolean shouldPost = 446 (mIsDozing && mDozeAmount == 0) || (!mIsDozing && mDozeAmount == 1); 447 if (mInteractionJankMonitor != null && mView != null && mView.isAttachedToWindow()) { 448 if (shouldPost) { 449 Choreographer.getInstance().postCallback( 450 Choreographer.CALLBACK_ANIMATION, this::beginInteractionJankMonitor, null); 451 } else { 452 Configuration.Builder builder = Configuration.Builder.withView(getCujType(), mView) 453 .setTag(getClockId()) 454 .setDeferMonitorForAnimationStart(false); 455 mInteractionJankMonitor.begin(builder); 456 } 457 } 458 } 459 endInteractionJankMonitor()460 private void endInteractionJankMonitor() { 461 if (mInteractionJankMonitor == null) { 462 return; 463 } 464 mInteractionJankMonitor.end(getCujType()); 465 } 466 cancelInteractionJankMonitor()467 private void cancelInteractionJankMonitor() { 468 if (mInteractionJankMonitor == null) { 469 return; 470 } 471 mInteractionJankMonitor.cancel(getCujType()); 472 } 473 getCujType()474 private int getCujType() { 475 return mIsDozing ? CUJ_LOCKSCREEN_TRANSITION_TO_AOD : CUJ_LOCKSCREEN_TRANSITION_FROM_AOD; 476 } 477 478 @Override goingToFullShade()479 public boolean goingToFullShade() { 480 return mState == StatusBarState.SHADE && mLeaveOpenOnKeyguardHide; 481 } 482 483 @Override setLeaveOpenOnKeyguardHide(boolean leaveOpen)484 public void setLeaveOpenOnKeyguardHide(boolean leaveOpen) { 485 mLeaveOpenOnKeyguardHide = leaveOpen; 486 } 487 488 @Override leaveOpenOnKeyguardHide()489 public boolean leaveOpenOnKeyguardHide() { 490 return mLeaveOpenOnKeyguardHide; 491 } 492 493 @Override fromShadeLocked()494 public boolean fromShadeLocked() { 495 return mLastState == StatusBarState.SHADE_LOCKED; 496 } 497 498 @Override addCallback(@onNull StateListener listener)499 public void addCallback(@NonNull StateListener listener) { 500 synchronized (mListeners) { 501 addListenerInternalLocked(listener, Integer.MAX_VALUE); 502 } 503 } 504 505 /** 506 * Add a listener and a rank based on the priority of this message 507 * @param listener the listener 508 * @param rank the order in which you'd like to be called. Ranked listeners will be 509 * notified before unranked, and we will sort ranked listeners from low to high 510 * 511 * @deprecated This method exists only to solve latent inter-dependencies from refactoring 512 * StatusBarState out of CentralSurfaces.java. Any new listeners should be built not to need 513 * ranking (i.e., they are non-dependent on the order of operations of StatusBarState 514 * listeners). 515 */ 516 @Deprecated 517 @Override addCallback(StateListener listener, @SbStateListenerRank int rank)518 public void addCallback(StateListener listener, @SbStateListenerRank int rank) { 519 synchronized (mListeners) { 520 addListenerInternalLocked(listener, rank); 521 } 522 } 523 524 @GuardedBy("mListeners") addListenerInternalLocked(StateListener listener, int rank)525 private void addListenerInternalLocked(StateListener listener, int rank) { 526 // Protect against double-subscribe 527 for (RankedListener rl : mListeners) { 528 if (rl.mListener.equals(listener)) { 529 return; 530 } 531 } 532 533 RankedListener rl = new SysuiStatusBarStateController.RankedListener(listener, rank); 534 mListeners.add(rl); 535 mListeners.sort(sComparator); 536 } 537 538 539 @Override removeCallback(@onNull StateListener listener)540 public void removeCallback(@NonNull StateListener listener) { 541 synchronized (mListeners) { 542 mListeners.removeIf((it) -> it.mListener.equals(listener)); 543 } 544 } 545 546 @Override setKeyguardRequested(boolean keyguardRequested)547 public void setKeyguardRequested(boolean keyguardRequested) { 548 mKeyguardRequested = keyguardRequested; 549 } 550 551 @Override isKeyguardRequested()552 public boolean isKeyguardRequested() { 553 return mKeyguardRequested; 554 } 555 556 @Override setSystemBarAttributes(@ppearance int appearance, @Behavior int behavior, @InsetsType int requestedVisibleTypes, String packageName)557 public void setSystemBarAttributes(@Appearance int appearance, @Behavior int behavior, 558 @InsetsType int requestedVisibleTypes, String packageName) { 559 boolean isFullscreen = (requestedVisibleTypes & WindowInsets.Type.statusBars()) == 0 560 || (requestedVisibleTypes & WindowInsets.Type.navigationBars()) == 0; 561 if (mIsFullscreen != isFullscreen) { 562 mIsFullscreen = isFullscreen; 563 synchronized (mListeners) { 564 for (RankedListener rl : new ArrayList<>(mListeners)) { 565 rl.mListener.onFullscreenStateChanged(isFullscreen); 566 } 567 } 568 } 569 570 // TODO (b/190543382): Finish the logging logic. 571 // This section can be removed if we don't need to print it on logcat. 572 if (DEBUG_IMMERSIVE_APPS) { 573 boolean dim = (appearance & APPEARANCE_LOW_PROFILE_BARS) != 0; 574 String behaviorName = ViewDebug.flagsToString(InsetsFlags.class, "behavior", behavior); 575 String requestedVisibleTypesString = WindowInsets.Type.toString(requestedVisibleTypes); 576 if (requestedVisibleTypesString.isEmpty()) { 577 requestedVisibleTypesString = "none"; 578 } 579 Log.d(TAG, packageName + " dim=" + dim + " behavior=" + behaviorName 580 + " requested visible types: " + requestedVisibleTypesString); 581 } 582 } 583 584 @Override setPulsing(boolean pulsing)585 public void setPulsing(boolean pulsing) { 586 if (mPulsing != pulsing) { 587 mPulsing = pulsing; 588 synchronized (mListeners) { 589 for (RankedListener rl : new ArrayList<>(mListeners)) { 590 rl.mListener.onPulsingChanged(pulsing); 591 } 592 } 593 } 594 } 595 596 /** 597 * Returns String readable state of status bar from {@link StatusBarState} 598 */ describe(int state)599 public static String describe(int state) { 600 return StatusBarState.toString(state); 601 } 602 603 @Override dump(PrintWriter pw, String[] args)604 public void dump(PrintWriter pw, String[] args) { 605 pw.println("StatusBarStateController: "); 606 pw.println(" mState=" + mState + " (" + describe(mState) + ")"); 607 pw.println(" mLastState=" + mLastState + " (" + describe(mLastState) + ")"); 608 pw.println(" mLeaveOpenOnKeyguardHide=" + mLeaveOpenOnKeyguardHide); 609 pw.println(" mKeyguardRequested=" + mKeyguardRequested); 610 pw.println(" mIsDozing=" + mIsDozing); 611 pw.println(" mIsDreaming=" + mIsDreaming); 612 pw.println(" mListeners{" + mListeners.size() + "}="); 613 for (RankedListener rl : mListeners) { 614 pw.println(" " + rl.mListener); 615 } 616 pw.println(" Historical states:"); 617 // Ignore records without a timestamp 618 int size = 0; 619 for (int i = 0; i < HISTORY_SIZE; i++) { 620 if (mHistoricalRecords[i].mTimestamp != 0) size++; 621 } 622 for (int i = mHistoryIndex + HISTORY_SIZE; 623 i >= mHistoryIndex + HISTORY_SIZE - size + 1; i--) { 624 pw.println(" (" + (mHistoryIndex + HISTORY_SIZE - i + 1) + ")" 625 + mHistoricalRecords[i & (HISTORY_SIZE - 1)]); 626 } 627 } 628 recordHistoricalState(int newState, int lastState, boolean upcoming)629 private void recordHistoricalState(int newState, int lastState, boolean upcoming) { 630 Trace.traceCounter(Trace.TRACE_TAG_APP, "statusBarState", newState); 631 mHistoryIndex = (mHistoryIndex + 1) % HISTORY_SIZE; 632 HistoricalState state = mHistoricalRecords[mHistoryIndex]; 633 state.mNewState = newState; 634 state.mLastState = lastState; 635 state.mTimestamp = System.currentTimeMillis(); 636 state.mUpcoming = upcoming; 637 } 638 639 /** 640 * For keeping track of our previous state to help with debugging 641 */ 642 private static class HistoricalState { 643 int mNewState; 644 int mLastState; 645 long mTimestamp; 646 boolean mUpcoming; 647 648 @Override toString()649 public String toString() { 650 if (mTimestamp != 0) { 651 StringBuilder sb = new StringBuilder(); 652 if (mUpcoming) { 653 sb.append("upcoming-"); 654 } 655 sb.append("newState=").append(mNewState) 656 .append("(").append(describe(mNewState)).append(")"); 657 sb.append(" lastState=").append(mLastState).append("(").append(describe(mLastState)) 658 .append(")"); 659 sb.append(" timestamp=") 660 .append(DateFormat.format("MM-dd HH:mm:ss", mTimestamp)); 661 662 return sb.toString(); 663 } 664 return "Empty " + getClass().getSimpleName(); 665 } 666 } 667 } 668