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