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.InjectionInflationController.VIEW_CONTEXT; 20 import static com.android.systemui.util.Utils.useQsMediaPlayer; 21 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.res.Configuration; 27 import android.content.res.Resources; 28 import android.graphics.PointF; 29 import android.metrics.LogMaker; 30 import android.os.Bundle; 31 import android.os.Handler; 32 import android.os.Message; 33 import android.util.AttributeSet; 34 import android.view.Gravity; 35 import android.view.LayoutInflater; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.widget.LinearLayout; 39 40 import com.android.internal.logging.MetricsLogger; 41 import com.android.internal.logging.UiEventLogger; 42 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 43 import com.android.internal.widget.RemeasuringLinearLayout; 44 import com.android.systemui.Dependency; 45 import com.android.systemui.Dumpable; 46 import com.android.systemui.R; 47 import com.android.systemui.broadcast.BroadcastDispatcher; 48 import com.android.systemui.dump.DumpManager; 49 import com.android.systemui.media.MediaHierarchyManager; 50 import com.android.systemui.media.MediaHost; 51 import com.android.systemui.plugins.qs.DetailAdapter; 52 import com.android.systemui.plugins.qs.QSTile; 53 import com.android.systemui.plugins.qs.QSTileView; 54 import com.android.systemui.qs.QSHost.Callback; 55 import com.android.systemui.qs.customize.QSCustomizer; 56 import com.android.systemui.qs.external.CustomTile; 57 import com.android.systemui.qs.logging.QSLogger; 58 import com.android.systemui.settings.BrightnessController; 59 import com.android.systemui.settings.ToggleSliderView; 60 import com.android.systemui.statusbar.policy.BrightnessMirrorController; 61 import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener; 62 import com.android.systemui.tuner.TunerService; 63 import com.android.systemui.tuner.TunerService.Tunable; 64 import com.android.systemui.util.animation.DisappearParameters; 65 66 import java.io.FileDescriptor; 67 import java.io.PrintWriter; 68 import java.util.ArrayList; 69 import java.util.Collection; 70 import java.util.function.Consumer; 71 import java.util.stream.Collectors; 72 73 import javax.inject.Inject; 74 import javax.inject.Named; 75 76 /** View that represents the quick settings tile panel (when expanded/pulled down). **/ 77 public class QSPanel extends LinearLayout implements Tunable, Callback, BrightnessMirrorListener, 78 Dumpable { 79 80 public static final String QS_SHOW_BRIGHTNESS = "qs_show_brightness"; 81 public static final String QS_SHOW_HEADER = "qs_show_header"; 82 83 private static final String TAG = "QSPanel"; 84 85 protected final Context mContext; 86 protected final ArrayList<TileRecord> mRecords = new ArrayList<>(); 87 private final BroadcastDispatcher mBroadcastDispatcher; 88 protected final MediaHost mMediaHost; 89 90 /** 91 * The index where the content starts that needs to be moved between parents 92 */ 93 private final int mMovableContentStartIndex; 94 private String mCachedSpecs = ""; 95 96 @Nullable 97 protected View mBrightnessView; 98 @Nullable 99 private BrightnessController mBrightnessController; 100 101 private final H mHandler = new H(); 102 private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); 103 private QSTileRevealController mQsTileRevealController; 104 /** Whether or not the QS media player feature is enabled. */ 105 protected boolean mUsingMediaPlayer; 106 private int mVisualMarginStart; 107 private int mVisualMarginEnd; 108 109 protected boolean mExpanded; 110 protected boolean mListening; 111 112 private QSDetail.Callback mCallback; 113 private final DumpManager mDumpManager; 114 private final QSLogger mQSLogger; 115 protected final UiEventLogger mUiEventLogger; 116 protected QSTileHost mHost; 117 118 @Nullable 119 protected QSSecurityFooter mSecurityFooter; 120 121 @Nullable 122 protected View mFooter; 123 @Nullable 124 protected View mDivider; 125 126 @Nullable 127 private ViewGroup mHeaderContainer; 128 private PageIndicator mFooterPageIndicator; 129 private boolean mGridContentVisible = true; 130 private int mContentMarginStart; 131 private int mContentMarginEnd; 132 private int mVisualTilePadding; 133 private boolean mUsingHorizontalLayout; 134 135 private QSCustomizer mCustomizePanel; 136 private Record mDetailRecord; 137 138 private BrightnessMirrorController mBrightnessMirrorController; 139 private LinearLayout mHorizontalLinearLayout; 140 private LinearLayout mHorizontalContentContainer; 141 142 // Only used with media 143 private QSTileLayout mHorizontalTileLayout; 144 protected QSTileLayout mRegularTileLayout; 145 protected QSTileLayout mTileLayout; 146 private int mLastOrientation = -1; 147 private int mMediaTotalBottomMargin; 148 private int mFooterMarginStartHorizontal; 149 private Consumer<Boolean> mMediaVisibilityChangedListener; 150 151 152 @Inject QSPanel( @amedVIEW_CONTEXT) Context context, AttributeSet attrs, DumpManager dumpManager, BroadcastDispatcher broadcastDispatcher, QSLogger qsLogger, MediaHost mediaHost, UiEventLogger uiEventLogger )153 public QSPanel( 154 @Named(VIEW_CONTEXT) Context context, 155 AttributeSet attrs, 156 DumpManager dumpManager, 157 BroadcastDispatcher broadcastDispatcher, 158 QSLogger qsLogger, 159 MediaHost mediaHost, 160 UiEventLogger uiEventLogger 161 ) { 162 super(context, attrs); 163 mUsingMediaPlayer = useQsMediaPlayer(context); 164 mMediaTotalBottomMargin = getResources().getDimensionPixelSize( 165 R.dimen.quick_settings_bottom_margin_media); 166 mMediaHost = mediaHost; 167 mMediaHost.addVisibilityChangeListener((visible) -> { 168 onMediaVisibilityChanged(visible); 169 return null; 170 }); 171 mContext = context; 172 mQSLogger = qsLogger; 173 mDumpManager = dumpManager; 174 mBroadcastDispatcher = broadcastDispatcher; 175 mUiEventLogger = uiEventLogger; 176 177 setOrientation(VERTICAL); 178 179 addViewsAboveTiles(); 180 mMovableContentStartIndex = getChildCount(); 181 mRegularTileLayout = createRegularTileLayout(); 182 183 if (mUsingMediaPlayer) { 184 mHorizontalLinearLayout = new RemeasuringLinearLayout(mContext); 185 mHorizontalLinearLayout.setOrientation(LinearLayout.HORIZONTAL); 186 mHorizontalLinearLayout.setClipChildren(false); 187 mHorizontalLinearLayout.setClipToPadding(false); 188 189 mHorizontalContentContainer = new RemeasuringLinearLayout(mContext); 190 mHorizontalContentContainer.setOrientation(LinearLayout.VERTICAL); 191 mHorizontalContentContainer.setClipChildren(false); 192 mHorizontalContentContainer.setClipToPadding(false); 193 194 mHorizontalTileLayout = createHorizontalTileLayout(); 195 LayoutParams lp = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1); 196 int marginSize = (int) mContext.getResources().getDimension(R.dimen.qqs_media_spacing); 197 lp.setMarginStart(0); 198 lp.setMarginEnd(marginSize); 199 lp.gravity = Gravity.CENTER_VERTICAL; 200 mHorizontalLinearLayout.addView(mHorizontalContentContainer, lp); 201 202 lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0, 1); 203 addView(mHorizontalLinearLayout, lp); 204 205 initMediaHostState(); 206 } 207 addSecurityFooter(); 208 if (mRegularTileLayout instanceof PagedTileLayout) { 209 mQsTileRevealController = new QSTileRevealController(mContext, this, 210 (PagedTileLayout) mRegularTileLayout); 211 } 212 mQSLogger.logAllTilesChangeListening(mListening, getDumpableTag(), mCachedSpecs); 213 updateResources(); 214 } 215 onMediaVisibilityChanged(Boolean visible)216 protected void onMediaVisibilityChanged(Boolean visible) { 217 switchTileLayout(); 218 if (mMediaVisibilityChangedListener != null) { 219 mMediaVisibilityChangedListener.accept(visible); 220 } 221 } 222 addSecurityFooter()223 protected void addSecurityFooter() { 224 mSecurityFooter = new QSSecurityFooter(this, mContext); 225 } 226 addViewsAboveTiles()227 protected void addViewsAboveTiles() { 228 mBrightnessView = LayoutInflater.from(mContext).inflate( 229 R.layout.quick_settings_brightness_dialog, this, false); 230 addView(mBrightnessView); 231 mBrightnessController = new BrightnessController(getContext(), 232 findViewById(R.id.brightness_slider), mBroadcastDispatcher); 233 } 234 createRegularTileLayout()235 protected QSTileLayout createRegularTileLayout() { 236 if (mRegularTileLayout == null) { 237 mRegularTileLayout = (QSTileLayout) LayoutInflater.from(mContext).inflate( 238 R.layout.qs_paged_tile_layout, this, false); 239 } 240 return mRegularTileLayout; 241 } 242 243 createHorizontalTileLayout()244 protected QSTileLayout createHorizontalTileLayout() { 245 return createRegularTileLayout(); 246 } 247 initMediaHostState()248 protected void initMediaHostState() { 249 mMediaHost.setExpansion(1.0f); 250 mMediaHost.setShowsOnlyActiveMedia(false); 251 updateMediaDisappearParameters(); 252 mMediaHost.init(MediaHierarchyManager.LOCATION_QS); 253 } 254 255 /** 256 * Update the way the media disappears based on if we're using the horizontal layout 257 */ updateMediaDisappearParameters()258 private void updateMediaDisappearParameters() { 259 if (!mUsingMediaPlayer) { 260 return; 261 } 262 DisappearParameters parameters = mMediaHost.getDisappearParameters(); 263 if (mUsingHorizontalLayout) { 264 // Only height remaining 265 parameters.getDisappearSize().set(0.0f, 0.4f); 266 // Disappearing on the right side on the bottom 267 parameters.getGonePivot().set(1.0f, 1.0f); 268 // translating a bit horizontal 269 parameters.getContentTranslationFraction().set(0.25f, 1.0f); 270 parameters.setDisappearEnd(0.6f); 271 } else { 272 // Only width remaining 273 parameters.getDisappearSize().set(1.0f, 0.0f); 274 // Disappearing on the bottom 275 parameters.getGonePivot().set(0.0f, 1.0f); 276 // translating a bit vertical 277 parameters.getContentTranslationFraction().set(0.0f, 1.05f); 278 parameters.setDisappearEnd(0.95f); 279 } 280 parameters.setFadeStartPosition(0.95f); 281 parameters.setDisappearStart(0.0f); 282 mMediaHost.setDisappearParameters(parameters); 283 } 284 285 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)286 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 287 if (mTileLayout instanceof PagedTileLayout) { 288 // Since PageIndicator gets measured before PagedTileLayout, we preemptively set the 289 // # of pages before the measurement pass so PageIndicator is measured appropriately 290 if (mFooterPageIndicator != null) { 291 mFooterPageIndicator.setNumPages(((PagedTileLayout) mTileLayout).getNumPages()); 292 } 293 294 // Allow the UI to be as big as it want's to, we're in a scroll view 295 int newHeight = 10000; 296 int availableHeight = MeasureSpec.getSize(heightMeasureSpec); 297 int excessHeight = newHeight - availableHeight; 298 // Measure with EXACTLY. That way, The content will only use excess height and will 299 // be measured last, after other views and padding is accounted for. This only 300 // works because our Layouts in here remeasure themselves with the exact content 301 // height. 302 heightMeasureSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY); 303 ((PagedTileLayout) mTileLayout).setExcessHeight(excessHeight); 304 } 305 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 306 307 // We want all the logic of LinearLayout#onMeasure, and for it to assign the excess space 308 // not used by the other children to PagedTileLayout. However, in this case, LinearLayout 309 // assumes that PagedTileLayout would use all the excess space. This is not the case as 310 // PagedTileLayout height is quantized (because it shows a certain number of rows). 311 // Therefore, after everything is measured, we need to make sure that we add up the correct 312 // total height 313 int height = getPaddingBottom() + getPaddingTop(); 314 int numChildren = getChildCount(); 315 for (int i = 0; i < numChildren; i++) { 316 View child = getChildAt(i); 317 if (child.getVisibility() != View.GONE) { 318 height += child.getMeasuredHeight(); 319 MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams(); 320 height += layoutParams.topMargin + layoutParams.bottomMargin; 321 } 322 } 323 setMeasuredDimension(getMeasuredWidth(), height); 324 } 325 getQsTileRevealController()326 public QSTileRevealController getQsTileRevealController() { 327 return mQsTileRevealController; 328 } 329 isShowingCustomize()330 public boolean isShowingCustomize() { 331 return mCustomizePanel != null && mCustomizePanel.isCustomizing(); 332 } 333 334 @Override onAttachedToWindow()335 protected void onAttachedToWindow() { 336 super.onAttachedToWindow(); 337 final TunerService tunerService = Dependency.get(TunerService.class); 338 tunerService.addTunable(this, QS_SHOW_BRIGHTNESS); 339 340 if (mHost != null) { 341 setTiles(mHost.getTiles()); 342 } 343 if (mBrightnessMirrorController != null) { 344 mBrightnessMirrorController.addCallback(this); 345 } 346 mDumpManager.registerDumpable(getDumpableTag(), this); 347 } 348 349 @Override onDetachedFromWindow()350 protected void onDetachedFromWindow() { 351 Dependency.get(TunerService.class).removeTunable(this); 352 if (mHost != null) { 353 mHost.removeCallback(this); 354 } 355 if (mTileLayout != null) { 356 mTileLayout.setListening(false); 357 } 358 for (TileRecord record : mRecords) { 359 record.tile.removeCallbacks(); 360 } 361 mRecords.clear(); 362 if (mBrightnessMirrorController != null) { 363 mBrightnessMirrorController.removeCallback(this); 364 } 365 mDumpManager.unregisterDumpable(getDumpableTag()); 366 super.onDetachedFromWindow(); 367 } 368 getDumpableTag()369 protected String getDumpableTag() { 370 return TAG; 371 } 372 373 @Override onTilesChanged()374 public void onTilesChanged() { 375 setTiles(mHost.getTiles()); 376 } 377 378 @Override onTuningChanged(String key, String newValue)379 public void onTuningChanged(String key, String newValue) { 380 if (QS_SHOW_BRIGHTNESS.equals(key) && mBrightnessView != null) { 381 updateViewVisibilityForTuningValue(mBrightnessView, newValue); 382 } 383 } 384 updateViewVisibilityForTuningValue(View view, @Nullable String newValue)385 private void updateViewVisibilityForTuningValue(View view, @Nullable String newValue) { 386 view.setVisibility(TunerService.parseIntegerSwitch(newValue, true) ? VISIBLE : GONE); 387 } 388 openDetails(String subPanel)389 public void openDetails(String subPanel) { 390 QSTile tile = getTile(subPanel); 391 // If there's no tile with that name (as defined in QSFactoryImpl or other QSFactory), 392 // QSFactory will not be able to create a tile and getTile will return null 393 if (tile != null) { 394 showDetailAdapter(true, tile.getDetailAdapter(), new int[]{getWidth() / 2, 0}); 395 } 396 } 397 getTile(String subPanel)398 private QSTile getTile(String subPanel) { 399 for (int i = 0; i < mRecords.size(); i++) { 400 if (subPanel.equals(mRecords.get(i).tile.getTileSpec())) { 401 return mRecords.get(i).tile; 402 } 403 } 404 return mHost.createTile(subPanel); 405 } 406 setBrightnessMirror(BrightnessMirrorController c)407 public void setBrightnessMirror(BrightnessMirrorController c) { 408 if (mBrightnessMirrorController != null) { 409 mBrightnessMirrorController.removeCallback(this); 410 } 411 mBrightnessMirrorController = c; 412 if (mBrightnessMirrorController != null) { 413 mBrightnessMirrorController.addCallback(this); 414 } 415 updateBrightnessMirror(); 416 } 417 418 @Override onBrightnessMirrorReinflated(View brightnessMirror)419 public void onBrightnessMirrorReinflated(View brightnessMirror) { 420 updateBrightnessMirror(); 421 } 422 423 @Nullable getBrightnessView()424 View getBrightnessView() { 425 return mBrightnessView; 426 } 427 setCallback(QSDetail.Callback callback)428 public void setCallback(QSDetail.Callback callback) { 429 mCallback = callback; 430 } 431 setHost(QSTileHost host, QSCustomizer customizer)432 public void setHost(QSTileHost host, QSCustomizer customizer) { 433 mHost = host; 434 mHost.addCallback(this); 435 setTiles(mHost.getTiles()); 436 if (mSecurityFooter != null) { 437 mSecurityFooter.setHostEnvironment(host); 438 } 439 mCustomizePanel = customizer; 440 if (mCustomizePanel != null) { 441 mCustomizePanel.setHost(mHost); 442 } 443 } 444 445 /** 446 * Links the footer's page indicator, which is used in landscape orientation to save space. 447 * 448 * @param pageIndicator indicator to use for page scrolling 449 */ setFooterPageIndicator(PageIndicator pageIndicator)450 public void setFooterPageIndicator(PageIndicator pageIndicator) { 451 if (mRegularTileLayout instanceof PagedTileLayout) { 452 mFooterPageIndicator = pageIndicator; 453 updatePageIndicator(); 454 } 455 } 456 updatePageIndicator()457 private void updatePageIndicator() { 458 if (mRegularTileLayout instanceof PagedTileLayout) { 459 if (mFooterPageIndicator != null) { 460 mFooterPageIndicator.setVisibility(View.GONE); 461 462 ((PagedTileLayout) mRegularTileLayout).setPageIndicator(mFooterPageIndicator); 463 } 464 } 465 } 466 getHost()467 public QSTileHost getHost() { 468 return mHost; 469 } 470 updateResources()471 public void updateResources() { 472 int tileSize = getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_size); 473 int tileBg = getResources().getDimensionPixelSize(R.dimen.qs_tile_background_size); 474 mFooterMarginStartHorizontal = getResources().getDimensionPixelSize( 475 R.dimen.qs_footer_horizontal_margin); 476 mVisualTilePadding = (int) ((tileSize - tileBg) / 2.0f); 477 updatePadding(); 478 479 updatePageIndicator(); 480 481 if (mListening) { 482 refreshAllTiles(); 483 } 484 if (mTileLayout != null) { 485 mTileLayout.updateResources(); 486 } 487 } 488 updatePadding()489 protected void updatePadding() { 490 final Resources res = mContext.getResources(); 491 int padding = res.getDimensionPixelSize(R.dimen.qs_panel_padding_top); 492 if (mUsingHorizontalLayout) { 493 // When using the horizontal layout, our space is quite constrained. We therefore 494 // reduce some of the padding on the top, which makes the brightness bar overlapp, 495 // but since that has naturally quite a bit of built in padding, that's fine. 496 padding = (int) (padding * 0.6f); 497 } 498 setPaddingRelative(getPaddingStart(), 499 padding, 500 getPaddingEnd(), 501 res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom)); 502 } 503 504 @Override onConfigurationChanged(Configuration newConfig)505 protected void onConfigurationChanged(Configuration newConfig) { 506 super.onConfigurationChanged(newConfig); 507 if (mSecurityFooter != null) { 508 mSecurityFooter.onConfigurationChanged(); 509 } 510 updateResources(); 511 512 updateBrightnessMirror(); 513 514 if (newConfig.orientation != mLastOrientation) { 515 mLastOrientation = newConfig.orientation; 516 switchTileLayout(); 517 } 518 } 519 520 @Override onFinishInflate()521 protected void onFinishInflate() { 522 super.onFinishInflate(); 523 mFooter = findViewById(R.id.qs_footer); 524 mDivider = findViewById(R.id.divider); 525 switchTileLayout(true /* force */); 526 } 527 switchTileLayout()528 boolean switchTileLayout() { 529 return switchTileLayout(false /* force */); 530 } 531 switchTileLayout(boolean force)532 private boolean switchTileLayout(boolean force) { 533 /** Whether or not the QuickQSPanel currently contains a media player. */ 534 boolean horizontal = shouldUseHorizontalLayout(); 535 if (mDivider != null) { 536 if (!horizontal && mUsingMediaPlayer && mMediaHost.getVisible()) { 537 mDivider.setVisibility(View.VISIBLE); 538 } else { 539 mDivider.setVisibility(View.GONE); 540 } 541 } 542 if (horizontal != mUsingHorizontalLayout || force) { 543 mUsingHorizontalLayout = horizontal; 544 View visibleView = horizontal ? mHorizontalLinearLayout : (View) mRegularTileLayout; 545 View hiddenView = horizontal ? (View) mRegularTileLayout : mHorizontalLinearLayout; 546 ViewGroup newParent = horizontal ? mHorizontalContentContainer : this; 547 QSTileLayout newLayout = horizontal ? mHorizontalTileLayout : mRegularTileLayout; 548 if (hiddenView != null && 549 (mRegularTileLayout != mHorizontalTileLayout || 550 hiddenView != mRegularTileLayout)) { 551 // Only hide the view if the horizontal and the regular view are different, 552 // otherwise its reattached. 553 hiddenView.setVisibility(View.GONE); 554 } 555 visibleView.setVisibility(View.VISIBLE); 556 switchAllContentToParent(newParent, newLayout); 557 reAttachMediaHost(); 558 if (mTileLayout != null) { 559 mTileLayout.setListening(false); 560 for (TileRecord record : mRecords) { 561 mTileLayout.removeTile(record); 562 record.tile.removeCallback(record.callback); 563 } 564 } 565 mTileLayout = newLayout; 566 if (mHost != null) setTiles(mHost.getTiles()); 567 newLayout.setListening(mListening); 568 if (needsDynamicRowsAndColumns()) { 569 newLayout.setMinRows(horizontal ? 2 : 1); 570 // Let's use 3 columns to match the current layout 571 newLayout.setMaxColumns(horizontal ? 3 : TileLayout.NO_MAX_COLUMNS); 572 } 573 updateTileLayoutMargins(); 574 updateFooterMargin(); 575 updateDividerMargin(); 576 updateMediaDisappearParameters(); 577 updateMediaHostContentMargins(); 578 updateHorizontalLinearLayoutMargins(); 579 updatePadding(); 580 return true; 581 } 582 return false; 583 } 584 updateHorizontalLinearLayoutMargins()585 private void updateHorizontalLinearLayoutMargins() { 586 if (mHorizontalLinearLayout != null && !displayMediaMarginsOnMedia()) { 587 LayoutParams lp = (LayoutParams) mHorizontalLinearLayout.getLayoutParams(); 588 lp.bottomMargin = mMediaTotalBottomMargin - getPaddingBottom(); 589 mHorizontalLinearLayout.setLayoutParams(lp); 590 } 591 } 592 593 /** 594 * @return true if the margin bottom of the media view should be on the media host or false 595 * if they should be on the HorizontalLinearLayout. Returning {@code false} is useful 596 * to visually center the tiles in the Media view, which doesn't work when the 597 * expanded panel actually scrolls. 598 */ displayMediaMarginsOnMedia()599 protected boolean displayMediaMarginsOnMedia() { 600 return true; 601 } 602 needsDynamicRowsAndColumns()603 protected boolean needsDynamicRowsAndColumns() { 604 return true; 605 } 606 switchAllContentToParent(ViewGroup parent, QSTileLayout newLayout)607 private void switchAllContentToParent(ViewGroup parent, QSTileLayout newLayout) { 608 int index = parent == this ? mMovableContentStartIndex : 0; 609 610 // Let's first move the tileLayout to the new parent, since that should come first. 611 switchToParent((View) newLayout, parent, index); 612 index++; 613 614 if (mSecurityFooter != null) { 615 View view = mSecurityFooter.getView(); 616 LinearLayout.LayoutParams layoutParams = (LayoutParams) view.getLayoutParams(); 617 if (mUsingHorizontalLayout && mHeaderContainer != null) { 618 // Adding the security view to the header, that enables us to avoid scrolling 619 layoutParams.width = 0; 620 layoutParams.weight = 1.6f; 621 switchToParent(view, mHeaderContainer, 1 /* always in second place */); 622 } else { 623 layoutParams.width = LayoutParams.WRAP_CONTENT; 624 layoutParams.weight = 0; 625 switchToParent(view, parent, index); 626 index++; 627 } 628 view.setLayoutParams(layoutParams); 629 } 630 631 if (mFooter != null) { 632 // Then the footer with the settings 633 switchToParent(mFooter, parent, index); 634 } 635 } 636 switchToParent(View child, ViewGroup parent, int index)637 private void switchToParent(View child, ViewGroup parent, int index) { 638 ViewGroup currentParent = (ViewGroup) child.getParent(); 639 if (currentParent != parent || currentParent.indexOfChild(child) != index) { 640 if (currentParent != null) { 641 currentParent.removeView(child); 642 } 643 parent.addView(child, index); 644 } 645 } 646 shouldUseHorizontalLayout()647 private boolean shouldUseHorizontalLayout() { 648 return mUsingMediaPlayer && mMediaHost.getVisible() 649 && getResources().getConfiguration().orientation 650 == Configuration.ORIENTATION_LANDSCAPE; 651 } 652 reAttachMediaHost()653 protected void reAttachMediaHost() { 654 if (!mUsingMediaPlayer) { 655 return; 656 } 657 boolean horizontal = shouldUseHorizontalLayout(); 658 ViewGroup host = mMediaHost.getHostView(); 659 ViewGroup newParent = horizontal ? mHorizontalLinearLayout : this; 660 ViewGroup currentParent = (ViewGroup) host.getParent(); 661 if (currentParent != newParent) { 662 if (currentParent != null) { 663 currentParent.removeView(host); 664 } 665 newParent.addView(host); 666 LinearLayout.LayoutParams layoutParams = (LayoutParams) host.getLayoutParams(); 667 layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; 668 layoutParams.width = horizontal ? 0 : ViewGroup.LayoutParams.MATCH_PARENT; 669 layoutParams.weight = horizontal ? 1.2f : 0; 670 // Add any bottom margin, such that the total spacing is correct. This is only 671 // necessary if the view isn't horizontal, since otherwise the padding is 672 // carried in the parent of this view (to ensure correct vertical alignment) 673 layoutParams.bottomMargin = !horizontal || displayMediaMarginsOnMedia() 674 ? mMediaTotalBottomMargin - getPaddingBottom() : 0; 675 } 676 } 677 updateBrightnessMirror()678 public void updateBrightnessMirror() { 679 if (mBrightnessMirrorController != null) { 680 ToggleSliderView brightnessSlider = findViewById(R.id.brightness_slider); 681 ToggleSliderView mirrorSlider = mBrightnessMirrorController.getMirror() 682 .findViewById(R.id.brightness_slider); 683 brightnessSlider.setMirror(mirrorSlider); 684 brightnessSlider.setMirrorController(mBrightnessMirrorController); 685 } 686 } 687 onCollapse()688 public void onCollapse() { 689 if (mCustomizePanel != null && mCustomizePanel.isShown()) { 690 mCustomizePanel.hide(); 691 } 692 } 693 setExpanded(boolean expanded)694 public void setExpanded(boolean expanded) { 695 if (mExpanded == expanded) return; 696 mQSLogger.logPanelExpanded(expanded, getDumpableTag()); 697 mExpanded = expanded; 698 if (!mExpanded && mTileLayout instanceof PagedTileLayout) { 699 ((PagedTileLayout) mTileLayout).setCurrentItem(0, false); 700 } 701 mMetricsLogger.visibility(MetricsEvent.QS_PANEL, mExpanded); 702 if (!mExpanded) { 703 mUiEventLogger.log(closePanelEvent()); 704 closeDetail(); 705 } else { 706 mUiEventLogger.log(openPanelEvent()); 707 logTiles(); 708 } 709 } 710 setPageListener(final PagedTileLayout.PageListener pageListener)711 public void setPageListener(final PagedTileLayout.PageListener pageListener) { 712 if (mTileLayout instanceof PagedTileLayout) { 713 ((PagedTileLayout) mTileLayout).setPageListener(pageListener); 714 } 715 } 716 isExpanded()717 public boolean isExpanded() { 718 return mExpanded; 719 } 720 setListening(boolean listening)721 public void setListening(boolean listening) { 722 if (mListening == listening) return; 723 mListening = listening; 724 if (mTileLayout != null) { 725 mQSLogger.logAllTilesChangeListening(listening, getDumpableTag(), mCachedSpecs); 726 mTileLayout.setListening(listening); 727 } 728 if (mListening) { 729 refreshAllTiles(); 730 } 731 } 732 getTilesSpecs()733 private String getTilesSpecs() { 734 return mRecords.stream() 735 .map(tileRecord -> tileRecord.tile.getTileSpec()) 736 .collect(Collectors.joining(",")); 737 } 738 setListening(boolean listening, boolean expanded)739 public void setListening(boolean listening, boolean expanded) { 740 setListening(listening && expanded); 741 if (mSecurityFooter != null) { 742 mSecurityFooter.setListening(listening); 743 } 744 // Set the listening as soon as the QS fragment starts listening regardless of the expansion, 745 // so it will update the current brightness before the slider is visible. 746 setBrightnessListening(listening); 747 } 748 setBrightnessListening(boolean listening)749 public void setBrightnessListening(boolean listening) { 750 if (mBrightnessController == null) { 751 return; 752 } 753 if (listening) { 754 mBrightnessController.registerCallbacks(); 755 } else { 756 mBrightnessController.unregisterCallbacks(); 757 } 758 } 759 refreshAllTiles()760 public void refreshAllTiles() { 761 if (mBrightnessController != null) { 762 mBrightnessController.checkRestrictionAndSetEnabled(); 763 } 764 for (TileRecord r : mRecords) { 765 r.tile.refreshState(); 766 } 767 if (mSecurityFooter != null) { 768 mSecurityFooter.refreshState(); 769 } 770 } 771 showDetailAdapter(boolean show, DetailAdapter adapter, int[] locationInWindow)772 public void showDetailAdapter(boolean show, DetailAdapter adapter, int[] locationInWindow) { 773 int xInWindow = locationInWindow[0]; 774 int yInWindow = locationInWindow[1]; 775 ((View) getParent()).getLocationInWindow(locationInWindow); 776 777 Record r = new Record(); 778 r.detailAdapter = adapter; 779 r.x = xInWindow - locationInWindow[0]; 780 r.y = yInWindow - locationInWindow[1]; 781 782 locationInWindow[0] = xInWindow; 783 locationInWindow[1] = yInWindow; 784 785 showDetail(show, r); 786 } 787 showDetail(boolean show, Record r)788 protected void showDetail(boolean show, Record r) { 789 mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0, r).sendToTarget(); 790 } 791 setTiles(Collection<QSTile> tiles)792 public void setTiles(Collection<QSTile> tiles) { 793 setTiles(tiles, false); 794 } 795 setTiles(Collection<QSTile> tiles, boolean collapsedView)796 public void setTiles(Collection<QSTile> tiles, boolean collapsedView) { 797 if (!collapsedView) { 798 mQsTileRevealController.updateRevealedTiles(tiles); 799 } 800 for (TileRecord record : mRecords) { 801 mTileLayout.removeTile(record); 802 record.tile.removeCallback(record.callback); 803 } 804 mRecords.clear(); 805 mCachedSpecs = ""; 806 for (QSTile tile : tiles) { 807 addTile(tile, collapsedView); 808 } 809 } 810 drawTile(TileRecord r, QSTile.State state)811 protected void drawTile(TileRecord r, QSTile.State state) { 812 r.tileView.onStateChanged(state); 813 } 814 createTileView(QSTile tile, boolean collapsedView)815 protected QSTileView createTileView(QSTile tile, boolean collapsedView) { 816 return mHost.createTileView(tile, collapsedView); 817 } 818 openPanelEvent()819 protected QSEvent openPanelEvent() { 820 return QSEvent.QS_PANEL_EXPANDED; 821 } 822 closePanelEvent()823 protected QSEvent closePanelEvent() { 824 return QSEvent.QS_PANEL_COLLAPSED; 825 } 826 tileVisibleEvent()827 protected QSEvent tileVisibleEvent() { 828 return QSEvent.QS_TILE_VISIBLE; 829 } 830 shouldShowDetail()831 protected boolean shouldShowDetail() { 832 return mExpanded; 833 } 834 addTile(final QSTile tile, boolean collapsedView)835 protected TileRecord addTile(final QSTile tile, boolean collapsedView) { 836 final TileRecord r = new TileRecord(); 837 r.tile = tile; 838 r.tileView = createTileView(tile, collapsedView); 839 final QSTile.Callback callback = new QSTile.Callback() { 840 @Override 841 public void onStateChanged(QSTile.State state) { 842 drawTile(r, state); 843 } 844 845 @Override 846 public void onShowDetail(boolean show) { 847 // Both the collapsed and full QS panels get this callback, this check determines 848 // which one should handle showing the detail. 849 if (shouldShowDetail()) { 850 QSPanel.this.showDetail(show, r); 851 } 852 } 853 854 @Override 855 public void onToggleStateChanged(boolean state) { 856 if (mDetailRecord == r) { 857 fireToggleStateChanged(state); 858 } 859 } 860 861 @Override 862 public void onScanStateChanged(boolean state) { 863 r.scanState = state; 864 if (mDetailRecord == r) { 865 fireScanStateChanged(r.scanState); 866 } 867 } 868 869 @Override 870 public void onAnnouncementRequested(CharSequence announcement) { 871 if (announcement != null) { 872 mHandler.obtainMessage(H.ANNOUNCE_FOR_ACCESSIBILITY, announcement) 873 .sendToTarget(); 874 } 875 } 876 }; 877 r.tile.addCallback(callback); 878 r.callback = callback; 879 r.tileView.init(r.tile); 880 r.tile.refreshState(); 881 mRecords.add(r); 882 mCachedSpecs = getTilesSpecs(); 883 884 if (mTileLayout != null) { 885 mTileLayout.addTile(r); 886 } 887 888 return r; 889 } 890 891 showEdit(final View v)892 public void showEdit(final View v) { 893 v.post(new Runnable() { 894 @Override 895 public void run() { 896 if (mCustomizePanel != null) { 897 if (!mCustomizePanel.isCustomizing()) { 898 int[] loc = v.getLocationOnScreen(); 899 int x = loc[0] + v.getWidth() / 2; 900 int y = loc[1] + v.getHeight() / 2; 901 mCustomizePanel.show(x, y); 902 } 903 } 904 905 } 906 }); 907 } 908 closeDetail()909 public void closeDetail() { 910 if (mCustomizePanel != null && mCustomizePanel.isShown()) { 911 // Treat this as a detail panel for now, to make things easy. 912 mCustomizePanel.hide(); 913 return; 914 } 915 showDetail(false, mDetailRecord); 916 } 917 getGridHeight()918 public int getGridHeight() { 919 return getMeasuredHeight(); 920 } 921 handleShowDetail(Record r, boolean show)922 protected void handleShowDetail(Record r, boolean show) { 923 if (r instanceof TileRecord) { 924 handleShowDetailTile((TileRecord) r, show); 925 } else { 926 int x = 0; 927 int y = 0; 928 if (r != null) { 929 x = r.x; 930 y = r.y; 931 } 932 handleShowDetailImpl(r, show, x, y); 933 } 934 } 935 handleShowDetailTile(TileRecord r, boolean show)936 private void handleShowDetailTile(TileRecord r, boolean show) { 937 if ((mDetailRecord != null) == show && mDetailRecord == r) return; 938 939 if (show) { 940 r.detailAdapter = r.tile.getDetailAdapter(); 941 if (r.detailAdapter == null) return; 942 } 943 r.tile.setDetailListening(show); 944 int x = r.tileView.getLeft() + r.tileView.getWidth() / 2; 945 int y = r.tileView.getDetailY() + mTileLayout.getOffsetTop(r) + getTop(); 946 handleShowDetailImpl(r, show, x, y); 947 } 948 handleShowDetailImpl(Record r, boolean show, int x, int y)949 private void handleShowDetailImpl(Record r, boolean show, int x, int y) { 950 setDetailRecord(show ? r : null); 951 fireShowingDetail(show ? r.detailAdapter : null, x, y); 952 } 953 setDetailRecord(Record r)954 protected void setDetailRecord(Record r) { 955 if (r == mDetailRecord) return; 956 mDetailRecord = r; 957 final boolean scanState = mDetailRecord instanceof TileRecord 958 && ((TileRecord) mDetailRecord).scanState; 959 fireScanStateChanged(scanState); 960 } 961 setGridContentVisibility(boolean visible)962 void setGridContentVisibility(boolean visible) { 963 int newVis = visible ? VISIBLE : INVISIBLE; 964 setVisibility(newVis); 965 if (mGridContentVisible != visible) { 966 mMetricsLogger.visibility(MetricsEvent.QS_PANEL, newVis); 967 } 968 mGridContentVisible = visible; 969 } 970 logTiles()971 private void logTiles() { 972 for (int i = 0; i < mRecords.size(); i++) { 973 QSTile tile = mRecords.get(i).tile; 974 mMetricsLogger.write(tile.populate(new LogMaker(tile.getMetricsCategory()) 975 .setType(MetricsEvent.TYPE_OPEN))); 976 } 977 } 978 fireShowingDetail(DetailAdapter detail, int x, int y)979 private void fireShowingDetail(DetailAdapter detail, int x, int y) { 980 if (mCallback != null) { 981 mCallback.onShowingDetail(detail, x, y); 982 } 983 } 984 fireToggleStateChanged(boolean state)985 private void fireToggleStateChanged(boolean state) { 986 if (mCallback != null) { 987 mCallback.onToggleStateChanged(state); 988 } 989 } 990 fireScanStateChanged(boolean state)991 private void fireScanStateChanged(boolean state) { 992 if (mCallback != null) { 993 mCallback.onScanStateChanged(state); 994 } 995 } 996 clickTile(ComponentName tile)997 public void clickTile(ComponentName tile) { 998 final String spec = CustomTile.toSpec(tile); 999 final int N = mRecords.size(); 1000 for (int i = 0; i < N; i++) { 1001 if (mRecords.get(i).tile.getTileSpec().equals(spec)) { 1002 mRecords.get(i).tile.click(); 1003 break; 1004 } 1005 } 1006 } 1007 getTileLayout()1008 QSTileLayout getTileLayout() { 1009 return mTileLayout; 1010 } 1011 getTileView(QSTile tile)1012 QSTileView getTileView(QSTile tile) { 1013 for (TileRecord r : mRecords) { 1014 if (r.tile == tile) { 1015 return r.tileView; 1016 } 1017 } 1018 return null; 1019 } 1020 1021 @Nullable getSecurityFooter()1022 public QSSecurityFooter getSecurityFooter() { 1023 return mSecurityFooter; 1024 } 1025 1026 @Nullable getDivider()1027 public View getDivider() { 1028 return mDivider; 1029 } 1030 showDeviceMonitoringDialog()1031 public void showDeviceMonitoringDialog() { 1032 if (mSecurityFooter != null) { 1033 mSecurityFooter.showDeviceMonitoringDialog(); 1034 } 1035 } 1036 setContentMargins(int startMargin, int endMargin)1037 public void setContentMargins(int startMargin, int endMargin) { 1038 // Only some views actually want this content padding, others want to go all the way 1039 // to the edge like the brightness slider 1040 mContentMarginStart = startMargin; 1041 mContentMarginEnd = endMargin; 1042 updateTileLayoutMargins(mContentMarginStart - mVisualTilePadding, 1043 mContentMarginEnd - mVisualTilePadding); 1044 updateMediaHostContentMargins(); 1045 updateFooterMargin(); 1046 updateDividerMargin(); 1047 } 1048 updateFooterMargin()1049 private void updateFooterMargin() { 1050 if (mFooter != null) { 1051 int footerMargin = 0; 1052 int indicatorMargin = 0; 1053 if (mUsingHorizontalLayout) { 1054 footerMargin = mFooterMarginStartHorizontal; 1055 indicatorMargin = footerMargin - mVisualMarginEnd; 1056 } 1057 updateMargins(mFooter, footerMargin, 0); 1058 // The page indicator isn't centered anymore because of the visual positioning. 1059 // Let's fix it by adding some margin 1060 if (mFooterPageIndicator != null) { 1061 updateMargins(mFooterPageIndicator, 0, indicatorMargin); 1062 } 1063 } 1064 } 1065 1066 /** 1067 * Update the margins of all tile Layouts. 1068 * 1069 * @param visualMarginStart the visual start margin of the tile, adjusted for local insets 1070 * to the tile. This can be set on a tileLayout 1071 * @param visualMarginEnd the visual end margin of the tile, adjusted for local insets 1072 * to the tile. This can be set on a tileLayout 1073 */ updateTileLayoutMargins(int visualMarginStart, int visualMarginEnd)1074 private void updateTileLayoutMargins(int visualMarginStart, int visualMarginEnd) { 1075 mVisualMarginStart = visualMarginStart; 1076 mVisualMarginEnd = visualMarginEnd; 1077 updateTileLayoutMargins(); 1078 } 1079 updateTileLayoutMargins()1080 private void updateTileLayoutMargins() { 1081 int marginEnd = mVisualMarginEnd; 1082 if (mUsingHorizontalLayout) { 1083 marginEnd = 0; 1084 } 1085 updateMargins((View) mTileLayout, mVisualMarginStart, marginEnd); 1086 } 1087 updateDividerMargin()1088 private void updateDividerMargin() { 1089 if (mDivider == null) return; 1090 updateMargins(mDivider, mContentMarginStart, mContentMarginEnd); 1091 } 1092 1093 /** 1094 * Update the margins of the media hosts 1095 */ updateMediaHostContentMargins()1096 protected void updateMediaHostContentMargins() { 1097 if (mUsingMediaPlayer) { 1098 int marginStart = mContentMarginStart; 1099 if (mUsingHorizontalLayout) { 1100 marginStart = 0; 1101 } 1102 updateMargins(mMediaHost.getHostView(), marginStart, mContentMarginEnd); 1103 } 1104 } 1105 1106 /** 1107 * Update the margins of a view. 1108 * 1109 * @param view the view to adjust 1110 * @param start the start margin to set 1111 * @param end the end margin to set 1112 */ updateMargins(View view, int start, int end)1113 protected void updateMargins(View view, int start, int end) { 1114 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 1115 lp.setMarginStart(start); 1116 lp.setMarginEnd(end); 1117 view.setLayoutParams(lp); 1118 } 1119 getMediaHost()1120 public MediaHost getMediaHost() { 1121 return mMediaHost; 1122 } 1123 1124 /** 1125 * Set the header container of quick settings. 1126 */ setHeaderContainer(@onNull ViewGroup headerContainer)1127 public void setHeaderContainer(@NonNull ViewGroup headerContainer) { 1128 mHeaderContainer = headerContainer; 1129 } 1130 setMediaVisibilityChangedListener(Consumer<Boolean> visibilityChangedListener)1131 public void setMediaVisibilityChangedListener(Consumer<Boolean> visibilityChangedListener) { 1132 mMediaVisibilityChangedListener = visibilityChangedListener; 1133 } 1134 1135 private class H extends Handler { 1136 private static final int SHOW_DETAIL = 1; 1137 private static final int SET_TILE_VISIBILITY = 2; 1138 private static final int ANNOUNCE_FOR_ACCESSIBILITY = 3; 1139 1140 @Override handleMessage(Message msg)1141 public void handleMessage(Message msg) { 1142 if (msg.what == SHOW_DETAIL) { 1143 handleShowDetail((Record) msg.obj, msg.arg1 != 0); 1144 } else if (msg.what == ANNOUNCE_FOR_ACCESSIBILITY) { 1145 announceForAccessibility((CharSequence) msg.obj); 1146 } 1147 } 1148 } 1149 1150 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)1151 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 1152 pw.println(getClass().getSimpleName() + ":"); 1153 pw.println(" Tile records:"); 1154 for (TileRecord record : mRecords) { 1155 if (record.tile instanceof Dumpable) { 1156 pw.print(" "); ((Dumpable) record.tile).dump(fd, pw, args); 1157 pw.print(" "); pw.println(record.tileView.toString()); 1158 } 1159 } 1160 } 1161 1162 1163 protected static class Record { 1164 DetailAdapter detailAdapter; 1165 int x; 1166 int y; 1167 } 1168 1169 public static final class TileRecord extends Record { 1170 public QSTile tile; 1171 public com.android.systemui.plugins.qs.QSTileView tileView; 1172 public boolean scanState; 1173 public QSTile.Callback callback; 1174 } 1175 1176 public interface QSTileLayout { 1177 saveInstanceState(Bundle outState)1178 default void saveInstanceState(Bundle outState) {} 1179 restoreInstanceState(Bundle savedInstanceState)1180 default void restoreInstanceState(Bundle savedInstanceState) {} 1181 addTile(TileRecord tile)1182 void addTile(TileRecord tile); 1183 removeTile(TileRecord tile)1184 void removeTile(TileRecord tile); 1185 getOffsetTop(TileRecord tile)1186 int getOffsetTop(TileRecord tile); 1187 updateResources()1188 boolean updateResources(); 1189 setListening(boolean listening)1190 void setListening(boolean listening); 1191 1192 /** 1193 * Set the minimum number of rows to show 1194 * 1195 * @param minRows the minimum. 1196 */ setMinRows(int minRows)1197 default boolean setMinRows(int minRows) { 1198 return false; 1199 } 1200 1201 /** 1202 * Set the max number of collums to show 1203 * 1204 * @param maxColumns the maximum 1205 * 1206 * @return true if the number of visible columns has changed. 1207 */ setMaxColumns(int maxColumns)1208 default boolean setMaxColumns(int maxColumns) { 1209 return false; 1210 } 1211 setExpansion(float expansion)1212 default void setExpansion(float expansion) {} 1213 getNumVisibleTiles()1214 int getNumVisibleTiles(); 1215 } 1216 } 1217