Revert "DO NOT MERGE: Move AM/WM unit tests out of FrameworksServicesTests to WmTests"
This reverts commit dee5a4dc2a5a6829e5509fc0e94903e30dcc238f.
Reason for revert: CLs on upstream branch are on hold.
Change-Id: I253294d080efdfd1dbf377812f12880e03dff1f6
diff --git a/services/tests/servicestests/src/com/android/server/wm/AnimatingAppWindowTokenRegistryTest.java b/services/tests/servicestests/src/com/android/server/wm/AnimatingAppWindowTokenRegistryTest.java
new file mode 100644
index 0000000..164c80b
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/AnimatingAppWindowTokenRegistryTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for the {@link TaskStack} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.wm.AnimatingAppWindowTokenRegistryTest
+ */
+@SmallTest
+@Presubmit
+@FlakyTest(detail = "Promote once confirmed non-flaky")
+@RunWith(AndroidJUnit4.class)
+public class AnimatingAppWindowTokenRegistryTest extends WindowTestsBase {
+
+ @Mock
+ AnimationAdapter mAdapter;
+
+ @Mock
+ Runnable mMockEndDeferFinishCallback1;
+ @Mock
+ Runnable mMockEndDeferFinishCallback2;
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void testDeferring() throws Exception {
+ final AppWindowToken window1 = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ final AppWindowToken window2 = createAppWindow(window1.getTask(), ACTIVITY_TYPE_STANDARD,
+ "window2").mAppToken;
+ final AnimatingAppWindowTokenRegistry registry =
+ window1.getStack().getAnimatingAppWindowTokenRegistry();
+
+ window1.startAnimation(window1.getPendingTransaction(), mAdapter, false /* hidden */);
+ window2.startAnimation(window1.getPendingTransaction(), mAdapter, false /* hidden */);
+ assertTrue(window1.isSelfAnimating());
+ assertTrue(window2.isSelfAnimating());
+
+ // Make sure that first animation finish is deferred, second one is not deferred, and first
+ // one gets cancelled.
+ assertTrue(registry.notifyAboutToFinish(window1, mMockEndDeferFinishCallback1));
+ assertFalse(registry.notifyAboutToFinish(window2, mMockEndDeferFinishCallback2));
+ verify(mMockEndDeferFinishCallback1).run();
+ verifyZeroInteractions(mMockEndDeferFinishCallback2);
+ }
+
+ @Test
+ public void testContainerRemoved() throws Exception {
+ final AppWindowToken window1 = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ final AppWindowToken window2 = createAppWindow(window1.getTask(), ACTIVITY_TYPE_STANDARD,
+ "window2").mAppToken;
+ final AnimatingAppWindowTokenRegistry registry =
+ window1.getStack().getAnimatingAppWindowTokenRegistry();
+
+ window1.startAnimation(window1.getPendingTransaction(), mAdapter, false /* hidden */);
+ window2.startAnimation(window1.getPendingTransaction(), mAdapter, false /* hidden */);
+ assertTrue(window1.isSelfAnimating());
+ assertTrue(window2.isSelfAnimating());
+
+ // Make sure that first animation finish is deferred, and removing the second window stops
+ // finishes all pending deferred finishings.
+ registry.notifyAboutToFinish(window1, mMockEndDeferFinishCallback1);
+ window2.setParent(null);
+ verify(mMockEndDeferFinishCallback1).run();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/servicestests/src/com/android/server/wm/AppTransitionTests.java
new file mode 100644
index 0000000..be7d781
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/AppTransitionTests.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.view.WindowManager.TRANSIT_ACTIVITY_OPEN;
+import static android.view.WindowManager.TRANSIT_CRASHING_ACTIVITY_CLOSE;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test class for {@link AppTransition}.
+ *
+ * atest AppTransitionTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AppTransitionTests {
+
+ @Rule
+ public final WindowManagerServiceRule mRule = new WindowManagerServiceRule();
+ private WindowManagerService mWm;
+
+ @Before
+ public void setUp() throws Exception {
+ final Context context = InstrumentationRegistry.getTargetContext();
+ mWm = mRule.getWindowManagerService();
+ }
+
+ @Test
+ public void testKeyguardOverride() throws Exception {
+ mWm.prepareAppTransition(TRANSIT_ACTIVITY_OPEN, false /* alwaysKeepCurrent */);
+ mWm.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY, false /* alwaysKeepCurrent */);
+ assertEquals(TRANSIT_KEYGUARD_GOING_AWAY, mWm.mAppTransition.getAppTransition());
+ }
+
+ @Test
+ public void testKeyguardKeep() throws Exception {
+ mWm.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY, false /* alwaysKeepCurrent */);
+ mWm.prepareAppTransition(TRANSIT_ACTIVITY_OPEN, false /* alwaysKeepCurrent */);
+ assertEquals(TRANSIT_KEYGUARD_GOING_AWAY, mWm.mAppTransition.getAppTransition());
+ }
+
+ @Test
+ public void testForceOverride() throws Exception {
+ mWm.prepareAppTransition(TRANSIT_KEYGUARD_UNOCCLUDE, false /* alwaysKeepCurrent */);
+ mWm.prepareAppTransition(TRANSIT_ACTIVITY_OPEN, false /* alwaysKeepCurrent */,
+ 0 /* flags */, true /* forceOverride */);
+ assertEquals(TRANSIT_ACTIVITY_OPEN, mWm.mAppTransition.getAppTransition());
+ }
+
+ @Test
+ public void testCrashing() throws Exception {
+ mWm.prepareAppTransition(TRANSIT_ACTIVITY_OPEN, false /* alwaysKeepCurrent */);
+ mWm.prepareAppTransition(TRANSIT_CRASHING_ACTIVITY_CLOSE, false /* alwaysKeepCurrent */);
+ assertEquals(TRANSIT_CRASHING_ACTIVITY_CLOSE, mWm.mAppTransition.getAppTransition());
+ }
+
+ @Test
+ public void testKeepKeyguard_withCrashing() throws Exception {
+ mWm.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY, false /* alwaysKeepCurrent */);
+ mWm.prepareAppTransition(TRANSIT_CRASHING_ACTIVITY_CLOSE, false /* alwaysKeepCurrent */);
+ assertEquals(TRANSIT_KEYGUARD_GOING_AWAY, mWm.mAppTransition.getAppTransition());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/AppWindowContainerControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/AppWindowContainerControllerTests.java
new file mode 100644
index 0000000..e0645b1
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/AppWindowContainerControllerTests.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import android.support.test.filters.FlakyTest;
+import org.junit.Test;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.res.Configuration.EMPTY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import com.android.server.wm.WindowTestUtils.TestTaskWindowContainerController;
+
+/**
+ * Test class for {@link AppWindowContainerController}.
+ *
+ * atest FrameworksServicesTests:com.android.server.wm.AppWindowContainerControllerTests
+ */
+@SmallTest
+@Presubmit
+@FlakyTest(bugId = 74078662)
+@org.junit.runner.RunWith(AndroidJUnit4.class)
+public class AppWindowContainerControllerTests extends WindowTestsBase {
+
+ @Test
+ public void testRemoveContainer() throws Exception {
+ final WindowTestUtils.TestAppWindowContainerController controller =
+ createAppWindowController();
+
+ // Assert token was added to display.
+ assertNotNull(mDisplayContent.getWindowToken(controller.mToken.asBinder()));
+ // Assert that the container was created and linked.
+ assertNotNull(controller.mContainer);
+
+ controller.removeContainer(mDisplayContent.getDisplayId());
+
+ // Assert token was remove from display.
+ assertNull(mDisplayContent.getWindowToken(controller.mToken.asBinder()));
+ // Assert that the container was removed.
+ assertNull(controller.mContainer);
+ }
+
+ @Test
+ public void testSetOrientation() throws Exception {
+ final WindowTestUtils.TestAppWindowContainerController controller =
+ createAppWindowController();
+
+ // Assert orientation is unspecified to start.
+ assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, controller.getOrientation());
+
+ controller.setOrientation(SCREEN_ORIENTATION_LANDSCAPE, mDisplayContent.getDisplayId(),
+ EMPTY /* displayConfig */, false /* freezeScreenIfNeeded */);
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, controller.getOrientation());
+
+ controller.removeContainer(mDisplayContent.getDisplayId());
+ // Assert orientation is unspecified to after container is removed.
+ assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, controller.getOrientation());
+
+ // Reset display frozen state
+ sWm.mDisplayFrozen = false;
+ }
+
+ private void assertHasStartingWindow(AppWindowToken atoken) {
+ assertNotNull(atoken.startingSurface);
+ assertNotNull(atoken.startingData);
+ assertNotNull(atoken.startingWindow);
+ }
+
+ private void assertNoStartingWindow(AppWindowToken atoken) {
+ assertNull(atoken.startingSurface);
+ assertNull(atoken.startingWindow);
+ assertNull(atoken.startingData);
+ atoken.forAllWindows(windowState -> {
+ assertFalse(windowState.getBaseType() == TYPE_APPLICATION_STARTING);
+ }, true);
+ }
+
+ @Test
+ public void testCreateRemoveStartingWindow() throws Exception {
+ final WindowTestUtils.TestAppWindowContainerController controller =
+ createAppWindowController();
+ controller.addStartingWindow(InstrumentationRegistry.getContext().getPackageName(),
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
+ false, false);
+ waitUntilHandlersIdle();
+ final AppWindowToken atoken = controller.getAppWindowToken(mDisplayContent);
+ assertHasStartingWindow(atoken);
+ controller.removeStartingWindow();
+ waitUntilHandlersIdle();
+ assertNoStartingWindow(atoken);
+ }
+
+ @Test
+ public void testAddRemoveRace() throws Exception {
+
+ // There was once a race condition between adding and removing starting windows
+ for (int i = 0; i < 1000; i++) {
+ final WindowTestUtils.TestAppWindowContainerController controller =
+ createAppWindowController();
+ controller.addStartingWindow(InstrumentationRegistry.getContext().getPackageName(),
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
+ false, false);
+ controller.removeStartingWindow();
+ waitUntilHandlersIdle();
+ assertNoStartingWindow(controller.getAppWindowToken(mDisplayContent));
+
+ controller.getAppWindowToken(mDisplayContent).getParent().getParent().removeImmediately();
+ }
+ }
+
+ @Test
+ public void testTransferStartingWindow() throws Exception {
+ final WindowTestUtils.TestAppWindowContainerController controller1 =
+ createAppWindowController();
+ final WindowTestUtils.TestAppWindowContainerController controller2 =
+ createAppWindowController();
+ controller1.addStartingWindow(InstrumentationRegistry.getContext().getPackageName(),
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
+ false, false);
+ waitUntilHandlersIdle();
+ controller2.addStartingWindow(InstrumentationRegistry.getContext().getPackageName(),
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0, controller1.mToken.asBinder(),
+ true, true, false, true, false, false);
+ waitUntilHandlersIdle();
+ assertNoStartingWindow(controller1.getAppWindowToken(mDisplayContent));
+ assertHasStartingWindow(controller2.getAppWindowToken(mDisplayContent));
+ }
+
+ @Test
+ public void testTransferStartingWindowWhileCreating() throws Exception {
+ final WindowTestUtils.TestAppWindowContainerController controller1 =
+ createAppWindowController();
+ final WindowTestUtils.TestAppWindowContainerController controller2 =
+ createAppWindowController();
+ ((TestWindowManagerPolicy) sWm.mPolicy).setRunnableWhenAddingSplashScreen(() -> {
+
+ // Surprise, ...! Transfer window in the middle of the creation flow.
+ controller2.addStartingWindow(InstrumentationRegistry.getContext().getPackageName(),
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0, controller1.mToken.asBinder(),
+ true, true, false, true, false, false);
+ });
+ controller1.addStartingWindow(InstrumentationRegistry.getContext().getPackageName(),
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
+ false, false);
+ waitUntilHandlersIdle();
+ assertNoStartingWindow(controller1.getAppWindowToken(mDisplayContent));
+ assertHasStartingWindow(controller2.getAppWindowToken(mDisplayContent));
+ }
+
+ @Test
+ public void testTryTransferStartingWindowFromHiddenAboveToken() throws Exception {
+
+ // Add two tasks on top of each other.
+ TestTaskWindowContainerController taskController =
+ new WindowTestUtils.TestTaskWindowContainerController(this);
+ final WindowTestUtils.TestAppWindowContainerController controllerTop =
+ createAppWindowController(taskController);
+ final WindowTestUtils.TestAppWindowContainerController controllerBottom =
+ createAppWindowController(taskController);
+
+ // Add a starting window.
+ controllerTop.addStartingWindow(InstrumentationRegistry.getContext().getPackageName(),
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
+ false, false);
+ waitUntilHandlersIdle();
+
+ // Make the top one invisible, and try transfering the starting window from the top to the
+ // bottom one.
+ controllerTop.setVisibility(false, false);
+ controllerBottom.mContainer.transferStartingWindowFromHiddenAboveTokenIfNeeded();
+
+ // Assert that the bottom window now has the starting window.
+ assertNoStartingWindow(controllerTop.getAppWindowToken(mDisplayContent));
+ assertHasStartingWindow(controllerBottom.getAppWindowToken(mDisplayContent));
+ }
+
+ @Test
+ public void testReparent() throws Exception {
+ final StackWindowController stackController =
+ createStackControllerOnDisplay(mDisplayContent);
+ final WindowTestUtils.TestTaskWindowContainerController taskController1 =
+ new WindowTestUtils.TestTaskWindowContainerController(stackController);
+ final WindowTestUtils.TestAppWindowContainerController appWindowController1 =
+ createAppWindowController(taskController1);
+ final WindowTestUtils.TestTaskWindowContainerController taskController2 =
+ new WindowTestUtils.TestTaskWindowContainerController(stackController);
+ final WindowTestUtils.TestAppWindowContainerController appWindowController2 =
+ createAppWindowController(taskController2);
+ final WindowTestUtils.TestTaskWindowContainerController taskController3 =
+ new WindowTestUtils.TestTaskWindowContainerController(stackController);
+
+ try {
+ appWindowController1.reparent(taskController1, 0);
+ fail("Should not be able to reparent to the same parent");
+ } catch (IllegalArgumentException e) {
+ // Expected
+ }
+
+ try {
+ taskController3.setContainer(null);
+ appWindowController1.reparent(taskController3, 0);
+ fail("Should not be able to reparent to a task that doesn't have a container");
+ } catch (IllegalArgumentException e) {
+ // Expected
+ }
+
+ // Reparent the app window and ensure that it is moved
+ appWindowController1.reparent(taskController2, 0);
+ assertEquals(taskController2.mContainer, appWindowController1.mContainer.getParent());
+ assertEquals(0, ((WindowTestUtils.TestAppWindowToken) appWindowController1.mContainer)
+ .positionInParent());
+ assertEquals(1, ((WindowTestUtils.TestAppWindowToken) appWindowController2.mContainer)
+ .positionInParent());
+ }
+
+ private WindowTestUtils.TestAppWindowContainerController createAppWindowController() {
+ return createAppWindowController(
+ new WindowTestUtils.TestTaskWindowContainerController(this));
+ }
+
+ private WindowTestUtils.TestAppWindowContainerController createAppWindowController(
+ WindowTestUtils.TestTaskWindowContainerController taskController) {
+ return new WindowTestUtils.TestAppWindowContainerController(taskController);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/AppWindowTokenTests.java b/services/tests/servicestests/src/com/android/server/wm/AppWindowTokenTests.java
new file mode 100644
index 0000000..f6599dc
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/AppWindowTokenTests.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
+import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
+import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
+import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
+import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static android.view.WindowManager.TRANSIT_UNSET;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+/**
+ * Tests for the {@link AppWindowToken} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.wm.AppWindowTokenTests
+ */
+@SmallTest
+// TODO: b/68267650
+// @Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AppWindowTokenTests extends WindowTestsBase {
+
+ TaskStack mStack;
+ Task mTask;
+ WindowTestUtils.TestAppWindowToken mToken;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mStack = createTaskStackOnDisplay(mDisplayContent);
+ mTask = createTaskInStack(mStack, 0 /* userId */);
+ mToken = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+
+ mTask.addChild(mToken, 0);
+ }
+
+ @Test
+ @Presubmit
+ public void testAddWindow_Order() throws Exception {
+ assertEquals(0, mToken.getWindowsCount());
+
+ final WindowState win1 = createWindow(null, TYPE_APPLICATION, mToken, "win1");
+ final WindowState startingWin = createWindow(null, TYPE_APPLICATION_STARTING, mToken,
+ "startingWin");
+ final WindowState baseWin = createWindow(null, TYPE_BASE_APPLICATION, mToken, "baseWin");
+ final WindowState win4 = createWindow(null, TYPE_APPLICATION, mToken, "win4");
+
+ // Should not contain the windows that were added above.
+ assertEquals(4, mToken.getWindowsCount());
+ assertTrue(mToken.hasWindow(win1));
+ assertTrue(mToken.hasWindow(startingWin));
+ assertTrue(mToken.hasWindow(baseWin));
+ assertTrue(mToken.hasWindow(win4));
+
+ // The starting window should be on-top of all other windows.
+ assertEquals(startingWin, mToken.getLastChild());
+
+ // The base application window should be below all other windows.
+ assertEquals(baseWin, mToken.getFirstChild());
+ mToken.removeImmediately();
+ }
+
+ @Test
+ @Presubmit
+ public void testFindMainWindow() throws Exception {
+ assertNull(mToken.findMainWindow());
+
+ final WindowState window1 = createWindow(null, TYPE_BASE_APPLICATION, mToken, "window1");
+ final WindowState window11 = createWindow(window1, FIRST_SUB_WINDOW, mToken, "window11");
+ final WindowState window12 = createWindow(window1, FIRST_SUB_WINDOW, mToken, "window12");
+ assertEquals(window1, mToken.findMainWindow());
+ window1.mAnimatingExit = true;
+ assertEquals(window1, mToken.findMainWindow());
+ final WindowState window2 = createWindow(null, TYPE_APPLICATION_STARTING, mToken, "window2");
+ assertEquals(window2, mToken.findMainWindow());
+ mToken.removeImmediately();
+ }
+
+ @Test
+ @Presubmit
+ public void testGetTopFullscreenWindow() throws Exception {
+ assertNull(mToken.getTopFullscreenWindow());
+
+ final WindowState window1 = createWindow(null, TYPE_BASE_APPLICATION, mToken, "window1");
+ final WindowState window11 = createWindow(null, TYPE_APPLICATION, mToken, "window11");
+ final WindowState window12 = createWindow(null, TYPE_APPLICATION, mToken, "window12");
+ assertEquals(window12, mToken.getTopFullscreenWindow());
+ window12.mAttrs.width = 500;
+ assertEquals(window11, mToken.getTopFullscreenWindow());
+ window11.mAttrs.width = 500;
+ assertEquals(window1, mToken.getTopFullscreenWindow());
+ mToken.removeImmediately();
+ }
+
+ @Test
+ public void testLandscapeSeascapeRotationByApp() throws Exception {
+ // Some plumbing to get the service ready for rotation updates.
+ sWm.mDisplayReady = true;
+ sWm.mDisplayEnabled = true;
+
+ final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(
+ TYPE_BASE_APPLICATION);
+ attrs.setTitle("AppWindow");
+ final WindowTestUtils.TestWindowState appWindow = createWindowState(attrs, mToken);
+ mToken.addWindow(appWindow);
+
+ // Set initial orientation and update.
+ mToken.setOrientation(SCREEN_ORIENTATION_LANDSCAPE);
+ sWm.updateOrientationFromAppTokens(mDisplayContent.getOverrideConfiguration(), null,
+ mDisplayContent.getDisplayId());
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, mDisplayContent.getLastOrientation());
+ appWindow.resizeReported = false;
+
+ // Update the orientation to perform 180 degree rotation and check that resize was reported.
+ mToken.setOrientation(SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
+ sWm.updateOrientationFromAppTokens(mDisplayContent.getOverrideConfiguration(), null,
+ mDisplayContent.getDisplayId());
+ sWm.mRoot.performSurfacePlacement(false /* recoveringMemory */);
+ assertEquals(SCREEN_ORIENTATION_REVERSE_LANDSCAPE, mDisplayContent.getLastOrientation());
+ assertTrue(appWindow.resizeReported);
+ appWindow.removeImmediately();
+ }
+
+ @Test
+ public void testLandscapeSeascapeRotationByPolicy() throws Exception {
+ // Some plumbing to get the service ready for rotation updates.
+ sWm.mDisplayReady = true;
+ sWm.mDisplayEnabled = true;
+
+ final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(
+ TYPE_BASE_APPLICATION);
+ attrs.setTitle("AppWindow");
+ final WindowTestUtils.TestWindowState appWindow = createWindowState(attrs, mToken);
+ mToken.addWindow(appWindow);
+
+ // Set initial orientation and update.
+ performRotation(Surface.ROTATION_90);
+ appWindow.resizeReported = false;
+
+ // Update the rotation to perform 180 degree rotation and check that resize was reported.
+ performRotation(Surface.ROTATION_270);
+ assertTrue(appWindow.resizeReported);
+ appWindow.removeImmediately();
+ }
+
+ private void performRotation(int rotationToReport) {
+ ((TestWindowManagerPolicy) sWm.mPolicy).rotationToReport = rotationToReport;
+ sWm.updateRotation(false, false);
+ // Simulate animator finishing orientation change
+ sWm.mRoot.mOrientationChangeComplete = true;
+ sWm.mRoot.performSurfacePlacement(false /* recoveringMemory */);
+ }
+
+ @Test
+ @Presubmit
+ public void testGetOrientation() throws Exception {
+ mToken.setOrientation(SCREEN_ORIENTATION_LANDSCAPE);
+
+ mToken.setFillsParent(false);
+ // Can specify orientation if app doesn't fill parent.
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, mToken.getOrientation());
+
+ mToken.setFillsParent(true);
+ mToken.setHidden(true);
+ mToken.sendingToBottom = true;
+ // Can not specify orientation if app isn't visible even though it fills parent.
+ assertEquals(SCREEN_ORIENTATION_UNSET, mToken.getOrientation());
+ // Can specify orientation if the current orientation candidate is orientation behind.
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, mToken.getOrientation(SCREEN_ORIENTATION_BEHIND));
+ }
+
+ @Test
+ @Presubmit
+ public void testKeyguardFlagsDuringRelaunch() throws Exception {
+ final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(
+ TYPE_BASE_APPLICATION);
+ attrs.flags |= FLAG_SHOW_WHEN_LOCKED | FLAG_DISMISS_KEYGUARD;
+ attrs.setTitle("AppWindow");
+ final WindowTestUtils.TestWindowState appWindow = createWindowState(attrs, mToken);
+
+ // Add window with show when locked flag
+ mToken.addWindow(appWindow);
+ assertTrue(mToken.containsShowWhenLockedWindow() && mToken.containsDismissKeyguardWindow());
+
+ // Start relaunching
+ mToken.startRelaunching();
+ assertTrue(mToken.containsShowWhenLockedWindow() && mToken.containsDismissKeyguardWindow());
+
+ // Remove window and make sure that we still report back flag
+ mToken.removeChild(appWindow);
+ assertTrue(mToken.containsShowWhenLockedWindow() && mToken.containsDismissKeyguardWindow());
+
+ // Finish relaunching and ensure flag is now not reported
+ mToken.finishRelaunching();
+ assertFalse(mToken.containsShowWhenLockedWindow() || mToken.containsDismissKeyguardWindow());
+ }
+
+ @Test
+ @FlakyTest(detail = "Promote once confirmed non-flaky")
+ public void testStuckExitingWindow() throws Exception {
+ final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
+ "closingWindow");
+ closingWindow.mAnimatingExit = true;
+ closingWindow.mRemoveOnExit = true;
+ closingWindow.mAppToken.setVisibility(null, false /* visible */, TRANSIT_UNSET,
+ true /* performLayout */, false /* isVoiceInteraction */);
+
+ // We pretended that we were running an exit animation, but that should have been cleared up
+ // by changing visibility of AppWindowToken
+ closingWindow.removeIfPossible();
+ assertTrue(closingWindow.mRemoved);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/BoundsAnimationControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/BoundsAnimationControllerTests.java
new file mode 100644
index 0000000..ff631e7
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/BoundsAnimationControllerTests.java
@@ -0,0 +1,595 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static com.android.server.wm.BoundsAnimationController.NO_PIP_MODE_CHANGED_CALLBACKS;
+import static com.android.server.wm.BoundsAnimationController.SCHEDULE_PIP_MODE_CHANGED_ON_END;
+import static com.android.server.wm.BoundsAnimationController.SCHEDULE_PIP_MODE_CHANGED_ON_START;
+import static com.android.server.wm.BoundsAnimationController.SchedulePipModeChangedState;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.server.wm.WindowManagerInternal.AppTransitionListener;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.server.wm.BoundsAnimationController.BoundsAnimator;
+
+/**
+ * Test class for {@link BoundsAnimationController} to ensure that it sends the right callbacks
+ * depending on the various interactions.
+ *
+ * We are really concerned about only three of the transition states [F = fullscreen, !F = floating]
+ * F->!F, !F->!F, and !F->F. Each animation can only be cancelled from the target mid-transition,
+ * or if a new animation starts on the same target. The tests below verifies that the target is
+ * notified of all the cases where it is animating and cancelled so that it can respond
+ * appropriately.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.BoundsAnimationControllerTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class BoundsAnimationControllerTests extends WindowTestsBase {
+
+ /**
+ * Mock value animator to simulate updates with.
+ */
+ private class MockValueAnimator extends ValueAnimator {
+
+ private float mFraction;
+
+ public MockValueAnimator getWithValue(float fraction) {
+ mFraction = fraction;
+ return this;
+ }
+
+ @Override
+ public Object getAnimatedValue() {
+ return mFraction;
+ }
+ }
+
+ /**
+ * Mock app transition to fire notifications to the bounds animator.
+ */
+ private class MockAppTransition extends AppTransition {
+
+ private AppTransitionListener mListener;
+
+ MockAppTransition(Context context) {
+ super(context, null);
+ }
+
+ @Override
+ void registerListenerLocked(AppTransitionListener listener) {
+ mListener = listener;
+ }
+
+ public void notifyTransitionPending() {
+ mListener.onAppTransitionPendingLocked();
+ }
+
+ public void notifyTransitionCancelled(int transit) {
+ mListener.onAppTransitionCancelledLocked(transit);
+ }
+
+ public void notifyTransitionStarting(int transit) {
+ mListener.onAppTransitionStartingLocked(transit, null, null, 0, 0, 0);
+ }
+
+ public void notifyTransitionFinished() {
+ mListener.onAppTransitionFinishedLocked(null);
+ }
+ }
+
+ /**
+ * A test animate bounds user to track callbacks from the bounds animation.
+ */
+ private class TestBoundsAnimationTarget implements BoundsAnimationTarget {
+
+ boolean mAwaitingAnimationStart;
+ boolean mMovedToFullscreen;
+ boolean mAnimationStarted;
+ boolean mSchedulePipModeChangedOnStart;
+ boolean mForcePipModeChangedCallback;
+ boolean mAnimationEnded;
+ Rect mAnimationEndFinalStackBounds;
+ boolean mSchedulePipModeChangedOnEnd;
+ boolean mBoundsUpdated;
+ boolean mCancelRequested;
+ Rect mStackBounds;
+ Rect mTaskBounds;
+
+ void initialize(Rect from) {
+ mAwaitingAnimationStart = true;
+ mMovedToFullscreen = false;
+ mAnimationStarted = false;
+ mAnimationEnded = false;
+ mAnimationEndFinalStackBounds = null;
+ mForcePipModeChangedCallback = false;
+ mSchedulePipModeChangedOnStart = false;
+ mSchedulePipModeChangedOnEnd = false;
+ mStackBounds = from;
+ mTaskBounds = null;
+ mBoundsUpdated = false;
+ }
+
+ @Override
+ public void onAnimationStart(boolean schedulePipModeChangedCallback, boolean forceUpdate) {
+ mAwaitingAnimationStart = false;
+ mAnimationStarted = true;
+ mSchedulePipModeChangedOnStart = schedulePipModeChangedCallback;
+ mForcePipModeChangedCallback = forceUpdate;
+ }
+
+ @Override
+ public boolean shouldDeferStartOnMoveToFullscreen() {
+ return true;
+ }
+
+ @Override
+ public boolean setPinnedStackSize(Rect stackBounds, Rect taskBounds) {
+ // TODO: Once we break the runs apart, we should fail() here if this is called outside
+ // of onAnimationStart() and onAnimationEnd()
+ if (mCancelRequested) {
+ mCancelRequested = false;
+ return false;
+ } else {
+ mBoundsUpdated = true;
+ mStackBounds = stackBounds;
+ mTaskBounds = taskBounds;
+ return true;
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(boolean schedulePipModeChangedCallback, Rect finalStackBounds,
+ boolean moveToFullscreen) {
+ mAnimationEnded = true;
+ mAnimationEndFinalStackBounds = finalStackBounds;
+ mSchedulePipModeChangedOnEnd = schedulePipModeChangedCallback;
+ mMovedToFullscreen = moveToFullscreen;
+ mTaskBounds = null;
+ }
+ }
+
+ /**
+ * Drives the animations, makes common assertions along the way.
+ */
+ private class BoundsAnimationDriver {
+
+ private BoundsAnimationController mController;
+ private TestBoundsAnimationTarget mTarget;
+ private BoundsAnimator mAnimator;
+
+ private Rect mFrom;
+ private Rect mTo;
+ private Rect mLargerBounds;
+ private Rect mExpectedFinalBounds;
+
+ BoundsAnimationDriver(BoundsAnimationController controller,
+ TestBoundsAnimationTarget target) {
+ mController = controller;
+ mTarget = target;
+ }
+
+ BoundsAnimationDriver start(Rect from, Rect to) {
+ if (mAnimator != null) {
+ throw new IllegalArgumentException("Call restart() to restart an animation");
+ }
+
+ boolean fromFullscreen = from.equals(BOUNDS_FULL);
+ boolean toFullscreen = to.equals(BOUNDS_FULL);
+
+ mTarget.initialize(from);
+
+ // Started, not running
+ assertTrue(mTarget.mAwaitingAnimationStart);
+ assertTrue(!mTarget.mAnimationStarted);
+
+ startImpl(from, to);
+
+ // Ensure that the animator is paused for the all windows drawn signal when animating
+ // to/from fullscreen
+ if (fromFullscreen || toFullscreen) {
+ assertTrue(mAnimator.isPaused());
+ mController.onAllWindowsDrawn();
+ } else {
+ assertTrue(!mAnimator.isPaused());
+ }
+
+ // Started and running
+ assertTrue(!mTarget.mAwaitingAnimationStart);
+ assertTrue(mTarget.mAnimationStarted);
+
+ return this;
+ }
+
+ BoundsAnimationDriver restart(Rect to, boolean expectStartedAndPipModeChangedCallback) {
+ if (mAnimator == null) {
+ throw new IllegalArgumentException("Call start() to start a new animation");
+ }
+
+ BoundsAnimator oldAnimator = mAnimator;
+ boolean toSameBounds = mAnimator.isStarted() && to.equals(mTo);
+
+ // Reset the animation start state
+ mTarget.mAnimationStarted = false;
+
+ // Start animation
+ startImpl(mTarget.mStackBounds, to);
+
+ if (toSameBounds) {
+ // Same animator if same final bounds
+ assertSame(oldAnimator, mAnimator);
+ }
+
+ if (expectStartedAndPipModeChangedCallback) {
+ // Replacing animation with pending pip mode changed callback, ensure we update
+ assertTrue(mTarget.mAnimationStarted);
+ assertTrue(mTarget.mSchedulePipModeChangedOnStart);
+ assertTrue(mTarget.mForcePipModeChangedCallback);
+ } else {
+ // No animation start for replacing animation
+ assertTrue(!mTarget.mAnimationStarted);
+ }
+ mTarget.mAnimationStarted = true;
+ return this;
+ }
+
+ private BoundsAnimationDriver startImpl(Rect from, Rect to) {
+ boolean fromFullscreen = from.equals(BOUNDS_FULL);
+ boolean toFullscreen = to.equals(BOUNDS_FULL);
+ mFrom = new Rect(from);
+ mTo = new Rect(to);
+ mExpectedFinalBounds = new Rect(to);
+ mLargerBounds = getLargerBounds(mFrom, mTo);
+
+ // Start animation
+ final @SchedulePipModeChangedState int schedulePipModeChangedState = toFullscreen
+ ? SCHEDULE_PIP_MODE_CHANGED_ON_START
+ : fromFullscreen
+ ? SCHEDULE_PIP_MODE_CHANGED_ON_END
+ : NO_PIP_MODE_CHANGED_CALLBACKS;
+ mAnimator = mController.animateBoundsImpl(mTarget, from, to, DURATION,
+ schedulePipModeChangedState, fromFullscreen, toFullscreen);
+
+ // Original stack bounds, frozen task bounds
+ assertEquals(mFrom, mTarget.mStackBounds);
+ assertEqualSizeAtOffset(mLargerBounds, mTarget.mTaskBounds);
+
+ // Animating to larger size
+ if (mFrom.equals(mLargerBounds)) {
+ assertTrue(!mAnimator.animatingToLargerSize());
+ } else if (mTo.equals(mLargerBounds)) {
+ assertTrue(mAnimator.animatingToLargerSize());
+ }
+
+ return this;
+ }
+
+ BoundsAnimationDriver expectStarted(boolean schedulePipModeChanged) {
+ // Callback made
+ assertTrue(mTarget.mAnimationStarted);
+
+ assertEquals(schedulePipModeChanged, mTarget.mSchedulePipModeChangedOnStart);
+ return this;
+ }
+
+ BoundsAnimationDriver update(float t) {
+ mAnimator.onAnimationUpdate(mMockAnimator.getWithValue(t));
+
+ // Temporary stack bounds, frozen task bounds
+ if (t == 0f) {
+ assertEquals(mFrom, mTarget.mStackBounds);
+ } else if (t == 1f) {
+ assertEquals(mTo, mTarget.mStackBounds);
+ } else {
+ assertNotEquals(mFrom, mTarget.mStackBounds);
+ assertNotEquals(mTo, mTarget.mStackBounds);
+ }
+ assertEqualSizeAtOffset(mLargerBounds, mTarget.mTaskBounds);
+ return this;
+ }
+
+ BoundsAnimationDriver cancel() {
+ // Cancel
+ mTarget.mCancelRequested = true;
+ mTarget.mBoundsUpdated = false;
+ mExpectedFinalBounds = null;
+
+ // Update
+ mAnimator.onAnimationUpdate(mMockAnimator.getWithValue(0.5f));
+
+ // Not started, not running, cancel reset
+ assertTrue(!mTarget.mCancelRequested);
+
+ // Stack/task bounds not updated
+ assertTrue(!mTarget.mBoundsUpdated);
+
+ // Callback made
+ assertTrue(mTarget.mAnimationEnded);
+ assertNull(mTarget.mAnimationEndFinalStackBounds);
+
+ return this;
+ }
+
+ BoundsAnimationDriver end() {
+ mAnimator.end();
+
+ // Final stack bounds
+ assertEquals(mTo, mTarget.mStackBounds);
+ assertEquals(mExpectedFinalBounds, mTarget.mAnimationEndFinalStackBounds);
+ assertNull(mTarget.mTaskBounds);
+
+ return this;
+ }
+
+ BoundsAnimationDriver expectEnded(boolean schedulePipModeChanged,
+ boolean moveToFullscreen) {
+ // Callback made
+ assertTrue(mTarget.mAnimationEnded);
+
+ assertEquals(schedulePipModeChanged, mTarget.mSchedulePipModeChangedOnEnd);
+ assertEquals(moveToFullscreen, mTarget.mMovedToFullscreen);
+ return this;
+ }
+
+ private Rect getLargerBounds(Rect r1, Rect r2) {
+ int r1Area = r1.width() * r1.height();
+ int r2Area = r2.width() * r2.height();
+ if (r1Area <= r2Area) {
+ return r2;
+ } else {
+ return r1;
+ }
+ }
+ }
+
+ // Constants
+ private static final boolean SCHEDULE_PIP_MODE_CHANGED = true;
+ private static final boolean MOVE_TO_FULLSCREEN = true;
+ private static final int DURATION = 100;
+
+ // Some dummy bounds to represent fullscreen and floating bounds
+ private static final Rect BOUNDS_FULL = new Rect(0, 0, 100, 100);
+ private static final Rect BOUNDS_FLOATING = new Rect(60, 60, 95, 95);
+ private static final Rect BOUNDS_SMALLER_FLOATING = new Rect(80, 80, 95, 95);
+
+ // Common
+ private MockAppTransition mMockAppTransition;
+ private MockValueAnimator mMockAnimator;
+ private TestBoundsAnimationTarget mTarget;
+ private BoundsAnimationController mController;
+ private BoundsAnimationDriver mDriver;
+
+ // Temp
+ private Rect mTmpRect = new Rect();
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ final Context context = InstrumentationRegistry.getTargetContext();
+ final Handler handler = new Handler(Looper.getMainLooper());
+ mMockAppTransition = new MockAppTransition(context);
+ mMockAnimator = new MockValueAnimator();
+ mTarget = new TestBoundsAnimationTarget();
+ mController = new BoundsAnimationController(context, mMockAppTransition, handler, null);
+ mDriver = new BoundsAnimationDriver(mController, mTarget);
+ }
+
+ /** BASE TRANSITIONS **/
+
+ @UiThreadTest
+ @Test
+ public void testFullscreenToFloatingTransition() throws Exception {
+ mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING)
+ .expectStarted(!SCHEDULE_PIP_MODE_CHANGED)
+ .update(0f)
+ .update(0.5f)
+ .update(1f)
+ .end()
+ .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testFloatingToFullscreenTransition() throws Exception {
+ mDriver.start(BOUNDS_FLOATING, BOUNDS_FULL)
+ .expectStarted(SCHEDULE_PIP_MODE_CHANGED)
+ .update(0f)
+ .update(0.5f)
+ .update(1f)
+ .end()
+ .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, MOVE_TO_FULLSCREEN);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testFloatingToSmallerFloatingTransition() throws Exception {
+ mDriver.start(BOUNDS_FLOATING, BOUNDS_SMALLER_FLOATING)
+ .expectStarted(!SCHEDULE_PIP_MODE_CHANGED)
+ .update(0f)
+ .update(0.5f)
+ .update(1f)
+ .end()
+ .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testFloatingToLargerFloatingTransition() throws Exception {
+ mDriver.start(BOUNDS_SMALLER_FLOATING, BOUNDS_FLOATING)
+ .expectStarted(!SCHEDULE_PIP_MODE_CHANGED)
+ .update(0f)
+ .update(0.5f)
+ .update(1f)
+ .end()
+ .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN);
+ }
+
+ /** F->!F w/ CANCEL **/
+
+ @UiThreadTest
+ @Test
+ public void testFullscreenToFloatingCancelFromTarget() throws Exception {
+ mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING)
+ .expectStarted(!SCHEDULE_PIP_MODE_CHANGED)
+ .update(0.25f)
+ .cancel()
+ .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testFullscreenToFloatingCancelFromAnimationToSameBounds() throws Exception {
+ mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING)
+ .expectStarted(!SCHEDULE_PIP_MODE_CHANGED)
+ .update(0.25f)
+ .restart(BOUNDS_FLOATING, false /* expectStartedAndPipModeChangedCallback */)
+ .end()
+ .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testFullscreenToFloatingCancelFromAnimationToFloatingBounds() throws Exception {
+ mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING)
+ .expectStarted(!SCHEDULE_PIP_MODE_CHANGED)
+ .update(0.25f)
+ .restart(BOUNDS_SMALLER_FLOATING,
+ false /* expectStartedAndPipModeChangedCallback */)
+ .end()
+ .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testFullscreenToFloatingCancelFromAnimationToFullscreenBounds() throws Exception {
+ // When animating from fullscreen and the animation is interruped, we expect the animation
+ // start callback to be made, with a forced pip mode change callback
+ mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING)
+ .expectStarted(!SCHEDULE_PIP_MODE_CHANGED)
+ .update(0.25f)
+ .restart(BOUNDS_FULL, true /* expectStartedAndPipModeChangedCallback */)
+ .end()
+ .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, MOVE_TO_FULLSCREEN);
+ }
+
+ /** !F->F w/ CANCEL **/
+
+ @UiThreadTest
+ @Test
+ public void testFloatingToFullscreenCancelFromTarget() throws Exception {
+ mDriver.start(BOUNDS_FLOATING, BOUNDS_FULL)
+ .expectStarted(SCHEDULE_PIP_MODE_CHANGED)
+ .update(0.25f)
+ .cancel()
+ .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testFloatingToFullscreenCancelFromAnimationToSameBounds() throws Exception {
+ mDriver.start(BOUNDS_FLOATING, BOUNDS_FULL)
+ .expectStarted(SCHEDULE_PIP_MODE_CHANGED)
+ .update(0.25f)
+ .restart(BOUNDS_FULL, false /* expectStartedAndPipModeChangedCallback */)
+ .end()
+ .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, MOVE_TO_FULLSCREEN);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testFloatingToFullscreenCancelFromAnimationToFloatingBounds() throws Exception {
+ mDriver.start(BOUNDS_FLOATING, BOUNDS_FULL)
+ .expectStarted(SCHEDULE_PIP_MODE_CHANGED)
+ .update(0.25f)
+ .restart(BOUNDS_SMALLER_FLOATING,
+ false /* expectStartedAndPipModeChangedCallback */)
+ .end()
+ .expectEnded(SCHEDULE_PIP_MODE_CHANGED, MOVE_TO_FULLSCREEN);
+ }
+
+ /** !F->!F w/ CANCEL **/
+
+ @UiThreadTest
+ @Test
+ public void testFloatingToSmallerFloatingCancelFromTarget() throws Exception {
+ mDriver.start(BOUNDS_FLOATING, BOUNDS_SMALLER_FLOATING)
+ .expectStarted(!SCHEDULE_PIP_MODE_CHANGED)
+ .update(0.25f)
+ .cancel()
+ .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testFloatingToLargerFloatingCancelFromTarget() throws Exception {
+ mDriver.start(BOUNDS_SMALLER_FLOATING, BOUNDS_FLOATING)
+ .expectStarted(!SCHEDULE_PIP_MODE_CHANGED)
+ .update(0.25f)
+ .cancel()
+ .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN);
+ }
+
+ /** MISC **/
+
+ @UiThreadTest
+ @Test
+ public void testBoundsAreCopied() throws Exception {
+ Rect from = new Rect(0, 0, 100, 100);
+ Rect to = new Rect(25, 25, 75, 75);
+ mDriver.start(from, to)
+ .update(0.25f)
+ .end();
+ assertEquals(new Rect(0, 0, 100, 100), from);
+ assertEquals(new Rect(25, 25, 75, 75), to);
+ }
+
+ /**
+ * @return whether the task and stack bounds would be the same if they were at the same offset.
+ */
+ private boolean assertEqualSizeAtOffset(Rect stackBounds, Rect taskBounds) {
+ mTmpRect.set(taskBounds);
+ mTmpRect.offsetTo(stackBounds.left, stackBounds.top);
+ return stackBounds.equals(mTmpRect);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/ConfigurationContainerTests.java b/services/tests/servicestests/src/com/android/server/wm/ConfigurationContainerTests.java
new file mode 100644
index 0000000..192e156
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/ConfigurationContainerTests.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+import static android.content.res.Configuration.EMPTY;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.res.Configuration;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test class for {@link ConfigurationContainer}.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.ConfigurationContainerTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class ConfigurationContainerTests {
+
+ @Test
+ public void testConfigurationInit() throws Exception {
+ // Check root container initial config.
+ final TestConfigurationContainer root = new TestConfigurationContainer();
+ assertEquals(EMPTY, root.getOverrideConfiguration());
+ assertEquals(EMPTY, root.getMergedOverrideConfiguration());
+ assertEquals(EMPTY, root.getConfiguration());
+
+ // Check child initial config.
+ final TestConfigurationContainer child1 = root.addChild();
+ assertEquals(EMPTY, child1.getOverrideConfiguration());
+ assertEquals(EMPTY, child1.getMergedOverrideConfiguration());
+ assertEquals(EMPTY, child1.getConfiguration());
+
+ // Check child initial config if root has overrides.
+ final Configuration rootOverrideConfig = new Configuration();
+ rootOverrideConfig.fontScale = 1.3f;
+ root.onOverrideConfigurationChanged(rootOverrideConfig);
+ final TestConfigurationContainer child2 = root.addChild();
+ assertEquals(EMPTY, child2.getOverrideConfiguration());
+ assertEquals(rootOverrideConfig, child2.getMergedOverrideConfiguration());
+ assertEquals(rootOverrideConfig, child2.getConfiguration());
+
+ // Check child initial config if root has parent config set.
+ final Configuration rootParentConfig = new Configuration();
+ rootParentConfig.fontScale = 0.8f;
+ rootParentConfig.orientation = SCREEN_ORIENTATION_LANDSCAPE;
+ root.onConfigurationChanged(rootParentConfig);
+ final Configuration rootFullConfig = new Configuration(rootParentConfig);
+ rootFullConfig.updateFrom(rootOverrideConfig);
+
+ final TestConfigurationContainer child3 = root.addChild();
+ assertEquals(EMPTY, child3.getOverrideConfiguration());
+ assertEquals(rootOverrideConfig, child3.getMergedOverrideConfiguration());
+ assertEquals(rootFullConfig, child3.getConfiguration());
+ }
+
+ @Test
+ public void testConfigurationChangeOnAddRemove() throws Exception {
+ // Init root's config.
+ final TestConfigurationContainer root = new TestConfigurationContainer();
+ final Configuration rootOverrideConfig = new Configuration();
+ rootOverrideConfig.fontScale = 1.3f;
+ root.onOverrideConfigurationChanged(rootOverrideConfig);
+
+ // Init child's config.
+ final TestConfigurationContainer child = root.addChild();
+ final Configuration childOverrideConfig = new Configuration();
+ childOverrideConfig.densityDpi = 320;
+ child.onOverrideConfigurationChanged(childOverrideConfig);
+ final Configuration mergedOverrideConfig = new Configuration(root.getConfiguration());
+ mergedOverrideConfig.updateFrom(childOverrideConfig);
+
+ // Check configuration update when child is removed from parent.
+ root.removeChild(child);
+ assertEquals(childOverrideConfig, child.getOverrideConfiguration());
+ assertEquals(mergedOverrideConfig, child.getMergedOverrideConfiguration());
+ assertEquals(mergedOverrideConfig, child.getConfiguration());
+
+ // It may be paranoia... but let's check if parent's config didn't change after removal.
+ assertEquals(rootOverrideConfig, root.getOverrideConfiguration());
+ assertEquals(rootOverrideConfig, root.getMergedOverrideConfiguration());
+ assertEquals(rootOverrideConfig, root.getConfiguration());
+
+ // Init different root
+ final TestConfigurationContainer root2 = new TestConfigurationContainer();
+ final Configuration rootOverrideConfig2 = new Configuration();
+ rootOverrideConfig2.fontScale = 1.1f;
+ root2.onOverrideConfigurationChanged(rootOverrideConfig2);
+
+ // Check configuration update when child is added to different parent.
+ mergedOverrideConfig.setTo(rootOverrideConfig2);
+ mergedOverrideConfig.updateFrom(childOverrideConfig);
+ root2.addChild(child);
+ assertEquals(childOverrideConfig, child.getOverrideConfiguration());
+ assertEquals(mergedOverrideConfig, child.getMergedOverrideConfiguration());
+ assertEquals(mergedOverrideConfig, child.getConfiguration());
+ }
+
+ @Test
+ public void testConfigurationChangePropagation() throws Exception {
+ // Builds 3-level vertical hierarchy with one configuration container on each level.
+ // In addition to different overrides on each level, everyone in hierarchy will have one
+ // common overridden value - orientation;
+
+ // Init root's config.
+ final TestConfigurationContainer root = new TestConfigurationContainer();
+ final Configuration rootOverrideConfig = new Configuration();
+ rootOverrideConfig.fontScale = 1.3f;
+ rootOverrideConfig.orientation = SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+ root.onOverrideConfigurationChanged(rootOverrideConfig);
+
+ // Init children.
+ final TestConfigurationContainer child1 = root.addChild();
+ final Configuration childOverrideConfig1 = new Configuration();
+ childOverrideConfig1.densityDpi = 320;
+ childOverrideConfig1.orientation = SCREEN_ORIENTATION_LANDSCAPE;
+ child1.onOverrideConfigurationChanged(childOverrideConfig1);
+
+ final TestConfigurationContainer child2 = child1.addChild();
+ final Configuration childOverrideConfig2 = new Configuration();
+ childOverrideConfig2.screenWidthDp = 150;
+ childOverrideConfig2.orientation = SCREEN_ORIENTATION_PORTRAIT;
+ child2.onOverrideConfigurationChanged(childOverrideConfig2);
+
+ // Check configuration on all levels when root override is updated.
+ rootOverrideConfig.smallestScreenWidthDp = 200;
+ root.onOverrideConfigurationChanged(rootOverrideConfig);
+
+ final Configuration mergedOverrideConfig1 = new Configuration(rootOverrideConfig);
+ mergedOverrideConfig1.updateFrom(childOverrideConfig1);
+ final Configuration mergedConfig1 = new Configuration(mergedOverrideConfig1);
+
+ final Configuration mergedOverrideConfig2 = new Configuration(mergedOverrideConfig1);
+ mergedOverrideConfig2.updateFrom(childOverrideConfig2);
+ final Configuration mergedConfig2 = new Configuration(mergedOverrideConfig2);
+
+ assertEquals(rootOverrideConfig, root.getOverrideConfiguration());
+ assertEquals(rootOverrideConfig, root.getMergedOverrideConfiguration());
+ assertEquals(rootOverrideConfig, root.getConfiguration());
+
+ assertEquals(childOverrideConfig1, child1.getOverrideConfiguration());
+ assertEquals(mergedOverrideConfig1, child1.getMergedOverrideConfiguration());
+ assertEquals(mergedConfig1, child1.getConfiguration());
+
+ assertEquals(childOverrideConfig2, child2.getOverrideConfiguration());
+ assertEquals(mergedOverrideConfig2, child2.getMergedOverrideConfiguration());
+ assertEquals(mergedConfig2, child2.getConfiguration());
+
+ // Check configuration on all levels when root parent config is updated.
+ final Configuration rootParentConfig = new Configuration();
+ rootParentConfig.screenHeightDp = 100;
+ rootParentConfig.orientation = SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+ root.onConfigurationChanged(rootParentConfig);
+ final Configuration mergedRootConfig = new Configuration(rootParentConfig);
+ mergedRootConfig.updateFrom(rootOverrideConfig);
+
+ mergedConfig1.setTo(mergedRootConfig);
+ mergedConfig1.updateFrom(mergedOverrideConfig1);
+
+ mergedConfig2.setTo(mergedConfig1);
+ mergedConfig2.updateFrom(mergedOverrideConfig2);
+
+ assertEquals(rootOverrideConfig, root.getOverrideConfiguration());
+ assertEquals(rootOverrideConfig, root.getMergedOverrideConfiguration());
+ assertEquals(mergedRootConfig, root.getConfiguration());
+
+ assertEquals(childOverrideConfig1, child1.getOverrideConfiguration());
+ assertEquals(mergedOverrideConfig1, child1.getMergedOverrideConfiguration());
+ assertEquals(mergedConfig1, child1.getConfiguration());
+
+ assertEquals(childOverrideConfig2, child2.getOverrideConfiguration());
+ assertEquals(mergedOverrideConfig2, child2.getMergedOverrideConfiguration());
+ assertEquals(mergedConfig2, child2.getConfiguration());
+ }
+
+ @Test
+ public void testSetWindowingMode() throws Exception {
+ final TestConfigurationContainer root = new TestConfigurationContainer();
+ root.setWindowingMode(WINDOWING_MODE_UNDEFINED);
+ final TestConfigurationContainer child = root.addChild();
+ child.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ assertEquals(WINDOWING_MODE_UNDEFINED, root.getWindowingMode());
+ assertEquals(WINDOWING_MODE_FREEFORM, child.getWindowingMode());
+
+ root.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ assertEquals(WINDOWING_MODE_FULLSCREEN, root.getWindowingMode());
+ assertEquals(WINDOWING_MODE_FREEFORM, child.getWindowingMode());
+ }
+
+ @Test
+ public void testSetActivityType() throws Exception {
+ final TestConfigurationContainer root = new TestConfigurationContainer();
+ root.setActivityType(ACTIVITY_TYPE_UNDEFINED);
+ final TestConfigurationContainer child = root.addChild();
+ child.setActivityType(ACTIVITY_TYPE_STANDARD);
+ assertEquals(ACTIVITY_TYPE_UNDEFINED, root.getActivityType());
+ assertEquals(ACTIVITY_TYPE_STANDARD, child.getActivityType());
+
+ boolean gotException = false;
+ try {
+ // Can't change activity type once set.
+ child.setActivityType(ACTIVITY_TYPE_HOME);
+ } catch (IllegalStateException e) {
+ gotException = true;
+ }
+ assertTrue("Can't change activity type once set.", gotException);
+
+ // TODO: Commenting out for now until we figure-out a good way to test these rules that
+ // should only apply to system process.
+ /*
+ gotException = false;
+ try {
+ // Parent can't change child's activity type once set.
+ root.setActivityType(ACTIVITY_TYPE_HOME);
+ } catch (IllegalStateException e) {
+ gotException = true;
+ }
+ assertTrue("Parent can't change activity type once set.", gotException);
+ assertEquals(ACTIVITY_TYPE_HOME, root.getActivityType());
+
+ final TestConfigurationContainer child2 = new TestConfigurationContainer();
+ child2.setActivityType(ACTIVITY_TYPE_RECENTS);
+
+ gotException = false;
+ try {
+ // Can't re-parent to a different activity type.
+ root.addChild(child2);
+ } catch (IllegalStateException e) {
+ gotException = true;
+ }
+ assertTrue("Can't re-parent to a different activity type.", gotException);
+ */
+
+ }
+
+ @Test
+ public void testRegisterConfigurationChangeListener() throws Exception {
+ final TestConfigurationContainer container = new TestConfigurationContainer();
+ final TestConfigurationChangeListener listener = new TestConfigurationChangeListener();
+ final Configuration config = new Configuration();
+ config.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ config.windowConfiguration.setAppBounds(10, 10, 10, 10);
+ container.onOverrideConfigurationChanged(config);
+ container.registerConfigurationChangeListener(listener);
+ // Assert listener got the current config. of the container after it was registered.
+ assertEquals(config, listener.mOverrideConfiguration);
+ // Assert listener gets changes to override configuration.
+ container.onOverrideConfigurationChanged(EMPTY);
+ assertEquals(EMPTY, listener.mOverrideConfiguration);
+ }
+
+ /**
+ * Contains minimal implementation of {@link ConfigurationContainer}'s abstract behavior needed
+ * for testing.
+ */
+ private class TestConfigurationContainer
+ extends ConfigurationContainer<TestConfigurationContainer> {
+ private List<TestConfigurationContainer> mChildren = new ArrayList<>();
+ private TestConfigurationContainer mParent;
+
+ TestConfigurationContainer addChild(TestConfigurationContainer childContainer) {
+ childContainer.mParent = this;
+ childContainer.onParentChanged();
+ mChildren.add(childContainer);
+ return childContainer;
+ }
+
+ TestConfigurationContainer addChild() {
+ return addChild(new TestConfigurationContainer());
+ }
+
+ void removeChild(TestConfigurationContainer child) {
+ child.mParent = null;
+ child.onParentChanged();
+ }
+
+ @Override
+ protected int getChildCount() {
+ return mChildren.size();
+ }
+
+ @Override
+ protected TestConfigurationContainer getChildAt(int index) {
+ return mChildren.get(index);
+ }
+
+ @Override
+ protected ConfigurationContainer getParent() {
+ return mParent;
+ }
+ }
+
+ private class TestConfigurationChangeListener implements ConfigurationContainerListener {
+
+ final Configuration mOverrideConfiguration = new Configuration();
+
+ public void onOverrideConfigurationChanged(Configuration overrideConfiguration) {
+ mOverrideConfiguration.setTo(overrideConfiguration);
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/DimmerTests.java b/services/tests/servicestests/src/com/android/server/wm/DimmerTests.java
new file mode 100644
index 0000000..6769e40
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/DimmerTests.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.wm.DimmerTests;
+ */
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class DimmerTests extends WindowTestsBase {
+
+ private class TestWindowContainer extends WindowContainer<TestWindowContainer> {
+ final SurfaceControl mControl = mock(SurfaceControl.class);
+ final SurfaceControl.Transaction mTransaction = mock(SurfaceControl.Transaction.class);
+
+ TestWindowContainer() {
+ super(sWm);
+ }
+
+ @Override
+ public SurfaceControl getSurfaceControl() {
+ return mControl;
+ }
+
+ @Override
+ public SurfaceControl.Transaction getPendingTransaction() {
+ return mTransaction;
+ }
+ }
+
+ private class MockSurfaceBuildingContainer extends WindowContainer<TestWindowContainer> {
+ final SurfaceSession mSession = new SurfaceSession();
+ final SurfaceControl mHostControl = mock(SurfaceControl.class);
+ final SurfaceControl.Transaction mHostTransaction = mock(SurfaceControl.Transaction.class);
+
+ MockSurfaceBuildingContainer() {
+ super(sWm);
+ }
+
+ class MockSurfaceBuilder extends SurfaceControl.Builder {
+ MockSurfaceBuilder(SurfaceSession ss) {
+ super(ss);
+ }
+
+ @Override
+ public SurfaceControl build() {
+ return mock(SurfaceControl.class);
+ }
+ }
+
+ @Override
+ SurfaceControl.Builder makeChildSurface(WindowContainer child) {
+ return new MockSurfaceBuilder(mSession);
+ }
+
+ @Override
+ public SurfaceControl getSurfaceControl() {
+ return mHostControl;
+ }
+
+ @Override
+ public SurfaceControl.Transaction getPendingTransaction() {
+ return mHostTransaction;
+ }
+ }
+
+ private MockSurfaceBuildingContainer mHost;
+ private Dimmer mDimmer;
+ private SurfaceControl.Transaction mTransaction;
+ private Dimmer.SurfaceAnimatorStarter mSurfaceAnimatorStarter;
+
+ private static class SurfaceAnimatorStarterImpl implements Dimmer.SurfaceAnimatorStarter {
+ @Override
+ public void startAnimation(SurfaceAnimator surfaceAnimator, SurfaceControl.Transaction t,
+ AnimationAdapter anim, boolean hidden) {
+ surfaceAnimator.mAnimationFinishedCallback.run();
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mHost = new MockSurfaceBuildingContainer();
+ mSurfaceAnimatorStarter = spy(new SurfaceAnimatorStarterImpl());
+ mTransaction = mock(SurfaceControl.Transaction.class);
+ mDimmer = new Dimmer(mHost, mSurfaceAnimatorStarter);
+ }
+
+ @Test
+ public void testDimAboveNoChildCreatesSurface() throws Exception {
+ final float alpha = 0.8f;
+ mDimmer.dimAbove(mTransaction, alpha);
+
+ SurfaceControl dimLayer = getDimLayer();
+
+ assertNotNull("Dimmer should have created a surface", dimLayer);
+
+ verify(mTransaction).setAlpha(dimLayer, alpha);
+ verify(mTransaction).setLayer(dimLayer, Integer.MAX_VALUE);
+ }
+
+ @Test
+ public void testDimAboveNoChildRedundantlyUpdatesAlphaOnExistingSurface() throws Exception {
+ float alpha = 0.8f;
+ mDimmer.dimAbove(mTransaction, alpha);
+ final SurfaceControl firstSurface = getDimLayer();
+
+ alpha = 0.9f;
+ mDimmer.dimAbove(mTransaction, alpha);
+
+ assertEquals(firstSurface, getDimLayer());
+ verify(mTransaction).setAlpha(firstSurface, 0.9f);
+ }
+
+ @Test
+ public void testUpdateDimsAppliesSize() throws Exception {
+ mDimmer.dimAbove(mTransaction, 0.8f);
+
+ int width = 100;
+ int height = 300;
+ Rect bounds = new Rect(0, 0, width, height);
+ mDimmer.updateDims(mTransaction, bounds);
+
+ verify(mTransaction).setSize(getDimLayer(), width, height);
+ verify(mTransaction).show(getDimLayer());
+ }
+
+ @Test
+ public void testDimAboveNoChildNotReset() throws Exception {
+ mDimmer.dimAbove(mTransaction, 0.8f);
+ SurfaceControl dimLayer = getDimLayer();
+ mDimmer.resetDimStates();
+
+ mDimmer.updateDims(mTransaction, new Rect());
+ verify(mTransaction).show(getDimLayer());
+ verify(dimLayer, never()).destroy();
+ }
+
+ @Test
+ public void testDimAboveWithChildCreatesSurfaceAboveChild() throws Exception {
+ TestWindowContainer child = new TestWindowContainer();
+ mHost.addChild(child, 0);
+
+ final float alpha = 0.8f;
+ mDimmer.dimAbove(mTransaction, child, alpha);
+ SurfaceControl dimLayer = getDimLayer();
+
+ assertNotNull("Dimmer should have created a surface", dimLayer);
+
+ verify(mTransaction).setAlpha(dimLayer, alpha);
+ verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, 1);
+ }
+
+ @Test
+ public void testDimBelowWithChildSurfaceCreatesSurfaceBelowChild() throws Exception {
+ TestWindowContainer child = new TestWindowContainer();
+ mHost.addChild(child, 0);
+
+ final float alpha = 0.8f;
+ mDimmer.dimBelow(mTransaction, child, alpha);
+ SurfaceControl dimLayer = getDimLayer();
+
+ assertNotNull("Dimmer should have created a surface", dimLayer);
+
+ verify(mTransaction).setAlpha(dimLayer, alpha);
+ verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, -1);
+ }
+
+ @Test
+ public void testDimBelowWithChildSurfaceDestroyedWhenReset() throws Exception {
+ TestWindowContainer child = new TestWindowContainer();
+ mHost.addChild(child, 0);
+
+ final float alpha = 0.8f;
+ mDimmer.dimAbove(mTransaction, child, alpha);
+ SurfaceControl dimLayer = getDimLayer();
+ mDimmer.resetDimStates();
+
+ mDimmer.updateDims(mTransaction, new Rect());
+ verify(mSurfaceAnimatorStarter).startAnimation(any(SurfaceAnimator.class), any(
+ SurfaceControl.Transaction.class), any(AnimationAdapter.class), anyBoolean());
+ verify(dimLayer).destroy();
+ }
+
+ @Test
+ public void testDimBelowWithChildSurfaceNotDestroyedWhenPersisted() throws Exception {
+ TestWindowContainer child = new TestWindowContainer();
+ mHost.addChild(child, 0);
+
+ final float alpha = 0.8f;
+ mDimmer.dimAbove(mTransaction, child, alpha);
+ SurfaceControl dimLayer = getDimLayer();
+ mDimmer.resetDimStates();
+ mDimmer.dimAbove(mTransaction, child, alpha);
+
+ mDimmer.updateDims(mTransaction, new Rect());
+ verify(mTransaction).show(dimLayer);
+ verify(dimLayer, never()).destroy();
+ }
+
+ @Test
+ public void testDimUpdateWhileDimming() throws Exception {
+ Rect bounds = new Rect();
+ TestWindowContainer child = new TestWindowContainer();
+ mHost.addChild(child, 0);
+
+ final float alpha = 0.8f;
+ mDimmer.dimAbove(mTransaction, child, alpha);
+
+ SurfaceControl dimLayer = getDimLayer();
+ bounds.set(0, 0, 10, 10);
+ mDimmer.updateDims(mTransaction, bounds);
+ verify(mTransaction, times(1)).show(dimLayer);
+ verify(mTransaction).setSize(dimLayer, bounds.width(), bounds.height());
+ verify(mTransaction).setPosition(dimLayer, 0, 0);
+
+ bounds.set(10, 10, 30, 30);
+ mDimmer.updateDims(mTransaction, bounds);
+ verify(mTransaction).setSize(dimLayer, bounds.width(), bounds.height());
+ verify(mTransaction).setPosition(dimLayer, 10, 10);
+ }
+
+ @Test
+ public void testRemoveDimImmediately() throws Exception {
+ TestWindowContainer child = new TestWindowContainer();
+ mHost.addChild(child, 0);
+
+ mDimmer.dimAbove(mTransaction, child, 1);
+ SurfaceControl dimLayer = getDimLayer();
+ mDimmer.updateDims(mTransaction, new Rect());
+ verify(mTransaction, times(1)).show(dimLayer);
+
+ reset(mSurfaceAnimatorStarter);
+ mDimmer.dontAnimateExit();
+ mDimmer.resetDimStates();
+ mDimmer.updateDims(mTransaction, new Rect());
+ verify(mSurfaceAnimatorStarter, never()).startAnimation(any(SurfaceAnimator.class), any(
+ SurfaceControl.Transaction.class), any(AnimationAdapter.class), anyBoolean());
+ verify(mTransaction).destroy(dimLayer);
+ }
+
+ private SurfaceControl getDimLayer() {
+ return mDimmer.mDimState.mDimLayer;
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/servicestests/src/com/android/server/wm/DisplayContentTests.java
new file mode 100644
index 0000000..ac196f9
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/DisplayContentTests.java
@@ -0,0 +1,550 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.DisplayCutout.fromBoundingRect;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
+import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION;
+import static com.android.server.wm.WindowContainer.POSITION_TOP;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.annotation.SuppressLint;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.DisplayMetrics;
+import android.util.SparseIntArray;
+import android.view.DisplayCutout;
+import android.view.MotionEvent;
+import android.view.Surface;
+
+import com.android.server.wm.utils.WmDisplayCutout;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Tests for the {@link DisplayContent} class.
+ *
+ * Build/Install/Run:
+ * atest com.android.server.wm.DisplayContentTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class DisplayContentTests extends WindowTestsBase {
+
+ @Test
+ @FlakyTest(bugId = 77772044)
+ public void testForAllWindows() throws Exception {
+ final WindowState exitingAppWindow = createWindow(null, TYPE_BASE_APPLICATION,
+ mDisplayContent, "exiting app");
+ final AppWindowToken exitingAppToken = exitingAppWindow.mAppToken;
+ exitingAppToken.mIsExiting = true;
+ exitingAppToken.getTask().mStack.mExitingAppTokens.add(exitingAppToken);
+
+ assertForAllWindowsOrder(Arrays.asList(
+ mWallpaperWindow,
+ exitingAppWindow,
+ mChildAppWindowBelow,
+ mAppWindow,
+ mChildAppWindowAbove,
+ mDockedDividerWindow,
+ mStatusBarWindow,
+ mNavBarWindow,
+ mImeWindow,
+ mImeDialogWindow));
+ }
+
+ @Test
+ public void testForAllWindows_WithAppImeTarget() throws Exception {
+ final WindowState imeAppTarget =
+ createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, "imeAppTarget");
+
+ sWm.mInputMethodTarget = imeAppTarget;
+
+ assertForAllWindowsOrder(Arrays.asList(
+ mWallpaperWindow,
+ mChildAppWindowBelow,
+ mAppWindow,
+ mChildAppWindowAbove,
+ imeAppTarget,
+ mImeWindow,
+ mImeDialogWindow,
+ mDockedDividerWindow,
+ mStatusBarWindow,
+ mNavBarWindow));
+ }
+
+ @Test
+ public void testForAllWindows_WithChildWindowImeTarget() throws Exception {
+ sWm.mInputMethodTarget = mChildAppWindowAbove;
+
+ assertForAllWindowsOrder(Arrays.asList(
+ mWallpaperWindow,
+ mChildAppWindowBelow,
+ mAppWindow,
+ mChildAppWindowAbove,
+ mImeWindow,
+ mImeDialogWindow,
+ mDockedDividerWindow,
+ mStatusBarWindow,
+ mNavBarWindow));
+ }
+
+ @Test
+ public void testForAllWindows_WithStatusBarImeTarget() throws Exception {
+ sWm.mInputMethodTarget = mStatusBarWindow;
+
+ assertForAllWindowsOrder(Arrays.asList(
+ mWallpaperWindow,
+ mChildAppWindowBelow,
+ mAppWindow,
+ mChildAppWindowAbove,
+ mDockedDividerWindow,
+ mStatusBarWindow,
+ mImeWindow,
+ mImeDialogWindow,
+ mNavBarWindow));
+ }
+
+ @Test
+ public void testForAllWindows_WithInBetweenWindowToken() throws Exception {
+ // This window is set-up to be z-ordered between some windows that go in the same token like
+ // the nav bar and status bar.
+ final WindowState voiceInteractionWindow = createWindow(null, TYPE_VOICE_INTERACTION,
+ mDisplayContent, "voiceInteractionWindow");
+
+ assertForAllWindowsOrder(Arrays.asList(
+ mWallpaperWindow,
+ mChildAppWindowBelow,
+ mAppWindow,
+ mChildAppWindowAbove,
+ mDockedDividerWindow,
+ voiceInteractionWindow,
+ mStatusBarWindow,
+ mNavBarWindow,
+ mImeWindow,
+ mImeDialogWindow));
+ }
+
+ @Test
+ public void testComputeImeTarget() throws Exception {
+ // Verify that an app window can be an ime target.
+ final WindowState appWin = createWindow(null, TYPE_APPLICATION, mDisplayContent, "appWin");
+ appWin.setHasSurface(true);
+ assertTrue(appWin.canBeImeTarget());
+ WindowState imeTarget = mDisplayContent.computeImeTarget(false /* updateImeTarget */);
+ assertEquals(appWin, imeTarget);
+ appWin.mHidden = false;
+
+ // Verify that an child window can be an ime target.
+ final WindowState childWin = createWindow(appWin,
+ TYPE_APPLICATION_ATTACHED_DIALOG, "childWin");
+ childWin.setHasSurface(true);
+ assertTrue(childWin.canBeImeTarget());
+ imeTarget = mDisplayContent.computeImeTarget(false /* updateImeTarget */);
+ assertEquals(childWin, imeTarget);
+ }
+
+ /**
+ * This tests stack movement between displays and proper stack's, task's and app token's display
+ * container references updates.
+ */
+ @Test
+ public void testMoveStackBetweenDisplays() throws Exception {
+ // Create a second display.
+ final DisplayContent dc = createNewDisplay();
+
+ // Add stack with activity.
+ final TaskStack stack = createTaskStackOnDisplay(dc);
+ assertEquals(dc.getDisplayId(), stack.getDisplayContent().getDisplayId());
+ assertEquals(dc, stack.getParent().getParent());
+ assertEquals(dc, stack.getDisplayContent());
+
+ final Task task = createTaskInStack(stack, 0 /* userId */);
+ final WindowTestUtils.TestAppWindowToken token = WindowTestUtils.createTestAppWindowToken(
+ dc);
+ task.addChild(token, 0);
+ assertEquals(dc, task.getDisplayContent());
+ assertEquals(dc, token.getDisplayContent());
+
+ // Move stack to first display.
+ mDisplayContent.moveStackToDisplay(stack, true /* onTop */);
+ assertEquals(mDisplayContent.getDisplayId(), stack.getDisplayContent().getDisplayId());
+ assertEquals(mDisplayContent, stack.getParent().getParent());
+ assertEquals(mDisplayContent, stack.getDisplayContent());
+ assertEquals(mDisplayContent, task.getDisplayContent());
+ assertEquals(mDisplayContent, token.getDisplayContent());
+ }
+
+ /**
+ * This tests override configuration updates for display content.
+ */
+ @Test
+ public void testDisplayOverrideConfigUpdate() throws Exception {
+ final int displayId = mDisplayContent.getDisplayId();
+ final Configuration currentOverrideConfig = mDisplayContent.getOverrideConfiguration();
+
+ // Create new, slightly changed override configuration and apply it to the display.
+ final Configuration newOverrideConfig = new Configuration(currentOverrideConfig);
+ newOverrideConfig.densityDpi += 120;
+ newOverrideConfig.fontScale += 0.3;
+
+ sWm.setNewDisplayOverrideConfiguration(newOverrideConfig, displayId);
+
+ // Check that override config is applied.
+ assertEquals(newOverrideConfig, mDisplayContent.getOverrideConfiguration());
+ }
+
+ /**
+ * This tests global configuration updates when default display config is updated.
+ */
+ @Test
+ public void testDefaultDisplayOverrideConfigUpdate() throws Exception {
+ final Configuration currentConfig = mDisplayContent.getConfiguration();
+
+ // Create new, slightly changed override configuration and apply it to the display.
+ final Configuration newOverrideConfig = new Configuration(currentConfig);
+ newOverrideConfig.densityDpi += 120;
+ newOverrideConfig.fontScale += 0.3;
+
+ sWm.setNewDisplayOverrideConfiguration(newOverrideConfig, DEFAULT_DISPLAY);
+
+ // Check that global configuration is updated, as we've updated default display's config.
+ Configuration globalConfig = sWm.mRoot.getConfiguration();
+ assertEquals(newOverrideConfig.densityDpi, globalConfig.densityDpi);
+ assertEquals(newOverrideConfig.fontScale, globalConfig.fontScale, 0.1 /* delta */);
+
+ // Return back to original values.
+ sWm.setNewDisplayOverrideConfiguration(currentConfig, DEFAULT_DISPLAY);
+ globalConfig = sWm.mRoot.getConfiguration();
+ assertEquals(currentConfig.densityDpi, globalConfig.densityDpi);
+ assertEquals(currentConfig.fontScale, globalConfig.fontScale, 0.1 /* delta */);
+ }
+
+ /**
+ * Tests tapping on a stack in different display results in window gaining focus.
+ */
+ @Test
+ public void testInputEventBringsCorrectDisplayInFocus() throws Exception {
+ DisplayContent dc0 = sWm.getDefaultDisplayContentLocked();
+ // Create a second display
+ final DisplayContent dc1 = createNewDisplay();
+
+ // Add stack with activity.
+ final TaskStack stack0 = createTaskStackOnDisplay(dc0);
+ final Task task0 = createTaskInStack(stack0, 0 /* userId */);
+ final WindowTestUtils.TestAppWindowToken token =
+ WindowTestUtils.createTestAppWindowToken(dc0);
+ task0.addChild(token, 0);
+ dc0.mTapDetector = new TaskTapPointerEventListener(sWm, dc0);
+ sWm.registerPointerEventListener(dc0.mTapDetector);
+ final TaskStack stack1 = createTaskStackOnDisplay(dc1);
+ final Task task1 = createTaskInStack(stack1, 0 /* userId */);
+ final WindowTestUtils.TestAppWindowToken token1 =
+ WindowTestUtils.createTestAppWindowToken(dc0);
+ task1.addChild(token1, 0);
+ dc1.mTapDetector = new TaskTapPointerEventListener(sWm, dc0);
+ sWm.registerPointerEventListener(dc1.mTapDetector);
+
+ // tap on primary display (by sending ACTION_DOWN followed by ACTION_UP)
+ DisplayMetrics dm0 = dc0.getDisplayMetrics();
+ dc0.mTapDetector.onPointerEvent(
+ createTapEvent(dm0.widthPixels / 2, dm0.heightPixels / 2, true));
+ dc0.mTapDetector.onPointerEvent(
+ createTapEvent(dm0.widthPixels / 2, dm0.heightPixels / 2, false));
+
+ // Check focus is on primary display.
+ assertEquals(sWm.mCurrentFocus, dc0.findFocusedWindow());
+
+ // Tap on secondary display
+ DisplayMetrics dm1 = dc1.getDisplayMetrics();
+ dc1.mTapDetector.onPointerEvent(
+ createTapEvent(dm1.widthPixels / 2, dm1.heightPixels / 2, true));
+ dc1.mTapDetector.onPointerEvent(
+ createTapEvent(dm1.widthPixels / 2, dm1.heightPixels / 2, false));
+
+ // Check focus is on secondary.
+ assertEquals(sWm.mCurrentFocus, dc1.findFocusedWindow());
+ }
+
+ @Test
+ public void testFocusedWindowMultipleDisplays() throws Exception {
+ // Create a focusable window and check that focus is calculated correctly
+ final WindowState window1 =
+ createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, "window1");
+ assertEquals(window1, sWm.mRoot.computeFocusedWindow());
+
+ // Check that a new display doesn't affect focus
+ final DisplayContent dc = createNewDisplay();
+ assertEquals(window1, sWm.mRoot.computeFocusedWindow());
+
+ // Add a window to the second display, and it should be focused
+ final WindowState window2 = createWindow(null, TYPE_BASE_APPLICATION, dc, "window2");
+ assertEquals(window2, sWm.mRoot.computeFocusedWindow());
+
+ // Move the first window to the to including parents, and make sure focus is updated
+ window1.getParent().positionChildAt(POSITION_TOP, window1, true);
+ assertEquals(window1, sWm.mRoot.computeFocusedWindow());
+ }
+
+ @Test
+ public void testKeyguard_preventsSecondaryDisplayFocus() throws Exception {
+ final WindowState keyguard = createWindow(null, TYPE_STATUS_BAR,
+ sWm.getDefaultDisplayContentLocked(), "keyguard");
+ assertEquals(keyguard, sWm.mRoot.computeFocusedWindow());
+
+ // Add a window to a second display, and it should be focused
+ final DisplayContent dc = createNewDisplay();
+ final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, dc, "win");
+ assertEquals(win, sWm.mRoot.computeFocusedWindow());
+
+ mWmRule.getWindowManagerPolicy().keyguardShowingAndNotOccluded = true;
+ assertEquals(keyguard, sWm.mRoot.computeFocusedWindow());
+ }
+
+ /**
+ * This tests setting the maximum ui width on a display.
+ */
+ @Test
+ public void testMaxUiWidth() throws Exception {
+ final int baseWidth = 1440;
+ final int baseHeight = 2560;
+ final int baseDensity = 300;
+
+ mDisplayContent.updateBaseDisplayMetrics(baseWidth, baseHeight, baseDensity);
+
+ final int maxWidth = 300;
+ final int resultingHeight = (maxWidth * baseHeight) / baseWidth;
+ final int resultingDensity = (maxWidth * baseDensity) / baseWidth;
+
+ mDisplayContent.setMaxUiWidth(maxWidth);
+ verifySizes(mDisplayContent, maxWidth, resultingHeight, resultingDensity);
+
+ // Assert setting values again does not change;
+ mDisplayContent.updateBaseDisplayMetrics(baseWidth, baseHeight, baseDensity);
+ verifySizes(mDisplayContent, maxWidth, resultingHeight, resultingDensity);
+
+ final int smallerWidth = 200;
+ final int smallerHeight = 400;
+ final int smallerDensity = 100;
+
+ // Specify smaller dimension, verify that it is honored
+ mDisplayContent.updateBaseDisplayMetrics(smallerWidth, smallerHeight, smallerDensity);
+ verifySizes(mDisplayContent, smallerWidth, smallerHeight, smallerDensity);
+
+ // Verify that setting the max width to a greater value than the base width has no effect
+ mDisplayContent.setMaxUiWidth(maxWidth);
+ verifySizes(mDisplayContent, smallerWidth, smallerHeight, smallerDensity);
+ }
+
+ /**
+ * This test enforces that the pinned stack is always kept as the top stack.
+ */
+ @Test
+ public void testPinnedStackLocation() {
+ final TaskStack pinnedStack = createStackControllerOnStackOnDisplay(
+ WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD, mDisplayContent).mContainer;
+ // Ensure that the pinned stack is the top stack
+ assertEquals(pinnedStack, mDisplayContent.getPinnedStack());
+ assertEquals(pinnedStack, mDisplayContent.getTopStack());
+ // By default, this should try to create a new stack on top
+ final TaskStack otherStack = createTaskStackOnDisplay(mDisplayContent);
+ // Ensure that the other stack is on the display.
+ assertEquals(mDisplayContent, otherStack.getDisplayContent());
+ // Ensure that the pinned stack is still on top
+ assertEquals(pinnedStack, mDisplayContent.getTopStack());
+ }
+
+ /**
+ * Test that WM does not report displays to AM that are pending to be removed.
+ */
+ @Test
+ public void testDontReportDeferredRemoval() {
+ // Create a display and add an animating window to it.
+ final DisplayContent dc = createNewDisplay();
+ final WindowState window = createWindow(null /* parent */, TYPE_BASE_APPLICATION, dc, "w");
+ window.mAnimatingExit = true;
+ // Request display removal, it should be deferred.
+ dc.removeIfPossible();
+ // Request ordered display ids from WM.
+ final SparseIntArray orderedDisplayIds = new SparseIntArray();
+ sWm.getDisplaysInFocusOrder(orderedDisplayIds);
+ // Make sure that display that is marked for removal is not reported.
+ assertEquals(-1, orderedDisplayIds.indexOfValue(dc.getDisplayId()));
+ }
+
+ @Test
+ public void testDisplayCutout_rot0() throws Exception {
+ synchronized (sWm.getWindowManagerLock()) {
+ final DisplayContent dc = createNewDisplay();
+ dc.mInitialDisplayWidth = 200;
+ dc.mInitialDisplayHeight = 400;
+ Rect r = new Rect(80, 0, 120, 10);
+ final DisplayCutout cutout = new WmDisplayCutout(
+ fromBoundingRect(r.left, r.top, r.right, r.bottom), null)
+ .computeSafeInsets(200, 400).getDisplayCutout();
+
+ dc.mInitialDisplayCutout = cutout;
+ dc.setRotation(Surface.ROTATION_0);
+ dc.computeScreenConfiguration(new Configuration()); // recomputes dc.mDisplayInfo.
+
+ assertEquals(cutout, dc.getDisplayInfo().displayCutout);
+ }
+ }
+
+ @Test
+ public void testDisplayCutout_rot90() throws Exception {
+ synchronized (sWm.getWindowManagerLock()) {
+ final DisplayContent dc = createNewDisplay();
+ dc.mInitialDisplayWidth = 200;
+ dc.mInitialDisplayHeight = 400;
+ Rect r1 = new Rect(80, 0, 120, 10);
+ final DisplayCutout cutout = new WmDisplayCutout(
+ fromBoundingRect(r1.left, r1.top, r1.right, r1.bottom), null)
+ .computeSafeInsets(200, 400).getDisplayCutout();
+
+ dc.mInitialDisplayCutout = cutout;
+ dc.setRotation(Surface.ROTATION_90);
+ dc.computeScreenConfiguration(new Configuration()); // recomputes dc.mDisplayInfo.
+
+ final Rect r = new Rect(0, 80, 10, 120);
+ assertEquals(new WmDisplayCutout(
+ fromBoundingRect(r.left, r.top, r.right, r.bottom), null)
+ .computeSafeInsets(400, 200).getDisplayCutout(), dc.getDisplayInfo().displayCutout);
+ }
+ }
+
+ @Test
+ public void testLayoutSeq_assignedDuringLayout() throws Exception {
+ synchronized (sWm.getWindowManagerLock()) {
+
+ final DisplayContent dc = createNewDisplay();
+ final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, dc, "w");
+
+ dc.setLayoutNeeded();
+ dc.performLayout(true /* initial */, false /* updateImeWindows */);
+
+ assertThat(win.mLayoutSeq, is(dc.mLayoutSeq));
+ }
+ }
+
+ @Test
+ @SuppressLint("InlinedApi")
+ public void testOrientationDefinedByKeyguard() {
+ final DisplayContent dc = createNewDisplay();
+ // Create a window that requests landscape orientation. It will define device orientation
+ // by default.
+ final WindowState window = createWindow(null /* parent */, TYPE_BASE_APPLICATION, dc, "w");
+ window.mAppToken.setOrientation(SCREEN_ORIENTATION_LANDSCAPE);
+
+ final WindowState keyguard = createWindow(null, TYPE_STATUS_BAR, dc, "keyguard");
+ keyguard.mHasSurface = true;
+ keyguard.mAttrs.screenOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
+
+ assertEquals("Screen orientation must be defined by the app window by default",
+ SCREEN_ORIENTATION_LANDSCAPE, dc.getOrientation());
+
+ keyguard.mAttrs.screenOrientation = SCREEN_ORIENTATION_PORTRAIT;
+ assertEquals("Visible keyguard must influence device orientation",
+ SCREEN_ORIENTATION_PORTRAIT, dc.getOrientation());
+
+ sWm.setKeyguardGoingAway(true);
+ assertEquals("Keyguard that is going away must not influence device orientation",
+ SCREEN_ORIENTATION_LANDSCAPE, dc.getOrientation());
+ }
+
+ @Test
+ public void testDisableDisplayInfoOverrideFromWindowManager() {
+ final DisplayContent dc = createNewDisplay();
+
+ assertTrue(dc.mShouldOverrideDisplayConfiguration);
+ sWm.dontOverrideDisplayInfo(dc.getDisplayId());
+
+ assertFalse(dc.mShouldOverrideDisplayConfiguration);
+ verify(sWm.mDisplayManagerInternal, times(1))
+ .setDisplayInfoOverrideFromWindowManager(dc.getDisplayId(), null);
+ }
+
+ private static void verifySizes(DisplayContent displayContent, int expectedBaseWidth,
+ int expectedBaseHeight, int expectedBaseDensity) {
+ assertEquals(displayContent.mBaseDisplayWidth, expectedBaseWidth);
+ assertEquals(displayContent.mBaseDisplayHeight, expectedBaseHeight);
+ assertEquals(displayContent.mBaseDisplayDensity, expectedBaseDensity);
+ }
+
+ private void assertForAllWindowsOrder(List<WindowState> expectedWindowsBottomToTop) {
+ final LinkedList<WindowState> actualWindows = new LinkedList<>();
+
+ // Test forward traversal.
+ mDisplayContent.forAllWindows(actualWindows::addLast, false /* traverseTopToBottom */);
+ assertThat("bottomToTop", actualWindows, is(expectedWindowsBottomToTop));
+
+ actualWindows.clear();
+
+ // Test backward traversal.
+ mDisplayContent.forAllWindows(actualWindows::addLast, true /* traverseTopToBottom */);
+ assertThat("topToBottom", actualWindows, is(reverseList(expectedWindowsBottomToTop)));
+ }
+
+ private static List<WindowState> reverseList(List<WindowState> list) {
+ final ArrayList<WindowState> result = new ArrayList<>(list);
+ Collections.reverse(result);
+ return result;
+ }
+
+ private MotionEvent createTapEvent(float x, float y, boolean isDownEvent) {
+ final long downTime = SystemClock.uptimeMillis();
+ final long eventTime = SystemClock.uptimeMillis() + 100;
+ final int metaState = 0;
+
+ return MotionEvent.obtain(
+ downTime,
+ eventTime,
+ isDownEvent ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP,
+ x,
+ y,
+ metaState);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/DragDropControllerTests.java
new file mode 100644
index 0000000..a09656c
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/DragDropControllerTests.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.ClipData;
+import android.graphics.PixelFormat;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.os.UserManagerInternal;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.InputChannel;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+import android.view.View;
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.LocalServices;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for the {@link DragDropController} class.
+ *
+ * atest com.android.server.wm.DragDropControllerTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@Presubmit
+public class DragDropControllerTests extends WindowTestsBase {
+ private static final int TIMEOUT_MS = 3000;
+ private TestDragDropController mTarget;
+ private WindowState mWindow;
+ private IBinder mToken;
+
+ static class TestDragDropController extends DragDropController {
+ @GuardedBy("sWm.mWindowMap")
+ private Runnable mCloseCallback;
+
+ TestDragDropController(WindowManagerService service, Looper looper) {
+ super(service, looper);
+ }
+
+ void setOnClosedCallbackLocked(Runnable runnable) {
+ assertTrue(dragDropActiveLocked());
+ mCloseCallback = runnable;
+ }
+
+ @Override
+ void onDragStateClosedLocked(DragState dragState) {
+ super.onDragStateClosedLocked(dragState);
+ if (mCloseCallback != null) {
+ mCloseCallback.run();
+ mCloseCallback = null;
+ }
+ }
+ }
+
+ /**
+ * Creates a window state which can be used as a drop target.
+ */
+ private WindowState createDropTargetWindow(String name, int ownerId) {
+ final WindowTestUtils.TestAppWindowToken token = WindowTestUtils.createTestAppWindowToken(
+ mDisplayContent);
+ final TaskStack stack = createStackControllerOnStackOnDisplay(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, mDisplayContent).mContainer;
+ final Task task = createTaskInStack(stack, ownerId);
+ task.addChild(token, 0);
+
+ final WindowState window = createWindow(
+ null, TYPE_BASE_APPLICATION, token, name, ownerId, false);
+ window.mInputChannel = new InputChannel();
+ window.mHasSurface = true;
+ return window;
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ final UserManagerInternal userManager = mock(UserManagerInternal.class);
+ LocalServices.addService(UserManagerInternal.class, userManager);
+
+ super.setUp();
+
+ mTarget = new TestDragDropController(sWm, sWm.mH.getLooper());
+ mDisplayContent = spy(mDisplayContent);
+ mWindow = createDropTargetWindow("Drag test window", 0);
+ when(mDisplayContent.getTouchableWinAtPointLocked(0, 0)).thenReturn(mWindow);
+ when(sWm.mInputManager.transferTouchFocus(any(), any())).thenReturn(true);
+
+ synchronized (sWm.mWindowMap) {
+ sWm.mWindowMap.put(mWindow.mClient.asBinder(), mWindow);
+ }
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ LocalServices.removeServiceForTest(UserManagerInternal.class);
+ final CountDownLatch latch;
+ synchronized (sWm.mWindowMap) {
+ if (!mTarget.dragDropActiveLocked()) {
+ return;
+ }
+ if (mToken != null) {
+ mTarget.cancelDragAndDrop(mToken);
+ }
+ latch = new CountDownLatch(1);
+ mTarget.setOnClosedCallbackLocked(() -> {
+ latch.countDown();
+ });
+ }
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testDragFlow() throws Exception {
+ dragFlow(0, ClipData.newPlainText("label", "Test"), 0, 0);
+ }
+
+ @Test
+ public void testPerformDrag_NullDataWithGrantUri() throws Exception {
+ dragFlow(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, null, 0, 0);
+ }
+
+ @Test
+ public void testPerformDrag_NullDataToOtherUser() throws Exception {
+ final WindowState otherUsersWindow =
+ createDropTargetWindow("Other user's window", 1 * UserHandle.PER_USER_RANGE);
+ when(mDisplayContent.getTouchableWinAtPointLocked(10, 10))
+ .thenReturn(otherUsersWindow);
+
+ dragFlow(0, null, 10, 10);
+ }
+
+ private void dragFlow(int flag, ClipData data, float dropX, float dropY) {
+ final SurfaceSession appSession = new SurfaceSession();
+ try {
+ final SurfaceControl surface = new SurfaceControl.Builder(appSession)
+ .setName("drag surface")
+ .setSize(100, 100)
+ .setFormat(PixelFormat.TRANSLUCENT)
+ .build();
+
+ assertTrue(sWm.mInputManager.transferTouchFocus(null, null));
+ mToken = mTarget.performDrag(
+ new SurfaceSession(), 0, 0, mWindow.mClient, flag, surface, 0, 0, 0, 0, 0,
+ data);
+ assertNotNull(mToken);
+
+ mTarget.handleMotionEvent(false, dropX, dropY);
+ mToken = mWindow.mClient.asBinder();
+ } finally {
+ appSession.kill();
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/PinnedStackControllerTest.java b/services/tests/servicestests/src/com/android/server/wm/PinnedStackControllerTest.java
new file mode 100644
index 0000000..96745fa
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/PinnedStackControllerTest.java
@@ -0,0 +1,63 @@
+package com.android.server.wm;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.IPinnedStackListener;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class PinnedStackControllerTest extends WindowTestsBase {
+
+ @Mock private IPinnedStackListener mIPinnedStackListener;
+ @Mock private IPinnedStackListener.Stub mIPinnedStackListenerStub;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ when(mIPinnedStackListener.asBinder()).thenReturn(mIPinnedStackListenerStub);
+ }
+
+ @Test
+ public void setShelfHeight_shelfVisibilityChangedTriggered() throws RemoteException {
+ sWm.mSupportsPictureInPicture = true;
+ sWm.registerPinnedStackListener(DEFAULT_DISPLAY, mIPinnedStackListener);
+
+ verify(mIPinnedStackListener).onImeVisibilityChanged(false, 0);
+ verify(mIPinnedStackListener).onShelfVisibilityChanged(false, 0);
+ verify(mIPinnedStackListener).onMovementBoundsChanged(any(), any(), any(), eq(false),
+ eq(false), anyInt());
+ verify(mIPinnedStackListener).onActionsChanged(any());
+ verify(mIPinnedStackListener).onMinimizedStateChanged(anyBoolean());
+
+ reset(mIPinnedStackListener);
+
+ final int SHELF_HEIGHT = 300;
+
+ sWm.setShelfHeight(true, SHELF_HEIGHT);
+ verify(mIPinnedStackListener).onShelfVisibilityChanged(true, SHELF_HEIGHT);
+ verify(mIPinnedStackListener).onMovementBoundsChanged(any(), any(), any(), eq(false),
+ eq(true), anyInt());
+ verify(mIPinnedStackListener, never()).onImeVisibilityChanged(anyBoolean(), anyInt());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/RecentsAnimationControllerTest.java b/services/tests/servicestests/src/com/android/server/wm/RecentsAnimationControllerTest.java
new file mode 100644
index 0000000..a2af9b8
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/RecentsAnimationControllerTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static com.android.server.wm.RecentsAnimationController.REORDER_KEEP_IN_PLACE;
+import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_ORIGINAL_POSITION;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.os.Binder;
+import android.os.IInterface;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.IRecentsAnimationRunner;
+import android.view.SurfaceControl;
+import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * atest FrameworksServicesTests:com.android.server.wm.RecentsAnimationControllerTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class RecentsAnimationControllerTest extends WindowTestsBase {
+
+ @Mock SurfaceControl mMockLeash;
+ @Mock SurfaceControl.Transaction mMockTransaction;
+ @Mock OnAnimationFinishedCallback mFinishedCallback;
+ @Mock IRecentsAnimationRunner mMockRunner;
+ @Mock RecentsAnimationController.RecentsAnimationCallbacks mAnimationCallbacks;
+ private RecentsAnimationController mController;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ when(mMockRunner.asBinder()).thenReturn(new Binder());
+ mController = new RecentsAnimationController(sWm, mMockRunner, mAnimationCallbacks,
+ DEFAULT_DISPLAY);
+ }
+
+ @Test
+ public void testRemovedBeforeStarted_expectCanceled() throws Exception {
+ final AppWindowToken appWindow = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ AnimationAdapter adapter = mController.addAnimation(appWindow.getTask(),
+ false /* isRecentTaskInvisible */);
+ adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback);
+
+ // Remove the app window so that the animation target can not be created
+ appWindow.removeImmediately();
+ mController.startAnimation();
+
+ // Verify that the finish callback to reparent the leash is called
+ verify(mFinishedCallback).onAnimationFinished(eq(adapter));
+ // Verify the animation canceled callback to the app was made
+ verify(mMockRunner).onAnimationCanceled();
+ verifyNoMoreInteractionsExceptAsBinder(mMockRunner);
+ }
+
+ @Test
+ public void testCancelAfterRemove_expectIgnored() throws Exception {
+ final AppWindowToken appWindow = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ AnimationAdapter adapter = mController.addAnimation(appWindow.getTask(),
+ false /* isRecentTaskInvisible */);
+ adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback);
+
+ // Remove the app window so that the animation target can not be created
+ appWindow.removeImmediately();
+ mController.startAnimation();
+ mController.cleanupAnimation(REORDER_KEEP_IN_PLACE);
+ try {
+ mController.cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "test");
+ } catch (Exception e) {
+ fail("Unexpected failure when canceling animation after finishing it");
+ }
+ }
+
+ private static void verifyNoMoreInteractionsExceptAsBinder(IInterface binder) {
+ verify(binder, atLeast(0)).asBinder();
+ verifyNoMoreInteractions(binder);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/servicestests/src/com/android/server/wm/RemoteAnimationControllerTest.java
new file mode 100644
index 0000000..95361f0
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/RemoteAnimationControllerTest.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.os.IInterface;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+
+import com.android.server.testutils.OffsettableClock;
+import com.android.server.testutils.TestHandler;
+import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * atest FrameworksServicesTests:com.android.server.wm.RemoteAnimationControllerTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class RemoteAnimationControllerTest extends WindowTestsBase {
+
+ @Mock SurfaceControl mMockLeash;
+ @Mock Transaction mMockTransaction;
+ @Mock OnAnimationFinishedCallback mFinishedCallback;
+ @Mock IRemoteAnimationRunner mMockRunner;
+ private RemoteAnimationAdapter mAdapter;
+ private RemoteAnimationController mController;
+ private final OffsettableClock mClock = new OffsettableClock.Stopped();
+ private TestHandler mHandler;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ when(mMockRunner.asBinder()).thenReturn(new Binder());
+ mAdapter = new RemoteAnimationAdapter(mMockRunner, 100, 50);
+ mAdapter.setCallingPid(123);
+ sWm.mH.runWithScissors(() -> {
+ mHandler = new TestHandler(null, mClock);
+ }, 0);
+ mController = new RemoteAnimationController(sWm, mAdapter, mHandler);
+ }
+
+ @Test
+ public void testRun() throws Exception {
+ final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin");
+ sWm.mOpeningApps.add(win.mAppToken);
+ try {
+ final AnimationAdapter adapter = mController.createAnimationAdapter(win.mAppToken,
+ new Point(50, 100), new Rect(50, 100, 150, 150));
+ adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback);
+ mController.goodToGo();
+ sWm.mAnimator.executeAfterPrepareSurfacesRunnables();
+ final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
+ ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
+ final ArgumentCaptor<IRemoteAnimationFinishedCallback> finishedCaptor =
+ ArgumentCaptor.forClass(IRemoteAnimationFinishedCallback.class);
+ verify(mMockRunner).onAnimationStart(appsCaptor.capture(), finishedCaptor.capture());
+ assertEquals(1, appsCaptor.getValue().length);
+ final RemoteAnimationTarget app = appsCaptor.getValue()[0];
+ assertEquals(new Point(50, 100), app.position);
+ assertEquals(new Rect(50, 100, 150, 150), app.sourceContainerBounds);
+ assertEquals(win.mAppToken.getPrefixOrderIndex(), app.prefixOrderIndex);
+ assertEquals(win.mAppToken.getTask().mTaskId, app.taskId);
+ assertEquals(mMockLeash, app.leash);
+ assertEquals(win.mWinAnimator.mLastClipRect, app.clipRect);
+ assertEquals(false, app.isTranslucent);
+ verify(mMockTransaction).setLayer(mMockLeash, app.prefixOrderIndex);
+ verify(mMockTransaction).setPosition(mMockLeash, app.position.x, app.position.y);
+ verify(mMockTransaction).setWindowCrop(mMockLeash, new Rect(0, 0, 100, 50));
+
+ finishedCaptor.getValue().onAnimationFinished();
+ verify(mFinishedCallback).onAnimationFinished(eq(adapter));
+ } finally {
+ sWm.mOpeningApps.clear();
+ }
+ }
+
+ @Test
+ public void testCancel() throws Exception {
+ final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin");
+ final AnimationAdapter adapter = mController.createAnimationAdapter(win.mAppToken,
+ new Point(50, 100), new Rect(50, 100, 150, 150));
+ adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback);
+ mController.goodToGo();
+
+ adapter.onAnimationCancelled(mMockLeash);
+ verify(mMockRunner).onAnimationCancelled();
+ }
+
+ @Test
+ public void testTimeout() throws Exception {
+ final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin");
+ final AnimationAdapter adapter = mController.createAnimationAdapter(win.mAppToken,
+ new Point(50, 100), new Rect(50, 100, 150, 150));
+ adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback);
+ mController.goodToGo();
+
+ mClock.fastForward(2500);
+ mHandler.timeAdvance();
+
+ verify(mMockRunner).onAnimationCancelled();
+ verify(mFinishedCallback).onAnimationFinished(eq(adapter));
+ }
+
+ @Test
+ public void testTimeout_scaled() throws Exception {
+ sWm.setAnimationScale(2, 5.0f);
+ try{
+ final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin");
+ final AnimationAdapter adapter = mController.createAnimationAdapter(win.mAppToken,
+ new Point(50, 100), new Rect(50, 100, 150, 150));
+ adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback);
+ mController.goodToGo();
+
+ mClock.fastForward(2500);
+ mHandler.timeAdvance();
+
+ verify(mMockRunner, never()).onAnimationCancelled();
+
+ mClock.fastForward(10000);
+ mHandler.timeAdvance();
+
+ verify(mMockRunner).onAnimationCancelled();
+ verify(mFinishedCallback).onAnimationFinished(eq(adapter));
+ } finally {
+ sWm.setAnimationScale(2, 1.0f);
+ }
+
+ }
+
+ @Test
+ public void testZeroAnimations() throws Exception {
+ mController.goodToGo();
+ verifyNoMoreInteractionsExceptAsBinder(mMockRunner);
+ }
+
+ @Test
+ public void testNotReallyStarted() throws Exception {
+ final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin");
+ mController.createAnimationAdapter(win.mAppToken,
+ new Point(50, 100), new Rect(50, 100, 150, 150));
+ mController.goodToGo();
+ verifyNoMoreInteractionsExceptAsBinder(mMockRunner);
+ }
+
+ @Test
+ public void testOneNotStarted() throws Exception {
+ final WindowState win1 = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin1");
+ final WindowState win2 = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin2");
+ mController.createAnimationAdapter(win1.mAppToken,
+ new Point(50, 100), new Rect(50, 100, 150, 150));
+ final AnimationAdapter adapter = mController.createAnimationAdapter(win2.mAppToken,
+ new Point(50, 100), new Rect(50, 100, 150, 150));
+ adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback);
+ mController.goodToGo();
+ sWm.mAnimator.executeAfterPrepareSurfacesRunnables();
+ final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
+ ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
+ final ArgumentCaptor<IRemoteAnimationFinishedCallback> finishedCaptor =
+ ArgumentCaptor.forClass(IRemoteAnimationFinishedCallback.class);
+ verify(mMockRunner).onAnimationStart(appsCaptor.capture(), finishedCaptor.capture());
+ assertEquals(1, appsCaptor.getValue().length);
+ assertEquals(mMockLeash, appsCaptor.getValue()[0].leash);
+ }
+
+ @Test
+ public void testRemovedBeforeStarted() throws Exception {
+ final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin");
+ final AnimationAdapter adapter = mController.createAnimationAdapter(win.mAppToken,
+ new Point(50, 100), new Rect(50, 100, 150, 150));
+ adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback);
+ win.mAppToken.removeImmediately();
+ mController.goodToGo();
+ verifyNoMoreInteractionsExceptAsBinder(mMockRunner);
+ verify(mFinishedCallback).onAnimationFinished(eq(adapter));
+ }
+
+ private static void verifyNoMoreInteractionsExceptAsBinder(IInterface binder) {
+ verify(binder, atLeast(0)).asBinder();
+ verifyNoMoreInteractions(binder);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/servicestests/src/com/android/server/wm/RootWindowContainerTests.java
new file mode 100644
index 0000000..204e26c
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/RootWindowContainerTests.java
@@ -0,0 +1,52 @@
+package com.android.server.wm;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.content.res.Configuration;
+import android.graphics.Rect;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for the {@link RootWindowContainer} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.wm.RootWindowContainerTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class RootWindowContainerTests extends WindowTestsBase {
+ @Test
+ public void testSetDisplayOverrideConfigurationIfNeeded() throws Exception {
+ synchronized (sWm.mWindowMap) {
+ // Add first stack we expect to be updated with configuration change.
+ final TaskStack stack = createTaskStackOnDisplay(mDisplayContent);
+ stack.getOverrideConfiguration().windowConfiguration.setBounds(new Rect(0, 0, 5, 5));
+
+ // Add second task that will be set for deferred removal that should not be returned
+ // with the configuration change.
+ final TaskStack deferredDeletedStack = createTaskStackOnDisplay(mDisplayContent);
+ deferredDeletedStack.getOverrideConfiguration().windowConfiguration.setBounds(
+ new Rect(0, 0, 5, 5));
+ deferredDeletedStack.mDeferRemoval = true;
+
+ final Configuration override = new Configuration(
+ mDisplayContent.getOverrideConfiguration());
+ override.windowConfiguration.setBounds(new Rect(0, 0, 10, 10));
+
+ // Set display override.
+ final int[] results = sWm.mRoot.setDisplayOverrideConfigurationIfNeeded(override,
+ mDisplayContent.getDisplayId());
+
+ // Ensure only first stack is returned.
+ assertTrue(results.length == 1);
+ assertTrue(results[0] == stack.mStackId);
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/ScreenDecorWindowTests.java b/services/tests/servicestests/src/com/android/server/wm/ScreenDecorWindowTests.java
new file mode 100644
index 0000000..a2ccee4
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/ScreenDecorWindowTests.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.graphics.Color.RED;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Gravity.BOTTOM;
+import static android.view.Gravity.LEFT;
+import static android.view.Gravity.RIGHT;
+import static android.view.Gravity.TOP;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IS_SCREEN_DECOR;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+import static org.junit.Assert.assertEquals;
+
+import android.app.Activity;
+import android.app.ActivityOptions;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.VirtualDisplay;
+import android.media.ImageReader;
+import android.os.Handler;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Pair;
+import android.view.Display;
+import android.view.DisplayInfo;
+import android.view.View;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.function.BooleanSupplier;
+
+/**
+ * Tests for the {@link android.view.WindowManager.LayoutParams#PRIVATE_FLAG_IS_SCREEN_DECOR} flag.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.wm.ScreenDecorWindowTests
+ */
+// TODO: Add test for FLAG_FULLSCREEN which hides the status bar and also other flags.
+// TODO: Test non-Activity windows.
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class ScreenDecorWindowTests {
+
+ private final Context mContext = InstrumentationRegistry.getTargetContext();
+ private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
+
+ private WindowManager mWm;
+ private ArrayList<View> mWindows = new ArrayList<>();
+
+ private Activity mTestActivity;
+ private VirtualDisplay mDisplay;
+ private ImageReader mImageReader;
+
+ private int mDecorThickness;
+ private int mHalfDecorThickness;
+
+ @Before
+ public void setUp() {
+ final Pair<VirtualDisplay, ImageReader> result = createDisplay();
+ mDisplay = result.first;
+ mImageReader = result.second;
+ final Display display = mDisplay.getDisplay();
+ final Context dContext = mContext.createDisplayContext(display);
+ mWm = dContext.getSystemService(WindowManager.class);
+ mTestActivity = startActivityOnDisplay(TestActivity.class, display.getDisplayId());
+ final Point size = new Point();
+ mDisplay.getDisplay().getRealSize(size);
+ mDecorThickness = Math.min(size.x, size.y) / 3;
+ mHalfDecorThickness = mDecorThickness / 2;
+ }
+
+ @After
+ public void tearDown() {
+ while (!mWindows.isEmpty()) {
+ removeWindow(mWindows.get(0));
+ }
+ finishActivity(mTestActivity);
+ mDisplay.release();
+ mImageReader.close();
+ }
+
+ @Test
+ public void testScreenSides() throws Exception {
+ // Decor on top
+ final View decorWindow = createDecorWindow(TOP, MATCH_PARENT, mDecorThickness);
+ assertInsetGreaterOrEqual(mTestActivity, TOP, mDecorThickness);
+
+ // Decor at the bottom
+ updateWindow(decorWindow, BOTTOM, MATCH_PARENT, mDecorThickness, 0, 0);
+ assertInsetGreaterOrEqual(mTestActivity, BOTTOM, mDecorThickness);
+
+ // Decor to the left
+ updateWindow(decorWindow, LEFT, mDecorThickness, MATCH_PARENT, 0, 0);
+ assertInsetGreaterOrEqual(mTestActivity, LEFT, mDecorThickness);
+
+ // Decor to the right
+ updateWindow(decorWindow, RIGHT, mDecorThickness, MATCH_PARENT, 0, 0);
+ assertInsetGreaterOrEqual(mTestActivity, RIGHT, mDecorThickness);
+ }
+
+ @Test
+ public void testMultipleDecors() throws Exception {
+ // Test 2 decor windows on-top.
+ createDecorWindow(TOP, MATCH_PARENT, mHalfDecorThickness);
+ assertInsetGreaterOrEqual(mTestActivity, TOP, mHalfDecorThickness);
+ createDecorWindow(TOP, MATCH_PARENT, mDecorThickness);
+ assertInsetGreaterOrEqual(mTestActivity, TOP, mDecorThickness);
+
+ // And one at the bottom.
+ createDecorWindow(BOTTOM, MATCH_PARENT, mHalfDecorThickness);
+ assertInsetGreaterOrEqual(mTestActivity, TOP, mDecorThickness);
+ assertInsetGreaterOrEqual(mTestActivity, BOTTOM, mHalfDecorThickness);
+ }
+
+ @Test
+ public void testFlagChange() throws Exception {
+ WindowInsets initialInsets = getInsets(mTestActivity);
+
+ final View decorWindow = createDecorWindow(TOP, MATCH_PARENT, mDecorThickness);
+ assertTopInsetEquals(mTestActivity, mDecorThickness);
+
+ updateWindow(decorWindow, TOP, MATCH_PARENT, mDecorThickness,
+ 0, PRIVATE_FLAG_IS_SCREEN_DECOR);
+
+ // TODO: fix test and re-enable assertion.
+ // initialInsets was not actually immutable and just updated to the current insets,
+ // meaning this assertion never actually tested anything. Now that WindowInsets actually is
+ // immutable, it turns out the test was broken.
+ // assertTopInsetEquals(mTestActivity, initialInsets.getSystemWindowInsetTop());
+
+ updateWindow(decorWindow, TOP, MATCH_PARENT, mDecorThickness,
+ PRIVATE_FLAG_IS_SCREEN_DECOR, PRIVATE_FLAG_IS_SCREEN_DECOR);
+ assertTopInsetEquals(mTestActivity, mDecorThickness);
+ }
+
+ @Test
+ public void testRemoval() throws Exception {
+ WindowInsets initialInsets = getInsets(mTestActivity);
+
+ final View decorWindow = createDecorWindow(TOP, MATCH_PARENT, mDecorThickness);
+ assertInsetGreaterOrEqual(mTestActivity, TOP, mDecorThickness);
+
+ removeWindow(decorWindow);
+ assertTopInsetEquals(mTestActivity, initialInsets.getSystemWindowInsetTop());
+ }
+
+ private View createDecorWindow(int gravity, int width, int height) {
+ return createWindow("decorWindow", gravity, width, height, RED,
+ FLAG_LAYOUT_IN_SCREEN, PRIVATE_FLAG_IS_SCREEN_DECOR);
+ }
+
+ private View createWindow(String name, int gravity, int width, int height, int color, int flags,
+ int privateFlags) {
+
+ final View[] viewHolder = new View[1];
+ final int finalFlag = flags
+ | FLAG_NOT_FOCUSABLE | FLAG_WATCH_OUTSIDE_TOUCH | FLAG_NOT_TOUCHABLE;
+
+ // Needs to run on the UI thread.
+ Handler.getMain().runWithScissors(() -> {
+ final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ width, height, TYPE_APPLICATION_OVERLAY, finalFlag, PixelFormat.OPAQUE);
+ lp.gravity = gravity;
+ lp.privateFlags |= privateFlags;
+
+ final TextView view = new TextView(mContext);
+ view.setText("ScreenDecorWindowTests - " + name);
+ view.setBackgroundColor(color);
+ mWm.addView(view, lp);
+ mWindows.add(view);
+ viewHolder[0] = view;
+ }, 0);
+
+ waitForIdle();
+ return viewHolder[0];
+ }
+
+ private void updateWindow(View v, int gravity, int width, int height,
+ int privateFlags, int privateFlagsMask) {
+ // Needs to run on the UI thread.
+ Handler.getMain().runWithScissors(() -> {
+ final WindowManager.LayoutParams lp = (WindowManager.LayoutParams) v.getLayoutParams();
+ lp.gravity = gravity;
+ lp.width = width;
+ lp.height = height;
+ setPrivateFlags(lp, privateFlags, privateFlagsMask);
+
+ mWm.updateViewLayout(v, lp);
+ }, 0);
+
+ waitForIdle();
+ }
+
+ private void removeWindow(View v) {
+ Handler.getMain().runWithScissors(() -> mWm.removeView(v), 0);
+ mWindows.remove(v);
+ waitForIdle();
+ }
+
+ private WindowInsets getInsets(Activity a) {
+ return new WindowInsets(a.getWindow().getDecorView().getRootWindowInsets());
+ }
+
+ /**
+ * Set the flags of the window, as per the
+ * {@link WindowManager.LayoutParams WindowManager.LayoutParams}
+ * flags.
+ *
+ * @param flags The new window flags (see WindowManager.LayoutParams).
+ * @param mask Which of the window flag bits to modify.
+ */
+ public void setPrivateFlags(WindowManager.LayoutParams lp, int flags, int mask) {
+ lp.flags = (lp.flags & ~mask) | (flags & mask);
+ }
+
+ /**
+ * Asserts the top inset of {@param activity} is equal to {@param expected} waiting as needed.
+ */
+ private void assertTopInsetEquals(Activity activity, int expected) throws Exception {
+ waitFor(() -> getInsets(activity).getSystemWindowInsetTop() == expected);
+ assertEquals(expected, getInsets(activity).getSystemWindowInsetTop());
+ }
+
+ /**
+ * Asserts the inset at {@param side} of {@param activity} is equal to {@param expected}
+ * waiting as needed.
+ */
+ private void assertInsetGreaterOrEqual(Activity activity, int side, int expected)
+ throws Exception {
+ waitFor(() -> {
+ final WindowInsets insets = getInsets(activity);
+ switch (side) {
+ case TOP: return insets.getSystemWindowInsetTop() >= expected;
+ case BOTTOM: return insets.getSystemWindowInsetBottom() >= expected;
+ case LEFT: return insets.getSystemWindowInsetLeft() >= expected;
+ case RIGHT: return insets.getSystemWindowInsetRight() >= expected;
+ default: return true;
+ }
+ });
+
+ final WindowInsets insets = getInsets(activity);
+ switch (side) {
+ case TOP: assertGreaterOrEqual(insets.getSystemWindowInsetTop(), expected); break;
+ case BOTTOM: assertGreaterOrEqual(insets.getSystemWindowInsetBottom(), expected); break;
+ case LEFT: assertGreaterOrEqual(insets.getSystemWindowInsetLeft(), expected); break;
+ case RIGHT: assertGreaterOrEqual(insets.getSystemWindowInsetRight(), expected); break;
+ }
+ }
+
+ /** Asserts that the first entry is greater than or equal to the second entry. */
+ private void assertGreaterOrEqual(int first, int second) throws Exception {
+ Assert.assertTrue("Excepted " + first + " >= " + second, first >= second);
+ }
+
+ private void waitFor(BooleanSupplier waitCondition) {
+ int retriesLeft = 5;
+ do {
+ if (waitCondition.getAsBoolean()) {
+ break;
+ }
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ // Well I guess we are not waiting...
+ }
+ } while (retriesLeft-- > 0);
+ }
+
+ private void finishActivity(Activity a) {
+ if (a == null) {
+ return;
+ }
+ a.finish();
+ waitForIdle();
+ }
+
+ private void waitForIdle() {
+ mInstrumentation.waitForIdleSync();
+ }
+
+ private Activity startActivityOnDisplay(Class<?> cls, int displayId) {
+ final Intent intent = new Intent(mContext, cls);
+ intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
+ final ActivityOptions options = ActivityOptions.makeBasic();
+ options.setLaunchDisplayId(displayId);
+ final Activity activity = mInstrumentation.startActivitySync(intent, options.toBundle());
+ waitForIdle();
+
+ assertEquals(displayId, activity.getDisplay().getDisplayId());
+ return activity;
+ }
+
+ private Pair<VirtualDisplay, ImageReader> createDisplay() {
+ final DisplayManager dm = mContext.getSystemService(DisplayManager.class);
+ final DisplayInfo displayInfo = new DisplayInfo();
+ final Display defaultDisplay = dm.getDisplay(DEFAULT_DISPLAY);
+ defaultDisplay.getDisplayInfo(displayInfo);
+ final String name = "ScreenDecorWindowTests";
+ int flags = VIRTUAL_DISPLAY_FLAG_PRESENTATION | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
+
+ final ImageReader imageReader = ImageReader.newInstance(
+ displayInfo.logicalWidth, displayInfo.logicalHeight, PixelFormat.RGBA_8888, 2);
+
+ final VirtualDisplay display = dm.createVirtualDisplay(name, displayInfo.logicalWidth,
+ displayInfo.logicalHeight, displayInfo.logicalDensityDpi, imageReader.getSurface(),
+ flags);
+
+ return Pair.create(display, imageReader);
+ }
+
+ public static class TestActivity extends Activity {
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/StackWindowControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/StackWindowControllerTests.java
new file mode 100644
index 0000000..ab0a2bd
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/StackWindowControllerTests.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import android.graphics.Rect;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test class for {@link StackWindowController}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:StackWindowControllerTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class StackWindowControllerTests extends WindowTestsBase {
+ @Test
+ public void testRemoveContainer() throws Exception {
+ final StackWindowController stackController =
+ createStackControllerOnDisplay(mDisplayContent);
+ final WindowTestUtils.TestTaskWindowContainerController taskController =
+ new WindowTestUtils.TestTaskWindowContainerController(stackController);
+
+ final TaskStack stack = stackController.mContainer;
+ final Task task = taskController.mContainer;
+ assertNotNull(stack);
+ assertNotNull(task);
+ stackController.removeContainer();
+ // Assert that the container was removed.
+ assertNull(stackController.mContainer);
+ assertNull(taskController.mContainer);
+ assertNull(stack.getDisplayContent());
+ assertNull(task.getDisplayContent());
+ assertNull(task.mStack);
+ }
+
+ @Test
+ public void testRemoveContainer_deferRemoval() throws Exception {
+ final StackWindowController stackController =
+ createStackControllerOnDisplay(mDisplayContent);
+ final WindowTestUtils.TestTaskWindowContainerController taskController =
+ new WindowTestUtils.TestTaskWindowContainerController(stackController);
+
+ final TaskStack stack = stackController.mContainer;
+ final WindowTestUtils.TestTask task = (WindowTestUtils.TestTask) taskController.mContainer;
+ // Stack removal is deferred if one of its child is animating.
+ task.setLocalIsAnimating(true);
+
+ stackController.removeContainer();
+ // For the case of deferred removal the stack controller will no longer be connected to the
+ // container, but the task controller will still be connected to the its container until
+ // the stack window container is removed.
+ assertNull(stackController.mContainer);
+ assertNull(stack.getController());
+ assertNotNull(taskController.mContainer);
+ assertNotNull(task.getController());
+
+ stack.removeImmediately();
+ assertNull(taskController.mContainer);
+ assertNull(task.getController());
+ }
+
+ @Test
+ public void testReparent() throws Exception {
+ // Create first stack on primary display.
+ final StackWindowController stack1Controller =
+ createStackControllerOnDisplay(mDisplayContent);
+ final TaskStack stack1 = stack1Controller.mContainer;
+ final WindowTestUtils.TestTaskWindowContainerController taskController =
+ new WindowTestUtils.TestTaskWindowContainerController(stack1Controller);
+ final WindowTestUtils.TestTask task1 = (WindowTestUtils.TestTask) taskController.mContainer;
+ task1.mOnDisplayChangedCalled = false;
+
+ // Create second display and put second stack on it.
+ final DisplayContent dc = createNewDisplay();
+ final StackWindowController stack2Controller =
+ createStackControllerOnDisplay(dc);
+ final TaskStack stack2 = stack2Controller.mContainer;
+
+ // Reparent
+ stack1Controller.reparent(dc.getDisplayId(), new Rect(), true /* onTop */);
+ assertEquals(dc, stack1.getDisplayContent());
+ final int stack1PositionInParent = stack1.getParent().mChildren.indexOf(stack1);
+ final int stack2PositionInParent = stack1.getParent().mChildren.indexOf(stack2);
+ assertEquals(stack1PositionInParent, stack2PositionInParent + 1);
+ assertTrue(task1.mOnDisplayChangedCalled);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java b/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java
new file mode 100644
index 0000000..edac8a5
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.animation.AnimationHandler;
+import android.animation.AnimationHandler.AnimationFrameCallbackProvider;
+import android.animation.ValueAnimator;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Log;
+import android.view.Choreographer;
+import android.view.Choreographer.FrameCallback;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+import android.view.animation.Animation;
+import android.view.animation.TranslateAnimation;
+
+import com.android.server.wm.LocalAnimationAdapter.AnimationSpec;
+import com.android.server.wm.SurfaceAnimationRunner.AnimatorFactory;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * Test class for {@link SurfaceAnimationRunner}.
+ *
+ * atest FrameworksServicesTests:com.android.server.wm.SurfaceAnimationRunnerTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class SurfaceAnimationRunnerTest extends WindowTestsBase {
+
+ @Mock SurfaceControl mMockSurface;
+ @Mock Transaction mMockTransaction;
+ @Mock AnimationSpec mMockAnimationSpec;
+ @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ private SurfaceAnimationRunner mSurfaceAnimationRunner;
+ private CountDownLatch mFinishCallbackLatch;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mFinishCallbackLatch = new CountDownLatch(1);
+ mSurfaceAnimationRunner = new SurfaceAnimationRunner(null /* callbackProvider */, null,
+ mMockTransaction);
+ }
+
+ private void finishedCallback() {
+ mFinishCallbackLatch.countDown();
+ }
+
+ @Test
+ public void testAnimation() throws Exception {
+ mSurfaceAnimationRunner
+ .startAnimation(createTranslateAnimation(), mMockSurface, mMockTransaction,
+ this::finishedCallback);
+
+ // Ensure that the initial transformation has been applied.
+ final Matrix m = new Matrix();
+ m.setTranslate(-10, 0);
+ verify(mMockTransaction, atLeastOnce()).setMatrix(eq(mMockSurface), eq(m), any());
+ verify(mMockTransaction, atLeastOnce()).setAlpha(eq(mMockSurface), eq(1.0f));
+
+ mFinishCallbackLatch.await(1, SECONDS);
+ assertFinishCallbackCalled();
+
+ m.setTranslate(10, 0);
+ verify(mMockTransaction, atLeastOnce()).setMatrix(eq(mMockSurface), eq(m), any());
+
+ // At least 3 times: After initialization, first frame, last frame.
+ verify(mMockTransaction, atLeast(3)).setAlpha(eq(mMockSurface), eq(1.0f));
+ }
+
+ @Test
+ public void testCancel_notStarted() throws Exception {
+ mSurfaceAnimationRunner = new SurfaceAnimationRunner(new NoOpFrameCallbackProvider(), null,
+ mMockTransaction);
+ mSurfaceAnimationRunner
+ .startAnimation(createTranslateAnimation(), mMockSurface, mMockTransaction,
+ this::finishedCallback);
+ mSurfaceAnimationRunner.onAnimationCancelled(mMockSurface);
+ waitUntilHandlersIdle();
+ assertTrue(mSurfaceAnimationRunner.mPendingAnimations.isEmpty());
+ assertFinishCallbackNotCalled();
+ }
+
+ @Test
+ public void testCancel_running() throws Exception {
+ mSurfaceAnimationRunner = new SurfaceAnimationRunner(new NoOpFrameCallbackProvider(), null,
+ mMockTransaction);
+ mSurfaceAnimationRunner.startAnimation(createTranslateAnimation(), mMockSurface,
+ mMockTransaction, this::finishedCallback);
+ waitUntilNextFrame();
+ assertFalse(mSurfaceAnimationRunner.mRunningAnimations.isEmpty());
+ mSurfaceAnimationRunner.onAnimationCancelled(mMockSurface);
+ assertTrue(mSurfaceAnimationRunner.mRunningAnimations.isEmpty());
+ waitUntilHandlersIdle();
+ assertFinishCallbackNotCalled();
+ }
+
+ @FlakyTest(bugId = 71719744)
+ @Test
+ public void testCancel_sneakyCancelBeforeUpdate() throws Exception {
+ mSurfaceAnimationRunner = new SurfaceAnimationRunner(null, () -> new ValueAnimator() {
+ {
+ setFloatValues(0f, 1f);
+ }
+
+ @Override
+ public void addUpdateListener(AnimatorUpdateListener listener) {
+ super.addUpdateListener(animation -> {
+ // Sneaky test cancels animation just before applying frame to simulate
+ // interleaving of multiple threads. Muahahaha
+ if (animation.getCurrentPlayTime() > 0) {
+ mSurfaceAnimationRunner.onAnimationCancelled(mMockSurface);
+ }
+ listener.onAnimationUpdate(animation);
+ });
+ }
+ }, mMockTransaction);
+ when(mMockAnimationSpec.getDuration()).thenReturn(200L);
+ mSurfaceAnimationRunner.startAnimation(mMockAnimationSpec, mMockSurface, mMockTransaction,
+ this::finishedCallback);
+
+ // We need to wait for two frames: The first frame starts the animation, the second frame
+ // actually cancels the animation.
+ waitUntilNextFrame();
+ waitUntilNextFrame();
+ assertTrue(mSurfaceAnimationRunner.mRunningAnimations.isEmpty());
+ verify(mMockAnimationSpec, atLeastOnce()).apply(any(), any(), eq(0L));
+ }
+
+ @FlakyTest(bugId = 74780584)
+ @Test
+ public void testDeferStartingAnimations() throws Exception {
+ mSurfaceAnimationRunner.deferStartingAnimations();
+ mSurfaceAnimationRunner.startAnimation(createTranslateAnimation(), mMockSurface,
+ mMockTransaction, this::finishedCallback);
+ waitUntilNextFrame();
+ assertTrue(mSurfaceAnimationRunner.mRunningAnimations.isEmpty());
+ mSurfaceAnimationRunner.continueStartingAnimations();
+ waitUntilNextFrame();
+ assertFalse(mSurfaceAnimationRunner.mRunningAnimations.isEmpty());
+ mFinishCallbackLatch.await(1, SECONDS);
+ assertFinishCallbackCalled();
+ }
+
+ private void waitUntilNextFrame() throws Exception {
+ final CountDownLatch latch = new CountDownLatch(1);
+ mSurfaceAnimationRunner.mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT,
+ latch::countDown, null /* token */);
+ latch.await();
+ }
+
+ private void assertFinishCallbackCalled() {
+ assertEquals(0, mFinishCallbackLatch.getCount());
+ }
+
+ private void assertFinishCallbackNotCalled() {
+ assertEquals(1, mFinishCallbackLatch.getCount());
+ }
+
+ private AnimationSpec createTranslateAnimation() {
+ final Animation a = new TranslateAnimation(-10, 10, 0, 0);
+ a.initialize(0, 0, 0, 0);
+ a.setDuration(50);
+ return new WindowAnimationSpec(a, new Point(0, 0), false /* canSkipFirstFrame */);
+ }
+
+ /**
+ * Callback provider that doesn't animate at all.
+ */
+ private static final class NoOpFrameCallbackProvider implements AnimationFrameCallbackProvider {
+
+ @Override
+ public void postFrameCallback(FrameCallback callback) {
+ }
+
+ @Override
+ public void postCommitCallback(Runnable runnable) {
+ }
+
+ @Override
+ public long getFrameTime() {
+ return 0;
+ }
+
+ @Override
+ public long getFrameDelay() {
+ return 0;
+ }
+
+ @Override
+ public void setFrameDelay(long delay) {
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimatorTest.java b/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimatorTest.java
new file mode 100644
index 0000000..16b8458
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimatorTest.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Builder;
+import android.view.SurfaceControl.Transaction;
+import android.view.SurfaceSession;
+
+import com.android.server.wm.SurfaceAnimator.Animatable;
+import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+
+/**
+ * Test class for {@link SurfaceAnimatorTest}.
+ *
+ * atest FrameworksServicesTests:com.android.server.wm.SurfaceAnimatorTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class SurfaceAnimatorTest extends WindowTestsBase {
+
+ @Mock AnimationAdapter mSpec;
+ @Mock AnimationAdapter mSpec2;
+ @Mock Transaction mTransaction;
+
+ private SurfaceSession mSession = new SurfaceSession();
+ private MyAnimatable mAnimatable;
+ private MyAnimatable mAnimatable2;
+ private DeferFinishAnimatable mDeferFinishAnimatable;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ mAnimatable = new MyAnimatable();
+ mAnimatable2 = new MyAnimatable();
+ mDeferFinishAnimatable = new DeferFinishAnimatable();
+ }
+
+ @Test
+ public void testRunAnimation() throws Exception {
+ mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+ final ArgumentCaptor<OnAnimationFinishedCallback> callbackCaptor = ArgumentCaptor.forClass(
+ OnAnimationFinishedCallback.class);
+ assertAnimating(mAnimatable);
+ verify(mTransaction).reparent(eq(mAnimatable.mSurface), eq(mAnimatable.mLeash.getHandle()));
+ verify(mSpec).startAnimation(any(), any(), callbackCaptor.capture());
+
+ callbackCaptor.getValue().onAnimationFinished(mSpec);
+ assertNotAnimating(mAnimatable);
+ assertTrue(mAnimatable.mFinishedCallbackCalled);
+ verify(mTransaction).destroy(eq(mAnimatable.mLeash));
+ // TODO: Verify reparenting once we use mPendingTransaction to reparent it back
+ }
+
+ @Test
+ public void testOverrideAnimation() throws Exception {
+ mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+ final SurfaceControl firstLeash = mAnimatable.mLeash;
+ mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec2, true /* hidden */);
+
+ verify(mTransaction).destroy(eq(firstLeash));
+ assertFalse(mAnimatable.mFinishedCallbackCalled);
+
+ final ArgumentCaptor<OnAnimationFinishedCallback> callbackCaptor = ArgumentCaptor.forClass(
+ OnAnimationFinishedCallback.class);
+ assertAnimating(mAnimatable);
+ verify(mSpec).startAnimation(any(), any(), callbackCaptor.capture());
+
+ // First animation was finished, but this shouldn't cancel the second animation
+ callbackCaptor.getValue().onAnimationFinished(mSpec);
+ assertTrue(mAnimatable.mSurfaceAnimator.isAnimating());
+
+ // Second animation was finished
+ verify(mSpec2).startAnimation(any(), any(), callbackCaptor.capture());
+ callbackCaptor.getValue().onAnimationFinished(mSpec2);
+ assertNotAnimating(mAnimatable);
+ assertTrue(mAnimatable.mFinishedCallbackCalled);
+ }
+
+ @Test
+ public void testCancelAnimation() throws Exception {
+ mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+ assertAnimating(mAnimatable);
+ mAnimatable.mSurfaceAnimator.cancelAnimation();
+ assertNotAnimating(mAnimatable);
+ verify(mSpec).onAnimationCancelled(any());
+ assertTrue(mAnimatable.mFinishedCallbackCalled);
+ verify(mTransaction).destroy(eq(mAnimatable.mLeash));
+ }
+
+ @Test
+ public void testDelayingAnimationStart() throws Exception {
+ mAnimatable.mSurfaceAnimator.startDelayingAnimationStart();
+ mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+ verifyZeroInteractions(mSpec);
+ assertAnimating(mAnimatable);
+ assertTrue(mAnimatable.mSurfaceAnimator.isAnimationStartDelayed());
+ mAnimatable.mSurfaceAnimator.endDelayingAnimationStart();
+ verify(mSpec).startAnimation(any(), any(), any());
+ }
+
+ @Test
+ public void testDelayingAnimationStartAndCancelled() throws Exception {
+ mAnimatable.mSurfaceAnimator.startDelayingAnimationStart();
+ mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+ mAnimatable.mSurfaceAnimator.cancelAnimation();
+ verifyZeroInteractions(mSpec);
+ assertNotAnimating(mAnimatable);
+ assertTrue(mAnimatable.mFinishedCallbackCalled);
+ verify(mTransaction).destroy(eq(mAnimatable.mLeash));
+ }
+
+ @Test
+ public void testTransferAnimation() throws Exception {
+ mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+
+ final ArgumentCaptor<OnAnimationFinishedCallback> callbackCaptor = ArgumentCaptor.forClass(
+ OnAnimationFinishedCallback.class);
+ verify(mSpec).startAnimation(any(), any(), callbackCaptor.capture());
+ final SurfaceControl leash = mAnimatable.mLeash;
+
+ mAnimatable2.mSurfaceAnimator.transferAnimation(mAnimatable.mSurfaceAnimator);
+ assertNotAnimating(mAnimatable);
+ assertAnimating(mAnimatable2);
+ assertEquals(leash, mAnimatable2.mSurfaceAnimator.mLeash);
+ verify(mTransaction, never()).destroy(eq(leash));
+ callbackCaptor.getValue().onAnimationFinished(mSpec);
+ assertNotAnimating(mAnimatable2);
+ assertTrue(mAnimatable2.mFinishedCallbackCalled);
+ verify(mTransaction).destroy(eq(leash));
+ }
+
+ @Test
+ @FlakyTest(detail = "Promote once confirmed non-flaky")
+ public void testDeferFinish() throws Exception {
+
+ // Start animation
+ mDeferFinishAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec,
+ true /* hidden */);
+ final ArgumentCaptor<OnAnimationFinishedCallback> callbackCaptor = ArgumentCaptor.forClass(
+ OnAnimationFinishedCallback.class);
+ assertAnimating(mDeferFinishAnimatable);
+ verify(mSpec).startAnimation(any(), any(), callbackCaptor.capture());
+
+ // Finish the animation but then make sure we are deferring.
+ callbackCaptor.getValue().onAnimationFinished(mSpec);
+ assertAnimating(mDeferFinishAnimatable);
+
+ // Now end defer finishing.
+ mDeferFinishAnimatable.endDeferFinishCallback.run();
+ assertNotAnimating(mAnimatable2);
+ assertTrue(mDeferFinishAnimatable.mFinishedCallbackCalled);
+ verify(mTransaction).destroy(eq(mDeferFinishAnimatable.mLeash));
+ }
+
+ private void assertAnimating(MyAnimatable animatable) {
+ assertTrue(animatable.mSurfaceAnimator.isAnimating());
+ assertNotNull(animatable.mSurfaceAnimator.getAnimation());
+ }
+
+ private void assertNotAnimating(MyAnimatable animatable) {
+ assertFalse(animatable.mSurfaceAnimator.isAnimating());
+ assertNull(animatable.mSurfaceAnimator.getAnimation());
+ }
+
+ private class MyAnimatable implements Animatable {
+
+ final SurfaceControl mParent;
+ final SurfaceControl mSurface;
+ final SurfaceAnimator mSurfaceAnimator;
+ SurfaceControl mLeash;
+ boolean mFinishedCallbackCalled;
+
+ MyAnimatable() {
+ mParent = sWm.makeSurfaceBuilder(mSession)
+ .setName("test surface parent")
+ .setSize(3000, 3000)
+ .build();
+ mSurface = sWm.makeSurfaceBuilder(mSession)
+ .setName("test surface")
+ .setSize(1, 1)
+ .build();
+ mFinishedCallbackCalled = false;
+ mLeash = null;
+ mSurfaceAnimator = new SurfaceAnimator(this, mFinishedCallback, sWm);
+ }
+
+ @Override
+ public Transaction getPendingTransaction() {
+ return mTransaction;
+ }
+
+ @Override
+ public void commitPendingTransaction() {
+ }
+
+ @Override
+ public void onAnimationLeashCreated(Transaction t, SurfaceControl leash) {
+ }
+
+ @Override
+ public void onAnimationLeashDestroyed(Transaction t) {
+ }
+
+ @Override
+ public Builder makeAnimationLeash() {
+ return new SurfaceControl.Builder(mSession) {
+
+ @Override
+ public SurfaceControl build() {
+ mLeash = super.build();
+ return mLeash;
+ }
+ }.setParent(mParent);
+ }
+
+ @Override
+ public SurfaceControl getAnimationLeashParent() {
+ return mParent;
+ }
+
+ @Override
+ public SurfaceControl getSurfaceControl() {
+ return mSurface;
+ }
+
+ @Override
+ public SurfaceControl getParentSurfaceControl() {
+ return mParent;
+ }
+
+ @Override
+ public int getSurfaceWidth() {
+ return 1;
+ }
+
+ @Override
+ public int getSurfaceHeight() {
+ return 1;
+ }
+
+ private final Runnable mFinishedCallback = () -> mFinishedCallbackCalled = true;
+ }
+
+ private class DeferFinishAnimatable extends MyAnimatable {
+
+ Runnable endDeferFinishCallback;
+
+ @Override
+ public boolean shouldDeferAnimationFinish(Runnable endDeferFinishCallback) {
+ this.endDeferFinishCallback = endDeferFinishCallback;
+ return true;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskPositionerTests.java b/services/tests/servicestests/src/com/android/server/wm/TaskPositionerTests.java
new file mode 100644
index 0000000..7bf7dd7
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskPositionerTests.java
@@ -0,0 +1,480 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.graphics.Rect;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Display;
+
+import static com.android.server.wm.TaskPositioner.MIN_ASPECT;
+import static com.android.server.wm.WindowManagerService.dipToPixel;
+import static com.android.server.wm.WindowState.MINIMUM_VISIBLE_HEIGHT_IN_DP;
+import static com.android.server.wm.WindowState.MINIMUM_VISIBLE_WIDTH_IN_DP;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for the {@link TaskPositioner} class.
+ *
+ * runtest frameworks-services -c com.android.server.wm.TaskPositionerTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TaskPositionerTests extends WindowTestsBase {
+
+ private final boolean DEBUGGING = false;
+ private final String TAG = "TaskPositionerTest";
+
+ private final static int MOUSE_DELTA_X = 5;
+ private final static int MOUSE_DELTA_Y = 5;
+
+ private int mMinVisibleWidth;
+ private int mMinVisibleHeight;
+ private TaskPositioner mPositioner;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ TaskPositioner.setFactory(null);
+
+ final Display display = mDisplayContent.getDisplay();
+ final DisplayMetrics dm = new DisplayMetrics();
+ display.getMetrics(dm);
+
+ // This should be the same calculation as the TaskPositioner uses.
+ mMinVisibleWidth = dipToPixel(MINIMUM_VISIBLE_WIDTH_IN_DP, dm);
+ mMinVisibleHeight = dipToPixel(MINIMUM_VISIBLE_HEIGHT_IN_DP, dm);
+
+ mPositioner = TaskPositioner.create(sWm);
+ mPositioner.register(mDisplayContent);
+ }
+
+ @Test
+ public void testOverrideFactory() throws Exception {
+ final boolean[] created = new boolean[1];
+ created[0] = false;
+ TaskPositioner.setFactory(new TaskPositioner.Factory() {
+ @Override
+ public TaskPositioner create(WindowManagerService service) {
+ created[0] = true;
+ return null;
+ }
+ });
+
+ assertNull(TaskPositioner.create(sWm));
+ assertTrue(created[0]);
+ }
+
+ /**
+ * This tests that free resizing will allow to change the orientation as well
+ * as does some basic tests (e.g. dragging in Y only will keep X stable).
+ */
+ @Test
+ @Ignore
+ public void testBasicFreeWindowResizing() throws Exception {
+ final Rect r = new Rect(100, 220, 700, 520);
+ final int midY = (r.top + r.bottom) / 2;
+
+ // Start a drag resize starting upper left.
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y, r);
+ assertBoundsEquals(r, mPositioner.getWindowDragBounds());
+
+ // Drag to a good landscape size.
+ mPositioner.resizeDrag(0.0f, 0.0f);
+ assertBoundsEquals(new Rect(MOUSE_DELTA_X, MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to a good portrait size.
+ mPositioner.resizeDrag(400.0f, 0.0f);
+ assertBoundsEquals(new Rect(400 + MOUSE_DELTA_X, MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to a too small size for the width.
+ mPositioner.resizeDrag(2000.0f, r.top);
+ assertBoundsEquals(
+ new Rect(r.right - mMinVisibleWidth, r.top + MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to a too small size for the height.
+ mPositioner.resizeDrag(r.left, 2000.0f);
+ assertBoundsEquals(
+ new Rect(r.left + MOUSE_DELTA_X, r.bottom - mMinVisibleHeight, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Start a drag resize left and see that only the left coord changes..
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, midY, r);
+
+ // Drag to the left.
+ mPositioner.resizeDrag(0.0f, midY);
+ assertBoundsEquals(new Rect(MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the right.
+ mPositioner.resizeDrag(200.0f, midY);
+ assertBoundsEquals(new Rect(200 + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the top
+ mPositioner.resizeDrag(r.left, 0.0f);
+ assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the bottom
+ mPositioner.resizeDrag(r.left, 1000.0f);
+ assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+ }
+
+ /**
+ * This tests that by dragging any edge, the fixed / opposite edge(s) remains anchored.
+ */
+ @Test
+ @Ignore
+ public void testFreeWindowResizingTestAllEdges() throws Exception {
+ final Rect r = new Rect(100, 220, 700, 520);
+ final int midX = (r.left + r.right) / 2;
+ final int midY = (r.top + r.bottom) / 2;
+
+ // Drag upper left.
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y, r);
+ mPositioner.resizeDrag(0.0f, 0.0f);
+ assertTrue(r.left != mPositioner.getWindowDragBounds().left);
+ assertEquals(r.right, mPositioner.getWindowDragBounds().right);
+ assertTrue(r.top != mPositioner.getWindowDragBounds().top);
+ assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom);
+
+ // Drag upper.
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/, midX, r.top - MOUSE_DELTA_Y, r);
+ mPositioner.resizeDrag(0.0f, 0.0f);
+ assertEquals(r.left, mPositioner.getWindowDragBounds().left);
+ assertEquals(r.right, mPositioner.getWindowDragBounds().right);
+ assertTrue(r.top != mPositioner.getWindowDragBounds().top);
+ assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom);
+
+ // Drag upper right.
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/, r.right + MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y, r);
+ mPositioner.resizeDrag(r.right + 100, 0.0f);
+ assertEquals(r.left, mPositioner.getWindowDragBounds().left);
+ assertTrue(r.right != mPositioner.getWindowDragBounds().right);
+ assertTrue(r.top != mPositioner.getWindowDragBounds().top);
+ assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom);
+
+ // Drag right.
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/, r.right + MOUSE_DELTA_X, midY, r);
+ mPositioner.resizeDrag(r.right + 100, 0.0f);
+ assertEquals(r.left, mPositioner.getWindowDragBounds().left);
+ assertTrue(r.right != mPositioner.getWindowDragBounds().right);
+ assertEquals(r.top, mPositioner.getWindowDragBounds().top);
+ assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom);
+
+ // Drag bottom right.
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/,
+ r.right + MOUSE_DELTA_X, r.bottom + MOUSE_DELTA_Y, r);
+ mPositioner.resizeDrag(r.right + 100, r.bottom + 100);
+ assertEquals(r.left, mPositioner.getWindowDragBounds().left);
+ assertTrue(r.right != mPositioner.getWindowDragBounds().right);
+ assertEquals(r.top, mPositioner.getWindowDragBounds().top);
+ assertTrue(r.bottom != mPositioner.getWindowDragBounds().bottom);
+
+ // Drag bottom.
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/, midX, r.bottom + MOUSE_DELTA_Y, r);
+ mPositioner.resizeDrag(r.right + 100, r.bottom + 100);
+ assertEquals(r.left, mPositioner.getWindowDragBounds().left);
+ assertEquals(r.right, mPositioner.getWindowDragBounds().right);
+ assertEquals(r.top, mPositioner.getWindowDragBounds().top);
+ assertTrue(r.bottom != mPositioner.getWindowDragBounds().bottom);
+
+ // Drag bottom left.
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.bottom + MOUSE_DELTA_Y, r);
+ mPositioner.resizeDrag(0.0f, r.bottom + 100);
+ assertTrue(r.left != mPositioner.getWindowDragBounds().left);
+ assertEquals(r.right, mPositioner.getWindowDragBounds().right);
+ assertEquals(r.top, mPositioner.getWindowDragBounds().top);
+ assertTrue(r.bottom != mPositioner.getWindowDragBounds().bottom);
+
+ // Drag left.
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, midX, r);
+ mPositioner.resizeDrag(0.0f, r.bottom + 100);
+ assertTrue(r.left != mPositioner.getWindowDragBounds().left);
+ assertEquals(r.right, mPositioner.getWindowDragBounds().right);
+ assertEquals(r.top, mPositioner.getWindowDragBounds().top);
+ assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom);
+ }
+
+ /**
+ * This tests that a constrained landscape window will keep the aspect and do the
+ * right things upon resizing when dragged from the top left corner.
+ */
+ @Test
+ @Ignore
+ public void testLandscapePreservedWindowResizingDragTopLeft() throws Exception {
+ final Rect r = new Rect(100, 220, 700, 520);
+
+ mPositioner.startDrag(true /*resizing*/,
+ true /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y, r);
+ assertBoundsEquals(r, mPositioner.getWindowDragBounds());
+
+ // Drag to a good landscape size.
+ mPositioner.resizeDrag(0.0f, 0.0f);
+ assertBoundsEquals(new Rect(MOUSE_DELTA_X, MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to a good portrait size.
+ mPositioner.resizeDrag(400.0f, 0.0f);
+ int width = Math.round((float) (r.bottom - MOUSE_DELTA_Y) * MIN_ASPECT);
+ assertBoundsEquals(new Rect(r.right - width, MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to a too small size for the width.
+ mPositioner.resizeDrag(2000.0f, r.top);
+ final int w = mMinVisibleWidth;
+ final int h = Math.round(w / MIN_ASPECT);
+ assertBoundsEquals(new Rect(r.right - w, r.bottom - h, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to a too small size for the height.
+ mPositioner.resizeDrag(r.left, 2000.0f);
+ assertBoundsEquals(
+ new Rect(r.left + MOUSE_DELTA_X, r.bottom - mMinVisibleHeight, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+ }
+
+ /**
+ * This tests that a constrained landscape window will keep the aspect and do the
+ * right things upon resizing when dragged from the left corner.
+ */
+ @Test
+ @Ignore
+ public void testLandscapePreservedWindowResizingDragLeft() throws Exception {
+ final Rect r = new Rect(100, 220, 700, 520);
+ final int midY = (r.top + r.bottom) / 2;
+
+ mPositioner.startDrag(true /*resizing*/,
+ true /*preserveOrientation*/, r.left - MOUSE_DELTA_X, midY, r);
+
+ // Drag to the left.
+ mPositioner.resizeDrag(0.0f, midY);
+ assertBoundsEquals(new Rect(MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the right.
+ mPositioner.resizeDrag(200.0f, midY);
+ assertBoundsEquals(new Rect(200 + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag all the way to the right and see the height also shrinking.
+ mPositioner.resizeDrag(2000.0f, midY);
+ final int w = mMinVisibleWidth;
+ final int h = Math.round((float)w / MIN_ASPECT);
+ assertBoundsEquals(new Rect(r.right - w, r.top, r.right, r.top + h),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the top.
+ mPositioner.resizeDrag(r.left, 0.0f);
+ assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the bottom.
+ mPositioner.resizeDrag(r.left, 1000.0f);
+ assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+ }
+
+ /**
+ * This tests that a constrained landscape window will keep the aspect and do the
+ * right things upon resizing when dragged from the top corner.
+ */
+ @Test
+ @Ignore
+ public void testLandscapePreservedWindowResizingDragTop() throws Exception {
+ final Rect r = new Rect(100, 220, 700, 520);
+ final int midX = (r.left + r.right) / 2;
+
+ mPositioner.startDrag(true /*resizing*/,
+ true /*preserveOrientation*/, midX, r.top - MOUSE_DELTA_Y, r);
+
+ // Drag to the left (no change).
+ mPositioner.resizeDrag(0.0f, r.top);
+ assertBoundsEquals(new Rect(r.left, r.top + MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the right (no change).
+ mPositioner.resizeDrag(2000.0f, r.top);
+ assertBoundsEquals(new Rect(r.left , r.top + MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the top.
+ mPositioner.resizeDrag(300.0f, 0.0f);
+ int h = r.bottom - MOUSE_DELTA_Y;
+ int w = Math.max(r.right - r.left, Math.round(h * MIN_ASPECT));
+ assertBoundsEquals(new Rect(r.left, MOUSE_DELTA_Y, r.left + w, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the bottom.
+ mPositioner.resizeDrag(r.left, 1000.0f);
+ h = mMinVisibleHeight;
+ assertBoundsEquals(new Rect(r.left, r.bottom - h, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+ }
+
+ /**
+ * This tests that a constrained portrait window will keep the aspect and do the
+ * right things upon resizing when dragged from the top left corner.
+ */
+ @Test
+ @Ignore
+ public void testPortraitPreservedWindowResizingDragTopLeft() throws Exception {
+ final Rect r = new Rect(330, 100, 630, 600);
+
+ mPositioner.startDrag(true /*resizing*/,
+ true /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y, r);
+ assertBoundsEquals(r, mPositioner.getWindowDragBounds());
+
+ // Drag to a good landscape size.
+ mPositioner.resizeDrag(0.0f, 0.0f);
+ int height = Math.round((float) (r.right - MOUSE_DELTA_X) * MIN_ASPECT);
+ assertBoundsEquals(new Rect(MOUSE_DELTA_X, r.bottom - height, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to a good portrait size.
+ mPositioner.resizeDrag(500.0f, 0.0f);
+ assertBoundsEquals(new Rect(500 + MOUSE_DELTA_X, MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to a too small size for the height and the the width shrinking.
+ mPositioner.resizeDrag(r.left + MOUSE_DELTA_X, 2000.0f);
+ final int w = Math.max(mMinVisibleWidth, Math.round(mMinVisibleHeight / MIN_ASPECT));
+ final int h = Math.max(mMinVisibleHeight, Math.round(w * MIN_ASPECT));
+ assertBoundsEquals(
+ new Rect(r.right - w, r.bottom - h, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+ }
+
+ /**
+ * This tests that a constrained portrait window will keep the aspect and do the
+ * right things upon resizing when dragged from the left corner.
+ */
+ @Test
+ @Ignore
+ public void testPortraitPreservedWindowResizingDragLeft() throws Exception {
+ final Rect r = new Rect(330, 100, 630, 600);
+ final int midY = (r.top + r.bottom) / 2;
+
+ mPositioner.startDrag(true /*resizing*/,
+ true /*preserveOrientation*/, r.left - MOUSE_DELTA_X, midY, r);
+
+ // Drag to the left.
+ mPositioner.resizeDrag(0.0f, midY);
+ int w = r.right - MOUSE_DELTA_X;
+ int h = Math.round(w * MIN_ASPECT);
+ assertBoundsEquals(new Rect(MOUSE_DELTA_X, r.top, r.right, r.top + h),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the right.
+ mPositioner.resizeDrag(450.0f, midY);
+ assertBoundsEquals(new Rect(450 + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag all the way to the right.
+ mPositioner.resizeDrag(2000.0f, midY);
+ w = mMinVisibleWidth;
+ h = Math.max(Math.round((float)w * MIN_ASPECT), r.height());
+ assertBoundsEquals(new Rect(r.right - w, r.top, r.right, r.top + h),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the top.
+ mPositioner.resizeDrag(r.left, 0.0f);
+ assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the bottom.
+ mPositioner.resizeDrag(r.left, 1000.0f);
+ assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+ }
+
+ /**
+ * This tests that a constrained portrait window will keep the aspect and do the
+ * right things upon resizing when dragged from the top corner.
+ */
+ @Test
+ @Ignore
+ public void testPortraitPreservedWindowResizingDragTop() throws Exception {
+ final Rect r = new Rect(330, 100, 630, 600);
+ final int midX = (r.left + r.right) / 2;
+
+ mPositioner.startDrag(true /*resizing*/,
+ true /*preserveOrientation*/, midX, r.top - MOUSE_DELTA_Y, r);
+
+ // Drag to the left (no change).
+ mPositioner.resizeDrag(0.0f, r.top);
+ assertBoundsEquals(new Rect(r.left, r.top + MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the right (no change).
+ mPositioner.resizeDrag(2000.0f, r.top);
+ assertBoundsEquals(new Rect(r.left , r.top + MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the top.
+ mPositioner.resizeDrag(300.0f, 0.0f);
+ int h = r.bottom - MOUSE_DELTA_Y;
+ int w = Math.min(r.width(), Math.round(h / MIN_ASPECT));
+ assertBoundsEquals(new Rect(r.left, MOUSE_DELTA_Y, r.left + w, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the bottom.
+ mPositioner.resizeDrag(r.left, 1000.0f);
+ h = Math.max(mMinVisibleHeight, Math.round(mMinVisibleWidth * MIN_ASPECT));
+ w = Math.round(h / MIN_ASPECT);
+ assertBoundsEquals(new Rect(r.left, r.bottom - h, r.left + w, r.bottom),
+ mPositioner.getWindowDragBounds());
+ }
+
+ private void assertBoundsEquals(Rect expected, Rect actual) {
+ if (DEBUGGING) {
+ if (!expected.equals(actual)) {
+ Log.e(TAG, "rect(" + actual.toString() + ") != isRect(" + actual.toString()
+ + ") " + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ assertEquals(expected.left, actual.left);
+ assertEquals(expected.right, actual.right);
+ assertEquals(expected.top, actual.top);
+ assertEquals(expected.bottom, actual.bottom);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskPositioningControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/TaskPositioningControllerTests.java
new file mode 100644
index 0000000..6070516
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskPositioningControllerTests.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.InputChannel;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for the {@link TaskPositioningController} class.
+ *
+ * atest com.android.server.wm.TaskPositioningControllerTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@Presubmit
+public class TaskPositioningControllerTests extends WindowTestsBase {
+ private static final int TIMEOUT_MS = 1000;
+ private TaskPositioningController mTarget;
+ private WindowState mWindow;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ assertNotNull(sWm.mTaskPositioningController);
+ mTarget = sWm.mTaskPositioningController;
+
+ when(sWm.mInputManager.transferTouchFocus(
+ any(InputChannel.class),
+ any(InputChannel.class))).thenReturn(true);
+
+ mWindow = createWindow(null, TYPE_BASE_APPLICATION, "window");
+ mWindow.mInputChannel = new InputChannel();
+ synchronized (sWm.mWindowMap) {
+ sWm.mWindowMap.put(mWindow.mClient.asBinder(), mWindow);
+ }
+ }
+
+ @Test
+ public void testStartAndFinishPositioning() throws Exception {
+ synchronized (sWm.mWindowMap) {
+ assertFalse(mTarget.isPositioningLocked());
+ assertNull(mTarget.getDragWindowHandleLocked());
+ }
+
+ assertTrue(mTarget.startMovingTask(mWindow.mClient, 0, 0));
+
+ synchronized (sWm.mWindowMap) {
+ assertTrue(mTarget.isPositioningLocked());
+ assertNotNull(mTarget.getDragWindowHandleLocked());
+ }
+
+ mTarget.finishTaskPositioning();
+ // Wait until the looper processes finishTaskPositioning.
+ assertTrue(sWm.mH.runWithScissors(() -> {}, TIMEOUT_MS));
+
+ assertFalse(mTarget.isPositioningLocked());
+ assertNull(mTarget.getDragWindowHandleLocked());
+ }
+
+ @Test
+ public void testHandleTapOutsideTask() throws Exception {
+ synchronized (sWm.mWindowMap) {
+
+ assertFalse(mTarget.isPositioningLocked());
+ assertNull(mTarget.getDragWindowHandleLocked());
+ }
+
+ final DisplayContent content = mock(DisplayContent.class);
+ when(content.findTaskForResizePoint(anyInt(), anyInt())).thenReturn(mWindow.getTask());
+ assertNotNull(mWindow.getTask().getTopVisibleAppMainWindow());
+
+ mTarget.handleTapOutsideTask(content, 0, 0);
+ // Wait until the looper processes finishTaskPositioning.
+ assertTrue(sWm.mH.runWithScissors(() -> {}, TIMEOUT_MS));
+
+ synchronized (sWm.mWindowMap) {
+ assertTrue(mTarget.isPositioningLocked());
+ assertNotNull(mTarget.getDragWindowHandleLocked());
+ }
+
+ mTarget.finishTaskPositioning();
+ // Wait until the looper processes finishTaskPositioning.
+ assertTrue(sWm.mH.runWithScissors(() -> {}, TIMEOUT_MS));
+
+ assertFalse(mTarget.isPositioningLocked());
+ assertNull(mTarget.getDragWindowHandleLocked());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotCacheTest.java b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotCacheTest.java
new file mode 100644
index 0000000..649de4a
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotCacheTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test class for {@link TaskSnapshotCache}.
+ *
+ * runtest frameworks-services -c com.android.server.wm.TaskSnapshotCacheTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class TaskSnapshotCacheTest extends TaskSnapshotPersisterTestBase {
+
+ private TaskSnapshotCache mCache;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mCache = new TaskSnapshotCache(sWm, mLoader);
+ }
+
+ @Test
+ public void testAppRemoved() throws Exception {
+ final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ mCache.putSnapshot(window.getTask(), createSnapshot());
+ assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, 0 /* userId */,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+ mCache.onAppRemoved(window.mAppToken);
+ assertNull(mCache.getSnapshot(window.getTask().mTaskId, 0 /* userId */,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+ }
+
+ @Test
+ public void testAppDied() throws Exception {
+ final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ mCache.putSnapshot(window.getTask(), createSnapshot());
+ assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, 0 /* userId */,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+ mCache.onAppDied(window.mAppToken);
+ assertNull(mCache.getSnapshot(window.getTask().mTaskId, 0 /* userId */,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+ }
+
+ @Test
+ public void testTaskRemoved() throws Exception {
+ final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ mCache.putSnapshot(window.getTask(), createSnapshot());
+ assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, 0 /* userId */,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+ mCache.onTaskRemoved(window.getTask().mTaskId);
+ assertNull(mCache.getSnapshot(window.getTask().mTaskId, 0 /* userId */,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+ }
+
+ @Test
+ public void testReduced_notCached() throws Exception {
+ final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ mPersister.persistSnapshot(window.getTask().mTaskId, sWm.mCurrentUserId, createSnapshot());
+ mPersister.waitForQueueEmpty();
+ assertNull(mCache.getSnapshot(window.getTask().mTaskId, sWm.mCurrentUserId,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+
+ // Load it from disk
+ assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, sWm.mCurrentUserId,
+ true /* restoreFromDisk */, true /* reducedResolution */));
+
+ // Make sure it's not in the cache now.
+ assertNull(mCache.getSnapshot(window.getTask().mTaskId, sWm.mCurrentUserId,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+ }
+
+ @Test
+ public void testRestoreFromDisk() throws Exception {
+ final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ mPersister.persistSnapshot(window.getTask().mTaskId, sWm.mCurrentUserId, createSnapshot());
+ mPersister.waitForQueueEmpty();
+ assertNull(mCache.getSnapshot(window.getTask().mTaskId, sWm.mCurrentUserId,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+
+ // Load it from disk
+ assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, sWm.mCurrentUserId,
+ true /* restoreFromDisk */, false /* reducedResolution */));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotControllerTest.java b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotControllerTest.java
new file mode 100644
index 0000000..5650050
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotControllerTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
+import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
+import static android.view.WindowManager.TRANSIT_UNSET;
+import static com.android.server.wm.TaskSnapshotController.*;
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.ArraySet;
+
+import com.google.android.collect.Sets;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test class for {@link TaskSnapshotController}.
+ *
+ * runtest frameworks-services -c com.android.server.wm.TaskSnapshotControllerTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class TaskSnapshotControllerTest extends WindowTestsBase {
+
+ @Test
+ public void testGetClosingApps_closing() throws Exception {
+ final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
+ "closingWindow");
+ closingWindow.mAppToken.setVisibility(null, false /* visible */, TRANSIT_UNSET,
+ true /* performLayout */, false /* isVoiceInteraction */);
+ final ArraySet<AppWindowToken> closingApps = new ArraySet<>();
+ closingApps.add(closingWindow.mAppToken);
+ final ArraySet<Task> closingTasks = new ArraySet<>();
+ sWm.mTaskSnapshotController.getClosingTasks(closingApps, closingTasks);
+ assertEquals(1, closingTasks.size());
+ assertEquals(closingWindow.mAppToken.getTask(), closingTasks.valueAt(0));
+ }
+
+ @Test
+ public void testGetClosingApps_notClosing() throws Exception {
+ final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
+ "closingWindow");
+ final WindowState openingWindow = createAppWindow(closingWindow.getTask(),
+ FIRST_APPLICATION_WINDOW, "openingWindow");
+ closingWindow.mAppToken.setVisibility(null, false /* visible */, TRANSIT_UNSET,
+ true /* performLayout */, false /* isVoiceInteraction */);
+ openingWindow.mAppToken.setVisibility(null, true /* visible */, TRANSIT_UNSET,
+ true /* performLayout */, false /* isVoiceInteraction */);
+ final ArraySet<AppWindowToken> closingApps = new ArraySet<>();
+ closingApps.add(closingWindow.mAppToken);
+ final ArraySet<Task> closingTasks = new ArraySet<>();
+ sWm.mTaskSnapshotController.getClosingTasks(closingApps, closingTasks);
+ assertEquals(0, closingTasks.size());
+ }
+
+ @Test
+ public void testGetClosingApps_skipClosingAppsSnapshotTasks() throws Exception {
+ final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
+ "closingWindow");
+ closingWindow.mAppToken.setVisibility(null, false /* visible */, TRANSIT_UNSET,
+ true /* performLayout */, false /* isVoiceInteraction */);
+ final ArraySet<AppWindowToken> closingApps = new ArraySet<>();
+ closingApps.add(closingWindow.mAppToken);
+ final ArraySet<Task> closingTasks = new ArraySet<>();
+ sWm.mTaskSnapshotController.addSkipClosingAppSnapshotTasks(
+ Sets.newArraySet(closingWindow.mAppToken.getTask()));
+ sWm.mTaskSnapshotController.getClosingTasks(closingApps, closingTasks);
+ assertEquals(0, closingTasks.size());
+ }
+
+ @Test
+ public void testGetSnapshotMode() throws Exception {
+ final WindowState disabledWindow = createWindow(null,
+ FIRST_APPLICATION_WINDOW, mDisplayContent, "disabledWindow");
+ disabledWindow.mAppToken.setDisablePreviewScreenshots(true);
+ assertEquals(SNAPSHOT_MODE_APP_THEME,
+ sWm.mTaskSnapshotController.getSnapshotMode(disabledWindow.getTask()));
+
+ final WindowState normalWindow = createWindow(null,
+ FIRST_APPLICATION_WINDOW, mDisplayContent, "normalWindow");
+ assertEquals(SNAPSHOT_MODE_REAL,
+ sWm.mTaskSnapshotController.getSnapshotMode(normalWindow.getTask()));
+
+ final WindowState secureWindow = createWindow(null,
+ FIRST_APPLICATION_WINDOW, mDisplayContent, "secureWindow");
+ secureWindow.mAttrs.flags |= FLAG_SECURE;
+ assertEquals(SNAPSHOT_MODE_APP_THEME,
+ sWm.mTaskSnapshotController.getSnapshotMode(secureWindow.getTask()));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java
new file mode 100644
index 0000000..325d42a
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.ActivityManager.TaskSnapshot;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.ArraySet;
+
+import android.view.View;
+import com.android.server.wm.TaskSnapshotPersister.RemoveObsoleteFilesQueueItem;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+/**
+ * Test class for {@link TaskSnapshotPersister} and {@link TaskSnapshotLoader}
+ *
+ * atest FrameworksServicesTests:TaskSnapshotPersisterLoaderTest
+ */
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class TaskSnapshotPersisterLoaderTest extends TaskSnapshotPersisterTestBase {
+
+ private static final Rect TEST_INSETS = new Rect(10, 20, 30, 40);
+
+ @Test
+ public void testPersistAndLoadSnapshot() {
+ mPersister.persistSnapshot(1 , mTestUserId, createSnapshot());
+ mPersister.waitForQueueEmpty();
+ final File[] files = new File[] { new File(sFilesDir.getPath() + "/snapshots/1.proto"),
+ new File(sFilesDir.getPath() + "/snapshots/1.jpg"),
+ new File(sFilesDir.getPath() + "/snapshots/1_reduced.jpg")};
+ assertTrueForFiles(files, File::exists, " must exist");
+ final TaskSnapshot snapshot = mLoader.loadTask(1, mTestUserId, false /* reduced */);
+ assertNotNull(snapshot);
+ assertEquals(TEST_INSETS, snapshot.getContentInsets());
+ assertNotNull(snapshot.getSnapshot());
+ assertEquals(Configuration.ORIENTATION_PORTRAIT, snapshot.getOrientation());
+ }
+
+ private void assertTrueForFiles(File[] files, Predicate<File> predicate, String message) {
+ for (File file : files) {
+ assertTrue(file.getName() + message, predicate.apply(file));
+ }
+ }
+
+ @Test
+ public void testTaskRemovedFromRecents() {
+ mPersister.persistSnapshot(1, mTestUserId, createSnapshot());
+ mPersister.onTaskRemovedFromRecents(1, mTestUserId);
+ mPersister.waitForQueueEmpty();
+ assertFalse(new File(sFilesDir.getPath() + "/snapshots/1.proto").exists());
+ assertFalse(new File(sFilesDir.getPath() + "/snapshots/1.jpg").exists());
+ assertFalse(new File(sFilesDir.getPath() + "/snapshots/1_reduced.jpg").exists());
+ }
+
+ /**
+ * Tests that persisting a couple of snapshots is being throttled.
+ */
+ @Test
+ public void testThrottling() {
+ long ms = SystemClock.elapsedRealtime();
+ mPersister.persistSnapshot(1, mTestUserId, createSnapshot());
+ mPersister.persistSnapshot(2, mTestUserId, createSnapshot());
+ mPersister.removeObsoleteFiles(new ArraySet<>(), new int[] { mTestUserId });
+ mPersister.removeObsoleteFiles(new ArraySet<>(), new int[] { mTestUserId });
+ mPersister.removeObsoleteFiles(new ArraySet<>(), new int[] { mTestUserId });
+ mPersister.removeObsoleteFiles(new ArraySet<>(), new int[] { mTestUserId });
+ mPersister.waitForQueueEmpty();
+ assertTrue(SystemClock.elapsedRealtime() - ms > 500);
+ }
+
+ /**
+ * Tests that too many store write queue items are being purged.
+ */
+ @Test
+ public void testPurging() {
+ mPersister.persistSnapshot(100, mTestUserId, createSnapshot());
+ mPersister.waitForQueueEmpty();
+ mPersister.setPaused(true);
+ mPersister.persistSnapshot(1, mTestUserId, createSnapshot());
+ mPersister.removeObsoleteFiles(new ArraySet<>(), new int[] { mTestUserId });
+ mPersister.persistSnapshot(2, mTestUserId, createSnapshot());
+ mPersister.persistSnapshot(3, mTestUserId, createSnapshot());
+ mPersister.persistSnapshot(4, mTestUserId, createSnapshot());
+ mPersister.setPaused(false);
+ mPersister.waitForQueueEmpty();
+
+ // Make sure 1,2 were purged but removeObsoleteFiles wasn't.
+ final File[] existsFiles = new File[] {
+ new File(sFilesDir.getPath() + "/snapshots/3.proto"),
+ new File(sFilesDir.getPath() + "/snapshots/4.proto")};
+ final File[] nonExistsFiles = new File[] {
+ new File(sFilesDir.getPath() + "/snapshots/100.proto"),
+ new File(sFilesDir.getPath() + "/snapshots/1.proto"),
+ new File(sFilesDir.getPath() + "/snapshots/1.proto")};
+ assertTrueForFiles(existsFiles, File::exists, " must exist");
+ assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist");
+ }
+
+ @Test
+ public void testGetTaskId() {
+ RemoveObsoleteFilesQueueItem removeObsoleteFilesQueueItem =
+ mPersister.new RemoveObsoleteFilesQueueItem(new ArraySet<>(), new int[] {});
+ assertEquals(-1, removeObsoleteFilesQueueItem.getTaskId("blablablulp"));
+ assertEquals(-1, removeObsoleteFilesQueueItem.getTaskId("nothing.err"));
+ assertEquals(-1, removeObsoleteFilesQueueItem.getTaskId("/invalid/"));
+ assertEquals(12, removeObsoleteFilesQueueItem.getTaskId("12.jpg"));
+ assertEquals(12, removeObsoleteFilesQueueItem.getTaskId("12.proto"));
+ assertEquals(1, removeObsoleteFilesQueueItem.getTaskId("1.jpg"));
+ assertEquals(1, removeObsoleteFilesQueueItem.getTaskId("1_reduced.jpg"));
+ }
+
+ @Test
+ public void testLowResolutionPersistAndLoadSnapshot() {
+ TaskSnapshot a = createSnapshot(0.5f /* reducedResolution */);
+ assertTrue(a.isReducedResolution());
+ mPersister.persistSnapshot(1 , mTestUserId, a);
+ mPersister.waitForQueueEmpty();
+ final File[] files = new File[] { new File(sFilesDir.getPath() + "/snapshots/1.proto"),
+ new File(sFilesDir.getPath() + "/snapshots/1_reduced.jpg")};
+ final File[] nonExistsFiles = new File[] {
+ new File(sFilesDir.getPath() + "/snapshots/1.jpg"),
+ };
+ assertTrueForFiles(files, File::exists, " must exist");
+ assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist");
+ final TaskSnapshot snapshot = mLoader.loadTask(1, mTestUserId, true /* reduced */);
+ assertNotNull(snapshot);
+ assertEquals(TEST_INSETS, snapshot.getContentInsets());
+ assertNotNull(snapshot.getSnapshot());
+ assertEquals(Configuration.ORIENTATION_PORTRAIT, snapshot.getOrientation());
+
+ final TaskSnapshot snapshotNotExist = mLoader.loadTask(1, mTestUserId, false /* reduced */);
+ assertNull(snapshotNotExist);
+ }
+
+ @Test
+ public void testIsRealSnapshotPersistAndLoadSnapshot() {
+ TaskSnapshot a = new TaskSnapshotBuilder()
+ .setIsRealSnapshot(true)
+ .build();
+ TaskSnapshot b = new TaskSnapshotBuilder()
+ .setIsRealSnapshot(false)
+ .build();
+ assertTrue(a.isRealSnapshot());
+ assertFalse(b.isRealSnapshot());
+ mPersister.persistSnapshot(1, mTestUserId, a);
+ mPersister.persistSnapshot(2, mTestUserId, b);
+ mPersister.waitForQueueEmpty();
+ final TaskSnapshot snapshotA = mLoader.loadTask(1, mTestUserId, false /* reduced */);
+ final TaskSnapshot snapshotB = mLoader.loadTask(2, mTestUserId, false /* reduced */);
+ assertNotNull(snapshotA);
+ assertNotNull(snapshotB);
+ assertTrue(snapshotA.isRealSnapshot());
+ assertFalse(snapshotB.isRealSnapshot());
+ }
+
+ @Test
+ public void testWindowingModePersistAndLoadSnapshot() {
+ TaskSnapshot a = new TaskSnapshotBuilder()
+ .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+ .build();
+ TaskSnapshot b = new TaskSnapshotBuilder()
+ .setWindowingMode(WINDOWING_MODE_PINNED)
+ .build();
+ assertTrue(a.getWindowingMode() == WINDOWING_MODE_FULLSCREEN);
+ assertTrue(b.getWindowingMode() == WINDOWING_MODE_PINNED);
+ mPersister.persistSnapshot(1, mTestUserId, a);
+ mPersister.persistSnapshot(2, mTestUserId, b);
+ mPersister.waitForQueueEmpty();
+ final TaskSnapshot snapshotA = mLoader.loadTask(1, mTestUserId, false /* reduced */);
+ final TaskSnapshot snapshotB = mLoader.loadTask(2, mTestUserId, false /* reduced */);
+ assertNotNull(snapshotA);
+ assertNotNull(snapshotB);
+ assertTrue(snapshotA.getWindowingMode() == WINDOWING_MODE_FULLSCREEN);
+ assertTrue(snapshotB.getWindowingMode() == WINDOWING_MODE_PINNED);
+ }
+
+ @Test
+ public void testIsTranslucentPersistAndLoadSnapshot() {
+ TaskSnapshot a = new TaskSnapshotBuilder()
+ .setIsTranslucent(true)
+ .build();
+ TaskSnapshot b = new TaskSnapshotBuilder()
+ .setIsTranslucent(false)
+ .build();
+ assertTrue(a.isTranslucent());
+ assertFalse(b.isTranslucent());
+ mPersister.persistSnapshot(1, mTestUserId, a);
+ mPersister.persistSnapshot(2, mTestUserId, b);
+ mPersister.waitForQueueEmpty();
+ final TaskSnapshot snapshotA = mLoader.loadTask(1, mTestUserId, false /* reduced */);
+ final TaskSnapshot snapshotB = mLoader.loadTask(2, mTestUserId, false /* reduced */);
+ assertNotNull(snapshotA);
+ assertNotNull(snapshotB);
+ assertTrue(snapshotA.isTranslucent());
+ assertFalse(snapshotB.isTranslucent());
+ }
+
+ @Test
+ public void testSystemUiVisibilityPersistAndLoadSnapshot() {
+ final int lightBarFlags = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
+ TaskSnapshot a = new TaskSnapshotBuilder()
+ .setSystemUiVisibility(0)
+ .build();
+ TaskSnapshot b = new TaskSnapshotBuilder()
+ .setSystemUiVisibility(lightBarFlags)
+ .build();
+ assertTrue(a.getSystemUiVisibility() == 0);
+ assertTrue(b.getSystemUiVisibility() == lightBarFlags);
+ mPersister.persistSnapshot(1, mTestUserId, a);
+ mPersister.persistSnapshot(2, mTestUserId, b);
+ mPersister.waitForQueueEmpty();
+ final TaskSnapshot snapshotA = mLoader.loadTask(1, mTestUserId, false /* reduced */);
+ final TaskSnapshot snapshotB = mLoader.loadTask(2, mTestUserId, false /* reduced */);
+ assertNotNull(snapshotA);
+ assertNotNull(snapshotB);
+ assertTrue(snapshotA.getSystemUiVisibility() == 0);
+ assertTrue(snapshotB.getSystemUiVisibility() == lightBarFlags);
+ }
+
+ @Test
+ public void testRemoveObsoleteFiles() {
+ mPersister.persistSnapshot(1, mTestUserId, createSnapshot());
+ mPersister.persistSnapshot(2, mTestUserId, createSnapshot());
+ final ArraySet<Integer> taskIds = new ArraySet<>();
+ taskIds.add(1);
+ mPersister.removeObsoleteFiles(taskIds, new int[] { mTestUserId });
+ mPersister.waitForQueueEmpty();
+ final File[] existsFiles = new File[] {
+ new File(sFilesDir.getPath() + "/snapshots/1.proto"),
+ new File(sFilesDir.getPath() + "/snapshots/1.jpg"),
+ new File(sFilesDir.getPath() + "/snapshots/1_reduced.jpg") };
+ final File[] nonExistsFiles = new File[] {
+ new File(sFilesDir.getPath() + "/snapshots/2.proto"),
+ new File(sFilesDir.getPath() + "/snapshots/2.jpg"),
+ new File(sFilesDir.getPath() + "/snapshots/2_reduced.jpg")};
+ assertTrueForFiles(existsFiles, File::exists, " must exist");
+ assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist");
+ }
+
+ @Test
+ public void testRemoveObsoleteFiles_addedOneInTheMeantime() {
+ mPersister.persistSnapshot(1, mTestUserId, createSnapshot());
+ final ArraySet<Integer> taskIds = new ArraySet<>();
+ taskIds.add(1);
+ mPersister.removeObsoleteFiles(taskIds, new int[] { mTestUserId });
+ mPersister.persistSnapshot(2, mTestUserId, createSnapshot());
+ mPersister.waitForQueueEmpty();
+ final File[] existsFiles = new File[] {
+ new File(sFilesDir.getPath() + "/snapshots/1.proto"),
+ new File(sFilesDir.getPath() + "/snapshots/1.jpg"),
+ new File(sFilesDir.getPath() + "/snapshots/1_reduced.jpg"),
+ new File(sFilesDir.getPath() + "/snapshots/2.proto"),
+ new File(sFilesDir.getPath() + "/snapshots/2.jpg"),
+ new File(sFilesDir.getPath() + "/snapshots/2_reduced.jpg")};
+ assertTrueForFiles(existsFiles, File::exists, " must exist");
+ }
+
+ /**
+ * Private predicate definition.
+ *
+ * This is needed because com.android.internal.util.Predicate is deprecated
+ * and can only be used with classes fron android.test.runner. This cannot
+ * use java.util.function.Predicate because that is not present on all API
+ * versions that this test must run on.
+ */
+ private interface Predicate<T> {
+ boolean apply(T t);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
new file mode 100644
index 0000000..8b86043
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.graphics.GraphicBuffer.USAGE_HW_TEXTURE;
+import static android.graphics.GraphicBuffer.USAGE_SW_READ_RARELY;
+
+import android.app.ActivityManager.TaskSnapshot;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.GraphicBuffer;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.os.UserManager;
+import android.support.test.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+
+import java.io.File;
+
+/**
+ * Base class for tests that use a {@link TaskSnapshotPersister}.
+ */
+class TaskSnapshotPersisterTestBase extends WindowTestsBase {
+
+ private static final Rect TEST_INSETS = new Rect(10, 20, 30, 40);
+
+ TaskSnapshotPersister mPersister;
+ TaskSnapshotLoader mLoader;
+ int mTestUserId;
+ static File sFilesDir;
+
+ @BeforeClass
+ public static void setUpUser() {
+ sFilesDir = InstrumentationRegistry.getContext().getFilesDir();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ final UserManager um = UserManager.get(InstrumentationRegistry.getContext());
+ mTestUserId = um.getUserHandle();
+ mPersister = new TaskSnapshotPersister(userId -> sFilesDir);
+ mLoader = new TaskSnapshotLoader(mPersister);
+ mPersister.start();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ cleanDirectory();
+ }
+
+ private void cleanDirectory() {
+ final File[] files = new File(sFilesDir, "snapshots").listFiles();
+ if (files == null) {
+ return;
+ }
+ for (File file : files) {
+ if (!file.isDirectory()) {
+ file.delete();
+ }
+ }
+ }
+
+ TaskSnapshot createSnapshot() {
+ return createSnapshot(1f /* scale */);
+ }
+
+ TaskSnapshot createSnapshot(float scale) {
+ return new TaskSnapshotBuilder()
+ .setScale(scale)
+ .build();
+ }
+
+ /**
+ * Builds a TaskSnapshot.
+ */
+ class TaskSnapshotBuilder {
+
+ private float mScale = 1f;
+ private boolean mIsRealSnapshot = true;
+ private boolean mIsTranslucent = false;
+ private int mWindowingMode = WINDOWING_MODE_FULLSCREEN;
+ private int mSystemUiVisibility = 0;
+
+ public TaskSnapshotBuilder setScale(float scale) {
+ mScale = scale;
+ return this;
+ }
+
+ public TaskSnapshotBuilder setIsRealSnapshot(boolean isRealSnapshot) {
+ mIsRealSnapshot = isRealSnapshot;
+ return this;
+ }
+
+ public TaskSnapshotBuilder setIsTranslucent(boolean isTranslucent) {
+ mIsTranslucent = isTranslucent;
+ return this;
+ }
+
+ public TaskSnapshotBuilder setWindowingMode(int windowingMode) {
+ mWindowingMode = windowingMode;
+ return this;
+ }
+
+ public TaskSnapshotBuilder setSystemUiVisibility(int systemUiVisibility) {
+ mSystemUiVisibility = systemUiVisibility;
+ return this;
+ }
+
+ public TaskSnapshot build() {
+ final GraphicBuffer buffer = GraphicBuffer.create(100, 100, PixelFormat.RGBA_8888,
+ USAGE_HW_TEXTURE | USAGE_SW_READ_RARELY | USAGE_SW_READ_RARELY);
+ Canvas c = buffer.lockCanvas();
+ c.drawColor(Color.RED);
+ buffer.unlockCanvasAndPost(c);
+ return new TaskSnapshot(buffer, ORIENTATION_PORTRAIT, TEST_INSETS,
+ mScale < 1f /* reducedResolution */, mScale, mIsRealSnapshot, mWindowingMode,
+ mSystemUiVisibility, mIsTranslucent);
+ }
+
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotSurfaceTest.java b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotSurfaceTest.java
new file mode 100644
index 0000000..b19373e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotSurfaceTest.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager.TaskSnapshot;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.GraphicBuffer;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.Surface;
+
+import com.android.server.wm.TaskSnapshotSurface.Window;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test class for {@link TaskSnapshotSurface}.
+ *
+ * runtest frameworks-services -c com.android.server.wm.TaskSnapshotSurfaceTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class TaskSnapshotSurfaceTest extends WindowTestsBase {
+
+ private TaskSnapshotSurface mSurface;
+
+ private void setupSurface(int width, int height, Rect contentInsets, int sysuiVis,
+ int windowFlags, Rect taskBounds) {
+ final GraphicBuffer buffer = GraphicBuffer.create(width, height, PixelFormat.RGBA_8888,
+ GraphicBuffer.USAGE_SW_READ_NEVER | GraphicBuffer.USAGE_SW_WRITE_NEVER);
+ final TaskSnapshot snapshot = new TaskSnapshot(buffer,
+ ORIENTATION_PORTRAIT, contentInsets, false, 1.0f, true /* isRealSnapshot */,
+ WINDOWING_MODE_FULLSCREEN, 0 /* systemUiVisibility */, false /* isTranslucent */);
+ mSurface = new TaskSnapshotSurface(sWm, new Window(), new Surface(), snapshot, "Test",
+ Color.WHITE, Color.RED, Color.BLUE, sysuiVis, windowFlags, 0, taskBounds,
+ ORIENTATION_PORTRAIT);
+ }
+
+ private void setupSurface(int width, int height) {
+ setupSurface(width, height, new Rect(), 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
+ new Rect(0, 0, width, height));
+ }
+
+ @Test
+ public void fillEmptyBackground_fillHorizontally() throws Exception {
+ setupSurface(200, 100);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(200);
+ when(mockCanvas.getHeight()).thenReturn(100);
+ mSurface.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 100, 200));
+ verify(mockCanvas).drawRect(eq(100.0f), eq(0.0f), eq(200.0f), eq(100.0f), any());
+ }
+
+ @Test
+ public void fillEmptyBackground_fillVertically() throws Exception {
+ setupSurface(100, 200);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(100);
+ when(mockCanvas.getHeight()).thenReturn(200);
+ mSurface.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 200, 100));
+ verify(mockCanvas).drawRect(eq(0.0f), eq(100.0f), eq(100.0f), eq(200.0f), any());
+ }
+
+ @Test
+ public void fillEmptyBackground_fillBoth() throws Exception {
+ setupSurface(200, 200);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(200);
+ when(mockCanvas.getHeight()).thenReturn(200);
+ mSurface.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 100, 100));
+ verify(mockCanvas).drawRect(eq(100.0f), eq(0.0f), eq(200.0f), eq(100.0f), any());
+ verify(mockCanvas).drawRect(eq(0.0f), eq(100.0f), eq(200.0f), eq(200.0f), any());
+ }
+
+ @Test
+ public void fillEmptyBackground_dontFill_sameSize() throws Exception {
+ setupSurface(100, 100);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(100);
+ when(mockCanvas.getHeight()).thenReturn(100);
+ mSurface.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 100, 100));
+ verify(mockCanvas, never()).drawRect(anyInt(), anyInt(), anyInt(), anyInt(), any());
+ }
+
+ @Test
+ public void fillEmptyBackground_dontFill_bitmapLarger() throws Exception {
+ setupSurface(100, 100);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(100);
+ when(mockCanvas.getHeight()).thenReturn(100);
+ mSurface.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 200, 200));
+ verify(mockCanvas, never()).drawRect(anyInt(), anyInt(), anyInt(), anyInt(), any());
+ }
+
+ @Test
+ public void testCalculateSnapshotCrop() {
+ setupSurface(100, 100, new Rect(0, 10, 0, 10), 0, 0, new Rect(0, 0, 100, 100));
+ assertEquals(new Rect(0, 0, 100, 90), mSurface.calculateSnapshotCrop());
+ }
+
+ @Test
+ public void testCalculateSnapshotCrop_taskNotOnTop() {
+ setupSurface(100, 100, new Rect(0, 10, 0, 10), 0, 0, new Rect(0, 50, 100, 100));
+ assertEquals(new Rect(0, 10, 100, 90), mSurface.calculateSnapshotCrop());
+ }
+
+ @Test
+ public void testCalculateSnapshotCrop_navBarLeft() {
+ setupSurface(100, 100, new Rect(10, 10, 0, 0), 0, 0, new Rect(0, 0, 100, 100));
+ assertEquals(new Rect(10, 0, 100, 100), mSurface.calculateSnapshotCrop());
+ }
+
+ @Test
+ public void testCalculateSnapshotCrop_navBarRight() {
+ setupSurface(100, 100, new Rect(0, 10, 10, 0), 0, 0, new Rect(0, 0, 100, 100));
+ assertEquals(new Rect(0, 0, 90, 100), mSurface.calculateSnapshotCrop());
+ }
+
+ @Test
+ public void testCalculateSnapshotFrame() {
+ setupSurface(100, 100);
+ final Rect insets = new Rect(0, 10, 0, 10);
+ mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+ assertEquals(new Rect(0, -10, 100, 70),
+ mSurface.calculateSnapshotFrame(new Rect(0, 10, 100, 90)));
+ }
+
+ @Test
+ public void testCalculateSnapshotFrame_navBarLeft() {
+ setupSurface(100, 100);
+ final Rect insets = new Rect(10, 10, 0, 0);
+ mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+ assertEquals(new Rect(0, -10, 90, 80),
+ mSurface.calculateSnapshotFrame(new Rect(10, 10, 100, 100)));
+ }
+
+ @Test
+ public void testDrawStatusBarBackground() {
+ setupSurface(100, 100);
+ final Rect insets = new Rect(0, 10, 10, 0);
+ mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(100);
+ when(mockCanvas.getHeight()).thenReturn(100);
+ mSurface.mSystemBarBackgroundPainter.drawStatusBarBackground(
+ mockCanvas, new Rect(0, 0, 50, 100), 10);
+ verify(mockCanvas).drawRect(eq(50.0f), eq(0.0f), eq(90.0f), eq(10.0f), any());
+ }
+
+ @Test
+ public void testDrawStatusBarBackground_nullFrame() {
+ setupSurface(100, 100);
+ final Rect insets = new Rect(0, 10, 10, 0);
+ mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(100);
+ when(mockCanvas.getHeight()).thenReturn(100);
+ mSurface.mSystemBarBackgroundPainter.drawStatusBarBackground(
+ mockCanvas, null, 10);
+ verify(mockCanvas).drawRect(eq(0.0f), eq(0.0f), eq(90.0f), eq(10.0f), any());
+ }
+
+ @Test
+ public void testDrawStatusBarBackground_nope() {
+ setupSurface(100, 100);
+ final Rect insets = new Rect(0, 10, 10, 0);
+ mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(100);
+ when(mockCanvas.getHeight()).thenReturn(100);
+ mSurface.mSystemBarBackgroundPainter.drawStatusBarBackground(
+ mockCanvas, new Rect(0, 0, 100, 100), 10);
+ verify(mockCanvas, never()).drawRect(anyInt(), anyInt(), anyInt(), anyInt(), any());
+ }
+
+ @Test
+ public void testDrawNavigationBarBackground() {
+ final Rect insets = new Rect(0, 10, 0, 10);
+ setupSurface(100, 100, insets, 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
+ new Rect(0, 0, 100, 100));
+ mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(100);
+ when(mockCanvas.getHeight()).thenReturn(100);
+ mSurface.mSystemBarBackgroundPainter.drawNavigationBarBackground(mockCanvas);
+ verify(mockCanvas).drawRect(eq(new Rect(0, 90, 100, 100)), any());
+ }
+
+ @Test
+ public void testDrawNavigationBarBackground_left() {
+ final Rect insets = new Rect(10, 10, 0, 0);
+ setupSurface(100, 100, insets, 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
+ new Rect(0, 0, 100, 100));
+ mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(100);
+ when(mockCanvas.getHeight()).thenReturn(100);
+ mSurface.mSystemBarBackgroundPainter.drawNavigationBarBackground(mockCanvas);
+ verify(mockCanvas).drawRect(eq(new Rect(0, 0, 10, 100)), any());
+ }
+
+ @Test
+ public void testDrawNavigationBarBackground_right() {
+ final Rect insets = new Rect(0, 10, 10, 0);
+ setupSurface(100, 100, insets, 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
+ new Rect(0, 0, 100, 100));
+ mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(100);
+ when(mockCanvas.getHeight()).thenReturn(100);
+ mSurface.mSystemBarBackgroundPainter.drawNavigationBarBackground(mockCanvas);
+ verify(mockCanvas).drawRect(eq(new Rect(90, 0, 100, 100)), any());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskStackContainersTests.java b/services/tests/servicestests/src/com/android/server/wm/TaskStackContainersTests.java
new file mode 100644
index 0000000..ca1994f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskStackContainersTests.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.Before;
+import org.junit.After;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+/**
+ * Tests for the {@link DisplayContent.TaskStackContainers} container in {@link DisplayContent}.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.TaskStackContainersTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class TaskStackContainersTests extends WindowTestsBase {
+
+ private TaskStack mPinnedStack;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mPinnedStack = createStackControllerOnStackOnDisplay(
+ WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD, mDisplayContent).mContainer;
+ // Stack should contain visible app window to be considered visible.
+ final Task pinnedTask = createTaskInStack(mPinnedStack, 0 /* userId */);
+ assertFalse(mPinnedStack.isVisible());
+ final WindowTestUtils.TestAppWindowToken pinnedApp =
+ WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ pinnedTask.addChild(pinnedApp, 0 /* addPos */);
+ assertTrue(mPinnedStack.isVisible());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mPinnedStack.removeImmediately();
+ }
+
+ @Test
+ public void testStackPositionChildAt() throws Exception {
+ // Test that always-on-top stack can't be moved to position other than top.
+ final TaskStack stack1 = createTaskStackOnDisplay(mDisplayContent);
+ final TaskStack stack2 = createTaskStackOnDisplay(mDisplayContent);
+
+ final WindowContainer taskStackContainer = stack1.getParent();
+
+ final int stack1Pos = taskStackContainer.mChildren.indexOf(stack1);
+ final int stack2Pos = taskStackContainer.mChildren.indexOf(stack2);
+ final int pinnedStackPos = taskStackContainer.mChildren.indexOf(mPinnedStack);
+ assertGreaterThan(pinnedStackPos, stack2Pos);
+ assertGreaterThan(stack2Pos, stack1Pos);
+
+ taskStackContainer.positionChildAt(WindowContainer.POSITION_BOTTOM, mPinnedStack, false);
+ assertEquals(taskStackContainer.mChildren.get(stack1Pos), stack1);
+ assertEquals(taskStackContainer.mChildren.get(stack2Pos), stack2);
+ assertEquals(taskStackContainer.mChildren.get(pinnedStackPos), mPinnedStack);
+
+ taskStackContainer.positionChildAt(1, mPinnedStack, false);
+ assertEquals(taskStackContainer.mChildren.get(stack1Pos), stack1);
+ assertEquals(taskStackContainer.mChildren.get(stack2Pos), stack2);
+ assertEquals(taskStackContainer.mChildren.get(pinnedStackPos), mPinnedStack);
+ }
+
+ @Test
+ public void testStackPositionBelowPinnedStack() throws Exception {
+ // Test that no stack can be above pinned stack.
+ final TaskStack stack1 = createTaskStackOnDisplay(mDisplayContent);
+
+ final WindowContainer taskStackContainer = stack1.getParent();
+
+ final int stackPos = taskStackContainer.mChildren.indexOf(stack1);
+ final int pinnedStackPos = taskStackContainer.mChildren.indexOf(mPinnedStack);
+ assertGreaterThan(pinnedStackPos, stackPos);
+
+ taskStackContainer.positionChildAt(WindowContainer.POSITION_TOP, stack1, false);
+ assertEquals(taskStackContainer.mChildren.get(stackPos), stack1);
+ assertEquals(taskStackContainer.mChildren.get(pinnedStackPos), mPinnedStack);
+
+ taskStackContainer.positionChildAt(taskStackContainer.mChildren.size() - 1, stack1, false);
+ assertEquals(taskStackContainer.mChildren.get(stackPos), stack1);
+ assertEquals(taskStackContainer.mChildren.get(pinnedStackPos), mPinnedStack);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskStackTests.java b/services/tests/servicestests/src/com/android/server/wm/TaskStackTests.java
new file mode 100644
index 0000000..eaf71f0
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskStackTests.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for the {@link TaskStack} class.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.TaskStackTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class TaskStackTests extends WindowTestsBase {
+
+ @Test
+ public void testStackPositionChildAt() throws Exception {
+ final TaskStack stack = createTaskStackOnDisplay(mDisplayContent);
+ final Task task1 = createTaskInStack(stack, 0 /* userId */);
+ final Task task2 = createTaskInStack(stack, 1 /* userId */);
+
+ // Current user task should be moved to top.
+ stack.positionChildAt(WindowContainer.POSITION_TOP, task1, false /* includingParents */);
+ assertEquals(stack.mChildren.get(0), task2);
+ assertEquals(stack.mChildren.get(1), task1);
+
+ // Non-current user won't be moved to top.
+ stack.positionChildAt(WindowContainer.POSITION_TOP, task2, false /* includingParents */);
+ assertEquals(stack.mChildren.get(0), task2);
+ assertEquals(stack.mChildren.get(1), task1);
+ }
+
+ @Test
+ public void testClosingAppDifferentStackOrientation() throws Exception {
+ final TaskStack stack = createTaskStackOnDisplay(mDisplayContent);
+ final Task task1 = createTaskInStack(stack, 0 /* userId */);
+ WindowTestUtils.TestAppWindowToken appWindowToken1 =
+ WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ task1.addChild(appWindowToken1, 0);
+ appWindowToken1.setOrientation(SCREEN_ORIENTATION_LANDSCAPE);
+
+ final Task task2 = createTaskInStack(stack, 1 /* userId */);
+ WindowTestUtils.TestAppWindowToken appWindowToken2 =
+ WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ task2.addChild(appWindowToken2, 0);
+ appWindowToken2.setOrientation(SCREEN_ORIENTATION_PORTRAIT);
+
+ assertEquals(SCREEN_ORIENTATION_PORTRAIT, stack.getOrientation());
+ sWm.mClosingApps.add(appWindowToken2);
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, stack.getOrientation());
+ }
+
+ @Test
+ public void testMoveTaskToBackDifferentStackOrientation() throws Exception {
+ final TaskStack stack = createTaskStackOnDisplay(mDisplayContent);
+ final Task task1 = createTaskInStack(stack, 0 /* userId */);
+ WindowTestUtils.TestAppWindowToken appWindowToken1 =
+ WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ task1.addChild(appWindowToken1, 0);
+ appWindowToken1.setOrientation(SCREEN_ORIENTATION_LANDSCAPE);
+
+ final Task task2 = createTaskInStack(stack, 1 /* userId */);
+ WindowTestUtils.TestAppWindowToken appWindowToken2 =
+ WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ task2.addChild(appWindowToken2, 0);
+ appWindowToken2.setOrientation(SCREEN_ORIENTATION_PORTRAIT);
+
+ assertEquals(SCREEN_ORIENTATION_PORTRAIT, stack.getOrientation());
+ task2.setSendingToBottom(true);
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, stack.getOrientation());
+ }
+
+ @Test
+ public void testStackRemoveImmediately() throws Exception {
+ final TaskStack stack = createTaskStackOnDisplay(mDisplayContent);
+ final Task task = createTaskInStack(stack, 0 /* userId */);
+ assertEquals(stack, task.mStack);
+
+ // Remove stack and check if its child is also removed.
+ stack.removeImmediately();
+ assertNull(stack.getDisplayContent());
+ assertNull(task.mStack);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskWindowContainerControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/TaskWindowContainerControllerTests.java
new file mode 100644
index 0000000..1dd9365
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskWindowContainerControllerTests.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test class for {@link TaskWindowContainerController}.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.TaskWindowContainerControllerTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class TaskWindowContainerControllerTests extends WindowTestsBase {
+
+ @Test
+ public void testRemoveContainer() throws Exception {
+ final WindowTestUtils.TestTaskWindowContainerController taskController =
+ new WindowTestUtils.TestTaskWindowContainerController(this);
+ final WindowTestUtils.TestAppWindowContainerController appController =
+ new WindowTestUtils.TestAppWindowContainerController(taskController);
+
+ taskController.removeContainer();
+ // Assert that the container was removed.
+ assertNull(taskController.mContainer);
+ assertNull(appController.mContainer);
+ }
+
+ @Test
+ public void testRemoveContainer_deferRemoval() throws Exception {
+ final WindowTestUtils.TestTaskWindowContainerController taskController =
+ new WindowTestUtils.TestTaskWindowContainerController(this);
+ final WindowTestUtils.TestAppWindowContainerController appController =
+ new WindowTestUtils.TestAppWindowContainerController(taskController);
+
+ final WindowTestUtils.TestTask task = (WindowTestUtils.TestTask) taskController.mContainer;
+ final AppWindowToken app = appController.mContainer;
+ task.mShouldDeferRemoval = true;
+
+ taskController.removeContainer();
+ // For the case of deferred removal the task controller will no longer be connected to the
+ // container, but the app controller will still be connected to the its container until
+ // the task window container is removed.
+ assertNull(taskController.mContainer);
+ assertNull(task.getController());
+ assertNotNull(appController.mContainer);
+ assertNotNull(app.getController());
+
+ task.removeImmediately();
+ assertNull(appController.mContainer);
+ assertNull(app.getController());
+ }
+
+ @Test
+ public void testReparent() throws Exception {
+ final StackWindowController stackController1 =
+ createStackControllerOnDisplay(mDisplayContent);
+ final WindowTestUtils.TestTaskWindowContainerController taskController =
+ new WindowTestUtils.TestTaskWindowContainerController(stackController1);
+ final StackWindowController stackController2 =
+ createStackControllerOnDisplay(mDisplayContent);
+ final WindowTestUtils.TestTaskWindowContainerController taskController2 =
+ new WindowTestUtils.TestTaskWindowContainerController(stackController2);
+
+ boolean gotException = false;
+ try {
+ taskController.reparent(stackController1, 0, false/* moveParents */);
+ } catch (IllegalArgumentException e) {
+ gotException = true;
+ }
+ assertTrue("Should not be able to reparent to the same parent", gotException);
+
+ final StackWindowController stackController3 =
+ createStackControllerOnDisplay(mDisplayContent);
+ stackController3.setContainer(null);
+ gotException = false;
+ try {
+ taskController.reparent(stackController3, 0, false/* moveParents */);
+ } catch (IllegalArgumentException e) {
+ gotException = true;
+ }
+ assertTrue("Should not be able to reparent to a stack that doesn't have a container",
+ gotException);
+
+ taskController.reparent(stackController2, 0, false/* moveParents */);
+ assertEquals(stackController2.mContainer, taskController.mContainer.getParent());
+ assertEquals(0, ((WindowTestUtils.TestTask) taskController.mContainer).positionInParent());
+ assertEquals(1, ((WindowTestUtils.TestTask) taskController2.mContainer).positionInParent());
+ }
+
+ @Test
+ public void testReparent_BetweenDisplays() throws Exception {
+ // Create first stack on primary display.
+ final StackWindowController stack1Controller =
+ createStackControllerOnDisplay(mDisplayContent);
+ final TaskStack stack1 = stack1Controller.mContainer;
+ final WindowTestUtils.TestTaskWindowContainerController taskController =
+ new WindowTestUtils.TestTaskWindowContainerController(stack1Controller);
+ final WindowTestUtils.TestTask task1 = (WindowTestUtils.TestTask) taskController.mContainer;
+ task1.mOnDisplayChangedCalled = false;
+ assertEquals(mDisplayContent, stack1.getDisplayContent());
+
+ // Create second display and put second stack on it.
+ final DisplayContent dc = createNewDisplay();
+ final StackWindowController stack2Controller = createStackControllerOnDisplay(dc);
+ final TaskStack stack2 = stack2Controller.mContainer;
+ final WindowTestUtils.TestTaskWindowContainerController taskController2 =
+ new WindowTestUtils.TestTaskWindowContainerController(stack2Controller);
+ final WindowTestUtils.TestTask task2 =
+ (WindowTestUtils.TestTask) taskController2.mContainer;
+
+ // Reparent and check state
+ taskController.reparent(stack2Controller, 0, false /* moveParents */);
+ assertEquals(stack2, task1.getParent());
+ assertEquals(0, task1.positionInParent());
+ assertEquals(1, task2.positionInParent());
+ assertTrue(task1.mOnDisplayChangedCalled);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TestIWindow.java b/services/tests/servicestests/src/com/android/server/wm/TestIWindow.java
new file mode 100644
index 0000000..353aa7d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TestIWindow.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import com.android.internal.os.IResultReceiver;
+
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.MergedConfiguration;
+import android.view.DisplayCutout;
+import android.view.DragEvent;
+import android.view.IWindow;
+
+public class TestIWindow extends IWindow.Stub {
+ @Override
+ public void executeCommand(String command, String parameters,
+ ParcelFileDescriptor descriptor)
+ throws RemoteException {
+
+ }
+
+ @Override
+ public void resized(Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets,
+ Rect stableInsets, Rect outsets, boolean reportDraw, MergedConfiguration mergedConfig,
+ Rect backDropFrame, boolean forceLayout, boolean alwaysConsumeNavBar, int displayId,
+ DisplayCutout.ParcelableWrapper displayCutout)
+ throws RemoteException {
+
+ }
+
+ @Override
+ public void moved(int newX, int newY) throws RemoteException {
+
+ }
+
+ @Override
+ public void dispatchAppVisibility(boolean visible) throws RemoteException {
+
+ }
+
+ @Override
+ public void dispatchGetNewSurface() throws RemoteException {
+
+ }
+
+ @Override
+ public void windowFocusChanged(boolean hasFocus, boolean inTouchMode)
+ throws RemoteException {
+
+ }
+
+ @Override
+ public void closeSystemDialogs(String reason) throws RemoteException {
+
+ }
+
+ @Override
+ public void dispatchWallpaperOffsets(float x, float y, float xStep, float yStep,
+ boolean sync)
+ throws RemoteException {
+
+ }
+
+ @Override
+ public void dispatchWallpaperCommand(String action, int x, int y, int z, Bundle extras,
+ boolean sync) throws RemoteException {
+
+ }
+
+ @Override
+ public void dispatchDragEvent(DragEvent event) throws RemoteException {
+
+ }
+
+ @Override
+ public void updatePointerIcon(float x, float y) throws RemoteException {
+
+ }
+
+ @Override
+ public void dispatchSystemUiVisibilityChanged(int seq, int globalVisibility, int localValue,
+ int localChanges) throws RemoteException {
+
+ }
+
+ @Override
+ public void dispatchWindowShown() throws RemoteException {
+
+ }
+
+ @Override
+ public void requestAppKeyboardShortcuts(IResultReceiver receiver, int deviceId)
+ throws RemoteException {
+
+ }
+
+ @Override
+ public void dispatchPointerCaptureChanged(boolean hasCapture) {
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/servicestests/src/com/android/server/wm/TestWindowManagerPolicy.java
new file mode 100644
index 0000000..ee028ba
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TestWindowManagerPolicy.java
@@ -0,0 +1,606 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
+import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
+
+import static org.mockito.Mockito.mock;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.CompatibilityInfo;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.proto.ProtoOutputStream;
+import android.view.Display;
+import android.view.DisplayCutout;
+import android.view.IWindow;
+import android.view.IWindowManager;
+import android.view.KeyEvent;
+import android.view.WindowManager;
+import android.view.animation.Animation;
+
+import com.android.internal.policy.IKeyguardDismissCallback;
+import com.android.internal.policy.IShortcutService;
+import com.android.server.policy.WindowManagerPolicy;
+
+import java.io.PrintWriter;
+import java.util.function.Supplier;
+
+class TestWindowManagerPolicy implements WindowManagerPolicy {
+ private static final String TAG = "TestWindowManagerPolicy";
+
+ private final Supplier<WindowManagerService> mWmSupplier;
+
+ int rotationToReport = 0;
+ boolean keyguardShowingAndNotOccluded = false;
+
+ private Runnable mRunnableWhenAddingSplashScreen;
+
+ public TestWindowManagerPolicy(Supplier<WindowManagerService> wmSupplier) {
+
+ mWmSupplier = wmSupplier;
+ }
+
+ @Override
+ public void registerShortcutKey(long shortcutCode, IShortcutService shortcutKeyReceiver)
+ throws RemoteException {
+
+ }
+
+ @Override
+ public void init(Context context, IWindowManager windowManager,
+ WindowManagerFuncs windowManagerFuncs) {
+
+ }
+
+ @Override
+ public boolean isDefaultOrientationForced() {
+ return false;
+ }
+
+ @Override
+ public void setInitialDisplaySize(Display display, int width, int height, int density) {
+
+ }
+
+ @Override
+ public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {
+ return 0;
+ }
+
+ @Override
+ public boolean checkShowToOwnerOnly(WindowManager.LayoutParams attrs) {
+ return false;
+ }
+
+ @Override
+ public void adjustWindowParamsLw(WindowState win, WindowManager.LayoutParams attrs,
+ boolean hasStatusBarServicePermission) {
+ }
+
+ @Override
+ public void adjustConfigurationLw(Configuration config, int keyboardPresence,
+ int navigationPresence) {
+
+ }
+
+ @Override
+ public int getMaxWallpaperLayer() {
+ return 0;
+ }
+
+ @Override
+ public int getNonDecorDisplayWidth(int fullWidth, int fullHeight, int rotation, int uiMode,
+ int displayId, DisplayCutout displayCutout) {
+ return 0;
+ }
+
+ @Override
+ public int getNonDecorDisplayHeight(int fullWidth, int fullHeight, int rotation, int uiMode,
+ int displayId, DisplayCutout displayCutout) {
+ return 0;
+ }
+
+ @Override
+ public int getConfigDisplayWidth(int fullWidth, int fullHeight, int rotation, int uiMode,
+ int displayId, DisplayCutout displayCutout) {
+ return 0;
+ }
+
+ @Override
+ public int getConfigDisplayHeight(int fullWidth, int fullHeight, int rotation, int uiMode,
+ int displayId, DisplayCutout displayCutout) {
+ return 0;
+ }
+
+ @Override
+ public boolean isKeyguardHostWindow(WindowManager.LayoutParams attrs) {
+ return attrs.type == TYPE_STATUS_BAR;
+ }
+
+ @Override
+ public boolean canBeHiddenByKeyguardLw(WindowState win) {
+ return false;
+ }
+
+ /**
+ * Sets a runnable to run when adding a splash screen which gets executed after the window has
+ * been added but before returning the surface.
+ */
+ void setRunnableWhenAddingSplashScreen(Runnable r) {
+ mRunnableWhenAddingSplashScreen = r;
+ }
+
+ @Override
+ public StartingSurface addSplashScreen(IBinder appToken, String packageName, int theme,
+ CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes, int icon,
+ int logo, int windowFlags, Configuration overrideConfig, int displayId) {
+ final com.android.server.wm.WindowState window;
+ final AppWindowToken atoken;
+ final WindowManagerService wm = mWmSupplier.get();
+ synchronized (wm.mWindowMap) {
+ atoken = wm.mRoot.getAppWindowToken(appToken);
+ window = WindowTestsBase.createWindow(null, TYPE_APPLICATION_STARTING, atoken,
+ "Starting window", 0 /* ownerId */, false /* internalWindows */, wm,
+ mock(Session.class), mock(IWindow.class));
+ atoken.startingWindow = window;
+ }
+ if (mRunnableWhenAddingSplashScreen != null) {
+ mRunnableWhenAddingSplashScreen.run();
+ mRunnableWhenAddingSplashScreen = null;
+ }
+ return () -> {
+ synchronized (wm.mWindowMap) {
+ atoken.removeChild(window);
+ atoken.startingWindow = null;
+ }
+ };
+ }
+
+ @Override
+ public int prepareAddWindowLw(WindowState win,
+ WindowManager.LayoutParams attrs) {
+ return 0;
+ }
+
+ @Override
+ public void removeWindowLw(WindowState win) {
+
+ }
+
+ @Override
+ public int selectAnimationLw(WindowState win, int transit) {
+ return 0;
+ }
+
+ @Override
+ public void selectRotationAnimationLw(int[] anim) {
+
+ }
+
+ @Override
+ public boolean validateRotationAnimationLw(int exitAnimId, int enterAnimId,
+ boolean forceDefault) {
+ return false;
+ }
+
+ @Override
+ public Animation createHiddenByKeyguardExit(boolean onWallpaper,
+ boolean goingToNotificationShade) {
+ return null;
+ }
+
+ @Override
+ public Animation createKeyguardWallpaperExit(boolean goingToNotificationShade) {
+ return null;
+ }
+
+ @Override
+ public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
+ return 0;
+ }
+
+ @Override
+ public int interceptMotionBeforeQueueingNonInteractive(long whenNanos, int policyFlags) {
+ return 0;
+ }
+
+ @Override
+ public long interceptKeyBeforeDispatching(WindowState win, KeyEvent event,
+ int policyFlags) {
+ return 0;
+ }
+
+ @Override
+ public KeyEvent dispatchUnhandledKey(WindowState win, KeyEvent event,
+ int policyFlags) {
+ return null;
+ }
+
+ @Override
+ public int getSystemDecorLayerLw() {
+ return 0;
+ }
+
+ @Override
+ public void beginPostLayoutPolicyLw(int displayWidth, int displayHeight) {
+
+ }
+
+ @Override
+ public void applyPostLayoutPolicyLw(WindowState win,
+ WindowManager.LayoutParams attrs, WindowState attached, WindowState imeTarget) {
+ }
+
+ @Override
+ public int finishPostLayoutPolicyLw() {
+ return 0;
+ }
+
+ @Override
+ public boolean allowAppAnimationsLw() {
+ return false;
+ }
+
+ @Override
+ public int focusChangedLw(WindowState lastFocus,
+ WindowState newFocus) {
+ return 0;
+ }
+
+ @Override
+ public void startedWakingUp() {
+
+ }
+
+ @Override
+ public void finishedWakingUp() {
+
+ }
+
+ @Override
+ public void startedGoingToSleep(int why) {
+
+ }
+
+ @Override
+ public void finishedGoingToSleep(int why) {
+
+ }
+
+ @Override
+ public void screenTurningOn(ScreenOnListener screenOnListener) {
+
+ }
+
+ @Override
+ public void screenTurnedOn() {
+
+ }
+
+ @Override
+ public void screenTurningOff(ScreenOffListener screenOffListener) {
+
+ }
+
+ @Override
+ public void screenTurnedOff() {
+
+ }
+
+ @Override
+ public boolean isScreenOn() {
+ return true;
+ }
+
+ @Override
+ public boolean okToAnimate() {
+ return true;
+ }
+
+ @Override
+ public void notifyLidSwitchChanged(long whenNanos, boolean lidOpen) {
+
+ }
+
+ @Override
+ public void notifyCameraLensCoverSwitchChanged(long whenNanos, boolean lensCovered) {
+
+ }
+
+ @Override
+ public void enableKeyguard(boolean enabled) {
+
+ }
+
+ @Override
+ public void exitKeyguardSecurely(OnKeyguardExitResult callback) {
+
+ }
+
+ @Override
+ public boolean isKeyguardLocked() {
+ return keyguardShowingAndNotOccluded;
+ }
+
+ @Override
+ public boolean isKeyguardSecure(int userId) {
+ return false;
+ }
+
+ @Override
+ public boolean isKeyguardOccluded() {
+ return false;
+ }
+
+ @Override
+ public boolean isKeyguardTrustedLw() {
+ return false;
+ }
+
+ @Override
+ public boolean isKeyguardShowingAndNotOccluded() {
+ return keyguardShowingAndNotOccluded;
+ }
+
+ @Override
+ public boolean inKeyguardRestrictedKeyInputMode() {
+ return false;
+ }
+
+ @Override
+ public void dismissKeyguardLw(@Nullable IKeyguardDismissCallback callback,
+ CharSequence message) {
+ }
+
+ @Override
+ public boolean isKeyguardDrawnLw() {
+ return false;
+ }
+
+ @Override
+ public boolean isShowingDreamLw() {
+ return false;
+ }
+
+ @Override
+ public void onKeyguardOccludedChangedLw(boolean occluded) {
+ }
+
+ @Override
+ public int rotationForOrientationLw(int orientation, int lastRotation, boolean defaultDisplay) {
+ return rotationToReport;
+ }
+
+ @Override
+ public boolean rotationHasCompatibleMetricsLw(int orientation, int rotation) {
+ return true;
+ }
+
+ @Override
+ public void setRotationLw(int rotation) {
+
+ }
+
+ @Override
+ public void setSafeMode(boolean safeMode) {
+
+ }
+
+ @Override
+ public void systemReady() {
+
+ }
+
+ @Override
+ public void systemBooted() {
+
+ }
+
+ @Override
+ public void showBootMessage(CharSequence msg, boolean always) {
+
+ }
+
+ @Override
+ public void hideBootMessages() {
+
+ }
+
+ @Override
+ public void userActivity() {
+
+ }
+
+ @Override
+ public void enableScreenAfterBoot() {
+
+ }
+
+ @Override
+ public void setCurrentOrientationLw(int newOrientation) {
+
+ }
+
+ @Override
+ public boolean performHapticFeedbackLw(WindowState win, int effectId,
+ boolean always) {
+ return false;
+ }
+
+ @Override
+ public void keepScreenOnStartedLw() {
+
+ }
+
+ @Override
+ public void keepScreenOnStoppedLw() {
+
+ }
+
+ @Override
+ public int getUserRotationMode() {
+ return 0;
+ }
+
+ @Override
+ public void setUserRotationMode(int mode,
+ int rotation) {
+
+ }
+
+ @Override
+ public int adjustSystemUiVisibilityLw(int visibility) {
+ return 0;
+ }
+
+ @Override
+ public boolean hasNavigationBar() {
+ return false;
+ }
+
+ @Override
+ public void lockNow(Bundle options) {
+
+ }
+
+ @Override
+ public void setLastInputMethodWindowLw(WindowState ime,
+ WindowState target) {
+
+ }
+
+ @Override
+ public void showRecentApps() {
+
+ }
+
+ @Override
+ public void showGlobalActions() {
+
+ }
+
+ @Override
+ public void setCurrentUserLw(int newUserId) {
+
+ }
+
+ @Override
+ public void setSwitchingUser(boolean switching) {
+
+ }
+
+ @Override
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+
+ }
+
+ @Override
+ public void dump(String prefix, PrintWriter writer, String[] args) {
+
+ }
+
+ @Override
+ public boolean isTopLevelWindow(int windowType) {
+ return false;
+ }
+
+ @Override
+ public void startKeyguardExitAnimation(long startTime, long fadeoutDuration) {
+
+ }
+
+ @Override
+ public void getStableInsetsLw(int displayRotation, int displayWidth, int displayHeight,
+ DisplayCutout cutout, Rect outInsets) {
+
+ }
+
+ @Override
+ public boolean isNavBarForcedShownLw(WindowState win) {
+ return false;
+ }
+
+ @NavigationBarPosition
+ @Override
+ public int getNavBarPosition() {
+ return NAV_BAR_BOTTOM;
+ }
+
+ @Override
+ public void getNonDecorInsetsLw(int displayRotation, int displayWidth, int displayHeight,
+ DisplayCutout cutout, Rect outInsets) {
+
+ }
+
+ @Override
+ public boolean isDockSideAllowed(int dockSide, int originalDockSide, int displayWidth,
+ int displayHeight, int displayRotation) {
+ return false;
+ }
+
+ @Override
+ public void onConfigurationChanged() {
+
+ }
+
+ @Override
+ public boolean shouldRotateSeamlessly(int oldRotation, int newRotation) {
+ return false;
+ }
+
+ @Override
+ public void setPipVisibilityLw(boolean visible) {
+
+ }
+
+ @Override
+ public void setRecentsVisibilityLw(boolean visible) {
+
+ }
+
+ @Override
+ public void setNavBarVirtualKeyHapticFeedbackEnabledLw(boolean enabled) {
+ }
+
+ @Override
+ public void onSystemUiStarted() {
+ }
+
+ @Override
+ public boolean canDismissBootAnimation() {
+ return true;
+ }
+
+ @Override
+ public void requestUserActivityNotification() {
+ }
+
+ @Override
+ public void onLockTaskStateChangedLw(int lockTaskState) {
+ }
+
+ @Override
+ public boolean setAodShowing(boolean aodShowing) {
+ return false;
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java b/services/tests/servicestests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java
new file mode 100644
index 0000000..a5c47de
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test class for {@link AppTransition}.
+ *
+ * runtest frameworks-services -c com.android.server.wm.UnknownAppVisibilityControllerTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class UnknownAppVisibilityControllerTest extends WindowTestsBase {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ sWm.mUnknownAppVisibilityController.clear();
+ }
+
+ @Test
+ public void testFlow() throws Exception {
+ final AppWindowToken token = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ sWm.mUnknownAppVisibilityController.notifyLaunched(token);
+ sWm.mUnknownAppVisibilityController.notifyAppResumedFinished(token);
+ sWm.mUnknownAppVisibilityController.notifyRelayouted(token);
+
+ // Make sure our handler processed the message.
+ Thread.sleep(100);
+ assertTrue(sWm.mUnknownAppVisibilityController.allResolved());
+ }
+
+ @Test
+ public void testMultiple() throws Exception {
+ final AppWindowToken token1 = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ final AppWindowToken token2 = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ sWm.mUnknownAppVisibilityController.notifyLaunched(token1);
+ sWm.mUnknownAppVisibilityController.notifyAppResumedFinished(token1);
+ sWm.mUnknownAppVisibilityController.notifyLaunched(token2);
+ sWm.mUnknownAppVisibilityController.notifyRelayouted(token1);
+ sWm.mUnknownAppVisibilityController.notifyAppResumedFinished(token2);
+ sWm.mUnknownAppVisibilityController.notifyRelayouted(token2);
+
+ // Make sure our handler processed the message.
+ Thread.sleep(100);
+ assertTrue(sWm.mUnknownAppVisibilityController.allResolved());
+ }
+
+ @Test
+ public void testClear() throws Exception {
+ final AppWindowToken token = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ sWm.mUnknownAppVisibilityController.notifyLaunched(token);
+ sWm.mUnknownAppVisibilityController.clear();;
+ assertTrue(sWm.mUnknownAppVisibilityController.allResolved());
+ }
+
+ @Test
+ public void testAppRemoved() throws Exception {
+ final AppWindowToken token = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ sWm.mUnknownAppVisibilityController.notifyLaunched(token);
+ sWm.mUnknownAppVisibilityController.appRemovedOrHidden(token);
+ assertTrue(sWm.mUnknownAppVisibilityController.allResolved());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/WallpaperControllerTests.java
new file mode 100644
index 0000000..71ead20
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WallpaperControllerTests.java
@@ -0,0 +1,67 @@
+package com.android.server.wm;
+
+import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+
+import static junit.framework.TestCase.assertNotNull;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Bitmap;
+import android.os.IBinder;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for the {@link WallpaperController} class.
+ *
+ * Build/Install/Run:
+ * atest com.android.server.wm.WallpaperControllerTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WallpaperControllerTests extends WindowTestsBase {
+ @Test
+ public void testWallpaperScreenshot() {
+ WindowSurfaceController windowSurfaceController = mock(WindowSurfaceController.class);
+
+ synchronized (sWm.mWindowMap) {
+ // No wallpaper
+ final DisplayContent dc = createNewDisplay();
+ Bitmap wallpaperBitmap = sWm.mRoot.mWallpaperController.screenshotWallpaperLocked();
+ assertNull(wallpaperBitmap);
+
+ // No wallpaper WSA Surface
+ WindowToken wallpaperWindowToken = new WallpaperWindowToken(sWm, mock(IBinder.class),
+ true, dc, true /* ownerCanManageAppTokens */);
+ WindowState wallpaperWindow = createWindow(null /* parent */, TYPE_WALLPAPER,
+ wallpaperWindowToken, "wallpaperWindow");
+ wallpaperBitmap = mWallpaperController.screenshotWallpaperLocked();
+ assertNull(wallpaperBitmap);
+
+ // Wallpaper with not visible WSA surface.
+ wallpaperWindow.mWinAnimator.mSurfaceController = windowSurfaceController;
+ wallpaperWindow.mWinAnimator.mLastAlpha = 1;
+ wallpaperBitmap = mWallpaperController.screenshotWallpaperLocked();
+ assertNull(wallpaperBitmap);
+
+ when(windowSurfaceController.getShown()).thenReturn(true);
+
+ // Wallpaper with WSA alpha set to 0.
+ wallpaperWindow.mWinAnimator.mLastAlpha = 0;
+ wallpaperBitmap = mWallpaperController.screenshotWallpaperLocked();
+ assertNull(wallpaperBitmap);
+
+ // Wallpaper window with WSA Surface
+ wallpaperWindow.mWinAnimator.mLastAlpha = 1;
+ wallpaperBitmap = mWallpaperController.screenshotWallpaperLocked();
+ assertNotNull(wallpaperBitmap);
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowAnimationSpecTest.java b/services/tests/servicestests/src/com/android/server/wm/WindowAnimationSpecTest.java
new file mode 100644
index 0000000..ca520ed
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowAnimationSpecTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static com.android.server.wm.WindowStateAnimator.STACK_CLIP_AFTER_ANIM;
+import static com.android.server.wm.WindowStateAnimator.STACK_CLIP_BEFORE_ANIM;
+import static com.android.server.wm.WindowStateAnimator.STACK_CLIP_NONE;
+
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.view.animation.ClipRectAnimation;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for the {@link WindowAnimationSpec} class.
+ *
+ * atest FrameworksServicesTests:com.android.server.wm.WindowAnimationSpecTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WindowAnimationSpecTest {
+ private final SurfaceControl mSurfaceControl = mock(SurfaceControl.class);
+ private final SurfaceControl.Transaction mTransaction = mock(SurfaceControl.Transaction.class);
+ private final Animation mAnimation = mock(Animation.class);
+ private final Rect mStackBounds = new Rect(0, 0, 10, 10);
+
+ @Test
+ public void testApply_clipNone() {
+ Rect windowCrop = new Rect(0, 0, 20, 20);
+ Animation a = createClipRectAnimation(windowCrop, windowCrop);
+ WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(a, null,
+ mStackBounds, false /* canSkipFirstFrame */, STACK_CLIP_NONE,
+ true /* isAppAnimation */);
+ windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
+ verify(mTransaction).setWindowCrop(eq(mSurfaceControl),
+ argThat(rect -> rect.equals(windowCrop)));
+ }
+
+ @Test
+ public void testApply_clipAfter() {
+ WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(mAnimation, null,
+ mStackBounds, false /* canSkipFirstFrame */, STACK_CLIP_AFTER_ANIM,
+ true /* isAppAnimation */);
+ windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
+ verify(mTransaction).setWindowCrop(eq(mSurfaceControl), argThat(Rect::isEmpty));
+ verify(mTransaction).setFinalCrop(eq(mSurfaceControl),
+ argThat(rect -> rect.equals(mStackBounds)));
+ }
+
+ @Test
+ public void testApply_clipAfterOffsetPosition() {
+ // Stack bounds is (0, 0, 10, 10) position is (20, 40)
+ WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(mAnimation,
+ new Point(20, 40), mStackBounds, false /* canSkipFirstFrame */,
+ STACK_CLIP_AFTER_ANIM,
+ true /* isAppAnimation */);
+ windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
+ verify(mTransaction).setWindowCrop(eq(mSurfaceControl), argThat(Rect::isEmpty));
+ verify(mTransaction).setFinalCrop(eq(mSurfaceControl),
+ argThat(rect -> rect.left == 20 && rect.top == 40 && rect.right == 30
+ && rect.bottom == 50));
+ }
+
+ @Test
+ public void testApply_clipBeforeNoAnimationBounds() {
+ // Stack bounds is (0, 0, 10, 10) animation clip is (0, 0, 0, 0)
+ WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(mAnimation, null,
+ mStackBounds, false /* canSkipFirstFrame */, STACK_CLIP_BEFORE_ANIM,
+ true /* isAppAnimation */);
+ windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
+ verify(mTransaction).setWindowCrop(eq(mSurfaceControl),
+ argThat(rect -> rect.equals(mStackBounds)));
+ }
+
+ @Test
+ public void testApply_clipBeforeNoStackBounds() {
+ // Stack bounds is (0, 0, 0, 0) animation clip is (0, 0, 20, 20)
+ Rect windowCrop = new Rect(0, 0, 20, 20);
+ Animation a = createClipRectAnimation(windowCrop, windowCrop);
+ a.initialize(0, 0, 0, 0);
+ WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(a, null,
+ null, false /* canSkipFirstFrame */, STACK_CLIP_BEFORE_ANIM,
+ true /* isAppAnimation */);
+ windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
+ verify(mTransaction).setWindowCrop(eq(mSurfaceControl), argThat(Rect::isEmpty));
+ }
+
+ @Test
+ public void testApply_clipBeforeSmallerAnimationClip() {
+ // Stack bounds is (0, 0, 10, 10) animation clip is (0, 0, 5, 5)
+ Rect windowCrop = new Rect(0, 0, 5, 5);
+ Animation a = createClipRectAnimation(windowCrop, windowCrop);
+ WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(a, null,
+ mStackBounds, false /* canSkipFirstFrame */, STACK_CLIP_BEFORE_ANIM,
+ true /* isAppAnimation */);
+ windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
+ verify(mTransaction).setWindowCrop(eq(mSurfaceControl),
+ argThat(rect -> rect.equals(windowCrop)));
+ }
+
+ @Test
+ public void testApply_clipBeforeSmallerStackClip() {
+ // Stack bounds is (0, 0, 10, 10) animation clip is (0, 0, 20, 20)
+ Rect windowCrop = new Rect(0, 0, 20, 20);
+ Animation a = createClipRectAnimation(windowCrop, windowCrop);
+ WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(a, null,
+ mStackBounds, false /* canSkipFirstFrame */, STACK_CLIP_BEFORE_ANIM,
+ true /* isAppAnimation */);
+ windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
+ verify(mTransaction).setWindowCrop(eq(mSurfaceControl),
+ argThat(rect -> rect.equals(mStackBounds)));
+ }
+
+ private Animation createClipRectAnimation(Rect fromClip, Rect toClip) {
+ Animation a = new ClipRectAnimation(fromClip, toClip);
+ a.initialize(0, 0, 0, 0);
+ return a;
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowConfigurationTests.java b/services/tests/servicestests/src/com/android/server/wm/WindowConfigurationTests.java
new file mode 100644
index 0000000..513c1ec
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowConfigurationTests.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import org.junit.Test;
+
+import android.app.WindowConfiguration;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.DisplayInfo;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOW_CONFIG_APP_BOUNDS;
+import static android.app.WindowConfiguration.WINDOW_CONFIG_WINDOWING_MODE;
+import static android.content.pm.ActivityInfo.CONFIG_WINDOW_CONFIGURATION;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test class to for {@link android.app.WindowConfiguration}.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.WindowConfigurationTests
+ */
+@SmallTest
+@FlakyTest(bugId = 74078662)
+@Presubmit
+@org.junit.runner.RunWith(AndroidJUnit4.class)
+public class WindowConfigurationTests extends WindowTestsBase {
+ private Rect mParentBounds;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mParentBounds = new Rect(10 /*left*/, 30 /*top*/, 80 /*right*/, 60 /*bottom*/);
+ }
+
+ /** Tests {@link android.app.WindowConfiguration#diff(WindowConfiguration, boolean)}. */
+ @Test
+ public void testDiff() {
+ final Configuration config1 = new Configuration();
+ final WindowConfiguration winConfig1 = config1.windowConfiguration;
+ final Configuration config2 = new Configuration();
+ final WindowConfiguration winConfig2 = config2.windowConfiguration;
+ final Configuration config3 = new Configuration();
+ final WindowConfiguration winConfig3 = config3.windowConfiguration;
+
+ winConfig1.setAppBounds(0, 1, 1, 0);
+ winConfig2.setAppBounds(1, 2, 2, 1);
+ winConfig3.setAppBounds(winConfig1.getAppBounds());
+
+
+ assertEquals(CONFIG_WINDOW_CONFIGURATION, config1.diff(config2));
+ assertEquals(0, config1.diffPublicOnly(config2));
+ assertEquals(WINDOW_CONFIG_APP_BOUNDS,
+ winConfig1.diff(winConfig2, false /* compareUndefined */));
+
+ winConfig2.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ assertEquals(WINDOW_CONFIG_APP_BOUNDS | WINDOW_CONFIG_WINDOWING_MODE,
+ winConfig1.diff(winConfig2, false /* compareUndefined */));
+
+ assertEquals(0, config1.diff(config3));
+ assertEquals(0, config1.diffPublicOnly(config3));
+ assertEquals(0, winConfig1.diff(winConfig3, false /* compareUndefined */));
+ }
+
+ /** Tests {@link android.app.WindowConfiguration#compareTo(WindowConfiguration)}. */
+ @Test
+ public void testConfigurationCompareTo() throws Exception {
+ final Configuration blankConfig = new Configuration();
+ final WindowConfiguration blankWinConfig = new WindowConfiguration();
+
+ final Configuration config1 = new Configuration();
+ final WindowConfiguration winConfig1 = config1.windowConfiguration;
+ winConfig1.setAppBounds(1, 2, 3, 4);
+
+ final Configuration config2 = new Configuration(config1);
+ final WindowConfiguration winConfig2 = config2.windowConfiguration;
+
+ assertEquals(config1.compareTo(config2), 0);
+ assertEquals(winConfig1.compareTo(winConfig2), 0);
+
+ // Different windowing mode
+ winConfig2.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ assertNotEquals(config1.compareTo(config2), 0);
+ assertNotEquals(winConfig1.compareTo(winConfig2), 0);
+ winConfig2.setWindowingMode(winConfig1.getWindowingMode());
+
+ // Different bounds
+ winConfig2.setAppBounds(0, 2, 3, 4);
+ assertNotEquals(config1.compareTo(config2), 0);
+ assertNotEquals(winConfig1.compareTo(winConfig2), 0);
+
+ // No bounds
+ assertEquals(config1.compareTo(blankConfig), -1);
+ assertEquals(winConfig1.compareTo(blankWinConfig), -1);
+
+ assertEquals(blankConfig.compareTo(config1), 1);
+ assertEquals(blankWinConfig.compareTo(winConfig1), 1);
+ }
+
+ @Test
+ public void testSetActivityType() throws Exception {
+ final WindowConfiguration config = new WindowConfiguration();
+ config.setActivityType(ACTIVITY_TYPE_HOME);
+ assertEquals(ACTIVITY_TYPE_HOME, config.getActivityType());
+
+ // Allowed to change from app process.
+ config.setActivityType(ACTIVITY_TYPE_STANDARD);
+ assertEquals(ACTIVITY_TYPE_STANDARD, config.getActivityType());
+ }
+
+ /** Ensures the configuration app bounds at the root level match the app dimensions. */
+ @Test
+ public void testAppBounds_RootConfigurationBounds() throws Exception {
+ final DisplayInfo info = mDisplayContent.getDisplayInfo();
+ info.appWidth = 1024;
+ info.appHeight = 768;
+
+ final Rect appBounds = sWm.computeNewConfiguration(
+ mDisplayContent.getDisplayId()).windowConfiguration.getAppBounds();
+ // The bounds should always be positioned in the top left.
+ assertEquals(appBounds.left, 0);
+ assertEquals(appBounds.top, 0);
+
+ // The bounds should equal the defined app width and height
+ assertEquals(appBounds.width(), info.appWidth);
+ assertEquals(appBounds.height(), info.appHeight);
+ }
+
+ /** Ensures that bounds are clipped to their parent. */
+ @Test
+ public void testAppBounds_BoundsClipping() throws Exception {
+ final Rect shiftedBounds = new Rect(mParentBounds);
+ shiftedBounds.offset(10, 10);
+ final Rect expectedBounds = new Rect(mParentBounds);
+ expectedBounds.intersect(shiftedBounds);
+ testStackBoundsConfiguration(WINDOWING_MODE_FULLSCREEN, mParentBounds, shiftedBounds,
+ expectedBounds);
+ }
+
+ /** Ensures that empty bounds are not propagated to the configuration. */
+ @Test
+ public void testAppBounds_EmptyBounds() throws Exception {
+ final Rect emptyBounds = new Rect();
+ testStackBoundsConfiguration(WINDOWING_MODE_FULLSCREEN, mParentBounds, emptyBounds,
+ null /*ExpectedBounds*/);
+ }
+
+ /** Ensures that bounds on freeform stacks are not clipped. */
+ @Test
+ public void testAppBounds_FreeFormBounds() throws Exception {
+ final Rect freeFormBounds = new Rect(mParentBounds);
+ freeFormBounds.offset(10, 10);
+ testStackBoundsConfiguration(WINDOWING_MODE_FREEFORM, mParentBounds, freeFormBounds,
+ freeFormBounds);
+ }
+
+ /** Ensures that fully contained bounds are not clipped. */
+ @Test
+ public void testAppBounds_ContainedBounds() throws Exception {
+ final Rect insetBounds = new Rect(mParentBounds);
+ insetBounds.inset(5, 5, 5, 5);
+ testStackBoundsConfiguration(
+ WINDOWING_MODE_FULLSCREEN, mParentBounds, insetBounds, insetBounds);
+ }
+
+ /** Ensures that full screen free form bounds are clipped */
+ @Test
+ public void testAppBounds_FullScreenFreeFormBounds() throws Exception {
+ final Rect fullScreenBounds = new Rect(0, 0, mDisplayInfo.logicalWidth,
+ mDisplayInfo.logicalHeight);
+ testStackBoundsConfiguration(WINDOWING_MODE_FULLSCREEN, mParentBounds, fullScreenBounds,
+ mParentBounds);
+ }
+
+ private void testStackBoundsConfiguration(int windowingMode, Rect parentBounds, Rect bounds,
+ Rect expectedConfigBounds) {
+ final StackWindowController stackController = createStackControllerOnStackOnDisplay(
+ windowingMode, ACTIVITY_TYPE_STANDARD, mDisplayContent);
+
+ final Configuration parentConfig = mDisplayContent.getConfiguration();
+ parentConfig.windowConfiguration.setAppBounds(parentBounds);
+
+ final Configuration config = new Configuration();
+ final WindowConfiguration winConfig = config.windowConfiguration;
+ stackController.adjustConfigurationForBounds(bounds, null /*insetBounds*/,
+ new Rect() /*nonDecorBounds*/, new Rect() /*stableBounds*/, false /*overrideWidth*/,
+ false /*overrideHeight*/, mDisplayInfo.logicalDensityDpi, config, parentConfig,
+ windowingMode);
+ // Assert that both expected and actual are null or are equal to each other
+
+ assertEquals(expectedConfigBounds, winConfig.getAppBounds());
+ }
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowContainerControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/WindowContainerControllerTests.java
new file mode 100644
index 0000000..502cb6e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowContainerControllerTests.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import android.app.WindowConfiguration;
+import android.content.res.Configuration;
+import android.support.test.filters.FlakyTest;
+import org.junit.Test;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.content.res.Configuration.EMPTY;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test class for {@link WindowContainerController}.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.WindowContainerControllerTests
+ */
+@SmallTest
+@Presubmit
+@FlakyTest(bugId = 74078662)
+@org.junit.runner.RunWith(AndroidJUnit4.class)
+public class WindowContainerControllerTests extends WindowTestsBase {
+
+ @Test
+ public void testCreation() throws Exception {
+ final WindowContainerController controller = new WindowContainerController(null, sWm);
+ final WindowContainer container = new WindowContainer(sWm);
+
+ container.setController(controller);
+ assertEquals(controller, container.getController());
+ assertEquals(controller.mContainer, container);
+ }
+
+ @Test
+ public void testSetContainer() throws Exception {
+ final WindowContainerController controller = new WindowContainerController(null, sWm);
+ final WindowContainer container = new WindowContainer(sWm);
+
+ controller.setContainer(container);
+ assertEquals(controller.mContainer, container);
+
+ // Assert we can't change the container to another one once set
+ boolean gotException = false;
+ try {
+ controller.setContainer(new WindowContainer(sWm));
+ } catch (IllegalArgumentException e) {
+ gotException = true;
+ }
+ assertTrue(gotException);
+
+ // Assert that we can set the container to null.
+ controller.setContainer(null);
+ assertNull(controller.mContainer);
+ }
+
+ @Test
+ public void testRemoveContainer() throws Exception {
+ final WindowContainerController controller = new WindowContainerController(null, sWm);
+ final WindowContainer container = new WindowContainer(sWm);
+
+ controller.setContainer(container);
+ assertEquals(controller.mContainer, container);
+
+ controller.removeContainer();
+ assertNull(controller.mContainer);
+ }
+
+ @Test
+ public void testOnOverrideConfigurationChanged() throws Exception {
+ final WindowContainerController controller = new WindowContainerController(null, sWm);
+ final WindowContainer container = new WindowContainer(sWm);
+
+ controller.setContainer(container);
+ assertEquals(controller.mContainer, container);
+ assertEquals(EMPTY, container.getOverrideConfiguration());
+
+ final Configuration config = new Configuration();
+ config.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ config.windowConfiguration.setAppBounds(10, 10, 10, 10);
+
+ // Assert that the config change through the controller is propagated to the container.
+ controller.onOverrideConfigurationChanged(config);
+ assertEquals(config, container.getOverrideConfiguration());
+
+ // Assert the container configuration isn't changed after removal from the controller.
+ controller.removeContainer();
+ controller.onOverrideConfigurationChanged(EMPTY);
+ assertEquals(config, container.getOverrideConfiguration());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/servicestests/src/com/android/server/wm/WindowContainerTests.java
new file mode 100644
index 0000000..6c7830e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowContainerTests.java
@@ -0,0 +1,872 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import android.support.test.filters.FlakyTest;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.util.Comparator;
+
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+
+import static com.android.server.wm.WindowContainer.POSITION_BOTTOM;
+import static com.android.server.wm.WindowContainer.POSITION_TOP;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyFloat;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Test class for {@link WindowContainer}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.wm.WindowContainerTests
+ */
+@SmallTest
+@Presubmit
+@FlakyTest(bugId = 74078662)
+@RunWith(AndroidJUnit4.class)
+public class WindowContainerTests extends WindowTestsBase {
+
+ @Test
+ public void testCreation() throws Exception {
+ final TestWindowContainer w = new TestWindowContainerBuilder().setLayer(0).build();
+ assertNull("window must have no parent", w.getParentWindow());
+ assertEquals("window must have no children", 0, w.getChildrenCount());
+ }
+
+ @Test
+ public void testAdd() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer layer1 = root.addChildWindow(builder.setLayer(1));
+ final TestWindowContainer secondLayer1 = root.addChildWindow(builder.setLayer(1));
+ final TestWindowContainer layer2 = root.addChildWindow(builder.setLayer(2));
+ final TestWindowContainer layerNeg1 = root.addChildWindow(builder.setLayer(-1));
+ final TestWindowContainer layerNeg2 = root.addChildWindow(builder.setLayer(-2));
+ final TestWindowContainer secondLayerNeg1 = root.addChildWindow(builder.setLayer(-1));
+ final TestWindowContainer layer0 = root.addChildWindow(builder.setLayer(0));
+
+ assertEquals(7, root.getChildrenCount());
+
+ assertEquals(root, layer1.getParentWindow());
+ assertEquals(root, secondLayer1.getParentWindow());
+ assertEquals(root, layer2.getParentWindow());
+ assertEquals(root, layerNeg1.getParentWindow());
+ assertEquals(root, layerNeg2.getParentWindow());
+ assertEquals(root, secondLayerNeg1.getParentWindow());
+ assertEquals(root, layer0.getParentWindow());
+
+ assertEquals(layerNeg2, root.getChildAt(0));
+ assertEquals(secondLayerNeg1, root.getChildAt(1));
+ assertEquals(layerNeg1, root.getChildAt(2));
+ assertEquals(layer0, root.getChildAt(3));
+ assertEquals(layer1, root.getChildAt(4));
+ assertEquals(secondLayer1, root.getChildAt(5));
+ assertEquals(layer2, root.getChildAt(6));
+
+ assertTrue(layer1.mOnParentSetCalled);
+ assertTrue(secondLayer1.mOnParentSetCalled);
+ assertTrue(layer2.mOnParentSetCalled);
+ assertTrue(layerNeg1.mOnParentSetCalled);
+ assertTrue(layerNeg2.mOnParentSetCalled);
+ assertTrue(secondLayerNeg1.mOnParentSetCalled);
+ assertTrue(layer0.mOnParentSetCalled);
+ }
+
+ @Test
+ public void testAddChildSetsSurfacePosition() throws Exception {
+ MockSurfaceBuildingContainer top = new MockSurfaceBuildingContainer();
+
+ final SurfaceControl.Transaction transaction = mock(SurfaceControl.Transaction.class);
+ sWm.mTransactionFactory = () -> transaction;
+
+ WindowContainer child = new WindowContainer(sWm);
+ child.setBounds(1, 1, 10, 10);
+
+ verify(transaction, never()).setPosition(any(), anyFloat(), anyFloat());
+ top.addChild(child, 0);
+ verify(transaction, times(1)).setPosition(any(), eq(1.f), eq(1.f));
+ }
+
+ @Test
+ public void testAdd_AlreadyHasParent() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = root.addChildWindow();
+ final TestWindowContainer child2 = root.addChildWindow();
+
+ boolean gotException = false;
+ try {
+ child1.addChildWindow(child2);
+ } catch (IllegalArgumentException e) {
+ gotException = true;
+ }
+ assertTrue(gotException);
+
+ gotException = false;
+ try {
+ root.addChildWindow(child2);
+ } catch (IllegalArgumentException e) {
+ gotException = true;
+ }
+ assertTrue(gotException);
+ }
+
+ @Test
+ public void testHasChild() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = root.addChildWindow();
+ final TestWindowContainer child2 = root.addChildWindow();
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child12 = child1.addChildWindow();
+ final TestWindowContainer child21 = child2.addChildWindow();
+
+ assertEquals(2, root.getChildrenCount());
+ assertEquals(2, child1.getChildrenCount());
+ assertEquals(1, child2.getChildrenCount());
+
+ assertTrue(root.hasChild(child1));
+ assertTrue(root.hasChild(child2));
+ assertTrue(root.hasChild(child11));
+ assertTrue(root.hasChild(child12));
+ assertTrue(root.hasChild(child21));
+
+ assertTrue(child1.hasChild(child11));
+ assertTrue(child1.hasChild(child12));
+ assertFalse(child1.hasChild(child21));
+
+ assertTrue(child2.hasChild(child21));
+ assertFalse(child2.hasChild(child11));
+ assertFalse(child2.hasChild(child12));
+ }
+
+ @Test
+ public void testRemoveImmediately() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = root.addChildWindow();
+ final TestWindowContainer child2 = root.addChildWindow();
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child12 = child1.addChildWindow();
+ final TestWindowContainer child21 = child2.addChildWindow();
+
+ assertNotNull(child12.getParentWindow());
+ child12.removeImmediately();
+ assertNull(child12.getParentWindow());
+ assertEquals(1, child1.getChildrenCount());
+ assertFalse(child1.hasChild(child12));
+ assertFalse(root.hasChild(child12));
+
+ assertTrue(root.hasChild(child2));
+ assertNotNull(child2.getParentWindow());
+ child2.removeImmediately();
+ assertNull(child2.getParentWindow());
+ assertNull(child21.getParentWindow());
+ assertEquals(0, child2.getChildrenCount());
+ assertEquals(1, root.getChildrenCount());
+ assertFalse(root.hasChild(child2));
+ assertFalse(root.hasChild(child21));
+
+ assertTrue(root.hasChild(child1));
+ assertTrue(root.hasChild(child11));
+
+ root.removeImmediately();
+ assertEquals(0, root.getChildrenCount());
+ }
+
+ @Test
+ public void testRemoveImmediately_WithController() throws Exception {
+ final WindowContainer container = new WindowContainer(sWm);
+ final WindowContainerController controller = new WindowContainerController(null, sWm);
+
+ container.setController(controller);
+ assertEquals(controller, container.getController());
+ assertEquals(container, controller.mContainer);
+
+ container.removeImmediately();
+ assertNull(container.getController());
+ assertNull(controller.mContainer);
+ }
+
+ @Test
+ public void testSetController() throws Exception {
+ final WindowContainerController controller = new WindowContainerController(null, sWm);
+ final WindowContainer container = new WindowContainer(sWm);
+
+ container.setController(controller);
+ assertEquals(controller, container.getController());
+ assertEquals(container, controller.mContainer);
+
+ // Assert we can't change the controller to another one once set
+ boolean gotException = false;
+ try {
+ container.setController(new WindowContainerController(null, sWm));
+ } catch (IllegalArgumentException e) {
+ gotException = true;
+ }
+ assertTrue(gotException);
+
+ // Assert that we can set the controller to null.
+ container.setController(null);
+ assertNull(container.getController());
+ assertNull(controller.mContainer);
+ }
+
+ @Test
+ public void testPositionChildAt() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = root.addChildWindow();
+ final TestWindowContainer child2 = root.addChildWindow();
+ final TestWindowContainer child3 = root.addChildWindow();
+
+ // Test position at top.
+ root.positionChildAt(POSITION_TOP, child1, false /* includingParents */);
+ assertEquals(child1, root.getChildAt(root.getChildrenCount() - 1));
+
+ // Test position at bottom.
+ root.positionChildAt(POSITION_BOTTOM, child1, false /* includingParents */);
+ assertEquals(child1, root.getChildAt(0));
+
+ // Test position in the middle.
+ root.positionChildAt(1, child3, false /* includingParents */);
+ assertEquals(child1, root.getChildAt(0));
+ assertEquals(child3, root.getChildAt(1));
+ assertEquals(child2, root.getChildAt(2));
+ }
+
+ @Test
+ public void testPositionChildAtIncludeParents() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = root.addChildWindow();
+ final TestWindowContainer child2 = root.addChildWindow();
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child12 = child1.addChildWindow();
+ final TestWindowContainer child13 = child1.addChildWindow();
+ final TestWindowContainer child21 = child2.addChildWindow();
+ final TestWindowContainer child22 = child2.addChildWindow();
+ final TestWindowContainer child23 = child2.addChildWindow();
+
+ // Test moving to top.
+ child1.positionChildAt(POSITION_TOP, child11, true /* includingParents */);
+ assertEquals(child12, child1.getChildAt(0));
+ assertEquals(child13, child1.getChildAt(1));
+ assertEquals(child11, child1.getChildAt(2));
+ assertEquals(child2, root.getChildAt(0));
+ assertEquals(child1, root.getChildAt(1));
+
+ // Test moving to bottom.
+ child1.positionChildAt(POSITION_BOTTOM, child11, true /* includingParents */);
+ assertEquals(child11, child1.getChildAt(0));
+ assertEquals(child12, child1.getChildAt(1));
+ assertEquals(child13, child1.getChildAt(2));
+ assertEquals(child1, root.getChildAt(0));
+ assertEquals(child2, root.getChildAt(1));
+
+ // Test moving to middle, includeParents shouldn't do anything.
+ child2.positionChildAt(1, child21, true /* includingParents */);
+ assertEquals(child11, child1.getChildAt(0));
+ assertEquals(child12, child1.getChildAt(1));
+ assertEquals(child13, child1.getChildAt(2));
+ assertEquals(child22, child2.getChildAt(0));
+ assertEquals(child21, child2.getChildAt(1));
+ assertEquals(child23, child2.getChildAt(2));
+ assertEquals(child1, root.getChildAt(0));
+ assertEquals(child2, root.getChildAt(1));
+ }
+
+ @Test
+ public void testPositionChildAtInvalid() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = root.addChildWindow();
+ final TestWindowContainer child2 = root.addChildWindow();
+
+ boolean gotException = false;
+ try {
+ // Check response to negative position.
+ root.positionChildAt(-1, child1, false /* includingParents */);
+ } catch (IllegalArgumentException e) {
+ gotException = true;
+ }
+ assertTrue(gotException);
+
+ gotException = false;
+ try {
+ // Check response to position that's bigger than child number.
+ root.positionChildAt(3, child1, false /* includingParents */);
+ } catch (IllegalArgumentException e) {
+ gotException = true;
+ }
+ assertTrue(gotException);
+ }
+
+ @Test
+ public void testIsAnimating() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = root.addChildWindow(builder.setIsAnimating(true));
+ final TestWindowContainer child2 = root.addChildWindow();
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child12 = child1.addChildWindow(builder.setIsAnimating(true));
+ final TestWindowContainer child21 = child2.addChildWindow();
+
+ assertFalse(root.isAnimating());
+ assertTrue(child1.isAnimating());
+ assertTrue(child11.isAnimating());
+ assertTrue(child12.isAnimating());
+ assertFalse(child2.isAnimating());
+ assertFalse(child21.isAnimating());
+
+ assertTrue(root.isSelfOrChildAnimating());
+ assertTrue(child1.isSelfOrChildAnimating());
+ assertFalse(child11.isSelfOrChildAnimating());
+ assertTrue(child12.isSelfOrChildAnimating());
+ assertFalse(child2.isSelfOrChildAnimating());
+ assertFalse(child21.isSelfOrChildAnimating());
+ }
+
+ @Test
+ public void testIsVisible() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = root.addChildWindow(builder.setIsVisible(true));
+ final TestWindowContainer child2 = root.addChildWindow();
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child12 = child1.addChildWindow(builder.setIsVisible(true));
+ final TestWindowContainer child21 = child2.addChildWindow();
+
+ assertFalse(root.isVisible());
+ assertTrue(child1.isVisible());
+ assertFalse(child11.isVisible());
+ assertTrue(child12.isVisible());
+ assertFalse(child2.isVisible());
+ assertFalse(child21.isVisible());
+ }
+
+ @Test
+ public void testOverrideConfigurationAncestorNotification() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer grandparent = builder.setLayer(0).build();
+
+ final TestWindowContainer parent = grandparent.addChildWindow();
+ final TestWindowContainer child = parent.addChildWindow();
+ child.onOverrideConfigurationChanged(new Configuration());
+
+ assertTrue(grandparent.mOnDescendantOverrideCalled);
+ }
+
+ @Test
+ public void testRemoveChild() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+ final TestWindowContainer child1 = root.addChildWindow();
+ final TestWindowContainer child2 = root.addChildWindow();
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child21 = child2.addChildWindow();
+
+ assertTrue(root.hasChild(child2));
+ assertTrue(root.hasChild(child21));
+ root.removeChild(child2);
+ assertFalse(root.hasChild(child2));
+ assertFalse(root.hasChild(child21));
+ assertNull(child2.getParentWindow());
+
+ boolean gotException = false;
+ assertTrue(root.hasChild(child11));
+ try {
+ // Can only detach our direct children.
+ root.removeChild(child11);
+ } catch (IllegalArgumentException e) {
+ gotException = true;
+ }
+ assertTrue(gotException);
+ }
+
+ @Test
+ public void testGetOrientation_childSpecified() throws Exception {
+ testGetOrientation_childSpecifiedConfig(false, SCREEN_ORIENTATION_LANDSCAPE,
+ SCREEN_ORIENTATION_LANDSCAPE);
+ testGetOrientation_childSpecifiedConfig(false, SCREEN_ORIENTATION_UNSET,
+ SCREEN_ORIENTATION_UNSPECIFIED);
+ }
+
+ private void testGetOrientation_childSpecifiedConfig(boolean childVisible, int childOrientation,
+ int expectedOrientation) {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+ root.setFillsParent(true);
+
+ builder.setIsVisible(childVisible);
+
+ if (childOrientation != SCREEN_ORIENTATION_UNSET) {
+ builder.setOrientation(childOrientation);
+ }
+
+ final TestWindowContainer child1 = root.addChildWindow(builder);
+ child1.setFillsParent(true);
+
+ assertEquals(expectedOrientation, root.getOrientation());
+ }
+
+ @Test
+ public void testGetOrientation_Unset() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).setIsVisible(true).build();
+ // Unspecified well because we didn't specify anything...
+ assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, root.getOrientation());
+ }
+
+ @Test
+ public void testGetOrientation_InvisibleParentUnsetVisibleChildren() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).setIsVisible(true).build();
+
+ builder.setIsVisible(false).setLayer(-1);
+ final TestWindowContainer invisible = root.addChildWindow(builder);
+ builder.setIsVisible(true).setLayer(-2);
+ final TestWindowContainer invisibleChild1VisibleAndSet = invisible.addChildWindow(builder);
+ invisibleChild1VisibleAndSet.setOrientation(SCREEN_ORIENTATION_LANDSCAPE);
+ // Landscape well because the container is visible and that is what we set on it above.
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, invisibleChild1VisibleAndSet.getOrientation());
+ // Landscape because even though the container isn't visible it has a child that is
+ // specifying it can influence the orientation by being visible.
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, invisible.getOrientation());
+ // Landscape because the grandchild is visible and therefore can participate.
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, root.getOrientation());
+
+ builder.setIsVisible(true).setLayer(-3);
+ final TestWindowContainer visibleUnset = root.addChildWindow(builder);
+ visibleUnset.setOrientation(SCREEN_ORIENTATION_UNSET);
+ assertEquals(SCREEN_ORIENTATION_UNSET, visibleUnset.getOrientation());
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, root.getOrientation());
+
+ }
+
+ @Test
+ public void testGetOrientation_setBehind() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).setIsVisible(true).build();
+
+ builder.setIsVisible(true).setLayer(-1);
+ final TestWindowContainer visibleUnset = root.addChildWindow(builder);
+ visibleUnset.setOrientation(SCREEN_ORIENTATION_UNSET);
+
+ builder.setIsVisible(true).setLayer(-2);
+ final TestWindowContainer visibleUnsetChild1VisibleSetBehind =
+ visibleUnset.addChildWindow(builder);
+ visibleUnsetChild1VisibleSetBehind.setOrientation(SCREEN_ORIENTATION_BEHIND);
+ // Setting to visible behind will be used by the parents if there isn't another other
+ // container behind this one that has an orientation set.
+ assertEquals(SCREEN_ORIENTATION_BEHIND,
+ visibleUnsetChild1VisibleSetBehind.getOrientation());
+ assertEquals(SCREEN_ORIENTATION_BEHIND, visibleUnset.getOrientation());
+ assertEquals(SCREEN_ORIENTATION_BEHIND, root.getOrientation());
+ }
+
+ @Test
+ public void testGetOrientation_fillsParent() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).setIsVisible(true).build();
+
+ builder.setIsVisible(true).setLayer(-1);
+ final TestWindowContainer visibleUnset = root.addChildWindow(builder);
+ visibleUnset.setOrientation(SCREEN_ORIENTATION_BEHIND);
+
+ builder.setLayer(1).setIsVisible(true);
+ final TestWindowContainer visibleUnspecifiedRootChild = root.addChildWindow(builder);
+ visibleUnspecifiedRootChild.setFillsParent(false);
+ visibleUnspecifiedRootChild.setOrientation(SCREEN_ORIENTATION_UNSPECIFIED);
+ // Unset because the child doesn't fill the parent. May as well be invisible...
+ assertEquals(SCREEN_ORIENTATION_UNSET, visibleUnspecifiedRootChild.getOrientation());
+ // The parent uses whatever orientation is set behind this container since it doesn't fill
+ // the parent.
+ assertEquals(SCREEN_ORIENTATION_BEHIND, root.getOrientation());
+
+ // Test case of child filling its parent, but its parent isn't filling its own parent.
+ builder.setLayer(2).setIsVisible(true);
+ final TestWindowContainer visibleUnspecifiedRootChildChildFillsParent =
+ visibleUnspecifiedRootChild.addChildWindow(builder);
+ visibleUnspecifiedRootChildChildFillsParent.setOrientation(
+ SCREEN_ORIENTATION_PORTRAIT);
+ assertEquals(SCREEN_ORIENTATION_PORTRAIT,
+ visibleUnspecifiedRootChildChildFillsParent.getOrientation());
+ assertEquals(SCREEN_ORIENTATION_UNSET, visibleUnspecifiedRootChild.getOrientation());
+ assertEquals(SCREEN_ORIENTATION_BEHIND, root.getOrientation());
+
+
+ visibleUnspecifiedRootChild.setFillsParent(true);
+ assertEquals(SCREEN_ORIENTATION_PORTRAIT, visibleUnspecifiedRootChild.getOrientation());
+ assertEquals(SCREEN_ORIENTATION_PORTRAIT, root.getOrientation());
+ }
+
+ @Test
+ public void testCompareTo() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = root.addChildWindow();
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child12 = child1.addChildWindow();
+
+ final TestWindowContainer child2 = root.addChildWindow();
+ final TestWindowContainer child21 = child2.addChildWindow();
+ final TestWindowContainer child22 = child2.addChildWindow();
+ final TestWindowContainer child23 = child2.addChildWindow();
+ final TestWindowContainer child221 = child22.addChildWindow();
+ final TestWindowContainer child222 = child22.addChildWindow();
+ final TestWindowContainer child223 = child22.addChildWindow();
+ final TestWindowContainer child2221 = child222.addChildWindow();
+ final TestWindowContainer child2222 = child222.addChildWindow();
+ final TestWindowContainer child2223 = child222.addChildWindow();
+
+ final TestWindowContainer root2 = builder.setLayer(0).build();
+
+ assertEquals(0, root.compareTo(root));
+ assertEquals(-1, child1.compareTo(child2));
+ assertEquals(1, child2.compareTo(child1));
+
+ boolean inTheSameTree = true;
+ try {
+ root.compareTo(root2);
+ } catch (IllegalArgumentException e) {
+ inTheSameTree = false;
+ }
+ assertFalse(inTheSameTree);
+
+ assertEquals(-1, child1.compareTo(child11));
+ assertEquals(1, child21.compareTo(root));
+ assertEquals(1, child21.compareTo(child12));
+ assertEquals(-1, child11.compareTo(child2));
+ assertEquals(1, child2221.compareTo(child11));
+ assertEquals(-1, child2222.compareTo(child223));
+ assertEquals(1, child2223.compareTo(child21));
+ }
+
+ @Test
+ public void testPrefixOrderIndex() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.build();
+
+ final TestWindowContainer child1 = root.addChildWindow();
+
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child12 = child1.addChildWindow();
+
+ final TestWindowContainer child2 = root.addChildWindow();
+
+ final TestWindowContainer child21 = child2.addChildWindow();
+ final TestWindowContainer child22 = child2.addChildWindow();
+
+ final TestWindowContainer child221 = child22.addChildWindow();
+ final TestWindowContainer child222 = child22.addChildWindow();
+ final TestWindowContainer child223 = child22.addChildWindow();
+
+ final TestWindowContainer child23 = child2.addChildWindow();
+
+ assertEquals(0, root.getPrefixOrderIndex());
+ assertEquals(1, child1.getPrefixOrderIndex());
+ assertEquals(2, child11.getPrefixOrderIndex());
+ assertEquals(3, child12.getPrefixOrderIndex());
+ assertEquals(4, child2.getPrefixOrderIndex());
+ assertEquals(5, child21.getPrefixOrderIndex());
+ assertEquals(6, child22.getPrefixOrderIndex());
+ assertEquals(7, child221.getPrefixOrderIndex());
+ assertEquals(8, child222.getPrefixOrderIndex());
+ assertEquals(9, child223.getPrefixOrderIndex());
+ assertEquals(10, child23.getPrefixOrderIndex());
+ }
+
+ @Test
+ public void testPrefixOrder_addEntireSubtree() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.build();
+ final TestWindowContainer subtree = builder.build();
+ final TestWindowContainer subtree2 = builder.build();
+
+ final TestWindowContainer child1 = subtree.addChildWindow();
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child2 = subtree2.addChildWindow();
+ final TestWindowContainer child3 = subtree2.addChildWindow();
+ subtree.addChild(subtree2, 1);
+ root.addChild(subtree, 0);
+
+ assertEquals(0, root.getPrefixOrderIndex());
+ assertEquals(1, subtree.getPrefixOrderIndex());
+ assertEquals(2, child1.getPrefixOrderIndex());
+ assertEquals(3, child11.getPrefixOrderIndex());
+ assertEquals(4, subtree2.getPrefixOrderIndex());
+ assertEquals(5, child2.getPrefixOrderIndex());
+ assertEquals(6, child3.getPrefixOrderIndex());
+ }
+
+ @Test
+ public void testPrefixOrder_remove() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.build();
+
+ final TestWindowContainer child1 = root.addChildWindow();
+
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child12 = child1.addChildWindow();
+
+ final TestWindowContainer child2 = root.addChildWindow();
+
+ assertEquals(0, root.getPrefixOrderIndex());
+ assertEquals(1, child1.getPrefixOrderIndex());
+ assertEquals(2, child11.getPrefixOrderIndex());
+ assertEquals(3, child12.getPrefixOrderIndex());
+ assertEquals(4, child2.getPrefixOrderIndex());
+
+ root.removeChild(child1);
+
+ assertEquals(1, child2.getPrefixOrderIndex());
+ }
+
+ /**
+ * Ensure children of a {@link WindowContainer} do not have
+ * {@link WindowContainer#onParentResize()} called when {@link WindowContainer#onParentResize()}
+ * is invoked with overridden bounds.
+ */
+ @Test
+ public void testOnParentResizePropagation() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.build();
+
+ final TestWindowContainer child = root.addChildWindow();
+ child.setBounds(new Rect(1,1,2,2));
+
+ final TestWindowContainer grandChild = mock(TestWindowContainer.class);
+
+ child.addChildWindow(grandChild);
+ root.onResize();
+
+ // Make sure the child does not propagate resize through onParentResize when bounds are set.
+ verify(grandChild, never()).onParentResize();
+
+ child.removeChild(grandChild);
+
+ child.setBounds(null);
+ child.addChildWindow(grandChild);
+ root.onResize();
+
+ // Make sure the child propagates resize through onParentResize when no bounds set.
+ verify(grandChild, times(1)).onParentResize();
+ }
+
+ /* Used so we can gain access to some protected members of the {@link WindowContainer} class */
+ private class TestWindowContainer extends WindowContainer<TestWindowContainer> {
+ private final int mLayer;
+ private boolean mIsAnimating;
+ private boolean mIsVisible;
+ private boolean mFillsParent;
+ private Integer mOrientation;
+
+ private boolean mOnParentSetCalled;
+ private boolean mOnDescendantOverrideCalled;
+
+ /**
+ * Compares 2 window layers and returns -1 if the first is lesser than the second in terms
+ * of z-order and 1 otherwise.
+ */
+ private final Comparator<TestWindowContainer> mWindowSubLayerComparator = (w1, w2) -> {
+ final int layer1 = w1.mLayer;
+ final int layer2 = w2.mLayer;
+ if (layer1 < layer2 || (layer1 == layer2 && layer2 < 0 )) {
+ // We insert the child window into the list ordered by the mLayer. For same layers,
+ // the negative one should go below others; the positive one should go above others.
+ return -1;
+ }
+ return 1;
+ };
+
+ TestWindowContainer(int layer, boolean isAnimating, boolean isVisible,
+ Integer orientation) {
+ super(sWm);
+ mLayer = layer;
+ mIsAnimating = isAnimating;
+ mIsVisible = isVisible;
+ mFillsParent = true;
+ mOrientation = orientation;
+ }
+
+ TestWindowContainer getParentWindow() {
+ return (TestWindowContainer) getParent();
+ }
+
+ int getChildrenCount() {
+ return mChildren.size();
+ }
+
+ TestWindowContainer addChildWindow(TestWindowContainer child) {
+ addChild(child, mWindowSubLayerComparator);
+ return child;
+ }
+
+ TestWindowContainer addChildWindow(TestWindowContainerBuilder childBuilder) {
+ TestWindowContainer child = childBuilder.build();
+ addChild(child, mWindowSubLayerComparator);
+ return child;
+ }
+
+ TestWindowContainer addChildWindow() {
+ return addChildWindow(new TestWindowContainerBuilder().setLayer(1));
+ }
+
+ @Override
+ void onParentSet() {
+ mOnParentSetCalled = true;
+ }
+
+ @Override
+ void onDescendantOverrideConfigurationChanged() {
+ mOnDescendantOverrideCalled = true;
+ super.onDescendantOverrideConfigurationChanged();
+ }
+
+ @Override
+ boolean isSelfAnimating() {
+ return mIsAnimating;
+ }
+
+ @Override
+ boolean isVisible() {
+ return mIsVisible;
+ }
+
+ @Override
+ int getOrientation(int candidate) {
+ return mOrientation != null ? mOrientation : super.getOrientation(candidate);
+ }
+
+ @Override
+ int getOrientation() {
+ return getOrientation(super.mOrientation);
+ }
+
+ @Override
+ boolean fillsParent() {
+ return mFillsParent;
+ }
+
+ void setFillsParent(boolean fillsParent) {
+ mFillsParent = fillsParent;
+ }
+ }
+
+ private class TestWindowContainerBuilder {
+ private int mLayer;
+ private boolean mIsAnimating;
+ private boolean mIsVisible;
+ private Integer mOrientation;
+
+ public TestWindowContainerBuilder() {
+ reset();
+ }
+
+ TestWindowContainerBuilder setLayer(int layer) {
+ mLayer = layer;
+ return this;
+ }
+
+ TestWindowContainerBuilder setIsAnimating(boolean isAnimating) {
+ mIsAnimating = isAnimating;
+ return this;
+ }
+
+ TestWindowContainerBuilder setIsVisible(boolean isVisible) {
+ mIsVisible = isVisible;
+ return this;
+ }
+
+ TestWindowContainerBuilder setOrientation(int orientation) {
+ mOrientation = orientation;
+ return this;
+ }
+
+ TestWindowContainerBuilder reset() {
+ mLayer = 0;
+ mIsAnimating = false;
+ mIsVisible = false;
+ mOrientation = null;
+ return this;
+ }
+
+ TestWindowContainer build() {
+ return new TestWindowContainer(mLayer, mIsAnimating, mIsVisible, mOrientation);
+ }
+ }
+
+ private class MockSurfaceBuildingContainer extends WindowContainer<WindowContainer> {
+ final SurfaceSession mSession = new SurfaceSession();
+
+ MockSurfaceBuildingContainer() {
+ super(sWm);
+ }
+
+ class MockSurfaceBuilder extends SurfaceControl.Builder {
+ MockSurfaceBuilder(SurfaceSession ss) {
+ super(ss);
+ }
+
+ @Override
+ public SurfaceControl build() {
+ return mock(SurfaceControl.class);
+ }
+ }
+
+ @Override
+ SurfaceControl.Builder makeChildSurface(WindowContainer child) {
+ return new MockSurfaceBuilder(mSession);
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowContainerTraversalTests.java b/services/tests/servicestests/src/com/android/server/wm/WindowContainerTraversalTests.java
new file mode 100644
index 0000000..e076399
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowContainerTraversalTests.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Matchers.eq;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.mockito.Mock;
+
+import java.util.function.Consumer;
+
+/**
+ * Tests for {@link WindowContainer#forAllWindows} and various implementations.
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WindowContainerTraversalTests extends WindowTestsBase {
+
+ @Test
+ public void testDockedDividerPosition() throws Exception {
+ final WindowState splitScreenWindow = createWindowOnStack(null,
+ WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION,
+ mDisplayContent, "splitScreenWindow");
+ final WindowState splitScreenSecondaryWindow = createWindowOnStack(null,
+ WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_STANDARD,
+ TYPE_BASE_APPLICATION, mDisplayContent, "splitScreenSecondaryWindow");
+
+ sWm.mInputMethodTarget = splitScreenWindow;
+
+ Consumer<WindowState> c = mock(Consumer.class);
+ mDisplayContent.forAllWindows(c, false);
+
+ verify(c).accept(eq(mDockedDividerWindow));
+ verify(c).accept(eq(mImeWindow));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowFrameTests.java b/services/tests/servicestests/src/com/android/server/wm/WindowFrameTests.java
new file mode 100644
index 0000000..5a56332
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowFrameTests.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.app.ActivityManager.TaskDescription;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.DisplayInfo;
+import android.view.Gravity;
+import android.view.IWindow;
+import android.view.WindowManager;
+
+import static android.view.DisplayCutout.fromBoundingRect;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.FILL_PARENT;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.server.wm.utils.WmDisplayCutout;
+
+/**
+ * Tests for the {@link WindowState#computeFrameLw} method and other window frame machinery.
+ *
+ * Build/Install/Run: bit FrameworksServicesTests:com.android.server.wm.WindowFrameTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WindowFrameTests extends WindowTestsBase {
+
+ private WindowToken mWindowToken;
+ private final IWindow mIWindow = new TestIWindow();
+
+ class WindowStateWithTask extends WindowState {
+ final Task mTask;
+ boolean mDockedResizingForTest = false;
+ WindowStateWithTask(WindowManager.LayoutParams attrs, Task t) {
+ super(sWm, null, mIWindow, mWindowToken, null, 0, 0, attrs, 0, 0,
+ false /* ownerCanAddInternalSystemWindow */);
+ mTask = t;
+ }
+
+ @Override
+ Task getTask() {
+ return mTask;
+ }
+
+ @Override
+ boolean isDockedResizing() {
+ return mDockedResizingForTest;
+ }
+ };
+
+ class TaskWithBounds extends Task {
+ final Rect mBounds;
+ final Rect mInsetBounds = new Rect();
+ boolean mFullscreenForTest = true;
+ TaskWithBounds(Rect bounds) {
+ super(0, mStubStack, 0, sWm, 0, false, new TaskDescription(), null);
+ mBounds = bounds;
+ setBounds(bounds);
+ }
+
+ @Override
+ public Rect getBounds() {
+ return mBounds;
+ }
+
+ @Override
+ public void getBounds(Rect out) {
+ out.set(mBounds);
+ }
+
+ @Override
+ public void getOverrideBounds(Rect outBounds) {
+ outBounds.set(mBounds);
+ }
+ @Override
+ void getTempInsetBounds(Rect outBounds) {
+ outBounds.set(mInsetBounds);
+ }
+ @Override
+ boolean isFullscreen() {
+ return mFullscreenForTest;
+ }
+ }
+
+ TaskStack mStubStack;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ // Just any non zero value.
+ sWm.mSystemDecorLayer = 10000;
+
+ mWindowToken = WindowTestUtils.createTestAppWindowToken(
+ sWm.getDefaultDisplayContentLocked());
+ mStubStack = new TaskStack(sWm, 0, null);
+ }
+
+ public void assertRect(Rect rect, int left, int top, int right, int bottom) {
+ assertEquals(left, rect.left);
+ assertEquals(top, rect.top);
+ assertEquals(right, rect.right);
+ assertEquals(bottom, rect.bottom);
+ }
+
+ @Test
+ public void testLayoutInFullscreenTaskInsets() throws Exception {
+ Task task = new TaskWithBounds(null); // fullscreen task doesn't use bounds for computeFrame
+ WindowState w = createWindow(task, FILL_PARENT, FILL_PARENT);
+ w.mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
+
+ final int bottomContentInset = 100;
+ final int topContentInset = 50;
+ final int bottomVisibleInset = 30;
+ final int topVisibleInset = 70;
+ final int leftStableInset = 20;
+ final int rightStableInset = 90;
+
+ // With no insets or system decor all the frames incoming from PhoneWindowManager
+ // are identical.
+ final Rect pf = new Rect(0, 0, 1000, 1000);
+ final Rect df = pf;
+ final Rect of = df;
+ final Rect cf = new Rect(pf);
+ // Produce some insets
+ cf.top += 50;
+ cf.bottom -= 100;
+ final Rect vf = new Rect(pf);
+ vf.top += topVisibleInset;
+ vf.bottom -= bottomVisibleInset;
+ final Rect sf = new Rect(pf);
+ sf.left += leftStableInset;
+ sf.right -= rightStableInset;
+
+ final Rect dcf = pf;
+ // When mFrame extends past cf, the content insets are
+ // the difference between mFrame and ContentFrame. Visible
+ // and stable frames work the same way.
+ w.computeFrameLw(pf, df, of, cf, vf, dcf, sf, null, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame,0, 0, 1000, 1000);
+ assertRect(w.mContentInsets, 0, topContentInset, 0, bottomContentInset);
+ assertRect(w.mVisibleInsets, 0, topVisibleInset, 0, bottomVisibleInset);
+ assertRect(w.mStableInsets, leftStableInset, 0, rightStableInset, 0);
+ // The frames remain as passed in shrunk to the window frame
+ assertTrue(cf.equals(w.getContentFrameLw()));
+ assertTrue(vf.equals(w.getVisibleFrameLw()));
+ assertTrue(sf.equals(w.getStableFrameLw()));
+ // On the other hand mFrame doesn't extend past cf we won't get any insets
+ w.mAttrs.x = 100;
+ w.mAttrs.y = 100;
+ w.mAttrs.width = 100; w.mAttrs.height = 100; //have to clear MATCH_PARENT
+ w.mRequestedWidth = 100;
+ w.mRequestedHeight = 100;
+ w.computeFrameLw(pf, df, of, cf, vf, dcf, sf, null, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, 100, 100, 200, 200);
+ assertRect(w.mContentInsets, 0, 0, 0, 0);
+ // In this case the frames are shrunk to the window frame.
+ assertTrue(w.mFrame.equals(w.getContentFrameLw()));
+ assertTrue(w.mFrame.equals(w.getVisibleFrameLw()));
+ assertTrue(w.mFrame.equals(w.getStableFrameLw()));
+ }
+
+ @Test
+ public void testLayoutInFullscreenTaskNoInsets() throws Exception {
+ Task task = new TaskWithBounds(null); // fullscreen task doesn't use bounds for computeFrame
+ WindowState w = createWindow(task, FILL_PARENT, FILL_PARENT);
+ w.mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
+
+ // With no insets or system decor all the frames incoming from PhoneWindowManager
+ // are identical.
+ final Rect pf = new Rect(0, 0, 1000, 1000);
+
+ // Here the window has FILL_PARENT, FILL_PARENT
+ // so we expect it to fill the entire available frame.
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, 0, 0, 1000, 1000);
+
+ // It can select various widths and heights within the bounds.
+ // Strangely the window attribute width is ignored for normal windows
+ // and we use mRequestedWidth/mRequestedHeight
+ w.mAttrs.width = 300;
+ w.mAttrs.height = 300;
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+ // Explicit width and height without requested width/height
+ // gets us nothing.
+ assertRect(w.mFrame, 0, 0, 0, 0);
+
+ w.mRequestedWidth = 300;
+ w.mRequestedHeight = 300;
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+ // With requestedWidth/Height we can freely choose our size within the
+ // parent bounds.
+ assertRect(w.mFrame, 0, 0, 300, 300);
+
+ // With FLAG_SCALED though, requestedWidth/height is used to control
+ // the unscaled surface size, and mAttrs.width/height becomes the
+ // layout controller.
+ w.mAttrs.flags = WindowManager.LayoutParams.FLAG_SCALED;
+ w.mRequestedHeight = -1;
+ w.mRequestedWidth = -1;
+ w.mAttrs.width = 100;
+ w.mAttrs.height = 100;
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, 0, 0, 100, 100);
+ w.mAttrs.flags = 0;
+
+ // But sizes too large will be clipped to the containing frame
+ w.mRequestedWidth = 1200;
+ w.mRequestedHeight = 1200;
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, 0, 0, 1000, 1000);
+
+ // Before they are clipped though windows will be shifted
+ w.mAttrs.x = 300;
+ w.mAttrs.y = 300;
+ w.mRequestedWidth = 1000;
+ w.mRequestedHeight = 1000;
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, 0, 0, 1000, 1000);
+
+ // If there is room to move around in the parent frame the window will be shifted according
+ // to gravity.
+ w.mAttrs.x = 0;
+ w.mAttrs.y = 0;
+ w.mRequestedWidth = 300;
+ w.mRequestedHeight = 300;
+ w.mAttrs.gravity = Gravity.RIGHT | Gravity.TOP;
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, 700, 0, 1000, 300);
+ w.mAttrs.gravity = Gravity.RIGHT | Gravity.BOTTOM;
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, 700, 700, 1000, 1000);
+ // Window specified x and y are interpreted as offsets in the opposite
+ // direction of gravity
+ w.mAttrs.x = 100;
+ w.mAttrs.y = 100;
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, 600, 600, 900, 900);
+ }
+
+ @Test
+ public void testLayoutNonfullscreenTask() {
+ final DisplayInfo displayInfo = sWm.getDefaultDisplayContentLocked().getDisplayInfo();
+ final int logicalWidth = displayInfo.logicalWidth;
+ final int logicalHeight = displayInfo.logicalHeight;
+
+ final int taskLeft = logicalWidth / 4;
+ final int taskTop = logicalHeight / 4;
+ final int taskRight = logicalWidth / 4 * 3;
+ final int taskBottom = logicalHeight / 4 * 3;
+ final Rect taskBounds = new Rect(taskLeft, taskTop, taskRight, taskBottom);
+ TaskWithBounds task = new TaskWithBounds(taskBounds);
+ task.mFullscreenForTest = false;
+ WindowState w = createWindow(task, FILL_PARENT, FILL_PARENT);
+ w.mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
+
+ final Rect pf = new Rect(0, 0, logicalWidth, logicalHeight);
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, null, WmDisplayCutout.NO_CUTOUT, false);
+ // For non fullscreen tasks the containing frame is based off the
+ // task bounds not the parent frame.
+ assertRect(w.mFrame, taskLeft, taskTop, taskRight, taskBottom);
+ assertRect(w.getContentFrameLw(), taskLeft, taskTop, taskRight, taskBottom);
+ assertRect(w.mContentInsets, 0, 0, 0, 0);
+
+ pf.set(0, 0, logicalWidth, logicalHeight);
+ // We still produce insets against the containing frame the same way.
+ final int cfRight = logicalWidth / 2;
+ final int cfBottom = logicalHeight / 2;
+ final Rect cf = new Rect(0, 0, cfRight, cfBottom);
+ w.computeFrameLw(pf, pf, pf, cf, cf, pf, cf, null, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, taskLeft, taskTop, taskRight, taskBottom);
+ int contentInsetRight = taskRight - cfRight;
+ int contentInsetBottom = taskBottom - cfBottom;
+ assertRect(w.mContentInsets, 0, 0, contentInsetRight, contentInsetBottom);
+ assertRect(w.getContentFrameLw(), taskLeft, taskTop, taskRight - contentInsetRight,
+ taskBottom - contentInsetBottom);
+
+ pf.set(0, 0, logicalWidth, logicalHeight);
+ // However if we set temp inset bounds, the insets will be computed
+ // as if our window was laid out there, but it will be laid out according to
+ // the task bounds.
+ final int insetLeft = logicalWidth / 5;
+ final int insetTop = logicalHeight / 5;
+ final int insetRight = insetLeft + (taskRight - taskLeft);
+ final int insetBottom = insetTop + (taskBottom - taskTop);
+ task.mInsetBounds.set(insetLeft, insetTop, insetRight, insetBottom);
+ w.computeFrameLw(pf, pf, pf, cf, cf, pf, cf, null, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, taskLeft, taskTop, taskRight, taskBottom);
+ contentInsetRight = insetRight - cfRight;
+ contentInsetBottom = insetBottom - cfBottom;
+ assertRect(w.mContentInsets, 0, 0, contentInsetRight, contentInsetBottom);
+ assertRect(w.getContentFrameLw(), taskLeft, taskTop, taskRight - contentInsetRight,
+ taskBottom - contentInsetBottom);
+ }
+
+ @Test
+ public void testCalculatePolicyCrop() {
+ final WindowStateWithTask w = createWindow(
+ new TaskWithBounds(null), FILL_PARENT, FILL_PARENT);
+ w.mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
+
+ final DisplayInfo displayInfo = w.getDisplayContent().getDisplayInfo();
+ final int logicalWidth = displayInfo.logicalWidth;
+ final int logicalHeight = displayInfo.logicalHeight;
+ final Rect pf = new Rect(0, 0, logicalWidth, logicalHeight);
+ final Rect df = pf;
+ final Rect of = df;
+ final Rect cf = new Rect(pf);
+ // Produce some insets
+ cf.top += displayInfo.logicalWidth / 10;
+ cf.bottom -= displayInfo.logicalWidth / 5;
+ final Rect vf = cf;
+ final Rect sf = vf;
+ // We use a decor content frame with insets to produce cropping.
+ Rect dcf = cf;
+
+ final Rect policyCrop = new Rect();
+
+ w.computeFrameLw(pf, df, of, cf, vf, dcf, sf, null, WmDisplayCutout.NO_CUTOUT, false);
+ w.calculatePolicyCrop(policyCrop);
+ assertRect(policyCrop, 0, cf.top, logicalWidth, cf.bottom);
+
+ dcf.setEmpty();
+ // Likewise with no decor frame we would get no crop
+ w.computeFrameLw(pf, df, of, cf, vf, dcf, sf, null, WmDisplayCutout.NO_CUTOUT, false);
+ w.calculatePolicyCrop(policyCrop);
+ assertRect(policyCrop, 0, 0, logicalWidth, logicalHeight);
+
+ // Now we set up a window which doesn't fill the entire decor frame.
+ // Normally it would be cropped to it's frame but in the case of docked resizing
+ // we need to account for the fact the windows surface will be made
+ // fullscreen and thus also make the crop fullscreen.
+ w.mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
+ w.mAttrs.width = logicalWidth / 2;
+ w.mAttrs.height = logicalHeight / 2;
+ w.mRequestedWidth = logicalWidth / 2;
+ w.mRequestedHeight = logicalHeight / 2;
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+
+ w.calculatePolicyCrop(policyCrop);
+ // Normally the crop is shrunk from the decor frame
+ // to the computed window frame.
+ assertRect(policyCrop, 0, 0, logicalWidth / 2, logicalHeight / 2);
+
+ w.mDockedResizingForTest = true;
+ w.calculatePolicyCrop(policyCrop);
+ // But if we are docked resizing it won't be, however we will still be
+ // shrunk to the decor frame and the display.
+ assertRect(policyCrop, 0, 0,
+ Math.min(pf.width(), displayInfo.logicalWidth),
+ Math.min(pf.height(), displayInfo.logicalHeight));
+ }
+
+ @Test
+ public void testLayoutLetterboxedWindow() {
+ // First verify task behavior in multi-window mode.
+ final DisplayInfo displayInfo = sWm.getDefaultDisplayContentLocked().getDisplayInfo();
+ final int logicalWidth = displayInfo.logicalWidth;
+ final int logicalHeight = displayInfo.logicalHeight;
+
+ final int taskLeft = logicalWidth / 5;
+ final int taskTop = logicalHeight / 5;
+ final int taskRight = logicalWidth / 4 * 3;
+ final int taskBottom = logicalHeight / 4 * 3;
+ final Rect taskBounds = new Rect(taskLeft, taskTop, taskRight, taskBottom);
+ TaskWithBounds task = new TaskWithBounds(taskBounds);
+ task.mInsetBounds.set(taskLeft, taskTop, taskRight, taskBottom);
+ task.mFullscreenForTest = false;
+ WindowState w = createWindow(task, FILL_PARENT, FILL_PARENT);
+ w.mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
+
+ final Rect pf = new Rect(0, 0, logicalWidth, logicalHeight);
+ w.computeFrameLw(pf /* parentFrame */, pf /* displayFrame */, pf /* overscanFrame */,
+ pf /* contentFrame */, pf /* visibleFrame */, pf /* decorFrame */,
+ pf /* stableFrame */, null /* outsetFrame */, WmDisplayCutout.NO_CUTOUT, false);
+ // For non fullscreen tasks the containing frame is based off the
+ // task bounds not the parent frame.
+ assertRect(w.mFrame, taskLeft, taskTop, taskRight, taskBottom);
+ assertRect(w.getContentFrameLw(), taskLeft, taskTop, taskRight, taskBottom);
+ assertRect(w.mContentInsets, 0, 0, 0, 0);
+
+ // Now simulate switch to fullscreen for letterboxed app.
+ final int xInset = logicalWidth / 10;
+ final int yInset = logicalWidth / 10;
+ final Rect cf = new Rect(xInset, yInset, logicalWidth - xInset, logicalHeight - yInset);
+ Configuration config = new Configuration(w.mAppToken.getOverrideConfiguration());
+ config.windowConfiguration.setBounds(cf);
+ w.mAppToken.onOverrideConfigurationChanged(config);
+ pf.set(0, 0, logicalWidth, logicalHeight);
+ task.mFullscreenForTest = true;
+
+ w.computeFrameLw(pf /* parentFrame */, pf /* displayFrame */, pf /* overscanFrame */,
+ cf /* contentFrame */, cf /* visibleFrame */, pf /* decorFrame */,
+ cf /* stableFrame */, null /* outsetFrame */, WmDisplayCutout.NO_CUTOUT, false);
+ assertEquals(cf, w.mFrame);
+ assertEquals(cf, w.getContentFrameLw());
+ assertRect(w.mContentInsets, 0, 0, 0, 0);
+ }
+
+ @Test
+ public void testDisplayCutout() {
+ // Regular fullscreen task and window
+ Task task = new TaskWithBounds(null);
+ WindowState w = createWindow(task, FILL_PARENT, FILL_PARENT);
+ w.mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
+
+ final Rect pf = new Rect(0, 0, 1000, 2000);
+ // Create a display cutout of size 50x50, aligned top-center
+ final WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(500, 0, 550, 50), pf.width(), pf.height());
+
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, cutout, false);
+
+ assertEquals(w.mDisplayCutout.getDisplayCutout().getSafeInsetTop(), 50);
+ assertEquals(w.mDisplayCutout.getDisplayCutout().getSafeInsetBottom(), 0);
+ assertEquals(w.mDisplayCutout.getDisplayCutout().getSafeInsetLeft(), 0);
+ assertEquals(w.mDisplayCutout.getDisplayCutout().getSafeInsetRight(), 0);
+ }
+
+ @Test
+ public void testDisplayCutout_tempInsetBounds() {
+ // Regular fullscreen task and window
+ TaskWithBounds task = new TaskWithBounds(new Rect(0, -500, 1000, 1500));
+ task.mFullscreenForTest = false;
+ task.mInsetBounds.set(0, 0, 1000, 2000);
+ WindowState w = createWindow(task, FILL_PARENT, FILL_PARENT);
+ w.mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
+
+ final Rect pf = new Rect(0, -500, 1000, 1500);
+ // Create a display cutout of size 50x50, aligned top-center
+ final WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(500, 0, 550, 50), pf.width(), pf.height());
+
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, cutout, false);
+
+ assertEquals(w.mDisplayCutout.getDisplayCutout().getSafeInsetTop(), 50);
+ assertEquals(w.mDisplayCutout.getDisplayCutout().getSafeInsetBottom(), 0);
+ assertEquals(w.mDisplayCutout.getDisplayCutout().getSafeInsetLeft(), 0);
+ assertEquals(w.mDisplayCutout.getDisplayCutout().getSafeInsetRight(), 0);
+ }
+
+ private WindowStateWithTask createWindow(Task task, int width, int height) {
+ final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(TYPE_APPLICATION);
+ attrs.width = width;
+ attrs.height = height;
+
+ return new WindowStateWithTask(attrs, task);
+ }
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowManagerServiceRule.java b/services/tests/servicestests/src/com/android/server/wm/WindowManagerServiceRule.java
new file mode 100644
index 0000000..d91079e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowManagerServiceRule.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.testing.DexmakerShareClassLoaderRule.runWithDexmakerShareClassLoader;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import android.app.ActivityManagerInternal;
+import android.content.Context;
+import android.hardware.display.DisplayManagerInternal;
+import android.os.PowerManagerInternal;
+import android.os.PowerSaveState;
+import android.support.test.InstrumentationRegistry;
+import android.view.InputChannel;
+
+import com.android.server.LocalServices;
+import com.android.server.input.InputManagerService;
+import com.android.server.policy.WindowManagerPolicy;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import org.mockito.invocation.InvocationOnMock;
+
+/**
+ * A test rule that sets up a fresh WindowManagerService instance before each test and makes sure
+ * to properly tear it down after.
+ *
+ * <p>
+ * Usage:
+ * <pre>
+ * {@literal @}Rule
+ * public final WindowManagerServiceRule mWmRule = new WindowManagerServiceRule();
+ * </pre>
+ */
+public class WindowManagerServiceRule implements TestRule {
+
+ private WindowManagerService mService;
+ private TestWindowManagerPolicy mPolicy;
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ runWithDexmakerShareClassLoader(this::setUp);
+ try {
+ base.evaluate();
+ } finally {
+ tearDown();
+ }
+ }
+
+ private void setUp() {
+ final Context context = InstrumentationRegistry.getTargetContext();
+
+ removeServices();
+
+ LocalServices.addService(DisplayManagerInternal.class,
+ mock(DisplayManagerInternal.class));
+
+ LocalServices.addService(PowerManagerInternal.class,
+ mock(PowerManagerInternal.class));
+ final PowerManagerInternal pm =
+ LocalServices.getService(PowerManagerInternal.class);
+ PowerSaveState state = new PowerSaveState.Builder().build();
+ doReturn(state).when(pm).getLowPowerState(anyInt());
+
+ LocalServices.addService(ActivityManagerInternal.class,
+ mock(ActivityManagerInternal.class));
+ final ActivityManagerInternal am =
+ LocalServices.getService(ActivityManagerInternal.class);
+ doAnswer((InvocationOnMock invocationOnMock) -> {
+ final Runnable runnable = invocationOnMock.<Runnable>getArgument(0);
+ if (runnable != null) {
+ runnable.run();
+ }
+ return null;
+ }).when(am).notifyKeyguardFlagsChanged(any());
+
+ InputManagerService ims = mock(InputManagerService.class);
+ // InputChannel is final and can't be mocked.
+ InputChannel[] input = InputChannel.openInputChannelPair(TAG_WM);
+ if (input != null && input.length > 1) {
+ doReturn(input[1]).when(ims).monitorInput(anyString());
+ }
+
+ mService = WindowManagerService.main(context, ims, true, false,
+ false, mPolicy = new TestWindowManagerPolicy(
+ WindowManagerServiceRule.this::getWindowManagerService));
+
+ mService.onInitReady();
+
+ // Display creation is driven by the ActivityManagerService via ActivityStackSupervisor.
+ // We emulate those steps here.
+ mService.mRoot.createDisplayContent(
+ mService.mDisplayManager.getDisplay(DEFAULT_DISPLAY),
+ mock(DisplayWindowController.class));
+ }
+
+ private void removeServices() {
+ LocalServices.removeServiceForTest(DisplayManagerInternal.class);
+ LocalServices.removeServiceForTest(PowerManagerInternal.class);
+ LocalServices.removeServiceForTest(ActivityManagerInternal.class);
+ LocalServices.removeServiceForTest(WindowManagerInternal.class);
+ LocalServices.removeServiceForTest(WindowManagerPolicy.class);
+ }
+
+ private void tearDown() {
+ waitUntilWindowManagerHandlersIdle();
+ removeServices();
+ mService = null;
+ mPolicy = null;
+ }
+ };
+ }
+
+ public WindowManagerService getWindowManagerService() {
+ return mService;
+ }
+
+ public TestWindowManagerPolicy getWindowManagerPolicy() {
+ return mPolicy;
+ }
+
+ public void waitUntilWindowManagerHandlersIdle() {
+ final WindowManagerService wm = getWindowManagerService();
+ if (wm != null) {
+ wm.mH.runWithScissors(() -> { }, 0);
+ wm.mAnimationHandler.runWithScissors(() -> { }, 0);
+ SurfaceAnimationThread.getHandler().runWithScissors(() -> { }, 0);
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowManagerServiceRuleTest.java b/services/tests/servicestests/src/com/android/server/wm/WindowManagerServiceRuleTest.java
new file mode 100644
index 0000000..6cf6d7b
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowManagerServiceRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.junit.Assert.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@Presubmit
+@SmallTest
+public class WindowManagerServiceRuleTest {
+
+ @Rule
+ public final WindowManagerServiceRule mRule = new WindowManagerServiceRule();
+
+ @Test
+ public void testWindowManagerSetUp() {
+ assertThat(mRule.getWindowManagerService(), notNullValue());
+ }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowStateTests.java b/services/tests/servicestests/src/com/android/server/wm/WindowStateTests.java
new file mode 100644
index 0000000..85e846d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowStateTests.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import android.view.WindowManager;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.util.LinkedList;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
+import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL;
+import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
+
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for the {@link WindowState} class.
+ *
+ * atest FrameworksServicesTests:com.android.server.wm.WindowStateTests
+ */
+@SmallTest
+@FlakyTest(bugId = 74078662)
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WindowStateTests extends WindowTestsBase {
+
+ @Test
+ public void testIsParentWindowHidden() throws Exception {
+ final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow");
+ final WindowState child1 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child1");
+ final WindowState child2 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child2");
+
+ // parentWindow is initially set to hidden.
+ assertTrue(parentWindow.mHidden);
+ assertFalse(parentWindow.isParentWindowHidden());
+ assertTrue(child1.isParentWindowHidden());
+ assertTrue(child2.isParentWindowHidden());
+
+ parentWindow.mHidden = false;
+ assertFalse(parentWindow.isParentWindowHidden());
+ assertFalse(child1.isParentWindowHidden());
+ assertFalse(child2.isParentWindowHidden());
+
+ }
+
+ @Test
+ public void testIsChildWindow() throws Exception {
+ final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow");
+ final WindowState child1 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child1");
+ final WindowState child2 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child2");
+ final WindowState randomWindow = createWindow(null, TYPE_APPLICATION, "randomWindow");
+
+ assertFalse(parentWindow.isChildWindow());
+ assertTrue(child1.isChildWindow());
+ assertTrue(child2.isChildWindow());
+ assertFalse(randomWindow.isChildWindow());
+ }
+
+ @Test
+ public void testHasChild() throws Exception {
+ final WindowState win1 = createWindow(null, TYPE_APPLICATION, "win1");
+ final WindowState win11 = createWindow(win1, FIRST_SUB_WINDOW, "win11");
+ final WindowState win12 = createWindow(win1, FIRST_SUB_WINDOW, "win12");
+ final WindowState win2 = createWindow(null, TYPE_APPLICATION, "win2");
+ final WindowState win21 = createWindow(win2, FIRST_SUB_WINDOW, "win21");
+ final WindowState randomWindow = createWindow(null, TYPE_APPLICATION, "randomWindow");
+
+ assertTrue(win1.hasChild(win11));
+ assertTrue(win1.hasChild(win12));
+ assertTrue(win2.hasChild(win21));
+
+ assertFalse(win1.hasChild(win21));
+ assertFalse(win1.hasChild(randomWindow));
+
+ assertFalse(win2.hasChild(win11));
+ assertFalse(win2.hasChild(win12));
+ assertFalse(win2.hasChild(randomWindow));
+ }
+
+ @Test
+ public void testGetParentWindow() throws Exception {
+ final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow");
+ final WindowState child1 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child1");
+ final WindowState child2 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child2");
+
+ assertNull(parentWindow.getParentWindow());
+ assertEquals(parentWindow, child1.getParentWindow());
+ assertEquals(parentWindow, child2.getParentWindow());
+ }
+
+ @Test
+ public void testOverlayWindowHiddenWhenSuspended() {
+ final WindowState overlayWindow = spy(createWindow(null, TYPE_APPLICATION_OVERLAY,
+ "overlayWindow"));
+ overlayWindow.setHiddenWhileSuspended(true);
+ verify(overlayWindow).hideLw(true, true);
+ overlayWindow.setHiddenWhileSuspended(false);
+ verify(overlayWindow).showLw(true, true);
+ }
+
+ @Test
+ public void testGetTopParentWindow() throws Exception {
+ final WindowState root = createWindow(null, TYPE_APPLICATION, "root");
+ final WindowState child1 = createWindow(root, FIRST_SUB_WINDOW, "child1");
+ final WindowState child2 = createWindow(child1, FIRST_SUB_WINDOW, "child2");
+
+ assertEquals(root, root.getTopParentWindow());
+ assertEquals(root, child1.getTopParentWindow());
+ assertEquals(child1, child2.getParentWindow());
+ assertEquals(root, child2.getTopParentWindow());
+
+ // Test case were child is detached from parent.
+ root.removeChild(child1);
+ assertEquals(child1, child1.getTopParentWindow());
+ assertEquals(child1, child2.getParentWindow());
+ }
+
+ @Test
+ public void testIsOnScreen_hiddenByPolicy() {
+ final WindowState window = createWindow(null, TYPE_APPLICATION, "window");
+ window.setHasSurface(true);
+ assertTrue(window.isOnScreen());
+ window.hideLw(false /* doAnimation */);
+ assertFalse(window.isOnScreen());
+ }
+
+ @Test
+ public void testCanBeImeTarget() throws Exception {
+ final WindowState appWindow = createWindow(null, TYPE_APPLICATION, "appWindow");
+ final WindowState imeWindow = createWindow(null, TYPE_INPUT_METHOD, "imeWindow");
+
+ // Setting FLAG_NOT_FOCUSABLE without FLAG_ALT_FOCUSABLE_IM prevents the window from being
+ // an IME target.
+ appWindow.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
+ imeWindow.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
+
+ // Make windows visible
+ appWindow.setHasSurface(true);
+ imeWindow.setHasSurface(true);
+
+ // Windows without flags (FLAG_NOT_FOCUSABLE|FLAG_ALT_FOCUSABLE_IM) can't be IME targets
+ assertFalse(appWindow.canBeImeTarget());
+ assertFalse(imeWindow.canBeImeTarget());
+
+ // Add IME target flags
+ appWindow.mAttrs.flags |= (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM);
+ imeWindow.mAttrs.flags |= (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM);
+
+ // Visible app window with flags can be IME target while an IME window can never be an IME
+ // target regardless of its visibility or flags.
+ assertTrue(appWindow.canBeImeTarget());
+ assertFalse(imeWindow.canBeImeTarget());
+
+ // Make windows invisible
+ appWindow.hideLw(false /* doAnimation */);
+ imeWindow.hideLw(false /* doAnimation */);
+
+ // Invisible window can't be IME targets even if they have the right flags.
+ assertFalse(appWindow.canBeImeTarget());
+ assertFalse(imeWindow.canBeImeTarget());
+ }
+
+ @Test
+ public void testGetWindow() throws Exception {
+ final WindowState root = createWindow(null, TYPE_APPLICATION, "root");
+ final WindowState mediaChild = createWindow(root, TYPE_APPLICATION_MEDIA, "mediaChild");
+ final WindowState mediaOverlayChild = createWindow(root,
+ TYPE_APPLICATION_MEDIA_OVERLAY, "mediaOverlayChild");
+ final WindowState attachedDialogChild = createWindow(root,
+ TYPE_APPLICATION_ATTACHED_DIALOG, "attachedDialogChild");
+ final WindowState subPanelChild = createWindow(root,
+ TYPE_APPLICATION_SUB_PANEL, "subPanelChild");
+ final WindowState aboveSubPanelChild = createWindow(root,
+ TYPE_APPLICATION_ABOVE_SUB_PANEL, "aboveSubPanelChild");
+
+ final LinkedList<WindowState> windows = new LinkedList();
+
+ root.getWindow(w -> {
+ windows.addLast(w);
+ return false;
+ });
+
+ // getWindow should have returned candidate windows in z-order.
+ assertEquals(aboveSubPanelChild, windows.pollFirst());
+ assertEquals(subPanelChild, windows.pollFirst());
+ assertEquals(attachedDialogChild, windows.pollFirst());
+ assertEquals(root, windows.pollFirst());
+ assertEquals(mediaOverlayChild, windows.pollFirst());
+ assertEquals(mediaChild, windows.pollFirst());
+ assertTrue(windows.isEmpty());
+ }
+
+ @Test
+ public void testPrepareWindowToDisplayDuringRelayout() throws Exception {
+ testPrepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ testPrepareWindowToDisplayDuringRelayout(true /*wasVisible*/);
+
+ // Call prepareWindowToDisplayDuringRelayout for a window without FLAG_TURN_SCREEN_ON
+ // before calling prepareWindowToDisplayDuringRelayout for windows with flag in the same
+ // appWindowToken.
+ final AppWindowToken appWindowToken = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ final WindowState first = createWindow(null, TYPE_APPLICATION, appWindowToken, "first");
+ final WindowState second = createWindow(null, TYPE_APPLICATION, appWindowToken, "second");
+ second.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
+
+ reset(mPowerManagerWrapper);
+ first.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ verify(mPowerManagerWrapper, never()).wakeUp(anyLong(), anyString());
+ assertTrue(appWindowToken.canTurnScreenOn());
+
+ reset(mPowerManagerWrapper);
+ second.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ verify(mPowerManagerWrapper).wakeUp(anyLong(), anyString());
+ assertFalse(appWindowToken.canTurnScreenOn());
+
+ // Call prepareWindowToDisplayDuringRelayout for two window that have FLAG_TURN_SCREEN_ON
+ // from the same appWindowToken. Only one should trigger the wakeup.
+ appWindowToken.setCanTurnScreenOn(true);
+ first.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
+ second.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
+
+ reset(mPowerManagerWrapper);
+ first.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ verify(mPowerManagerWrapper).wakeUp(anyLong(), anyString());
+ assertFalse(appWindowToken.canTurnScreenOn());
+
+ reset(mPowerManagerWrapper);
+ second.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ verify(mPowerManagerWrapper, never()).wakeUp(anyLong(), anyString());
+ assertFalse(appWindowToken.canTurnScreenOn());
+
+ // Call prepareWindowToDisplayDuringRelayout for a windows that are not children of an
+ // appWindowToken. Both windows have the FLAG_TURNS_SCREEN_ON so both should call wakeup
+ final WindowToken windowToken = WindowTestUtils.createTestWindowToken(FIRST_SUB_WINDOW,
+ mDisplayContent);
+ final WindowState firstWindow = createWindow(null, TYPE_APPLICATION, windowToken,
+ "firstWindow");
+ final WindowState secondWindow = createWindow(null, TYPE_APPLICATION, windowToken,
+ "secondWindow");
+ firstWindow.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
+ secondWindow.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
+
+ reset(mPowerManagerWrapper);
+ firstWindow.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ verify(mPowerManagerWrapper).wakeUp(anyLong(), anyString());
+
+ reset(mPowerManagerWrapper);
+ secondWindow.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ verify(mPowerManagerWrapper).wakeUp(anyLong(), anyString());
+ }
+
+ @Test
+ public void testCanAffectSystemUiFlags() throws Exception {
+ final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ app.mToken.setHidden(false);
+ assertTrue(app.canAffectSystemUiFlags());
+ app.mToken.setHidden(true);
+ assertFalse(app.canAffectSystemUiFlags());
+ app.mToken.setHidden(false);
+ app.mAttrs.alpha = 0.0f;
+ assertFalse(app.canAffectSystemUiFlags());
+
+ }
+
+ @Test
+ public void testCanAffectSystemUiFlags_disallow() throws Exception {
+ final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ app.mToken.setHidden(false);
+ assertTrue(app.canAffectSystemUiFlags());
+ app.getTask().setCanAffectSystemUiFlags(false);
+ assertFalse(app.canAffectSystemUiFlags());
+ }
+
+ @Test
+ public void testIsSelfOrAncestorWindowAnimating() throws Exception {
+ final WindowState root = createWindow(null, TYPE_APPLICATION, "root");
+ final WindowState child1 = createWindow(root, FIRST_SUB_WINDOW, "child1");
+ final WindowState child2 = createWindow(child1, FIRST_SUB_WINDOW, "child2");
+ assertFalse(child2.isSelfOrAncestorWindowAnimatingExit());
+ child2.mAnimatingExit = true;
+ assertTrue(child2.isSelfOrAncestorWindowAnimatingExit());
+ child2.mAnimatingExit = false;
+ root.mAnimatingExit = true;
+ assertTrue(child2.isSelfOrAncestorWindowAnimatingExit());
+ }
+
+ @Test
+ public void testLayoutSeqResetOnReparent() throws Exception {
+ final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ app.mLayoutSeq = 1;
+ mDisplayContent.mLayoutSeq = 1;
+
+ app.onDisplayChanged(mDisplayContent);
+
+ assertThat(app.mLayoutSeq, not(is(mDisplayContent.mLayoutSeq)));
+ }
+
+ @Test
+ public void testDisplayIdUpdatedOnReparent() throws Exception {
+ final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ // fake a different display
+ app.mInputWindowHandle.displayId = mDisplayContent.getDisplayId() + 1;
+ app.onDisplayChanged(mDisplayContent);
+
+ assertThat(app.mInputWindowHandle.displayId, is(mDisplayContent.getDisplayId()));
+ assertThat(app.getDisplayId(), is(mDisplayContent.getDisplayId()));
+ }
+
+ private void testPrepareWindowToDisplayDuringRelayout(boolean wasVisible) {
+ reset(mPowerManagerWrapper);
+ final WindowState root = createWindow(null, TYPE_APPLICATION, "root");
+ root.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
+
+ root.prepareWindowToDisplayDuringRelayout(wasVisible /*wasVisible*/);
+ verify(mPowerManagerWrapper).wakeUp(anyLong(), anyString());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowSurfacePlacerTest.java b/services/tests/servicestests/src/com/android/server/wm/WindowSurfacePlacerTest.java
new file mode 100644
index 0000000..e173b7d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowSurfacePlacerTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.WindowManager.TRANSIT_TASK_CLOSE;
+import static android.view.WindowManager.TRANSIT_TASK_OPEN;
+import static junit.framework.Assert.assertEquals;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.WindowManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WindowSurfacePlacerTest extends WindowTestsBase {
+
+ private WindowSurfacePlacer mWindowSurfacePlacer;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mWindowSurfacePlacer = new WindowSurfacePlacer(sWm);
+ }
+
+ @Test
+ public void testTranslucentOpen() throws Exception {
+ synchronized (sWm.mWindowMap) {
+ final AppWindowToken behind = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ final AppWindowToken translucentOpening = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ translucentOpening.setFillsParent(false);
+ translucentOpening.setHidden(true);
+ sWm.mOpeningApps.add(behind);
+ sWm.mOpeningApps.add(translucentOpening);
+ assertEquals(WindowManager.TRANSIT_TRANSLUCENT_ACTIVITY_OPEN,
+ mWindowSurfacePlacer.maybeUpdateTransitToTranslucentAnim(TRANSIT_TASK_OPEN));
+ }
+ }
+
+ @Test
+ public void testTranslucentClose() throws Exception {
+ synchronized (sWm.mWindowMap) {
+ final AppWindowToken behind = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ final AppWindowToken translucentClosing = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ translucentClosing.setFillsParent(false);
+ sWm.mClosingApps.add(translucentClosing);
+ assertEquals(WindowManager.TRANSIT_TRANSLUCENT_ACTIVITY_CLOSE,
+ mWindowSurfacePlacer.maybeUpdateTransitToTranslucentAnim(TRANSIT_TASK_CLOSE));
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowTestUtils.java b/services/tests/servicestests/src/com/android/server/wm/WindowTestUtils.java
new file mode 100644
index 0000000..2e4740b
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowTestUtils.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.os.IBinder;
+import android.view.IApplicationToken;
+import android.view.IWindow;
+import android.view.WindowManager;
+
+import static android.app.AppOpsManager.OP_NONE;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+
+import static com.android.server.wm.WindowContainer.POSITION_TOP;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyFloat;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.mockito.invocation.InvocationOnMock;
+
+/**
+ * A collection of static functions that can be referenced by other test packages to provide access
+ * to WindowManager related test functionality.
+ */
+public class WindowTestUtils {
+ public static int sNextTaskId = 0;
+
+ /**
+ * Retrieves an instance of a mock {@link WindowManagerService}.
+ */
+ public static WindowManagerService getMockWindowManagerService() {
+ final WindowManagerService service = mock(WindowManagerService.class);
+ final WindowHashMap windowMap = new WindowHashMap();
+ when(service.getWindowManagerLock()).thenReturn(windowMap);
+ return service;
+ }
+
+ /**
+ * Creates a mock instance of {@link StackWindowController}.
+ */
+ public static StackWindowController createMockStackWindowContainerController() {
+ StackWindowController controller = mock(StackWindowController.class);
+ controller.mContainer = mock(TestTaskStack.class);
+
+ // many components rely on the {@link StackWindowController#adjustConfigurationForBounds}
+ // to properly set bounds values in the configuration. We must mimick those actions here.
+ doAnswer((InvocationOnMock invocationOnMock) -> {
+ final Configuration config = invocationOnMock.<Configuration>getArgument(7);
+ final Rect bounds = invocationOnMock.<Rect>getArgument(0);
+ config.windowConfiguration.setBounds(bounds);
+ return null;
+ }).when(controller).adjustConfigurationForBounds(any(), any(), any(), any(),
+ anyBoolean(), anyBoolean(), anyFloat(), any(), any(), anyInt());
+
+ return controller;
+ }
+
+ /** Creates a {@link Task} and adds it to the specified {@link TaskStack}. */
+ public static Task createTaskInStack(WindowManagerService service, TaskStack stack,
+ int userId) {
+ synchronized (service.mWindowMap) {
+ final Task newTask = new Task(sNextTaskId++, stack, userId, service, 0, false,
+ new ActivityManager.TaskDescription(), null);
+ stack.addTask(newTask, POSITION_TOP);
+ return newTask;
+ }
+ }
+
+ /**
+ * An extension of {@link TestTaskStack}, which overrides package scoped methods that would not
+ * normally be mocked out.
+ */
+ public static class TestTaskStack extends TaskStack {
+ TestTaskStack(WindowManagerService service, int stackId) {
+ super(service, stackId, null);
+ }
+
+ @Override
+ void addTask(Task task, int position, boolean showForAllUsers, boolean moveParents) {
+ // Do nothing.
+ }
+ }
+
+ static TestAppWindowToken createTestAppWindowToken(DisplayContent dc) {
+ synchronized (dc.mService.mWindowMap) {
+ return new TestAppWindowToken(dc);
+ }
+ }
+
+ /** Used so we can gain access to some protected members of the {@link AppWindowToken} class. */
+ public static class TestAppWindowToken extends AppWindowToken {
+ boolean mOnTop = false;
+
+ private TestAppWindowToken(DisplayContent dc) {
+ super(dc.mService, new IApplicationToken.Stub() {
+ public String getName() {return null;}
+ }, false, dc, true /* fillsParent */);
+ }
+
+ TestAppWindowToken(WindowManagerService service, IApplicationToken token,
+ boolean voiceInteraction, DisplayContent dc, long inputDispatchingTimeoutNanos,
+ boolean fullscreen, boolean showForAllUsers, int targetSdk, int orientation,
+ int rotationAnimationHint, int configChanges, boolean launchTaskBehind,
+ boolean alwaysFocusable, AppWindowContainerController controller) {
+ super(service, token, voiceInteraction, dc, inputDispatchingTimeoutNanos, fullscreen,
+ showForAllUsers, targetSdk, orientation, rotationAnimationHint, configChanges,
+ launchTaskBehind, alwaysFocusable, controller);
+ }
+
+ int getWindowsCount() {
+ return mChildren.size();
+ }
+
+ boolean hasWindow(WindowState w) {
+ return mChildren.contains(w);
+ }
+
+ WindowState getFirstChild() {
+ return mChildren.peekFirst();
+ }
+
+ WindowState getLastChild() {
+ return mChildren.peekLast();
+ }
+
+ int positionInParent() {
+ return getParent().mChildren.indexOf(this);
+ }
+
+ void setIsOnTop(boolean onTop) {
+ mOnTop = onTop;
+ }
+
+ @Override
+ boolean isOnTop() {
+ return mOnTop;
+ }
+ }
+
+ static TestWindowToken createTestWindowToken(int type, DisplayContent dc) {
+ return createTestWindowToken(type, dc, false /* persistOnEmpty */);
+ }
+
+ static TestWindowToken createTestWindowToken(int type, DisplayContent dc,
+ boolean persistOnEmpty) {
+ synchronized (dc.mService.mWindowMap) {
+ return new TestWindowToken(type, dc, persistOnEmpty);
+ }
+ }
+
+ /* Used so we can gain access to some protected members of the {@link WindowToken} class */
+ public static class TestWindowToken extends WindowToken {
+
+ private TestWindowToken(int type, DisplayContent dc, boolean persistOnEmpty) {
+ super(dc.mService, mock(IBinder.class), type, persistOnEmpty, dc,
+ false /* ownerCanManageAppTokens */);
+ }
+
+ int getWindowsCount() {
+ return mChildren.size();
+ }
+
+ boolean hasWindow(WindowState w) {
+ return mChildren.contains(w);
+ }
+ }
+
+ /* Used so we can gain access to some protected members of the {@link Task} class */
+ public static class TestTask extends Task {
+ boolean mShouldDeferRemoval = false;
+ boolean mOnDisplayChangedCalled = false;
+ private boolean mIsAnimating = false;
+
+ TestTask(int taskId, TaskStack stack, int userId, WindowManagerService service,
+ int resizeMode, boolean supportsPictureInPicture,
+ TaskWindowContainerController controller) {
+ super(taskId, stack, userId, service, resizeMode, supportsPictureInPicture,
+ new ActivityManager.TaskDescription(), controller);
+ }
+
+ boolean shouldDeferRemoval() {
+ return mShouldDeferRemoval;
+ }
+
+ int positionInParent() {
+ return getParent().mChildren.indexOf(this);
+ }
+
+ @Override
+ void onDisplayChanged(DisplayContent dc) {
+ super.onDisplayChanged(dc);
+ mOnDisplayChangedCalled = true;
+ }
+
+ @Override
+ boolean isSelfAnimating() {
+ return mIsAnimating;
+ }
+
+ void setLocalIsAnimating(boolean isAnimating) {
+ mIsAnimating = isAnimating;
+ }
+ }
+
+ /**
+ * Used so we can gain access to some protected members of {@link TaskWindowContainerController}
+ * class.
+ */
+ public static class TestTaskWindowContainerController extends TaskWindowContainerController {
+
+ TestTaskWindowContainerController(WindowTestsBase testsBase) {
+ this(testsBase.createStackControllerOnDisplay(testsBase.mDisplayContent));
+ }
+
+ TestTaskWindowContainerController(StackWindowController stackController) {
+ super(sNextTaskId++, new TaskWindowContainerListener() {
+ @Override
+ public void registerConfigurationChangeListener(
+ ConfigurationContainerListener listener) {
+
+ }
+
+ @Override
+ public void unregisterConfigurationChangeListener(
+ ConfigurationContainerListener listener) {
+
+ }
+
+ @Override
+ public void onSnapshotChanged(ActivityManager.TaskSnapshot snapshot) {
+
+ }
+
+ @Override
+ public void requestResize(Rect bounds, int resizeMode) {
+
+ }
+ }, stackController, 0 /* userId */, null /* bounds */, RESIZE_MODE_UNRESIZEABLE,
+ false /* supportsPictureInPicture */, true /* toTop*/,
+ true /* showForAllUsers */, new ActivityManager.TaskDescription(),
+ stackController.mService);
+ }
+
+ @Override
+ TestTask createTask(int taskId, TaskStack stack, int userId, int resizeMode,
+ boolean supportsPictureInPicture, ActivityManager.TaskDescription taskDescription) {
+ return new TestTask(taskId, stack, userId, mService, resizeMode,
+ supportsPictureInPicture, this);
+ }
+ }
+
+ public static class TestAppWindowContainerController extends AppWindowContainerController {
+
+ final IApplicationToken mToken;
+
+ TestAppWindowContainerController(TestTaskWindowContainerController taskController) {
+ this(taskController, new TestIApplicationToken());
+ }
+
+ TestAppWindowContainerController(TestTaskWindowContainerController taskController,
+ IApplicationToken token) {
+ super(taskController, token, null /* listener */, 0 /* index */,
+ SCREEN_ORIENTATION_UNSPECIFIED, true /* fullscreen */,
+ true /* showForAllUsers */, 0 /* configChanges */, false /* voiceInteraction */,
+ false /* launchTaskBehind */, false /* alwaysFocusable */,
+ 0 /* targetSdkVersion */, 0 /* rotationAnimationHint */,
+ 0 /* inputDispatchingTimeoutNanos */, taskController.mService);
+ mToken = token;
+ }
+
+ @Override
+ AppWindowToken createAppWindow(WindowManagerService service, IApplicationToken token,
+ boolean voiceInteraction, DisplayContent dc, long inputDispatchingTimeoutNanos,
+ boolean fullscreen, boolean showForAllUsers, int targetSdk, int orientation,
+ int rotationAnimationHint, int configChanges, boolean launchTaskBehind,
+ boolean alwaysFocusable, AppWindowContainerController controller) {
+ return new TestAppWindowToken(service, token, voiceInteraction, dc,
+ inputDispatchingTimeoutNanos, fullscreen, showForAllUsers, targetSdk,
+ orientation,
+ rotationAnimationHint, configChanges, launchTaskBehind, alwaysFocusable,
+ controller);
+ }
+
+ AppWindowToken getAppWindowToken(DisplayContent dc) {
+ return (AppWindowToken) dc.getWindowToken(mToken.asBinder());
+ }
+ }
+
+ public static class TestIApplicationToken implements IApplicationToken {
+
+ private final Binder mBinder = new Binder();
+ @Override
+ public IBinder asBinder() {
+ return mBinder;
+ }
+ @Override
+ public String getName() {
+ return null;
+ }
+ }
+
+ /** Used to track resize reports. */
+ public static class TestWindowState extends WindowState {
+ boolean resizeReported;
+
+ TestWindowState(WindowManagerService service, Session session, IWindow window,
+ WindowManager.LayoutParams attrs, WindowToken token) {
+ super(service, session, window, token, null, OP_NONE, 0, attrs, 0, 0,
+ false /* ownerCanAddInternalSystemWindow */);
+ }
+
+ @Override
+ void reportResized() {
+ super.reportResized();
+ resizeReported = true;
+ }
+
+ @Override
+ public boolean isGoneForLayoutLw() {
+ return false;
+ }
+
+ @Override
+ void updateResizingWindowIfNeeded() {
+ // Used in AppWindowTokenTests#testLandscapeSeascapeRotationRelayout to deceive
+ // the system that it can actually update the window.
+ boolean hadSurface = mHasSurface;
+ mHasSurface = true;
+
+ super.updateResizingWindowIfNeeded();
+
+ mHasSurface = hadSurface;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java
new file mode 100644
index 0000000..473a287
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.View.VISIBLE;
+
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManagerGlobal;
+import android.testing.DexmakerShareClassLoaderRule;
+import android.util.Log;
+import android.view.Display;
+import android.view.DisplayInfo;
+import org.junit.Assert;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.view.IWindow;
+import android.view.WindowManager;
+
+import static android.app.AppOpsManager.OP_NONE;
+import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
+import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
+import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
+import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG;
+import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
+import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
+import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+import static org.mockito.Mockito.mock;
+
+import com.android.server.AttributeCache;
+
+import java.util.HashSet;
+import java.util.LinkedList;
+
+/**
+ * Common base class for window manager unit test classes.
+ *
+ * Make sure any requests to WM hold the WM lock if needed b/73966377
+ */
+class WindowTestsBase {
+ private static final String TAG = WindowTestsBase.class.getSimpleName();
+ WindowManagerService sWm = null; // TODO(roosa): rename to mWm in follow-up CL
+ private final IWindow mIWindow = new TestIWindow();
+ private Session mMockSession;
+ // The default display is removed in {@link #setUp} and then we iterate over all displays to
+ // make sure we don't collide with any existing display. If we run into no other display, the
+ // added display should be treated as default. This cannot be the default display
+ private static int sNextDisplayId = DEFAULT_DISPLAY + 1;
+ static int sNextStackId = 1000;
+
+ DisplayContent mDisplayContent;
+ DisplayInfo mDisplayInfo = new DisplayInfo();
+ WindowState mWallpaperWindow;
+ WindowState mImeWindow;
+ WindowState mImeDialogWindow;
+ WindowState mStatusBarWindow;
+ WindowState mDockedDividerWindow;
+ WindowState mNavBarWindow;
+ WindowState mAppWindow;
+ WindowState mChildAppWindowAbove;
+ WindowState mChildAppWindowBelow;
+ HashSet<WindowState> mCommonWindows;
+ WallpaperController mWallpaperController;
+
+ @Rule
+ public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
+ new DexmakerShareClassLoaderRule();
+
+ @Rule
+ public final WindowManagerServiceRule mWmRule = new WindowManagerServiceRule();
+
+ static WindowState.PowerManagerWrapper mPowerManagerWrapper; // TODO(roosa): make non-static.
+
+ @Before
+ public void setUp() throws Exception {
+ // If @Before throws an exception, the error isn't logged. This will make sure any failures
+ // in the set up are clear. This can be removed when b/37850063 is fixed.
+ try {
+ mMockSession = mock(Session.class);
+ mPowerManagerWrapper = mock(WindowState.PowerManagerWrapper.class);
+
+ final Context context = InstrumentationRegistry.getTargetContext();
+ AttributeCache.init(context);
+
+ sWm = mWmRule.getWindowManagerService();
+ beforeCreateDisplay();
+
+ mWallpaperController = new WallpaperController(sWm);
+
+ context.getDisplay().getDisplayInfo(mDisplayInfo);
+ mDisplayContent = createNewDisplay();
+ sWm.mDisplayEnabled = true;
+ sWm.mDisplayReady = true;
+
+ // Set-up some common windows.
+ mCommonWindows = new HashSet();
+ synchronized (sWm.mWindowMap) {
+ mWallpaperWindow = createCommonWindow(null, TYPE_WALLPAPER, "wallpaperWindow");
+ mImeWindow = createCommonWindow(null, TYPE_INPUT_METHOD, "mImeWindow");
+ sWm.mInputMethodWindow = mImeWindow;
+ mImeDialogWindow = createCommonWindow(null, TYPE_INPUT_METHOD_DIALOG,
+ "mImeDialogWindow");
+ mStatusBarWindow = createCommonWindow(null, TYPE_STATUS_BAR, "mStatusBarWindow");
+ mNavBarWindow = createCommonWindow(null, TYPE_NAVIGATION_BAR, "mNavBarWindow");
+ mDockedDividerWindow = createCommonWindow(null, TYPE_DOCK_DIVIDER,
+ "mDockedDividerWindow");
+ mAppWindow = createCommonWindow(null, TYPE_BASE_APPLICATION, "mAppWindow");
+ mChildAppWindowAbove = createCommonWindow(mAppWindow,
+ TYPE_APPLICATION_ATTACHED_DIALOG,
+ "mChildAppWindowAbove");
+ mChildAppWindowBelow = createCommonWindow(mAppWindow,
+ TYPE_APPLICATION_MEDIA_OVERLAY,
+ "mChildAppWindowBelow");
+ }
+ // Adding a display will cause freezing the display. Make sure to wait until it's
+ // unfrozen to not run into race conditions with the tests.
+ waitUntilHandlersIdle();
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to set up test", e);
+ throw e;
+ }
+ }
+
+ void beforeCreateDisplay() {
+ // Called before display is created.
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ // If @After throws an exception, the error isn't logged. This will make sure any failures
+ // in the tear down are clear. This can be removed when b/37850063 is fixed.
+ try {
+ final LinkedList<WindowState> nonCommonWindows = new LinkedList();
+
+ synchronized (sWm.mWindowMap) {
+ sWm.mRoot.forAllWindows(w -> {
+ if (!mCommonWindows.contains(w)) {
+ nonCommonWindows.addLast(w);
+ }
+ }, true /* traverseTopToBottom */);
+
+ while (!nonCommonWindows.isEmpty()) {
+ nonCommonWindows.pollLast().removeImmediately();
+ }
+
+ mDisplayContent.removeImmediately();
+ sWm.mInputMethodTarget = null;
+ sWm.mClosingApps.clear();
+ sWm.mOpeningApps.clear();
+ }
+
+ // Wait until everything is really cleaned up.
+ waitUntilHandlersIdle();
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to tear down test", e);
+ throw e;
+ }
+ }
+
+ /**
+ * @return A SurfaceBuilderFactory to inject in to the WindowManagerService during
+ * set-up (or null).
+ */
+ SurfaceBuilderFactory getSurfaceBuilderFactory() {
+ return null;
+ }
+
+ private WindowState createCommonWindow(WindowState parent, int type, String name) {
+ synchronized (sWm.mWindowMap) {
+ final WindowState win = createWindow(parent, type, name);
+ mCommonWindows.add(win);
+ // Prevent common windows from been IMe targets
+ win.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
+ return win;
+ }
+ }
+
+ /** Asserts that the first entry is greater than the second entry. */
+ void assertGreaterThan(int first, int second) throws Exception {
+ Assert.assertTrue("Excepted " + first + " to be greater than " + second, first > second);
+ }
+
+ /** Asserts that the first entry is greater than the second entry. */
+ void assertLessThan(int first, int second) throws Exception {
+ Assert.assertTrue("Excepted " + first + " to be less than " + second, first < second);
+ }
+
+ /**
+ * Waits until the main handler for WM has processed all messages.
+ */
+ void waitUntilHandlersIdle() {
+ mWmRule.waitUntilWindowManagerHandlersIdle();
+ }
+
+ private WindowToken createWindowToken(
+ DisplayContent dc, int windowingMode, int activityType, int type) {
+ synchronized (sWm.mWindowMap) {
+ if (type < FIRST_APPLICATION_WINDOW || type > LAST_APPLICATION_WINDOW) {
+ return WindowTestUtils.createTestWindowToken(type, dc);
+ }
+
+ return createAppWindowToken(dc, windowingMode, activityType);
+ }
+ }
+
+ AppWindowToken createAppWindowToken(DisplayContent dc, int windowingMode, int activityType) {
+ final TaskStack stack = createStackControllerOnStackOnDisplay(windowingMode, activityType,
+ dc).mContainer;
+ final Task task = createTaskInStack(stack, 0 /* userId */);
+ final WindowTestUtils.TestAppWindowToken appWindowToken =
+ WindowTestUtils.createTestAppWindowToken(dc);
+ task.addChild(appWindowToken, 0);
+ return appWindowToken;
+ }
+
+ WindowState createWindow(WindowState parent, int type, String name) {
+ synchronized (sWm.mWindowMap) {
+ return (parent == null)
+ ? createWindow(parent, type, mDisplayContent, name)
+ : createWindow(parent, type, parent.mToken, name);
+ }
+ }
+
+ WindowState createWindowOnStack(WindowState parent, int windowingMode, int activityType,
+ int type, DisplayContent dc, String name) {
+ synchronized (sWm.mWindowMap) {
+ final WindowToken token = createWindowToken(dc, windowingMode, activityType, type);
+ return createWindow(parent, type, token, name);
+ }
+ }
+
+ WindowState createAppWindow(Task task, int type, String name) {
+ synchronized (sWm.mWindowMap) {
+ final AppWindowToken token = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ task.addChild(token, 0);
+ return createWindow(null, type, token, name);
+ }
+ }
+
+ WindowState createWindow(WindowState parent, int type, DisplayContent dc, String name) {
+ synchronized (sWm.mWindowMap) {
+ final WindowToken token = createWindowToken(
+ dc, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, type);
+ return createWindow(parent, type, token, name);
+ }
+ }
+
+ WindowState createWindow(WindowState parent, int type, DisplayContent dc, String name,
+ boolean ownerCanAddInternalSystemWindow) {
+ synchronized (sWm.mWindowMap) {
+ final WindowToken token = createWindowToken(
+ dc, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, type);
+ return createWindow(parent, type, token, name, 0 /* ownerId */,
+ ownerCanAddInternalSystemWindow);
+ }
+ }
+
+ WindowState createWindow(WindowState parent, int type, WindowToken token, String name) {
+ synchronized (sWm.mWindowMap) {
+ return createWindow(parent, type, token, name, 0 /* ownerId */,
+ false /* ownerCanAddInternalSystemWindow */);
+ }
+ }
+
+ WindowState createWindow(WindowState parent, int type, WindowToken token, String name,
+ int ownerId, boolean ownerCanAddInternalSystemWindow) {
+ return createWindow(parent, type, token, name, ownerId, ownerCanAddInternalSystemWindow,
+ sWm, mMockSession, mIWindow);
+ }
+
+ static WindowState createWindow(WindowState parent, int type, WindowToken token,
+ String name, int ownerId, boolean ownerCanAddInternalSystemWindow,
+ WindowManagerService service, Session session, IWindow iWindow) {
+ synchronized (service.mWindowMap) {
+ final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(type);
+ attrs.setTitle(name);
+
+ final WindowState w = new WindowState(service, session, iWindow, token, parent,
+ OP_NONE,
+ 0, attrs, VISIBLE, ownerId, ownerCanAddInternalSystemWindow,
+ mPowerManagerWrapper);
+ // TODO: Probably better to make this call in the WindowState ctor to avoid errors with
+ // adding it to the token...
+ token.addWindow(w);
+ return w;
+ }
+ }
+
+ /** Creates a {@link TaskStack} and adds it to the specified {@link DisplayContent}. */
+ TaskStack createTaskStackOnDisplay(DisplayContent dc) {
+ synchronized (sWm.mWindowMap) {
+ return createStackControllerOnDisplay(dc).mContainer;
+ }
+ }
+
+ StackWindowController createStackControllerOnDisplay(DisplayContent dc) {
+ synchronized (sWm.mWindowMap) {
+ return createStackControllerOnStackOnDisplay(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, dc);
+ }
+ }
+
+ StackWindowController createStackControllerOnStackOnDisplay(
+ int windowingMode, int activityType, DisplayContent dc) {
+ synchronized (sWm.mWindowMap) {
+ final Configuration overrideConfig = new Configuration();
+ overrideConfig.windowConfiguration.setWindowingMode(windowingMode);
+ overrideConfig.windowConfiguration.setActivityType(activityType);
+ final int stackId = ++sNextStackId;
+ final StackWindowController controller = new StackWindowController(stackId, null,
+ dc.getDisplayId(), true /* onTop */, new Rect(), sWm);
+ controller.onOverrideConfigurationChanged(overrideConfig);
+ return controller;
+ }
+ }
+
+ /** Creates a {@link Task} and adds it to the specified {@link TaskStack}. */
+ Task createTaskInStack(TaskStack stack, int userId) {
+ return WindowTestUtils.createTaskInStack(sWm, stack, userId);
+ }
+
+ /** Creates a {@link DisplayContent} and adds it to the system. */
+ DisplayContent createNewDisplay() {
+ final int displayId = sNextDisplayId++;
+ final Display display = new Display(DisplayManagerGlobal.getInstance(), displayId,
+ mDisplayInfo, DEFAULT_DISPLAY_ADJUSTMENTS);
+ synchronized (sWm.mWindowMap) {
+ return new DisplayContent(display, sWm, mWallpaperController,
+ mock(DisplayWindowController.class));
+ }
+ }
+
+ /** Creates a {@link com.android.server.wm.WindowTestUtils.TestWindowState} */
+ WindowTestUtils.TestWindowState createWindowState(WindowManager.LayoutParams attrs,
+ WindowToken token) {
+ synchronized (sWm.mWindowMap) {
+ return new WindowTestUtils.TestWindowState(sWm, mMockSession, mIWindow, attrs, token);
+ }
+ }
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowTokenTests.java b/services/tests/servicestests/src/com/android/server/wm/WindowTokenTests.java
new file mode 100644
index 0000000..e3b7174
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowTokenTests.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for the {@link WindowToken} class.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.WindowTokenTests
+ */
+@SmallTest
+@FlakyTest(bugId = 74078662)
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WindowTokenTests extends WindowTestsBase {
+
+ @Test
+ public void testAddWindow() throws Exception {
+ final WindowTestUtils.TestWindowToken token =
+ WindowTestUtils.createTestWindowToken(0, mDisplayContent);
+
+ assertEquals(0, token.getWindowsCount());
+
+ final WindowState window1 = createWindow(null, TYPE_APPLICATION, token, "window1");
+ final WindowState window11 = createWindow(window1, FIRST_SUB_WINDOW, token, "window11");
+ final WindowState window12 = createWindow(window1, FIRST_SUB_WINDOW, token, "window12");
+ final WindowState window2 = createWindow(null, TYPE_APPLICATION, token, "window2");
+ final WindowState window3 = createWindow(null, TYPE_APPLICATION, token, "window3");
+
+ token.addWindow(window1);
+ // NOTE: Child windows will not be added to the token as window containers can only
+ // contain/reference their direct children.
+ token.addWindow(window11);
+ token.addWindow(window12);
+ token.addWindow(window2);
+ token.addWindow(window3);
+
+ // Should not contain the child windows that were added above.
+ assertEquals(3, token.getWindowsCount());
+ assertTrue(token.hasWindow(window1));
+ assertFalse(token.hasWindow(window11));
+ assertFalse(token.hasWindow(window12));
+ assertTrue(token.hasWindow(window2));
+ assertTrue(token.hasWindow(window3));
+ }
+
+ @Test
+ public void testChildRemoval() throws Exception {
+ final DisplayContent dc = mDisplayContent;
+ final WindowTestUtils.TestWindowToken token = WindowTestUtils.createTestWindowToken(0, dc);
+
+ assertEquals(token, dc.getWindowToken(token.token));
+
+ final WindowState window1 = createWindow(null, TYPE_APPLICATION, token, "window1");
+ final WindowState window2 = createWindow(null, TYPE_APPLICATION, token, "window2");
+
+ window2.removeImmediately();
+ // The token should still be mapped in the display content since it still has a child.
+ assertEquals(token, dc.getWindowToken(token.token));
+
+ window1.removeImmediately();
+ // The token should have been removed from the display content since it no longer has a
+ // child.
+ assertEquals(null, dc.getWindowToken(token.token));
+ }
+
+ /**
+ * Test that a window token isn't orphaned by the system when it is requested to be removed.
+ * Tokens should only be removed from the system when all their windows are gone.
+ */
+ @Test
+ public void testTokenRemovalProcess() throws Exception {
+ final WindowTestUtils.TestWindowToken token = WindowTestUtils.createTestWindowToken(
+ TYPE_TOAST, mDisplayContent, true /* persistOnEmpty */);
+
+ // Verify that the token is on the display
+ assertNotNull(mDisplayContent.getWindowToken(token.token));
+
+ final WindowState window1 = createWindow(null, TYPE_TOAST, token, "window1");
+ final WindowState window2 = createWindow(null, TYPE_TOAST, token, "window2");
+
+ mDisplayContent.removeWindowToken(token.token);
+ // Verify that the token is no longer mapped on the display
+ assertNull(mDisplayContent.getWindowToken(token.token));
+ // Verify that the token is still attached to its parent
+ assertNotNull(token.getParent());
+ // Verify that the token windows are still around.
+ assertEquals(2, token.getWindowsCount());
+
+ window1.removeImmediately();
+ // Verify that the token is still attached to its parent
+ assertNotNull(token.getParent());
+ // Verify that the other token window is still around.
+ assertEquals(1, token.getWindowsCount());
+
+ window2.removeImmediately();
+ // Verify that the token is no-longer attached to its parent
+ assertNull(token.getParent());
+ // Verify that the token windows are no longer attached to it.
+ assertEquals(0, token.getWindowsCount());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowTracingTest.java b/services/tests/servicestests/src/com/android/server/wm/WindowTracingTest.java
new file mode 100644
index 0000000..5085254
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowTracingTest.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.filters.FlakyTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.wm.WindowManagerTraceProto;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Test class for {@link WindowTracing}.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.WindowTracingTest
+ */
+@SmallTest
+@FlakyTest(bugId = 74078662)
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WindowTracingTest extends WindowTestsBase {
+
+ private static final byte[] MAGIC_HEADER = new byte[] {
+ 0x9, 0x57, 0x49, 0x4e, 0x54, 0x52, 0x41, 0x43, 0x45,
+ };
+
+ private Context mTestContext;
+ private WindowTracing mWindowTracing;
+ private WindowManagerService mWmMock;
+ private File mFile;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mWmMock = mock(WindowManagerService.class);
+
+ mTestContext = InstrumentationRegistry.getContext();
+
+ mFile = mTestContext.getFileStreamPath("tracing_test.dat");
+ mFile.delete();
+
+ mWindowTracing = new WindowTracing(mFile);
+ }
+
+ @Test
+ public void isEnabled_returnsFalseByDefault() throws Exception {
+ assertFalse(mWindowTracing.isEnabled());
+ }
+
+ @Test
+ public void isEnabled_returnsTrueAfterStart() throws Exception {
+ mWindowTracing.startTrace(mock(PrintWriter.class));
+ assertTrue(mWindowTracing.isEnabled());
+ }
+
+ @Test
+ public void isEnabled_returnsFalseAfterStop() throws Exception {
+ mWindowTracing.startTrace(mock(PrintWriter.class));
+ mWindowTracing.stopTrace(mock(PrintWriter.class));
+ assertFalse(mWindowTracing.isEnabled());
+ }
+
+ @Test
+ public void trace_discared_whenNotTracing() throws Exception {
+ mWindowTracing.traceStateLocked("where", mWmMock);
+ verifyZeroInteractions(mWmMock);
+ }
+
+ @Test
+ public void trace_dumpsWindowManagerState_whenTracing() throws Exception {
+ mWindowTracing.startTrace(mock(PrintWriter.class));
+ mWindowTracing.traceStateLocked("where", mWmMock);
+
+ verify(mWmMock).writeToProtoLocked(any(), eq(true));
+ }
+
+ @Test
+ public void traceFile_startsWithMagicHeader() throws Exception {
+ mWindowTracing.startTrace(mock(PrintWriter.class));
+ mWindowTracing.stopTrace(mock(PrintWriter.class));
+
+ byte[] header = new byte[MAGIC_HEADER.length];
+ try (InputStream is = new FileInputStream(mFile)) {
+ assertEquals(MAGIC_HEADER.length, is.read(header));
+ assertArrayEquals(MAGIC_HEADER, header);
+ }
+ }
+
+ @Test
+ @Ignore("Figure out why this test is crashing when setting up mWmMock.")
+ public void tracing_endsUpInFile() throws Exception {
+ mWindowTracing.startTrace(mock(PrintWriter.class));
+
+ doAnswer((inv) -> {
+ inv.<ProtoOutputStream>getArgument(0).write(
+ WindowManagerTraceProto.WHERE, "TEST_WM_PROTO");
+ return null;
+ }).when(mWmMock).writeToProtoLocked(any(), any());
+ mWindowTracing.traceStateLocked("TEST_WHERE", mWmMock);
+
+ mWindowTracing.stopTrace(mock(PrintWriter.class));
+
+ byte[] file = new byte[1000];
+ int fileLength;
+ try (InputStream is = new FileInputStream(mFile)) {
+ fileLength = is.read(file);
+ assertTrue(containsBytes(file, fileLength,
+ "TEST_WHERE".getBytes(StandardCharsets.UTF_8)));
+ assertTrue(containsBytes(file, fileLength,
+ "TEST_WM_PROTO".getBytes(StandardCharsets.UTF_8)));
+ }
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+
+ mFile.delete();
+ }
+
+ /** Return true if {@code needle} appears anywhere in {@code haystack[0..length]} */
+ boolean containsBytes(byte[] haystack, int haystackLenght, byte[] needle) {
+ Preconditions.checkArgument(haystackLenght > 0);
+ Preconditions.checkArgument(needle.length > 0);
+
+ outer: for (int i = 0; i <= haystackLenght - needle.length; i++) {
+ for (int j = 0; j < needle.length; j++) {
+ if (haystack[i+j] != needle[j]) {
+ continue outer;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Test
+ public void test_containsBytes() {
+ byte[] haystack = "hello_world".getBytes(StandardCharsets.UTF_8);
+ assertTrue(containsBytes(haystack, haystack.length,
+ "hello".getBytes(StandardCharsets.UTF_8)));
+ assertTrue(containsBytes(haystack, haystack.length,
+ "world".getBytes(StandardCharsets.UTF_8)));
+ assertFalse(containsBytes(haystack, 6,
+ "world".getBytes(StandardCharsets.UTF_8)));
+ assertFalse(containsBytes(haystack, haystack.length,
+ "world_".getBytes(StandardCharsets.UTF_8)));
+ assertFalse(containsBytes(haystack, haystack.length,
+ "absent".getBytes(StandardCharsets.UTF_8)));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/ZOrderingTests.java b/services/tests/servicestests/src/com/android/server/wm/ZOrderingTests.java
new file mode 100644
index 0000000..547be55
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/ZOrderingTests.java
@@ -0,0 +1,399 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
+import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
+import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL;
+import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL;
+import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.filters.FlakyTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+
+/**
+ * Tests for the {@link WindowLayersController} class.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.ZOrderingTests
+ */
+@SmallTest
+@FlakyTest(bugId = 74078662)
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class ZOrderingTests extends WindowTestsBase {
+
+ private class LayerRecordingTransaction extends SurfaceControl.Transaction {
+ HashMap<SurfaceControl, Integer> mLayersForControl = new HashMap();
+ HashMap<SurfaceControl, SurfaceControl> mRelativeLayersForControl = new HashMap();
+
+ @Override
+ public SurfaceControl.Transaction setLayer(SurfaceControl sc, int layer) {
+ mRelativeLayersForControl.remove(sc);
+ mLayersForControl.put(sc, layer);
+ return super.setLayer(sc, layer);
+ }
+
+ @Override
+ public SurfaceControl.Transaction setRelativeLayer(SurfaceControl sc,
+ SurfaceControl relativeTo,
+ int layer) {
+ mRelativeLayersForControl.put(sc, relativeTo);
+ mLayersForControl.put(sc, layer);
+ return super.setRelativeLayer(sc, relativeTo, layer);
+ }
+
+ private int getLayer(SurfaceControl sc) {
+ return mLayersForControl.getOrDefault(sc, 0);
+ }
+
+ private SurfaceControl getRelativeLayer(SurfaceControl sc) {
+ return mRelativeLayersForControl.get(sc);
+ }
+ };
+
+ // We have WM use our Hierarchy recording subclass of SurfaceControl.Builder
+ // such that we can keep track of the parents of Surfaces as they are constructed.
+ private HashMap<SurfaceControl, SurfaceControl> mParentFor = new HashMap();
+
+ private class HierarchyRecorder extends SurfaceControl.Builder {
+ SurfaceControl mPendingParent;
+
+ HierarchyRecorder(SurfaceSession s) {
+ super(s);
+ }
+
+ public SurfaceControl.Builder setParent(SurfaceControl sc) {
+ mPendingParent = sc;
+ return super.setParent(sc);
+ }
+ public SurfaceControl build() {
+ SurfaceControl sc = super.build();
+ mParentFor.put(sc, mPendingParent);
+ mPendingParent = null;
+ return sc;
+ }
+ };
+
+ class HierarchyRecordingBuilderFactory implements SurfaceBuilderFactory {
+ public SurfaceControl.Builder make(SurfaceSession s) {
+ return new HierarchyRecorder(s);
+ }
+ };
+
+ private LayerRecordingTransaction mTransaction;
+
+ @Override
+ void beforeCreateDisplay() {
+ // We can't use @Before here because it may happen after WindowTestsBase @Before
+ // which is after construction of the DisplayContent, meaning the HierarchyRecorder
+ // would miss construction of the top-level layers.
+ mTransaction = new LayerRecordingTransaction();
+ sWm.mSurfaceBuilderFactory = new HierarchyRecordingBuilderFactory();
+ sWm.mTransactionFactory = () -> mTransaction;
+ }
+
+ @After
+ public void after() {
+ mTransaction.close();
+ mParentFor.clear();
+ }
+
+ LinkedList<SurfaceControl> getAncestors(LayerRecordingTransaction t, SurfaceControl sc) {
+ LinkedList<SurfaceControl> p = new LinkedList();
+ SurfaceControl current = sc;
+ do {
+ p.addLast(current);
+
+ SurfaceControl rs = t.getRelativeLayer(current);
+ if (rs != null) {
+ current = rs;
+ } else {
+ current = mParentFor.get(current);
+ }
+ } while (current != null);
+ return p;
+ }
+
+
+ void assertZOrderGreaterThan(LayerRecordingTransaction t, SurfaceControl left,
+ SurfaceControl right) throws Exception {
+ final LinkedList<SurfaceControl> leftParentChain = getAncestors(t, left);
+ final LinkedList<SurfaceControl> rightParentChain = getAncestors(t, right);
+
+ SurfaceControl commonAncestor = null;
+ SurfaceControl leftTop = leftParentChain.peekLast();
+ SurfaceControl rightTop = rightParentChain.peekLast();
+ while (leftTop != null && rightTop != null && leftTop == rightTop) {
+ commonAncestor = leftParentChain.removeLast();
+ rightParentChain.removeLast();
+ leftTop = leftParentChain.peekLast();
+ rightTop = rightParentChain.peekLast();
+ }
+
+ if (rightTop == null) { // right is the parent of left.
+ assertGreaterThan(t.getLayer(leftTop), 0);
+ } else if (leftTop == null) { // left is the parent of right.
+ assertGreaterThan(0, t.getLayer(rightTop));
+ } else {
+ assertGreaterThan(t.getLayer(leftTop),
+ t.getLayer(rightTop));
+ }
+ }
+
+ void assertWindowHigher(WindowState left, WindowState right) throws Exception {
+ assertZOrderGreaterThan(mTransaction, left.getSurfaceControl(), right.getSurfaceControl());
+ }
+
+ WindowState createWindow(String name) {
+ return createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, name);
+ }
+
+ @Test
+ public void testAssignWindowLayers_ForImeWithNoTarget() throws Exception {
+ sWm.mInputMethodTarget = null;
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ // The Ime has an higher base layer than app windows and lower base layer than system
+ // windows, so it should be above app windows and below system windows if there isn't an IME
+ // target.
+ assertWindowHigher(mImeWindow, mChildAppWindowAbove);
+ assertWindowHigher(mImeWindow, mAppWindow);
+ assertWindowHigher(mNavBarWindow, mImeWindow);
+ assertWindowHigher(mStatusBarWindow, mImeWindow);
+
+ // And, IME dialogs should always have an higher layer than the IME.
+ assertWindowHigher(mImeDialogWindow, mImeWindow);
+ }
+
+ @Test
+ public void testAssignWindowLayers_ForImeWithAppTarget() throws Exception {
+ final WindowState imeAppTarget = createWindow("imeAppTarget");
+ sWm.mInputMethodTarget = imeAppTarget;
+
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ // Ime should be above all app windows and below system windows if it is targeting an app
+ // window.
+ assertWindowHigher(mImeWindow, imeAppTarget);
+ assertWindowHigher(mImeWindow, mChildAppWindowAbove);
+ assertWindowHigher(mImeWindow, mAppWindow);
+ assertWindowHigher(mNavBarWindow, mImeWindow);
+ assertWindowHigher(mStatusBarWindow, mImeWindow);
+
+ // And, IME dialogs should always have an higher layer than the IME.
+ assertWindowHigher(mImeDialogWindow, mImeWindow);
+ }
+
+ @Test
+ public void testAssignWindowLayers_ForImeWithAppTargetWithChildWindows() throws Exception {
+ final WindowState imeAppTarget = createWindow("imeAppTarget");
+ final WindowState imeAppTargetChildAboveWindow = createWindow(imeAppTarget,
+ TYPE_APPLICATION_ATTACHED_DIALOG, imeAppTarget.mToken,
+ "imeAppTargetChildAboveWindow");
+ final WindowState imeAppTargetChildBelowWindow = createWindow(imeAppTarget,
+ TYPE_APPLICATION_MEDIA_OVERLAY, imeAppTarget.mToken,
+ "imeAppTargetChildBelowWindow");
+
+ sWm.mInputMethodTarget = imeAppTarget;
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ // Ime should be above all app windows except for child windows that are z-ordered above it
+ // and below system windows if it is targeting an app window.
+ assertWindowHigher(mImeWindow, imeAppTarget);
+ assertWindowHigher(imeAppTargetChildAboveWindow, mImeWindow);
+ assertWindowHigher(mImeWindow, mChildAppWindowAbove);
+ assertWindowHigher(mImeWindow, mAppWindow);
+ assertWindowHigher(mNavBarWindow, mImeWindow);
+ assertWindowHigher(mStatusBarWindow, mImeWindow);
+
+ // And, IME dialogs should always have an higher layer than the IME.
+ assertWindowHigher(mImeDialogWindow, mImeWindow);
+ }
+
+ @Test
+ public void testAssignWindowLayers_ForImeWithAppTargetAndAppAbove() throws Exception {
+ final WindowState appBelowImeTarget = createWindow("appBelowImeTarget");
+ final WindowState imeAppTarget = createWindow("imeAppTarget");
+ final WindowState appAboveImeTarget = createWindow("appAboveImeTarget");
+
+ sWm.mInputMethodTarget = imeAppTarget;
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ // Ime should be above all app windows except for non-fullscreen app window above it and
+ // below system windows if it is targeting an app window.
+ assertWindowHigher(mImeWindow, imeAppTarget);
+ assertWindowHigher(mImeWindow, appBelowImeTarget);
+ assertWindowHigher(appAboveImeTarget, mImeWindow);
+ assertWindowHigher(mImeWindow, mChildAppWindowAbove);
+ assertWindowHigher(mImeWindow, mAppWindow);
+ assertWindowHigher(mNavBarWindow, mImeWindow);
+ assertWindowHigher(mStatusBarWindow, mImeWindow);
+
+ // And, IME dialogs should always have an higher layer than the IME.
+ assertWindowHigher(mImeDialogWindow, mImeWindow);
+ }
+
+ @Test
+ public void testAssignWindowLayers_ForImeNonAppImeTarget() throws Exception {
+ final WindowState imeSystemOverlayTarget = createWindow(null, TYPE_SYSTEM_OVERLAY,
+ mDisplayContent, "imeSystemOverlayTarget",
+ true /* ownerCanAddInternalSystemWindow */);
+
+ sWm.mInputMethodTarget = imeSystemOverlayTarget;
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ // The IME target base layer is higher than all window except for the nav bar window, so the
+ // IME should be above all windows except for the nav bar.
+ assertWindowHigher(mImeWindow, imeSystemOverlayTarget);
+ assertWindowHigher(mImeWindow, mChildAppWindowAbove);
+ assertWindowHigher(mImeWindow, mAppWindow);
+ assertWindowHigher(mImeWindow, mDockedDividerWindow);
+
+ // The IME has a higher base layer than the status bar so we may expect it to go
+ // above the status bar once they are both in the Non-App layer, as past versions of this
+ // test enforced. However this seems like the wrong behavior unless the status bar is the
+ // IME target.
+ assertWindowHigher(mNavBarWindow, mImeWindow);
+ assertWindowHigher(mStatusBarWindow, mImeWindow);
+
+ // And, IME dialogs should always have an higher layer than the IME.
+ assertWindowHigher(mImeDialogWindow, mImeWindow);
+ }
+
+ @Test
+ public void testAssignWindowLayers_ForStatusBarImeTarget() throws Exception {
+ sWm.mInputMethodTarget = mStatusBarWindow;
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ assertWindowHigher(mImeWindow, mChildAppWindowAbove);
+ assertWindowHigher(mImeWindow, mAppWindow);
+ assertWindowHigher(mImeWindow, mDockedDividerWindow);
+ assertWindowHigher(mImeWindow, mStatusBarWindow);
+
+ // And, IME dialogs should always have an higher layer than the IME.
+ assertWindowHigher(mImeDialogWindow, mImeWindow);
+ }
+
+ @Test
+ public void testStackLayers() throws Exception {
+ final WindowState anyWindow1 = createWindow("anyWindow");
+ final WindowState pinnedStackWindow = createWindowOnStack(null, WINDOWING_MODE_PINNED,
+ ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION, mDisplayContent,
+ "pinnedStackWindow");
+ final WindowState dockedStackWindow = createWindowOnStack(null,
+ WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION,
+ mDisplayContent, "dockedStackWindow");
+ final WindowState assistantStackWindow = createWindowOnStack(null, WINDOWING_MODE_FULLSCREEN,
+ ACTIVITY_TYPE_ASSISTANT, TYPE_BASE_APPLICATION,
+ mDisplayContent, "assistantStackWindow");
+ final WindowState homeActivityWindow = createWindowOnStack(null, WINDOWING_MODE_FULLSCREEN,
+ ACTIVITY_TYPE_HOME, TYPE_BASE_APPLICATION,
+ mDisplayContent, "homeActivityWindow");
+ final WindowState anyWindow2 = createWindow("anyWindow2");
+
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ assertWindowHigher(dockedStackWindow, homeActivityWindow);
+ assertWindowHigher(assistantStackWindow, homeActivityWindow);
+ assertWindowHigher(pinnedStackWindow, homeActivityWindow);
+ assertWindowHigher(anyWindow1, homeActivityWindow);
+ assertWindowHigher(anyWindow2, homeActivityWindow);
+ assertWindowHigher(pinnedStackWindow, anyWindow1);
+ assertWindowHigher(pinnedStackWindow, anyWindow2);
+ assertWindowHigher(pinnedStackWindow, dockedStackWindow);
+ assertWindowHigher(pinnedStackWindow, assistantStackWindow);
+ }
+
+ @Test
+ public void testAssignWindowLayers_ForSysUiPanels() throws Exception {
+ final WindowState navBarPanel =
+ createWindow(null, TYPE_NAVIGATION_BAR_PANEL, mDisplayContent, "NavBarPanel");
+ final WindowState statusBarPanel =
+ createWindow(null, TYPE_STATUS_BAR_PANEL, mDisplayContent, "StatusBarPanel");
+ final WindowState statusBarSubPanel =
+ createWindow(null, TYPE_STATUS_BAR_SUB_PANEL, mDisplayContent, "StatusBarSubPanel");
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ // Ime should be above all app windows and below system windows if it is targeting an app
+ // window.
+ assertWindowHigher(navBarPanel, mNavBarWindow);
+ assertWindowHigher(statusBarPanel, mStatusBarWindow);
+ assertWindowHigher(statusBarSubPanel, statusBarPanel);
+ }
+
+ @Test
+ public void testAssignWindowLayers_ForNegativelyZOrderedSubtype() throws Exception {
+ // TODO(b/70040778): We should aim to eliminate the last user of TYPE_APPLICATION_MEDIA
+ // then we can drop all negative layering on the windowing side.
+
+ final WindowState anyWindow = createWindow("anyWindow");
+ final WindowState child = createWindow(anyWindow, TYPE_APPLICATION_MEDIA, mDisplayContent,
+ "TypeApplicationMediaChild");
+ final WindowState mediaOverlayChild = createWindow(anyWindow, TYPE_APPLICATION_MEDIA_OVERLAY,
+ mDisplayContent, "TypeApplicationMediaOverlayChild");
+
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ assertWindowHigher(anyWindow, mediaOverlayChild);
+ assertWindowHigher(mediaOverlayChild, child);
+ }
+
+ @Test
+ public void testDockedDividerPosition() throws Exception {
+ final WindowState pinnedStackWindow = createWindowOnStack(null, WINDOWING_MODE_PINNED,
+ ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION, mDisplayContent,
+ "pinnedStackWindow");
+ final WindowState splitScreenWindow = createWindowOnStack(null,
+ WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION,
+ mDisplayContent, "splitScreenWindow");
+ final WindowState splitScreenSecondaryWindow = createWindowOnStack(null,
+ WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_STANDARD,
+ TYPE_BASE_APPLICATION, mDisplayContent, "splitScreenSecondaryWindow");
+ final WindowState assistantStackWindow = createWindowOnStack(null, WINDOWING_MODE_FULLSCREEN,
+ ACTIVITY_TYPE_ASSISTANT, TYPE_BASE_APPLICATION,
+ mDisplayContent, "assistantStackWindow");
+
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ assertWindowHigher(mDockedDividerWindow, splitScreenWindow);
+ assertWindowHigher(mDockedDividerWindow, splitScreenSecondaryWindow);
+ assertWindowHigher(pinnedStackWindow, mDockedDividerWindow);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/utils/CoordinateTransformsTest.java b/services/tests/servicestests/src/com/android/server/wm/utils/CoordinateTransformsTest.java
new file mode 100644
index 0000000..40a10e0
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/utils/CoordinateTransformsTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.utils;
+
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+
+import static com.android.server.wm.utils.CoordinateTransforms.transformPhysicalToLogicalCoordinates;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.*;
+
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.PointF;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ErrorCollector;
+
+public class CoordinateTransformsTest {
+
+ private static final int W = 200;
+ private static final int H = 400;
+
+ private final Matrix mMatrix = new Matrix();
+
+ @Rule
+ public final ErrorCollector mErrorCollector = new ErrorCollector();
+
+ @Before
+ public void setUp() throws Exception {
+ mMatrix.setTranslate(0xdeadbeef, 0xdeadbeef);
+ }
+
+ @Test
+ public void transformPhysicalToLogicalCoordinates_rot0() throws Exception {
+ transformPhysicalToLogicalCoordinates(ROTATION_0, W, H, mMatrix);
+ assertThat(mMatrix, is(Matrix.IDENTITY_MATRIX));
+ }
+
+ @Test
+ public void transformPhysicalToLogicalCoordinates_rot90() throws Exception {
+ transformPhysicalToLogicalCoordinates(ROTATION_90, W, H, mMatrix);
+
+ checkDevicePoint(0, 0).mapsToLogicalPoint(0, W);
+ checkDevicePoint(W, H).mapsToLogicalPoint(H, 0);
+ }
+
+ @Test
+ public void transformPhysicalToLogicalCoordinates_rot180() throws Exception {
+ transformPhysicalToLogicalCoordinates(ROTATION_180, W, H, mMatrix);
+
+ checkDevicePoint(0, 0).mapsToLogicalPoint(W, H);
+ checkDevicePoint(W, H).mapsToLogicalPoint(0, 0);
+ }
+
+ @Test
+ public void transformPhysicalToLogicalCoordinates_rot270() throws Exception {
+ transformPhysicalToLogicalCoordinates(ROTATION_270, W, H, mMatrix);
+
+ checkDevicePoint(0, 0).mapsToLogicalPoint(H, 0);
+ checkDevicePoint(W, H).mapsToLogicalPoint(0, W);
+ }
+
+ private DevicePointAssertable checkDevicePoint(int x, int y) {
+ final Point devicePoint = new Point(x, y);
+ final float[] fs = new float[] {x, y};
+ mMatrix.mapPoints(fs);
+ final PointF transformedPoint = new PointF(fs[0], fs[1]);
+
+ return (expectedX, expectedY) -> {
+ mErrorCollector.checkThat("t(" + devicePoint + ")",
+ transformedPoint, is(new PointF(expectedX, expectedY)));
+ };
+ }
+
+ public interface DevicePointAssertable {
+ void mapsToLogicalPoint(int x, int y);
+ }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/wm/utils/InsetUtilsTest.java b/services/tests/servicestests/src/com/android/server/wm/utils/InsetUtilsTest.java
new file mode 100644
index 0000000..d0f0fe3
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/utils/InsetUtilsTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm.utils;
+
+import static junit.framework.Assert.assertEquals;
+
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Pair;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class InsetUtilsTest {
+
+ @Test
+ public void testAdd() throws Exception {
+ final Rect rect1 = new Rect(10, 20, 30, 40);
+ final Rect rect2 = new Rect(50, 60, 70, 80);
+ InsetUtils.addInsets(rect1, rect2);
+ assertEquals(new Rect(60, 80, 100, 120), rect1);
+ }
+}
+
diff --git a/services/tests/servicestests/src/com/android/server/wm/utils/RotationCacheTest.java b/services/tests/servicestests/src/com/android/server/wm/utils/RotationCacheTest.java
new file mode 100644
index 0000000..6bbc7eb
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/utils/RotationCacheTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.utils;
+
+import static android.util.Pair.create;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Pair;
+
+import com.android.server.wm.utils.RotationCache.RotationDependentComputation;
+
+import org.hamcrest.Matchers;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@FlakyTest(bugId = 74078662)
+@Presubmit
+public class RotationCacheTest {
+
+ private RotationCache<Object, Pair<Object, Integer>> mCache;
+ private boolean mComputationCalled;
+
+ @Before
+ public void setUp() throws Exception {
+ mComputationCalled = false;
+ mCache = new RotationCache<>((o, rot) -> {
+ mComputationCalled = true;
+ return create(o, rot);
+ });
+ }
+
+ @Test
+ public void getOrCompute_computes() throws Exception {
+ assertThat(mCache.getOrCompute("hello", 0), equalTo(create("hello", 0)));
+ assertThat(mCache.getOrCompute("hello", 1), equalTo(create("hello", 1)));
+ assertThat(mCache.getOrCompute("hello", 2), equalTo(create("hello", 2)));
+ assertThat(mCache.getOrCompute("hello", 3), equalTo(create("hello", 3)));
+ }
+
+ @Test
+ public void getOrCompute_sameParam_sameRot_hitsCache() throws Exception {
+ assertNotNull(mCache.getOrCompute("hello", 1));
+
+ mComputationCalled = false;
+ assertThat(mCache.getOrCompute("hello", 1), equalTo(create("hello", 1)));
+ assertThat(mComputationCalled, is(false));
+ }
+
+ @Test
+ public void getOrCompute_sameParam_hitsCache_forAllRots() throws Exception {
+ assertNotNull(mCache.getOrCompute("hello", 3));
+ assertNotNull(mCache.getOrCompute("hello", 2));
+ assertNotNull(mCache.getOrCompute("hello", 1));
+ assertNotNull(mCache.getOrCompute("hello", 0));
+
+ mComputationCalled = false;
+ assertThat(mCache.getOrCompute("hello", 1), equalTo(create("hello", 1)));
+ assertThat(mCache.getOrCompute("hello", 0), equalTo(create("hello", 0)));
+ assertThat(mCache.getOrCompute("hello", 2), equalTo(create("hello", 2)));
+ assertThat(mCache.getOrCompute("hello", 3), equalTo(create("hello", 3)));
+ assertThat(mComputationCalled, is(false));
+ }
+
+ @Test
+ public void getOrCompute_changingParam_recomputes() throws Exception {
+ assertNotNull(mCache.getOrCompute("hello", 1));
+
+ assertThat(mCache.getOrCompute("world", 1), equalTo(create("world", 1)));
+ }
+
+ @Test
+ public void getOrCompute_changingParam_clearsCacheForDifferentRots() throws Exception {
+ assertNotNull(mCache.getOrCompute("hello", 1));
+ assertNotNull(mCache.getOrCompute("world", 2));
+
+ mComputationCalled = false;
+ assertThat(mCache.getOrCompute("hello", 1), equalTo(create("hello", 1)));
+ assertThat(mComputationCalled, is(true));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/utils/WmDisplayCutoutTest.java b/services/tests/servicestests/src/com/android/server/wm/utils/WmDisplayCutoutTest.java
new file mode 100644
index 0000000..f7addf6
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/utils/WmDisplayCutoutTest.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.utils;
+
+
+import static android.view.DisplayCutout.NO_CUTOUT;
+import static android.view.DisplayCutout.fromBoundingRect;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Size;
+import android.view.DisplayCutout;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+/**
+ * Tests for {@link WmDisplayCutout}
+ *
+ * Run with: atest WmDisplayCutoutTest
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class WmDisplayCutoutTest {
+
+ private final DisplayCutout mCutoutTop = new DisplayCutout(
+ new Rect(0, 100, 0, 0),
+ Arrays.asList(new Rect(50, 0, 75, 100)));
+
+ @Test
+ public void calculateRelativeTo_top() {
+ WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(0, 0, 100, 20), 200, 400)
+ .calculateRelativeTo(new Rect(5, 5, 95, 195));
+
+ assertEquals(new Rect(0, 15, 0, 0), cutout.getDisplayCutout().getSafeInsets());
+ }
+
+ @Test
+ public void calculateRelativeTo_left() {
+ WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(0, 0, 20, 100), 400, 200)
+ .calculateRelativeTo(new Rect(5, 5, 195, 95));
+
+ assertEquals(new Rect(15, 0, 0, 0), cutout.getDisplayCutout().getSafeInsets());
+ }
+
+ @Test
+ public void calculateRelativeTo_bottom() {
+ WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(0, 180, 100, 200), 100, 200)
+ .calculateRelativeTo(new Rect(5, 5, 95, 195));
+
+ assertEquals(new Rect(0, 0, 0, 15), cutout.getDisplayCutout().getSafeInsets());
+ }
+
+ @Test
+ public void calculateRelativeTo_right() {
+ WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(180, 0, 200, 100), 200, 100)
+ .calculateRelativeTo(new Rect(5, 5, 195, 95));
+
+ assertEquals(new Rect(0, 0, 15, 0), cutout.getDisplayCutout().getSafeInsets());
+ }
+
+ @Test
+ public void calculateRelativeTo_bounds() {
+ WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(0, 0, 100, 20), 200, 400)
+ .calculateRelativeTo(new Rect(5, 10, 95, 180));
+
+ assertEquals(new Rect(-5, -10, 95, 10), cutout.getDisplayCutout().getBounds().getBounds());
+ }
+
+ @Test
+ public void computeSafeInsets_top() {
+ WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(0, 0, 100, 20), 200, 400);
+
+ assertEquals(new Rect(0, 20, 0, 0), cutout.getDisplayCutout().getSafeInsets());
+ }
+
+ @Test
+ public void computeSafeInsets_left() {
+ WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(0, 0, 20, 100), 400, 200);
+
+ assertEquals(new Rect(20, 0, 0, 0), cutout.getDisplayCutout().getSafeInsets());
+ }
+
+ @Test
+ public void computeSafeInsets_bottom() {
+ WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(0, 180, 100, 200), 100, 200);
+
+ assertEquals(new Rect(0, 0, 0, 20), cutout.getDisplayCutout().getSafeInsets());
+ }
+
+ @Test
+ public void computeSafeInsets_right() {
+ WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(180, 0, 200, 100), 200, 100);
+
+ assertEquals(new Rect(0, 0, 20, 0), cutout.getDisplayCutout().getSafeInsets());
+ }
+
+ @Test
+ public void computeSafeInsets_bounds() {
+ DisplayCutout cutout = WmDisplayCutout.computeSafeInsets(mCutoutTop, 1000,
+ 2000).getDisplayCutout();
+
+ assertEquals(mCutoutTop.getBounds().getBounds(),
+ cutout.getBounds().getBounds());
+ }
+
+ @Test
+ public void test_equals() {
+ assertEquals(new WmDisplayCutout(NO_CUTOUT, null), new WmDisplayCutout(NO_CUTOUT, null));
+ assertEquals(new WmDisplayCutout(mCutoutTop, new Size(1, 2)),
+ new WmDisplayCutout(mCutoutTop, new Size(1, 2)));
+
+ assertNotEquals(new WmDisplayCutout(mCutoutTop, new Size(1, 2)),
+ new WmDisplayCutout(mCutoutTop, new Size(5, 6)));
+ assertNotEquals(new WmDisplayCutout(mCutoutTop, new Size(1, 2)),
+ new WmDisplayCutout(NO_CUTOUT, new Size(1, 2)));
+ }
+
+ @Test
+ public void test_hashCode() {
+ assertEquals(new WmDisplayCutout(NO_CUTOUT, null).hashCode(),
+ new WmDisplayCutout(NO_CUTOUT, null).hashCode());
+ assertEquals(new WmDisplayCutout(mCutoutTop, new Size(1, 2)).hashCode(),
+ new WmDisplayCutout(mCutoutTop, new Size(1, 2)).hashCode());
+ }
+}
\ No newline at end of file