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 android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS;
20 
21 import static com.android.systemui.Flags.centralizedStatusBarHeightFix;
22 
23 import android.content.Context;
24 import android.graphics.Canvas;
25 import android.graphics.Path;
26 import android.graphics.PointF;
27 import android.util.AttributeSet;
28 import android.view.MotionEvent;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.widget.FrameLayout;
32 
33 import androidx.annotation.Nullable;
34 
35 import com.android.systemui.Dumpable;
36 import com.android.systemui.qs.customize.QSCustomizer;
37 import com.android.systemui.res.R;
38 import com.android.systemui.shade.LargeScreenHeaderHelper;
39 import com.android.systemui.shade.TouchLogger;
40 import com.android.systemui.util.LargeScreenUtils;
41 
42 import java.io.PrintWriter;
43 
44 /**
45  * Wrapper view with background which contains {@link QSPanel} and {@link QuickStatusBarHeader}
46  */
47 public class QSContainerImpl extends FrameLayout implements Dumpable {
48 
49     private int mFancyClippingLeftInset;
50     private int mFancyClippingTop;
51     private int mFancyClippingRightInset;
52     private int mFancyClippingBottom;
53     private final float[] mFancyClippingRadii = new float[] {0, 0, 0, 0, 0, 0, 0, 0};
54     private  final Path mFancyClippingPath = new Path();
55     private int mHeightOverride = -1;
56     private QuickStatusBarHeader mHeader;
57     private float mQsExpansion;
58     private QSCustomizer mQSCustomizer;
59     private QSPanel mQSPanel;
60     private NonInterceptingScrollView mQSPanelContainer;
61 
62     private int mHorizontalMargins;
63     private int mTilesPageMargin;
64     private boolean mQsDisabled;
65     private int mContentHorizontalPadding = -1;
66     private boolean mClippingEnabled;
67     private boolean mIsFullWidth;
68 
69     private boolean mSceneContainerEnabled;
70 
QSContainerImpl(Context context, AttributeSet attrs)71     public QSContainerImpl(Context context, AttributeSet attrs) {
72         super(context, attrs);
73     }
74 
75     @Override
onFinishInflate()76     protected void onFinishInflate() {
77         super.onFinishInflate();
78         mQSPanelContainer = findViewById(R.id.expanded_qs_scroll_view);
79         mQSPanel = findViewById(R.id.quick_settings_panel);
80         mHeader = findViewById(R.id.header);
81         mQSCustomizer = findViewById(R.id.qs_customize);
82         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
83     }
84 
setSceneContainerEnabled(boolean enabled)85     void setSceneContainerEnabled(boolean enabled) {
86         mSceneContainerEnabled = enabled;
87         if (enabled) {
88             mQSPanelContainer.removeAllViews();
89             removeView(mQSPanelContainer);
90             LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,
91                     ViewGroup.LayoutParams.WRAP_CONTENT);
92             addView(mQSPanel, 0, lp);
93         }
94     }
95 
96     @Override
hasOverlappingRendering()97     public boolean hasOverlappingRendering() {
98         return false;
99     }
100 
101     @Override
performClick()102     public boolean performClick() {
103         // Want to receive clicks so missing QQS tiles doesn't cause collapse, but
104         // don't want to do anything with them.
105         return true;
106     }
107 
108     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)109     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
110         // QSPanel will show as many rows as it can (up to TileLayout.MAX_ROWS) such that the
111         // bottom and footer are inside the screen.
112         int availableHeight = View.MeasureSpec.getSize(heightMeasureSpec);
113 
114         if (!mSceneContainerEnabled) {
115             MarginLayoutParams layoutParams =
116                     (MarginLayoutParams) mQSPanelContainer.getLayoutParams();
117             int maxQs = availableHeight - layoutParams.topMargin - layoutParams.bottomMargin
118                     - getPaddingBottom();
119             int padding = mPaddingLeft + mPaddingRight + layoutParams.leftMargin
120                     + layoutParams.rightMargin;
121             final int qsPanelWidthSpec = getChildMeasureSpec(widthMeasureSpec, padding,
122                     layoutParams.width);
123             mQSPanelContainer.measure(qsPanelWidthSpec,
124                     MeasureSpec.makeMeasureSpec(maxQs, MeasureSpec.AT_MOST));
125             int width = mQSPanelContainer.getMeasuredWidth() + padding;
126             super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
127                     MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY));
128         } else {
129             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
130         }
131 
132         // QSCustomizer will always be the height of the screen, but do this after
133         // other measuring to avoid changing the height of the QS.
134         mQSCustomizer.measure(widthMeasureSpec,
135                 MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY));
136     }
137 
138     @Override
dispatchDraw(Canvas canvas)139     public void dispatchDraw(Canvas canvas) {
140         if (!mFancyClippingPath.isEmpty()) {
141             canvas.translate(0, -getTranslationY());
142             canvas.clipOutPath(mFancyClippingPath);
143             canvas.translate(0, getTranslationY());
144         }
145         super.dispatchDraw(canvas);
146     }
147 
148     @Override
measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)149     protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
150             int parentHeightMeasureSpec, int heightUsed) {
151         if (!mSceneContainerEnabled) {
152             // Do not measure QSPanel again when doing super.onMeasure.
153             // This prevents the pages in PagedTileLayout to be remeasured with a different
154             // (incorrect) size to the one used for determining the number of rows and then the
155             // number of pages.
156             if (child != mQSPanelContainer) {
157                 super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed,
158                         parentHeightMeasureSpec, heightUsed);
159             }
160         } else {
161             // Don't measure the customizer with all the children, it will be measured separately
162             if (child != mQSCustomizer) {
163                 super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed,
164                         parentHeightMeasureSpec, heightUsed);
165             }
166         }
167     }
168 
169     @Override
dispatchTouchEvent(MotionEvent ev)170     public boolean dispatchTouchEvent(MotionEvent ev) {
171         return TouchLogger.logDispatchTouch("QS", ev, super.dispatchTouchEvent(ev));
172     }
173 
174     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)175     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
176         super.onLayout(changed, left, top, right, bottom);
177         updateExpansion();
178         updateClippingPath();
179     }
180 
181     @Nullable
getQSPanelContainer()182     public NonInterceptingScrollView getQSPanelContainer() {
183         return mQSPanelContainer;
184     }
185 
disable(int state1, int state2, boolean animate)186     public void disable(int state1, int state2, boolean animate) {
187         final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0;
188         if (disabled == mQsDisabled) return;
189         mQsDisabled = disabled;
190     }
191 
updateResources(QSPanelController qsPanelController, QuickStatusBarHeaderController quickStatusBarHeaderController)192     void updateResources(QSPanelController qsPanelController,
193             QuickStatusBarHeaderController quickStatusBarHeaderController) {
194         int topPadding = QSUtils.getQsHeaderSystemIconsAreaHeight(mContext);
195         if (!LargeScreenUtils.shouldUseLargeScreenShadeHeader(mContext.getResources())) {
196             topPadding =
197                     centralizedStatusBarHeightFix()
198                             ? LargeScreenHeaderHelper.getLargeScreenHeaderHeight(mContext)
199                             : mContext.getResources()
200                                     .getDimensionPixelSize(
201                                             R.dimen.large_screen_shade_header_height);
202         }
203         if (mQSPanelContainer != null) {
204             mQSPanelContainer.setPaddingRelative(
205                     mQSPanelContainer.getPaddingStart(),
206                     mSceneContainerEnabled ? 0 : topPadding,
207                     mQSPanelContainer.getPaddingEnd(),
208                     mQSPanelContainer.getPaddingBottom());
209         } else {
210             mQSPanel.setPaddingRelative(
211                     mQSPanel.getPaddingStart(),
212                     mSceneContainerEnabled ? 0 : topPadding,
213                     mQSPanel.getPaddingEnd(),
214                     mQSPanel.getPaddingBottom());
215         }
216 
217         int horizontalMargins = getResources().getDimensionPixelSize(R.dimen.qs_horizontal_margin);
218         int horizontalPadding = getResources().getDimensionPixelSize(
219                 R.dimen.qs_content_horizontal_padding);
220         int tilesPageMargin = getResources().getDimensionPixelSize(
221                 R.dimen.qs_tiles_page_horizontal_margin);
222         boolean marginsChanged = horizontalPadding != mContentHorizontalPadding
223                 || horizontalMargins != mHorizontalMargins
224                 || tilesPageMargin != mTilesPageMargin;
225         mContentHorizontalPadding = horizontalPadding;
226         mHorizontalMargins = horizontalMargins;
227         mTilesPageMargin = tilesPageMargin;
228         if (marginsChanged) {
229             updatePaddingsAndMargins(qsPanelController, quickStatusBarHeaderController);
230         }
231     }
232 
233     /**
234      * Overrides the height of this view (post-layout), so that the content is clipped to that
235      * height and the background is set to that height.
236      *
237      * @param heightOverride the overridden height
238      */
setHeightOverride(int heightOverride)239     public void setHeightOverride(int heightOverride) {
240         mHeightOverride = heightOverride;
241         updateExpansion();
242     }
243 
updateExpansion()244     public void updateExpansion() {
245         int height = calculateContainerHeight();
246         setBottom(getTop() + height);
247     }
248 
calculateContainerHeight()249     protected int calculateContainerHeight() {
250         int heightOverride = mHeightOverride != -1 ? mHeightOverride : getMeasuredHeight();
251         // Need to add the dragHandle height so touches will be intercepted by it.
252         return mQSCustomizer.isCustomizing() ? mQSCustomizer.getHeight()
253                 : Math.round(mQsExpansion * (heightOverride - mHeader.getHeight()))
254                 + mHeader.getHeight();
255     }
256 
257     // These next two methods are used with Scene container to determine the size of QQS and QS .
258 
259     /**
260      * Returns the size of the QQS container, regardless of the measured size of this view.
261      * @return size in pixels of QQS
262      */
getQqsHeight()263     public int getQqsHeight() {
264         return mHeader.getHeight();
265     }
266 
267     /**
268      * Returns the size of QS (or the QSCustomizer), regardless of the measured size of this view
269      * @return size in pixels of QS (or QSCustomizer)
270      */
getQsHeight()271     public int getQsHeight() {
272         return mQSCustomizer.isCustomizing() ? mQSCustomizer.getMeasuredHeight()
273                 : mQSPanel.getMeasuredHeight();
274     }
275 
setExpansion(float expansion)276     public void setExpansion(float expansion) {
277         mQsExpansion = expansion;
278         if (mQSPanelContainer != null) {
279             mQSPanelContainer.setScrollingEnabled(expansion > 0f);
280         }
281         updateExpansion();
282     }
283 
updatePaddingsAndMargins(QSPanelController qsPanelController, QuickStatusBarHeaderController quickStatusBarHeaderController)284     private void updatePaddingsAndMargins(QSPanelController qsPanelController,
285             QuickStatusBarHeaderController quickStatusBarHeaderController) {
286         for (int i = 0; i < getChildCount(); i++) {
287             View view = getChildAt(i);
288             if (view == mQSCustomizer) {
289                 // Some views are always full width or have dependent padding
290                 continue;
291             }
292             if (view.getId() != R.id.qs_footer_actions) {
293                 // Only padding for FooterActionsView, no margin. That way, the background goes
294                 // all the way to the edge.
295                 LayoutParams lp = (LayoutParams) view.getLayoutParams();
296                 lp.rightMargin = mHorizontalMargins;
297                 lp.leftMargin = mHorizontalMargins;
298             }
299             if (view == mQSPanelContainer || view == mQSPanel) {
300                 // QS panel lays out some of its content full width
301                 qsPanelController.setContentMargins(mContentHorizontalPadding,
302                         mContentHorizontalPadding);
303                 qsPanelController.setPageMargin(mTilesPageMargin);
304             } else if (view == mHeader) {
305                 quickStatusBarHeaderController.setContentMargins(mContentHorizontalPadding,
306                         mContentHorizontalPadding);
307             } else {
308                 // Set the horizontal paddings unless the view is the Compose implementation of the
309                 // footer actions.
310                 if (view.getId() != R.id.qs_footer_actions) {
311                     view.setPaddingRelative(
312                             mContentHorizontalPadding,
313                             view.getPaddingTop(),
314                             mContentHorizontalPadding,
315                             view.getPaddingBottom());
316                 }
317             }
318         }
319     }
320 
321     /**
322      * Clip QS bottom using a concave shape.
323      */
setFancyClipping(int leftInset, int top, int rightInset, int bottom, int radius, boolean enabled, boolean fullWidth)324     public void setFancyClipping(int leftInset, int top, int rightInset, int bottom, int radius,
325             boolean enabled, boolean fullWidth) {
326         boolean updatePath = false;
327         if (mFancyClippingRadii[0] != radius) {
328             mFancyClippingRadii[0] = radius;
329             mFancyClippingRadii[1] = radius;
330             mFancyClippingRadii[2] = radius;
331             mFancyClippingRadii[3] = radius;
332             updatePath = true;
333         }
334         if (mFancyClippingLeftInset != leftInset) {
335             mFancyClippingLeftInset = leftInset;
336             updatePath = true;
337         }
338         if (mFancyClippingTop != top) {
339             mFancyClippingTop = top;
340             updatePath = true;
341         }
342         if (mFancyClippingRightInset != rightInset) {
343             mFancyClippingRightInset = rightInset;
344             updatePath = true;
345         }
346         if (mFancyClippingBottom != bottom) {
347             mFancyClippingBottom = bottom;
348             updatePath = true;
349         }
350         if (mClippingEnabled != enabled) {
351             mClippingEnabled = enabled;
352             updatePath = true;
353         }
354         if (mIsFullWidth != fullWidth) {
355             mIsFullWidth = fullWidth;
356             updatePath = true;
357         }
358 
359         if (updatePath) {
360             updateClippingPath();
361         }
362     }
363 
364     @Override
isTransformedTouchPointInView(float x, float y, View child, PointF outLocalPoint)365     protected boolean isTransformedTouchPointInView(float x, float y,
366             View child, PointF outLocalPoint) {
367         // Prevent touches outside the clipped area from propagating to a child in that area.
368         if (mClippingEnabled && y + getTranslationY() > mFancyClippingTop) {
369             return false;
370         }
371         return super.isTransformedTouchPointInView(x, y, child, outLocalPoint);
372     }
373 
updateClippingPath()374     private void updateClippingPath() {
375         mFancyClippingPath.reset();
376         if (!mClippingEnabled) {
377             invalidate();
378             return;
379         }
380 
381         int clippingLeft = mIsFullWidth ? -mFancyClippingLeftInset : 0;
382         int clippingRight = mIsFullWidth ? getWidth() + mFancyClippingRightInset : getWidth();
383         mFancyClippingPath.addRoundRect(clippingLeft, mFancyClippingTop, clippingRight,
384                 mFancyClippingBottom, mFancyClippingRadii, Path.Direction.CW);
385         invalidate();
386     }
387 
388     @Override
dump(PrintWriter pw, String[] args)389     public void dump(PrintWriter pw, String[] args) {
390         pw.println(getClass().getSimpleName() + " updateClippingPath: "
391                 + "leftInset(" + mFancyClippingLeftInset + ") "
392                 + "top(" + mFancyClippingTop + ") "
393                 + "rightInset(" + mFancyClippingRightInset + ") "
394                 + "bottom(" + mFancyClippingBottom  + ") "
395                 + "mClippingEnabled(" + mClippingEnabled + ") "
396                 + "mIsFullWidth(" + mIsFullWidth + ")");
397     }
398 }
399