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.InjectionInflationController.VIEW_CONTEXT;
20 import static com.android.systemui.util.Utils.useQsMediaPlayer;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.res.Configuration;
27 import android.content.res.Resources;
28 import android.graphics.PointF;
29 import android.metrics.LogMaker;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.Message;
33 import android.util.AttributeSet;
34 import android.view.Gravity;
35 import android.view.LayoutInflater;
36 import android.view.View;
37 import android.view.ViewGroup;
38 import android.widget.LinearLayout;
39 
40 import com.android.internal.logging.MetricsLogger;
41 import com.android.internal.logging.UiEventLogger;
42 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
43 import com.android.internal.widget.RemeasuringLinearLayout;
44 import com.android.systemui.Dependency;
45 import com.android.systemui.Dumpable;
46 import com.android.systemui.R;
47 import com.android.systemui.broadcast.BroadcastDispatcher;
48 import com.android.systemui.dump.DumpManager;
49 import com.android.systemui.media.MediaHierarchyManager;
50 import com.android.systemui.media.MediaHost;
51 import com.android.systemui.plugins.qs.DetailAdapter;
52 import com.android.systemui.plugins.qs.QSTile;
53 import com.android.systemui.plugins.qs.QSTileView;
54 import com.android.systemui.qs.QSHost.Callback;
55 import com.android.systemui.qs.customize.QSCustomizer;
56 import com.android.systemui.qs.external.CustomTile;
57 import com.android.systemui.qs.logging.QSLogger;
58 import com.android.systemui.settings.BrightnessController;
59 import com.android.systemui.settings.ToggleSliderView;
60 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
61 import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener;
62 import com.android.systemui.tuner.TunerService;
63 import com.android.systemui.tuner.TunerService.Tunable;
64 import com.android.systemui.util.animation.DisappearParameters;
65 
66 import java.io.FileDescriptor;
67 import java.io.PrintWriter;
68 import java.util.ArrayList;
69 import java.util.Collection;
70 import java.util.function.Consumer;
71 import java.util.stream.Collectors;
72 
73 import javax.inject.Inject;
74 import javax.inject.Named;
75 
76 /** View that represents the quick settings tile panel (when expanded/pulled down). **/
77 public class QSPanel extends LinearLayout implements Tunable, Callback, BrightnessMirrorListener,
78         Dumpable {
79 
80     public static final String QS_SHOW_BRIGHTNESS = "qs_show_brightness";
81     public static final String QS_SHOW_HEADER = "qs_show_header";
82 
83     private static final String TAG = "QSPanel";
84 
85     protected final Context mContext;
86     protected final ArrayList<TileRecord> mRecords = new ArrayList<>();
87     private final BroadcastDispatcher mBroadcastDispatcher;
88     protected final MediaHost mMediaHost;
89 
90     /**
91      * The index where the content starts that needs to be moved between parents
92      */
93     private final int mMovableContentStartIndex;
94     private String mCachedSpecs = "";
95 
96     @Nullable
97     protected View mBrightnessView;
98     @Nullable
99     private BrightnessController mBrightnessController;
100 
101     private final H mHandler = new H();
102     private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
103     private QSTileRevealController mQsTileRevealController;
104     /** Whether or not the QS media player feature is enabled. */
105     protected boolean mUsingMediaPlayer;
106     private int mVisualMarginStart;
107     private int mVisualMarginEnd;
108 
109     protected boolean mExpanded;
110     protected boolean mListening;
111 
112     private QSDetail.Callback mCallback;
113     private final DumpManager mDumpManager;
114     private final QSLogger mQSLogger;
115     protected final UiEventLogger mUiEventLogger;
116     protected QSTileHost mHost;
117 
118     @Nullable
119     protected QSSecurityFooter mSecurityFooter;
120 
121     @Nullable
122     protected View mFooter;
123     @Nullable
124     protected View mDivider;
125 
126     @Nullable
127     private ViewGroup mHeaderContainer;
128     private PageIndicator mFooterPageIndicator;
129     private boolean mGridContentVisible = true;
130     private int mContentMarginStart;
131     private int mContentMarginEnd;
132     private int mVisualTilePadding;
133     private boolean mUsingHorizontalLayout;
134 
135     private QSCustomizer mCustomizePanel;
136     private Record mDetailRecord;
137 
138     private BrightnessMirrorController mBrightnessMirrorController;
139     private LinearLayout mHorizontalLinearLayout;
140     private LinearLayout mHorizontalContentContainer;
141 
142     // Only used with media
143     private QSTileLayout mHorizontalTileLayout;
144     protected QSTileLayout mRegularTileLayout;
145     protected QSTileLayout mTileLayout;
146     private int mLastOrientation = -1;
147     private int mMediaTotalBottomMargin;
148     private int mFooterMarginStartHorizontal;
149     private Consumer<Boolean> mMediaVisibilityChangedListener;
150 
151 
152     @Inject
QSPanel( @amedVIEW_CONTEXT) Context context, AttributeSet attrs, DumpManager dumpManager, BroadcastDispatcher broadcastDispatcher, QSLogger qsLogger, MediaHost mediaHost, UiEventLogger uiEventLogger )153     public QSPanel(
154             @Named(VIEW_CONTEXT) Context context,
155             AttributeSet attrs,
156             DumpManager dumpManager,
157             BroadcastDispatcher broadcastDispatcher,
158             QSLogger qsLogger,
159             MediaHost mediaHost,
160             UiEventLogger uiEventLogger
161     ) {
162         super(context, attrs);
163         mUsingMediaPlayer = useQsMediaPlayer(context);
164         mMediaTotalBottomMargin = getResources().getDimensionPixelSize(
165                 R.dimen.quick_settings_bottom_margin_media);
166         mMediaHost = mediaHost;
167         mMediaHost.addVisibilityChangeListener((visible) -> {
168             onMediaVisibilityChanged(visible);
169             return null;
170         });
171         mContext = context;
172         mQSLogger = qsLogger;
173         mDumpManager = dumpManager;
174         mBroadcastDispatcher = broadcastDispatcher;
175         mUiEventLogger = uiEventLogger;
176 
177         setOrientation(VERTICAL);
178 
179         addViewsAboveTiles();
180         mMovableContentStartIndex = getChildCount();
181         mRegularTileLayout = createRegularTileLayout();
182 
183         if (mUsingMediaPlayer) {
184             mHorizontalLinearLayout = new RemeasuringLinearLayout(mContext);
185             mHorizontalLinearLayout.setOrientation(LinearLayout.HORIZONTAL);
186             mHorizontalLinearLayout.setClipChildren(false);
187             mHorizontalLinearLayout.setClipToPadding(false);
188 
189             mHorizontalContentContainer = new RemeasuringLinearLayout(mContext);
190             mHorizontalContentContainer.setOrientation(LinearLayout.VERTICAL);
191             mHorizontalContentContainer.setClipChildren(false);
192             mHorizontalContentContainer.setClipToPadding(false);
193 
194             mHorizontalTileLayout = createHorizontalTileLayout();
195             LayoutParams lp = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1);
196             int marginSize = (int) mContext.getResources().getDimension(R.dimen.qqs_media_spacing);
197             lp.setMarginStart(0);
198             lp.setMarginEnd(marginSize);
199             lp.gravity = Gravity.CENTER_VERTICAL;
200             mHorizontalLinearLayout.addView(mHorizontalContentContainer, lp);
201 
202             lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0, 1);
203             addView(mHorizontalLinearLayout, lp);
204 
205             initMediaHostState();
206         }
207         addSecurityFooter();
208         if (mRegularTileLayout instanceof PagedTileLayout) {
209             mQsTileRevealController = new QSTileRevealController(mContext, this,
210                     (PagedTileLayout) mRegularTileLayout);
211         }
212         mQSLogger.logAllTilesChangeListening(mListening, getDumpableTag(), mCachedSpecs);
213         updateResources();
214     }
215 
onMediaVisibilityChanged(Boolean visible)216     protected void onMediaVisibilityChanged(Boolean visible) {
217         switchTileLayout();
218         if (mMediaVisibilityChangedListener != null) {
219             mMediaVisibilityChangedListener.accept(visible);
220         }
221     }
222 
addSecurityFooter()223     protected void addSecurityFooter() {
224         mSecurityFooter = new QSSecurityFooter(this, mContext);
225     }
226 
addViewsAboveTiles()227     protected void addViewsAboveTiles() {
228         mBrightnessView = LayoutInflater.from(mContext).inflate(
229             R.layout.quick_settings_brightness_dialog, this, false);
230         addView(mBrightnessView);
231         mBrightnessController = new BrightnessController(getContext(),
232                 findViewById(R.id.brightness_slider), mBroadcastDispatcher);
233     }
234 
createRegularTileLayout()235     protected QSTileLayout createRegularTileLayout() {
236         if (mRegularTileLayout == null) {
237             mRegularTileLayout = (QSTileLayout) LayoutInflater.from(mContext).inflate(
238                     R.layout.qs_paged_tile_layout, this, false);
239         }
240         return mRegularTileLayout;
241     }
242 
243 
createHorizontalTileLayout()244     protected QSTileLayout createHorizontalTileLayout() {
245         return createRegularTileLayout();
246     }
247 
initMediaHostState()248     protected void initMediaHostState() {
249         mMediaHost.setExpansion(1.0f);
250         mMediaHost.setShowsOnlyActiveMedia(false);
251         updateMediaDisappearParameters();
252         mMediaHost.init(MediaHierarchyManager.LOCATION_QS);
253     }
254 
255     /**
256      * Update the way the media disappears based on if we're using the horizontal layout
257      */
updateMediaDisappearParameters()258     private void updateMediaDisappearParameters() {
259         if (!mUsingMediaPlayer) {
260             return;
261         }
262         DisappearParameters parameters = mMediaHost.getDisappearParameters();
263         if (mUsingHorizontalLayout) {
264             // Only height remaining
265             parameters.getDisappearSize().set(0.0f, 0.4f);
266             // Disappearing on the right side on the bottom
267             parameters.getGonePivot().set(1.0f, 1.0f);
268             // translating a bit horizontal
269             parameters.getContentTranslationFraction().set(0.25f, 1.0f);
270             parameters.setDisappearEnd(0.6f);
271         } else {
272             // Only width remaining
273             parameters.getDisappearSize().set(1.0f, 0.0f);
274             // Disappearing on the bottom
275             parameters.getGonePivot().set(0.0f, 1.0f);
276             // translating a bit vertical
277             parameters.getContentTranslationFraction().set(0.0f, 1.05f);
278             parameters.setDisappearEnd(0.95f);
279         }
280         parameters.setFadeStartPosition(0.95f);
281         parameters.setDisappearStart(0.0f);
282         mMediaHost.setDisappearParameters(parameters);
283     }
284 
285     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)286     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
287         if (mTileLayout instanceof PagedTileLayout) {
288             // Since PageIndicator gets measured before PagedTileLayout, we preemptively set the
289             // # of pages before the measurement pass so PageIndicator is measured appropriately
290             if (mFooterPageIndicator != null) {
291                 mFooterPageIndicator.setNumPages(((PagedTileLayout) mTileLayout).getNumPages());
292             }
293 
294             // Allow the UI to be as big as it want's to, we're in a scroll view
295             int newHeight = 10000;
296             int availableHeight = MeasureSpec.getSize(heightMeasureSpec);
297             int excessHeight = newHeight - availableHeight;
298             // Measure with EXACTLY. That way, The content will only use excess height and will
299             // be measured last, after other views and padding is accounted for. This only
300             // works because our Layouts in here remeasure themselves with the exact content
301             // height.
302             heightMeasureSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY);
303             ((PagedTileLayout) mTileLayout).setExcessHeight(excessHeight);
304         }
305         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
306 
307         // We want all the logic of LinearLayout#onMeasure, and for it to assign the excess space
308         // not used by the other children to PagedTileLayout. However, in this case, LinearLayout
309         // assumes that PagedTileLayout would use all the excess space. This is not the case as
310         // PagedTileLayout height is quantized (because it shows a certain number of rows).
311         // Therefore, after everything is measured, we need to make sure that we add up the correct
312         // total height
313         int height = getPaddingBottom() + getPaddingTop();
314         int numChildren = getChildCount();
315         for (int i = 0; i < numChildren; i++) {
316             View child = getChildAt(i);
317             if (child.getVisibility() != View.GONE) {
318                 height += child.getMeasuredHeight();
319                 MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams();
320                 height += layoutParams.topMargin + layoutParams.bottomMargin;
321             }
322         }
323         setMeasuredDimension(getMeasuredWidth(), height);
324     }
325 
getQsTileRevealController()326     public QSTileRevealController getQsTileRevealController() {
327         return mQsTileRevealController;
328     }
329 
isShowingCustomize()330     public boolean isShowingCustomize() {
331         return mCustomizePanel != null && mCustomizePanel.isCustomizing();
332     }
333 
334     @Override
onAttachedToWindow()335     protected void onAttachedToWindow() {
336         super.onAttachedToWindow();
337         final TunerService tunerService = Dependency.get(TunerService.class);
338         tunerService.addTunable(this, QS_SHOW_BRIGHTNESS);
339 
340         if (mHost != null) {
341             setTiles(mHost.getTiles());
342         }
343         if (mBrightnessMirrorController != null) {
344             mBrightnessMirrorController.addCallback(this);
345         }
346         mDumpManager.registerDumpable(getDumpableTag(), this);
347     }
348 
349     @Override
onDetachedFromWindow()350     protected void onDetachedFromWindow() {
351         Dependency.get(TunerService.class).removeTunable(this);
352         if (mHost != null) {
353             mHost.removeCallback(this);
354         }
355         if (mTileLayout != null) {
356             mTileLayout.setListening(false);
357         }
358         for (TileRecord record : mRecords) {
359             record.tile.removeCallbacks();
360         }
361         mRecords.clear();
362         if (mBrightnessMirrorController != null) {
363             mBrightnessMirrorController.removeCallback(this);
364         }
365         mDumpManager.unregisterDumpable(getDumpableTag());
366         super.onDetachedFromWindow();
367     }
368 
getDumpableTag()369     protected String getDumpableTag() {
370         return TAG;
371     }
372 
373     @Override
onTilesChanged()374     public void onTilesChanged() {
375         setTiles(mHost.getTiles());
376     }
377 
378     @Override
onTuningChanged(String key, String newValue)379     public void onTuningChanged(String key, String newValue) {
380         if (QS_SHOW_BRIGHTNESS.equals(key) && mBrightnessView != null) {
381             updateViewVisibilityForTuningValue(mBrightnessView, newValue);
382         }
383     }
384 
updateViewVisibilityForTuningValue(View view, @Nullable String newValue)385     private void updateViewVisibilityForTuningValue(View view, @Nullable String newValue) {
386         view.setVisibility(TunerService.parseIntegerSwitch(newValue, true) ? VISIBLE : GONE);
387     }
388 
openDetails(String subPanel)389     public void openDetails(String subPanel) {
390         QSTile tile = getTile(subPanel);
391         // If there's no tile with that name (as defined in QSFactoryImpl or other QSFactory),
392         // QSFactory will not be able to create a tile and getTile will return null
393         if (tile != null) {
394             showDetailAdapter(true, tile.getDetailAdapter(), new int[]{getWidth() / 2, 0});
395         }
396     }
397 
getTile(String subPanel)398     private QSTile getTile(String subPanel) {
399         for (int i = 0; i < mRecords.size(); i++) {
400             if (subPanel.equals(mRecords.get(i).tile.getTileSpec())) {
401                 return mRecords.get(i).tile;
402             }
403         }
404         return mHost.createTile(subPanel);
405     }
406 
setBrightnessMirror(BrightnessMirrorController c)407     public void setBrightnessMirror(BrightnessMirrorController c) {
408         if (mBrightnessMirrorController != null) {
409             mBrightnessMirrorController.removeCallback(this);
410         }
411         mBrightnessMirrorController = c;
412         if (mBrightnessMirrorController != null) {
413             mBrightnessMirrorController.addCallback(this);
414         }
415         updateBrightnessMirror();
416     }
417 
418     @Override
onBrightnessMirrorReinflated(View brightnessMirror)419     public void onBrightnessMirrorReinflated(View brightnessMirror) {
420         updateBrightnessMirror();
421     }
422 
423     @Nullable
getBrightnessView()424     View getBrightnessView() {
425         return mBrightnessView;
426     }
427 
setCallback(QSDetail.Callback callback)428     public void setCallback(QSDetail.Callback callback) {
429         mCallback = callback;
430     }
431 
setHost(QSTileHost host, QSCustomizer customizer)432     public void setHost(QSTileHost host, QSCustomizer customizer) {
433         mHost = host;
434         mHost.addCallback(this);
435         setTiles(mHost.getTiles());
436         if (mSecurityFooter != null) {
437             mSecurityFooter.setHostEnvironment(host);
438         }
439         mCustomizePanel = customizer;
440         if (mCustomizePanel != null) {
441             mCustomizePanel.setHost(mHost);
442         }
443     }
444 
445     /**
446      * Links the footer's page indicator, which is used in landscape orientation to save space.
447      *
448      * @param pageIndicator indicator to use for page scrolling
449      */
setFooterPageIndicator(PageIndicator pageIndicator)450     public void setFooterPageIndicator(PageIndicator pageIndicator) {
451         if (mRegularTileLayout instanceof PagedTileLayout) {
452             mFooterPageIndicator = pageIndicator;
453             updatePageIndicator();
454         }
455     }
456 
updatePageIndicator()457     private void updatePageIndicator() {
458         if (mRegularTileLayout instanceof PagedTileLayout) {
459             if (mFooterPageIndicator != null) {
460                 mFooterPageIndicator.setVisibility(View.GONE);
461 
462                 ((PagedTileLayout) mRegularTileLayout).setPageIndicator(mFooterPageIndicator);
463             }
464         }
465     }
466 
getHost()467     public QSTileHost getHost() {
468         return mHost;
469     }
470 
updateResources()471     public void updateResources() {
472         int tileSize = getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_size);
473         int tileBg = getResources().getDimensionPixelSize(R.dimen.qs_tile_background_size);
474         mFooterMarginStartHorizontal = getResources().getDimensionPixelSize(
475                 R.dimen.qs_footer_horizontal_margin);
476         mVisualTilePadding = (int) ((tileSize - tileBg) / 2.0f);
477         updatePadding();
478 
479         updatePageIndicator();
480 
481         if (mListening) {
482             refreshAllTiles();
483         }
484         if (mTileLayout != null) {
485             mTileLayout.updateResources();
486         }
487     }
488 
updatePadding()489     protected void updatePadding() {
490         final Resources res = mContext.getResources();
491         int padding = res.getDimensionPixelSize(R.dimen.qs_panel_padding_top);
492         if (mUsingHorizontalLayout) {
493             // When using the horizontal layout, our space is quite constrained. We therefore
494             // reduce some of the padding on the top, which makes the brightness bar overlapp,
495             // but since that has naturally quite a bit of built in padding, that's fine.
496             padding = (int) (padding * 0.6f);
497         }
498         setPaddingRelative(getPaddingStart(),
499                 padding,
500                 getPaddingEnd(),
501                 res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom));
502     }
503 
504     @Override
onConfigurationChanged(Configuration newConfig)505     protected void onConfigurationChanged(Configuration newConfig) {
506         super.onConfigurationChanged(newConfig);
507         if (mSecurityFooter != null) {
508             mSecurityFooter.onConfigurationChanged();
509         }
510         updateResources();
511 
512         updateBrightnessMirror();
513 
514         if (newConfig.orientation != mLastOrientation) {
515             mLastOrientation = newConfig.orientation;
516             switchTileLayout();
517         }
518     }
519 
520     @Override
onFinishInflate()521     protected void onFinishInflate() {
522         super.onFinishInflate();
523         mFooter = findViewById(R.id.qs_footer);
524         mDivider = findViewById(R.id.divider);
525         switchTileLayout(true /* force */);
526     }
527 
switchTileLayout()528     boolean switchTileLayout() {
529         return switchTileLayout(false /* force */);
530     }
531 
switchTileLayout(boolean force)532     private boolean switchTileLayout(boolean force) {
533         /** Whether or not the QuickQSPanel currently contains a media player. */
534         boolean horizontal = shouldUseHorizontalLayout();
535         if (mDivider != null) {
536             if (!horizontal && mUsingMediaPlayer && mMediaHost.getVisible()) {
537                 mDivider.setVisibility(View.VISIBLE);
538             } else {
539                 mDivider.setVisibility(View.GONE);
540             }
541         }
542         if (horizontal != mUsingHorizontalLayout || force) {
543             mUsingHorizontalLayout = horizontal;
544             View visibleView = horizontal ? mHorizontalLinearLayout : (View) mRegularTileLayout;
545             View hiddenView = horizontal ? (View) mRegularTileLayout : mHorizontalLinearLayout;
546             ViewGroup newParent = horizontal ? mHorizontalContentContainer : this;
547             QSTileLayout newLayout = horizontal ? mHorizontalTileLayout : mRegularTileLayout;
548             if (hiddenView != null &&
549                     (mRegularTileLayout != mHorizontalTileLayout ||
550                             hiddenView != mRegularTileLayout)) {
551                 // Only hide the view if the horizontal and the regular view are different,
552                 // otherwise its reattached.
553                 hiddenView.setVisibility(View.GONE);
554             }
555             visibleView.setVisibility(View.VISIBLE);
556             switchAllContentToParent(newParent, newLayout);
557             reAttachMediaHost();
558             if (mTileLayout != null) {
559                 mTileLayout.setListening(false);
560                 for (TileRecord record : mRecords) {
561                     mTileLayout.removeTile(record);
562                     record.tile.removeCallback(record.callback);
563                 }
564             }
565             mTileLayout = newLayout;
566             if (mHost != null) setTiles(mHost.getTiles());
567             newLayout.setListening(mListening);
568             if (needsDynamicRowsAndColumns()) {
569                 newLayout.setMinRows(horizontal ? 2 : 1);
570                 // Let's use 3 columns to match the current layout
571                 newLayout.setMaxColumns(horizontal ? 3 : TileLayout.NO_MAX_COLUMNS);
572             }
573             updateTileLayoutMargins();
574             updateFooterMargin();
575             updateDividerMargin();
576             updateMediaDisappearParameters();
577             updateMediaHostContentMargins();
578             updateHorizontalLinearLayoutMargins();
579             updatePadding();
580             return true;
581         }
582         return false;
583     }
584 
updateHorizontalLinearLayoutMargins()585     private void updateHorizontalLinearLayoutMargins() {
586         if (mHorizontalLinearLayout != null && !displayMediaMarginsOnMedia()) {
587             LayoutParams lp = (LayoutParams) mHorizontalLinearLayout.getLayoutParams();
588             lp.bottomMargin = mMediaTotalBottomMargin - getPaddingBottom();
589             mHorizontalLinearLayout.setLayoutParams(lp);
590         }
591     }
592 
593     /**
594      * @return true if the margin bottom of the media view should be on the media host or false
595      *         if they should be on the HorizontalLinearLayout. Returning {@code false} is useful
596      *         to visually center the tiles in the Media view, which doesn't work when the
597      *         expanded panel actually scrolls.
598      */
displayMediaMarginsOnMedia()599     protected boolean displayMediaMarginsOnMedia() {
600         return true;
601     }
602 
needsDynamicRowsAndColumns()603     protected boolean needsDynamicRowsAndColumns() {
604         return true;
605     }
606 
switchAllContentToParent(ViewGroup parent, QSTileLayout newLayout)607     private void switchAllContentToParent(ViewGroup parent, QSTileLayout newLayout) {
608         int index = parent == this ? mMovableContentStartIndex : 0;
609 
610         // Let's first move the tileLayout to the new parent, since that should come first.
611         switchToParent((View) newLayout, parent, index);
612         index++;
613 
614         if (mSecurityFooter != null) {
615             View view = mSecurityFooter.getView();
616             LinearLayout.LayoutParams layoutParams = (LayoutParams) view.getLayoutParams();
617             if (mUsingHorizontalLayout && mHeaderContainer != null) {
618                 // Adding the security view to the header, that enables us to avoid scrolling
619                 layoutParams.width = 0;
620                 layoutParams.weight = 1.6f;
621                 switchToParent(view, mHeaderContainer, 1 /* always in second place */);
622             } else {
623                 layoutParams.width = LayoutParams.WRAP_CONTENT;
624                 layoutParams.weight = 0;
625                 switchToParent(view, parent, index);
626                 index++;
627             }
628             view.setLayoutParams(layoutParams);
629         }
630 
631         if (mFooter != null) {
632             // Then the footer with the settings
633             switchToParent(mFooter, parent, index);
634         }
635     }
636 
switchToParent(View child, ViewGroup parent, int index)637     private void switchToParent(View child, ViewGroup parent, int index) {
638         ViewGroup currentParent = (ViewGroup) child.getParent();
639         if (currentParent != parent || currentParent.indexOfChild(child) != index) {
640             if (currentParent != null) {
641                 currentParent.removeView(child);
642             }
643             parent.addView(child, index);
644         }
645     }
646 
shouldUseHorizontalLayout()647     private boolean shouldUseHorizontalLayout() {
648         return mUsingMediaPlayer && mMediaHost.getVisible()
649                 && getResources().getConfiguration().orientation
650                 == Configuration.ORIENTATION_LANDSCAPE;
651     }
652 
reAttachMediaHost()653     protected void reAttachMediaHost() {
654         if (!mUsingMediaPlayer) {
655             return;
656         }
657         boolean horizontal = shouldUseHorizontalLayout();
658         ViewGroup host = mMediaHost.getHostView();
659         ViewGroup newParent = horizontal ? mHorizontalLinearLayout : this;
660         ViewGroup currentParent = (ViewGroup) host.getParent();
661         if (currentParent != newParent) {
662             if (currentParent != null) {
663                 currentParent.removeView(host);
664             }
665             newParent.addView(host);
666             LinearLayout.LayoutParams layoutParams = (LayoutParams) host.getLayoutParams();
667             layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
668             layoutParams.width = horizontal ? 0 : ViewGroup.LayoutParams.MATCH_PARENT;
669             layoutParams.weight = horizontal ? 1.2f : 0;
670             // Add any bottom margin, such that the total spacing is correct. This is only
671             // necessary if the view isn't horizontal, since otherwise the padding is
672             // carried in the parent of this view (to ensure correct vertical alignment)
673             layoutParams.bottomMargin = !horizontal || displayMediaMarginsOnMedia()
674                     ? mMediaTotalBottomMargin - getPaddingBottom() : 0;
675         }
676     }
677 
updateBrightnessMirror()678     public void updateBrightnessMirror() {
679         if (mBrightnessMirrorController != null) {
680             ToggleSliderView brightnessSlider = findViewById(R.id.brightness_slider);
681             ToggleSliderView mirrorSlider = mBrightnessMirrorController.getMirror()
682                     .findViewById(R.id.brightness_slider);
683             brightnessSlider.setMirror(mirrorSlider);
684             brightnessSlider.setMirrorController(mBrightnessMirrorController);
685         }
686     }
687 
onCollapse()688     public void onCollapse() {
689         if (mCustomizePanel != null && mCustomizePanel.isShown()) {
690             mCustomizePanel.hide();
691         }
692     }
693 
setExpanded(boolean expanded)694     public void setExpanded(boolean expanded) {
695         if (mExpanded == expanded) return;
696         mQSLogger.logPanelExpanded(expanded, getDumpableTag());
697         mExpanded = expanded;
698         if (!mExpanded && mTileLayout instanceof PagedTileLayout) {
699             ((PagedTileLayout) mTileLayout).setCurrentItem(0, false);
700         }
701         mMetricsLogger.visibility(MetricsEvent.QS_PANEL, mExpanded);
702         if (!mExpanded) {
703             mUiEventLogger.log(closePanelEvent());
704             closeDetail();
705         } else {
706             mUiEventLogger.log(openPanelEvent());
707             logTiles();
708         }
709     }
710 
setPageListener(final PagedTileLayout.PageListener pageListener)711     public void setPageListener(final PagedTileLayout.PageListener pageListener) {
712         if (mTileLayout instanceof PagedTileLayout) {
713             ((PagedTileLayout) mTileLayout).setPageListener(pageListener);
714         }
715     }
716 
isExpanded()717     public boolean isExpanded() {
718         return mExpanded;
719     }
720 
setListening(boolean listening)721     public void setListening(boolean listening) {
722         if (mListening == listening) return;
723         mListening = listening;
724         if (mTileLayout != null) {
725             mQSLogger.logAllTilesChangeListening(listening, getDumpableTag(), mCachedSpecs);
726             mTileLayout.setListening(listening);
727         }
728         if (mListening) {
729             refreshAllTiles();
730         }
731     }
732 
getTilesSpecs()733     private String getTilesSpecs() {
734         return mRecords.stream()
735                 .map(tileRecord ->  tileRecord.tile.getTileSpec())
736                 .collect(Collectors.joining(","));
737     }
738 
setListening(boolean listening, boolean expanded)739     public void setListening(boolean listening, boolean expanded) {
740         setListening(listening && expanded);
741         if (mSecurityFooter != null) {
742             mSecurityFooter.setListening(listening);
743         }
744         // Set the listening as soon as the QS fragment starts listening regardless of the expansion,
745         // so it will update the current brightness before the slider is visible.
746         setBrightnessListening(listening);
747     }
748 
setBrightnessListening(boolean listening)749     public void setBrightnessListening(boolean listening) {
750         if (mBrightnessController == null) {
751             return;
752         }
753         if (listening) {
754             mBrightnessController.registerCallbacks();
755         } else {
756             mBrightnessController.unregisterCallbacks();
757         }
758     }
759 
refreshAllTiles()760     public void refreshAllTiles() {
761         if (mBrightnessController != null) {
762             mBrightnessController.checkRestrictionAndSetEnabled();
763         }
764         for (TileRecord r : mRecords) {
765             r.tile.refreshState();
766         }
767         if (mSecurityFooter != null) {
768             mSecurityFooter.refreshState();
769         }
770     }
771 
showDetailAdapter(boolean show, DetailAdapter adapter, int[] locationInWindow)772     public void showDetailAdapter(boolean show, DetailAdapter adapter, int[] locationInWindow) {
773         int xInWindow = locationInWindow[0];
774         int yInWindow = locationInWindow[1];
775         ((View) getParent()).getLocationInWindow(locationInWindow);
776 
777         Record r = new Record();
778         r.detailAdapter = adapter;
779         r.x = xInWindow - locationInWindow[0];
780         r.y = yInWindow - locationInWindow[1];
781 
782         locationInWindow[0] = xInWindow;
783         locationInWindow[1] = yInWindow;
784 
785         showDetail(show, r);
786     }
787 
showDetail(boolean show, Record r)788     protected void showDetail(boolean show, Record r) {
789         mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0, r).sendToTarget();
790     }
791 
setTiles(Collection<QSTile> tiles)792     public void setTiles(Collection<QSTile> tiles) {
793         setTiles(tiles, false);
794     }
795 
setTiles(Collection<QSTile> tiles, boolean collapsedView)796     public void setTiles(Collection<QSTile> tiles, boolean collapsedView) {
797         if (!collapsedView) {
798             mQsTileRevealController.updateRevealedTiles(tiles);
799         }
800         for (TileRecord record : mRecords) {
801             mTileLayout.removeTile(record);
802             record.tile.removeCallback(record.callback);
803         }
804         mRecords.clear();
805         mCachedSpecs = "";
806         for (QSTile tile : tiles) {
807             addTile(tile, collapsedView);
808         }
809     }
810 
drawTile(TileRecord r, QSTile.State state)811     protected void drawTile(TileRecord r, QSTile.State state) {
812         r.tileView.onStateChanged(state);
813     }
814 
createTileView(QSTile tile, boolean collapsedView)815     protected QSTileView createTileView(QSTile tile, boolean collapsedView) {
816         return mHost.createTileView(tile, collapsedView);
817     }
818 
openPanelEvent()819     protected QSEvent openPanelEvent() {
820         return QSEvent.QS_PANEL_EXPANDED;
821     }
822 
closePanelEvent()823     protected QSEvent closePanelEvent() {
824         return QSEvent.QS_PANEL_COLLAPSED;
825     }
826 
tileVisibleEvent()827     protected QSEvent tileVisibleEvent() {
828         return QSEvent.QS_TILE_VISIBLE;
829     }
830 
shouldShowDetail()831     protected boolean shouldShowDetail() {
832         return mExpanded;
833     }
834 
addTile(final QSTile tile, boolean collapsedView)835     protected TileRecord addTile(final QSTile tile, boolean collapsedView) {
836         final TileRecord r = new TileRecord();
837         r.tile = tile;
838         r.tileView = createTileView(tile, collapsedView);
839         final QSTile.Callback callback = new QSTile.Callback() {
840             @Override
841             public void onStateChanged(QSTile.State state) {
842                 drawTile(r, state);
843             }
844 
845             @Override
846             public void onShowDetail(boolean show) {
847                 // Both the collapsed and full QS panels get this callback, this check determines
848                 // which one should handle showing the detail.
849                 if (shouldShowDetail()) {
850                     QSPanel.this.showDetail(show, r);
851                 }
852             }
853 
854             @Override
855             public void onToggleStateChanged(boolean state) {
856                 if (mDetailRecord == r) {
857                     fireToggleStateChanged(state);
858                 }
859             }
860 
861             @Override
862             public void onScanStateChanged(boolean state) {
863                 r.scanState = state;
864                 if (mDetailRecord == r) {
865                     fireScanStateChanged(r.scanState);
866                 }
867             }
868 
869             @Override
870             public void onAnnouncementRequested(CharSequence announcement) {
871                 if (announcement != null) {
872                     mHandler.obtainMessage(H.ANNOUNCE_FOR_ACCESSIBILITY, announcement)
873                             .sendToTarget();
874                 }
875             }
876         };
877         r.tile.addCallback(callback);
878         r.callback = callback;
879         r.tileView.init(r.tile);
880         r.tile.refreshState();
881         mRecords.add(r);
882         mCachedSpecs = getTilesSpecs();
883 
884         if (mTileLayout != null) {
885             mTileLayout.addTile(r);
886         }
887 
888         return r;
889     }
890 
891 
showEdit(final View v)892     public void showEdit(final View v) {
893         v.post(new Runnable() {
894             @Override
895             public void run() {
896                 if (mCustomizePanel != null) {
897                     if (!mCustomizePanel.isCustomizing()) {
898                         int[] loc = v.getLocationOnScreen();
899                         int x = loc[0] + v.getWidth() / 2;
900                         int y = loc[1] + v.getHeight() / 2;
901                         mCustomizePanel.show(x, y);
902                     }
903                 }
904 
905             }
906         });
907     }
908 
closeDetail()909     public void closeDetail() {
910         if (mCustomizePanel != null && mCustomizePanel.isShown()) {
911             // Treat this as a detail panel for now, to make things easy.
912             mCustomizePanel.hide();
913             return;
914         }
915         showDetail(false, mDetailRecord);
916     }
917 
getGridHeight()918     public int getGridHeight() {
919         return getMeasuredHeight();
920     }
921 
handleShowDetail(Record r, boolean show)922     protected void handleShowDetail(Record r, boolean show) {
923         if (r instanceof TileRecord) {
924             handleShowDetailTile((TileRecord) r, show);
925         } else {
926             int x = 0;
927             int y = 0;
928             if (r != null) {
929                 x = r.x;
930                 y = r.y;
931             }
932             handleShowDetailImpl(r, show, x, y);
933         }
934     }
935 
handleShowDetailTile(TileRecord r, boolean show)936     private void handleShowDetailTile(TileRecord r, boolean show) {
937         if ((mDetailRecord != null) == show && mDetailRecord == r) return;
938 
939         if (show) {
940             r.detailAdapter = r.tile.getDetailAdapter();
941             if (r.detailAdapter == null) return;
942         }
943         r.tile.setDetailListening(show);
944         int x = r.tileView.getLeft() + r.tileView.getWidth() / 2;
945         int y = r.tileView.getDetailY() + mTileLayout.getOffsetTop(r) + getTop();
946         handleShowDetailImpl(r, show, x, y);
947     }
948 
handleShowDetailImpl(Record r, boolean show, int x, int y)949     private void handleShowDetailImpl(Record r, boolean show, int x, int y) {
950         setDetailRecord(show ? r : null);
951         fireShowingDetail(show ? r.detailAdapter : null, x, y);
952     }
953 
setDetailRecord(Record r)954     protected void setDetailRecord(Record r) {
955         if (r == mDetailRecord) return;
956         mDetailRecord = r;
957         final boolean scanState = mDetailRecord instanceof TileRecord
958                 && ((TileRecord) mDetailRecord).scanState;
959         fireScanStateChanged(scanState);
960     }
961 
setGridContentVisibility(boolean visible)962     void setGridContentVisibility(boolean visible) {
963         int newVis = visible ? VISIBLE : INVISIBLE;
964         setVisibility(newVis);
965         if (mGridContentVisible != visible) {
966             mMetricsLogger.visibility(MetricsEvent.QS_PANEL, newVis);
967         }
968         mGridContentVisible = visible;
969     }
970 
logTiles()971     private void logTiles() {
972         for (int i = 0; i < mRecords.size(); i++) {
973             QSTile tile = mRecords.get(i).tile;
974             mMetricsLogger.write(tile.populate(new LogMaker(tile.getMetricsCategory())
975                     .setType(MetricsEvent.TYPE_OPEN)));
976         }
977     }
978 
fireShowingDetail(DetailAdapter detail, int x, int y)979     private void fireShowingDetail(DetailAdapter detail, int x, int y) {
980         if (mCallback != null) {
981             mCallback.onShowingDetail(detail, x, y);
982         }
983     }
984 
fireToggleStateChanged(boolean state)985     private void fireToggleStateChanged(boolean state) {
986         if (mCallback != null) {
987             mCallback.onToggleStateChanged(state);
988         }
989     }
990 
fireScanStateChanged(boolean state)991     private void fireScanStateChanged(boolean state) {
992         if (mCallback != null) {
993             mCallback.onScanStateChanged(state);
994         }
995     }
996 
clickTile(ComponentName tile)997     public void clickTile(ComponentName tile) {
998         final String spec = CustomTile.toSpec(tile);
999         final int N = mRecords.size();
1000         for (int i = 0; i < N; i++) {
1001             if (mRecords.get(i).tile.getTileSpec().equals(spec)) {
1002                 mRecords.get(i).tile.click();
1003                 break;
1004             }
1005         }
1006     }
1007 
getTileLayout()1008     QSTileLayout getTileLayout() {
1009         return mTileLayout;
1010     }
1011 
getTileView(QSTile tile)1012     QSTileView getTileView(QSTile tile) {
1013         for (TileRecord r : mRecords) {
1014             if (r.tile == tile) {
1015                 return r.tileView;
1016             }
1017         }
1018         return null;
1019     }
1020 
1021     @Nullable
getSecurityFooter()1022     public QSSecurityFooter getSecurityFooter() {
1023         return mSecurityFooter;
1024     }
1025 
1026     @Nullable
getDivider()1027     public View getDivider() {
1028         return mDivider;
1029     }
1030 
showDeviceMonitoringDialog()1031     public void showDeviceMonitoringDialog() {
1032         if (mSecurityFooter != null) {
1033             mSecurityFooter.showDeviceMonitoringDialog();
1034         }
1035     }
1036 
setContentMargins(int startMargin, int endMargin)1037     public void setContentMargins(int startMargin, int endMargin) {
1038         // Only some views actually want this content padding, others want to go all the way
1039         // to the edge like the brightness slider
1040         mContentMarginStart = startMargin;
1041         mContentMarginEnd = endMargin;
1042         updateTileLayoutMargins(mContentMarginStart - mVisualTilePadding,
1043                 mContentMarginEnd - mVisualTilePadding);
1044         updateMediaHostContentMargins();
1045         updateFooterMargin();
1046         updateDividerMargin();
1047     }
1048 
updateFooterMargin()1049     private void updateFooterMargin() {
1050         if (mFooter != null) {
1051             int footerMargin = 0;
1052             int indicatorMargin = 0;
1053             if (mUsingHorizontalLayout) {
1054                 footerMargin = mFooterMarginStartHorizontal;
1055                 indicatorMargin = footerMargin - mVisualMarginEnd;
1056             }
1057             updateMargins(mFooter, footerMargin, 0);
1058             // The page indicator isn't centered anymore because of the visual positioning.
1059             // Let's fix it by adding some margin
1060             if (mFooterPageIndicator != null) {
1061                 updateMargins(mFooterPageIndicator, 0, indicatorMargin);
1062             }
1063         }
1064     }
1065 
1066     /**
1067      * Update the margins of all tile Layouts.
1068      *
1069      * @param visualMarginStart the visual start margin of the tile, adjusted for local insets
1070      *                          to the tile. This can be set on a tileLayout
1071      * @param visualMarginEnd the visual end margin of the tile, adjusted for local insets
1072      *                        to the tile. This can be set on a tileLayout
1073      */
updateTileLayoutMargins(int visualMarginStart, int visualMarginEnd)1074     private void updateTileLayoutMargins(int visualMarginStart, int visualMarginEnd) {
1075         mVisualMarginStart = visualMarginStart;
1076         mVisualMarginEnd = visualMarginEnd;
1077         updateTileLayoutMargins();
1078     }
1079 
updateTileLayoutMargins()1080     private void updateTileLayoutMargins() {
1081         int marginEnd = mVisualMarginEnd;
1082         if (mUsingHorizontalLayout) {
1083             marginEnd = 0;
1084         }
1085         updateMargins((View) mTileLayout, mVisualMarginStart, marginEnd);
1086     }
1087 
updateDividerMargin()1088     private void updateDividerMargin() {
1089         if (mDivider == null) return;
1090         updateMargins(mDivider, mContentMarginStart, mContentMarginEnd);
1091     }
1092 
1093     /**
1094      * Update the margins of the media hosts
1095      */
updateMediaHostContentMargins()1096     protected void updateMediaHostContentMargins() {
1097         if (mUsingMediaPlayer) {
1098             int marginStart = mContentMarginStart;
1099             if (mUsingHorizontalLayout) {
1100                 marginStart = 0;
1101             }
1102             updateMargins(mMediaHost.getHostView(), marginStart, mContentMarginEnd);
1103         }
1104     }
1105 
1106     /**
1107      * Update the margins of a view.
1108      *
1109      * @param view the view to adjust
1110      * @param start the start margin to set
1111      * @param end the end margin to set
1112      */
updateMargins(View view, int start, int end)1113     protected void updateMargins(View view, int start, int end) {
1114         LayoutParams lp = (LayoutParams) view.getLayoutParams();
1115         lp.setMarginStart(start);
1116         lp.setMarginEnd(end);
1117         view.setLayoutParams(lp);
1118     }
1119 
getMediaHost()1120     public MediaHost getMediaHost() {
1121         return mMediaHost;
1122     }
1123 
1124     /**
1125      * Set the header container of quick settings.
1126      */
setHeaderContainer(@onNull ViewGroup headerContainer)1127     public void setHeaderContainer(@NonNull ViewGroup headerContainer) {
1128         mHeaderContainer = headerContainer;
1129     }
1130 
setMediaVisibilityChangedListener(Consumer<Boolean> visibilityChangedListener)1131     public void setMediaVisibilityChangedListener(Consumer<Boolean> visibilityChangedListener) {
1132         mMediaVisibilityChangedListener = visibilityChangedListener;
1133     }
1134 
1135     private class H extends Handler {
1136         private static final int SHOW_DETAIL = 1;
1137         private static final int SET_TILE_VISIBILITY = 2;
1138         private static final int ANNOUNCE_FOR_ACCESSIBILITY = 3;
1139 
1140         @Override
handleMessage(Message msg)1141         public void handleMessage(Message msg) {
1142             if (msg.what == SHOW_DETAIL) {
1143                 handleShowDetail((Record) msg.obj, msg.arg1 != 0);
1144             } else if (msg.what == ANNOUNCE_FOR_ACCESSIBILITY) {
1145                 announceForAccessibility((CharSequence) msg.obj);
1146             }
1147         }
1148     }
1149 
1150     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)1151     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1152         pw.println(getClass().getSimpleName() + ":");
1153         pw.println("  Tile records:");
1154         for (TileRecord record : mRecords) {
1155             if (record.tile instanceof Dumpable) {
1156                 pw.print("    "); ((Dumpable) record.tile).dump(fd, pw, args);
1157                 pw.print("    "); pw.println(record.tileView.toString());
1158             }
1159         }
1160     }
1161 
1162 
1163     protected static class Record {
1164         DetailAdapter detailAdapter;
1165         int x;
1166         int y;
1167     }
1168 
1169     public static final class TileRecord extends Record {
1170         public QSTile tile;
1171         public com.android.systemui.plugins.qs.QSTileView tileView;
1172         public boolean scanState;
1173         public QSTile.Callback callback;
1174     }
1175 
1176     public interface QSTileLayout {
1177 
saveInstanceState(Bundle outState)1178         default void saveInstanceState(Bundle outState) {}
1179 
restoreInstanceState(Bundle savedInstanceState)1180         default void restoreInstanceState(Bundle savedInstanceState) {}
1181 
addTile(TileRecord tile)1182         void addTile(TileRecord tile);
1183 
removeTile(TileRecord tile)1184         void removeTile(TileRecord tile);
1185 
getOffsetTop(TileRecord tile)1186         int getOffsetTop(TileRecord tile);
1187 
updateResources()1188         boolean updateResources();
1189 
setListening(boolean listening)1190         void setListening(boolean listening);
1191 
1192         /**
1193          * Set the minimum number of rows to show
1194          *
1195          * @param minRows the minimum.
1196          */
setMinRows(int minRows)1197         default boolean setMinRows(int minRows) {
1198             return false;
1199         }
1200 
1201         /**
1202          * Set the max number of collums to show
1203          *
1204          * @param maxColumns the maximum
1205          *
1206          * @return true if the number of visible columns has changed.
1207          */
setMaxColumns(int maxColumns)1208         default boolean setMaxColumns(int maxColumns) {
1209             return false;
1210         }
1211 
setExpansion(float expansion)1212         default void setExpansion(float expansion) {}
1213 
getNumVisibleTiles()1214         int getNumVisibleTiles();
1215     }
1216 }
1217