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