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