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