1 /* 2 * Copyright (C) 2023 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.qs; 18 19 import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS; 20 21 import static com.android.systemui.media.dagger.MediaModule.QS_PANEL; 22 import static com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL; 23 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; 24 import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED; 25 26 import android.animation.Animator; 27 import android.animation.AnimatorListenerAdapter; 28 import android.content.Context; 29 import android.content.res.Configuration; 30 import android.content.res.Resources; 31 import android.graphics.Rect; 32 import android.os.Bundle; 33 import android.util.IndentingPrintWriter; 34 import android.util.Log; 35 import android.view.View; 36 import android.view.ViewGroup; 37 38 import androidx.annotation.FloatRange; 39 import androidx.annotation.Nullable; 40 import androidx.annotation.VisibleForTesting; 41 import androidx.compose.ui.platform.ComposeView; 42 import androidx.lifecycle.Lifecycle; 43 import androidx.lifecycle.LifecycleOwner; 44 import androidx.lifecycle.LifecycleRegistry; 45 46 import com.android.app.animation.Interpolators; 47 import com.android.keyguard.BouncerPanelExpansionCalculator; 48 import com.android.systemui.Dumpable; 49 import com.android.systemui.animation.ShadeInterpolation; 50 import com.android.systemui.dump.DumpManager; 51 import com.android.systemui.media.controls.ui.view.MediaHost; 52 import com.android.systemui.plugins.qs.QS; 53 import com.android.systemui.plugins.qs.QSContainerController; 54 import com.android.systemui.plugins.statusbar.StatusBarStateController; 55 import com.android.systemui.qs.customize.QSCustomizerController; 56 import com.android.systemui.qs.dagger.QSComponent; 57 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel; 58 import com.android.systemui.qs.logging.QSLogger; 59 import com.android.systemui.res.R; 60 import com.android.systemui.scene.shared.flag.SceneContainerFlag; 61 import com.android.systemui.settings.brightness.MirrorController; 62 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; 63 import com.android.systemui.statusbar.CommandQueue; 64 import com.android.systemui.statusbar.StatusBarState; 65 import com.android.systemui.statusbar.SysuiStatusBarStateController; 66 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger; 67 import com.android.systemui.statusbar.notification.stack.StackStateAnimator; 68 import com.android.systemui.statusbar.phone.KeyguardBypassController; 69 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler; 70 import com.android.systemui.util.Utils; 71 72 import dalvik.annotation.optimization.NeverCompile; 73 74 import java.io.PrintWriter; 75 import java.util.Arrays; 76 import java.util.function.Consumer; 77 78 import javax.inject.Inject; 79 import javax.inject.Named; 80 81 public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateController.StateListener, 82 Dumpable { 83 private static final String TAG = "QS"; 84 private static final boolean DEBUG = false; 85 private static final String EXTRA_EXPANDED = "expanded"; 86 private static final String EXTRA_LISTENING = "listening"; 87 private static final String EXTRA_VISIBLE = "visible"; 88 89 private final Rect mQsBounds = new Rect(); 90 private final SysuiStatusBarStateController mStatusBarStateController; 91 private final KeyguardBypassController mBypassController; 92 private boolean mQsExpanded; 93 private boolean mHeaderAnimating; 94 private boolean mStackScrollerOverscrolling; 95 96 private QSAnimator mQSAnimator; 97 @Nullable 98 private HeightListener mPanelView; 99 private QSSquishinessController mQSSquishinessController; 100 protected QuickStatusBarHeader mHeader; 101 protected NonInterceptingScrollView mQSPanelScrollView; 102 private boolean mListening; 103 private QSContainerImpl mContainer; 104 private int mLayoutDirection; 105 private QSFooter mFooter; 106 private float mLastQSExpansion = -1; 107 private float mLastPanelFraction; 108 private float mSquishinessFraction = 1; 109 private boolean mQsDisabled; 110 private int[] mLocationTemp = new int[2]; 111 112 private final RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler; 113 private final MediaHost mQsMediaHost; 114 private final MediaHost mQqsMediaHost; 115 private final QSDisableFlagsLogger mQsDisableFlagsLogger; 116 private final LargeScreenShadeInterpolator mLargeScreenShadeInterpolator; 117 private final QSLogger mLogger; 118 private final FooterActionsController mFooterActionsController; 119 private final FooterActionsViewModel.Factory mFooterActionsViewModelFactory; 120 private final ListeningAndVisibilityLifecycleOwner mListeningAndVisibilityLifecycleOwner; 121 private boolean mShowCollapsedOnKeyguard; 122 private boolean mLastKeyguardAndExpanded; 123 /** 124 * The last received state from the controller. This should not be used directly to check if 125 * we're on keyguard but use {@link #isKeyguardState()} instead since that is more accurate 126 * during state transitions which often call into us. 127 */ 128 private int mStatusBarState = -1; 129 private QSContainerImplController mQSContainerImplController; 130 private int[] mTmpLocation = new int[2]; 131 private int mLastViewHeight; 132 private float mLastHeaderTranslation; 133 private QSPanelController mQSPanelController; 134 private QuickQSPanelController mQuickQSPanelController; 135 private QSCustomizerController mQSCustomizerController; 136 private FooterActionsViewModel mQSFooterActionsViewModel; 137 @Nullable 138 private ScrollListener mScrollListener; 139 /** 140 * When true, QS will translate from outside the screen. It will be clipped with parallax 141 * otherwise. 142 */ 143 private boolean mInSplitShade; 144 145 /** 146 * Are we currently transitioning from lockscreen to the full shade? 147 */ 148 private boolean mTransitioningToFullShade; 149 150 private final DumpManager mDumpManager; 151 152 /** 153 * Progress of pull down from the center of the lock screen. 154 * @see com.android.systemui.statusbar.LockscreenShadeTransitionController 155 */ 156 private float mLockscreenToShadeProgress; 157 158 private boolean mOverScrolling; 159 160 // Whether QQS or QS is visible. When in lockscreen, this is true if and only if QQS or QS is 161 // visible; 162 private boolean mQsVisible; 163 164 private boolean mIsSmallScreen; 165 166 /** Should the squishiness fraction be updated on the media host. */ 167 private boolean mShouldUpdateMediaSquishiness; 168 169 private CommandQueue mCommandQueue; 170 171 private View mRootView; 172 @Nullable 173 private ComposeView mFooterActionsView; 174 175 @Inject QSImpl(RemoteInputQuickSettingsDisabler remoteInputQsDisabler, SysuiStatusBarStateController statusBarStateController, CommandQueue commandQueue, @Named(QS_PANEL) MediaHost qsMediaHost, @Named(QUICK_QS_PANEL) MediaHost qqsMediaHost, KeyguardBypassController keyguardBypassController, QSDisableFlagsLogger qsDisableFlagsLogger, DumpManager dumpManager, QSLogger qsLogger, FooterActionsController footerActionsController, FooterActionsViewModel.Factory footerActionsViewModelFactory, LargeScreenShadeInterpolator largeScreenShadeInterpolator)176 public QSImpl(RemoteInputQuickSettingsDisabler remoteInputQsDisabler, 177 SysuiStatusBarStateController statusBarStateController, CommandQueue commandQueue, 178 @Named(QS_PANEL) MediaHost qsMediaHost, 179 @Named(QUICK_QS_PANEL) MediaHost qqsMediaHost, 180 KeyguardBypassController keyguardBypassController, 181 QSDisableFlagsLogger qsDisableFlagsLogger, 182 DumpManager dumpManager, QSLogger qsLogger, 183 FooterActionsController footerActionsController, 184 FooterActionsViewModel.Factory footerActionsViewModelFactory, 185 LargeScreenShadeInterpolator largeScreenShadeInterpolator) { 186 mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler; 187 mQsMediaHost = qsMediaHost; 188 mQqsMediaHost = qqsMediaHost; 189 mQsDisableFlagsLogger = qsDisableFlagsLogger; 190 mLogger = qsLogger; 191 mLargeScreenShadeInterpolator = largeScreenShadeInterpolator; 192 mCommandQueue = commandQueue; 193 mBypassController = keyguardBypassController; 194 mStatusBarStateController = statusBarStateController; 195 mDumpManager = dumpManager; 196 mFooterActionsController = footerActionsController; 197 mFooterActionsViewModelFactory = footerActionsViewModelFactory; 198 mListeningAndVisibilityLifecycleOwner = new ListeningAndVisibilityLifecycleOwner(); 199 if (SceneContainerFlag.isEnabled()) { 200 mStatusBarState = StatusBarState.SHADE; 201 } 202 } 203 204 /** 205 * This method will set up all the necessary fields. Methods from the implemented interfaces 206 * should not be called before this method returns. 207 */ onComponentCreated(QSComponent qsComponent, @Nullable Bundle savedInstanceState)208 public void onComponentCreated(QSComponent qsComponent, @Nullable Bundle savedInstanceState) { 209 mRootView = qsComponent.getRootView(); 210 211 mQSPanelController = qsComponent.getQSPanelController(); 212 mQuickQSPanelController = qsComponent.getQuickQSPanelController(); 213 214 mQSPanelController.init(); 215 mQuickQSPanelController.init(); 216 217 if (!SceneContainerFlag.isEnabled()) { 218 mQSFooterActionsViewModel = mFooterActionsViewModelFactory 219 .create(mListeningAndVisibilityLifecycleOwner); 220 bindFooterActionsView(mRootView); 221 mFooterActionsController.init(); 222 } else { 223 View footerView = mRootView.findViewById(R.id.qs_footer_actions); 224 if (footerView != null) { 225 ((ViewGroup) footerView.getParent()).removeView(footerView); 226 } 227 } 228 229 mQSPanelScrollView = mRootView.findViewById(R.id.expanded_qs_scroll_view); 230 mQSPanelScrollView.addOnLayoutChangeListener( 231 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { 232 updateQsBounds(); 233 }); 234 mQSPanelScrollView.setOnScrollChangeListener( 235 (v, scrollX, scrollY, oldScrollX, oldScrollY) -> { 236 // Lazily update animators whenever the scrolling changes 237 mQSAnimator.requestAnimatorUpdate(); 238 if (mScrollListener != null) { 239 mScrollListener.onQsPanelScrollChanged(scrollY); 240 } 241 }); 242 mQSPanelScrollView.setScrollingEnabled(!SceneContainerFlag.isEnabled()); 243 mHeader = mRootView.findViewById(R.id.header); 244 mFooter = qsComponent.getQSFooter(); 245 246 mQSContainerImplController = qsComponent.getQSContainerImplController(); 247 mQSContainerImplController.init(); 248 mContainer = mQSContainerImplController.getView(); 249 mDumpManager.registerDumpable(mContainer.getClass().getSimpleName(), mContainer); 250 251 mQSAnimator = qsComponent.getQSAnimator(); 252 mQSSquishinessController = qsComponent.getQSSquishinessController(); 253 254 mQSCustomizerController = qsComponent.getQSCustomizerController(); 255 mQSCustomizerController.init(); 256 mQSCustomizerController.setQs(this); 257 if (savedInstanceState != null) { 258 setQsVisible(savedInstanceState.getBoolean(EXTRA_VISIBLE)); 259 setExpanded(savedInstanceState.getBoolean(EXTRA_EXPANDED)); 260 setListening(savedInstanceState.getBoolean(EXTRA_LISTENING)); 261 setEditLocation(mRootView); 262 mQSCustomizerController.restoreInstanceState(savedInstanceState); 263 if (mQsExpanded) { 264 mQSPanelController.getTileLayout().restoreInstanceState(savedInstanceState); 265 } 266 } 267 mStatusBarStateController.addCallback(this); 268 onStateChanged(mStatusBarStateController.getState()); 269 mRootView.addOnLayoutChangeListener( 270 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { 271 boolean sizeChanged = (oldTop - oldBottom) != (top - bottom); 272 if (sizeChanged) { 273 setQsExpansion(mLastQSExpansion, mLastPanelFraction, 274 mLastHeaderTranslation, mSquishinessFraction); 275 } 276 }); 277 mQSPanelController.setUsingHorizontalLayoutChangeListener( 278 () -> { 279 // The hostview may be faded out in the horizontal layout. Let's make sure to 280 // reset the alpha when switching layouts. This is fine since the animator will 281 // update the alpha if it's not supposed to be 1.0f 282 mQSPanelController.getMediaHost().getHostView().setAlpha(1.0f); 283 mQSAnimator.requestAnimatorUpdate(); 284 }); 285 286 // This will immediately call disable, so it needs to be added after setting up the fields. 287 mCommandQueue.addCallback(this); 288 } 289 bindFooterActionsView(View root)290 private void bindFooterActionsView(View root) { 291 mFooterActionsView = root.findViewById(R.id.qs_footer_actions); 292 QSUtils.setFooterActionsViewContent(mFooterActionsView, 293 mQSFooterActionsViewModel, mListeningAndVisibilityLifecycleOwner); 294 } 295 296 @Override setScrollListener(ScrollListener listener)297 public void setScrollListener(ScrollListener listener) { 298 mScrollListener = listener; 299 } 300 onCreate(Bundle savedInstanceState)301 public void onCreate(Bundle savedInstanceState) { 302 mDumpManager.registerDumpable(getClass().getSimpleName(), this); 303 } 304 onDestroy()305 public void onDestroy() { 306 mCommandQueue.removeCallback(this); 307 mStatusBarStateController.removeCallback(this); 308 mQSPanelController.destroy(); 309 mQuickQSPanelController.destroy(); 310 if (mListening) { 311 setListening(false); 312 } 313 if (mQSCustomizerController != null) { 314 mQSCustomizerController.setQs(null); 315 mQSCustomizerController.setContainerController(null); 316 } 317 mScrollListener = null; 318 if (mContainer != null) { 319 mDumpManager.unregisterDumpable(mContainer.getClass().getSimpleName()); 320 } 321 mDumpManager.unregisterDumpable(getClass().getSimpleName()); 322 mListeningAndVisibilityLifecycleOwner.destroy(); 323 ViewGroup parent = ((ViewGroup) getView().getParent()); 324 if (parent != null) { 325 parent.removeView(getView()); 326 } 327 } 328 onSaveInstanceState(Bundle outState)329 public void onSaveInstanceState(Bundle outState) { 330 outState.putBoolean(EXTRA_EXPANDED, mQsExpanded); 331 outState.putBoolean(EXTRA_LISTENING, mListening); 332 outState.putBoolean(EXTRA_VISIBLE, mQsVisible); 333 if (mQSCustomizerController != null) { 334 mQSCustomizerController.saveInstanceState(outState); 335 } 336 if (mQsExpanded) { 337 mQSPanelController.getTileLayout().saveInstanceState(outState); 338 } 339 } 340 341 @VisibleForTesting isListening()342 boolean isListening() { 343 return mListening; 344 } 345 346 @VisibleForTesting isExpanded()347 boolean isExpanded() { 348 return mQsExpanded; 349 } 350 351 @VisibleForTesting isQsVisible()352 boolean isQsVisible() { 353 return mQsVisible; 354 } 355 356 @Override getHeader()357 public View getHeader() { 358 return mHeader; 359 } 360 361 @Override setHasNotifications(boolean hasNotifications)362 public void setHasNotifications(boolean hasNotifications) { 363 } 364 365 @Override setPanelView(HeightListener panelView)366 public void setPanelView(HeightListener panelView) { 367 mPanelView = panelView; 368 } 369 onConfigurationChanged(Configuration newConfig)370 public void onConfigurationChanged(Configuration newConfig) { 371 setEditLocation(getView()); 372 if (newConfig.getLayoutDirection() != mLayoutDirection) { 373 mLayoutDirection = newConfig.getLayoutDirection(); 374 if (mQSAnimator != null) { 375 mQSAnimator.onRtlChanged(); 376 } 377 } 378 updateQsState(); 379 } 380 381 @Override setFancyClipping(int leftInset, int top, int rightInset, int bottom, int cornerRadius, boolean visible, boolean fullWidth)382 public void setFancyClipping(int leftInset, int top, int rightInset, int bottom, 383 int cornerRadius, boolean visible, boolean fullWidth) { 384 if (getView() instanceof QSContainerImpl) { 385 ((QSContainerImpl) getView()).setFancyClipping(leftInset, top, rightInset, bottom, 386 cornerRadius, visible, fullWidth); 387 } 388 } 389 390 @Override isFullyCollapsed()391 public boolean isFullyCollapsed() { 392 return mLastQSExpansion == 0.0f || mLastQSExpansion == -1; 393 } 394 395 @Override setCollapsedMediaVisibilityChangedListener(Consumer<Boolean> listener)396 public void setCollapsedMediaVisibilityChangedListener(Consumer<Boolean> listener) { 397 mQuickQSPanelController.setMediaVisibilityChangedListener(listener); 398 } 399 setEditLocation(View view)400 private void setEditLocation(View view) { 401 View edit = view.findViewById(android.R.id.edit); 402 int[] loc = edit.getLocationOnScreen(); 403 int x = loc[0] + edit.getWidth() / 2; 404 int y = loc[1] + edit.getHeight() / 2; 405 mQSCustomizerController.setEditLocation(x, y); 406 } 407 408 @Override setContainerController(QSContainerController controller)409 public void setContainerController(QSContainerController controller) { 410 mQSCustomizerController.setContainerController(controller); 411 } 412 413 @Override isCustomizing()414 public boolean isCustomizing() { 415 return mQSCustomizerController.isCustomizing(); 416 } 417 418 @Override disable(int displayId, int state1, int state2, boolean animate)419 public void disable(int displayId, int state1, int state2, boolean animate) { 420 if (displayId != getContext().getDisplayId()) { 421 return; 422 } 423 int state2BeforeAdjustment = state2; 424 state2 = mRemoteInputQuickSettingsDisabler.adjustDisableFlags(state2); 425 426 mQsDisableFlagsLogger.logDisableFlagChange( 427 /* new= */ new DisableFlagsLogger.DisableState(state1, state2BeforeAdjustment), 428 /* newAfterLocalModification= */ new DisableFlagsLogger.DisableState(state1, state2) 429 ); 430 431 final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0; 432 if (disabled == mQsDisabled) return; 433 mQsDisabled = disabled; 434 mContainer.disable(state1, state2, animate); 435 mHeader.disable(state1, state2, animate); 436 mFooter.disable(state1, state2, animate); 437 updateQsState(); 438 } 439 updateQsState()440 private void updateQsState() { 441 final boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling 442 || mHeaderAnimating; 443 mQSPanelController.setExpanded(mQsExpanded); 444 boolean keyguardShowing = isKeyguardState(); 445 mHeader.setVisibility((mQsExpanded || !keyguardShowing || mHeaderAnimating 446 || mShowCollapsedOnKeyguard) 447 ? View.VISIBLE 448 : View.INVISIBLE); 449 mHeader.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard) 450 || (mQsExpanded && !mStackScrollerOverscrolling), mQuickQSPanelController); 451 boolean qsPanelVisible = !mQsDisabled && expandVisually; 452 boolean footerVisible = qsPanelVisible && (mQsExpanded || !keyguardShowing 453 || mHeaderAnimating || mShowCollapsedOnKeyguard); 454 mFooter.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE); 455 if (mFooterActionsView != null) { 456 mFooterActionsView.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE); 457 } 458 mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard) 459 || (mQsExpanded && !mStackScrollerOverscrolling)); 460 mQSPanelController.setVisibility(qsPanelVisible ? View.VISIBLE : View.INVISIBLE); 461 if (DEBUG) { 462 Log.d(TAG, "Footer: " + footerVisible + ", QS Panel: " + qsPanelVisible); 463 } 464 } 465 466 @VisibleForTesting isKeyguardState()467 boolean isKeyguardState() { 468 if (SceneContainerFlag.isEnabled()) { 469 return false; 470 } else { 471 // We want the freshest state here since otherwise we'll have some weirdness if earlier 472 // listeners trigger updates 473 return mStatusBarStateController.getCurrentOrUpcomingState() == KEYGUARD; 474 } 475 } 476 477 @VisibleForTesting getStatusBarState()478 int getStatusBarState() { 479 return mStatusBarState; 480 } 481 updateShowCollapsedOnKeyguard()482 private void updateShowCollapsedOnKeyguard() { 483 boolean showCollapsed = mBypassController.getBypassEnabled() 484 || (mTransitioningToFullShade && !mInSplitShade); 485 if (showCollapsed != mShowCollapsedOnKeyguard) { 486 mShowCollapsedOnKeyguard = showCollapsed; 487 updateQsState(); 488 if (mQSAnimator != null) { 489 mQSAnimator.setShowCollapsedOnKeyguard(showCollapsed); 490 } 491 if (!showCollapsed && isKeyguardState()) { 492 setQsExpansion(mLastQSExpansion, mLastPanelFraction, 0, 493 mSquishinessFraction); 494 } 495 } 496 } 497 getQSPanelController()498 public QSPanelController getQSPanelController() { 499 return mQSPanelController; 500 } 501 setBrightnessMirrorController( @ullable MirrorController brightnessMirrorController)502 public void setBrightnessMirrorController( 503 @Nullable MirrorController brightnessMirrorController) { 504 mQSPanelController.setBrightnessMirror(brightnessMirrorController); 505 } 506 507 @Override isShowingDetail()508 public boolean isShowingDetail() { 509 return mQSCustomizerController.isCustomizing(); 510 } 511 512 @Override setHeaderClickable(boolean clickable)513 public void setHeaderClickable(boolean clickable) { 514 if (DEBUG) Log.d(TAG, "setHeaderClickable " + clickable); 515 } 516 517 @Override setExpanded(boolean expanded)518 public void setExpanded(boolean expanded) { 519 if (DEBUG) Log.d(TAG, "setExpanded " + expanded); 520 mQsExpanded = expanded; 521 if (mInSplitShade && mQsExpanded) { 522 // in split shade QS is expanded immediately when shade expansion starts and then we 523 // also need to listen to changes - otherwise QS is updated only once its fully expanded 524 setListening(true); 525 } else { 526 updateQsPanelControllerListening(); 527 } 528 updateQsState(); 529 } 530 setKeyguardShowing(boolean keyguardShowing)531 private void setKeyguardShowing(boolean keyguardShowing) { 532 if (!SceneContainerFlag.isEnabled()) { 533 if (DEBUG) Log.d(TAG, "setKeyguardShowing " + keyguardShowing); 534 mLastQSExpansion = -1; 535 536 if (mQSAnimator != null) { 537 mQSAnimator.setOnKeyguard(keyguardShowing); 538 } 539 540 mFooter.setKeyguardShowing(keyguardShowing); 541 updateQsState(); 542 } 543 } 544 545 @Override setOverscrolling(boolean stackScrollerOverscrolling)546 public void setOverscrolling(boolean stackScrollerOverscrolling) { 547 if (DEBUG) Log.d(TAG, "setOverscrolling " + stackScrollerOverscrolling); 548 mStackScrollerOverscrolling = stackScrollerOverscrolling; 549 updateQsState(); 550 } 551 552 @Override setListening(boolean listening)553 public void setListening(boolean listening) { 554 if (DEBUG) Log.d(TAG, "setListening " + listening); 555 mListening = listening; 556 mQSContainerImplController.setListening(listening && mQsVisible); 557 mListeningAndVisibilityLifecycleOwner.updateState(); 558 updateQsPanelControllerListening(); 559 } 560 updateQsPanelControllerListening()561 private void updateQsPanelControllerListening() { 562 mQSPanelController.setListening(mListening && mQsVisible, mQsExpanded); 563 } 564 565 @Override setQsVisible(boolean visible)566 public void setQsVisible(boolean visible) { 567 if (DEBUG) Log.d(TAG, "setQsVisible " + visible); 568 mQsVisible = visible; 569 setListening(mListening); 570 mListeningAndVisibilityLifecycleOwner.updateState(); 571 } 572 573 @Override setHeaderListening(boolean listening)574 public void setHeaderListening(boolean listening) { 575 mQSContainerImplController.setListening(listening); 576 } 577 578 @Override setInSplitShade(boolean inSplitShade)579 public void setInSplitShade(boolean inSplitShade) { 580 mInSplitShade = inSplitShade; 581 updateShowCollapsedOnKeyguard(); 582 updateQsState(); 583 } 584 585 @Override setTransitionToFullShadeProgress( boolean isTransitioningToFullShade, @FloatRange(from = 0.0, to = 1.0) float qsTransitionFraction, @FloatRange(from = 0.0, to = 1.0) float qsSquishinessFraction)586 public void setTransitionToFullShadeProgress( 587 boolean isTransitioningToFullShade, 588 @FloatRange(from = 0.0, to = 1.0) float qsTransitionFraction, 589 @FloatRange(from = 0.0, to = 1.0) float qsSquishinessFraction) { 590 if (isTransitioningToFullShade != mTransitioningToFullShade) { 591 mTransitioningToFullShade = isTransitioningToFullShade; 592 updateShowCollapsedOnKeyguard(); 593 } 594 mLockscreenToShadeProgress = qsTransitionFraction; 595 setQsExpansion(mLastQSExpansion, mLastPanelFraction, mLastHeaderTranslation, 596 isTransitioningToFullShade ? qsSquishinessFraction : mSquishinessFraction); 597 } 598 599 @Override setOverScrollAmount(int overScrollAmount)600 public void setOverScrollAmount(int overScrollAmount) { 601 mOverScrolling = overScrollAmount != 0; 602 View view = getView(); 603 if (view != null) { 604 view.setTranslationY(overScrollAmount); 605 } 606 } 607 608 @Override getHeightDiff()609 public int getHeightDiff() { 610 if (SceneContainerFlag.isEnabled()) { 611 return mQSPanelController.getViewBottom() - mHeader.getBottom() 612 + mHeader.getPaddingBottom(); 613 } else { 614 return mQSPanelScrollView.getBottom() - mHeader.getBottom() 615 + mHeader.getPaddingBottom(); 616 } 617 } 618 619 @Override setIsNotificationPanelFullWidth(boolean isFullWidth)620 public void setIsNotificationPanelFullWidth(boolean isFullWidth) { 621 mIsSmallScreen = isFullWidth; 622 } 623 624 @Override setShouldUpdateSquishinessOnMedia(boolean shouldUpdate)625 public void setShouldUpdateSquishinessOnMedia(boolean shouldUpdate) { 626 if (DEBUG) Log.d(TAG, "setShouldUpdateSquishinessOnMedia " + shouldUpdate); 627 mShouldUpdateMediaSquishiness = shouldUpdate; 628 } 629 630 @Override setQsExpansion(float expansion, float panelExpansionFraction, float proposedTranslation, float squishinessFraction)631 public void setQsExpansion(float expansion, float panelExpansionFraction, 632 float proposedTranslation, float squishinessFraction) { 633 float headerTranslation = mTransitioningToFullShade ? 0 : proposedTranslation; 634 float alphaProgress = calculateAlphaProgress(panelExpansionFraction); 635 setAlphaAnimationProgress(alphaProgress); 636 mContainer.setExpansion(expansion); 637 final float translationScaleY = (mInSplitShade 638 ? 1 : QSAnimator.SHORT_PARALLAX_AMOUNT) * (expansion - 1); 639 boolean onKeyguard = isKeyguardState(); 640 boolean onKeyguardAndExpanded = onKeyguard && !mShowCollapsedOnKeyguard; 641 if (!mHeaderAnimating && !headerWillBeAnimating() && !mOverScrolling) { 642 getView().setTranslationY( 643 onKeyguardAndExpanded 644 ? translationScaleY * mHeader.getHeight() 645 : headerTranslation); 646 } 647 int currentHeight = getView().getHeight(); 648 if (expansion == mLastQSExpansion 649 && mLastKeyguardAndExpanded == onKeyguardAndExpanded 650 && mLastViewHeight == currentHeight 651 && mLastHeaderTranslation == headerTranslation 652 && mSquishinessFraction == squishinessFraction 653 && mLastPanelFraction == panelExpansionFraction) { 654 return; 655 } 656 mLastHeaderTranslation = headerTranslation; 657 mLastPanelFraction = panelExpansionFraction; 658 mSquishinessFraction = squishinessFraction; 659 mLastQSExpansion = expansion; 660 mLastKeyguardAndExpanded = onKeyguardAndExpanded; 661 mLastViewHeight = currentHeight; 662 663 boolean fullyExpanded = expansion == 1; 664 boolean fullyCollapsed = expansion == 0.0f; 665 int heightDiff = getHeightDiff(); 666 float panelTranslationY = translationScaleY * heightDiff; 667 668 if (expansion < 1 && expansion > 0.99) { 669 if (mQuickQSPanelController.switchTileLayout(false)) { 670 mHeader.updateResources(); 671 } 672 } 673 mQSPanelController.setIsOnKeyguard(onKeyguard); 674 mFooter.setExpansion(onKeyguardAndExpanded ? 1 : expansion); 675 float footerActionsExpansion = 676 onKeyguardAndExpanded ? 1 : mInSplitShade ? alphaProgress : expansion; 677 if (mQSFooterActionsViewModel != null) { 678 mQSFooterActionsViewModel.onQuickSettingsExpansionChanged(footerActionsExpansion, 679 mInSplitShade); 680 } 681 mQSPanelController.setRevealExpansion(expansion); 682 mQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation); 683 mQuickQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation); 684 685 if (!SceneContainerFlag.isEnabled()) { 686 float qsScrollViewTranslation = 687 onKeyguard && !mShowCollapsedOnKeyguard ? panelTranslationY : 0; 688 mQSPanelScrollView.setTranslationY(qsScrollViewTranslation); 689 690 if (fullyCollapsed) { 691 mQSPanelScrollView.setScrollY(0); 692 } 693 694 if (!fullyExpanded) { 695 // Set bounds on the QS panel so it doesn't run over the header when animating. 696 mQsBounds.top = (int) -mQSPanelScrollView.getTranslationY(); 697 mQsBounds.right = mQSPanelScrollView.getWidth(); 698 mQsBounds.bottom = mQSPanelScrollView.getHeight(); 699 } 700 } 701 updateQsBounds(); 702 703 if (mQSSquishinessController != null) { 704 mQSSquishinessController.setSquishiness(mSquishinessFraction); 705 } 706 if (mQSAnimator != null) { 707 mQSAnimator.setPosition(expansion); 708 } 709 if (!mShouldUpdateMediaSquishiness 710 && (!mInSplitShade 711 || mStatusBarStateController.getState() == KEYGUARD 712 || mStatusBarStateController.getState() == SHADE_LOCKED) 713 ) { 714 // At beginning, state is 0 and will apply wrong squishiness to MediaHost in lockscreen 715 // and media player expect no change by squishiness in lock screen shade. Don't bother 716 // squishing mQsMediaHost when not in split shade to prevent problems with stale state. 717 mQsMediaHost.setSquishFraction(1.0F); 718 } else { 719 mQsMediaHost.setSquishFraction(mSquishinessFraction); 720 } 721 updateMediaPositions(); 722 } 723 setAlphaAnimationProgress(float progress)724 private void setAlphaAnimationProgress(float progress) { 725 final View view = getView(); 726 if (progress == 0 && view.getVisibility() != View.INVISIBLE) { 727 mLogger.logVisibility("QS fragment", View.INVISIBLE); 728 view.setVisibility(View.INVISIBLE); 729 } else if (progress > 0 && view.getVisibility() != View.VISIBLE) { 730 mLogger.logVisibility("QS fragment", View.VISIBLE); 731 view.setVisibility((View.VISIBLE)); 732 } 733 view.setAlpha(interpolateAlphaAnimationProgress(progress)); 734 } 735 calculateAlphaProgress(float panelExpansionFraction)736 private float calculateAlphaProgress(float panelExpansionFraction) { 737 if (mIsSmallScreen) { 738 // Small screens. QS alpha is not animated. 739 return 1; 740 } 741 if (mInSplitShade) { 742 // Large screens in landscape. 743 // Need to check upcoming state as for unlocked -> AOD transition current state is 744 // not updated yet, but we're transitioning and UI should already follow KEYGUARD state 745 if (mTransitioningToFullShade 746 || mStatusBarStateController.getCurrentOrUpcomingState() == KEYGUARD) { 747 // Always use "mFullShadeProgress" on keyguard, because 748 // "panelExpansionFractions" is always 1 on keyguard split shade. 749 return mLockscreenToShadeProgress; 750 } else { 751 return panelExpansionFraction; 752 } 753 } 754 // Large screens in portrait. 755 if (mTransitioningToFullShade) { 756 // Only use this value during the standard lock screen shade expansion. During the 757 // "quick" expansion from top, this value is 0. 758 return mLockscreenToShadeProgress; 759 } else { 760 return panelExpansionFraction; 761 } 762 } 763 interpolateAlphaAnimationProgress(float progress)764 private float interpolateAlphaAnimationProgress(float progress) { 765 if (mQSPanelController.isBouncerInTransit()) { 766 return BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(progress); 767 } 768 if (isKeyguardState()) { 769 // Alpha progress should be linear on lockscreen shade expansion. 770 return progress; 771 } 772 if (mIsSmallScreen) { 773 return ShadeInterpolation.getContentAlpha(progress); 774 } else { 775 return mLargeScreenShadeInterpolator.getQsAlpha(progress); 776 } 777 } 778 779 @VisibleForTesting updateQsBounds()780 void updateQsBounds() { 781 if (mLastQSExpansion == 1.0f) { 782 // Fully expanded, let's set the layout bounds as clip bounds. This is necessary because 783 // it's a scrollview and otherwise wouldn't be clipped. However, we set the horizontal 784 // bounds so the pages go to the ends of QSContainerImpl (most cases) or its parent 785 // (large screen portrait) 786 int sideMargin = getResources().getDimensionPixelSize( 787 R.dimen.qs_tiles_page_horizontal_margin) * 2; 788 mQsBounds.set(-sideMargin, 0, mQSPanelScrollView.getWidth() + sideMargin, 789 mQSPanelScrollView.getHeight()); 790 } 791 if (!SceneContainerFlag.isEnabled()) { 792 mQSPanelScrollView.setClipBounds(mQsBounds); 793 794 mQSPanelScrollView.getLocationOnScreen(mLocationTemp); 795 int left = mLocationTemp[0]; 796 int top = mLocationTemp[1]; 797 mQsMediaHost.getCurrentClipping().set(left, top, 798 left + getView().getMeasuredWidth(), 799 top + mQSPanelScrollView.getMeasuredHeight() 800 - mQSPanelController.getPaddingBottom()); 801 } 802 } 803 updateMediaPositions()804 private void updateMediaPositions() { 805 if (Utils.useQsMediaPlayer(getContext())) { 806 View hostView = mQsMediaHost.getHostView(); 807 // Make sure the media appears a bit from the top to make it look nicer 808 if (mLastQSExpansion > 0 && !isKeyguardState() && !mQqsMediaHost.getVisible() 809 && !mQSPanelController.shouldUseHorizontalLayout() && !mInSplitShade) { 810 float interpolation = 1.0f - mLastQSExpansion; 811 interpolation = Interpolators.ACCELERATE.getInterpolation(interpolation); 812 float translationY = -hostView.getHeight() * 1.3f * interpolation; 813 hostView.setTranslationY(translationY); 814 } else { 815 hostView.setTranslationY(0); 816 } 817 } 818 } 819 headerWillBeAnimating()820 private boolean headerWillBeAnimating() { 821 return mStatusBarState == KEYGUARD && mShowCollapsedOnKeyguard && !isKeyguardState(); 822 } 823 824 @Override animateHeaderSlidingOut()825 public void animateHeaderSlidingOut() { 826 if (DEBUG) Log.d(TAG, "animateHeaderSlidingOut"); 827 if (getView().getY() == -mHeader.getHeight()) { 828 return; 829 } 830 mHeaderAnimating = true; 831 getView().animate().y(-mHeader.getHeight()) 832 .setStartDelay(0) 833 .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD) 834 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 835 .setListener(new AnimatorListenerAdapter() { 836 @Override 837 public void onAnimationEnd(Animator animation) { 838 if (getView() != null) { 839 // The view could be destroyed before the animation completes when 840 // switching users. 841 getView().animate().setListener(null); 842 } 843 mHeaderAnimating = false; 844 updateQsState(); 845 } 846 }) 847 .start(); 848 } 849 850 @Override setCollapseExpandAction(Runnable action)851 public void setCollapseExpandAction(Runnable action) { 852 mQSPanelController.setCollapseExpandAction(action); 853 mQuickQSPanelController.setCollapseExpandAction(action); 854 } 855 856 @Override closeDetail()857 public void closeDetail() { 858 mQSPanelController.closeDetail(); 859 } 860 861 @Override closeCustomizer()862 public void closeCustomizer() { 863 mQSCustomizerController.hide(); 864 } 865 closeCustomizerImmediately()866 public void closeCustomizerImmediately() { 867 mQSCustomizerController.hide(false); 868 } 869 notifyCustomizeChanged()870 public void notifyCustomizeChanged() { 871 // The customize state changed, so our height changed. 872 mContainer.updateExpansion(); 873 boolean customizing = isCustomizing(); 874 if (SceneContainerFlag.isEnabled()) { 875 mQSPanelController.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); 876 } else { 877 mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); 878 } 879 mFooter.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); 880 if (mFooterActionsView != null) { 881 mFooterActionsView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); 882 } 883 mHeader.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); 884 // Let the panel know the position changed and it needs to update where notifications 885 // and whatnot are. 886 if (mPanelView != null) { 887 mPanelView.onQsHeightChanged(); 888 } 889 } 890 891 /** 892 * The height this view wants to be. This is different from {@link View#getMeasuredHeight} such 893 * that during closing the detail panel, this already returns the smaller height. 894 */ 895 @Override getDesiredHeight()896 public int getDesiredHeight() { 897 if (mQSCustomizerController.isCustomizing()) { 898 return getView().getHeight(); 899 } 900 return getView().getMeasuredHeight(); 901 } 902 903 @Override setHeightOverride(int desiredHeight)904 public void setHeightOverride(int desiredHeight) { 905 mContainer.setHeightOverride(desiredHeight); 906 } 907 908 @Override getQsMinExpansionHeight()909 public int getQsMinExpansionHeight() { 910 if (mInSplitShade) { 911 return getQsMinExpansionHeightForSplitShade(); 912 } 913 return mHeader.getHeight(); 914 } 915 916 /** 917 * Returns the min expansion height for split shade. 918 * 919 * On split shade, QS is always expanded and goes from the top of the screen to the bottom of 920 * the QS container. 921 */ getQsMinExpansionHeightForSplitShade()922 private int getQsMinExpansionHeightForSplitShade() { 923 getView().getLocationOnScreen(mLocationTemp); 924 int top = mLocationTemp[1]; 925 // We want to get the original top position, so we subtract any translation currently set. 926 int originalTop = (int) (top - getView().getTranslationY()); 927 // On split shade the QS view doesn't start at the top of the screen, so we need to add the 928 // top margin. 929 return originalTop + getView().getHeight(); 930 } 931 932 @Override hideImmediately()933 public void hideImmediately() { 934 getView().animate().cancel(); 935 getView().setY(-getQsMinExpansionHeight()); 936 } 937 938 @Override onUpcomingStateChanged(int upcomingState)939 public void onUpcomingStateChanged(int upcomingState) { 940 if (upcomingState == KEYGUARD) { 941 // refresh state of QS as soon as possible - while it's still upcoming - so in case of 942 // transition to KEYGUARD (e.g. from unlocked to AOD) all objects are aware they should 943 // already behave like on keyguard. Otherwise we might be doing extra work, 944 // e.g. QSAnimator making QS visible and then quickly invisible 945 onStateChanged(upcomingState); 946 } 947 } 948 949 @Override onStateChanged(int newState)950 public void onStateChanged(int newState) { 951 if (SceneContainerFlag.isEnabled() || newState == mStatusBarState) { 952 return; 953 } 954 mStatusBarState = newState; 955 setKeyguardShowing(newState == KEYGUARD); 956 updateShowCollapsedOnKeyguard(); 957 } 958 959 @VisibleForTesting getListeningAndVisibilityLifecycleOwner()960 public ListeningAndVisibilityLifecycleOwner getListeningAndVisibilityLifecycleOwner() { 961 return mListeningAndVisibilityLifecycleOwner; 962 } 963 getQQSHeight()964 public int getQQSHeight() { 965 return mContainer.getQqsHeight(); 966 } 967 getQSHeight()968 public int getQSHeight() { 969 return mContainer.getQsHeight(); 970 } 971 972 /** 973 * Pass the size of the navbar when it's at the bottom of the device so it can be used as 974 * padding 975 * @param padding size of the bottom nav bar in px 976 */ applyBottomNavBarToCustomizerPadding(int padding)977 public void applyBottomNavBarToCustomizerPadding(int padding) { 978 mQSCustomizerController.applyBottomNavBarSizeToRecyclerViewPadding(padding); 979 } 980 981 @NeverCompile 982 @Override dump(PrintWriter pw, String[] args)983 public void dump(PrintWriter pw, String[] args) { 984 IndentingPrintWriter indentingPw = new IndentingPrintWriter(pw, /* singleIndent= */ " "); 985 indentingPw.println("QSImpl:"); 986 indentingPw.increaseIndent(); 987 indentingPw.println("mQsBounds: " + mQsBounds); 988 indentingPw.println("mQsExpanded: " + mQsExpanded); 989 indentingPw.println("mHeaderAnimating: " + mHeaderAnimating); 990 indentingPw.println("mStackScrollerOverscrolling: " + mStackScrollerOverscrolling); 991 indentingPw.println("mListening: " + mListening); 992 indentingPw.println("mQsVisible: " + mQsVisible); 993 indentingPw.println("mLayoutDirection: " + mLayoutDirection); 994 indentingPw.println("mLastQSExpansion: " + mLastQSExpansion); 995 indentingPw.println("mLastPanelFraction: " + mLastPanelFraction); 996 indentingPw.println("mSquishinessFraction: " + mSquishinessFraction); 997 indentingPw.println("mQsDisabled: " + mQsDisabled); 998 indentingPw.println("mTemp: " + Arrays.toString(mLocationTemp)); 999 indentingPw.println("mShowCollapsedOnKeyguard: " + mShowCollapsedOnKeyguard); 1000 indentingPw.println("mLastKeyguardAndExpanded: " + mLastKeyguardAndExpanded); 1001 indentingPw.println("mStatusBarState: " + StatusBarState.toString(mStatusBarState)); 1002 indentingPw.println("mTmpLocation: " + Arrays.toString(mTmpLocation)); 1003 indentingPw.println("mLastViewHeight: " + mLastViewHeight); 1004 indentingPw.println("mLastHeaderTranslation: " + mLastHeaderTranslation); 1005 indentingPw.println("mInSplitShade: " + mInSplitShade); 1006 indentingPw.println("mTransitioningToFullShade: " + mTransitioningToFullShade); 1007 indentingPw.println("mLockscreenToShadeProgress: " + mLockscreenToShadeProgress); 1008 indentingPw.println("mOverScrolling: " + mOverScrolling); 1009 indentingPw.println("mShouldUpdateMediaSquishiness: " + mShouldUpdateMediaSquishiness); 1010 indentingPw.println("isCustomizing: " + mQSCustomizerController.isCustomizing()); 1011 View view = getView(); 1012 if (view != null) { 1013 indentingPw.println("top: " + view.getTop()); 1014 indentingPw.println("y: " + view.getY()); 1015 indentingPw.println("translationY: " + view.getTranslationY()); 1016 indentingPw.println("alpha: " + view.getAlpha()); 1017 indentingPw.println("height: " + view.getHeight()); 1018 indentingPw.println("measuredHeight: " + view.getMeasuredHeight()); 1019 indentingPw.println("clipBounds: " + view.getClipBounds()); 1020 } else { 1021 indentingPw.println("getView(): null"); 1022 } 1023 QuickStatusBarHeader header = mHeader; 1024 if (header != null) { 1025 indentingPw.println("headerHeight: " + header.getHeight()); 1026 indentingPw.println("Header visibility: " + visibilityToString(header.getVisibility())); 1027 } else { 1028 indentingPw.println("mHeader: null"); 1029 } 1030 } 1031 visibilityToString(int visibility)1032 private static String visibilityToString(int visibility) { 1033 if (visibility == View.VISIBLE) { 1034 return "VISIBLE"; 1035 } 1036 if (visibility == View.INVISIBLE) { 1037 return "INVISIBLE"; 1038 } 1039 return "GONE"; 1040 } 1041 1042 @Override getView()1043 public View getView() { 1044 return mRootView; 1045 } 1046 1047 @Override getContext()1048 public Context getContext() { 1049 return mRootView.getContext(); 1050 } 1051 getResources()1052 private Resources getResources() { 1053 return getContext().getResources(); 1054 } 1055 1056 /** 1057 * A {@link LifecycleOwner} whose state is driven by the current state of this fragment: 1058 * 1059 * - DESTROYED when the fragment is destroyed. 1060 * - CREATED when mListening == mQsVisible == false. 1061 * - STARTED when mListening == true && mQsVisible == false. 1062 * - RESUMED when mListening == true && mQsVisible == true. 1063 */ 1064 @VisibleForTesting 1065 class ListeningAndVisibilityLifecycleOwner implements LifecycleOwner { 1066 private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this); 1067 private boolean mDestroyed = false; 1068 1069 { updateState()1070 updateState(); 1071 } 1072 1073 @Override getLifecycle()1074 public Lifecycle getLifecycle() { 1075 return mLifecycleRegistry; 1076 } 1077 1078 /** 1079 * Update the state of the associated lifecycle. This should be called whenever 1080 * {@code mListening} or {@code mQsVisible} is changed. 1081 */ updateState()1082 public void updateState() { 1083 if (mDestroyed) { 1084 mLifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED); 1085 return; 1086 } 1087 1088 if (!mListening) { 1089 mLifecycleRegistry.setCurrentState(Lifecycle.State.CREATED); 1090 return; 1091 } 1092 1093 // mListening && !mQsVisible. 1094 if (!mQsVisible) { 1095 mLifecycleRegistry.setCurrentState(Lifecycle.State.STARTED); 1096 return; 1097 } 1098 1099 // mListening && mQsVisible. 1100 mLifecycleRegistry.setCurrentState(Lifecycle.State.RESUMED); 1101 } 1102 destroy()1103 public void destroy() { 1104 mDestroyed = true; 1105 updateState(); 1106 } 1107 } 1108 } 1109