• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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