1 /* 2 * Copyright (C) 2020 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.internal.logging.nano.MetricsProto.MetricsEvent; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.content.ComponentName; 24 import android.content.res.Configuration; 25 import android.content.res.Configuration.Orientation; 26 import android.metrics.LogMaker; 27 import android.util.Log; 28 import android.view.View; 29 30 import com.android.internal.annotations.VisibleForTesting; 31 import com.android.internal.logging.MetricsLogger; 32 import com.android.internal.logging.UiEventLogger; 33 import com.android.systemui.Dumpable; 34 import com.android.systemui.dump.DumpManager; 35 import com.android.systemui.haptics.qs.QSLongPressEffect; 36 import com.android.systemui.media.controls.ui.view.MediaHost; 37 import com.android.systemui.plugins.qs.QSTile; 38 import com.android.systemui.plugins.qs.QSTileView; 39 import com.android.systemui.qs.customize.QSCustomizerController; 40 import com.android.systemui.qs.external.CustomTile; 41 import com.android.systemui.qs.logging.QSLogger; 42 import com.android.systemui.qs.tileimpl.QSTileViewImpl; 43 import com.android.systemui.scene.shared.flag.SceneContainerFlag; 44 import com.android.systemui.shade.ShadeDisplayAware; 45 import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround; 46 import com.android.systemui.statusbar.policy.ConfigurationController; 47 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; 48 import com.android.systemui.statusbar.policy.SplitShadeStateController; 49 import com.android.systemui.util.ViewController; 50 import com.android.systemui.util.animation.DisappearParameters; 51 import com.android.systemui.util.kotlin.JavaAdapterKt; 52 53 import kotlin.Unit; 54 import kotlin.jvm.functions.Function1; 55 56 import kotlinx.coroutines.DisposableHandle; 57 import kotlinx.coroutines.flow.StateFlow; 58 59 import java.io.PrintWriter; 60 import java.util.ArrayList; 61 import java.util.Collection; 62 import java.util.List; 63 import java.util.Objects; 64 import java.util.function.Consumer; 65 import java.util.stream.Collectors; 66 67 import javax.inject.Provider; 68 69 70 /** 71 * Controller for QSPanel views. 72 * 73 * @param <T> Type of QSPanel. 74 */ 75 public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewController<T> 76 implements Dumpable{ 77 private static final String TAG = "QSPanelControllerBase"; 78 protected final QSHost mHost; 79 private final QSCustomizerController mQsCustomizerController; 80 private final boolean mUsingMediaPlayer; 81 protected final MediaHost mMediaHost; 82 protected final MetricsLogger mMetricsLogger; 83 private final UiEventLogger mUiEventLogger; 84 protected final QSLogger mQSLogger; 85 private final DumpManager mDumpManager; 86 protected final ArrayList<TileRecord> mRecords = new ArrayList<>(); 87 protected boolean mShouldUseSplitNotificationShade; 88 89 @Nullable 90 private Consumer<Boolean> mMediaVisibilityChangedListener; 91 @Orientation 92 private int mLastOrientation; 93 private int mLastScreenLayout; 94 private String mCachedSpecs = ""; 95 @Nullable 96 private QSTileRevealController mQsTileRevealController; 97 private float mRevealExpansion; 98 99 private final QSHost.Callback mQSHostCallback = this::setTiles; 100 101 private SplitShadeStateController mSplitShadeStateController; 102 private final ConfigurationController mConfigurationController; 103 104 private final Provider<QSLongPressEffect> mLongPressEffectProvider; 105 106 private boolean mDestroyed = false; 107 108 private boolean mMediaVisibleFromInteractor; 109 110 private final Consumer<Boolean> mMediaOrRecommendationVisibleConsumer = mediaVisible -> { 111 mMediaVisibleFromInteractor = mediaVisible; 112 setLayoutForMediaInScene(); 113 }; 114 115 private DisposableHandle mJavaAdapterDisposableHandle; 116 117 private boolean mLastListening; 118 119 private final ConfigurationListener mConfigurationListener = new ConfigurationListener() { 120 @Override 121 public void onConfigChanged(Configuration newConfig) { 122 final boolean previousSplitShadeState = mShouldUseSplitNotificationShade; 123 final int previousOrientation = mLastOrientation; 124 final int previousScreenLayout = mLastScreenLayout; 125 mShouldUseSplitNotificationShade = mSplitShadeStateController 126 .shouldUseSplitNotificationShade(getResources()); 127 mLastOrientation = newConfig.orientation; 128 mLastScreenLayout = newConfig.screenLayout; 129 130 mQSLogger.logOnConfigurationChanged( 131 /* oldOrientation= */ previousOrientation, 132 /* newOrientation= */ mLastOrientation, 133 /* oldShouldUseSplitShade= */ previousSplitShadeState, 134 /* newShouldUseSplitShade= */ mShouldUseSplitNotificationShade, 135 /* oldScreenLayout= */ previousScreenLayout, 136 /* newScreenLayout= */ mLastScreenLayout, 137 /* containerName= */ mView.getDumpableTag()); 138 139 if (SceneContainerFlag.isEnabled()) { 140 setLayoutForMediaInScene(); 141 } else { 142 switchTileLayoutIfNeeded(); 143 } 144 onConfigurationChanged(); 145 if (previousSplitShadeState != mShouldUseSplitNotificationShade) { 146 onSplitShadeChanged(mShouldUseSplitNotificationShade); 147 } 148 } 149 }; 150 /** When {@link ShadeWindowGoesAround} is enabled, this listener is not used anymore.*/ 151 @VisibleForTesting 152 @Deprecated 153 protected final QSPanel.OnConfigurationChangedListener mOnConfigurationChangedListener = 154 newConfig -> mConfigurationListener.onConfigChanged(newConfig); 155 onConfigurationChanged()156 protected void onConfigurationChanged() { } 157 onSplitShadeChanged(boolean shouldUseSplitNotificationShade)158 protected void onSplitShadeChanged(boolean shouldUseSplitNotificationShade) { } 159 160 private final Function1<Boolean, Unit> mMediaHostVisibilityListener = (visible) -> { 161 if (mMediaVisibilityChangedListener != null) { 162 mMediaVisibilityChangedListener.accept(visible); 163 } 164 switchTileLayout(false); 165 return null; 166 }; 167 168 private boolean mUsingHorizontalLayout; 169 170 @Nullable 171 private Runnable mUsingHorizontalLayoutChangedListener; 172 QSPanelControllerBase( T view, QSHost host, QSCustomizerController qsCustomizerController, boolean usingMediaPlayer, MediaHost mediaHost, MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger, DumpManager dumpManager, SplitShadeStateController splitShadeStateController, Provider<QSLongPressEffect> longPressEffectProvider, @ShadeDisplayAware ConfigurationController configurationController )173 protected QSPanelControllerBase( 174 T view, 175 QSHost host, 176 QSCustomizerController qsCustomizerController, 177 boolean usingMediaPlayer, 178 MediaHost mediaHost, 179 MetricsLogger metricsLogger, 180 UiEventLogger uiEventLogger, 181 QSLogger qsLogger, 182 DumpManager dumpManager, 183 SplitShadeStateController splitShadeStateController, 184 Provider<QSLongPressEffect> longPressEffectProvider, 185 @ShadeDisplayAware ConfigurationController configurationController 186 ) { 187 super(view); 188 mHost = host; 189 mQsCustomizerController = qsCustomizerController; 190 mUsingMediaPlayer = usingMediaPlayer; 191 mMediaHost = mediaHost; 192 mMetricsLogger = metricsLogger; 193 mUiEventLogger = uiEventLogger; 194 mQSLogger = qsLogger; 195 mDumpManager = dumpManager; 196 mSplitShadeStateController = splitShadeStateController; 197 mConfigurationController = configurationController; 198 mShouldUseSplitNotificationShade = 199 mSplitShadeStateController.shouldUseSplitNotificationShade(getResources()); 200 mLongPressEffectProvider = longPressEffectProvider; 201 } 202 203 @Override onInit()204 protected void onInit() { 205 mView.initialize(mQSLogger, mUsingMediaPlayer); 206 mQSLogger.logAllTilesChangeListening(mView.isListening(), mView.getDumpableTag(), ""); 207 mHost.addCallback(mQSHostCallback); 208 if (SceneContainerFlag.isEnabled()) { 209 registerForMediaInteractorChanges(); 210 } 211 } 212 213 /** 214 * @return the media host for this panel 215 */ getMediaHost()216 public MediaHost getMediaHost() { 217 return mMediaHost; 218 } 219 setSquishinessFraction(float squishinessFraction)220 public void setSquishinessFraction(float squishinessFraction) { 221 mView.setSquishinessFraction(squishinessFraction); 222 } 223 224 @Override destroy()225 public void destroy() { 226 // Don't call super as this may be called before the view is dettached and calling super 227 // will remove the attach listener. We don't need to do that, because once this object is 228 // detached from the graph, it will be gc. 229 mHost.removeCallback(mQSHostCallback); 230 mDestroyed = true; 231 for (TileRecord record : mRecords) { 232 record.tile.removeCallback(record.callback); 233 mView.removeTile(record); 234 } 235 mRecords.clear(); 236 if (mJavaAdapterDisposableHandle != null) { 237 mJavaAdapterDisposableHandle.dispose(); 238 } 239 } 240 241 @Override onViewAttached()242 protected void onViewAttached() { 243 mQsTileRevealController = createTileRevealController(); 244 if (mQsTileRevealController != null) { 245 mQsTileRevealController.setExpansion(mRevealExpansion); 246 } 247 248 if (!SceneContainerFlag.isEnabled()) { 249 mMediaHost.addVisibilityChangeListener(mMediaHostVisibilityListener); 250 } 251 if (ShadeWindowGoesAround.isEnabled()) { 252 mConfigurationController.addCallback(mConfigurationListener); 253 } else { 254 mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener); 255 } 256 // We were not attached and the configuration may have changed, trigger the listener. 257 if (mView.hadConfigurationChangeWhileDetached()) { 258 mOnConfigurationChangedListener.onConfigurationChange( 259 getContext().getResources().getConfiguration() 260 ); 261 } 262 setTiles(); 263 mLastOrientation = getResources().getConfiguration().orientation; 264 mLastScreenLayout = getResources().getConfiguration().screenLayout; 265 mQSLogger.logOnViewAttached(mLastOrientation, mView.getDumpableTag()); 266 if (SceneContainerFlag.isEnabled()) { 267 setLayoutForMediaInScene(); 268 } 269 switchTileLayout(true); 270 271 mDumpManager.registerDumpable(mView.getDumpableTag(), this); 272 273 setListening(mLastListening); 274 } 275 registerForMediaInteractorChanges()276 private void registerForMediaInteractorChanges() { 277 mJavaAdapterDisposableHandle = JavaAdapterKt.collectFlow( 278 mView, 279 getMediaVisibleFlow(), 280 mMediaOrRecommendationVisibleConsumer 281 ); 282 } 283 getMediaVisibleFlow()284 abstract StateFlow<Boolean> getMediaVisibleFlow(); 285 286 @Override onViewDetached()287 protected void onViewDetached() { 288 mQSLogger.logOnViewDetached(mLastOrientation, mView.getDumpableTag()); 289 if (ShadeWindowGoesAround.isEnabled()) { 290 mConfigurationController.removeCallback(mConfigurationListener); 291 } else { 292 mView.removeOnConfigurationChangedListener(mOnConfigurationChangedListener); 293 } 294 // Call directly so mLastListening is not modified. We want that to have the last actual 295 // value. 296 mView.getTileLayout().setListening(false, mUiEventLogger); 297 mView.setListening(false); 298 299 mMediaHost.removeVisibilityChangeListener(mMediaHostVisibilityListener); 300 301 mDumpManager.unregisterDumpable(mView.getDumpableTag()); 302 } 303 304 @Nullable createTileRevealController()305 protected QSTileRevealController createTileRevealController() { 306 return null; 307 } 308 309 /** */ setTiles()310 public void setTiles() { 311 setTiles(mHost.getTiles(), false); 312 } 313 314 /** */ setTiles(Collection<QSTile> tiles, boolean collapsedView)315 public void setTiles(Collection<QSTile> tiles, boolean collapsedView) { 316 if (mDestroyed) return; 317 // TODO(b/168904199): move this logic into QSPanelController. 318 if (!collapsedView && mQsTileRevealController != null) { 319 mQsTileRevealController.updateRevealedTiles(tiles); 320 } 321 boolean shouldChangeAll = false; 322 // If the new tiles are a prefix of the old tiles, we delete the extra tiles (from the old). 323 // If not (even if they share a prefix) we remove all and add all the new ones. 324 if (tiles.size() <= mRecords.size()) { 325 int i = 0; 326 // Iterate through the requested tiles and check if they are the same as the existing 327 // tiles. 328 for (QSTile tile : tiles) { 329 if (tile != mRecords.get(i).tile) { 330 shouldChangeAll = true; 331 break; 332 } 333 i++; 334 } 335 336 // If the first tiles are the same as the new ones, we reuse them and remove any extra 337 // tiles. 338 if (!shouldChangeAll && i < mRecords.size()) { 339 List<TileRecord> extraRecords = mRecords.subList(i, mRecords.size()); 340 for (QSPanelControllerBase.TileRecord record : extraRecords) { 341 mView.removeTile(record); 342 record.tile.removeCallback(record.callback); 343 } 344 extraRecords.clear(); 345 mCachedSpecs = getTilesSpecs(); 346 } 347 } else { 348 shouldChangeAll = true; 349 } 350 351 // If we detected that the existing tiles are different than the requested tiles, clear them 352 // and add the new tiles. 353 if (shouldChangeAll) { 354 for (QSPanelControllerBase.TileRecord record : mRecords) { 355 mView.removeTile(record); 356 record.tile.removeCallback(record.callback); 357 } 358 mRecords.clear(); 359 mCachedSpecs = ""; 360 for (QSTile tile : tiles) { 361 addTile(tile, collapsedView); 362 } 363 } else { 364 for (QSPanelControllerBase.TileRecord record : mRecords) { 365 record.tile.addCallback(record.callback); 366 } 367 } 368 } 369 370 /** */ refreshAllTiles()371 public void refreshAllTiles() { 372 for (QSPanelControllerBase.TileRecord r : mRecords) { 373 if (!r.tile.isListening()) { 374 // Only refresh tiles that were not already in the listening state. Tiles that are 375 // already listening is as if they are already expanded (for example, tiles that 376 // are both in QQS and QS). 377 r.tile.refreshState(); 378 } 379 } 380 } 381 addTile(final QSTile tile, boolean collapsedView)382 private void addTile(final QSTile tile, boolean collapsedView) { 383 QSLongPressEffect longPressEffect = mLongPressEffectProvider.get(); 384 final QSTileViewImpl tileView = new QSTileViewImpl( 385 getContext(), collapsedView, longPressEffect); 386 final TileRecord r = new TileRecord(tile, tileView); 387 // TODO(b/250618218): Remove the QSLogger in QSTileViewImpl once we know the root cause of 388 // b/250618218. 389 try { 390 QSTileViewImpl qsTileView = (QSTileViewImpl) (r.tileView); 391 if (qsTileView != null) { 392 qsTileView.setQsLogger(mQSLogger); 393 } 394 } catch (ClassCastException e) { 395 Log.e(TAG, "Failed to cast QSTileView to QSTileViewImpl", e); 396 } 397 mView.addTile(r); 398 mRecords.add(r); 399 mCachedSpecs = getTilesSpecs(); 400 } 401 402 /** */ clickTile(ComponentName tile)403 public void clickTile(ComponentName tile) { 404 final String spec = CustomTile.toSpec(tile); 405 for (TileRecord record : mRecords) { 406 if (record.tile.getTileSpec().equals(spec)) { 407 record.tile.click(null /* view */); 408 break; 409 } 410 } 411 } 412 areThereTiles()413 boolean areThereTiles() { 414 return !mRecords.isEmpty(); 415 } 416 417 @Nullable getTileView(QSTile tile)418 QSTileView getTileView(QSTile tile) { 419 for (QSPanelControllerBase.TileRecord r : mRecords) { 420 if (r.tile == tile) { 421 return r.tileView; 422 } 423 } 424 return null; 425 } 426 getTileView(String spec)427 QSTileView getTileView(String spec) { 428 for (QSPanelControllerBase.TileRecord r : mRecords) { 429 if (Objects.equals(r.tile.getTileSpec(), spec)) { 430 return r.tileView; 431 } 432 } 433 return null; 434 } 435 getTilesSpecs()436 private String getTilesSpecs() { 437 return mRecords.stream() 438 .map(tileRecord -> tileRecord.tile.getTileSpec()) 439 .collect(Collectors.joining(",")); 440 } 441 442 /** */ setExpanded(boolean expanded)443 public void setExpanded(boolean expanded) { 444 if (mView.isExpanded() == expanded) { 445 return; 446 } 447 mQSLogger.logPanelExpanded(expanded, mView.getDumpableTag()); 448 449 mView.setExpanded(expanded); 450 mMetricsLogger.visibility(MetricsEvent.QS_PANEL, expanded); 451 if (!expanded) { 452 mUiEventLogger.log(mView.closePanelEvent()); 453 closeDetail(); 454 } else { 455 mUiEventLogger.log(mView.openPanelEvent()); 456 logTiles(); 457 } 458 } 459 460 /** */ closeDetail()461 public void closeDetail() { 462 if (mQsCustomizerController.isShown()) { 463 mQsCustomizerController.hide(); 464 return; 465 } 466 } 467 setListening(boolean listening)468 void setListening(boolean listening) { 469 mLastListening = listening; 470 if (mView.isListening() == listening) return; 471 mView.setListening(listening); 472 473 if (mView.getTileLayout() != null) { 474 mQSLogger.logAllTilesChangeListening(listening, mView.getDumpableTag(), mCachedSpecs); 475 mView.getTileLayout().setListening(listening, mUiEventLogger); 476 } 477 478 if (mView.isListening()) { 479 refreshAllTiles(); 480 } 481 } 482 switchTileLayoutIfNeeded()483 private void switchTileLayoutIfNeeded() { 484 switchTileLayout(/* force= */ false); 485 } 486 switchTileLayout(boolean force)487 boolean switchTileLayout(boolean force) { 488 /* Whether or not the panel currently contains a media player. */ 489 boolean horizontal = shouldUseHorizontalLayout(); 490 if ((!SceneContainerFlag.isEnabled() && horizontal != mUsingHorizontalLayout) || force) { 491 mQSLogger.logSwitchTileLayout(horizontal, mUsingHorizontalLayout, force, 492 mView.getDumpableTag()); 493 mUsingHorizontalLayout = horizontal; 494 mView.setUsingHorizontalLayout(mUsingHorizontalLayout, mMediaHost.getHostView(), force); 495 updateMediaDisappearParameters(); 496 if (mUsingHorizontalLayoutChangedListener != null) { 497 mUsingHorizontalLayoutChangedListener.run(); 498 } 499 return true; 500 } 501 return false; 502 } 503 setLayoutForMediaInScene()504 private void setLayoutForMediaInScene() { 505 boolean withMedia = shouldUseHorizontalInScene(); 506 mView.setColumnRowLayout(withMedia); 507 } 508 509 /** 510 * Update the way the media disappears based on if we're using the horizontal layout 511 */ updateMediaDisappearParameters()512 void updateMediaDisappearParameters() { 513 if (!mUsingMediaPlayer) { 514 return; 515 } 516 DisappearParameters parameters = mMediaHost.getDisappearParameters(); 517 if (mUsingHorizontalLayout) { 518 // Only height remaining 519 parameters.getDisappearSize().set(0.0f, 0.4f); 520 // Disappearing on the right side on the top 521 parameters.getGonePivot().set(1.0f, 0.0f); 522 // translating a bit horizontal 523 parameters.getContentTranslationFraction().set(0.25f, 1.0f); 524 parameters.setDisappearEnd(0.6f); 525 } else { 526 // Only width remaining 527 parameters.getDisappearSize().set(1.0f, 0.0f); 528 // Disappearing on the top 529 parameters.getGonePivot().set(0.0f, 0.0f); 530 // translating a bit vertical 531 parameters.getContentTranslationFraction().set(0.0f, 1f); 532 parameters.setDisappearEnd(0.95f); 533 } 534 parameters.setFadeStartPosition(0.95f); 535 parameters.setDisappearStart(0.0f); 536 mMediaHost.setDisappearParameters(parameters); 537 } 538 shouldUseHorizontalLayout()539 boolean shouldUseHorizontalLayout() { 540 if (mShouldUseSplitNotificationShade) { 541 return false; 542 } 543 return mUsingMediaPlayer && mMediaHost.getVisible() 544 && mLastOrientation == Configuration.ORIENTATION_LANDSCAPE 545 && (mLastScreenLayout & Configuration.SCREENLAYOUT_LONG_MASK) 546 == Configuration.SCREENLAYOUT_LONG_YES; 547 } 548 shouldUseHorizontalInScene()549 boolean shouldUseHorizontalInScene() { 550 if (mShouldUseSplitNotificationShade) { 551 return false; 552 } 553 return mMediaVisibleFromInteractor 554 && mLastOrientation == Configuration.ORIENTATION_LANDSCAPE 555 && (mLastScreenLayout & Configuration.SCREENLAYOUT_LONG_MASK) 556 == Configuration.SCREENLAYOUT_LONG_YES; 557 } 558 logTiles()559 private void logTiles() { 560 for (int i = 0; i < mRecords.size(); i++) { 561 QSTile tile = mRecords.get(i).tile; 562 mMetricsLogger.write(tile.populate(new LogMaker(tile.getMetricsCategory()) 563 .setType(MetricsEvent.TYPE_OPEN))); 564 } 565 } 566 567 /** Set the expansion on the associated {@link QSTileRevealController}. */ setRevealExpansion(float expansion)568 public void setRevealExpansion(float expansion) { 569 mRevealExpansion = expansion; 570 if (mQsTileRevealController != null) { 571 mQsTileRevealController.setExpansion(expansion); 572 } 573 } 574 575 @Override dump(PrintWriter pw, String[] args)576 public void dump(PrintWriter pw, String[] args) { 577 pw.println(getClass().getSimpleName() + ":"); 578 pw.println(" Tile records:"); 579 for (QSPanelControllerBase.TileRecord record : mRecords) { 580 if (record.tile instanceof Dumpable) { 581 pw.print(" "); ((Dumpable) record.tile).dump(pw, args); 582 pw.print(" "); pw.println(record.tileView.toString()); 583 } 584 } 585 if (mMediaHost != null) { 586 pw.println(" media bounds: " + mMediaHost.getCurrentBounds()); 587 pw.println(" media visibility: " + mMediaHost.getVisible()); 588 pw.println(" horizontal layout: " + mUsingHorizontalLayout); 589 pw.println(" last orientation: " + mLastOrientation); 590 } 591 pw.println(" mShouldUseSplitNotificationShade: " + mShouldUseSplitNotificationShade); 592 } 593 getTileLayout()594 public QSPanel.QSTileLayout getTileLayout() { 595 return mView.getTileLayout(); 596 } 597 598 /** 599 * Add a listener for when the media visibility changes. 600 */ setMediaVisibilityChangedListener(@onNull Consumer<Boolean> listener)601 public void setMediaVisibilityChangedListener(@NonNull Consumer<Boolean> listener) { 602 mMediaVisibilityChangedListener = listener; 603 } 604 605 /** 606 * Add a listener when the horizontal layout changes 607 */ setUsingHorizontalLayoutChangeListener(Runnable listener)608 public void setUsingHorizontalLayoutChangeListener(Runnable listener) { 609 mUsingHorizontalLayoutChangedListener = listener; 610 } 611 612 @Nullable getBrightnessView()613 public View getBrightnessView() { 614 return mView.getBrightnessView(); 615 } 616 617 /** 618 * Set a listener to collapse/expand QS. 619 * @param action 620 */ setCollapseExpandAction(Runnable action)621 public void setCollapseExpandAction(Runnable action) { 622 mView.setCollapseExpandAction(action); 623 } 624 625 /** Sets whether we are currently on lock screen. */ setIsOnKeyguard(boolean isOnKeyguard)626 public void setIsOnKeyguard(boolean isOnKeyguard) { 627 boolean isOnSplitShadeLockscreen = mShouldUseSplitNotificationShade && isOnKeyguard; 628 // When the split shade is expanding on lockscreen, the media container transitions from the 629 // lockscreen to QS. 630 // We have to prevent the media container position from moving during the transition to have 631 // a smooth translation animation without stuttering. 632 mView.setShouldMoveMediaOnExpansion(!isOnSplitShadeLockscreen); 633 } 634 635 /** */ 636 public static final class TileRecord { TileRecord(QSTile tile, com.android.systemui.plugins.qs.QSTileView tileView)637 public TileRecord(QSTile tile, com.android.systemui.plugins.qs.QSTileView tileView) { 638 this.tile = tile; 639 this.tileView = tileView; 640 } 641 642 public QSTile tile; 643 public com.android.systemui.plugins.qs.QSTileView tileView; 644 @Nullable 645 public QSTile.Callback callback; 646 } 647 } 648