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.shade;
18 
19 import static androidx.constraintlayout.core.widgets.Optimizer.OPTIMIZATION_GRAPH;
20 
21 import android.app.Fragment;
22 import android.content.Context;
23 import android.content.res.Configuration;
24 import android.graphics.Canvas;
25 import android.graphics.Rect;
26 import android.util.AttributeSet;
27 import android.view.MotionEvent;
28 import android.view.View;
29 import android.view.WindowInsets;
30 
31 import androidx.annotation.Nullable;
32 import androidx.constraintlayout.widget.ConstraintLayout;
33 import androidx.constraintlayout.widget.ConstraintSet;
34 
35 import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
36 import com.android.systemui.keyguard.MigrateClocksToBlueprint;
37 import com.android.systemui.plugins.qs.QS;
38 import com.android.systemui.res.R;
39 import com.android.systemui.statusbar.notification.AboveShelfObserver;
40 
41 import java.util.ArrayList;
42 import java.util.Comparator;
43 import java.util.function.Consumer;
44 
45 /**
46  * The container with notification stack scroller and quick settings inside.
47  */
48 public class NotificationsQuickSettingsContainer extends ConstraintLayout
49         implements FragmentListener, AboveShelfObserver.HasViewAboveShelfChangedListener {
50 
51     private View mQsFrame;
52     private View mStackScroller;
53     private View mKeyguardStatusBar;
54 
55     private final ArrayList<View> mDrawingOrderedChildren = new ArrayList<>();
56     private final ArrayList<View> mLayoutDrawingOrder = new ArrayList<>();
57     private final Comparator<View> mIndexComparator = Comparator.comparingInt(this::indexOfChild);
58     private Consumer<WindowInsets> mInsetsChangedListener = insets -> {};
59     private Consumer<QS> mQSFragmentAttachedListener = qs -> {};
60     private QS mQs;
61     private View mQSContainer;
62     private int mLastQSPaddingBottom;
63 
64     /**
65      *  These are used to compute the bounding box containing the shade and the notification scrim,
66      *  which is then used to drive the Back gesture animation.
67      */
68     private final Rect mUpperRect = new Rect();
69     private final Rect mBoundingBoxRect = new Rect();
70 
71     @Nullable
72     private Consumer<Configuration> mConfigurationChangedListener;
73 
NotificationsQuickSettingsContainer(Context context, AttributeSet attrs)74     public NotificationsQuickSettingsContainer(Context context, AttributeSet attrs) {
75         super(context, attrs);
76         setOptimizationLevel(getOptimizationLevel() | OPTIMIZATION_GRAPH);
77     }
78 
79     @Override
onFinishInflate()80     protected void onFinishInflate() {
81         super.onFinishInflate();
82         mQsFrame = findViewById(R.id.qs_frame);
83         mKeyguardStatusBar = findViewById(R.id.keyguard_header);
84     }
85 
setStackScroller(View stackScroller)86     void setStackScroller(View stackScroller) {
87         mStackScroller = stackScroller;
88     }
89 
90     @Override
onFragmentViewCreated(String tag, Fragment fragment)91     public void onFragmentViewCreated(String tag, Fragment fragment) {
92         mQs = (QS) fragment;
93         mQSFragmentAttachedListener.accept(mQs);
94         mQSContainer = mQs.getView().findViewById(R.id.quick_settings_container);
95         // We need to restore the bottom padding as the fragment may have been recreated due to
96         // some special Configuration change, so we apply the last known padding (this will be
97         // correct even if it has changed while the fragment was destroyed and re-created).
98         setQSContainerPaddingBottom(mLastQSPaddingBottom);
99     }
100 
101     @Override
onHasViewsAboveShelfChanged(boolean hasViewsAboveShelf)102     public void onHasViewsAboveShelfChanged(boolean hasViewsAboveShelf) {
103         invalidate();
104     }
105 
106     @Override
onConfigurationChanged(Configuration newConfig)107     protected void onConfigurationChanged(Configuration newConfig) {
108         super.onConfigurationChanged(newConfig);
109         if (mConfigurationChangedListener != null) {
110             mConfigurationChangedListener.accept(newConfig);
111         }
112     }
113 
setConfigurationChangedListener(Consumer<Configuration> listener)114     public void setConfigurationChangedListener(Consumer<Configuration> listener) {
115         mConfigurationChangedListener = listener;
116     }
117 
setNotificationsMarginBottom(int margin)118     public void setNotificationsMarginBottom(int margin) {
119         MarginLayoutParams params = (MarginLayoutParams) mStackScroller.getLayoutParams();
120         params.bottomMargin = margin;
121         mStackScroller.setLayoutParams(params);
122     }
123 
setQSContainerPaddingBottom(int paddingBottom)124     public void setQSContainerPaddingBottom(int paddingBottom) {
125         mLastQSPaddingBottom = paddingBottom;
126         if (mQSContainer != null) {
127             mQSContainer.setPadding(
128                     mQSContainer.getPaddingLeft(),
129                     mQSContainer.getPaddingTop(),
130                     mQSContainer.getPaddingRight(),
131                     paddingBottom
132             );
133         }
134     }
135 
setInsetsChangedListener(Consumer<WindowInsets> onInsetsChangedListener)136     public void setInsetsChangedListener(Consumer<WindowInsets> onInsetsChangedListener) {
137         mInsetsChangedListener = onInsetsChangedListener;
138     }
139 
removeOnInsetsChangedListener()140     public void removeOnInsetsChangedListener() {
141         mInsetsChangedListener = insets -> {};
142     }
143 
setQSFragmentAttachedListener(Consumer<QS> qsFragmentAttachedListener)144     public void setQSFragmentAttachedListener(Consumer<QS> qsFragmentAttachedListener) {
145         mQSFragmentAttachedListener = qsFragmentAttachedListener;
146         // listener might be attached after fragment is attached
147         if (mQs != null) {
148             mQSFragmentAttachedListener.accept(mQs);
149         }
150     }
151 
removeQSFragmentAttachedListener()152     public void removeQSFragmentAttachedListener() {
153         mQSFragmentAttachedListener = qs -> {};
154     }
155 
156     @Override
onApplyWindowInsets(WindowInsets insets)157     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
158         mInsetsChangedListener.accept(insets);
159         return insets;
160     }
161 
162     @Override
dispatchDraw(Canvas canvas)163     protected void dispatchDraw(Canvas canvas) {
164         mDrawingOrderedChildren.clear();
165         mLayoutDrawingOrder.clear();
166         if (mKeyguardStatusBar.getVisibility() == View.VISIBLE) {
167             mDrawingOrderedChildren.add(mKeyguardStatusBar);
168             mLayoutDrawingOrder.add(mKeyguardStatusBar);
169         }
170         if (mQsFrame.getVisibility() == View.VISIBLE) {
171             mDrawingOrderedChildren.add(mQsFrame);
172             mLayoutDrawingOrder.add(mQsFrame);
173         }
174         if (mStackScroller.getVisibility() == View.VISIBLE) {
175             mDrawingOrderedChildren.add(mStackScroller);
176             mLayoutDrawingOrder.add(mStackScroller);
177         }
178 
179         // Let's now find the order that the view has when drawing regularly by sorting
180         mLayoutDrawingOrder.sort(mIndexComparator);
181         super.dispatchDraw(canvas);
182     }
183 
184     @Override
dispatchTouchEvent(MotionEvent ev)185     public boolean dispatchTouchEvent(MotionEvent ev) {
186         return TouchLogger.logDispatchTouch("NotificationsQuickSettingsContainer", ev,
187                 super.dispatchTouchEvent(ev));
188     }
189 
190     @Override
drawChild(Canvas canvas, View child, long drawingTime)191     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
192         if (MigrateClocksToBlueprint.isEnabled()) {
193             return super.drawChild(canvas, child, drawingTime);
194         }
195         int layoutIndex = mLayoutDrawingOrder.indexOf(child);
196         if (layoutIndex >= 0) {
197             return super.drawChild(canvas, mDrawingOrderedChildren.get(layoutIndex), drawingTime);
198         } else {
199             return super.drawChild(canvas, child, drawingTime);
200         }
201     }
202 
applyConstraints(ConstraintSet constraintSet)203     public void applyConstraints(ConstraintSet constraintSet) {
204         constraintSet.applyTo(this);
205     }
206 
207     /**
208      *  Scale multiple elements in tandem, for the predictive back animation.
209      *  This is how the Shade responds to the Back gesture (by scaling).
210      *  Without the common center, individual elements will scale about their respective centers.
211      *  Scaling the entire NotificationsQuickSettingsContainer will also resize the shade header
212      *  (which we don't want).
213      */
applyBackScaling(float scale, boolean usingSplitShade)214     public void applyBackScaling(float scale, boolean usingSplitShade) {
215         if (mStackScroller == null || mQSContainer == null) {
216             return;
217         }
218 
219         mQSContainer.getBoundsOnScreen(mUpperRect);
220         mStackScroller.getBoundsOnScreen(mBoundingBoxRect);
221         mBoundingBoxRect.union(mUpperRect);
222 
223         float cx = mBoundingBoxRect.centerX();
224         float cy = mBoundingBoxRect.centerY();
225 
226         mQSContainer.setPivotX(cx);
227         mQSContainer.setPivotY(cy);
228         mQSContainer.setScaleX(scale);
229         mQSContainer.setScaleY(scale);
230 
231         // When in large-screen split-shade mode, the notification stack scroller scales correctly
232         // only if the pivot point is at the left edge of the screen (because of its dimensions).
233         // When not in large-screen split-shade mode, we can scale correctly via the (cx,cy) above.
234         mStackScroller.setPivotX(usingSplitShade ? 0.0f : cx);
235         mStackScroller.setPivotY(cy);
236         mStackScroller.setScaleX(scale);
237         mStackScroller.setScaleY(scale);
238     }
239 }
240