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