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