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