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 android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.content.Context; 22 import android.content.res.Configuration; 23 import android.graphics.Rect; 24 import android.os.Bundle; 25 import android.util.Log; 26 import android.view.ContextThemeWrapper; 27 import android.view.LayoutInflater; 28 import android.view.MotionEvent; 29 import android.view.View; 30 import android.view.View.OnClickListener; 31 import android.view.ViewGroup; 32 import android.view.ViewTreeObserver; 33 import android.widget.FrameLayout.LayoutParams; 34 35 import androidx.annotation.Nullable; 36 import androidx.annotation.VisibleForTesting; 37 38 import com.android.systemui.Interpolators; 39 import com.android.systemui.R; 40 import com.android.systemui.R.id; 41 import com.android.systemui.SysUiServiceProvider; 42 import com.android.systemui.plugins.qs.QS; 43 import com.android.systemui.plugins.statusbar.StatusBarStateController; 44 import com.android.systemui.qs.customize.QSCustomizer; 45 import com.android.systemui.statusbar.CommandQueue; 46 import com.android.systemui.statusbar.StatusBarState; 47 import com.android.systemui.statusbar.notification.stack.StackStateAnimator; 48 import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer; 49 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler; 50 import com.android.systemui.util.InjectionInflationController; 51 import com.android.systemui.util.LifecycleFragment; 52 53 import javax.inject.Inject; 54 55 public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Callbacks, 56 StatusBarStateController.StateListener { 57 private static final String TAG = "QS"; 58 private static final boolean DEBUG = false; 59 private static final String EXTRA_EXPANDED = "expanded"; 60 private static final String EXTRA_LISTENING = "listening"; 61 62 private final Rect mQsBounds = new Rect(); 63 private final StatusBarStateController mStatusBarStateController; 64 private boolean mQsExpanded; 65 private boolean mHeaderAnimating; 66 private boolean mStackScrollerOverscrolling; 67 68 private long mDelay; 69 70 private QSAnimator mQSAnimator; 71 private HeightListener mPanelView; 72 protected QuickStatusBarHeader mHeader; 73 private QSCustomizer mQSCustomizer; 74 protected QSPanel mQSPanel; 75 private QSDetail mQSDetail; 76 private boolean mListening; 77 private QSContainerImpl mContainer; 78 private int mLayoutDirection; 79 private QSFooter mFooter; 80 private float mLastQSExpansion = -1; 81 private boolean mQsDisabled; 82 83 private final RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler; 84 private final InjectionInflationController mInjectionInflater; 85 private final QSTileHost mHost; 86 private boolean mShowCollapsedOnKeyguard; 87 private boolean mLastKeyguardAndExpanded; 88 /** 89 * The last received state from the controller. This should not be used directly to check if 90 * we're on keyguard but use {@link #isKeyguardShowing()} instead since that is more accurate 91 * during state transitions which often call into us. 92 */ 93 private int mState; 94 95 @Inject QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler, InjectionInflationController injectionInflater, Context context, QSTileHost qsTileHost, StatusBarStateController statusBarStateController)96 public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler, 97 InjectionInflationController injectionInflater, 98 Context context, 99 QSTileHost qsTileHost, 100 StatusBarStateController statusBarStateController) { 101 mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler; 102 mInjectionInflater = injectionInflater; 103 SysUiServiceProvider.getComponent(context, CommandQueue.class) 104 .observe(getLifecycle(), this); 105 mHost = qsTileHost; 106 mStatusBarStateController = statusBarStateController; 107 } 108 109 @Override onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState)110 public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, 111 Bundle savedInstanceState) { 112 inflater = mInjectionInflater.injectable( 113 inflater.cloneInContext(new ContextThemeWrapper(getContext(), R.style.qs_theme))); 114 return inflater.inflate(R.layout.qs_panel, container, false); 115 } 116 117 @Override onViewCreated(View view, @Nullable Bundle savedInstanceState)118 public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 119 super.onViewCreated(view, savedInstanceState); 120 mQSPanel = view.findViewById(R.id.quick_settings_panel); 121 mQSDetail = view.findViewById(R.id.qs_detail); 122 mHeader = view.findViewById(R.id.header); 123 mFooter = view.findViewById(R.id.qs_footer); 124 mContainer = view.findViewById(id.quick_settings_container); 125 126 mQSDetail.setQsPanel(mQSPanel, mHeader, (View) mFooter); 127 mQSAnimator = new QSAnimator(this, 128 mHeader.findViewById(R.id.quick_qs_panel), mQSPanel); 129 130 mQSCustomizer = view.findViewById(R.id.qs_customize); 131 mQSCustomizer.setQs(this); 132 if (savedInstanceState != null) { 133 setExpanded(savedInstanceState.getBoolean(EXTRA_EXPANDED)); 134 setListening(savedInstanceState.getBoolean(EXTRA_LISTENING)); 135 setEditLocation(view); 136 mQSCustomizer.restoreInstanceState(savedInstanceState); 137 if (mQsExpanded) { 138 mQSPanel.getTileLayout().restoreInstanceState(savedInstanceState); 139 } 140 } 141 setHost(mHost); 142 mStatusBarStateController.addCallback(this); 143 onStateChanged(mStatusBarStateController.getState()); 144 } 145 146 @Override onDestroy()147 public void onDestroy() { 148 super.onDestroy(); 149 mStatusBarStateController.removeCallback(this); 150 if (mListening) { 151 setListening(false); 152 } 153 } 154 155 @Override onSaveInstanceState(Bundle outState)156 public void onSaveInstanceState(Bundle outState) { 157 super.onSaveInstanceState(outState); 158 outState.putBoolean(EXTRA_EXPANDED, mQsExpanded); 159 outState.putBoolean(EXTRA_LISTENING, mListening); 160 mQSCustomizer.saveInstanceState(outState); 161 if (mQsExpanded) { 162 mQSPanel.getTileLayout().saveInstanceState(outState); 163 } 164 } 165 166 @VisibleForTesting isListening()167 boolean isListening() { 168 return mListening; 169 } 170 171 @VisibleForTesting isExpanded()172 boolean isExpanded() { 173 return mQsExpanded; 174 } 175 176 @Override getHeader()177 public View getHeader() { 178 return mHeader; 179 } 180 181 @Override setHasNotifications(boolean hasNotifications)182 public void setHasNotifications(boolean hasNotifications) { 183 } 184 185 @Override setPanelView(HeightListener panelView)186 public void setPanelView(HeightListener panelView) { 187 mPanelView = panelView; 188 } 189 190 @Override onConfigurationChanged(Configuration newConfig)191 public void onConfigurationChanged(Configuration newConfig) { 192 super.onConfigurationChanged(newConfig); 193 setEditLocation(getView()); 194 if (newConfig.getLayoutDirection() != mLayoutDirection) { 195 mLayoutDirection = newConfig.getLayoutDirection(); 196 if (mQSAnimator != null) { 197 mQSAnimator.onRtlChanged(); 198 } 199 } 200 } 201 setEditLocation(View view)202 private void setEditLocation(View view) { 203 View edit = view.findViewById(android.R.id.edit); 204 int[] loc = edit.getLocationOnScreen(); 205 int x = loc[0] + edit.getWidth() / 2; 206 int y = loc[1] + edit.getHeight() / 2; 207 mQSCustomizer.setEditLocation(x, y); 208 } 209 210 @Override setContainer(ViewGroup container)211 public void setContainer(ViewGroup container) { 212 if (container instanceof NotificationsQuickSettingsContainer) { 213 mQSCustomizer.setContainer((NotificationsQuickSettingsContainer) container); 214 } 215 } 216 217 @Override isCustomizing()218 public boolean isCustomizing() { 219 return mQSCustomizer.isCustomizing(); 220 } 221 setHost(QSTileHost qsh)222 public void setHost(QSTileHost qsh) { 223 mQSPanel.setHost(qsh, mQSCustomizer); 224 mHeader.setQSPanel(mQSPanel); 225 mFooter.setQSPanel(mQSPanel); 226 mQSDetail.setHost(qsh); 227 228 if (mQSAnimator != null) { 229 mQSAnimator.setHost(qsh); 230 } 231 } 232 233 @Override disable(int displayId, int state1, int state2, boolean animate)234 public void disable(int displayId, int state1, int state2, boolean animate) { 235 if (displayId != getContext().getDisplayId()) { 236 return; 237 } 238 state2 = mRemoteInputQuickSettingsDisabler.adjustDisableFlags(state2); 239 240 final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0; 241 if (disabled == mQsDisabled) return; 242 mQsDisabled = disabled; 243 mContainer.disable(state1, state2, animate); 244 mHeader.disable(state1, state2, animate); 245 mFooter.disable(state1, state2, animate); 246 updateQsState(); 247 } 248 updateQsState()249 private void updateQsState() { 250 final boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling 251 || mHeaderAnimating; 252 mQSPanel.setExpanded(mQsExpanded); 253 mQSDetail.setExpanded(mQsExpanded); 254 boolean keyguardShowing = isKeyguardShowing(); 255 mHeader.setVisibility((mQsExpanded || !keyguardShowing || mHeaderAnimating 256 || mShowCollapsedOnKeyguard) 257 ? View.VISIBLE 258 : View.INVISIBLE); 259 mHeader.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard) 260 || (mQsExpanded && !mStackScrollerOverscrolling)); 261 mFooter.setVisibility( 262 !mQsDisabled && (mQsExpanded || !keyguardShowing || mHeaderAnimating 263 || mShowCollapsedOnKeyguard) 264 ? View.VISIBLE 265 : View.INVISIBLE); 266 mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard) 267 || (mQsExpanded && !mStackScrollerOverscrolling)); 268 mQSPanel.setVisibility(!mQsDisabled && expandVisually ? View.VISIBLE : View.INVISIBLE); 269 } 270 isKeyguardShowing()271 private boolean isKeyguardShowing() { 272 // We want the freshest state here since otherwise we'll have some weirdness if earlier 273 // listeners trigger updates 274 return mStatusBarStateController.getState() == StatusBarState.KEYGUARD; 275 } 276 277 @Override setShowCollapsedOnKeyguard(boolean showCollapsedOnKeyguard)278 public void setShowCollapsedOnKeyguard(boolean showCollapsedOnKeyguard) { 279 if (showCollapsedOnKeyguard != mShowCollapsedOnKeyguard) { 280 mShowCollapsedOnKeyguard = showCollapsedOnKeyguard; 281 updateQsState(); 282 if (mQSAnimator != null) { 283 mQSAnimator.setShowCollapsedOnKeyguard(showCollapsedOnKeyguard); 284 } 285 if (!showCollapsedOnKeyguard && isKeyguardShowing()) { 286 setQsExpansion(mLastQSExpansion, 0); 287 } 288 } 289 } 290 getQsPanel()291 public QSPanel getQsPanel() { 292 return mQSPanel; 293 } 294 getCustomizer()295 public QSCustomizer getCustomizer() { 296 return mQSCustomizer; 297 } 298 299 @Override isShowingDetail()300 public boolean isShowingDetail() { 301 return mQSPanel.isShowingCustomize() || mQSDetail.isShowingDetail(); 302 } 303 304 @Override onInterceptTouchEvent(MotionEvent event)305 public boolean onInterceptTouchEvent(MotionEvent event) { 306 return isCustomizing(); 307 } 308 309 @Override setHeaderClickable(boolean clickable)310 public void setHeaderClickable(boolean clickable) { 311 if (DEBUG) Log.d(TAG, "setHeaderClickable " + clickable); 312 } 313 314 @Override setExpanded(boolean expanded)315 public void setExpanded(boolean expanded) { 316 if (DEBUG) Log.d(TAG, "setExpanded " + expanded); 317 mQsExpanded = expanded; 318 mQSPanel.setListening(mListening, mQsExpanded); 319 updateQsState(); 320 } 321 setKeyguardShowing(boolean keyguardShowing)322 private void setKeyguardShowing(boolean keyguardShowing) { 323 if (DEBUG) Log.d(TAG, "setKeyguardShowing " + keyguardShowing); 324 mLastQSExpansion = -1; 325 326 if (mQSAnimator != null) { 327 mQSAnimator.setOnKeyguard(keyguardShowing); 328 } 329 330 mFooter.setKeyguardShowing(keyguardShowing); 331 updateQsState(); 332 } 333 334 @Override setOverscrolling(boolean stackScrollerOverscrolling)335 public void setOverscrolling(boolean stackScrollerOverscrolling) { 336 if (DEBUG) Log.d(TAG, "setOverscrolling " + stackScrollerOverscrolling); 337 mStackScrollerOverscrolling = stackScrollerOverscrolling; 338 updateQsState(); 339 } 340 341 @Override setListening(boolean listening)342 public void setListening(boolean listening) { 343 if (DEBUG) Log.d(TAG, "setListening " + listening); 344 mListening = listening; 345 mHeader.setListening(listening); 346 mFooter.setListening(listening); 347 mQSPanel.setListening(mListening, mQsExpanded); 348 } 349 350 @Override setHeaderListening(boolean listening)351 public void setHeaderListening(boolean listening) { 352 mHeader.setListening(listening); 353 mFooter.setListening(listening); 354 } 355 356 @Override setQsExpansion(float expansion, float headerTranslation)357 public void setQsExpansion(float expansion, float headerTranslation) { 358 if (DEBUG) Log.d(TAG, "setQSExpansion " + expansion + " " + headerTranslation); 359 mContainer.setExpansion(expansion); 360 final float translationScaleY = expansion - 1; 361 boolean onKeyguardAndExpanded = isKeyguardShowing() && !mShowCollapsedOnKeyguard; 362 if (!mHeaderAnimating && !headerWillBeAnimating()) { 363 getView().setTranslationY( 364 onKeyguardAndExpanded 365 ? translationScaleY * mHeader.getHeight() 366 : headerTranslation); 367 } 368 if (expansion == mLastQSExpansion && mLastKeyguardAndExpanded == onKeyguardAndExpanded) { 369 return; 370 } 371 mLastQSExpansion = expansion; 372 mLastKeyguardAndExpanded = onKeyguardAndExpanded; 373 374 boolean fullyExpanded = expansion == 1; 375 int heightDiff = mQSPanel.getBottom() - mHeader.getBottom() + mHeader.getPaddingBottom() 376 + mFooter.getHeight(); 377 float panelTranslationY = translationScaleY * heightDiff; 378 379 // Let the views animate their contents correctly by giving them the necessary context. 380 mHeader.setExpansion(onKeyguardAndExpanded, expansion, 381 panelTranslationY); 382 mFooter.setExpansion(onKeyguardAndExpanded ? 1 : expansion); 383 mQSPanel.getQsTileRevealController().setExpansion(expansion); 384 mQSPanel.getTileLayout().setExpansion(expansion); 385 mQSPanel.setTranslationY(translationScaleY * heightDiff); 386 mQSDetail.setFullyExpanded(fullyExpanded); 387 388 if (fullyExpanded) { 389 // Always draw within the bounds of the view when fully expanded. 390 mQSPanel.setClipBounds(null); 391 } else { 392 // Set bounds on the QS panel so it doesn't run over the header when animating. 393 mQsBounds.top = (int) -mQSPanel.getTranslationY(); 394 mQsBounds.right = mQSPanel.getWidth(); 395 mQsBounds.bottom = mQSPanel.getHeight(); 396 mQSPanel.setClipBounds(mQsBounds); 397 } 398 399 if (mQSAnimator != null) { 400 mQSAnimator.setPosition(expansion); 401 } 402 } 403 headerWillBeAnimating()404 private boolean headerWillBeAnimating() { 405 return mState == StatusBarState.KEYGUARD && mShowCollapsedOnKeyguard 406 && !isKeyguardShowing(); 407 } 408 409 @Override animateHeaderSlidingIn(long delay)410 public void animateHeaderSlidingIn(long delay) { 411 if (DEBUG) Log.d(TAG, "animateHeaderSlidingIn"); 412 // If the QS is already expanded we don't need to slide in the header as it's already 413 // visible. 414 if (!mQsExpanded && getView().getTranslationY() != 0) { 415 mHeaderAnimating = true; 416 mDelay = delay; 417 getView().getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn); 418 } 419 } 420 421 @Override animateHeaderSlidingOut()422 public void animateHeaderSlidingOut() { 423 if (DEBUG) Log.d(TAG, "animateHeaderSlidingOut"); 424 if (getView().getY() == -mHeader.getHeight()) { 425 return; 426 } 427 mHeaderAnimating = true; 428 getView().animate().y(-mHeader.getHeight()) 429 .setStartDelay(0) 430 .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD) 431 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 432 .setListener(new AnimatorListenerAdapter() { 433 @Override 434 public void onAnimationEnd(Animator animation) { 435 if (getView() != null) { 436 // The view could be destroyed before the animation completes when 437 // switching users. 438 getView().animate().setListener(null); 439 } 440 mHeaderAnimating = false; 441 updateQsState(); 442 } 443 }) 444 .start(); 445 } 446 447 @Override setExpandClickListener(OnClickListener onClickListener)448 public void setExpandClickListener(OnClickListener onClickListener) { 449 mFooter.setExpandClickListener(onClickListener); 450 } 451 452 @Override closeDetail()453 public void closeDetail() { 454 mQSPanel.closeDetail(); 455 } 456 notifyCustomizeChanged()457 public void notifyCustomizeChanged() { 458 // The customize state changed, so our height changed. 459 mContainer.updateExpansion(); 460 mQSPanel.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE); 461 mFooter.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE); 462 // Let the panel know the position changed and it needs to update where notifications 463 // and whatnot are. 464 mPanelView.onQsHeightChanged(); 465 } 466 467 /** 468 * The height this view wants to be. This is different from {@link #getMeasuredHeight} such that 469 * during closing the detail panel, this already returns the smaller height. 470 */ 471 @Override getDesiredHeight()472 public int getDesiredHeight() { 473 if (mQSCustomizer.isCustomizing()) { 474 return getView().getHeight(); 475 } 476 if (mQSDetail.isClosingDetail()) { 477 LayoutParams layoutParams = (LayoutParams) mQSPanel.getLayoutParams(); 478 int panelHeight = layoutParams.topMargin + layoutParams.bottomMargin + 479 + mQSPanel.getMeasuredHeight(); 480 return panelHeight + getView().getPaddingBottom(); 481 } else { 482 return getView().getMeasuredHeight(); 483 } 484 } 485 486 @Override setHeightOverride(int desiredHeight)487 public void setHeightOverride(int desiredHeight) { 488 mContainer.setHeightOverride(desiredHeight); 489 } 490 491 @Override getQsMinExpansionHeight()492 public int getQsMinExpansionHeight() { 493 return mHeader.getHeight(); 494 } 495 496 @Override hideImmediately()497 public void hideImmediately() { 498 getView().animate().cancel(); 499 getView().setY(-mHeader.getHeight()); 500 } 501 502 private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn 503 = new ViewTreeObserver.OnPreDrawListener() { 504 @Override 505 public boolean onPreDraw() { 506 getView().getViewTreeObserver().removeOnPreDrawListener(this); 507 getView().animate() 508 .translationY(0f) 509 .setStartDelay(mDelay) 510 .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE) 511 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 512 .setListener(mAnimateHeaderSlidingInListener) 513 .start(); 514 return true; 515 } 516 }; 517 518 private final Animator.AnimatorListener mAnimateHeaderSlidingInListener 519 = new AnimatorListenerAdapter() { 520 @Override 521 public void onAnimationEnd(Animator animation) { 522 mHeaderAnimating = false; 523 updateQsState(); 524 } 525 }; 526 527 @Override onStateChanged(int newState)528 public void onStateChanged(int newState) { 529 mState = newState; 530 setKeyguardShowing(newState == StatusBarState.KEYGUARD); 531 } 532 } 533