1 /*
2  * Copyright (C) 2019 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 android.systemui.cts;
18 
19 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
20 
21 import android.annotation.MainThread;
22 import android.graphics.Insets;
23 import android.graphics.Point;
24 import android.graphics.Rect;
25 import android.os.Bundle;
26 import android.util.DisplayMetrics;
27 import android.view.Display;
28 import android.view.DisplayCutout;
29 import android.view.Gravity;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.view.Window;
33 import android.view.WindowInsets;
34 import android.widget.TextView;
35 
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.List;
39 import java.util.function.Consumer;
40 
41 public class WindowInsetsActivity extends LightBarBaseActivity implements View.OnClickListener,
42         View.OnApplyWindowInsetsListener {
43     private static final int DISPLAY_CUTOUT_SLACK_DP = 20;
44 
45     private TextView mContent;
46     private WindowInsets mContentWindowInsets;
47     private WindowInsets mDecorViewWindowInsets;
48     private Rect mDecorBound;
49     private Rect mContentBoundOnScreen;
50     private Rect mContentBoundInWindow;
51 
52     private Consumer<Boolean> mInitialFinishCallBack;
53     private int mClickCount;
54     private Consumer<View> mClickConsumer;
55     private final DisplayMetrics mDisplayMetrics = new DisplayMetrics();
56 
57 
58     /**
59      * To setup the Activity to get better diagnoise.
60      * To setup WindowInsetPresenterDrawable that will show the boundary of the windowInset and
61      * the touch track.
62      */
63     @Override
onCreate(Bundle bundle)64     protected void onCreate(Bundle bundle) {
65         getWindow().requestFeature(Window.FEATURE_NO_TITLE);
66 
67         super.onCreate(bundle);
68 
69         mContent = new TextView(this);
70         mContent.setTextSize(10);
71         mContent.setGravity(Gravity.CENTER);
72         mContent.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
73                 ViewGroup.LayoutParams.MATCH_PARENT));
74         WindowInsetsPresenterDrawable presenterDrawable =
75             new WindowInsetsPresenterDrawable(getWindow().getDecorView().getRootWindowInsets());
76         mContent.setOnTouchListener(presenterDrawable);
77         mContent.setBackground(presenterDrawable);
78         mContent.setOnApplyWindowInsetsListener(this::onApplyWindowInsets);
79         mContent.getRootView().setOnApplyWindowInsetsListener(this::onApplyWindowInsets);
80         getWindow().getDecorView().setOnApplyWindowInsetsListener(this::onApplyWindowInsets);
81         getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
82                 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
83                 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
84         getWindow().getAttributes().layoutInDisplayCutoutMode
85                 = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
86         getWindow().setStatusBarColor(0x22ff0000);
87         getWindow().setNavigationBarColor(0x22ff0000);
88         mContent.setOnClickListener(this);
89         mContent.requestApplyInsets();
90         setContentView(mContent);
91 
92         mContentWindowInsets = getWindow().getDecorView().getRootWindowInsets();
93         mInitialFinishCallBack = null;
94 
95         getDisplay().getRealMetrics(mDisplayMetrics);
96     }
97 
98     @Override
onResume()99     protected void onResume() {
100         super.onResume();
101         mClickCount = 0;
102     }
103 
showVisualBoundary(WindowInsets insets)104     private void showVisualBoundary(WindowInsets insets) {
105         if (insets != null) {
106             WindowInsetsPresenterDrawable presenterDrawable =
107                     (WindowInsetsPresenterDrawable) mContent.getBackground();
108             presenterDrawable.setWindowInsets(insets);
109         }
110     }
111 
112     /**
113      * To get the WindowInsets that comes from onApplyWindowInsets(mContent, insets).
114      */
getContentViewWindowInsets()115     WindowInsets getContentViewWindowInsets() {
116         showVisualBoundary(mContentWindowInsets);
117         return mContentWindowInsets;
118     }
119 
120     /**
121      * To get the WindowInsets that comes from onApplyWindowInsets(DecorView, insets).
122      */
getDecorViewWindowInsets()123     WindowInsets getDecorViewWindowInsets() {
124         showVisualBoundary(mDecorViewWindowInsets);
125         return mDecorViewWindowInsets;
126     }
127 
getContentBoundOnScreen()128     Rect getContentBoundOnScreen() {
129         return mContentBoundOnScreen;
130     }
131 
getContentBoundInWindow()132     Rect getContentBoundInWindow() {
133         return mContentBoundInWindow;
134     }
135 
136     /**
137      * To catch the WindowInsets that passwd to the content view.
138      * This WindowInset should have already consumed the SystemWindowInset.
139      */
140     @Override
onApplyWindowInsets(View v, WindowInsets insets)141     public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
142         if (insets != null) {
143             if (v == mContent) {
144                 mContentWindowInsets = new WindowInsets.Builder(insets).build();
145             } else {
146                 mDecorViewWindowInsets = new WindowInsets.Builder(insets).build();
147             }
148             showInfoInTextView();
149             showVisualBoundary(mDecorViewWindowInsets);
150         }
151 
152         return insets;
153     }
154 
155     @Override
onAttachedToWindow()156     public void onAttachedToWindow() {
157         super.onAttachedToWindow();
158 
159         mContent.post(() -> {
160             mContentBoundOnScreen = getViewBoundOnScreen(mContent);
161             mContentBoundInWindow = getViewBoundInWindow(mContent);
162             mDecorBound = getViewBoundOnScreen(getWindow().getDecorView());
163             showInfoInTextView();
164 
165             if (mInitialFinishCallBack != null) {
166                 mInitialFinishCallBack.accept(true);
167             }
168         });
169     }
170 
171     /**
172      * To present the WindowInsets information to mContent.
173      * To show all of results of getSystemWindowInsets(), getMandatorySytemGestureInsets(),
174      * getSystemGestureInsets(), getTappableElementsInsets() and the exclude rects
175      *
176      * @param rect the rectangle want to add or pass into to setSystemGestureExclusionRects
177      */
178     @MainThread
setSystemGestureExclusion(Rect rect)179     public void setSystemGestureExclusion(Rect rect) {
180         List<Rect> rects = new ArrayList<>();
181         if (rect != null) {
182             rects.add(rect);
183         }
184         getContentView().setSystemGestureExclusionRects(rects);
185         showInfoInTextView();
186     }
187 
showInfoInTextView()188     private void showInfoInTextView() {
189         StringBuilder sb = new StringBuilder();
190         sb.append("exclude rect list = " + Arrays.deepToString(mContent
191                 .getSystemGestureExclusionRects().toArray())).append("\n");
192 
193         if (mDecorViewWindowInsets != null) {
194             sb.append("onApplyWindowInsets mDecorViewWindowInsets = " + mDecorViewWindowInsets);
195             sb.append("\n");
196             sb.append("getSystemWindowInsets = " + mDecorViewWindowInsets.getSystemWindowInsets());
197             sb.append("\n");
198             sb.append("getSystemGestureInsets = "
199                     + mDecorViewWindowInsets.getSystemGestureInsets()).append("\n");
200             sb.append("getMandatorySystemGestureInsets = "
201                     + mDecorViewWindowInsets.getMandatorySystemGestureInsets()).append("\n");
202             sb.append("getTappableElementInsets = "
203                     + mDecorViewWindowInsets.getTappableElementInsets()).append("\n");
204             sb.append("decor boundary = ").append(mDecorBound).append("\n");
205         }
206         if (mContentWindowInsets != null) {
207             sb.append("------------------------").append("\n");
208             sb.append("onApplyWindowInsets mContentWindowInsets = " + mContentWindowInsets);
209             sb.append("\n");
210             sb.append("getSystemWindowInsets = " + mContentWindowInsets.getSystemWindowInsets());
211             sb.append("\n");
212             sb.append("getSystemGestureInsets = "
213                     + mContentWindowInsets.getSystemGestureInsets()).append("\n");
214             sb.append("getMandatorySystemGestureInsets = "
215                     + mContentWindowInsets.getMandatorySystemGestureInsets()).append("\n");
216             sb.append("getTappableElementInsets = "
217                     + mContentWindowInsets.getTappableElementInsets()).append("\n");
218             sb.append("content boundary on screen = ").append(mContentBoundOnScreen).append("\n");
219             sb.append("content boundary in window = ").append(mContentBoundInWindow).append("\n");
220         }
221 
222         Display display = getDisplay();
223         if (display != null) {
224             sb.append("------------------------").append("\n");
225             DisplayCutout displayCutout = display.getCutout();
226             if (displayCutout != null) {
227                 sb.append("displayCutout = ").append(displayCutout.toString()).append("\n");
228             } else {
229                 sb.append("Display cut out = null\n");
230             }
231 
232             sb.append("real size = (").append(mDisplayMetrics.widthPixels).append(",")
233                     .append(mDisplayMetrics.heightPixels).append(")\n");
234         }
235 
236 
237         mContent.setText(sb.toString());
238     }
239 
240     @MainThread
getContentView()241     public View getContentView() {
242         return mContent;
243     }
244 
getViewBoundOnScreen(View view)245     Rect getViewBoundOnScreen(View view) {
246         int [] location = new int[2];
247         view.getLocationOnScreen(location);
248         return new Rect(location[0], location[1],
249                 location[0] + view.getWidth(),
250                 location[1] + view.getHeight());
251     }
252 
getViewBoundInWindow(View view)253     Rect getViewBoundInWindow(View view) {
254         int [] location = new int[2];
255         view.getLocationInWindow(location);
256         return new Rect(location[0], location[1],
257                 location[0] + view.getWidth(),
258                 location[1] + view.getHeight());
259     }
260 
261     @MainThread
getActionBounds(Insets insets, WindowInsets windowInsets)262     public Rect getActionBounds(Insets insets, WindowInsets windowInsets) {
263         return calculateBoundsWithInsets(insets, windowInsets, mContentBoundOnScreen);
264     }
265 
266     @MainThread
getSystemGestureExclusionBounds(Insets insets, WindowInsets windowInsets)267     public Rect getSystemGestureExclusionBounds(Insets insets, WindowInsets windowInsets) {
268         return calculateBoundsWithInsets(insets, windowInsets, mContentBoundInWindow);
269     }
270 
271     /**
272      * Calculate the bounds for performing actions(click, tap or swipe) or for setting the exclusion
273      * rect and the coordinate space of the return Rect could be the display or window coordinate
274      * space which is determined by the passed in refRect.
275      *
276      * @param insets the insets to be tested.
277      * @param windowInsets the WindowInsets that pass to the activity.
278      * @param refRect the rect which determines whether the return rect is the display or the window
279      *                coordinate space.
280      * @return the bounds for performing actions or for setting the exclusion rect.
281      **/
calculateBoundsWithInsets(Insets insets, WindowInsets windowInsets, Rect refRect)282     private Rect calculateBoundsWithInsets(Insets insets, WindowInsets windowInsets, Rect refRect) {
283         int left = insets.left;
284         int top = insets.top;
285         int right = insets.right;
286         int bottom = insets.bottom;
287 
288         final DisplayCutout cutout = windowInsets.getDisplayCutout();
289         if (cutout != null) {
290             int slack = (int) (DISPLAY_CUTOUT_SLACK_DP * mDisplayMetrics.density);
291             if (cutout.getSafeInsetLeft() > 0) {
292                 left = Math.max(left, cutout.getSafeInsetLeft() + slack);
293             }
294             if (cutout.getSafeInsetTop() > 0) {
295                 top = Math.max(top, cutout.getSafeInsetTop() + slack);
296             }
297             if (cutout.getSafeInsetRight() > 0) {
298                 right = Math.max(right, cutout.getSafeInsetRight() + slack);
299             }
300             if (cutout.getSafeInsetBottom() > 0) {
301                 bottom = Math.max(bottom, cutout.getSafeInsetBottom() + slack);
302             }
303         }
304 
305         Rect rect = new Rect(refRect);
306         rect.left += left;
307         rect.top += top;
308         rect.right -= right;
309         rect.bottom -= bottom;
310 
311         return rect;
312     }
313 
314     @MainThread
getActionCancelPoints()315     public List<Point> getActionCancelPoints() {
316         return ((WindowInsetsPresenterDrawable) mContent.getBackground()).getActionCancelPoints();
317     }
318 
319     @MainThread
getActionDownPoints()320     public List<Point> getActionDownPoints() {
321         return ((WindowInsetsPresenterDrawable) mContent.getBackground()).getActionDownPoints();
322     }
323 
324     @MainThread
getActionUpPoints()325     public List<Point> getActionUpPoints() {
326         return ((WindowInsetsPresenterDrawable) mContent.getBackground()).getActionUpPoints();
327     }
328 
329     /**
330      * To set the callback to notify the onClickListener is triggered.
331      * @param clickConsumer trigger the callback after clicking view.
332      */
setOnClickConsumer( Consumer<View> clickConsumer)333     public void setOnClickConsumer(
334             Consumer<View> clickConsumer) {
335         mClickConsumer = clickConsumer;
336     }
337 
338     /**
339      * Because the view needs the focus to catch the ACTION_DOWN otherwise do nothing.
340      * Only for the focus to receive the ACTION_DOWN, ACTION_MOVE, ACTION_UP and ACTION_CANCEL.
341      **/
342     @Override
onClick(View v)343     public void onClick(View v) {
344         mClickCount++;
345         if (mClickConsumer != null) {
346             mClickConsumer.accept(v);
347         }
348     }
349 
getClickCount()350     public int getClickCount() {
351         return mClickCount;
352     }
353 
354     /**
355      * To set the callback to notify the test with the initial finish.
356      * @param initialFinishCallBack trigger the callback after initial finish.
357      */
setInitialFinishCallBack( Consumer<Boolean> initialFinishCallBack)358     public void setInitialFinishCallBack(
359             Consumer<Boolean> initialFinishCallBack) {
360         mInitialFinishCallBack = initialFinishCallBack;
361     }
362 }
363