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