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