1 /*
2  * Copyright (C) 2021 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.insets;
18 
19 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
20 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
21 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
22 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
23 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
24 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
25 import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT;
26 import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT;
27 import static android.view.RoundedCorner.POSITION_TOP_LEFT;
28 import static android.view.RoundedCorner.POSITION_TOP_RIGHT;
29 import static android.view.Surface.ROTATION_0;
30 import static android.view.Surface.ROTATION_180;
31 import static android.view.Surface.ROTATION_270;
32 import static android.view.Surface.ROTATION_90;
33 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
34 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
35 
36 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
37 
38 import static org.junit.Assert.assertEquals;
39 import static org.junit.Assert.assertNull;
40 
41 import android.app.Activity;
42 import android.content.Intent;
43 import android.graphics.Rect;
44 import android.os.Bundle;
45 import android.platform.test.annotations.Presubmit;
46 import android.server.wm.ActivityManagerTestBase;
47 import android.server.wm.RotationSession;
48 import android.server.wm.WindowManagerStateHelper;
49 import android.util.Log;
50 import android.view.Display;
51 import android.view.Gravity;
52 import android.view.RoundedCorner;
53 import android.view.View;
54 import android.view.Window;
55 import android.view.WindowInsets;
56 import android.view.WindowManager;
57 import android.view.WindowMetrics;
58 
59 import androidx.annotation.NonNull;
60 import androidx.test.rule.ActivityTestRule;
61 
62 import com.android.compatibility.common.util.PollingCheck;
63 
64 import org.junit.After;
65 import org.junit.Rule;
66 import org.junit.Test;
67 import org.junit.runner.RunWith;
68 import org.junit.runners.Parameterized;
69 
70 @Presubmit
71 @RunWith(Parameterized.class)
72 public class RoundedCornerTests extends ActivityManagerTestBase {
73     private static final String TAG = "RoundedCornerTests";
74     private static final int POSITION_LENGTH = 4;
75     private static final long TIMEOUT_IN_MILLISECONDS = 1000;
76 
77     @Parameterized.Parameters(name= "{1}({0})")
data()78     public static Object[][] data() {
79         return new Object[][]{
80                 {SCREEN_ORIENTATION_PORTRAIT, "SCREEN_ORIENTATION_PORTRAIT"},
81                 {SCREEN_ORIENTATION_LANDSCAPE, "SCREEN_ORIENTATION_LANDSCAPE"},
82                 {SCREEN_ORIENTATION_REVERSE_LANDSCAPE, "SCREEN_ORIENTATION_REVERSE_LANDSCAPE"},
83                 {SCREEN_ORIENTATION_REVERSE_PORTRAIT, "SCREEN_ORIENTATION_REVERSE_PORTRAIT"},
84         };
85     }
86 
87     @Parameterized.Parameter(0)
88     public int orientation;
89 
90     @Parameterized.Parameter(1)
91     public String orientationName;
92 
93     private final WindowManagerStateHelper mWindowManagerStateHelper =
94             new WindowManagerStateHelper();
95 
96     @After
tearDown()97     public void tearDown() {
98         mTestActivityRule.finishActivity();
99         mWindowManagerStateHelper.waitForDisplayUnfrozen();
100     }
101 
102     @Rule
103     public final ActivityTestRule<TestActivity> mTestActivityRule =
104             new ActivityTestRule<>(TestActivity.class, false /* initialTouchMode */,
105                     false /* launchActivity */);
106 
107     @Test
testRoundedCorner_fullscreen()108     public void testRoundedCorner_fullscreen() {
109         verifyRoundedCorners(false /* excludeRoundedCorners */);
110     }
111 
112     @Test
testRoundedCorner_excludeRoundedCorners()113     public void testRoundedCorner_excludeRoundedCorners() {
114         verifyRoundedCorners(true /* excludeRoundedCorners */);
115     }
116 
verifyRoundedCorners(boolean excludedRoundedCorners)117     private void verifyRoundedCorners(boolean excludedRoundedCorners) {
118         final TestActivity activity = mTestActivityRule.launchActivity(new Intent());
119 
120         if (excludedRoundedCorners && !activity.hasRoundedCorners()) {
121             Log.d(TAG, "There is no rounded corner on the display. Skipped!!");
122             return;
123         }
124 
125         waitAndAssertResumedActivity(activity.getComponentName(), "Activity must be resumed.");
126 
127         int rotation = getRotation(activity, orientation);
128 
129         if (rotation != ROTATION_0) {
130             // If the device doesn't support rotation, just verify the rounded corner with
131             // the current orientation.
132             if (!supportsRotation()) {
133                 return;
134             }
135             RotationSession rotationSession = createManagedRotationSession();
136             rotationSession.set(rotation);
137 
138             mInstrumentation.getUiAutomation().syncInputTransactions();
139         }
140 
141         runOnMainSync(() -> activity.addChildWindow(
142                 activity.calculateWindowBounds(excludedRoundedCorners)));
143         try {
144             // Make sure the child window has been laid out.
145             PollingCheck.waitFor(TIMEOUT_IN_MILLISECONDS,
146                     () -> activity.getDispatchedInsets() != null);
147             final WindowInsets insets = activity.getDispatchedInsets();
148 
149             if (excludedRoundedCorners) {
150                 for (int i = 0; i < POSITION_LENGTH; i++) {
151                     assertNull("The rounded corners should be null.",
152                             insets.getRoundedCorner(i));
153                 }
154             } else {
155                 final Display display = activity.getDisplay();
156                 for (int j = 0; j < POSITION_LENGTH; j++) {
157                     assertEquals(insets.getRoundedCorner(j), display.getRoundedCorner(j));
158                 }
159             }
160         } finally {
161             runOnMainSync(activity::removeChildWindow);
162         }
163     }
164 
165     /**
166      * Returns the rotation based on {@code orientations}.
167      */
getRotation(@onNull Activity activity, int requestedOrientation)168     private static int getRotation(@NonNull Activity activity, int requestedOrientation) {
169         // Not use Activity#getRequestedOrientation because the possible values are dozens and hard
170         // to determine the rotation.
171         int currentOrientation = activity.getResources().getConfiguration().orientation;
172         if (currentOrientation == ORIENTATION_PORTRAIT) {
173             switch (requestedOrientation) {
174                 case SCREEN_ORIENTATION_PORTRAIT: {
175                     return ROTATION_0;
176                 }
177                 case SCREEN_ORIENTATION_LANDSCAPE: {
178                     return ROTATION_90;
179                 }
180                 case SCREEN_ORIENTATION_REVERSE_PORTRAIT: {
181                     return ROTATION_180;
182                 }
183                 case SCREEN_ORIENTATION_REVERSE_LANDSCAPE: {
184                     return ROTATION_270;
185                 }
186             }
187         } else {
188             switch (requestedOrientation) {
189                 case SCREEN_ORIENTATION_PORTRAIT: {
190                     return ROTATION_90;
191                 }
192                 case SCREEN_ORIENTATION_LANDSCAPE: {
193                     return ROTATION_0;
194                 }
195                 case SCREEN_ORIENTATION_REVERSE_PORTRAIT: {
196                     return ROTATION_270;
197                 }
198                 case SCREEN_ORIENTATION_REVERSE_LANDSCAPE: {
199                     return ROTATION_180;
200                 }
201             }
202         }
203         throw new IllegalArgumentException("Unknown orientation value:" + requestedOrientation);
204     }
205 
runOnMainSync(Runnable runnable)206     private void runOnMainSync(Runnable runnable) {
207         getInstrumentation().runOnMainSync(runnable);
208     }
209 
210     public static class TestActivity extends Activity {
211         static final String EXTRA_ORIENTATION = "extra.orientation";
212 
213         private View mChildWindowRoot;
214         private WindowInsets mDispatchedInsets;
215 
216         @Override
onCreate(Bundle savedInstanceState)217         protected void onCreate(Bundle savedInstanceState) {
218             super.onCreate(savedInstanceState);
219             getWindow().requestFeature(Window.FEATURE_NO_TITLE);
220             getWindow().getAttributes().layoutInDisplayCutoutMode =
221                     LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
222             getWindow().getDecorView().getWindowInsetsController().hide(
223                     WindowInsets.Type.systemBars());
224             if (getIntent() != null) {
225                 setRequestedOrientation(getIntent().getIntExtra(
226                         EXTRA_ORIENTATION, SCREEN_ORIENTATION_UNSPECIFIED));
227             }
228         }
229 
addChildWindow(Rect bounds)230         void addChildWindow(Rect bounds) {
231             final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams();
232             attrs.x = bounds.left;
233             attrs.y = bounds.top;
234             attrs.width = bounds.width();
235             attrs.height = bounds.height();
236             attrs.gravity = Gravity.LEFT | Gravity.TOP;
237             attrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
238             attrs.flags = FLAG_NOT_FOCUSABLE;
239             attrs.setFitInsetsTypes(0);
240             mChildWindowRoot = new View(this);
241             mChildWindowRoot.setOnApplyWindowInsetsListener(
242                     (v, insets) -> mDispatchedInsets = insets);
243             getWindowManager().addView(mChildWindowRoot, attrs);
244         }
245 
removeChildWindow()246         void removeChildWindow() {
247             if (mChildWindowRoot != null) {
248                 getWindowManager().removeViewImmediate(mChildWindowRoot);
249             }
250         }
251 
getDispatchedInsets()252         WindowInsets getDispatchedInsets() {
253             return mDispatchedInsets;
254         }
255 
hasRoundedCorners()256         boolean hasRoundedCorners() {
257             final Display display = getDisplay();
258             return display.getRoundedCorner(POSITION_TOP_LEFT) != null
259                     || display.getRoundedCorner(POSITION_TOP_RIGHT) != null
260                     || display.getRoundedCorner(POSITION_BOTTOM_RIGHT) != null
261                     || display.getRoundedCorner(POSITION_BOTTOM_LEFT) != null;
262         }
263 
calculateWindowBounds(boolean excludeRoundedCorners)264         Rect calculateWindowBounds(boolean excludeRoundedCorners) {
265             final Display display = getDisplay();
266             final WindowMetrics windowMetrics = getWindowManager().getCurrentWindowMetrics();
267             if (!excludeRoundedCorners) {
268                 return windowMetrics.getBounds();
269             }
270             final Rect bounds = new Rect();
271             final int width = windowMetrics.getBounds().width();
272             final int height = windowMetrics.getBounds().height();
273             final RoundedCorner topLeft = display.getRoundedCorner(POSITION_TOP_LEFT);
274             final RoundedCorner topRight = display.getRoundedCorner(POSITION_TOP_RIGHT);
275             final RoundedCorner bottomRight = display.getRoundedCorner(POSITION_BOTTOM_RIGHT);
276             final RoundedCorner bottomLeft = display.getRoundedCorner(POSITION_BOTTOM_LEFT);
277 
278             bounds.left = Math.max(topLeft != null ? topLeft.getCenter().x : 0,
279                     bottomLeft != null ? bottomLeft.getCenter().x : 0);
280             bounds.top = Math.max(topLeft != null ? topLeft.getCenter().y : 0,
281                     bottomLeft != null ? bottomLeft.getCenter().y : 0);
282             bounds.right = Math.min(topRight != null ? topRight.getCenter().x : width,
283                     bottomRight != null ? bottomRight.getCenter().x : width);
284             bounds.bottom = Math.min(bottomRight != null ? bottomRight.getCenter().y : height,
285                     bottomLeft != null ? bottomLeft.getCenter().y : height);
286 
287             Log.d(TAG, "Window bounds with rounded corners excluded = " + bounds);
288             return bounds;
289         }
290     }
291 }
292