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