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 android.animation.ObjectAnimator;
20 import android.animation.ValueAnimator;
21 import android.text.format.DateFormat;
22 import android.util.FloatProperty;
23 import android.util.Log;
24 import android.view.animation.Interpolator;
25 
26 import com.android.internal.annotations.GuardedBy;
27 import com.android.internal.logging.UiEventLogger;
28 import com.android.systemui.DejankUtils;
29 import com.android.systemui.Dumpable;
30 import com.android.systemui.Interpolators;
31 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
32 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
33 import com.android.systemui.statusbar.policy.CallbackController;
34 
35 import java.io.FileDescriptor;
36 import java.io.PrintWriter;
37 import java.util.ArrayList;
38 import java.util.Comparator;
39 
40 import javax.inject.Inject;
41 import javax.inject.Singleton;
42 
43 /**
44  * Tracks and reports on {@link StatusBarState}.
45  */
46 @Singleton
47 public class StatusBarStateControllerImpl implements SysuiStatusBarStateController,
48         CallbackController<StateListener>, Dumpable {
49     private static final String TAG = "SbStateController";
50     // Must be a power of 2
51     private static final int HISTORY_SIZE = 32;
52 
53     private static final int MAX_STATE = StatusBarState.FULLSCREEN_USER_SWITCHER;
54     private static final int MIN_STATE = StatusBarState.SHADE;
55 
56     private static final Comparator<RankedListener> sComparator =
57             Comparator.comparingInt(o -> o.mRank);
58     private static final FloatProperty<StatusBarStateControllerImpl> SET_DARK_AMOUNT_PROPERTY =
59             new FloatProperty<StatusBarStateControllerImpl>("mDozeAmount") {
60 
61                 @Override
62                 public void setValue(StatusBarStateControllerImpl object, float value) {
63                     object.setDozeAmountInternal(value);
64                 }
65 
66                 @Override
67                 public Float get(StatusBarStateControllerImpl object) {
68                     return object.mDozeAmount;
69                 }
70             };
71 
72     private final ArrayList<RankedListener> mListeners = new ArrayList<>();
73     private final UiEventLogger mUiEventLogger;
74     private int mState;
75     private int mLastState;
76     private boolean mLeaveOpenOnKeyguardHide;
77     private boolean mKeyguardRequested;
78 
79     // Record the HISTORY_SIZE most recent states
80     private int mHistoryIndex = 0;
81     private HistoricalState[] mHistoricalRecords = new HistoricalState[HISTORY_SIZE];
82 
83     /**
84      * If any of the system bars is hidden.
85      */
86     private boolean mIsFullscreen = false;
87 
88     /**
89      * If the navigation bar can stay hidden when the display gets tapped.
90      */
91     private boolean mIsImmersive = false;
92 
93     /**
94      * If the device is currently pulsing (AOD2).
95      */
96     private boolean mPulsing;
97 
98     /**
99      * If the device is currently dozing or not.
100      */
101     private boolean mIsDozing;
102 
103     /**
104      * Current {@link #mDozeAmount} animator.
105      */
106     private ValueAnimator mDarkAnimator;
107 
108     /**
109      * Current doze amount in this frame.
110      */
111     private float mDozeAmount;
112 
113     /**
114      * Where the animator will stop.
115      */
116     private float mDozeAmountTarget;
117 
118     /**
119      * The type of interpolator that should be used to the doze animation.
120      */
121     private Interpolator mDozeInterpolator = Interpolators.FAST_OUT_SLOW_IN;
122 
123     @Inject
StatusBarStateControllerImpl(UiEventLogger uiEventLogger)124     public StatusBarStateControllerImpl(UiEventLogger uiEventLogger) {
125         mUiEventLogger = uiEventLogger;
126         for (int i = 0; i < HISTORY_SIZE; i++) {
127             mHistoricalRecords[i] = new HistoricalState();
128         }
129     }
130 
131     @Override
getState()132     public int getState() {
133         return mState;
134     }
135 
136     @Override
setState(int state)137     public boolean setState(int state) {
138         if (state > MAX_STATE || state < MIN_STATE) {
139             throw new IllegalArgumentException("Invalid state " + state);
140         }
141         if (state == mState) {
142             return false;
143         }
144 
145         // Record the to-be mState and mLastState
146         recordHistoricalState(state, mState);
147 
148         // b/139259891
149         if (mState == StatusBarState.SHADE && state == StatusBarState.SHADE_LOCKED) {
150             Log.e(TAG, "Invalid state transition: SHADE -> SHADE_LOCKED", new Throwable());
151         }
152 
153         synchronized (mListeners) {
154             String tag = getClass().getSimpleName() + "#setState(" + state + ")";
155             DejankUtils.startDetectingBlockingIpcs(tag);
156             for (RankedListener rl : new ArrayList<>(mListeners)) {
157                 rl.mListener.onStatePreChange(mState, state);
158             }
159             mLastState = mState;
160             mState = state;
161             mUiEventLogger.log(StatusBarStateEvent.fromState(mState));
162             for (RankedListener rl : new ArrayList<>(mListeners)) {
163                 rl.mListener.onStateChanged(mState);
164             }
165 
166             for (RankedListener rl : new ArrayList<>(mListeners)) {
167                 rl.mListener.onStatePostChange();
168             }
169             DejankUtils.stopDetectingBlockingIpcs(tag);
170         }
171 
172         return true;
173     }
174 
175     @Override
isDozing()176     public boolean isDozing() {
177         return mIsDozing;
178     }
179 
180     @Override
isPulsing()181     public boolean isPulsing() {
182         return mPulsing;
183     }
184 
185     @Override
getDozeAmount()186     public float getDozeAmount() {
187         return mDozeAmount;
188     }
189 
190     @Override
getInterpolatedDozeAmount()191     public float getInterpolatedDozeAmount() {
192         return mDozeInterpolator.getInterpolation(mDozeAmount);
193     }
194 
195     @Override
setIsDozing(boolean isDozing)196     public boolean setIsDozing(boolean isDozing) {
197         if (mIsDozing == isDozing) {
198             return false;
199         }
200 
201         mIsDozing = isDozing;
202 
203         synchronized (mListeners) {
204             String tag = getClass().getSimpleName() + "#setIsDozing";
205             DejankUtils.startDetectingBlockingIpcs(tag);
206             for (RankedListener rl : new ArrayList<>(mListeners)) {
207                 rl.mListener.onDozingChanged(isDozing);
208             }
209             DejankUtils.stopDetectingBlockingIpcs(tag);
210         }
211 
212         return true;
213     }
214 
215     @Override
setDozeAmount(float dozeAmount, boolean animated)216     public void setDozeAmount(float dozeAmount, boolean animated) {
217         if (mDarkAnimator != null && mDarkAnimator.isRunning()) {
218             if (animated && mDozeAmountTarget == dozeAmount) {
219                 return;
220             } else {
221                 mDarkAnimator.cancel();
222             }
223         }
224 
225         mDozeAmountTarget = dozeAmount;
226         if (animated) {
227             startDozeAnimation();
228         } else {
229             setDozeAmountInternal(dozeAmount);
230         }
231     }
232 
startDozeAnimation()233     private void startDozeAnimation() {
234         if (mDozeAmount == 0f || mDozeAmount == 1f) {
235             mDozeInterpolator = mIsDozing
236                     ? Interpolators.FAST_OUT_SLOW_IN
237                     : Interpolators.TOUCH_RESPONSE_REVERSE;
238         }
239         mDarkAnimator = ObjectAnimator.ofFloat(this, SET_DARK_AMOUNT_PROPERTY, mDozeAmountTarget);
240         mDarkAnimator.setInterpolator(Interpolators.LINEAR);
241         mDarkAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_WAKEUP);
242         mDarkAnimator.start();
243     }
244 
setDozeAmountInternal(float dozeAmount)245     private void setDozeAmountInternal(float dozeAmount) {
246         mDozeAmount = dozeAmount;
247         float interpolatedAmount = mDozeInterpolator.getInterpolation(dozeAmount);
248         synchronized (mListeners) {
249             String tag = getClass().getSimpleName() + "#setDozeAmount";
250             DejankUtils.startDetectingBlockingIpcs(tag);
251             for (RankedListener rl : new ArrayList<>(mListeners)) {
252                 rl.mListener.onDozeAmountChanged(mDozeAmount, interpolatedAmount);
253             }
254             DejankUtils.stopDetectingBlockingIpcs(tag);
255         }
256     }
257 
258     @Override
goingToFullShade()259     public boolean goingToFullShade() {
260         return mState == StatusBarState.SHADE && mLeaveOpenOnKeyguardHide;
261     }
262 
263     @Override
setLeaveOpenOnKeyguardHide(boolean leaveOpen)264     public void setLeaveOpenOnKeyguardHide(boolean leaveOpen) {
265         mLeaveOpenOnKeyguardHide = leaveOpen;
266     }
267 
268     @Override
leaveOpenOnKeyguardHide()269     public boolean leaveOpenOnKeyguardHide() {
270         return mLeaveOpenOnKeyguardHide;
271     }
272 
273     @Override
fromShadeLocked()274     public boolean fromShadeLocked() {
275         return mLastState == StatusBarState.SHADE_LOCKED;
276     }
277 
278     @Override
addCallback(StateListener listener)279     public void addCallback(StateListener listener) {
280         synchronized (mListeners) {
281             addListenerInternalLocked(listener, Integer.MAX_VALUE);
282         }
283     }
284 
285     /**
286      * Add a listener and a rank based on the priority of this message
287      * @param listener the listener
288      * @param rank the order in which you'd like to be called. Ranked listeners will be
289      * notified before unranked, and we will sort ranked listeners from low to high
290      *
291      * @deprecated This method exists only to solve latent inter-dependencies from refactoring
292      * StatusBarState out of StatusBar.java. Any new listeners should be built not to need ranking
293      * (i.e., they are non-dependent on the order of operations of StatusBarState listeners).
294      */
295     @Deprecated
296     @Override
addCallback(StateListener listener, @SbStateListenerRank int rank)297     public void addCallback(StateListener listener, @SbStateListenerRank int rank) {
298         synchronized (mListeners) {
299             addListenerInternalLocked(listener, rank);
300         }
301     }
302 
303     @GuardedBy("mListeners")
addListenerInternalLocked(StateListener listener, int rank)304     private void addListenerInternalLocked(StateListener listener, int rank) {
305         // Protect against double-subscribe
306         for (RankedListener rl : mListeners) {
307             if (rl.mListener.equals(listener)) {
308                 return;
309             }
310         }
311 
312         RankedListener rl = new SysuiStatusBarStateController.RankedListener(listener, rank);
313         mListeners.add(rl);
314         mListeners.sort(sComparator);
315     }
316 
317 
318     @Override
removeCallback(StateListener listener)319     public void removeCallback(StateListener listener) {
320         synchronized (mListeners) {
321             mListeners.removeIf((it) -> it.mListener.equals(listener));
322         }
323     }
324 
325     @Override
setKeyguardRequested(boolean keyguardRequested)326     public void setKeyguardRequested(boolean keyguardRequested) {
327         mKeyguardRequested = keyguardRequested;
328     }
329 
330     @Override
isKeyguardRequested()331     public boolean isKeyguardRequested() {
332         return mKeyguardRequested;
333     }
334 
335     @Override
setFullscreenState(boolean isFullscreen, boolean isImmersive)336     public void setFullscreenState(boolean isFullscreen, boolean isImmersive) {
337         if (mIsFullscreen != isFullscreen || mIsImmersive != isImmersive) {
338             mIsFullscreen = isFullscreen;
339             mIsImmersive = isImmersive;
340             synchronized (mListeners) {
341                 for (RankedListener rl : new ArrayList<>(mListeners)) {
342                     rl.mListener.onFullscreenStateChanged(isFullscreen, isImmersive);
343                 }
344             }
345         }
346     }
347 
348     @Override
setPulsing(boolean pulsing)349     public void setPulsing(boolean pulsing) {
350         if (mPulsing != pulsing) {
351             mPulsing = pulsing;
352             synchronized (mListeners) {
353                 for (RankedListener rl : new ArrayList<>(mListeners)) {
354                     rl.mListener.onPulsingChanged(pulsing);
355                 }
356             }
357         }
358     }
359 
360     /**
361      * Returns String readable state of status bar from {@link StatusBarState}
362      */
describe(int state)363     public static String describe(int state) {
364         return StatusBarState.toShortString(state);
365     }
366 
367     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)368     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
369         pw.println("StatusBarStateController: ");
370         pw.println(" mState=" + mState + " (" + describe(mState) + ")");
371         pw.println(" mLastState=" + mLastState + " (" + describe(mLastState) + ")");
372         pw.println(" mLeaveOpenOnKeyguardHide=" + mLeaveOpenOnKeyguardHide);
373         pw.println(" mKeyguardRequested=" + mKeyguardRequested);
374         pw.println(" mIsDozing=" + mIsDozing);
375         pw.println(" Historical states:");
376         // Ignore records without a timestamp
377         int size = 0;
378         for (int i = 0; i < HISTORY_SIZE; i++) {
379             if (mHistoricalRecords[i].mTimestamp != 0) size++;
380         }
381         for (int i = mHistoryIndex + HISTORY_SIZE;
382                 i >= mHistoryIndex + HISTORY_SIZE - size + 1; i--) {
383             pw.println("  (" + (mHistoryIndex + HISTORY_SIZE - i + 1) + ")"
384                     + mHistoricalRecords[i & (HISTORY_SIZE - 1)]);
385         }
386     }
387 
recordHistoricalState(int currentState, int lastState)388     private void recordHistoricalState(int currentState, int lastState) {
389         mHistoryIndex = (mHistoryIndex + 1) % HISTORY_SIZE;
390         HistoricalState state = mHistoricalRecords[mHistoryIndex];
391         state.mState = currentState;
392         state.mLastState = lastState;
393         state.mTimestamp = System.currentTimeMillis();
394     }
395 
396     /**
397      * For keeping track of our previous state to help with debugging
398      */
399     private static class HistoricalState {
400         int mState;
401         int mLastState;
402         long mTimestamp;
403 
404         @Override
toString()405         public String toString() {
406             if (mTimestamp != 0) {
407                 StringBuilder sb = new StringBuilder();
408                 sb.append("state=").append(mState)
409                         .append(" (").append(describe(mState)).append(")");
410                 sb.append("lastState=").append(mLastState).append(" (").append(describe(mLastState))
411                         .append(")");
412                 sb.append("timestamp=")
413                         .append(DateFormat.format("MM-dd HH:mm:ss", mTimestamp));
414 
415                 return sb.toString();
416             }
417             return "Empty " + getClass().getSimpleName();
418         }
419     }
420 }
421