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