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