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