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