1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.qs; 18 19 import static com.android.systemui.util.Utils.useQsMediaPlayer; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.content.Context; 24 import android.content.res.Configuration; 25 import android.content.res.Resources; 26 import android.graphics.Rect; 27 import android.os.Bundle; 28 import android.util.ArrayMap; 29 import android.util.AttributeSet; 30 import android.util.Log; 31 import android.view.Gravity; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.view.ViewGroup; 35 import android.view.accessibility.AccessibilityNodeInfo; 36 import android.widget.LinearLayout; 37 38 import androidx.annotation.VisibleForTesting; 39 40 import com.android.internal.logging.UiEventLogger; 41 import com.android.internal.widget.RemeasuringLinearLayout; 42 import com.android.systemui.plugins.qs.QSTile; 43 import com.android.systemui.qs.logging.QSLogger; 44 import com.android.systemui.res.R; 45 import com.android.systemui.scene.shared.flag.SceneContainerFlag; 46 import com.android.systemui.settings.brightness.BrightnessSliderController; 47 import com.android.systemui.tuner.TunerService; 48 import com.android.systemui.tuner.TunerService.Tunable; 49 50 import java.util.ArrayList; 51 import java.util.List; 52 53 /** View that represents the quick settings tile panel (when expanded/pulled down). **/ 54 public class QSPanel extends LinearLayout implements Tunable { 55 56 public static final String QS_SHOW_BRIGHTNESS = "qs_show_brightness"; 57 public static final String QS_SHOW_HEADER = "qs_show_header"; 58 59 private static final String TAG = "QSPanel"; 60 61 protected final Context mContext; 62 private final int mMediaTopMargin; 63 private final int mMediaTotalBottomMargin; 64 65 private Runnable mCollapseExpandAction; 66 67 /** 68 * The index where the content starts that needs to be moved between parents 69 */ 70 private int mMovableContentStartIndex; 71 72 @Nullable 73 protected View mBrightnessView; 74 @Nullable 75 protected BrightnessSliderController mToggleSliderController; 76 77 /** Whether or not the QS media player feature is enabled. */ 78 protected boolean mUsingMediaPlayer; 79 80 protected boolean mExpanded; 81 protected boolean mListening; 82 83 private final List<OnConfigurationChangedListener> mOnConfigurationChangedListeners = 84 new ArrayList<>(); 85 86 @Nullable 87 protected View mFooter; 88 89 @Nullable 90 private PageIndicator mFooterPageIndicator; 91 private int mContentMarginStart; 92 private int mContentMarginEnd; 93 private boolean mUsingHorizontalLayout; 94 95 @Nullable 96 private LinearLayout mHorizontalLinearLayout; 97 @Nullable 98 protected LinearLayout mHorizontalContentContainer; 99 100 @Nullable 101 protected QSTileLayout mTileLayout; 102 private float mSquishinessFraction = 1f; 103 private final ArrayMap<View, Integer> mChildrenLayoutTop = new ArrayMap<>(); 104 private final Rect mClippingRect = new Rect(); 105 private ViewGroup mMediaHostView; 106 private boolean mShouldMoveMediaOnExpansion = true; 107 private QSLogger mQsLogger; 108 /** 109 * Specifies if we can collapse to QQS in current state. In split shade that should be always 110 * false. It influences available accessibility actions. 111 */ 112 private boolean mCanCollapse = true; 113 114 private boolean mSceneContainerEnabled; 115 116 @Nullable 117 private View mMediaViewPlaceHolderForScene; 118 119 private boolean mHadConfigurationChangeWhileDetached; 120 QSPanel(Context context, AttributeSet attrs)121 public QSPanel(Context context, AttributeSet attrs) { 122 super(context, attrs); 123 mUsingMediaPlayer = useQsMediaPlayer(context); 124 mMediaTotalBottomMargin = getResources().getDimensionPixelSize( 125 R.dimen.quick_settings_bottom_margin_media); 126 mMediaTopMargin = getResources().getDimensionPixelSize( 127 R.dimen.qs_tile_margin_vertical); 128 mContext = context; 129 130 setOrientation(VERTICAL); 131 132 mMovableContentStartIndex = getChildCount(); 133 } 134 initialize(QSLogger qsLogger, boolean usingMediaPlayer)135 void initialize(QSLogger qsLogger, boolean usingMediaPlayer) { 136 mQsLogger = qsLogger; 137 mUsingMediaPlayer = usingMediaPlayer; 138 mTileLayout = getOrCreateTileLayout(); 139 140 if (mUsingMediaPlayer || SceneContainerFlag.isEnabled()) { 141 mHorizontalLinearLayout = new RemeasuringLinearLayout(mContext); 142 mHorizontalLinearLayout.setOrientation(LinearLayout.HORIZONTAL); 143 mHorizontalLinearLayout.setVisibility( 144 mUsingHorizontalLayout ? View.VISIBLE : View.GONE); 145 mHorizontalLinearLayout.setClipChildren(false); 146 mHorizontalLinearLayout.setClipToPadding(false); 147 148 mHorizontalContentContainer = new RemeasuringLinearLayout(mContext); 149 mHorizontalContentContainer.setOrientation(LinearLayout.VERTICAL); 150 setHorizontalContentContainerClipping(); 151 152 LayoutParams lp = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1); 153 int marginSize = (int) mContext.getResources().getDimension(R.dimen.qs_media_padding); 154 lp.setMarginStart(0); 155 lp.setMarginEnd(marginSize); 156 lp.gravity = Gravity.CENTER_VERTICAL; 157 mHorizontalLinearLayout.addView(mHorizontalContentContainer, lp); 158 if (SceneContainerFlag.isEnabled()) { 159 int mediaHeight = mContext.getResources() 160 .getDimensionPixelSize(R.dimen.qs_media_session_height_expanded); 161 lp = new LayoutParams(0, mediaHeight, 1); 162 mMediaViewPlaceHolderForScene = new View(mContext); 163 mHorizontalLinearLayout.addView(mMediaViewPlaceHolderForScene, lp); 164 } 165 166 lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0, 1); 167 addView(mHorizontalLinearLayout, lp); 168 } 169 } 170 setSceneContainerEnabled(boolean enabled)171 void setSceneContainerEnabled(boolean enabled) { 172 mSceneContainerEnabled = enabled; 173 if (mSceneContainerEnabled) { 174 updatePadding(); 175 } 176 } 177 setHorizontalContentContainerClipping()178 protected void setHorizontalContentContainerClipping() { 179 if (mHorizontalContentContainer != null) { 180 mHorizontalContentContainer.setClipChildren(true); 181 mHorizontalContentContainer.setClipToPadding(false); 182 // Don't clip on the top, that way, secondary pages tiles can animate up 183 // Clipping coordinates should be relative to this view, not absolute 184 // (parent coordinates) 185 mHorizontalContentContainer.addOnLayoutChangeListener( 186 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { 187 if ((right - left) != (oldRight - oldLeft) 188 || ((bottom - top) != (oldBottom - oldTop))) { 189 mClippingRect.right = right - left; 190 mClippingRect.bottom = bottom - top; 191 mHorizontalContentContainer.setClipBounds(mClippingRect); 192 } 193 }); 194 mClippingRect.left = 0; 195 mClippingRect.top = -1000; 196 mHorizontalContentContainer.setClipBounds(mClippingRect); 197 } 198 } 199 200 /** 201 * Add brightness view above the tile layout. 202 * 203 * Used to add the brightness slider after construction. 204 */ setBrightnessView(@onNull View view)205 public void setBrightnessView(@NonNull View view) { 206 if (mBrightnessView != null) { 207 removeView(mBrightnessView); 208 mChildrenLayoutTop.remove(mBrightnessView); 209 mMovableContentStartIndex--; 210 } 211 addView(view, 0); 212 mBrightnessView = view; 213 214 setBrightnessViewMargin(); 215 216 mMovableContentStartIndex++; 217 } 218 setBrightnessViewMargin()219 private void setBrightnessViewMargin() { 220 if (mBrightnessView != null) { 221 MarginLayoutParams lp = (MarginLayoutParams) mBrightnessView.getLayoutParams(); 222 // For Brightness Slider to extend its boundary to draw focus background 223 int offset = getResources() 224 .getDimensionPixelSize(R.dimen.rounded_slider_boundary_offset); 225 lp.topMargin = mContext.getResources() 226 .getDimensionPixelSize(R.dimen.qs_brightness_margin_top) - offset; 227 lp.bottomMargin = mContext.getResources() 228 .getDimensionPixelSize(R.dimen.qs_brightness_margin_bottom) - offset; 229 mBrightnessView.setLayoutParams(lp); 230 } 231 } 232 233 /** */ getOrCreateTileLayout()234 public QSTileLayout getOrCreateTileLayout() { 235 if (mTileLayout == null) { 236 mTileLayout = (QSTileLayout) LayoutInflater.from(mContext) 237 .inflate(R.layout.qs_paged_tile_layout, this, false); 238 mTileLayout.setLogger(mQsLogger); 239 mTileLayout.setSquishinessFraction(mSquishinessFraction); 240 } 241 return mTileLayout; 242 } 243 setSquishinessFraction(float squishinessFraction)244 public void setSquishinessFraction(float squishinessFraction) { 245 if (Float.compare(squishinessFraction, mSquishinessFraction) == 0) { 246 return; 247 } 248 mSquishinessFraction = squishinessFraction; 249 if (mTileLayout == null) { 250 return; 251 } 252 mTileLayout.setSquishinessFraction(squishinessFraction); 253 if (getMeasuredWidth() == 0) { 254 return; 255 } 256 updateViewPositions(); 257 } 258 259 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)260 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 261 if (mTileLayout instanceof PagedTileLayout) { 262 // Since PageIndicator gets measured before PagedTileLayout, we preemptively set the 263 // # of pages before the measurement pass so PageIndicator is measured appropriately 264 if (mFooterPageIndicator != null) { 265 mFooterPageIndicator.setNumPages(((PagedTileLayout) mTileLayout).getNumPages()); 266 } 267 268 // In landscape, mTileLayout's parent is not the panel but a view that contains the 269 // tile layout and the media controls. 270 if (((View) mTileLayout).getParent() == this) { 271 // Allow the UI to be as big as it want's to, we're in a scroll view 272 int newHeight = 10000; 273 int availableHeight = MeasureSpec.getSize(heightMeasureSpec); 274 int excessHeight = newHeight - availableHeight; 275 // Measure with EXACTLY. That way, The content will only use excess height and will 276 // be measured last, after other views and padding is accounted for. This only 277 // works because our Layouts in here remeasure themselves with the exact content 278 // height. 279 heightMeasureSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY); 280 ((PagedTileLayout) mTileLayout).setExcessHeight(excessHeight); 281 } 282 } 283 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 284 285 // We want all the logic of LinearLayout#onMeasure, and for it to assign the excess space 286 // not used by the other children to PagedTileLayout. However, in this case, LinearLayout 287 // assumes that PagedTileLayout would use all the excess space. This is not the case as 288 // PagedTileLayout height is quantized (because it shows a certain number of rows). 289 // Therefore, after everything is measured, we need to make sure that we add up the correct 290 // total height 291 int height = getPaddingBottom() + getPaddingTop(); 292 int numChildren = getChildCount(); 293 for (int i = 0; i < numChildren; i++) { 294 View child = getChildAt(i); 295 if (child.getVisibility() != View.GONE) { 296 height += child.getMeasuredHeight(); 297 MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams(); 298 height += layoutParams.topMargin + layoutParams.bottomMargin; 299 } 300 } 301 setMeasuredDimension(getMeasuredWidth(), height); 302 } 303 304 @Override onLayout(boolean changed, int l, int t, int r, int b)305 protected void onLayout(boolean changed, int l, int t, int r, int b) { 306 super.onLayout(changed, l, t, r, b); 307 for (int i = 0; i < getChildCount(); i++) { 308 View child = getChildAt(i); 309 mChildrenLayoutTop.put(child, child.getTop()); 310 } 311 updateViewPositions(); 312 } 313 updateViewPositions()314 private void updateViewPositions() { 315 // Adjust view positions based on tile squishing 316 int tileHeightOffset = mTileLayout.getTilesHeight() - mTileLayout.getHeight(); 317 318 boolean move = false; 319 for (int i = 0; i < getChildCount(); i++) { 320 View child = getChildAt(i); 321 if (move) { 322 int topOffset; 323 if (child == mMediaHostView && !mShouldMoveMediaOnExpansion) { 324 topOffset = 0; 325 } else { 326 topOffset = tileHeightOffset; 327 } 328 // Animation can occur before the layout pass, meaning setSquishinessFraction() gets 329 // called before onLayout(). So, a child view could be null because it has not 330 // been added to mChildrenLayoutTop yet (which happens in onLayout()). 331 // We use a continue statement here to catch this NPE because, on the layout pass, 332 // this code will be called again from onLayout() with the populated children views. 333 Integer childLayoutTop = mChildrenLayoutTop.get(child); 334 if (childLayoutTop == null) { 335 continue; 336 } 337 int top = childLayoutTop; 338 child.setLeftTopRightBottom(child.getLeft(), top + topOffset, 339 child.getRight(), top + topOffset + child.getHeight()); 340 } 341 if (child == mTileLayout) { 342 move = true; 343 } 344 } 345 } 346 getDumpableTag()347 protected String getDumpableTag() { 348 return TAG; 349 } 350 351 @Override onTuningChanged(String key, String newValue)352 public void onTuningChanged(String key, String newValue) { 353 if (QS_SHOW_BRIGHTNESS.equals(key) && mBrightnessView != null) { 354 updateViewVisibilityForTuningValue(mBrightnessView, newValue); 355 } 356 } 357 updateViewVisibilityForTuningValue(View view, @Nullable String newValue)358 private void updateViewVisibilityForTuningValue(View view, @Nullable String newValue) { 359 view.setVisibility(TunerService.parseIntegerSwitch(newValue, true) ? VISIBLE : GONE); 360 } 361 362 363 @Nullable getBrightnessView()364 View getBrightnessView() { 365 return mBrightnessView; 366 } 367 368 /** 369 * Links the footer's page indicator, which is used in landscape orientation to save space. 370 * 371 * @param pageIndicator indicator to use for page scrolling 372 */ setFooterPageIndicator(PageIndicator pageIndicator)373 public void setFooterPageIndicator(PageIndicator pageIndicator) { 374 if (mTileLayout instanceof PagedTileLayout) { 375 mFooterPageIndicator = pageIndicator; 376 updatePageIndicator(); 377 } 378 } 379 updatePageIndicator()380 private void updatePageIndicator() { 381 if (mTileLayout instanceof PagedTileLayout) { 382 if (mFooterPageIndicator != null) { 383 mFooterPageIndicator.setVisibility(View.GONE); 384 385 ((PagedTileLayout) mTileLayout).setPageIndicator(mFooterPageIndicator); 386 } 387 } 388 } 389 updateResources()390 public void updateResources() { 391 updatePadding(); 392 393 updatePageIndicator(); 394 395 setBrightnessViewMargin(); 396 397 if (mTileLayout != null) { 398 mTileLayout.updateResources(); 399 } 400 401 if (mMediaViewPlaceHolderForScene != null) { 402 ViewGroup.LayoutParams lp = mMediaViewPlaceHolderForScene.getLayoutParams(); 403 lp.height = mContext.getResources() 404 .getDimensionPixelSize(R.dimen.qs_media_session_height_expanded); 405 mMediaViewPlaceHolderForScene.setLayoutParams(lp); 406 } 407 } 408 updatePadding()409 protected void updatePadding() { 410 final Resources res = mContext.getResources(); 411 int paddingTop = res.getDimensionPixelSize(R.dimen.qs_panel_padding_top); 412 int paddingBottom = res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom); 413 setPaddingRelative(getPaddingStart(), 414 mSceneContainerEnabled ? 0 : paddingTop, 415 getPaddingEnd(), 416 mSceneContainerEnabled ? 0 : paddingBottom); 417 } 418 addOnConfigurationChangedListener(OnConfigurationChangedListener listener)419 void addOnConfigurationChangedListener(OnConfigurationChangedListener listener) { 420 mOnConfigurationChangedListeners.add(listener); 421 } 422 removeOnConfigurationChangedListener(OnConfigurationChangedListener listener)423 void removeOnConfigurationChangedListener(OnConfigurationChangedListener listener) { 424 mOnConfigurationChangedListeners.remove(listener); 425 } 426 427 @Override onConfigurationChanged(Configuration newConfig)428 protected void onConfigurationChanged(Configuration newConfig) { 429 super.onConfigurationChanged(newConfig); 430 if (!isAttachedToWindow()) { 431 mHadConfigurationChangeWhileDetached = true; 432 } 433 mOnConfigurationChangedListeners.forEach( 434 listener -> listener.onConfigurationChange(newConfig)); 435 } 436 hadConfigurationChangeWhileDetached()437 final boolean hadConfigurationChangeWhileDetached() { 438 return mHadConfigurationChangeWhileDetached; 439 } 440 441 @Override onDetachedFromWindow()442 protected void onDetachedFromWindow() { 443 super.onDetachedFromWindow(); 444 mHadConfigurationChangeWhileDetached = false; 445 } 446 447 @Override onFinishInflate()448 protected void onFinishInflate() { 449 super.onFinishInflate(); 450 mFooter = findViewById(R.id.qs_footer); 451 } 452 updateHorizontalLinearLayoutMargins()453 private void updateHorizontalLinearLayoutMargins() { 454 if ((mUsingMediaPlayer || SceneContainerFlag.isEnabled()) && mHorizontalLinearLayout != null 455 && !displayMediaMarginsOnMedia()) { 456 LayoutParams lp = (LayoutParams) mHorizontalLinearLayout.getLayoutParams(); 457 lp.bottomMargin = Math.max(mMediaTotalBottomMargin - getPaddingBottom(), 0); 458 mHorizontalLinearLayout.setLayoutParams(lp); 459 } 460 } 461 462 /** 463 * @return true if the margin bottom of the media view should be on the media host or false 464 * if they should be on the HorizontalLinearLayout. Returning {@code false} is useful 465 * to visually center the tiles in the Media view, which doesn't work when the 466 * expanded panel actually scrolls. 467 */ displayMediaMarginsOnMedia()468 protected boolean displayMediaMarginsOnMedia() { 469 return true; 470 } 471 472 /** 473 * @return true if the media view needs margin on the top to separate it from the qs tiles 474 */ mediaNeedsTopMargin()475 protected boolean mediaNeedsTopMargin() { 476 return false; 477 } 478 needsDynamicRowsAndColumns()479 private boolean needsDynamicRowsAndColumns() { 480 return !SceneContainerFlag.isEnabled(); 481 } 482 switchAllContentToParent(ViewGroup parent, QSTileLayout newLayout)483 private void switchAllContentToParent(ViewGroup parent, QSTileLayout newLayout) { 484 int index = parent == this ? mMovableContentStartIndex : 0; 485 486 // Let's first move the tileLayout to the new parent, since that should come first. 487 switchToParent((View) newLayout, parent, index); 488 index++; 489 490 if (mFooter != null) { 491 // Then the footer with the settings 492 switchToParent(mFooter, parent, index); 493 index++; 494 } 495 } 496 switchToParent(View child, ViewGroup parent, int index)497 private void switchToParent(View child, ViewGroup parent, int index) { 498 switchToParent(child, parent, index, getDumpableTag()); 499 } 500 501 /** Call when orientation has changed and MediaHost needs to be adjusted. */ reAttachMediaHost(ViewGroup hostView, boolean horizontal)502 private void reAttachMediaHost(ViewGroup hostView, boolean horizontal) { 503 if (!mUsingMediaPlayer) { 504 // If the host view was attached, detach it. 505 ViewGroup parent = (ViewGroup) hostView.getParent(); 506 if (parent != null) { 507 parent.removeView(hostView); 508 } 509 return; 510 } 511 mMediaHostView = hostView; 512 ViewGroup newParent = horizontal ? mHorizontalLinearLayout : this; 513 ViewGroup currentParent = (ViewGroup) hostView.getParent(); 514 Log.d(getDumpableTag(), "Reattaching media host: " + horizontal 515 + ", current " + currentParent + ", new " + newParent); 516 if (currentParent != newParent) { 517 if (currentParent != null) { 518 currentParent.removeView(hostView); 519 } 520 newParent.addView(hostView); 521 LinearLayout.LayoutParams layoutParams = (LayoutParams) hostView.getLayoutParams(); 522 layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; 523 layoutParams.width = horizontal ? 0 : ViewGroup.LayoutParams.MATCH_PARENT; 524 layoutParams.weight = horizontal ? 1f : 0; 525 // Add any bottom margin, such that the total spacing is correct. This is only 526 // necessary if the view isn't horizontal, since otherwise the padding is 527 // carried in the parent of this view (to ensure correct vertical alignment) 528 layoutParams.bottomMargin = !horizontal || displayMediaMarginsOnMedia() 529 ? Math.max(mMediaTotalBottomMargin - getPaddingBottom(), 0) : 0; 530 layoutParams.topMargin = mediaNeedsTopMargin() && !horizontal 531 ? mMediaTopMargin : 0; 532 // Call setLayoutParams explicitly to ensure that requestLayout happens 533 hostView.setLayoutParams(layoutParams); 534 } 535 } 536 setExpanded(boolean expanded)537 public void setExpanded(boolean expanded) { 538 if (mExpanded == expanded) return; 539 mExpanded = expanded; 540 if (!mExpanded && mTileLayout instanceof PagedTileLayout tilesLayout) { 541 // Use post, so it will wait until the view is attached. If the view is not attached, 542 // it will not populate corresponding views (and will not do it later when attached). 543 tilesLayout.post(() -> tilesLayout.setCurrentItem(0, false)); 544 } 545 } 546 setPageListener(final PagedTileLayout.PageListener pageListener)547 public void setPageListener(final PagedTileLayout.PageListener pageListener) { 548 if (mTileLayout instanceof PagedTileLayout) { 549 ((PagedTileLayout) mTileLayout).setPageListener(pageListener); 550 } 551 } 552 isExpanded()553 public boolean isExpanded() { 554 return mExpanded; 555 } 556 557 /** */ setListening(boolean listening)558 public void setListening(boolean listening) { 559 mListening = listening; 560 } 561 drawTile(QSPanelControllerBase.TileRecord r, QSTile.State state)562 protected void drawTile(QSPanelControllerBase.TileRecord r, QSTile.State state) { 563 r.tileView.onStateChanged(state); 564 } 565 openPanelEvent()566 protected QSEvent openPanelEvent() { 567 return QSEvent.QS_PANEL_EXPANDED; 568 } 569 closePanelEvent()570 protected QSEvent closePanelEvent() { 571 return QSEvent.QS_PANEL_COLLAPSED; 572 } 573 tileVisibleEvent()574 protected QSEvent tileVisibleEvent() { 575 return QSEvent.QS_TILE_VISIBLE; 576 } 577 shouldShowDetail()578 protected boolean shouldShowDetail() { 579 return mExpanded; 580 } 581 addTile(QSPanelControllerBase.TileRecord tileRecord)582 final void addTile(QSPanelControllerBase.TileRecord tileRecord) { 583 final QSTile.Callback callback = new QSTile.Callback() { 584 @Override 585 public void onStateChanged(QSTile.State state) { 586 drawTile(tileRecord, state); 587 } 588 }; 589 590 tileRecord.tile.addCallback(callback); 591 tileRecord.callback = callback; 592 tileRecord.tileView.init(tileRecord.tile); 593 tileRecord.tile.refreshState(); 594 595 if (mTileLayout != null) { 596 mTileLayout.addTile(tileRecord); 597 } 598 } 599 removeTile(QSPanelControllerBase.TileRecord tileRecord)600 void removeTile(QSPanelControllerBase.TileRecord tileRecord) { 601 mTileLayout.removeTile(tileRecord); 602 } 603 getGridHeight()604 public int getGridHeight() { 605 return getMeasuredHeight(); 606 } 607 608 @Nullable getTileLayout()609 QSTileLayout getTileLayout() { 610 return mTileLayout; 611 } 612 613 /** */ setContentMargins(int startMargin, int endMargin, ViewGroup mediaHostView)614 public void setContentMargins(int startMargin, int endMargin, ViewGroup mediaHostView) { 615 // Only some views actually want this content padding, others want to go all the way 616 // to the edge like the brightness slider 617 mContentMarginStart = startMargin; 618 mContentMarginEnd = endMargin; 619 updateMediaHostContentMargins(mediaHostView); 620 } 621 622 /** 623 * Update the margins of the media hosts 624 */ updateMediaHostContentMargins(ViewGroup mediaHostView)625 protected void updateMediaHostContentMargins(ViewGroup mediaHostView) { 626 if (mUsingMediaPlayer) { 627 int marginStart = 0; 628 int marginEnd = 0; 629 if (mUsingHorizontalLayout) { 630 marginEnd = mContentMarginEnd; 631 } 632 updateMargins(mediaHostView, marginStart, marginEnd); 633 } 634 } 635 636 /** 637 * Update the margins of a view. 638 * 639 * @param view the view to adjust 640 * @param start the start margin to set 641 * @param end the end margin to set 642 */ updateMargins(View view, int start, int end)643 protected void updateMargins(View view, int start, int end) { 644 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 645 if (lp != null) { 646 lp.setMarginStart(start); 647 lp.setMarginEnd(end); 648 view.setLayoutParams(lp); 649 } 650 } 651 isListening()652 public boolean isListening() { 653 return mListening; 654 } 655 setPageMargin(int pageMarginStart, int pageMarginEnd)656 protected void setPageMargin(int pageMarginStart, int pageMarginEnd) { 657 if (mTileLayout instanceof PagedTileLayout) { 658 ((PagedTileLayout) mTileLayout).setPageMargin(pageMarginStart, pageMarginEnd); 659 } 660 } 661 setUsingHorizontalLayout(boolean horizontal, ViewGroup mediaHostView, boolean force)662 void setUsingHorizontalLayout(boolean horizontal, ViewGroup mediaHostView, boolean force) { 663 if (horizontal != mUsingHorizontalLayout || force) { 664 Log.d(getDumpableTag(), "setUsingHorizontalLayout: " + horizontal + ", " + force); 665 mUsingHorizontalLayout = horizontal; 666 // The tile layout should be reparented if horizontal and we are using media. If not 667 // using media, the parent should always be this. 668 ViewGroup newParent = 669 horizontal && mUsingMediaPlayer ? mHorizontalContentContainer : this; 670 if (SceneContainerFlag.isEnabled()) return; 671 switchAllContentToParent(newParent, mTileLayout); 672 reAttachMediaHost(mediaHostView, horizontal); 673 if (needsDynamicRowsAndColumns()) { 674 setColumnRowLayout(horizontal); 675 } 676 updateMargins(mediaHostView); 677 if (mHorizontalLinearLayout != null) { 678 mHorizontalLinearLayout.setVisibility(horizontal ? View.VISIBLE : View.GONE); 679 } 680 } 681 } 682 setColumnRowLayout(boolean withMedia)683 void setColumnRowLayout(boolean withMedia) { 684 mTileLayout.setMinRows(withMedia ? 2 : 1); 685 mTileLayout.setMaxColumns(withMedia ? 2 : 4); 686 placeTileLayoutForScene(withMedia); 687 } 688 placeTileLayoutForScene(boolean withMedia)689 protected void placeTileLayoutForScene(boolean withMedia) { 690 // The tile layout should be reparented if horizontal and we are using media. If not 691 // using media, the parent should always be this. 692 ViewGroup newParent = withMedia ? mHorizontalContentContainer : this; 693 if (mTileLayout != null && ((View) mTileLayout).getParent() != newParent) { 694 switchAllContentToParent(newParent, mTileLayout); 695 } 696 if (mHorizontalLinearLayout != null) { 697 mHorizontalLinearLayout.setVisibility(withMedia ? View.VISIBLE : View.GONE); 698 } 699 } 700 updateMargins(ViewGroup mediaHostView)701 private void updateMargins(ViewGroup mediaHostView) { 702 updateMediaHostContentMargins(mediaHostView); 703 updateHorizontalLinearLayoutMargins(); 704 updatePadding(); 705 } 706 707 /** 708 * Sets whether the media container should move during the expansion of the QS Panel. 709 * 710 * As the QS Panel expands and the QS unsquish, the views below the QS tiles move to adapt to 711 * the new height of the QS tiles. 712 * 713 * In some cases this might not be wanted for media. One example is when there is a transition 714 * animation of the media container happening on split shade lock screen. 715 */ setShouldMoveMediaOnExpansion(boolean shouldMoveMediaOnExpansion)716 public void setShouldMoveMediaOnExpansion(boolean shouldMoveMediaOnExpansion) { 717 mShouldMoveMediaOnExpansion = shouldMoveMediaOnExpansion; 718 } 719 720 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)721 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 722 super.onInitializeAccessibilityNodeInfo(info); 723 if (mCanCollapse) { 724 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE); 725 } 726 } 727 728 @Override performAccessibilityAction(int action, Bundle arguments)729 public boolean performAccessibilityAction(int action, Bundle arguments) { 730 if (action == AccessibilityNodeInfo.ACTION_EXPAND 731 || action == AccessibilityNodeInfo.ACTION_COLLAPSE) { 732 if (mCollapseExpandAction != null) { 733 mCollapseExpandAction.run(); 734 return true; 735 } 736 } 737 return super.performAccessibilityAction(action, arguments); 738 } 739 setCollapseExpandAction(Runnable action)740 public void setCollapseExpandAction(Runnable action) { 741 mCollapseExpandAction = action; 742 } 743 744 /** 745 * Specifies if these expanded QS can collapse to QQS. 746 */ setCanCollapse(boolean canCollapse)747 public void setCanCollapse(boolean canCollapse) { 748 mCanCollapse = canCollapse; 749 } 750 751 /** 752 * @return height with the {@link QSPanel#setSquishinessFraction(float)} applied. 753 */ getSquishedHeight()754 public int getSquishedHeight() { 755 if (mFooter != null) { 756 final ViewGroup.LayoutParams footerLayoutParams = mFooter.getLayoutParams(); 757 final int footerBottomMargin; 758 if (footerLayoutParams instanceof MarginLayoutParams) { 759 footerBottomMargin = ((MarginLayoutParams) footerLayoutParams).bottomMargin; 760 } else { 761 footerBottomMargin = 0; 762 } 763 // This is the distance between the top of the QSPanel and the last view in the 764 // layout (which is the effective the bottom) 765 return mFooter.getBottom() + footerBottomMargin - getTop(); 766 } 767 if (mTileLayout != null) { 768 // Footer absence means that the panel is in the QQS. In this case it's just height 769 // of the tiles + paddings. 770 return mTileLayout.getTilesHeight() + getPaddingBottom() + getPaddingTop(); 771 } 772 return getHeight(); 773 } 774 775 @Nullable 776 @VisibleForTesting getMediaPlaceholder()777 View getMediaPlaceholder() { 778 return mMediaViewPlaceHolderForScene; 779 } 780 781 public interface QSTileLayout { 782 /** */ saveInstanceState(Bundle outState)783 default void saveInstanceState(Bundle outState) {} 784 785 /** */ restoreInstanceState(Bundle savedInstanceState)786 default void restoreInstanceState(Bundle savedInstanceState) {} 787 788 /** */ addTile(QSPanelControllerBase.TileRecord tile)789 void addTile(QSPanelControllerBase.TileRecord tile); 790 791 /** */ removeTile(QSPanelControllerBase.TileRecord tile)792 void removeTile(QSPanelControllerBase.TileRecord tile); 793 794 /** */ getOffsetTop(QSPanelControllerBase.TileRecord tile)795 int getOffsetTop(QSPanelControllerBase.TileRecord tile); 796 797 /** */ updateResources()798 boolean updateResources(); 799 800 /** */ setListening(boolean listening, UiEventLogger uiEventLogger)801 void setListening(boolean listening, UiEventLogger uiEventLogger); 802 803 /** */ getHeight()804 int getHeight(); 805 806 /** */ getTilesHeight()807 int getTilesHeight(); 808 809 /** 810 * Sets a size modifier for the tile. Where 0 means collapsed, and 1 expanded. 811 */ setSquishinessFraction(float squishinessFraction)812 void setSquishinessFraction(float squishinessFraction); 813 814 /** 815 * Sets the minimum number of rows to show 816 * 817 * @param minRows the minimum. 818 */ setMinRows(int minRows)819 default boolean setMinRows(int minRows) { 820 return false; 821 } 822 getMinRows()823 int getMinRows(); 824 825 /** 826 * Sets the max number of columns to show 827 * 828 * @param maxColumns the maximum 829 * 830 * @return true if the number of visible columns has changed. 831 */ setMaxColumns(int maxColumns)832 default boolean setMaxColumns(int maxColumns) { 833 return false; 834 } 835 getMaxColumns()836 int getMaxColumns(); 837 838 /** 839 * Sets the expansion value and proposedTranslation to panel. 840 */ setExpansion(float expansion, float proposedTranslation)841 default void setExpansion(float expansion, float proposedTranslation) {} 842 getNumVisibleTiles()843 int getNumVisibleTiles(); 844 setLogger(QSLogger qsLogger)845 default void setLogger(QSLogger qsLogger) { } 846 } 847 848 interface OnConfigurationChangedListener { onConfigurationChange(Configuration newConfig)849 void onConfigurationChange(Configuration newConfig); 850 } 851 852 @VisibleForTesting switchToParent(View child, ViewGroup parent, int index, String tag)853 static void switchToParent(View child, ViewGroup parent, int index, String tag) { 854 if (parent == null) { 855 Log.w(tag, "Trying to move view to null parent", 856 new IllegalStateException()); 857 return; 858 } 859 ViewGroup currentParent = (ViewGroup) child.getParent(); 860 if (currentParent != parent) { 861 if (currentParent != null) { 862 currentParent.removeView(child); 863 } 864 parent.addView(child, index); 865 return; 866 } 867 // Same parent, we are just changing indices 868 int currentIndex = parent.indexOfChild(child); 869 if (currentIndex == index) { 870 // We want to be in the same place. Nothing to do here 871 return; 872 } 873 parent.removeView(child); 874 parent.addView(child, index); 875 } 876 } 877