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.os.Bundle; 27 import android.os.Handler; 28 import android.os.Message; 29 import android.util.ArrayMap; 30 import android.util.AttributeSet; 31 import android.util.Log; 32 import android.view.Gravity; 33 import android.view.LayoutInflater; 34 import android.view.View; 35 import android.view.ViewGroup; 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.DetailAdapter; 44 import com.android.systemui.plugins.qs.QSTile; 45 import com.android.systemui.settings.brightness.BrightnessSliderController; 46 import com.android.systemui.statusbar.policy.BrightnessMirrorController; 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 /** 66 * The index where the content starts that needs to be moved between parents 67 */ 68 private int mMovableContentStartIndex; 69 70 @Nullable 71 protected View mBrightnessView; 72 @Nullable 73 protected BrightnessSliderController mToggleSliderController; 74 75 private final H mHandler = new H(); 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 QSDetail.Callback mCallback; 83 protected QSTileHost mHost; 84 private final List<OnConfigurationChangedListener> mOnConfigurationChangedListeners = 85 new ArrayList<>(); 86 87 @Nullable 88 protected View mSecurityFooter; 89 90 @Nullable 91 protected View mFooter; 92 93 @Nullable 94 private ViewGroup mHeaderContainer; 95 private PageIndicator mFooterPageIndicator; 96 private int mContentMarginStart; 97 private int mContentMarginEnd; 98 private boolean mUsingHorizontalLayout; 99 100 private Record mDetailRecord; 101 102 private BrightnessMirrorController mBrightnessMirrorController; 103 private LinearLayout mHorizontalLinearLayout; 104 protected LinearLayout mHorizontalContentContainer; 105 106 protected QSTileLayout mTileLayout; 107 private float mSquishinessFraction = 1f; 108 private final ArrayMap<View, Integer> mChildrenLayoutTop = new ArrayMap<>(); 109 QSPanel(Context context, AttributeSet attrs)110 public QSPanel(Context context, AttributeSet attrs) { 111 super(context, attrs); 112 mUsingMediaPlayer = useQsMediaPlayer(context); 113 mMediaTotalBottomMargin = getResources().getDimensionPixelSize( 114 R.dimen.quick_settings_bottom_margin_media); 115 mMediaTopMargin = getResources().getDimensionPixelSize( 116 R.dimen.qs_tile_margin_vertical); 117 mContext = context; 118 119 setOrientation(VERTICAL); 120 121 mMovableContentStartIndex = getChildCount(); 122 123 } 124 initialize()125 void initialize() { 126 mTileLayout = getOrCreateTileLayout(); 127 128 if (mUsingMediaPlayer) { 129 mHorizontalLinearLayout = new RemeasuringLinearLayout(mContext); 130 mHorizontalLinearLayout.setOrientation(LinearLayout.HORIZONTAL); 131 mHorizontalLinearLayout.setClipChildren(false); 132 mHorizontalLinearLayout.setClipToPadding(false); 133 134 mHorizontalContentContainer = new RemeasuringLinearLayout(mContext); 135 mHorizontalContentContainer.setOrientation(LinearLayout.VERTICAL); 136 mHorizontalContentContainer.setClipChildren(true); 137 mHorizontalContentContainer.setClipToPadding(false); 138 139 LayoutParams lp = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1); 140 int marginSize = (int) mContext.getResources().getDimension(R.dimen.qs_media_padding); 141 lp.setMarginStart(0); 142 lp.setMarginEnd(marginSize); 143 lp.gravity = Gravity.CENTER_VERTICAL; 144 mHorizontalLinearLayout.addView(mHorizontalContentContainer, lp); 145 146 lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0, 1); 147 addView(mHorizontalLinearLayout, lp); 148 } 149 } 150 151 /** 152 * Add brightness view above the tile layout. 153 * 154 * Used to add the brightness slider after construction. 155 */ setBrightnessView(@onNull View view)156 public void setBrightnessView(@NonNull View view) { 157 if (mBrightnessView != null) { 158 removeView(mBrightnessView); 159 mMovableContentStartIndex--; 160 } 161 addView(view, 0); 162 mBrightnessView = view; 163 164 setBrightnessViewMargin(); 165 166 mMovableContentStartIndex++; 167 } 168 setBrightnessViewMargin()169 private void setBrightnessViewMargin() { 170 if (mBrightnessView != null) { 171 MarginLayoutParams lp = (MarginLayoutParams) mBrightnessView.getLayoutParams(); 172 lp.topMargin = mContext.getResources() 173 .getDimensionPixelSize(R.dimen.qs_brightness_margin_top); 174 lp.bottomMargin = mContext.getResources() 175 .getDimensionPixelSize(R.dimen.qs_brightness_margin_bottom); 176 mBrightnessView.setLayoutParams(lp); 177 } 178 } 179 180 /** */ getOrCreateTileLayout()181 public QSTileLayout getOrCreateTileLayout() { 182 if (mTileLayout == null) { 183 mTileLayout = (QSTileLayout) LayoutInflater.from(mContext) 184 .inflate(R.layout.qs_paged_tile_layout, this, false); 185 mTileLayout.setSquishinessFraction(mSquishinessFraction); 186 } 187 return mTileLayout; 188 } 189 setSquishinessFraction(float squishinessFraction)190 public void setSquishinessFraction(float squishinessFraction) { 191 if (Float.compare(squishinessFraction, mSquishinessFraction) == 0) { 192 return; 193 } 194 mSquishinessFraction = squishinessFraction; 195 if (mTileLayout == null) { 196 return; 197 } 198 mTileLayout.setSquishinessFraction(squishinessFraction); 199 if (getMeasuredWidth() == 0) { 200 return; 201 } 202 updateViewPositions(); 203 } 204 205 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)206 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 207 if (mTileLayout instanceof PagedTileLayout) { 208 // Since PageIndicator gets measured before PagedTileLayout, we preemptively set the 209 // # of pages before the measurement pass so PageIndicator is measured appropriately 210 if (mFooterPageIndicator != null) { 211 mFooterPageIndicator.setNumPages(((PagedTileLayout) mTileLayout).getNumPages()); 212 } 213 214 // In landscape, mTileLayout's parent is not the panel but a view that contains the 215 // tile layout and the media controls. 216 if (((View) mTileLayout).getParent() == this) { 217 // Allow the UI to be as big as it want's to, we're in a scroll view 218 int newHeight = 10000; 219 int availableHeight = MeasureSpec.getSize(heightMeasureSpec); 220 int excessHeight = newHeight - availableHeight; 221 // Measure with EXACTLY. That way, The content will only use excess height and will 222 // be measured last, after other views and padding is accounted for. This only 223 // works because our Layouts in here remeasure themselves with the exact content 224 // height. 225 heightMeasureSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY); 226 ((PagedTileLayout) mTileLayout).setExcessHeight(excessHeight); 227 } 228 } 229 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 230 231 // We want all the logic of LinearLayout#onMeasure, and for it to assign the excess space 232 // not used by the other children to PagedTileLayout. However, in this case, LinearLayout 233 // assumes that PagedTileLayout would use all the excess space. This is not the case as 234 // PagedTileLayout height is quantized (because it shows a certain number of rows). 235 // Therefore, after everything is measured, we need to make sure that we add up the correct 236 // total height 237 int height = getPaddingBottom() + getPaddingTop(); 238 int numChildren = getChildCount(); 239 for (int i = 0; i < numChildren; i++) { 240 View child = getChildAt(i); 241 if (child.getVisibility() != View.GONE) { 242 height += child.getMeasuredHeight(); 243 MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams(); 244 height += layoutParams.topMargin + layoutParams.bottomMargin; 245 } 246 } 247 setMeasuredDimension(getMeasuredWidth(), height); 248 } 249 250 @Override onLayout(boolean changed, int l, int t, int r, int b)251 protected void onLayout(boolean changed, int l, int t, int r, int b) { 252 super.onLayout(changed, l, t, r, b); 253 for (int i = 0; i < getChildCount(); i++) { 254 View child = getChildAt(i); 255 mChildrenLayoutTop.put(child, child.getTop()); 256 } 257 updateViewPositions(); 258 } 259 updateViewPositions()260 private void updateViewPositions() { 261 // Adjust view positions based on tile squishing 262 int tileHeightOffset = mTileLayout.getTilesHeight() - mTileLayout.getHeight(); 263 264 boolean move = false; 265 for (int i = 0; i < getChildCount(); i++) { 266 View child = getChildAt(i); 267 if (move) { 268 int top = mChildrenLayoutTop.get(child); 269 child.setLeftTopRightBottom(child.getLeft(), top + tileHeightOffset, 270 child.getRight(), top + tileHeightOffset + child.getHeight()); 271 } 272 if (child == mTileLayout) { 273 move = true; 274 } 275 } 276 } 277 getDumpableTag()278 protected String getDumpableTag() { 279 return TAG; 280 } 281 282 @Override onTuningChanged(String key, String newValue)283 public void onTuningChanged(String key, String newValue) { 284 if (QS_SHOW_BRIGHTNESS.equals(key) && mBrightnessView != null) { 285 updateViewVisibilityForTuningValue(mBrightnessView, newValue); 286 } 287 } 288 updateViewVisibilityForTuningValue(View view, @Nullable String newValue)289 private void updateViewVisibilityForTuningValue(View view, @Nullable String newValue) { 290 view.setVisibility(TunerService.parseIntegerSwitch(newValue, true) ? VISIBLE : GONE); 291 } 292 293 /** */ openDetails(QSTile tile)294 public void openDetails(QSTile tile) { 295 // If there's no tile with that name (as defined in QSFactoryImpl or other QSFactory), 296 // QSFactory will not be able to create a tile and getTile will return null 297 if (tile != null) { 298 showDetailAdapter(true, tile.getDetailAdapter(), new int[]{getWidth() / 2, 0}); 299 } 300 } 301 302 @Nullable getBrightnessView()303 View getBrightnessView() { 304 return mBrightnessView; 305 } 306 setCallback(QSDetail.Callback callback)307 public void setCallback(QSDetail.Callback callback) { 308 mCallback = callback; 309 } 310 311 /** 312 * Links the footer's page indicator, which is used in landscape orientation to save space. 313 * 314 * @param pageIndicator indicator to use for page scrolling 315 */ setFooterPageIndicator(PageIndicator pageIndicator)316 public void setFooterPageIndicator(PageIndicator pageIndicator) { 317 if (mTileLayout instanceof PagedTileLayout) { 318 mFooterPageIndicator = pageIndicator; 319 updatePageIndicator(); 320 } 321 } 322 updatePageIndicator()323 private void updatePageIndicator() { 324 if (mTileLayout instanceof PagedTileLayout) { 325 if (mFooterPageIndicator != null) { 326 mFooterPageIndicator.setVisibility(View.GONE); 327 328 ((PagedTileLayout) mTileLayout).setPageIndicator(mFooterPageIndicator); 329 } 330 } 331 } 332 getHost()333 public QSTileHost getHost() { 334 return mHost; 335 } 336 updateResources()337 public void updateResources() { 338 updatePadding(); 339 340 updatePageIndicator(); 341 342 setBrightnessViewMargin(); 343 344 if (mTileLayout != null) { 345 mTileLayout.updateResources(); 346 } 347 } 348 updatePadding()349 protected void updatePadding() { 350 final Resources res = mContext.getResources(); 351 int padding = res.getDimensionPixelSize(R.dimen.qs_panel_padding_top); 352 setPaddingRelative(getPaddingStart(), 353 padding, 354 getPaddingEnd(), 355 res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom)); 356 } 357 addOnConfigurationChangedListener(OnConfigurationChangedListener listener)358 void addOnConfigurationChangedListener(OnConfigurationChangedListener listener) { 359 mOnConfigurationChangedListeners.add(listener); 360 } 361 removeOnConfigurationChangedListener(OnConfigurationChangedListener listener)362 void removeOnConfigurationChangedListener(OnConfigurationChangedListener listener) { 363 mOnConfigurationChangedListeners.remove(listener); 364 } 365 366 @Override onConfigurationChanged(Configuration newConfig)367 protected void onConfigurationChanged(Configuration newConfig) { 368 super.onConfigurationChanged(newConfig); 369 mOnConfigurationChangedListeners.forEach( 370 listener -> listener.onConfigurationChange(newConfig)); 371 } 372 373 @Override onFinishInflate()374 protected void onFinishInflate() { 375 super.onFinishInflate(); 376 mFooter = findViewById(R.id.qs_footer); 377 } 378 updateHorizontalLinearLayoutMargins()379 private void updateHorizontalLinearLayoutMargins() { 380 if (mHorizontalLinearLayout != null && !displayMediaMarginsOnMedia()) { 381 LayoutParams lp = (LayoutParams) mHorizontalLinearLayout.getLayoutParams(); 382 lp.bottomMargin = Math.max(mMediaTotalBottomMargin - getPaddingBottom(), 0); 383 mHorizontalLinearLayout.setLayoutParams(lp); 384 } 385 } 386 387 /** 388 * @return true if the margin bottom of the media view should be on the media host or false 389 * if they should be on the HorizontalLinearLayout. Returning {@code false} is useful 390 * to visually center the tiles in the Media view, which doesn't work when the 391 * expanded panel actually scrolls. 392 */ displayMediaMarginsOnMedia()393 protected boolean displayMediaMarginsOnMedia() { 394 return true; 395 } 396 397 /** 398 * @return true if the media view needs margin on the top to separate it from the qs tiles 399 */ mediaNeedsTopMargin()400 protected boolean mediaNeedsTopMargin() { 401 return false; 402 } 403 needsDynamicRowsAndColumns()404 private boolean needsDynamicRowsAndColumns() { 405 return true; 406 } 407 switchAllContentToParent(ViewGroup parent, QSTileLayout newLayout)408 private void switchAllContentToParent(ViewGroup parent, QSTileLayout newLayout) { 409 int index = parent == this ? mMovableContentStartIndex : 0; 410 411 // Let's first move the tileLayout to the new parent, since that should come first. 412 switchToParent((View) newLayout, parent, index); 413 index++; 414 415 if (mFooter != null) { 416 // Then the footer with the settings 417 switchToParent(mFooter, parent, index); 418 index++; 419 } 420 } 421 422 /** Switch the security footer between top and bottom of QS depending on orientation. */ switchSecurityFooter(boolean shouldUseSplitNotificationShade)423 public void switchSecurityFooter(boolean shouldUseSplitNotificationShade) { 424 if (mSecurityFooter == null) return; 425 426 if (!shouldUseSplitNotificationShade 427 && mContext.getResources().getConfiguration().orientation 428 == Configuration.ORIENTATION_LANDSCAPE && mHeaderContainer != null) { 429 // Adding the security view to the header, that enables us to avoid scrolling 430 switchToParent(mSecurityFooter, mHeaderContainer, 0); 431 } else { 432 // Add after the footer 433 int index = indexOfChild(mFooter); 434 switchToParent(mSecurityFooter, this, index + 1); 435 } 436 } 437 switchToParent(View child, ViewGroup parent, int index)438 private void switchToParent(View child, ViewGroup parent, int index) { 439 switchToParent(child, parent, index, getDumpableTag()); 440 } 441 442 /** Call when orientation has changed and MediaHost needs to be adjusted. */ reAttachMediaHost(ViewGroup hostView, boolean horizontal)443 private void reAttachMediaHost(ViewGroup hostView, boolean horizontal) { 444 if (!mUsingMediaPlayer) { 445 return; 446 } 447 ViewGroup newParent = horizontal ? mHorizontalLinearLayout : this; 448 ViewGroup currentParent = (ViewGroup) hostView.getParent(); 449 if (currentParent != newParent) { 450 if (currentParent != null) { 451 currentParent.removeView(hostView); 452 } 453 newParent.addView(hostView); 454 LinearLayout.LayoutParams layoutParams = (LayoutParams) hostView.getLayoutParams(); 455 layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; 456 layoutParams.width = horizontal ? 0 : ViewGroup.LayoutParams.MATCH_PARENT; 457 layoutParams.weight = horizontal ? 1f : 0; 458 // Add any bottom margin, such that the total spacing is correct. This is only 459 // necessary if the view isn't horizontal, since otherwise the padding is 460 // carried in the parent of this view (to ensure correct vertical alignment) 461 layoutParams.bottomMargin = !horizontal || displayMediaMarginsOnMedia() 462 ? Math.max(mMediaTotalBottomMargin - getPaddingBottom(), 0) : 0; 463 layoutParams.topMargin = mediaNeedsTopMargin() && !horizontal 464 ? mMediaTopMargin : 0; 465 } 466 } 467 setExpanded(boolean expanded)468 public void setExpanded(boolean expanded) { 469 if (mExpanded == expanded) return; 470 mExpanded = expanded; 471 if (!mExpanded && mTileLayout instanceof PagedTileLayout) { 472 ((PagedTileLayout) mTileLayout).setCurrentItem(0, false); 473 } 474 } 475 setPageListener(final PagedTileLayout.PageListener pageListener)476 public void setPageListener(final PagedTileLayout.PageListener pageListener) { 477 if (mTileLayout instanceof PagedTileLayout) { 478 ((PagedTileLayout) mTileLayout).setPageListener(pageListener); 479 } 480 } 481 isExpanded()482 public boolean isExpanded() { 483 return mExpanded; 484 } 485 486 /** */ setListening(boolean listening)487 public void setListening(boolean listening) { 488 mListening = listening; 489 } 490 showDetailAdapter(boolean show, DetailAdapter adapter, int[] locationInWindow)491 public void showDetailAdapter(boolean show, DetailAdapter adapter, int[] locationInWindow) { 492 int xInWindow = locationInWindow[0]; 493 int yInWindow = locationInWindow[1]; 494 ((View) getParent()).getLocationInWindow(locationInWindow); 495 496 Record r = new Record(); 497 r.detailAdapter = adapter; 498 r.x = xInWindow - locationInWindow[0]; 499 r.y = yInWindow - locationInWindow[1]; 500 501 locationInWindow[0] = xInWindow; 502 locationInWindow[1] = yInWindow; 503 504 showDetail(show, r); 505 } 506 showDetail(boolean show, Record r)507 protected void showDetail(boolean show, Record r) { 508 mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0, r).sendToTarget(); 509 } 510 drawTile(QSPanelControllerBase.TileRecord r, QSTile.State state)511 protected void drawTile(QSPanelControllerBase.TileRecord r, QSTile.State state) { 512 r.tileView.onStateChanged(state); 513 } 514 openPanelEvent()515 protected QSEvent openPanelEvent() { 516 return QSEvent.QS_PANEL_EXPANDED; 517 } 518 closePanelEvent()519 protected QSEvent closePanelEvent() { 520 return QSEvent.QS_PANEL_COLLAPSED; 521 } 522 tileVisibleEvent()523 protected QSEvent tileVisibleEvent() { 524 return QSEvent.QS_TILE_VISIBLE; 525 } 526 shouldShowDetail()527 protected boolean shouldShowDetail() { 528 return mExpanded; 529 } 530 addTile(QSPanelControllerBase.TileRecord tileRecord)531 void addTile(QSPanelControllerBase.TileRecord tileRecord) { 532 final QSTile.Callback callback = new QSTile.Callback() { 533 @Override 534 public void onStateChanged(QSTile.State state) { 535 drawTile(tileRecord, state); 536 } 537 538 @Override 539 public void onShowDetail(boolean show) { 540 // Both the collapsed and full QS panels get this callback, this check determines 541 // which one should handle showing the detail. 542 if (shouldShowDetail()) { 543 QSPanel.this.showDetail(show, tileRecord); 544 } 545 } 546 547 @Override 548 public void onToggleStateChanged(boolean state) { 549 if (mDetailRecord == tileRecord) { 550 fireToggleStateChanged(state); 551 } 552 } 553 554 @Override 555 public void onScanStateChanged(boolean state) { 556 tileRecord.scanState = state; 557 if (mDetailRecord == tileRecord) { 558 fireScanStateChanged(tileRecord.scanState); 559 } 560 } 561 562 @Override 563 public void onAnnouncementRequested(CharSequence announcement) { 564 if (announcement != null) { 565 mHandler.obtainMessage(H.ANNOUNCE_FOR_ACCESSIBILITY, announcement) 566 .sendToTarget(); 567 } 568 } 569 }; 570 571 tileRecord.tile.addCallback(callback); 572 tileRecord.callback = callback; 573 tileRecord.tileView.init(tileRecord.tile); 574 tileRecord.tile.refreshState(); 575 576 if (mTileLayout != null) { 577 mTileLayout.addTile(tileRecord); 578 } 579 } 580 removeTile(QSPanelControllerBase.TileRecord tileRecord)581 void removeTile(QSPanelControllerBase.TileRecord tileRecord) { 582 mTileLayout.removeTile(tileRecord); 583 } 584 closeDetail()585 void closeDetail() { 586 showDetail(false, mDetailRecord); 587 } 588 getGridHeight()589 public int getGridHeight() { 590 return getMeasuredHeight(); 591 } 592 handleShowDetail(Record r, boolean show)593 protected void handleShowDetail(Record r, boolean show) { 594 if (r instanceof QSPanelControllerBase.TileRecord) { 595 handleShowDetailTile((QSPanelControllerBase.TileRecord) r, show); 596 } else { 597 int x = 0; 598 int y = 0; 599 if (r != null) { 600 x = r.x; 601 y = r.y; 602 } 603 handleShowDetailImpl(r, show, x, y); 604 } 605 } 606 handleShowDetailTile(QSPanelControllerBase.TileRecord r, boolean show)607 private void handleShowDetailTile(QSPanelControllerBase.TileRecord r, boolean show) { 608 if ((mDetailRecord != null) == show && mDetailRecord == r) return; 609 610 if (show) { 611 r.detailAdapter = r.tile.getDetailAdapter(); 612 if (r.detailAdapter == null) return; 613 } 614 r.tile.setDetailListening(show); 615 int x = r.tileView.getLeft() + r.tileView.getWidth() / 2; 616 int y = r.tileView.getDetailY() + mTileLayout.getOffsetTop(r) + getTop(); 617 handleShowDetailImpl(r, show, x, y); 618 } 619 handleShowDetailImpl(Record r, boolean show, int x, int y)620 private void handleShowDetailImpl(Record r, boolean show, int x, int y) { 621 setDetailRecord(show ? r : null); 622 fireShowingDetail(show ? r.detailAdapter : null, x, y); 623 } 624 setDetailRecord(Record r)625 protected void setDetailRecord(Record r) { 626 if (r == mDetailRecord) return; 627 mDetailRecord = r; 628 final boolean scanState = mDetailRecord instanceof QSPanelControllerBase.TileRecord 629 && ((QSPanelControllerBase.TileRecord) mDetailRecord).scanState; 630 fireScanStateChanged(scanState); 631 } 632 fireShowingDetail(DetailAdapter detail, int x, int y)633 private void fireShowingDetail(DetailAdapter detail, int x, int y) { 634 if (mCallback != null) { 635 mCallback.onShowingDetail(detail, x, y); 636 } 637 } 638 fireToggleStateChanged(boolean state)639 private void fireToggleStateChanged(boolean state) { 640 if (mCallback != null) { 641 mCallback.onToggleStateChanged(state); 642 } 643 } 644 fireScanStateChanged(boolean state)645 private void fireScanStateChanged(boolean state) { 646 if (mCallback != null) { 647 mCallback.onScanStateChanged(state); 648 } 649 } 650 getTileLayout()651 QSTileLayout getTileLayout() { 652 return mTileLayout; 653 } 654 655 /** */ setContentMargins(int startMargin, int endMargin, ViewGroup mediaHostView)656 public void setContentMargins(int startMargin, int endMargin, ViewGroup mediaHostView) { 657 // Only some views actually want this content padding, others want to go all the way 658 // to the edge like the brightness slider 659 mContentMarginStart = startMargin; 660 mContentMarginEnd = endMargin; 661 updateMediaHostContentMargins(mediaHostView); 662 } 663 664 /** 665 * Update the margins of the media hosts 666 */ updateMediaHostContentMargins(ViewGroup mediaHostView)667 protected void updateMediaHostContentMargins(ViewGroup mediaHostView) { 668 if (mUsingMediaPlayer) { 669 int marginStart = 0; 670 int marginEnd = 0; 671 if (mUsingHorizontalLayout) { 672 marginEnd = mContentMarginEnd; 673 } 674 updateMargins(mediaHostView, marginStart, marginEnd); 675 } 676 } 677 678 /** 679 * Update the margins of a view. 680 * 681 * @param view the view to adjust 682 * @param start the start margin to set 683 * @param end the end margin to set 684 */ updateMargins(View view, int start, int end)685 protected void updateMargins(View view, int start, int end) { 686 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 687 if (lp != null) { 688 lp.setMarginStart(start); 689 lp.setMarginEnd(end); 690 view.setLayoutParams(lp); 691 } 692 } 693 694 /** 695 * Set the header container of quick settings. 696 */ setHeaderContainer(@onNull ViewGroup headerContainer)697 public void setHeaderContainer(@NonNull ViewGroup headerContainer) { 698 mHeaderContainer = headerContainer; 699 } 700 isListening()701 public boolean isListening() { 702 return mListening; 703 } 704 705 /** 706 * Set the security footer view and switch it into the right place 707 * @param view the view in question 708 * @param shouldUseSplitNotificationShade if QS is in split shade mode 709 */ setSecurityFooter(View view, boolean shouldUseSplitNotificationShade)710 public void setSecurityFooter(View view, boolean shouldUseSplitNotificationShade) { 711 mSecurityFooter = view; 712 switchSecurityFooter(shouldUseSplitNotificationShade); 713 } 714 setPageMargin(int pageMargin)715 protected void setPageMargin(int pageMargin) { 716 if (mTileLayout instanceof PagedTileLayout) { 717 ((PagedTileLayout) mTileLayout).setPageMargin(pageMargin); 718 } 719 } 720 setUsingHorizontalLayout(boolean horizontal, ViewGroup mediaHostView, boolean force)721 void setUsingHorizontalLayout(boolean horizontal, ViewGroup mediaHostView, boolean force) { 722 if (horizontal != mUsingHorizontalLayout || force) { 723 mUsingHorizontalLayout = horizontal; 724 ViewGroup newParent = horizontal ? mHorizontalContentContainer : this; 725 switchAllContentToParent(newParent, mTileLayout); 726 reAttachMediaHost(mediaHostView, horizontal); 727 if (needsDynamicRowsAndColumns()) { 728 mTileLayout.setMinRows(horizontal ? 2 : 1); 729 mTileLayout.setMaxColumns(horizontal ? 2 : 4); 730 } 731 updateMargins(mediaHostView); 732 mHorizontalLinearLayout.setVisibility(horizontal ? View.VISIBLE : View.GONE); 733 } 734 } 735 updateMargins(ViewGroup mediaHostView)736 private void updateMargins(ViewGroup mediaHostView) { 737 updateMediaHostContentMargins(mediaHostView); 738 updateHorizontalLinearLayoutMargins(); 739 updatePadding(); 740 } 741 742 private class H extends Handler { 743 private static final int SHOW_DETAIL = 1; 744 private static final int SET_TILE_VISIBILITY = 2; 745 private static final int ANNOUNCE_FOR_ACCESSIBILITY = 3; 746 747 @Override handleMessage(Message msg)748 public void handleMessage(Message msg) { 749 if (msg.what == SHOW_DETAIL) { 750 handleShowDetail((Record) msg.obj, msg.arg1 != 0); 751 } else if (msg.what == ANNOUNCE_FOR_ACCESSIBILITY) { 752 announceForAccessibility((CharSequence) msg.obj); 753 } 754 } 755 } 756 757 protected static class Record { 758 DetailAdapter detailAdapter; 759 int x; 760 int y; 761 } 762 763 public interface QSTileLayout { 764 /** */ saveInstanceState(Bundle outState)765 default void saveInstanceState(Bundle outState) {} 766 767 /** */ restoreInstanceState(Bundle savedInstanceState)768 default void restoreInstanceState(Bundle savedInstanceState) {} 769 770 /** */ addTile(QSPanelControllerBase.TileRecord tile)771 void addTile(QSPanelControllerBase.TileRecord tile); 772 773 /** */ removeTile(QSPanelControllerBase.TileRecord tile)774 void removeTile(QSPanelControllerBase.TileRecord tile); 775 776 /** */ getOffsetTop(QSPanelControllerBase.TileRecord tile)777 int getOffsetTop(QSPanelControllerBase.TileRecord tile); 778 779 /** */ updateResources()780 boolean updateResources(); 781 782 /** */ setListening(boolean listening, UiEventLogger uiEventLogger)783 void setListening(boolean listening, UiEventLogger uiEventLogger); 784 785 /** */ getHeight()786 int getHeight(); 787 788 /** */ getTilesHeight()789 int getTilesHeight(); 790 791 /** 792 * Sets a size modifier for the tile. Where 0 means collapsed, and 1 expanded. 793 */ setSquishinessFraction(float squishinessFraction)794 void setSquishinessFraction(float squishinessFraction); 795 796 /** 797 * Sets the minimum number of rows to show 798 * 799 * @param minRows the minimum. 800 */ setMinRows(int minRows)801 default boolean setMinRows(int minRows) { 802 return false; 803 } 804 805 /** 806 * Sets the max number of columns to show 807 * 808 * @param maxColumns the maximum 809 * 810 * @return true if the number of visible columns has changed. 811 */ setMaxColumns(int maxColumns)812 default boolean setMaxColumns(int maxColumns) { 813 return false; 814 } 815 816 /** 817 * Sets the expansion value and proposedTranslation to panel. 818 */ setExpansion(float expansion, float proposedTranslation)819 default void setExpansion(float expansion, float proposedTranslation) {} 820 getNumVisibleTiles()821 int getNumVisibleTiles(); 822 } 823 824 interface OnConfigurationChangedListener { onConfigurationChange(Configuration newConfig)825 void onConfigurationChange(Configuration newConfig); 826 } 827 828 @VisibleForTesting switchToParent(View child, ViewGroup parent, int index, String tag)829 static void switchToParent(View child, ViewGroup parent, int index, String tag) { 830 if (parent == null) { 831 Log.w(tag, "Trying to move view to null parent", 832 new IllegalStateException()); 833 return; 834 } 835 ViewGroup currentParent = (ViewGroup) child.getParent(); 836 if (currentParent != parent) { 837 if (currentParent != null) { 838 currentParent.removeView(child); 839 } 840 parent.addView(child, index); 841 return; 842 } 843 // Same parent, we are just changing indices 844 int currentIndex = parent.indexOfChild(child); 845 if (currentIndex == index) { 846 // We want to be in the same place. Nothing to do here 847 return; 848 } 849 parent.removeView(child); 850 parent.addView(child, index); 851 } 852 } 853