1/*
2 * Copyright (C) 2017 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
17package com.android.server.wm;
18
19import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
20import static android.graphics.Color.RED;
21import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
22import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
23import static android.view.Display.DEFAULT_DISPLAY;
24import static android.view.Gravity.BOTTOM;
25import static android.view.Gravity.LEFT;
26import static android.view.Gravity.RIGHT;
27import static android.view.Gravity.TOP;
28import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
29import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
30import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
31import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
32import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
33import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IS_SCREEN_DECOR;
34import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
35import static org.junit.Assert.assertEquals;
36
37import android.app.Activity;
38import android.app.ActivityOptions;
39import android.app.Instrumentation;
40import android.content.Context;
41import android.content.Intent;
42import android.graphics.PixelFormat;
43import android.graphics.Point;
44import android.hardware.display.DisplayManager;
45import android.hardware.display.VirtualDisplay;
46import android.media.ImageReader;
47import android.os.Handler;
48import android.platform.test.annotations.Presubmit;
49import android.support.test.InstrumentationRegistry;
50import android.support.test.filters.SmallTest;
51import android.support.test.runner.AndroidJUnit4;
52import android.util.Pair;
53import android.view.Display;
54import android.view.DisplayInfo;
55import android.view.View;
56import android.view.WindowInsets;
57import android.view.WindowManager;
58import android.widget.TextView;
59
60import org.junit.After;
61import org.junit.Assert;
62import org.junit.Before;
63import org.junit.Test;
64import org.junit.runner.RunWith;
65
66import java.util.ArrayList;
67import java.util.function.BooleanSupplier;
68
69/**
70 * Tests for the {@link android.view.WindowManager.LayoutParams#PRIVATE_FLAG_IS_SCREEN_DECOR} flag.
71 *
72 * Build/Install/Run:
73 *  atest FrameworksServicesTests:com.android.server.wm.ScreenDecorWindowTests
74 */
75// TODO: Add test for FLAG_FULLSCREEN which hides the status bar and also other flags.
76// TODO: Test non-Activity windows.
77@SmallTest
78@Presubmit
79@RunWith(AndroidJUnit4.class)
80public class ScreenDecorWindowTests {
81
82    private final Context mContext = InstrumentationRegistry.getTargetContext();
83    private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
84
85    private WindowManager mWm;
86    private ArrayList<View> mWindows = new ArrayList<>();
87
88    private Activity mTestActivity;
89    private VirtualDisplay mDisplay;
90    private ImageReader mImageReader;
91
92    private int mDecorThickness;
93    private int mHalfDecorThickness;
94
95    @Before
96    public void setUp() {
97        final Pair<VirtualDisplay, ImageReader> result = createDisplay();
98        mDisplay = result.first;
99        mImageReader = result.second;
100        final Display display = mDisplay.getDisplay();
101        final Context dContext = mContext.createDisplayContext(display);
102        mWm = dContext.getSystemService(WindowManager.class);
103        mTestActivity = startActivityOnDisplay(TestActivity.class, display.getDisplayId());
104        final Point size = new Point();
105        mDisplay.getDisplay().getRealSize(size);
106        mDecorThickness = Math.min(size.x, size.y) / 3;
107        mHalfDecorThickness = mDecorThickness / 2;
108    }
109
110    @After
111    public void tearDown() {
112        while (!mWindows.isEmpty()) {
113            removeWindow(mWindows.get(0));
114        }
115        finishActivity(mTestActivity);
116        mDisplay.release();
117        mImageReader.close();
118    }
119
120    @Test
121    public void testScreenSides() throws Exception {
122        // Decor on top
123        final View decorWindow = createDecorWindow(TOP, MATCH_PARENT, mDecorThickness);
124        assertInsetGreaterOrEqual(mTestActivity, TOP, mDecorThickness);
125
126        // Decor at the bottom
127        updateWindow(decorWindow, BOTTOM, MATCH_PARENT, mDecorThickness, 0, 0);
128        assertInsetGreaterOrEqual(mTestActivity, BOTTOM, mDecorThickness);
129
130        // Decor to the left
131        updateWindow(decorWindow, LEFT, mDecorThickness, MATCH_PARENT, 0, 0);
132        assertInsetGreaterOrEqual(mTestActivity, LEFT, mDecorThickness);
133
134        // Decor to the right
135        updateWindow(decorWindow, RIGHT, mDecorThickness, MATCH_PARENT, 0, 0);
136        assertInsetGreaterOrEqual(mTestActivity, RIGHT, mDecorThickness);
137    }
138
139    @Test
140    public void testMultipleDecors() throws Exception {
141        // Test 2 decor windows on-top.
142        createDecorWindow(TOP, MATCH_PARENT, mHalfDecorThickness);
143        assertInsetGreaterOrEqual(mTestActivity, TOP, mHalfDecorThickness);
144        createDecorWindow(TOP, MATCH_PARENT, mDecorThickness);
145        assertInsetGreaterOrEqual(mTestActivity, TOP, mDecorThickness);
146
147        // And one at the bottom.
148        createDecorWindow(BOTTOM, MATCH_PARENT, mHalfDecorThickness);
149        assertInsetGreaterOrEqual(mTestActivity, TOP, mDecorThickness);
150        assertInsetGreaterOrEqual(mTestActivity, BOTTOM, mHalfDecorThickness);
151    }
152
153    @Test
154    public void testFlagChange() throws Exception {
155        WindowInsets initialInsets = getInsets(mTestActivity);
156
157        final View decorWindow = createDecorWindow(TOP, MATCH_PARENT, mDecorThickness);
158        assertTopInsetEquals(mTestActivity, mDecorThickness);
159
160        updateWindow(decorWindow, TOP, MATCH_PARENT, mDecorThickness,
161                0, PRIVATE_FLAG_IS_SCREEN_DECOR);
162
163        // TODO: fix test and re-enable assertion.
164        // initialInsets was not actually immutable and just updated to the current insets,
165        // meaning this assertion never actually tested anything. Now that WindowInsets actually is
166        // immutable, it turns out the test was broken.
167        // assertTopInsetEquals(mTestActivity, initialInsets.getSystemWindowInsetTop());
168
169        updateWindow(decorWindow, TOP, MATCH_PARENT, mDecorThickness,
170                PRIVATE_FLAG_IS_SCREEN_DECOR, PRIVATE_FLAG_IS_SCREEN_DECOR);
171        assertTopInsetEquals(mTestActivity, mDecorThickness);
172    }
173
174    @Test
175    public void testRemoval() throws Exception {
176        WindowInsets initialInsets = getInsets(mTestActivity);
177
178        final View decorWindow = createDecorWindow(TOP, MATCH_PARENT, mDecorThickness);
179        assertInsetGreaterOrEqual(mTestActivity, TOP, mDecorThickness);
180
181        removeWindow(decorWindow);
182        assertTopInsetEquals(mTestActivity, initialInsets.getSystemWindowInsetTop());
183    }
184
185    private View createDecorWindow(int gravity, int width, int height) {
186        return createWindow("decorWindow", gravity, width, height, RED,
187                FLAG_LAYOUT_IN_SCREEN, PRIVATE_FLAG_IS_SCREEN_DECOR);
188    }
189
190    private View createWindow(String name, int gravity, int width, int height, int color, int flags,
191            int privateFlags) {
192
193        final View[] viewHolder = new View[1];
194        final int finalFlag = flags
195                | FLAG_NOT_FOCUSABLE | FLAG_WATCH_OUTSIDE_TOUCH | FLAG_NOT_TOUCHABLE;
196
197        // Needs to run on the UI thread.
198        Handler.getMain().runWithScissors(() -> {
199            final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
200                    width, height, TYPE_APPLICATION_OVERLAY, finalFlag, PixelFormat.OPAQUE);
201            lp.gravity = gravity;
202            lp.privateFlags |= privateFlags;
203
204            final TextView view = new TextView(mContext);
205            view.setText("ScreenDecorWindowTests - " + name);
206            view.setBackgroundColor(color);
207            mWm.addView(view, lp);
208            mWindows.add(view);
209            viewHolder[0] = view;
210        }, 0);
211
212        waitForIdle();
213        return viewHolder[0];
214    }
215
216    private void updateWindow(View v, int gravity, int width, int height,
217            int privateFlags, int privateFlagsMask) {
218        // Needs to run on the UI thread.
219        Handler.getMain().runWithScissors(() -> {
220            final WindowManager.LayoutParams lp = (WindowManager.LayoutParams) v.getLayoutParams();
221            lp.gravity = gravity;
222            lp.width = width;
223            lp.height = height;
224            setPrivateFlags(lp, privateFlags, privateFlagsMask);
225
226            mWm.updateViewLayout(v, lp);
227        }, 0);
228
229        waitForIdle();
230    }
231
232    private void removeWindow(View v) {
233        Handler.getMain().runWithScissors(() -> mWm.removeView(v), 0);
234        mWindows.remove(v);
235        waitForIdle();
236    }
237
238    private WindowInsets getInsets(Activity a) {
239        return new WindowInsets(a.getWindow().getDecorView().getRootWindowInsets());
240    }
241
242    /**
243     * Set the flags of the window, as per the
244     * {@link WindowManager.LayoutParams WindowManager.LayoutParams}
245     * flags.
246     *
247     * @param flags The new window flags (see WindowManager.LayoutParams).
248     * @param mask Which of the window flag bits to modify.
249     */
250    public void setPrivateFlags(WindowManager.LayoutParams lp, int flags, int mask) {
251        lp.flags = (lp.flags & ~mask) | (flags & mask);
252    }
253
254    /**
255     * Asserts the top inset of {@param activity} is equal to {@param expected} waiting as needed.
256     */
257    private void assertTopInsetEquals(Activity activity, int expected) throws Exception {
258        waitFor(() -> getInsets(activity).getSystemWindowInsetTop() == expected);
259        assertEquals(expected, getInsets(activity).getSystemWindowInsetTop());
260    }
261
262    /**
263     * Asserts the inset at {@param side} of {@param activity} is equal to {@param expected}
264     * waiting as needed.
265     */
266    private void assertInsetGreaterOrEqual(Activity activity, int side, int expected)
267            throws Exception {
268        waitFor(() -> {
269            final WindowInsets insets = getInsets(activity);
270            switch (side) {
271                case TOP: return insets.getSystemWindowInsetTop() >= expected;
272                case BOTTOM: return insets.getSystemWindowInsetBottom() >= expected;
273                case LEFT: return insets.getSystemWindowInsetLeft() >= expected;
274                case RIGHT: return insets.getSystemWindowInsetRight() >= expected;
275                default: return true;
276            }
277        });
278
279        final WindowInsets insets = getInsets(activity);
280        switch (side) {
281            case TOP: assertGreaterOrEqual(insets.getSystemWindowInsetTop(), expected); break;
282            case BOTTOM: assertGreaterOrEqual(insets.getSystemWindowInsetBottom(), expected); break;
283            case LEFT: assertGreaterOrEqual(insets.getSystemWindowInsetLeft(), expected); break;
284            case RIGHT: assertGreaterOrEqual(insets.getSystemWindowInsetRight(), expected); break;
285        }
286    }
287
288    /** Asserts that the first entry is greater than or equal to the second entry. */
289    private void assertGreaterOrEqual(int first, int second) throws Exception {
290        Assert.assertTrue("Excepted " + first + " >= " + second, first >= second);
291    }
292
293    private void waitFor(BooleanSupplier waitCondition) {
294        int retriesLeft = 5;
295        do {
296            if (waitCondition.getAsBoolean()) {
297                break;
298            }
299            try {
300                Thread.sleep(500);
301            } catch (InterruptedException e) {
302                // Well I guess we are not waiting...
303            }
304        } while (retriesLeft-- > 0);
305    }
306
307    private void finishActivity(Activity a) {
308        if (a == null) {
309            return;
310        }
311        a.finish();
312        waitForIdle();
313    }
314
315    private void waitForIdle() {
316        mInstrumentation.waitForIdleSync();
317    }
318
319    private Activity startActivityOnDisplay(Class<?> cls, int displayId) {
320        final Intent intent = new Intent(mContext, cls);
321        intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
322        final ActivityOptions options = ActivityOptions.makeBasic();
323        options.setLaunchDisplayId(displayId);
324        final Activity activity = mInstrumentation.startActivitySync(intent, options.toBundle());
325        waitForIdle();
326
327        assertEquals(displayId, activity.getDisplay().getDisplayId());
328        return activity;
329    }
330
331    private Pair<VirtualDisplay, ImageReader> createDisplay() {
332        final DisplayManager dm = mContext.getSystemService(DisplayManager.class);
333        final DisplayInfo displayInfo = new DisplayInfo();
334        final Display defaultDisplay = dm.getDisplay(DEFAULT_DISPLAY);
335        defaultDisplay.getDisplayInfo(displayInfo);
336        final String name = "ScreenDecorWindowTests";
337        int flags = VIRTUAL_DISPLAY_FLAG_PRESENTATION | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
338
339        final ImageReader imageReader = ImageReader.newInstance(
340                displayInfo.logicalWidth, displayInfo.logicalHeight, PixelFormat.RGBA_8888, 2);
341
342        final VirtualDisplay display = dm.createVirtualDisplay(name, displayInfo.logicalWidth,
343                displayInfo.logicalHeight, displayInfo.logicalDensityDpi, imageReader.getSurface(),
344                flags);
345
346        return Pair.create(display, imageReader);
347    }
348
349    public static class TestActivity extends Activity {
350    }
351}
352