1 /*
2  * Copyright (C) 2024 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.server.wm.window;
18 
19 import static android.content.res.Resources.ID_NULL;
20 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
21 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
22 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
23 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
24 
25 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
26 
27 import static org.junit.Assert.assertEquals;
28 import static org.junit.Assert.assertNotNull;
29 import static org.junit.Assert.assertTrue;
30 import static org.junit.Assert.fail;
31 
32 import android.content.Context;
33 import android.graphics.Color;
34 import android.graphics.Insets;
35 import android.graphics.Rect;
36 import android.os.Bundle;
37 import android.platform.test.annotations.Presubmit;
38 import android.platform.test.annotations.RequiresFlagsEnabled;
39 import android.server.wm.WindowManagerTestBase;
40 import android.server.wm.cts.R;
41 import android.util.DisplayMetrics;
42 import android.view.Display;
43 import android.view.View;
44 import android.view.Window;
45 import android.view.WindowInsets;
46 
47 import com.android.window.flags.Flags;
48 
49 import org.junit.Test;
50 
51 import java.util.concurrent.CountDownLatch;
52 import java.util.concurrent.TimeUnit;
53 
54 /**
55  * Ensure window policies are applied as expected.
56  *
57  * Build/Install/Run:
58  *     atest CtsWindowManagerDeviceWindow:WindowPolicyTests
59  */
60 @Presubmit
61 public class WindowPolicyTests extends WindowManagerTestBase {
62 
63     @Override
setUp()64     public void setUp() throws Exception {
65         super.setUp();
66         TestActivity.sStyleId = ID_NULL;
67         TestActivity.sLayoutInDisplayCutoutMode =
68                 TestActivity.LAYOUT_IN_DISPLAY_CUTOUT_MODE_UNSPECIFIED;
69     }
70 
71     @RequiresFlagsEnabled(Flags.FLAG_ENFORCE_EDGE_TO_EDGE)
72     @Test
testWindowInsets()73     public void testWindowInsets() {
74         final TestActivity activity = startActivitySync(TestActivity.class);
75 
76         getInstrumentation().runOnMainSync(() -> {
77             assertEquals(
78                     "WindowInsets must not be changed.",
79                     activity.mContentView.getRootWindowInsets(),
80                     activity.mContentView.mWindowInsets);
81         });
82     }
83 
assertFillWindowBounds(TestActivity activity)84     private static void assertFillWindowBounds(TestActivity activity) {
85         getInstrumentation().runOnMainSync(() -> {
86             assertEquals(
87                     "Decor view must fill window bounds.",
88                     activity.getResources().getConfiguration().windowConfiguration.getBounds(),
89                     getFrameOnScreen(activity.getWindow().getDecorView()));
90             assertEquals(
91                     "Content view must fill window bounds.",
92                     activity.getResources().getConfiguration().windowConfiguration.getBounds(),
93                     getFrameOnScreen(activity.mContentView));
94         });
95     }
96 
getFrameOnScreen(View view)97     private static Rect getFrameOnScreen(View view) {
98         final int[] location = {0, 0};
99         view.getLocationOnScreen(location);
100         return new Rect(
101                 location[0],
102                 location[1],
103                 location[0] + view.getWidth(),
104                 location[1] + view.getHeight());
105     }
106 
107     @RequiresFlagsEnabled(Flags.FLAG_ENFORCE_EDGE_TO_EDGE)
108     @Test
testWindowStyleLayoutInDisplayCutoutMode_unspecified()109     public void testWindowStyleLayoutInDisplayCutoutMode_unspecified() {
110         TestActivity.sStyleId = R.style.LayoutInDisplayCutoutModeUnspecified;
111         assertFillWindowBounds(startActivitySync(TestActivity.class));
112     }
113 
114     @RequiresFlagsEnabled(Flags.FLAG_ENFORCE_EDGE_TO_EDGE)
115     @Test
testWindowStyleLayoutInDisplayCutoutMode_never()116     public void testWindowStyleLayoutInDisplayCutoutMode_never() {
117         TestActivity.sStyleId = R.style.LayoutInDisplayCutoutModeNever;
118         assertFillWindowBounds(startActivitySync(TestActivity.class));
119     }
120 
121     @RequiresFlagsEnabled(Flags.FLAG_ENFORCE_EDGE_TO_EDGE)
122     @Test
testWindowStyleLayoutInDisplayCutoutMode_default()123     public void testWindowStyleLayoutInDisplayCutoutMode_default() {
124         TestActivity.sStyleId = R.style.LayoutInDisplayCutoutModeDefault;
125         assertFillWindowBounds(startActivitySync(TestActivity.class));
126     }
127 
128     @RequiresFlagsEnabled(Flags.FLAG_ENFORCE_EDGE_TO_EDGE)
129     @Test
testWindowStyleLayoutInDisplayCutoutMode_shortEdges()130     public void testWindowStyleLayoutInDisplayCutoutMode_shortEdges() {
131         TestActivity.sStyleId = R.style.LayoutInDisplayCutoutModeShortEdges;
132         assertFillWindowBounds(startActivitySync(TestActivity.class));
133     }
134 
135     @RequiresFlagsEnabled(Flags.FLAG_ENFORCE_EDGE_TO_EDGE)
136     @Test
testWindowStyleLayoutInDisplayCutoutMode_always()137     public void testWindowStyleLayoutInDisplayCutoutMode_always() {
138         TestActivity.sStyleId = R.style.LayoutInDisplayCutoutModeAlways;
139         assertFillWindowBounds(startActivitySync(TestActivity.class));
140     }
141 
142     @RequiresFlagsEnabled(Flags.FLAG_ENFORCE_EDGE_TO_EDGE)
143     @Test
testLayoutParamsLayoutInDisplayCutoutMode_unspecified()144     public void testLayoutParamsLayoutInDisplayCutoutMode_unspecified() {
145         assertFillWindowBounds(startActivitySync(TestActivity.class));
146     }
147 
148     @RequiresFlagsEnabled(Flags.FLAG_ENFORCE_EDGE_TO_EDGE)
149     @Test
testLayoutParamsLayoutInDisplayCutoutMode_never()150     public void testLayoutParamsLayoutInDisplayCutoutMode_never() {
151         TestActivity.sLayoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
152         assertFillWindowBounds(startActivitySync(TestActivity.class));
153     }
154 
155     @RequiresFlagsEnabled(Flags.FLAG_ENFORCE_EDGE_TO_EDGE)
156     @Test
testLayoutParamsLayoutInDisplayCutoutMode_default()157     public void testLayoutParamsLayoutInDisplayCutoutMode_default() {
158         TestActivity.sLayoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
159         assertFillWindowBounds(startActivitySync(TestActivity.class));
160     }
161 
162     @RequiresFlagsEnabled(Flags.FLAG_ENFORCE_EDGE_TO_EDGE)
163     @Test
testLayoutParamsLayoutInDisplayCutoutMode_shortEdges()164     public void testLayoutParamsLayoutInDisplayCutoutMode_shortEdges() {
165         TestActivity.sLayoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
166         assertFillWindowBounds(startActivitySync(TestActivity.class));
167     }
168 
169     @RequiresFlagsEnabled(Flags.FLAG_ENFORCE_EDGE_TO_EDGE)
170     @Test
testLayoutParamsLayoutInDisplayCutoutMode_always()171     public void testLayoutParamsLayoutInDisplayCutoutMode_always() {
172         TestActivity.sLayoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
173         assertFillWindowBounds(startActivitySync(TestActivity.class));
174     }
175 
176     @RequiresFlagsEnabled(Flags.FLAG_INSETS_DECOUPLED_CONFIGURATION)
177     @Test
testOptOutEdgeToEdgeAppBounds()178     public void testOptOutEdgeToEdgeAppBounds() {
179         TestActivity.sLayoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
180         TestActivity activity = startActivitySync(OptOutEdgeToEdgeActivity.class);
181         getInstrumentation().runOnMainSync(() -> {
182             final WindowInsets windowInsets =
183                     activity.getWindow().getDecorView().getRootWindowInsets();
184             final Insets insets = windowInsets.getInsets(
185                     WindowInsets.Type.displayCutout() | WindowInsets.Type.navigationBars());
186             final Rect expectedBounds = new Rect(
187                     activity.getResources().getConfiguration().windowConfiguration.getBounds());
188             expectedBounds.inset(insets);
189             assertEquals(
190                     "The bounds must exclude display cutout and navigation bar area.",
191                     expectedBounds,
192                     activity.getResources().getConfiguration().windowConfiguration.getAppBounds());
193         });
194     }
195 
196     @RequiresFlagsEnabled(Flags.FLAG_INSETS_DECOUPLED_CONFIGURATION)
197     @Test
testOptOutEdgeToEdgeDisplayMetrics()198     public void testOptOutEdgeToEdgeDisplayMetrics() {
199         TestActivity.sLayoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
200         TestActivity activity = startActivitySync(OptOutEdgeToEdgeActivity.class);
201         getInstrumentation().runOnMainSync(() -> {
202             final WindowInsets windowInsets =
203                     activity.getWindow().getDecorView().getRootWindowInsets();
204             final Insets insets = windowInsets.getInsets(
205                     WindowInsets.Type.displayCutout() | WindowInsets.Type.navigationBars());
206             final Rect expectedBounds = new Rect(
207                     activity.getResources().getConfiguration().windowConfiguration.getBounds());
208             expectedBounds.inset(insets);
209             final Display display = activity.getDisplay();
210             assertNotNull(display);
211             final DisplayMetrics displayMetrics = new DisplayMetrics();
212             display.getMetrics(displayMetrics);
213             assertEquals(
214                     "The width must exclude display cutout and navigation bar area.",
215                     expectedBounds.width(),
216                     displayMetrics.widthPixels);
217             assertEquals(
218                     "The height must exclude display cutout and navigation bar area.",
219                     expectedBounds.height(),
220                     displayMetrics.heightPixels);
221         });
222     }
223 
224     @RequiresFlagsEnabled(Flags.FLAG_ENFORCE_EDGE_TO_EDGE)
225     @Test
testSystemBarColor()226     public void testSystemBarColor() {
227         TestActivity.sStyleId = R.style.BlackSystemBars;
228         final TestActivity activity = startActivitySync(TestActivity.class);
229         getInstrumentation().runOnMainSync(() -> {
230             final Window window = activity.getWindow();
231             assertEquals("Status bar color must be transparent.",
232                     Color.TRANSPARENT, Color.alpha(window.getStatusBarColor()));
233             assertEquals("Navigation bar color must be transparent.",
234                     Color.TRANSPARENT, window.getNavigationBarColor());
235             assertEquals("Navigation bar divider color must be transparent.",
236                     Color.TRANSPARENT, window.getNavigationBarDividerColor());
237 
238             window.setStatusBarColor(Color.BLACK);
239             assertEquals("Status bar color must not be changed.",
240                     Color.TRANSPARENT, window.getStatusBarColor());
241             window.setNavigationBarColor(Color.BLACK);
242             assertEquals("Navigation bar color must not be changed.",
243                     Color.TRANSPARENT, window.getNavigationBarColor());
244             window.setNavigationBarDividerColor(Color.BLACK);
245             assertEquals("Navigation bar divider color not be changed.",
246                     Color.TRANSPARENT, window.getNavigationBarDividerColor());
247         });
248     }
249 
startActivitySync(Class<T> activityClass)250     private static <T extends TestActivity> TestActivity startActivitySync(Class<T> activityClass) {
251         final TestActivity activity = startActivity(activityClass);
252         activity.waitForLayout();
253         return activity;
254     }
255 
256     public static class TestActivity extends FocusableActivity {
257 
258         private static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_UNSPECIFIED = -1;
259 
260         private static int sStyleId = ID_NULL;
261         private static int sLayoutInDisplayCutoutMode = -1;
262         private final CountDownLatch mLayoutLatch = new CountDownLatch(1);
263         private ContentView mContentView;
264 
265         @Override
onCreate(Bundle savedInstanceState)266         protected void onCreate(Bundle savedInstanceState) {
267             super.onCreate(savedInstanceState);
268             if (sStyleId != ID_NULL) {
269                 getTheme().applyStyle(sStyleId, true /* force */);
270             }
271             if (sLayoutInDisplayCutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_UNSPECIFIED) {
272                 getWindow().getAttributes().layoutInDisplayCutoutMode = sLayoutInDisplayCutoutMode;
273             }
274             mContentView = new ContentView(this);
275             mContentView.getViewTreeObserver().addOnGlobalLayoutListener(mLayoutLatch::countDown);
276             setContentView(mContentView);
277         }
278 
waitForLayout()279         private void waitForLayout() {
280             final String errorMessage = "Unable to wait for layout.";
281             try {
282                 assertTrue(errorMessage, mLayoutLatch.await(3, TimeUnit.SECONDS));
283             } catch (InterruptedException e) {
284                 fail(errorMessage);
285             }
286         }
287 
288         private static class ContentView extends View {
289 
290             private WindowInsets mWindowInsets;
291 
ContentView(Context context)292             private ContentView(Context context) {
293                 super(context);
294             }
295 
296             @Override
onApplyWindowInsets(WindowInsets insets)297             public WindowInsets onApplyWindowInsets(WindowInsets insets) {
298                 mWindowInsets = insets;
299                 return super.onApplyWindowInsets(insets);
300             }
301         }
302     }
303 
304     public static class OptOutEdgeToEdgeActivity extends TestActivity {
305     }
306 }
307