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.os.Bundle;
27 import android.os.Handler;
28 import android.os.Message;
29 import android.util.ArrayMap;
30 import android.util.AttributeSet;
31 import android.util.Log;
32 import android.view.Gravity;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.ViewGroup;
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.DetailAdapter;
44 import com.android.systemui.plugins.qs.QSTile;
45 import com.android.systemui.settings.brightness.BrightnessSliderController;
46 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
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     /**
66      * The index where the content starts that needs to be moved between parents
67      */
68     private int mMovableContentStartIndex;
69 
70     @Nullable
71     protected View mBrightnessView;
72     @Nullable
73     protected BrightnessSliderController mToggleSliderController;
74 
75     private final H mHandler = new H();
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 QSDetail.Callback mCallback;
83     protected QSTileHost mHost;
84     private final List<OnConfigurationChangedListener> mOnConfigurationChangedListeners =
85             new ArrayList<>();
86 
87     @Nullable
88     protected View mSecurityFooter;
89 
90     @Nullable
91     protected View mFooter;
92 
93     @Nullable
94     private ViewGroup mHeaderContainer;
95     private PageIndicator mFooterPageIndicator;
96     private int mContentMarginStart;
97     private int mContentMarginEnd;
98     private boolean mUsingHorizontalLayout;
99 
100     private Record mDetailRecord;
101 
102     private BrightnessMirrorController mBrightnessMirrorController;
103     private LinearLayout mHorizontalLinearLayout;
104     protected LinearLayout mHorizontalContentContainer;
105 
106     protected QSTileLayout mTileLayout;
107     private float mSquishinessFraction = 1f;
108     private final ArrayMap<View, Integer> mChildrenLayoutTop = new ArrayMap<>();
109 
QSPanel(Context context, AttributeSet attrs)110     public QSPanel(Context context, AttributeSet attrs) {
111         super(context, attrs);
112         mUsingMediaPlayer = useQsMediaPlayer(context);
113         mMediaTotalBottomMargin = getResources().getDimensionPixelSize(
114                 R.dimen.quick_settings_bottom_margin_media);
115         mMediaTopMargin = getResources().getDimensionPixelSize(
116                 R.dimen.qs_tile_margin_vertical);
117         mContext = context;
118 
119         setOrientation(VERTICAL);
120 
121         mMovableContentStartIndex = getChildCount();
122 
123     }
124 
initialize()125     void initialize() {
126         mTileLayout = getOrCreateTileLayout();
127 
128         if (mUsingMediaPlayer) {
129             mHorizontalLinearLayout = new RemeasuringLinearLayout(mContext);
130             mHorizontalLinearLayout.setOrientation(LinearLayout.HORIZONTAL);
131             mHorizontalLinearLayout.setClipChildren(false);
132             mHorizontalLinearLayout.setClipToPadding(false);
133 
134             mHorizontalContentContainer = new RemeasuringLinearLayout(mContext);
135             mHorizontalContentContainer.setOrientation(LinearLayout.VERTICAL);
136             mHorizontalContentContainer.setClipChildren(true);
137             mHorizontalContentContainer.setClipToPadding(false);
138 
139             LayoutParams lp = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1);
140             int marginSize = (int) mContext.getResources().getDimension(R.dimen.qs_media_padding);
141             lp.setMarginStart(0);
142             lp.setMarginEnd(marginSize);
143             lp.gravity = Gravity.CENTER_VERTICAL;
144             mHorizontalLinearLayout.addView(mHorizontalContentContainer, lp);
145 
146             lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0, 1);
147             addView(mHorizontalLinearLayout, lp);
148         }
149     }
150 
151     /**
152      * Add brightness view above the tile layout.
153      *
154      * Used to add the brightness slider after construction.
155      */
setBrightnessView(@onNull View view)156     public void setBrightnessView(@NonNull View view) {
157         if (mBrightnessView != null) {
158             removeView(mBrightnessView);
159             mMovableContentStartIndex--;
160         }
161         addView(view, 0);
162         mBrightnessView = view;
163 
164         setBrightnessViewMargin();
165 
166         mMovableContentStartIndex++;
167     }
168 
setBrightnessViewMargin()169     private void setBrightnessViewMargin() {
170         if (mBrightnessView != null) {
171             MarginLayoutParams lp = (MarginLayoutParams) mBrightnessView.getLayoutParams();
172             lp.topMargin = mContext.getResources()
173                     .getDimensionPixelSize(R.dimen.qs_brightness_margin_top);
174             lp.bottomMargin = mContext.getResources()
175                     .getDimensionPixelSize(R.dimen.qs_brightness_margin_bottom);
176             mBrightnessView.setLayoutParams(lp);
177         }
178     }
179 
180     /** */
getOrCreateTileLayout()181     public QSTileLayout getOrCreateTileLayout() {
182         if (mTileLayout == null) {
183             mTileLayout = (QSTileLayout) LayoutInflater.from(mContext)
184                     .inflate(R.layout.qs_paged_tile_layout, this, false);
185             mTileLayout.setSquishinessFraction(mSquishinessFraction);
186         }
187         return mTileLayout;
188     }
189 
setSquishinessFraction(float squishinessFraction)190     public void setSquishinessFraction(float squishinessFraction) {
191         if (Float.compare(squishinessFraction, mSquishinessFraction) == 0) {
192             return;
193         }
194         mSquishinessFraction = squishinessFraction;
195         if (mTileLayout == null) {
196             return;
197         }
198         mTileLayout.setSquishinessFraction(squishinessFraction);
199         if (getMeasuredWidth() == 0) {
200             return;
201         }
202         updateViewPositions();
203     }
204 
205     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)206     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
207         if (mTileLayout instanceof PagedTileLayout) {
208             // Since PageIndicator gets measured before PagedTileLayout, we preemptively set the
209             // # of pages before the measurement pass so PageIndicator is measured appropriately
210             if (mFooterPageIndicator != null) {
211                 mFooterPageIndicator.setNumPages(((PagedTileLayout) mTileLayout).getNumPages());
212             }
213 
214             // In landscape, mTileLayout's parent is not the panel but a view that contains the
215             // tile layout and the media controls.
216             if (((View) mTileLayout).getParent() == this) {
217                 // Allow the UI to be as big as it want's to, we're in a scroll view
218                 int newHeight = 10000;
219                 int availableHeight = MeasureSpec.getSize(heightMeasureSpec);
220                 int excessHeight = newHeight - availableHeight;
221                 // Measure with EXACTLY. That way, The content will only use excess height and will
222                 // be measured last, after other views and padding is accounted for. This only
223                 // works because our Layouts in here remeasure themselves with the exact content
224                 // height.
225                 heightMeasureSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY);
226                 ((PagedTileLayout) mTileLayout).setExcessHeight(excessHeight);
227             }
228         }
229         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
230 
231         // We want all the logic of LinearLayout#onMeasure, and for it to assign the excess space
232         // not used by the other children to PagedTileLayout. However, in this case, LinearLayout
233         // assumes that PagedTileLayout would use all the excess space. This is not the case as
234         // PagedTileLayout height is quantized (because it shows a certain number of rows).
235         // Therefore, after everything is measured, we need to make sure that we add up the correct
236         // total height
237         int height = getPaddingBottom() + getPaddingTop();
238         int numChildren = getChildCount();
239         for (int i = 0; i < numChildren; i++) {
240             View child = getChildAt(i);
241             if (child.getVisibility() != View.GONE) {
242                 height += child.getMeasuredHeight();
243                 MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams();
244                 height += layoutParams.topMargin + layoutParams.bottomMargin;
245             }
246         }
247         setMeasuredDimension(getMeasuredWidth(), height);
248     }
249 
250     @Override
onLayout(boolean changed, int l, int t, int r, int b)251     protected void onLayout(boolean changed, int l, int t, int r, int b) {
252         super.onLayout(changed, l, t, r, b);
253         for (int i = 0; i < getChildCount(); i++) {
254             View child = getChildAt(i);
255             mChildrenLayoutTop.put(child, child.getTop());
256         }
257         updateViewPositions();
258     }
259 
updateViewPositions()260     private void updateViewPositions() {
261         // Adjust view positions based on tile squishing
262         int tileHeightOffset = mTileLayout.getTilesHeight() - mTileLayout.getHeight();
263 
264         boolean move = false;
265         for (int i = 0; i < getChildCount(); i++) {
266             View child = getChildAt(i);
267             if (move) {
268                 int top = mChildrenLayoutTop.get(child);
269                 child.setLeftTopRightBottom(child.getLeft(), top + tileHeightOffset,
270                         child.getRight(), top + tileHeightOffset + child.getHeight());
271             }
272             if (child == mTileLayout) {
273                 move = true;
274             }
275         }
276     }
277 
getDumpableTag()278     protected String getDumpableTag() {
279         return TAG;
280     }
281 
282     @Override
onTuningChanged(String key, String newValue)283     public void onTuningChanged(String key, String newValue) {
284         if (QS_SHOW_BRIGHTNESS.equals(key) && mBrightnessView != null) {
285             updateViewVisibilityForTuningValue(mBrightnessView, newValue);
286         }
287     }
288 
updateViewVisibilityForTuningValue(View view, @Nullable String newValue)289     private void updateViewVisibilityForTuningValue(View view, @Nullable String newValue) {
290         view.setVisibility(TunerService.parseIntegerSwitch(newValue, true) ? VISIBLE : GONE);
291     }
292 
293     /** */
openDetails(QSTile tile)294     public void openDetails(QSTile tile) {
295         // If there's no tile with that name (as defined in QSFactoryImpl or other QSFactory),
296         // QSFactory will not be able to create a tile and getTile will return null
297         if (tile != null) {
298             showDetailAdapter(true, tile.getDetailAdapter(), new int[]{getWidth() / 2, 0});
299         }
300     }
301 
302     @Nullable
getBrightnessView()303     View getBrightnessView() {
304         return mBrightnessView;
305     }
306 
setCallback(QSDetail.Callback callback)307     public void setCallback(QSDetail.Callback callback) {
308         mCallback = callback;
309     }
310 
311     /**
312      * Links the footer's page indicator, which is used in landscape orientation to save space.
313      *
314      * @param pageIndicator indicator to use for page scrolling
315      */
setFooterPageIndicator(PageIndicator pageIndicator)316     public void setFooterPageIndicator(PageIndicator pageIndicator) {
317         if (mTileLayout instanceof PagedTileLayout) {
318             mFooterPageIndicator = pageIndicator;
319             updatePageIndicator();
320         }
321     }
322 
updatePageIndicator()323     private void updatePageIndicator() {
324         if (mTileLayout instanceof PagedTileLayout) {
325             if (mFooterPageIndicator != null) {
326                 mFooterPageIndicator.setVisibility(View.GONE);
327 
328                 ((PagedTileLayout) mTileLayout).setPageIndicator(mFooterPageIndicator);
329             }
330         }
331     }
332 
getHost()333     public QSTileHost getHost() {
334         return mHost;
335     }
336 
updateResources()337     public void updateResources() {
338         updatePadding();
339 
340         updatePageIndicator();
341 
342         setBrightnessViewMargin();
343 
344         if (mTileLayout != null) {
345             mTileLayout.updateResources();
346         }
347     }
348 
updatePadding()349     protected void updatePadding() {
350         final Resources res = mContext.getResources();
351         int padding = res.getDimensionPixelSize(R.dimen.qs_panel_padding_top);
352         setPaddingRelative(getPaddingStart(),
353                 padding,
354                 getPaddingEnd(),
355                 res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom));
356     }
357 
addOnConfigurationChangedListener(OnConfigurationChangedListener listener)358     void addOnConfigurationChangedListener(OnConfigurationChangedListener listener) {
359         mOnConfigurationChangedListeners.add(listener);
360     }
361 
removeOnConfigurationChangedListener(OnConfigurationChangedListener listener)362     void removeOnConfigurationChangedListener(OnConfigurationChangedListener listener) {
363         mOnConfigurationChangedListeners.remove(listener);
364     }
365 
366     @Override
onConfigurationChanged(Configuration newConfig)367     protected void onConfigurationChanged(Configuration newConfig) {
368         super.onConfigurationChanged(newConfig);
369         mOnConfigurationChangedListeners.forEach(
370                 listener -> listener.onConfigurationChange(newConfig));
371     }
372 
373     @Override
onFinishInflate()374     protected void onFinishInflate() {
375         super.onFinishInflate();
376         mFooter = findViewById(R.id.qs_footer);
377     }
378 
updateHorizontalLinearLayoutMargins()379     private void updateHorizontalLinearLayoutMargins() {
380         if (mHorizontalLinearLayout != null && !displayMediaMarginsOnMedia()) {
381             LayoutParams lp = (LayoutParams) mHorizontalLinearLayout.getLayoutParams();
382             lp.bottomMargin = Math.max(mMediaTotalBottomMargin - getPaddingBottom(), 0);
383             mHorizontalLinearLayout.setLayoutParams(lp);
384         }
385     }
386 
387     /**
388      * @return true if the margin bottom of the media view should be on the media host or false
389      *         if they should be on the HorizontalLinearLayout. Returning {@code false} is useful
390      *         to visually center the tiles in the Media view, which doesn't work when the
391      *         expanded panel actually scrolls.
392      */
displayMediaMarginsOnMedia()393     protected boolean displayMediaMarginsOnMedia() {
394         return true;
395     }
396 
397     /**
398      * @return true if the media view needs margin on the top to separate it from the qs tiles
399      */
mediaNeedsTopMargin()400     protected boolean mediaNeedsTopMargin() {
401         return false;
402     }
403 
needsDynamicRowsAndColumns()404     private boolean needsDynamicRowsAndColumns() {
405         return true;
406     }
407 
switchAllContentToParent(ViewGroup parent, QSTileLayout newLayout)408     private void switchAllContentToParent(ViewGroup parent, QSTileLayout newLayout) {
409         int index = parent == this ? mMovableContentStartIndex : 0;
410 
411         // Let's first move the tileLayout to the new parent, since that should come first.
412         switchToParent((View) newLayout, parent, index);
413         index++;
414 
415         if (mFooter != null) {
416             // Then the footer with the settings
417             switchToParent(mFooter, parent, index);
418             index++;
419         }
420     }
421 
422     /** Switch the security footer between top and bottom of QS depending on orientation. */
switchSecurityFooter(boolean shouldUseSplitNotificationShade)423     public void switchSecurityFooter(boolean shouldUseSplitNotificationShade) {
424         if (mSecurityFooter == null) return;
425 
426         if (!shouldUseSplitNotificationShade
427                 && mContext.getResources().getConfiguration().orientation
428                 == Configuration.ORIENTATION_LANDSCAPE && mHeaderContainer != null) {
429             // Adding the security view to the header, that enables us to avoid scrolling
430             switchToParent(mSecurityFooter, mHeaderContainer, 0);
431         } else {
432             // Add after the footer
433             int index = indexOfChild(mFooter);
434             switchToParent(mSecurityFooter, this, index + 1);
435         }
436     }
437 
switchToParent(View child, ViewGroup parent, int index)438     private void switchToParent(View child, ViewGroup parent, int index) {
439         switchToParent(child, parent, index, getDumpableTag());
440     }
441 
442     /** Call when orientation has changed and MediaHost needs to be adjusted. */
reAttachMediaHost(ViewGroup hostView, boolean horizontal)443     private void reAttachMediaHost(ViewGroup hostView, boolean horizontal) {
444         if (!mUsingMediaPlayer) {
445             return;
446         }
447         ViewGroup newParent = horizontal ? mHorizontalLinearLayout : this;
448         ViewGroup currentParent = (ViewGroup) hostView.getParent();
449         if (currentParent != newParent) {
450             if (currentParent != null) {
451                 currentParent.removeView(hostView);
452             }
453             newParent.addView(hostView);
454             LinearLayout.LayoutParams layoutParams = (LayoutParams) hostView.getLayoutParams();
455             layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
456             layoutParams.width = horizontal ? 0 : ViewGroup.LayoutParams.MATCH_PARENT;
457             layoutParams.weight = horizontal ? 1f : 0;
458             // Add any bottom margin, such that the total spacing is correct. This is only
459             // necessary if the view isn't horizontal, since otherwise the padding is
460             // carried in the parent of this view (to ensure correct vertical alignment)
461             layoutParams.bottomMargin = !horizontal || displayMediaMarginsOnMedia()
462                     ? Math.max(mMediaTotalBottomMargin - getPaddingBottom(), 0) : 0;
463             layoutParams.topMargin = mediaNeedsTopMargin() && !horizontal
464                     ? mMediaTopMargin : 0;
465         }
466     }
467 
setExpanded(boolean expanded)468     public void setExpanded(boolean expanded) {
469         if (mExpanded == expanded) return;
470         mExpanded = expanded;
471         if (!mExpanded && mTileLayout instanceof PagedTileLayout) {
472             ((PagedTileLayout) mTileLayout).setCurrentItem(0, false);
473         }
474     }
475 
setPageListener(final PagedTileLayout.PageListener pageListener)476     public void setPageListener(final PagedTileLayout.PageListener pageListener) {
477         if (mTileLayout instanceof PagedTileLayout) {
478             ((PagedTileLayout) mTileLayout).setPageListener(pageListener);
479         }
480     }
481 
isExpanded()482     public boolean isExpanded() {
483         return mExpanded;
484     }
485 
486     /** */
setListening(boolean listening)487     public void setListening(boolean listening) {
488         mListening = listening;
489     }
490 
showDetailAdapter(boolean show, DetailAdapter adapter, int[] locationInWindow)491     public void showDetailAdapter(boolean show, DetailAdapter adapter, int[] locationInWindow) {
492         int xInWindow = locationInWindow[0];
493         int yInWindow = locationInWindow[1];
494         ((View) getParent()).getLocationInWindow(locationInWindow);
495 
496         Record r = new Record();
497         r.detailAdapter = adapter;
498         r.x = xInWindow - locationInWindow[0];
499         r.y = yInWindow - locationInWindow[1];
500 
501         locationInWindow[0] = xInWindow;
502         locationInWindow[1] = yInWindow;
503 
504         showDetail(show, r);
505     }
506 
showDetail(boolean show, Record r)507     protected void showDetail(boolean show, Record r) {
508         mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0, r).sendToTarget();
509     }
510 
drawTile(QSPanelControllerBase.TileRecord r, QSTile.State state)511     protected void drawTile(QSPanelControllerBase.TileRecord r, QSTile.State state) {
512         r.tileView.onStateChanged(state);
513     }
514 
openPanelEvent()515     protected QSEvent openPanelEvent() {
516         return QSEvent.QS_PANEL_EXPANDED;
517     }
518 
closePanelEvent()519     protected QSEvent closePanelEvent() {
520         return QSEvent.QS_PANEL_COLLAPSED;
521     }
522 
tileVisibleEvent()523     protected QSEvent tileVisibleEvent() {
524         return QSEvent.QS_TILE_VISIBLE;
525     }
526 
shouldShowDetail()527     protected boolean shouldShowDetail() {
528         return mExpanded;
529     }
530 
addTile(QSPanelControllerBase.TileRecord tileRecord)531     void addTile(QSPanelControllerBase.TileRecord tileRecord) {
532         final QSTile.Callback callback = new QSTile.Callback() {
533             @Override
534             public void onStateChanged(QSTile.State state) {
535                 drawTile(tileRecord, state);
536             }
537 
538             @Override
539             public void onShowDetail(boolean show) {
540                 // Both the collapsed and full QS panels get this callback, this check determines
541                 // which one should handle showing the detail.
542                 if (shouldShowDetail()) {
543                     QSPanel.this.showDetail(show, tileRecord);
544                 }
545             }
546 
547             @Override
548             public void onToggleStateChanged(boolean state) {
549                 if (mDetailRecord == tileRecord) {
550                     fireToggleStateChanged(state);
551                 }
552             }
553 
554             @Override
555             public void onScanStateChanged(boolean state) {
556                 tileRecord.scanState = state;
557                 if (mDetailRecord == tileRecord) {
558                     fireScanStateChanged(tileRecord.scanState);
559                 }
560             }
561 
562             @Override
563             public void onAnnouncementRequested(CharSequence announcement) {
564                 if (announcement != null) {
565                     mHandler.obtainMessage(H.ANNOUNCE_FOR_ACCESSIBILITY, announcement)
566                             .sendToTarget();
567                 }
568             }
569         };
570 
571         tileRecord.tile.addCallback(callback);
572         tileRecord.callback = callback;
573         tileRecord.tileView.init(tileRecord.tile);
574         tileRecord.tile.refreshState();
575 
576         if (mTileLayout != null) {
577             mTileLayout.addTile(tileRecord);
578         }
579     }
580 
removeTile(QSPanelControllerBase.TileRecord tileRecord)581     void removeTile(QSPanelControllerBase.TileRecord tileRecord) {
582         mTileLayout.removeTile(tileRecord);
583     }
584 
closeDetail()585     void closeDetail() {
586         showDetail(false, mDetailRecord);
587     }
588 
getGridHeight()589     public int getGridHeight() {
590         return getMeasuredHeight();
591     }
592 
handleShowDetail(Record r, boolean show)593     protected void handleShowDetail(Record r, boolean show) {
594         if (r instanceof QSPanelControllerBase.TileRecord) {
595             handleShowDetailTile((QSPanelControllerBase.TileRecord) r, show);
596         } else {
597             int x = 0;
598             int y = 0;
599             if (r != null) {
600                 x = r.x;
601                 y = r.y;
602             }
603             handleShowDetailImpl(r, show, x, y);
604         }
605     }
606 
handleShowDetailTile(QSPanelControllerBase.TileRecord r, boolean show)607     private void handleShowDetailTile(QSPanelControllerBase.TileRecord r, boolean show) {
608         if ((mDetailRecord != null) == show && mDetailRecord == r) return;
609 
610         if (show) {
611             r.detailAdapter = r.tile.getDetailAdapter();
612             if (r.detailAdapter == null) return;
613         }
614         r.tile.setDetailListening(show);
615         int x = r.tileView.getLeft() + r.tileView.getWidth() / 2;
616         int y = r.tileView.getDetailY() + mTileLayout.getOffsetTop(r) + getTop();
617         handleShowDetailImpl(r, show, x, y);
618     }
619 
handleShowDetailImpl(Record r, boolean show, int x, int y)620     private void handleShowDetailImpl(Record r, boolean show, int x, int y) {
621         setDetailRecord(show ? r : null);
622         fireShowingDetail(show ? r.detailAdapter : null, x, y);
623     }
624 
setDetailRecord(Record r)625     protected void setDetailRecord(Record r) {
626         if (r == mDetailRecord) return;
627         mDetailRecord = r;
628         final boolean scanState = mDetailRecord instanceof QSPanelControllerBase.TileRecord
629                 && ((QSPanelControllerBase.TileRecord) mDetailRecord).scanState;
630         fireScanStateChanged(scanState);
631     }
632 
fireShowingDetail(DetailAdapter detail, int x, int y)633     private void fireShowingDetail(DetailAdapter detail, int x, int y) {
634         if (mCallback != null) {
635             mCallback.onShowingDetail(detail, x, y);
636         }
637     }
638 
fireToggleStateChanged(boolean state)639     private void fireToggleStateChanged(boolean state) {
640         if (mCallback != null) {
641             mCallback.onToggleStateChanged(state);
642         }
643     }
644 
fireScanStateChanged(boolean state)645     private void fireScanStateChanged(boolean state) {
646         if (mCallback != null) {
647             mCallback.onScanStateChanged(state);
648         }
649     }
650 
getTileLayout()651     QSTileLayout getTileLayout() {
652         return mTileLayout;
653     }
654 
655     /** */
setContentMargins(int startMargin, int endMargin, ViewGroup mediaHostView)656     public void setContentMargins(int startMargin, int endMargin, ViewGroup mediaHostView) {
657         // Only some views actually want this content padding, others want to go all the way
658         // to the edge like the brightness slider
659         mContentMarginStart = startMargin;
660         mContentMarginEnd = endMargin;
661         updateMediaHostContentMargins(mediaHostView);
662     }
663 
664     /**
665      * Update the margins of the media hosts
666      */
updateMediaHostContentMargins(ViewGroup mediaHostView)667     protected void updateMediaHostContentMargins(ViewGroup mediaHostView) {
668         if (mUsingMediaPlayer) {
669             int marginStart = 0;
670             int marginEnd = 0;
671             if (mUsingHorizontalLayout) {
672                 marginEnd = mContentMarginEnd;
673             }
674             updateMargins(mediaHostView, marginStart, marginEnd);
675         }
676     }
677 
678     /**
679      * Update the margins of a view.
680      *
681      * @param view the view to adjust
682      * @param start the start margin to set
683      * @param end the end margin to set
684      */
updateMargins(View view, int start, int end)685     protected void updateMargins(View view, int start, int end) {
686         LayoutParams lp = (LayoutParams) view.getLayoutParams();
687         if (lp != null) {
688             lp.setMarginStart(start);
689             lp.setMarginEnd(end);
690             view.setLayoutParams(lp);
691         }
692     }
693 
694     /**
695      * Set the header container of quick settings.
696      */
setHeaderContainer(@onNull ViewGroup headerContainer)697     public void setHeaderContainer(@NonNull ViewGroup headerContainer) {
698         mHeaderContainer = headerContainer;
699     }
700 
isListening()701     public boolean isListening() {
702         return mListening;
703     }
704 
705     /**
706      * Set the security footer view and switch it into the right place
707      * @param view the view in question
708      * @param shouldUseSplitNotificationShade if QS is in split shade mode
709      */
setSecurityFooter(View view, boolean shouldUseSplitNotificationShade)710     public void setSecurityFooter(View view, boolean shouldUseSplitNotificationShade) {
711         mSecurityFooter = view;
712         switchSecurityFooter(shouldUseSplitNotificationShade);
713     }
714 
setPageMargin(int pageMargin)715     protected void setPageMargin(int pageMargin) {
716         if (mTileLayout instanceof PagedTileLayout) {
717             ((PagedTileLayout) mTileLayout).setPageMargin(pageMargin);
718         }
719     }
720 
setUsingHorizontalLayout(boolean horizontal, ViewGroup mediaHostView, boolean force)721     void setUsingHorizontalLayout(boolean horizontal, ViewGroup mediaHostView, boolean force) {
722         if (horizontal != mUsingHorizontalLayout || force) {
723             mUsingHorizontalLayout = horizontal;
724             ViewGroup newParent = horizontal ? mHorizontalContentContainer : this;
725             switchAllContentToParent(newParent, mTileLayout);
726             reAttachMediaHost(mediaHostView, horizontal);
727             if (needsDynamicRowsAndColumns()) {
728                 mTileLayout.setMinRows(horizontal ? 2 : 1);
729                 mTileLayout.setMaxColumns(horizontal ? 2 : 4);
730             }
731             updateMargins(mediaHostView);
732             mHorizontalLinearLayout.setVisibility(horizontal ? View.VISIBLE : View.GONE);
733         }
734     }
735 
updateMargins(ViewGroup mediaHostView)736     private void updateMargins(ViewGroup mediaHostView) {
737         updateMediaHostContentMargins(mediaHostView);
738         updateHorizontalLinearLayoutMargins();
739         updatePadding();
740     }
741 
742     private class H extends Handler {
743         private static final int SHOW_DETAIL = 1;
744         private static final int SET_TILE_VISIBILITY = 2;
745         private static final int ANNOUNCE_FOR_ACCESSIBILITY = 3;
746 
747         @Override
handleMessage(Message msg)748         public void handleMessage(Message msg) {
749             if (msg.what == SHOW_DETAIL) {
750                 handleShowDetail((Record) msg.obj, msg.arg1 != 0);
751             } else if (msg.what == ANNOUNCE_FOR_ACCESSIBILITY) {
752                 announceForAccessibility((CharSequence) msg.obj);
753             }
754         }
755     }
756 
757     protected static class Record {
758         DetailAdapter detailAdapter;
759         int x;
760         int y;
761     }
762 
763     public interface QSTileLayout {
764         /** */
saveInstanceState(Bundle outState)765         default void saveInstanceState(Bundle outState) {}
766 
767         /** */
restoreInstanceState(Bundle savedInstanceState)768         default void restoreInstanceState(Bundle savedInstanceState) {}
769 
770         /** */
addTile(QSPanelControllerBase.TileRecord tile)771         void addTile(QSPanelControllerBase.TileRecord tile);
772 
773         /** */
removeTile(QSPanelControllerBase.TileRecord tile)774         void removeTile(QSPanelControllerBase.TileRecord tile);
775 
776         /** */
getOffsetTop(QSPanelControllerBase.TileRecord tile)777         int getOffsetTop(QSPanelControllerBase.TileRecord tile);
778 
779         /** */
updateResources()780         boolean updateResources();
781 
782         /** */
setListening(boolean listening, UiEventLogger uiEventLogger)783         void setListening(boolean listening, UiEventLogger uiEventLogger);
784 
785         /** */
getHeight()786         int getHeight();
787 
788         /** */
getTilesHeight()789         int getTilesHeight();
790 
791         /**
792          * Sets a size modifier for the tile. Where 0 means collapsed, and 1 expanded.
793          */
setSquishinessFraction(float squishinessFraction)794         void setSquishinessFraction(float squishinessFraction);
795 
796         /**
797          * Sets the minimum number of rows to show
798          *
799          * @param minRows the minimum.
800          */
setMinRows(int minRows)801         default boolean setMinRows(int minRows) {
802             return false;
803         }
804 
805         /**
806          * Sets the max number of columns to show
807          *
808          * @param maxColumns the maximum
809          *
810          * @return true if the number of visible columns has changed.
811          */
setMaxColumns(int maxColumns)812         default boolean setMaxColumns(int maxColumns) {
813             return false;
814         }
815 
816         /**
817          * Sets the expansion value and proposedTranslation to panel.
818          */
setExpansion(float expansion, float proposedTranslation)819         default void setExpansion(float expansion, float proposedTranslation) {}
820 
getNumVisibleTiles()821         int getNumVisibleTiles();
822     }
823 
824     interface OnConfigurationChangedListener {
onConfigurationChange(Configuration newConfig)825         void onConfigurationChange(Configuration newConfig);
826     }
827 
828     @VisibleForTesting
switchToParent(View child, ViewGroup parent, int index, String tag)829     static void switchToParent(View child, ViewGroup parent, int index, String tag) {
830         if (parent == null) {
831             Log.w(tag, "Trying to move view to null parent",
832                     new IllegalStateException());
833             return;
834         }
835         ViewGroup currentParent = (ViewGroup) child.getParent();
836         if (currentParent != parent) {
837             if (currentParent != null) {
838                 currentParent.removeView(child);
839             }
840             parent.addView(child, index);
841             return;
842         }
843         // Same parent, we are just changing indices
844         int currentIndex = parent.indexOfChild(child);
845         if (currentIndex == index) {
846             // We want to be in the same place. Nothing to do here
847             return;
848         }
849         parent.removeView(child);
850         parent.addView(child, index);
851     }
852 }
853