Move WindowManager tests to WmTests
Bug: 113800711
Test: All presubmit and non-flaky tests in WmTests pass
$ tradefed.sh run commandAndExit WmTests \
--include-annotation android.platform.test.annotations.Presubmit \
--exclude-annotation androidx.test.filters.FlakyTest
Change-Id: I593eb03c8ee3c297eae85e39434d04138a67ab15
diff --git a/services/tests/wmtests/src/com/android/server/wm/AnimatingAppWindowTokenRegistryTest.java b/services/tests/wmtests/src/com/android/server/wm/AnimatingAppWindowTokenRegistryTest.java
new file mode 100644
index 0000000..5556a15
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/AnimatingAppWindowTokenRegistryTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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 com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for the {@link TaskStack} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:AnimatingAppWindowTokenRegistryTest
+ */
+@SmallTest
+@Presubmit
+@FlakyTest(detail = "Promote once confirmed non-flaky")
+public class AnimatingAppWindowTokenRegistryTest extends WindowTestsBase {
+
+ @Mock
+ AnimationAdapter mAdapter;
+
+ @Mock
+ Runnable mMockEndDeferFinishCallback1;
+ @Mock
+ Runnable mMockEndDeferFinishCallback2;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void testDeferring() {
+ 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() {
+ 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/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
new file mode 100644
index 0000000..5e12a95
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.view.WindowManager;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:AppTransitionControllerTest
+ */
+@SmallTest
+@Presubmit
+public class AppTransitionControllerTest extends WindowTestsBase {
+
+ private AppTransitionController mAppTransitionController;
+
+ @Before
+ public void setUp() throws Exception {
+ mAppTransitionController = new AppTransitionController(mWm, mDisplayContent);
+ }
+
+ @Test
+ public void testTranslucentOpen() {
+ synchronized (mWm.mGlobalLock) {
+ 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);
+ mDisplayContent.mOpeningApps.add(behind);
+ mDisplayContent.mOpeningApps.add(translucentOpening);
+ assertEquals(WindowManager.TRANSIT_TRANSLUCENT_ACTIVITY_OPEN,
+ mAppTransitionController.maybeUpdateTransitToTranslucentAnim(
+ TRANSIT_TASK_OPEN));
+ }
+ }
+
+ @Test
+ public void testTranslucentClose() {
+ synchronized (mWm.mGlobalLock) {
+ 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);
+ mDisplayContent.mClosingApps.add(translucentClosing);
+ assertEquals(WindowManager.TRANSIT_TRANSLUCENT_ACTIVITY_CLOSE,
+ mAppTransitionController.maybeUpdateTransitToTranslucentAnim(
+ TRANSIT_TASK_CLOSE));
+ }
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
new file mode 100644
index 0000000..577859c
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -0,0 +1,170 @@
+/*
+ * 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.WindowManager.TRANSIT_ACTIVITY_CLOSE;
+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 com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.view.Display;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for {@link AppTransition}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:AppTransitionTests
+ */
+@SmallTest
+@Presubmit
+public class AppTransitionTests extends WindowTestsBase {
+
+ private DisplayContent mDc;
+
+ @Before
+ public void setUp() throws Exception {
+ mDc = mWm.getDefaultDisplayContentLocked();
+ // For unit test, we don't need to test performSurfacePlacement to prevent some
+ // abnormal interaction with surfaceflinger native side.
+ mWm.mRoot = spy(mWm.mRoot);
+ doNothing().when(mWm.mRoot).performSurfacePlacement(anyBoolean());
+ }
+
+ @Test
+ public void testKeyguardOverride() {
+ mWm.prepareAppTransition(TRANSIT_ACTIVITY_OPEN, false /* alwaysKeepCurrent */);
+ mWm.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY, false /* alwaysKeepCurrent */);
+ assertEquals(TRANSIT_KEYGUARD_GOING_AWAY, mDc.mAppTransition.getAppTransition());
+ }
+
+ @Test
+ public void testKeyguardKeep() {
+ mWm.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY, false /* alwaysKeepCurrent */);
+ mWm.prepareAppTransition(TRANSIT_ACTIVITY_OPEN, false /* alwaysKeepCurrent */);
+ assertEquals(TRANSIT_KEYGUARD_GOING_AWAY, mDc.mAppTransition.getAppTransition());
+ }
+
+ @Test
+ public void testForceOverride() {
+ mWm.prepareAppTransition(TRANSIT_KEYGUARD_UNOCCLUDE, false /* alwaysKeepCurrent */);
+ mDc.getController().prepareAppTransition(TRANSIT_ACTIVITY_OPEN,
+ false /* alwaysKeepCurrent */, 0 /* flags */, true /* forceOverride */);
+ assertEquals(TRANSIT_ACTIVITY_OPEN, mDc.mAppTransition.getAppTransition());
+ }
+
+ @Test
+ public void testCrashing() {
+ mWm.prepareAppTransition(TRANSIT_ACTIVITY_OPEN, false /* alwaysKeepCurrent */);
+ mWm.prepareAppTransition(TRANSIT_CRASHING_ACTIVITY_CLOSE, false /* alwaysKeepCurrent */);
+ assertEquals(TRANSIT_CRASHING_ACTIVITY_CLOSE, mDc.mAppTransition.getAppTransition());
+ }
+
+ @Test
+ public void testKeepKeyguard_withCrashing() {
+ mWm.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY, false /* alwaysKeepCurrent */);
+ mWm.prepareAppTransition(TRANSIT_CRASHING_ACTIVITY_CLOSE, false /* alwaysKeepCurrent */);
+ assertEquals(TRANSIT_KEYGUARD_GOING_AWAY, mDc.mAppTransition.getAppTransition());
+ }
+
+ @Test
+ public void testAppTransitionStateForMultiDisplay() {
+ // Create 2 displays & presume both display the state is ON for ready to display & animate.
+ final DisplayContent dc1 = createNewDisplayWithController(Display.STATE_ON);
+ final DisplayContent dc2 = createNewDisplayWithController(Display.STATE_ON);
+
+ // Create 2 app window tokens to represent 2 activity window.
+ final WindowTestUtils.TestAppWindowToken token1 = createTestAppWindowToken(dc1,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ final WindowTestUtils.TestAppWindowToken token2 = createTestAppWindowToken(dc2,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+
+ token1.allDrawn = true;
+ token1.startingDisplayed = true;
+ token1.startingMoved = true;
+
+ // Simulate activity resume / finish flows to prepare app transition & set visibility,
+ // make sure transition is set as expected for each display.
+ dc1.getController().prepareAppTransition(TRANSIT_ACTIVITY_OPEN,
+ false /* alwaysKeepCurrent */, 0 /* flags */, false /* forceOverride */);
+ assertEquals(TRANSIT_ACTIVITY_OPEN, dc1.mAppTransition.getAppTransition());
+ dc2.getController().prepareAppTransition(TRANSIT_ACTIVITY_CLOSE,
+ false /* alwaysKeepCurrent */, 0 /* flags */, false /* forceOverride */);
+ assertEquals(TRANSIT_ACTIVITY_CLOSE, dc2.mAppTransition.getAppTransition());
+ // One activity window is visible for resuming & the other activity window is invisible
+ // for finishing in different display.
+ token1.setVisibility(true, false);
+ token2.setVisibility(false, false);
+
+ // Make sure each display is in animating stage.
+ assertTrue(dc1.mOpeningApps.size() > 0);
+ assertTrue(dc2.mClosingApps.size() > 0);
+ assertTrue(dc1.isAppAnimating());
+ assertTrue(dc2.isAppAnimating());
+ }
+
+ @Test
+ public void testCleanAppTransitionWhenTaskStackReparent() {
+ // Create 2 displays & presume both display the state is ON for ready to display & animate.
+ final DisplayContent dc1 = createNewDisplayWithController(Display.STATE_ON);
+ final DisplayContent dc2 = createNewDisplayWithController(Display.STATE_ON);
+
+ final TaskStack stack1 = createTaskStackOnDisplay(dc1);
+ final Task task1 = createTaskInStack(stack1, 0 /* userId */);
+ final WindowTestUtils.TestAppWindowToken token1 =
+ WindowTestUtils.createTestAppWindowToken(dc1);
+ task1.addChild(token1, 0);
+
+ // Simulate same app is during opening / closing transition set stage.
+ dc1.mClosingApps.add(token1);
+ assertTrue(dc1.mClosingApps.size() > 0);
+
+ dc1.getController().prepareAppTransition(TRANSIT_ACTIVITY_OPEN,
+ false /* alwaysKeepCurrent */, 0 /* flags */, false /* forceOverride */);
+ assertEquals(TRANSIT_ACTIVITY_OPEN, dc1.mAppTransition.getAppTransition());
+ assertTrue(dc1.mAppTransition.isTransitionSet());
+
+ dc1.mOpeningApps.add(token1);
+ assertTrue(dc1.mOpeningApps.size() > 0);
+
+ // Move stack to another display.
+ stack1.getController().reparent(dc2.getDisplayId(), new Rect(), true);
+
+ // Verify if token are cleared from both pending transition list in former display.
+ assertFalse(dc1.mOpeningApps.contains(token1));
+ assertFalse(dc1.mOpeningApps.contains(token1));
+ }
+
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java b/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java
new file mode 100644
index 0000000..dcfb879
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java
@@ -0,0 +1,117 @@
+/*
+ * 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.SurfaceControl.Transaction;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+
+import android.view.SurfaceControl;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.WindowTestUtils.TestAppWindowToken;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Animation related tests for the {@link AppWindowToken} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:AppWindowTokenAnimationTests
+ */
+@SmallTest
+public class AppWindowTokenAnimationTests extends WindowTestsBase {
+
+ private TestAppWindowToken mToken;
+
+ @Mock
+ private Transaction mTransaction;
+ @Mock
+ private AnimationAdapter mSpec;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mToken = createTestAppWindowToken(mDisplayContent, WINDOWING_MODE_FULLSCREEN,
+ ACTIVITY_TYPE_STANDARD);
+ mToken.setPendingTransaction(mTransaction);
+ }
+
+ @Test
+ public void clipAfterAnim_boundsLayerIsCreated() {
+ mToken.mNeedsAnimationBoundsLayer = true;
+
+ mToken.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+ verify(mTransaction).reparent(eq(mToken.getSurfaceControl()),
+ eq(mToken.mSurfaceAnimator.mLeash.getHandle()));
+ verify(mTransaction).reparent(eq(mToken.mSurfaceAnimator.mLeash),
+ eq(mToken.mAnimationBoundsLayer.getHandle()));
+ }
+
+ @Test
+ public void clipAfterAnim_boundsLayerIsDestroyed() {
+ mToken.mNeedsAnimationBoundsLayer = true;
+ mToken.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+ final SurfaceControl leash = mToken.mSurfaceAnimator.mLeash;
+ final SurfaceControl animationBoundsLayer = mToken.mAnimationBoundsLayer;
+ final ArgumentCaptor<SurfaceAnimator.OnAnimationFinishedCallback> callbackCaptor =
+ ArgumentCaptor.forClass(
+ SurfaceAnimator.OnAnimationFinishedCallback.class);
+ verify(mSpec).startAnimation(any(), any(), callbackCaptor.capture());
+
+ callbackCaptor.getValue().onAnimationFinished(mSpec);
+ verify(mTransaction).destroy(eq(leash));
+ verify(mTransaction).destroy(eq(animationBoundsLayer));
+ assertThat(mToken.mNeedsAnimationBoundsLayer).isFalse();
+ }
+
+ @Test
+ public void clipAfterAnimCancelled_boundsLayerIsDestroyed() {
+ mToken.mNeedsAnimationBoundsLayer = true;
+ mToken.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+ final SurfaceControl leash = mToken.mSurfaceAnimator.mLeash;
+ final SurfaceControl animationBoundsLayer = mToken.mAnimationBoundsLayer;
+
+ mToken.mSurfaceAnimator.cancelAnimation();
+ verify(mTransaction).destroy(eq(leash));
+ verify(mTransaction).destroy(eq(animationBoundsLayer));
+ assertThat(mToken.mNeedsAnimationBoundsLayer).isFalse();
+ }
+
+ @Test
+ public void clipNoneAnim_boundsLayerIsNotCreated() {
+ mToken.mNeedsAnimationBoundsLayer = false;
+
+ mToken.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+ verify(mTransaction).reparent(eq(mToken.getSurfaceControl()),
+ eq(mToken.mSurfaceAnimator.mLeash.getHandle()));
+ assertThat(mToken.mAnimationBoundsLayer).isNull();
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenTests.java b/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenTests.java
new file mode 100644
index 0000000..8653bf9
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenTests.java
@@ -0,0 +1,431 @@
+/*
+ * 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.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
+import static android.content.ActivityInfoProto.SCREEN_ORIENTATION_UNSPECIFIED;
+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 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+
+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.anyInt;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for the {@link AppWindowToken} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:AppWindowTokenTests
+ */
+@FlakyTest(bugId = 68267650)
+@SmallTest
+@Presubmit
+public class AppWindowTokenTests extends WindowTestsBase {
+
+ TaskStack mStack;
+ Task mTask;
+ WindowTestUtils.TestAppWindowToken mToken;
+
+ private final String mPackageName = getInstrumentation().getTargetContext().getPackageName();
+
+ @Before
+ public void setUp() throws Exception {
+ mStack = createTaskStackOnDisplay(mDisplayContent);
+ mTask = createTaskInStack(mStack, 0 /* userId */);
+ mToken = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+
+ mTask.addChild(mToken, 0);
+ }
+
+ @Test
+ @Presubmit
+ public void testAddWindow_Order() {
+ 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() {
+ 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() {
+ 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() {
+ // Some plumbing to get the service ready for rotation updates.
+ mWm.mDisplayReady = true;
+ mWm.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);
+ mWm.updateOrientationFromAppTokens(mDisplayContent.getOverrideConfiguration(), null,
+ mDisplayContent.getDisplayId());
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, mDisplayContent.getLastOrientation());
+ appWindow.mResizeReported = false;
+
+ // Update the orientation to perform 180 degree rotation and check that resize was reported.
+ mToken.setOrientation(SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
+ mWm.updateOrientationFromAppTokens(mDisplayContent.getOverrideConfiguration(), null,
+ mDisplayContent.getDisplayId());
+ mWm.mRoot.performSurfacePlacement(false /* recoveringMemory */);
+ assertEquals(SCREEN_ORIENTATION_REVERSE_LANDSCAPE, mDisplayContent.getLastOrientation());
+ assertTrue(appWindow.mResizeReported);
+ appWindow.removeImmediately();
+ }
+
+ @Test
+ public void testLandscapeSeascapeRotationByPolicy() {
+ // Some plumbing to get the service ready for rotation updates.
+ mWm.mDisplayReady = true;
+ mWm.mDisplayEnabled = true;
+
+ final DisplayRotation spiedRotation = spy(mDisplayContent.getDisplayRotation());
+ mDisplayContent.setDisplayRotation(spiedRotation);
+
+ 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(spiedRotation, Surface.ROTATION_90);
+ appWindow.mResizeReported = false;
+
+ // Update the rotation to perform 180 degree rotation and check that resize was reported.
+ performRotation(spiedRotation, Surface.ROTATION_270);
+ assertTrue(appWindow.mResizeReported);
+
+ appWindow.removeImmediately();
+ }
+
+ private void performRotation(DisplayRotation spiedRotation, int rotationToReport) {
+ doReturn(rotationToReport).when(spiedRotation).rotationForOrientation(anyInt(), anyInt());
+ int oldRotation = mDisplayContent.getRotation();
+ mWm.updateRotation(false, false);
+ // Must manually apply here since ATM doesn't know about the display during this test
+ // (meaning it can't perform the normal sendNewConfiguration flow).
+ mDisplayContent.applyRotationLocked(oldRotation, mDisplayContent.getRotation());
+ // Prevent the next rotation from being deferred by animation.
+ mWm.mAnimator.setScreenRotationAnimationLocked(mDisplayContent.getDisplayId(), null);
+ mWm.mRoot.performSurfacePlacement(false /* recoveringMemory */);
+ }
+
+ @Test
+ @Presubmit
+ public void testGetOrientation() {
+ 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() {
+ 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() {
+ final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
+ "closingWindow");
+ closingWindow.mAnimatingExit = true;
+ closingWindow.mRemoveOnExit = true;
+ closingWindow.mAppToken.commitVisibility(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);
+ }
+
+ @Test
+ public void testSetOrientation() {
+ // Assert orientation is unspecified to start.
+ assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, mToken.getOrientation());
+
+ mToken.setOrientation(SCREEN_ORIENTATION_LANDSCAPE);
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, mToken.getOrientation());
+
+ mDisplayContent.removeAppToken(mToken.token);
+ // Assert orientation is unset to after container is removed.
+ assertEquals(SCREEN_ORIENTATION_UNSET, mToken.getOrientation());
+
+ // Reset display frozen state
+ mWm.mDisplayFrozen = false;
+ }
+
+ @Test
+ public void testCreateRemoveStartingWindow() {
+ mToken.addStartingWindow(mPackageName,
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
+ false, false);
+ waitUntilHandlersIdle();
+ assertHasStartingWindow(mToken);
+ mToken.removeStartingWindow();
+ waitUntilHandlersIdle();
+ assertNoStartingWindow(mToken);
+ }
+
+ @Test
+ public void testAddRemoveRace() {
+ // There was once a race condition between adding and removing starting windows
+ for (int i = 0; i < 1000; i++) {
+ final WindowTestUtils.TestAppWindowToken appToken = createIsolatedTestAppWindowToken();
+
+ appToken.addStartingWindow(mPackageName,
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
+ false, false);
+ appToken.removeStartingWindow();
+ waitUntilHandlersIdle();
+ assertNoStartingWindow(appToken);
+
+ appToken.getParent().getParent().removeImmediately();
+ }
+ }
+
+ @Test
+ public void testTransferStartingWindow() {
+ final WindowTestUtils.TestAppWindowToken token1 = createIsolatedTestAppWindowToken();
+ final WindowTestUtils.TestAppWindowToken token2 = createIsolatedTestAppWindowToken();
+ token1.addStartingWindow(mPackageName,
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
+ false, false);
+ waitUntilHandlersIdle();
+ token2.addStartingWindow(mPackageName,
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0, token1.appToken.asBinder(),
+ true, true, false, true, false, false);
+ waitUntilHandlersIdle();
+ assertNoStartingWindow(token1);
+ assertHasStartingWindow(token2);
+ }
+
+ @Test
+ public void testTransferStartingWindowWhileCreating() {
+ final WindowTestUtils.TestAppWindowToken token1 = createIsolatedTestAppWindowToken();
+ final WindowTestUtils.TestAppWindowToken token2 = createIsolatedTestAppWindowToken();
+ ((TestWindowManagerPolicy) token1.mService.mPolicy).setRunnableWhenAddingSplashScreen(
+ () -> {
+ // Surprise, ...! Transfer window in the middle of the creation flow.
+ token2.addStartingWindow(mPackageName,
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0,
+ token1.appToken.asBinder(), true, true, false,
+ true, false, false);
+ });
+ token1.addStartingWindow(mPackageName,
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
+ false, false);
+ waitUntilHandlersIdle();
+ assertNoStartingWindow(token1);
+ assertHasStartingWindow(token2);
+ }
+
+ private WindowTestUtils.TestAppWindowToken createIsolatedTestAppWindowToken() {
+ final TaskStack taskStack = createTaskStackOnDisplay(mDisplayContent);
+ final Task task = createTaskInStack(taskStack, 0 /* userId */);
+ return createTestAppWindowTokenForGivenTask(task);
+ }
+
+ private WindowTestUtils.TestAppWindowToken createTestAppWindowTokenForGivenTask(Task task) {
+ final WindowTestUtils.TestAppWindowToken appToken =
+ WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ task.addChild(appToken, 0);
+ waitUntilHandlersIdle();
+ return appToken;
+ }
+
+ @Test
+ public void testTryTransferStartingWindowFromHiddenAboveToken() {
+ // Add two tasks on top of each other.
+ final WindowTestUtils.TestAppWindowToken tokenTop = createIsolatedTestAppWindowToken();
+ final WindowTestUtils.TestAppWindowToken tokenBottom =
+ createTestAppWindowTokenForGivenTask(tokenTop.getTask());
+
+ // Add a starting window.
+ tokenTop.addStartingWindow(mPackageName,
+ 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 transferring the starting window from the top to the
+ // bottom one.
+ tokenTop.setVisibility(false, false);
+ tokenBottom.transferStartingWindowFromHiddenAboveTokenIfNeeded();
+
+ // Assert that the bottom window now has the starting window.
+ assertNoStartingWindow(tokenTop);
+ assertHasStartingWindow(tokenBottom);
+ }
+
+ @Test
+ public void testTransitionAnimationPositionAndBounds() {
+ final Rect stackBounds = new Rect(
+ 0/* left */, 0 /* top */, 1000 /* right */, 1000 /* bottom */);
+ final Rect taskBounds = new Rect(
+ 100/* left */, 200 /* top */, 600 /* right */, 600 /* bottom */);
+ mStack.setBounds(stackBounds);
+ mTask.setBounds(taskBounds);
+
+ mTask.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ assertTransitionAnimationPositionAndBounds(taskBounds.left, taskBounds.top, stackBounds);
+
+ mTask.setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
+ assertTransitionAnimationPositionAndBounds(stackBounds.left, stackBounds.top, stackBounds);
+ }
+
+ private void assertTransitionAnimationPositionAndBounds(int expectedX, int expectedY,
+ Rect expectedBounds) {
+ final Point outPosition = new Point();
+ final Rect outBounds = new Rect();
+ mToken.getAnimationBounds(outPosition, outBounds);
+ assertEquals(expectedX, outPosition.x);
+ assertEquals(expectedY, outPosition.y);
+ assertEquals(expectedBounds, outBounds);
+ }
+
+ 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);
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/BoundsAnimationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BoundsAnimationControllerTests.java
new file mode 100644
index 0000000..1c5391e
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/BoundsAnimationControllerTests.java
@@ -0,0 +1,588 @@
+/*
+ * 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 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+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 androidx.test.annotation.UiThreadTest;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.BoundsAnimationController.BoundsAnimator;
+import com.android.server.wm.WindowManagerInternal.AppTransitionListener;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * 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:
+ * atest FrameworksServicesTests:BoundsAnimationControllerTests
+ */
+@SmallTest
+@Presubmit
+public class BoundsAnimationControllerTests extends WindowTestsBase {
+
+ /**
+ * Mock value animator to simulate updates with.
+ */
+ private static class MockValueAnimator extends ValueAnimator {
+
+ private float mFraction;
+
+ 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 static class MockAppTransition extends AppTransition {
+
+ private AppTransitionListener mListener;
+
+ MockAppTransition(Context context, WindowManagerService wm, DisplayContent displayContent) {
+ super(context, wm, displayContent);
+ }
+
+ @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 static 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 boolean onAnimationStart(boolean schedulePipModeChangedCallback,
+ boolean forceUpdate) {
+ mAwaitingAnimationStart = false;
+ mAnimationStarted = true;
+ mSchedulePipModeChangedOnStart = schedulePipModeChangedCallback;
+ mForcePipModeChangedCallback = forceUpdate;
+ return true;
+ }
+
+ @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 static class BoundsAnimationDriver {
+
+ private final BoundsAnimationController mController;
+ private final TestBoundsAnimationTarget mTarget;
+ private final MockValueAnimator mMockAnimator;
+
+ private BoundsAnimator mAnimator;
+ private Rect mFrom;
+ private Rect mTo;
+ private Rect mLargerBounds;
+ private Rect mExpectedFinalBounds;
+
+ BoundsAnimationDriver(BoundsAnimationController controller,
+ TestBoundsAnimationTarget target, MockValueAnimator mockValueAnimator) {
+ mController = controller;
+ mTarget = target;
+ mMockAnimator = mockValueAnimator;
+ }
+
+ 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);
+ assertFalse(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
+ assertFalse(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
+ assertFalse(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)) {
+ assertFalse(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
+ assertFalse(mTarget.mCancelRequested);
+
+ // Stack/task bounds not updated
+ assertFalse(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 static Rect getLargerBounds(Rect r1, Rect r2) {
+ int r1Area = r1.width() * r1.height();
+ int r2Area = r2.width() * r2.height();
+ return (r1Area <= r2Area) ? r2 : 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 TestBoundsAnimationTarget mTarget;
+ private BoundsAnimationController mController;
+ private BoundsAnimationDriver mDriver;
+
+ // Temp
+ private static final Rect sTmpRect = new Rect();
+
+ @Before
+ public void setUp() throws Exception {
+ final Context context = getInstrumentation().getTargetContext();
+ final Handler handler = new Handler(Looper.getMainLooper());
+ mMockAppTransition = new MockAppTransition(context, mWm, mDisplayContent);
+ mTarget = new TestBoundsAnimationTarget();
+ mController = new BoundsAnimationController(context, mMockAppTransition, handler, null);
+ final MockValueAnimator mockValueAnimator = new MockValueAnimator();
+ mDriver = new BoundsAnimationDriver(mController, mTarget, mockValueAnimator);
+ }
+
+ /** BASE TRANSITIONS **/
+
+ @UiThreadTest
+ @Test
+ public void testFullscreenToFloatingTransition() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+ // 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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 static boolean assertEqualSizeAtOffset(Rect stackBounds, Rect taskBounds) {
+ sTmpRect.set(taskBounds);
+ sTmpRect.offsetTo(stackBounds.left, stackBounds.top);
+ return stackBounds.equals(sTmpRect);
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
new file mode 100644
index 0000000..ee1c8df
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
@@ -0,0 +1,278 @@
+/*
+ * 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.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.reset;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+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 android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:DimmerTests
+ */
+@Presubmit
+public class DimmerTests extends WindowTestsBase {
+
+ private static class TestWindowContainer extends WindowContainer<TestWindowContainer> {
+ final SurfaceControl mControl = mock(SurfaceControl.class);
+ final SurfaceControl.Transaction mTransaction = mock(SurfaceControl.Transaction.class);
+
+ TestWindowContainer(WindowManagerService wm) {
+ super(wm);
+ }
+
+ @Override
+ public SurfaceControl getSurfaceControl() {
+ return mControl;
+ }
+
+ @Override
+ public SurfaceControl.Transaction getPendingTransaction() {
+ return mTransaction;
+ }
+ }
+
+ private static 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(WindowManagerService wm) {
+ super(wm);
+ }
+
+ 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 {
+ mHost = new MockSurfaceBuildingContainer(mWm);
+ mSurfaceAnimatorStarter = spy(new SurfaceAnimatorStarterImpl());
+ mTransaction = mock(SurfaceControl.Transaction.class);
+ mDimmer = new Dimmer(mHost, mSurfaceAnimatorStarter);
+ }
+
+ @Test
+ public void testDimAboveNoChildCreatesSurface() {
+ 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() {
+ 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 testUpdateDimsAppliesCrop() {
+ 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).setWindowCrop(getDimLayer(), width, height);
+ verify(mTransaction).show(getDimLayer());
+ }
+
+ @Test
+ public void testDimAboveNoChildNotReset() {
+ 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() {
+ TestWindowContainer child = new TestWindowContainer(mWm);
+ 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() {
+ TestWindowContainer child = new TestWindowContainer(mWm);
+ 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() {
+ TestWindowContainer child = new TestWindowContainer(mWm);
+ 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(mHost.getPendingTransaction()).destroy(dimLayer);
+ }
+
+ @Test
+ public void testDimBelowWithChildSurfaceNotDestroyedWhenPersisted() {
+ TestWindowContainer child = new TestWindowContainer(mWm);
+ 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() {
+ Rect bounds = new Rect();
+ TestWindowContainer child = new TestWindowContainer(mWm);
+ 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).setWindowCrop(dimLayer, bounds.width(), bounds.height());
+ verify(mTransaction, times(1)).show(dimLayer);
+ verify(mTransaction).setPosition(dimLayer, 0, 0);
+
+ bounds.set(10, 10, 30, 30);
+ mDimmer.updateDims(mTransaction, bounds);
+ verify(mTransaction).setWindowCrop(dimLayer, bounds.width(), bounds.height());
+ verify(mTransaction).setPosition(dimLayer, 10, 10);
+ }
+
+ @Test
+ public void testRemoveDimImmediately() {
+ TestWindowContainer child = new TestWindowContainer(mWm);
+ 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/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
new file mode 100644
index 0000000..3b8d71d
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -0,0 +1,623 @@
+/*
+ * 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.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.BOUNDS_POSITION_LEFT;
+import static android.view.DisplayCutout.BOUNDS_POSITION_TOP;
+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.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.server.wm.WindowContainer.POSITION_TOP;
+import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
+
+import static org.hamcrest.Matchers.is;
+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.assertThat;
+import static org.junit.Assert.assertTrue;
+
+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.util.DisplayMetrics;
+import android.view.DisplayCutout;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.Surface;
+
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.utils.WmDisplayCutout;
+
+import org.junit.Test;
+
+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 FrameworksServicesTests:DisplayContentTests
+ */
+@SmallTest
+@Presubmit
+public class DisplayContentTests extends WindowTestsBase {
+
+ @Test
+ @FlakyTest(bugId = 77772044)
+ public void testForAllWindows() {
+ 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() {
+ final WindowState imeAppTarget =
+ createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, "imeAppTarget");
+
+ mDisplayContent.mInputMethodTarget = imeAppTarget;
+
+ assertForAllWindowsOrder(Arrays.asList(
+ mWallpaperWindow,
+ mChildAppWindowBelow,
+ mAppWindow,
+ mChildAppWindowAbove,
+ imeAppTarget,
+ mImeWindow,
+ mImeDialogWindow,
+ mDockedDividerWindow,
+ mStatusBarWindow,
+ mNavBarWindow));
+ }
+
+ @Test
+ public void testForAllWindows_WithChildWindowImeTarget() throws Exception {
+ mDisplayContent.mInputMethodTarget = mChildAppWindowAbove;
+
+ assertForAllWindowsOrder(Arrays.asList(
+ mWallpaperWindow,
+ mChildAppWindowBelow,
+ mAppWindow,
+ mChildAppWindowAbove,
+ mImeWindow,
+ mImeDialogWindow,
+ mDockedDividerWindow,
+ mStatusBarWindow,
+ mNavBarWindow));
+ }
+
+ @Test
+ public void testForAllWindows_WithStatusBarImeTarget() throws Exception {
+ mDisplayContent.mInputMethodTarget = mStatusBarWindow;
+
+ assertForAllWindowsOrder(Arrays.asList(
+ mWallpaperWindow,
+ mChildAppWindowBelow,
+ mAppWindow,
+ mChildAppWindowAbove,
+ mDockedDividerWindow,
+ mStatusBarWindow,
+ mImeWindow,
+ mImeDialogWindow,
+ mNavBarWindow));
+ }
+
+ @Test
+ public void testForAllWindows_WithInBetweenWindowToken() {
+ // 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() {
+ // 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() {
+ // 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() {
+ 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;
+
+ mWm.setNewDisplayOverrideConfiguration(newOverrideConfig, mDisplayContent);
+
+ // 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() {
+ DisplayContent defaultDisplay = mWm.mRoot.getDisplayContent(DEFAULT_DISPLAY);
+ final Configuration currentConfig = defaultDisplay.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;
+
+ mWm.setNewDisplayOverrideConfiguration(newOverrideConfig, defaultDisplay);
+
+ // Check that global configuration is updated, as we've updated default display's config.
+ Configuration globalConfig = mWm.mRoot.getConfiguration();
+ assertEquals(newOverrideConfig.densityDpi, globalConfig.densityDpi);
+ assertEquals(newOverrideConfig.fontScale, globalConfig.fontScale, 0.1 /* delta */);
+
+ // Return back to original values.
+ mWm.setNewDisplayOverrideConfiguration(currentConfig, defaultDisplay);
+ globalConfig = mWm.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() {
+ DisplayContent dc0 = mWm.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.configureDisplayPolicy();
+ assertNotNull(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.configureDisplayPolicy();
+ assertNotNull(dc1.mTapDetector);
+
+ // tap on primary display.
+ tapOnDisplay(dc0);
+ // Check focus is on primary display.
+ assertEquals(mWm.mRoot.getTopFocusedDisplayContent().mCurrentFocus,
+ dc0.findFocusedWindow());
+
+ // Tap on secondary display.
+ tapOnDisplay(dc1);
+ // Check focus is on secondary.
+ assertEquals(mWm.mRoot.getTopFocusedDisplayContent().mCurrentFocus,
+ dc1.findFocusedWindow());
+ }
+
+ @Test
+ public void testFocusedWindowMultipleDisplays() {
+ // Create a focusable window and check that focus is calculated correctly
+ final WindowState window1 =
+ createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, "window1");
+ updateFocusedWindow();
+ assertTrue(window1.isFocused());
+ assertEquals(window1, mWm.mRoot.getTopFocusedDisplayContent().mCurrentFocus);
+
+ // Check that a new display doesn't affect focus
+ final DisplayContent dc = createNewDisplay();
+ updateFocusedWindow();
+ assertTrue(window1.isFocused());
+ assertEquals(window1, mWm.mRoot.getTopFocusedDisplayContent().mCurrentFocus);
+
+ // Add a window to the second display, and it should be focused
+ final WindowState window2 = createWindow(null, TYPE_BASE_APPLICATION, dc, "window2");
+ updateFocusedWindow();
+ assertTrue(window1.isFocused());
+ assertTrue(window2.isFocused());
+ assertEquals(window2, mWm.mRoot.getTopFocusedDisplayContent().mCurrentFocus);
+
+ // Move the first window to the to including parents, and make sure focus is updated
+ window1.getParent().positionChildAt(POSITION_TOP, window1, true);
+ updateFocusedWindow();
+ assertTrue(window1.isFocused());
+ assertTrue(window2.isFocused());
+ assertEquals(window1, mWm.mRoot.getTopFocusedDisplayContent().mCurrentFocus);
+ }
+
+ /**
+ * This tests setting the maximum ui width on a display.
+ */
+ @Test
+ public void testMaxUiWidth() {
+ // Prevent base display metrics for test from being updated to the value of real display.
+ final DisplayContent displayContent = createDisplayNoUpdateDisplayInfo();
+ final int baseWidth = 1440;
+ final int baseHeight = 2560;
+ final int baseDensity = 300;
+
+ displayContent.updateBaseDisplayMetrics(baseWidth, baseHeight, baseDensity);
+
+ final int maxWidth = 300;
+ final int resultingHeight = (maxWidth * baseHeight) / baseWidth;
+ final int resultingDensity = (maxWidth * baseDensity) / baseWidth;
+
+ displayContent.setMaxUiWidth(maxWidth);
+ verifySizes(displayContent, maxWidth, resultingHeight, resultingDensity);
+
+ // Assert setting values again does not change;
+ displayContent.updateBaseDisplayMetrics(baseWidth, baseHeight, baseDensity);
+ verifySizes(displayContent, maxWidth, resultingHeight, resultingDensity);
+
+ final int smallerWidth = 200;
+ final int smallerHeight = 400;
+ final int smallerDensity = 100;
+
+ // Specify smaller dimension, verify that it is honored
+ displayContent.updateBaseDisplayMetrics(smallerWidth, smallerHeight, smallerDensity);
+ verifySizes(displayContent, smallerWidth, smallerHeight, smallerDensity);
+
+ // Verify that setting the max width to a greater value than the base width has no effect
+ displayContent.setMaxUiWidth(maxWidth);
+ verifySizes(displayContent, smallerWidth, smallerHeight, smallerDensity);
+ }
+
+ @Test
+ public void testDisplayCutout_rot0() {
+ synchronized (mWm.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, BOUNDS_POSITION_TOP), 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() {
+ synchronized (mWm.getWindowManagerLock()) {
+ // Prevent mInitialDisplayCutout from being updated from real display (e.g. null
+ // if the device has no cutout).
+ final DisplayContent dc = createDisplayNoUpdateDisplayInfo();
+ // Rotation may use real display info to compute bound, so here also uses the
+ // same width and height.
+ final int displayWidth = dc.mInitialDisplayWidth;
+ final int displayHeight = dc.mInitialDisplayHeight;
+ final int cutoutWidth = 40;
+ final int cutoutHeight = 10;
+ final int left = (displayWidth - cutoutWidth) / 2;
+ final int top = 0;
+ final int right = (displayWidth + cutoutWidth) / 2;
+ final int bottom = cutoutHeight;
+
+ final Rect r1 = new Rect(left, top, right, bottom);
+ final DisplayCutout cutout = new WmDisplayCutout(
+ fromBoundingRect(r1.left, r1.top, r1.right, r1.bottom, BOUNDS_POSITION_TOP),
+ null)
+ .computeSafeInsets(displayWidth, displayHeight).getDisplayCutout();
+
+ dc.mInitialDisplayCutout = cutout;
+ dc.setRotation(Surface.ROTATION_90);
+ dc.computeScreenConfiguration(new Configuration()); // recomputes dc.mDisplayInfo.
+
+ // ----o---------- -------------
+ // | | | | |
+ // | ------o | o---
+ // | | | |
+ // | | -> | |
+ // | | ---o
+ // | | |
+ // | | -------------
+ final Rect r = new Rect(top, left, bottom, right);
+ assertEquals(new WmDisplayCutout(
+ fromBoundingRect(r.left, r.top, r.right, r.bottom, BOUNDS_POSITION_LEFT), null)
+ .computeSafeInsets(displayHeight, displayWidth)
+ .getDisplayCutout(), dc.getDisplayInfo().displayCutout);
+ }
+ }
+
+ @Test
+ public void testLayoutSeq_assignedDuringLayout() {
+ synchronized (mWm.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());
+
+ mWm.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);
+ mWm.dontOverrideDisplayInfo(dc.getDisplayId());
+
+ assertFalse(dc.mShouldOverrideDisplayConfiguration);
+ verify(mWm.mDisplayManagerInternal, times(1))
+ .setDisplayInfoOverrideFromWindowManager(dc.getDisplayId(), null);
+ }
+
+ @Test
+ public void testClearLastFocusWhenReparentingFocusedWindow() {
+ final DisplayContent defaultDisplay = mWm.getDefaultDisplayContentLocked();
+ final WindowState window = createWindow(null /* parent */, TYPE_BASE_APPLICATION,
+ defaultDisplay, "window");
+ defaultDisplay.mLastFocus = window;
+ mDisplayContent.mCurrentFocus = window;
+ mDisplayContent.reParentWindowToken(window.mToken);
+
+ assertNull(defaultDisplay.mLastFocus);
+ }
+
+ @Test
+ public void testGetPreferredOptionsPanelGravityFromDifferentDisplays() {
+ final DisplayContent portraitDisplay = createNewDisplay();
+ portraitDisplay.mInitialDisplayHeight = 2000;
+ portraitDisplay.mInitialDisplayWidth = 1000;
+
+ portraitDisplay.setRotation(Surface.ROTATION_0);
+ assertFalse(isOptionsPanelAtRight(portraitDisplay.getDisplayId()));
+ portraitDisplay.setRotation(Surface.ROTATION_90);
+ assertTrue(isOptionsPanelAtRight(portraitDisplay.getDisplayId()));
+
+ final DisplayContent landscapeDisplay = createNewDisplay();
+ landscapeDisplay.mInitialDisplayHeight = 1000;
+ landscapeDisplay.mInitialDisplayWidth = 2000;
+
+ landscapeDisplay.setRotation(Surface.ROTATION_0);
+ assertTrue(isOptionsPanelAtRight(landscapeDisplay.getDisplayId()));
+ landscapeDisplay.setRotation(Surface.ROTATION_90);
+ assertFalse(isOptionsPanelAtRight(landscapeDisplay.getDisplayId()));
+ }
+
+ @Test
+ public void testInputMethodTargetUpdateWhenSwitchingOnDisplays() {
+ final DisplayContent newDisplay = createNewDisplay();
+
+ final WindowState appWin = createWindow(null, TYPE_APPLICATION, mDisplayContent, "appWin");
+ final WindowState appWin1 = createWindow(null, TYPE_APPLICATION, newDisplay, "appWin1");
+ appWin.setHasSurface(true);
+ appWin1.setHasSurface(true);
+
+ // Set current input method window on default display, make sure the input method target
+ // is appWin & null on the other display.
+ mDisplayContent.setInputMethodWindowLocked(mImeWindow);
+ newDisplay.setInputMethodWindowLocked(null);
+ assertTrue("appWin should be IME target window",
+ appWin.equals(mDisplayContent.mInputMethodTarget));
+ assertNull("newDisplay Ime target: ", newDisplay.mInputMethodTarget);
+
+ // Switch input method window on new display & make sure the input method target also
+ // switched as expected.
+ newDisplay.setInputMethodWindowLocked(mImeWindow);
+ mDisplayContent.setInputMethodWindowLocked(null);
+ assertTrue("appWin1 should be IME target window",
+ appWin1.equals(newDisplay.mInputMethodTarget));
+ assertNull("default display Ime target: ", mDisplayContent.mInputMethodTarget);
+ }
+
+ private boolean isOptionsPanelAtRight(int displayId) {
+ return (mWm.getPreferredOptionsPanelGravity(displayId) & Gravity.RIGHT) == Gravity.RIGHT;
+ }
+
+ 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 updateFocusedWindow() {
+ synchronized (mWm.mGlobalLock) {
+ mWm.updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL, false);
+ }
+ }
+
+ /**
+ * Create DisplayContent that does not update display base/initial values from device to keep
+ * the values set by test.
+ */
+ private DisplayContent createDisplayNoUpdateDisplayInfo() {
+ final DisplayContent displayContent = spy(createNewDisplay());
+ doNothing().when(displayContent).updateDisplayInfo();
+ return displayContent;
+ }
+
+ 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 void tapOnDisplay(final DisplayContent dc) {
+ final DisplayMetrics dm = dc.getDisplayMetrics();
+ final float x = dm.widthPixels / 2;
+ final float y = dm.heightPixels / 2;
+ final long downTime = SystemClock.uptimeMillis();
+ final long eventTime = SystemClock.uptimeMillis() + 100;
+ // sending ACTION_DOWN
+ final MotionEvent downEvent = MotionEvent.obtain(
+ downTime,
+ downTime,
+ MotionEvent.ACTION_DOWN,
+ x,
+ y,
+ 0 /*metaState*/);
+ downEvent.setDisplayId(dc.getDisplayId());
+ dc.mTapDetector.onPointerEvent(downEvent);
+
+ // sending ACTION_UP
+ final MotionEvent upEvent = MotionEvent.obtain(
+ downTime,
+ eventTime,
+ MotionEvent.ACTION_UP,
+ x,
+ y,
+ 0 /*metaState*/);
+ upEvent.setDisplayId(dc.getDisplayId());
+ dc.mTapDetector.onPointerEvent(upEvent);
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyInsetsTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyInsetsTests.java
new file mode 100644
index 0000000..6767465
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyInsetsTests.java
@@ -0,0 +1,177 @@
+/*
+ * 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.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 org.hamcrest.Matchers.equalTo;
+
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.view.DisplayInfo;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ErrorCollector;
+
+@SmallTest
+@Presubmit
+public class DisplayPolicyInsetsTests extends DisplayPolicyTestsBase {
+
+ @Rule
+ public final ErrorCollector mErrorCollector = new ErrorCollector();
+
+ @Test
+ public void portrait() {
+ final DisplayInfo di = displayInfoForRotation(ROTATION_0, false /* withCutout */);
+
+ verifyStableInsets(di, 0, STATUS_BAR_HEIGHT, 0, NAV_BAR_HEIGHT);
+ verifyNonDecorInsets(di, 0, 0, 0, NAV_BAR_HEIGHT);
+ verifyConsistency(di);
+ }
+
+ @Test
+ public void portrait_withCutout() {
+ final DisplayInfo di = displayInfoForRotation(ROTATION_0, true /* withCutout */);
+
+ verifyStableInsets(di, 0, STATUS_BAR_HEIGHT, 0, NAV_BAR_HEIGHT);
+ verifyNonDecorInsets(di, 0, DISPLAY_CUTOUT_HEIGHT, 0, NAV_BAR_HEIGHT);
+ verifyConsistency(di);
+ }
+
+ @Test
+ public void landscape() {
+ final DisplayInfo di = displayInfoForRotation(ROTATION_90, false /* withCutout */);
+
+ verifyStableInsets(di, 0, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT, 0);
+ verifyNonDecorInsets(di, 0, 0, NAV_BAR_HEIGHT, 0);
+ verifyConsistency(di);
+ }
+
+ @Test
+ public void landscape_withCutout() {
+ final DisplayInfo di = displayInfoForRotation(ROTATION_90, true /* withCutout */);
+
+ verifyStableInsets(di, DISPLAY_CUTOUT_HEIGHT, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT, 0);
+ verifyNonDecorInsets(di, DISPLAY_CUTOUT_HEIGHT, 0, NAV_BAR_HEIGHT, 0);
+ verifyConsistency(di);
+ }
+
+ @Test
+ public void seascape() {
+ final DisplayInfo di = displayInfoForRotation(ROTATION_270, false /* withCutout */);
+
+ verifyStableInsets(di, NAV_BAR_HEIGHT, STATUS_BAR_HEIGHT, 0, 0);
+ verifyNonDecorInsets(di, NAV_BAR_HEIGHT, 0, 0, 0);
+ verifyConsistency(di);
+ }
+
+ @Test
+ public void seascape_withCutout() {
+ final DisplayInfo di = displayInfoForRotation(ROTATION_270, true /* withCutout */);
+
+ verifyStableInsets(di, NAV_BAR_HEIGHT, STATUS_BAR_HEIGHT, DISPLAY_CUTOUT_HEIGHT, 0);
+ verifyNonDecorInsets(di, NAV_BAR_HEIGHT, 0, DISPLAY_CUTOUT_HEIGHT, 0);
+ verifyConsistency(di);
+ }
+
+ @Test
+ public void upsideDown() {
+ final DisplayInfo di = displayInfoForRotation(ROTATION_180, false /* withCutout */);
+
+ verifyStableInsets(di, 0, STATUS_BAR_HEIGHT, 0, NAV_BAR_HEIGHT);
+ verifyNonDecorInsets(di, 0, 0, 0, NAV_BAR_HEIGHT);
+ verifyConsistency(di);
+ }
+
+ @Test
+ public void upsideDown_withCutout() {
+ final DisplayInfo di = displayInfoForRotation(ROTATION_180, true /* withCutout */);
+
+ verifyStableInsets(di, 0, STATUS_BAR_HEIGHT, 0, NAV_BAR_HEIGHT + DISPLAY_CUTOUT_HEIGHT);
+ verifyNonDecorInsets(di, 0, 0, 0, NAV_BAR_HEIGHT + DISPLAY_CUTOUT_HEIGHT);
+ verifyConsistency(di);
+ }
+
+ private void verifyStableInsets(DisplayInfo di, int left, int top, int right, int bottom) {
+ mErrorCollector.checkThat("stableInsets", getStableInsetsLw(di), equalTo(new Rect(
+ left, top, right, bottom)));
+ }
+
+ private void verifyNonDecorInsets(DisplayInfo di, int left, int top, int right, int bottom) {
+ mErrorCollector.checkThat("nonDecorInsets", getNonDecorInsetsLw(di), equalTo(new Rect(
+ left, top, right, bottom)));
+ }
+
+ private void verifyConsistency(DisplayInfo di) {
+ verifyConsistency("configDisplay", di, getStableInsetsLw(di),
+ getConfigDisplayWidth(di), getConfigDisplayHeight(di));
+ verifyConsistency("nonDecorDisplay", di, getNonDecorInsetsLw(di),
+ getNonDecorDisplayWidth(di), getNonDecorDisplayHeight(di));
+ }
+
+ private void verifyConsistency(String what, DisplayInfo di, Rect insets, int width,
+ int height) {
+ mErrorCollector.checkThat(what + ":width", width,
+ equalTo(di.logicalWidth - insets.left - insets.right));
+ mErrorCollector.checkThat(what + ":height", height,
+ equalTo(di.logicalHeight - insets.top - insets.bottom));
+ }
+
+ private Rect getStableInsetsLw(DisplayInfo di) {
+ Rect result = new Rect();
+ mDisplayPolicy.getStableInsetsLw(di.rotation, di.logicalWidth, di.logicalHeight,
+ di.displayCutout, result);
+ return result;
+ }
+
+ private Rect getNonDecorInsetsLw(DisplayInfo di) {
+ Rect result = new Rect();
+ mDisplayPolicy.getNonDecorInsetsLw(di.rotation, di.logicalWidth, di.logicalHeight,
+ di.displayCutout, result);
+ return result;
+ }
+
+ private int getNonDecorDisplayWidth(DisplayInfo di) {
+ return mDisplayPolicy.getNonDecorDisplayWidth(di.logicalWidth, di.logicalHeight,
+ di.rotation, 0 /* ui */, di.displayCutout);
+ }
+
+ private int getNonDecorDisplayHeight(DisplayInfo di) {
+ return mDisplayPolicy.getNonDecorDisplayHeight(di.logicalWidth, di.logicalHeight,
+ di.rotation, 0 /* ui */, di.displayCutout);
+ }
+
+ private int getConfigDisplayWidth(DisplayInfo di) {
+ return mDisplayPolicy.getConfigDisplayWidth(di.logicalWidth, di.logicalHeight,
+ di.rotation, 0 /* ui */, di.displayCutout);
+ }
+
+ private int getConfigDisplayHeight(DisplayInfo di) {
+ return mDisplayPolicy.getConfigDisplayHeight(di.logicalWidth, di.logicalHeight,
+ di.rotation, 0 /* ui */, di.displayCutout);
+ }
+
+ private static DisplayInfo displayInfoForRotation(int rotation, boolean withDisplayCutout) {
+ return displayInfoAndCutoutForRotation(rotation, withDisplayCutout).first;
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyLayoutTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyLayoutTests.java
new file mode 100644
index 0000000..b94f472
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyLayoutTests.java
@@ -0,0 +1,419 @@
+/*
+ * 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.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
+import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
+import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.spy;
+
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.util.Pair;
+import android.view.DisplayCutout;
+import android.view.DisplayInfo;
+import android.view.WindowManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.utils.WmDisplayCutout;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@SmallTest
+@Presubmit
+public class DisplayPolicyLayoutTests extends DisplayPolicyTestsBase {
+
+ private DisplayFrames mFrames;
+ private WindowState mWindow;
+ private int mRotation = ROTATION_0;
+ private boolean mHasDisplayCutout;
+
+ @Before
+ public void setUp() throws Exception {
+ updateDisplayFrames();
+
+ mWindow = spy(createWindow(null, TYPE_APPLICATION, "window"));
+ // We only test window frames set by DisplayPolicy, so here prevents computeFrameLw from
+ // changing those frames.
+ doNothing().when(mWindow).computeFrameLw();
+
+ final WindowManager.LayoutParams attrs = mWindow.mAttrs;
+ attrs.width = MATCH_PARENT;
+ attrs.height = MATCH_PARENT;
+ attrs.flags =
+ FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR | FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+ attrs.format = PixelFormat.TRANSLUCENT;
+ }
+
+ public void setRotation(int rotation) {
+ mRotation = rotation;
+ updateDisplayFrames();
+ }
+
+ public void addDisplayCutout() {
+ mHasDisplayCutout = true;
+ updateDisplayFrames();
+ }
+
+ private void updateDisplayFrames() {
+ final Pair<DisplayInfo, WmDisplayCutout> info = displayInfoAndCutoutForRotation(mRotation,
+ mHasDisplayCutout);
+ mFrames = new DisplayFrames(mDisplayContent.getDisplayId(), info.first, info.second);
+ }
+
+ @Test
+ public void layoutWindowLw_appDrawsBars() {
+ mWindow.mAttrs.flags |= FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+ addWindow(mWindow);
+
+ mDisplayPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mDisplayPolicy.layoutWindowLw(mWindow, null, mFrames);
+
+ assertInsetByTopBottom(mWindow.getParentFrame(), 0, 0);
+ assertInsetByTopBottom(mWindow.getStableFrameLw(), STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getContentFrameLw(), STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getDecorFrame(), 0, 0);
+ assertInsetBy(mWindow.getDisplayFrameLw(), 0, 0, 0, 0);
+ }
+
+ @Test
+ public void layoutWindowLw_appWontDrawBars() {
+ mWindow.mAttrs.flags &= ~FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+ addWindow(mWindow);
+
+ mDisplayPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mDisplayPolicy.layoutWindowLw(mWindow, null, mFrames);
+
+ assertInsetByTopBottom(mWindow.getParentFrame(), 0, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getStableFrameLw(), STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getContentFrameLw(), STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getDecorFrame(), STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getDisplayFrameLw(), 0, NAV_BAR_HEIGHT);
+ }
+
+ @Test
+ public void layoutWindowLw_appWontDrawBars_forceStatus() throws Exception {
+ mWindow.mAttrs.flags &= ~FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+ mWindow.mAttrs.privateFlags |= PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND;
+ addWindow(mWindow);
+
+ mDisplayPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mDisplayPolicy.layoutWindowLw(mWindow, null, mFrames);
+
+ assertInsetByTopBottom(mWindow.getParentFrame(), 0, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getStableFrameLw(), STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getContentFrameLw(), STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getDecorFrame(), 0, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getDisplayFrameLw(), 0, NAV_BAR_HEIGHT);
+ }
+
+ @Test
+ public void addingWindow_doesNotTamperWithSysuiFlags() {
+ mWindow.mAttrs.flags |= FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+ addWindow(mWindow);
+
+ assertEquals(0, mWindow.mAttrs.systemUiVisibility);
+ assertEquals(0, mWindow.mAttrs.subtreeSystemUiVisibility);
+ }
+
+ @Test
+ public void layoutWindowLw_withDisplayCutout() {
+ addDisplayCutout();
+
+ addWindow(mWindow);
+
+ mDisplayPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mDisplayPolicy.layoutWindowLw(mWindow, null, mFrames);
+
+ assertInsetByTopBottom(mWindow.getParentFrame(), 0, 0);
+ assertInsetByTopBottom(mWindow.getStableFrameLw(), STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getContentFrameLw(), STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getDecorFrame(), 0, 0);
+ assertInsetByTopBottom(mWindow.getDisplayFrameLw(), 0, 0);
+ }
+
+ @Test
+ public void layoutWindowLw_withDisplayCutout_never() {
+ addDisplayCutout();
+
+ mWindow.mAttrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
+ addWindow(mWindow);
+
+ mDisplayPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mDisplayPolicy.layoutWindowLw(mWindow, null, mFrames);
+
+ assertInsetByTopBottom(mWindow.getParentFrame(), STATUS_BAR_HEIGHT, 0);
+ assertInsetByTopBottom(mWindow.getStableFrameLw(), STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getContentFrameLw(), STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getDecorFrame(), 0, 0);
+ assertInsetByTopBottom(mWindow.getDisplayFrameLw(), STATUS_BAR_HEIGHT, 0);
+ }
+
+ @Test
+ public void layoutWindowLw_withDisplayCutout_layoutFullscreen() {
+ addDisplayCutout();
+
+ mWindow.mAttrs.subtreeSystemUiVisibility |= SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
+ addWindow(mWindow);
+
+ mDisplayPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mDisplayPolicy.layoutWindowLw(mWindow, null, mFrames);
+
+ assertInsetByTopBottom(mWindow.getParentFrame(), 0, 0);
+ assertInsetByTopBottom(mWindow.getStableFrameLw(), STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getContentFrameLw(), STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getDecorFrame(), 0, 0);
+ assertInsetBy(mWindow.getDisplayFrameLw(), 0, 0, 0, 0);
+ }
+
+ @Test
+ public void layoutWindowLw_withDisplayCutout_fullscreen() {
+ addDisplayCutout();
+
+ mWindow.mAttrs.subtreeSystemUiVisibility |= SYSTEM_UI_FLAG_FULLSCREEN;
+ addWindow(mWindow);
+
+ mDisplayPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mDisplayPolicy.layoutWindowLw(mWindow, null, mFrames);
+
+ assertInsetByTopBottom(mWindow.getParentFrame(), STATUS_BAR_HEIGHT, 0);
+ assertInsetByTopBottom(mWindow.getStableFrameLw(), STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getContentFrameLw(), STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getDecorFrame(), 0, 0);
+ assertInsetByTopBottom(mWindow.getDisplayFrameLw(), STATUS_BAR_HEIGHT, 0);
+ }
+
+ @Test
+ public void layoutWindowLw_withDisplayCutout_fullscreenInCutout() {
+ addDisplayCutout();
+
+ mWindow.mAttrs.subtreeSystemUiVisibility |= SYSTEM_UI_FLAG_FULLSCREEN;
+ mWindow.mAttrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+ addWindow(mWindow);
+
+ mDisplayPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mDisplayPolicy.layoutWindowLw(mWindow, null, mFrames);
+
+ assertInsetByTopBottom(mWindow.getParentFrame(), 0, 0);
+ assertInsetByTopBottom(mWindow.getStableFrameLw(), STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getContentFrameLw(), STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getDecorFrame(), 0, 0);
+ assertInsetByTopBottom(mWindow.getDisplayFrameLw(), 0, 0);
+ }
+
+
+ @Test
+ public void layoutWindowLw_withDisplayCutout_landscape() {
+ addDisplayCutout();
+ setRotation(ROTATION_90);
+ addWindow(mWindow);
+
+ mDisplayPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mDisplayPolicy.layoutWindowLw(mWindow, null, mFrames);
+
+ assertInsetBy(mWindow.getParentFrame(), DISPLAY_CUTOUT_HEIGHT, 0, 0, 0);
+ assertInsetBy(mWindow.getStableFrameLw(), 0, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT, 0);
+ assertInsetBy(mWindow.getContentFrameLw(),
+ DISPLAY_CUTOUT_HEIGHT, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT, 0);
+ assertInsetBy(mWindow.getDecorFrame(), 0, 0, 0, 0);
+ assertInsetBy(mWindow.getDisplayFrameLw(), DISPLAY_CUTOUT_HEIGHT, 0, 0, 0);
+ }
+
+ @Test
+ public void layoutWindowLw_withDisplayCutout_seascape() {
+ addDisplayCutout();
+ setRotation(ROTATION_270);
+ addWindow(mWindow);
+
+ mDisplayPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mDisplayPolicy.layoutWindowLw(mWindow, null, mFrames);
+
+ assertInsetBy(mWindow.getParentFrame(), 0, 0, DISPLAY_CUTOUT_HEIGHT, 0);
+ assertInsetBy(mWindow.getStableFrameLw(), NAV_BAR_HEIGHT, STATUS_BAR_HEIGHT, 0, 0);
+ assertInsetBy(mWindow.getContentFrameLw(),
+ NAV_BAR_HEIGHT, STATUS_BAR_HEIGHT, DISPLAY_CUTOUT_HEIGHT, 0);
+ assertInsetBy(mWindow.getDecorFrame(), 0, 0, 0, 0);
+ assertInsetBy(mWindow.getDisplayFrameLw(), 0, 0, DISPLAY_CUTOUT_HEIGHT, 0);
+ }
+
+ @Test
+ public void layoutWindowLw_withDisplayCutout_fullscreen_landscape() {
+ addDisplayCutout();
+ setRotation(ROTATION_90);
+
+ mWindow.mAttrs.subtreeSystemUiVisibility |= SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
+ addWindow(mWindow);
+
+ mDisplayPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mDisplayPolicy.layoutWindowLw(mWindow, null, mFrames);
+
+ assertInsetBy(mWindow.getParentFrame(), DISPLAY_CUTOUT_HEIGHT, 0, 0, 0);
+ assertInsetBy(mWindow.getStableFrameLw(), 0, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT, 0);
+ assertInsetBy(mWindow.getContentFrameLw(),
+ DISPLAY_CUTOUT_HEIGHT, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT, 0);
+ assertInsetBy(mWindow.getDecorFrame(), 0, 0, 0, 0);
+ }
+
+ @Test
+ public void layoutWindowLw_withDisplayCutout_floatingInScreen() {
+ addDisplayCutout();
+
+ mWindow.mAttrs.flags = FLAG_LAYOUT_IN_SCREEN;
+ mWindow.mAttrs.type = TYPE_APPLICATION_OVERLAY;
+ mWindow.mAttrs.width = DISPLAY_WIDTH;
+ mWindow.mAttrs.height = DISPLAY_HEIGHT;
+ addWindow(mWindow);
+
+ mDisplayPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mDisplayPolicy.layoutWindowLw(mWindow, null, mFrames);
+
+ assertInsetByTopBottom(mWindow.getParentFrame(), 0, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mWindow.getDisplayFrameLw(), STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ }
+
+ @Test
+ public void layoutWindowLw_withDisplayCutout_fullscreenInCutout_landscape() {
+ addDisplayCutout();
+ setRotation(ROTATION_90);
+
+ mWindow.mAttrs.subtreeSystemUiVisibility |= SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
+ mWindow.mAttrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+ addWindow(mWindow);
+
+ mDisplayPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mDisplayPolicy.layoutWindowLw(mWindow, null, mFrames);
+
+ assertInsetBy(mWindow.getParentFrame(), 0, 0, 0, 0);
+ assertInsetBy(mWindow.getStableFrameLw(), 0, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT, 0);
+ assertInsetBy(mWindow.getContentFrameLw(),
+ DISPLAY_CUTOUT_HEIGHT, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT, 0);
+ assertInsetBy(mWindow.getDecorFrame(), 0, 0, 0, 0);
+ }
+
+ @Test
+ public void layoutHint_appWindow() {
+ // Initialize DisplayFrames
+ mDisplayPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+
+ final Rect outFrame = new Rect();
+ final Rect outContentInsets = new Rect();
+ final Rect outStableInsets = new Rect();
+ final Rect outOutsets = new Rect();
+ final DisplayCutout.ParcelableWrapper outDisplayCutout =
+ new DisplayCutout.ParcelableWrapper();
+
+ mDisplayPolicy.getLayoutHintLw(mWindow.mAttrs, null, mFrames,
+ false /* floatingStack */, outFrame, outContentInsets, outStableInsets, outOutsets,
+ outDisplayCutout);
+
+ assertThat(outFrame, is(mFrames.mUnrestricted));
+ assertThat(outContentInsets, is(new Rect(0, STATUS_BAR_HEIGHT, 0, NAV_BAR_HEIGHT)));
+ assertThat(outStableInsets, is(new Rect(0, STATUS_BAR_HEIGHT, 0, NAV_BAR_HEIGHT)));
+ assertThat(outOutsets, is(new Rect()));
+ assertThat(outDisplayCutout, is(new DisplayCutout.ParcelableWrapper()));
+ }
+
+ @Test
+ public void layoutHint_appWindowInTask() {
+ // Initialize DisplayFrames
+ mDisplayPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+
+ final Rect taskBounds = new Rect(100, 100, 200, 200);
+
+ final Rect outFrame = new Rect();
+ final Rect outContentInsets = new Rect();
+ final Rect outStableInsets = new Rect();
+ final Rect outOutsets = new Rect();
+ final DisplayCutout.ParcelableWrapper outDisplayCutout =
+ new DisplayCutout.ParcelableWrapper();
+
+ mDisplayPolicy.getLayoutHintLw(mWindow.mAttrs, taskBounds, mFrames,
+ false /* floatingStack */, outFrame, outContentInsets, outStableInsets, outOutsets,
+ outDisplayCutout);
+
+ assertThat(outFrame, is(taskBounds));
+ assertThat(outContentInsets, is(new Rect()));
+ assertThat(outStableInsets, is(new Rect()));
+ assertThat(outOutsets, is(new Rect()));
+ assertThat(outDisplayCutout, is(new DisplayCutout.ParcelableWrapper()));
+ }
+
+ @Test
+ public void layoutHint_appWindowInTask_outsideContentFrame() {
+ // Initialize DisplayFrames
+ mDisplayPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+
+ // Task is in the nav bar area (usually does not happen, but this is similar enough to the
+ // possible overlap with the IME)
+ final Rect taskBounds = new Rect(100, mFrames.mContent.bottom + 1,
+ 200, mFrames.mContent.bottom + 10);
+
+ final Rect outFrame = new Rect();
+ final Rect outContentInsets = new Rect();
+ final Rect outStableInsets = new Rect();
+ final Rect outOutsets = new Rect();
+ final DisplayCutout.ParcelableWrapper outDisplayCutout =
+ new DisplayCutout.ParcelableWrapper();
+
+ mDisplayPolicy.getLayoutHintLw(mWindow.mAttrs, taskBounds, mFrames,
+ true /* floatingStack */, outFrame, outContentInsets, outStableInsets, outOutsets,
+ outDisplayCutout);
+
+ assertThat(outFrame, is(taskBounds));
+ assertThat(outContentInsets, is(new Rect()));
+ assertThat(outStableInsets, is(new Rect()));
+ assertThat(outOutsets, is(new Rect()));
+ assertThat(outDisplayCutout, is(new DisplayCutout.ParcelableWrapper()));
+ }
+
+ /**
+ * Asserts that {@code actual} is inset by the given amounts from the full display rect.
+ *
+ * Convenience wrapper for when only the top and bottom inset are non-zero.
+ */
+ private void assertInsetByTopBottom(Rect actual, int expectedInsetTop,
+ int expectedInsetBottom) {
+ assertInsetBy(actual, 0, expectedInsetTop, 0, expectedInsetBottom);
+ }
+
+ /** Asserts that {@code actual} is inset by the given amounts from the full display rect. */
+ private void assertInsetBy(Rect actual, int expectedInsetLeft, int expectedInsetTop,
+ int expectedInsetRight, int expectedInsetBottom) {
+ assertEquals(new Rect(expectedInsetLeft, expectedInsetTop,
+ mFrames.mDisplayWidth - expectedInsetRight,
+ mFrames.mDisplayHeight - expectedInsetBottom), actual);
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
new file mode 100644
index 0000000..8349ac7f
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -0,0 +1,199 @@
+/*
+ * 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.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+import static android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND;
+import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
+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.TYPE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
+
+import static com.android.server.policy.WindowManagerPolicy.NAV_BAR_BOTTOM;
+import static com.android.server.policy.WindowManagerPolicy.NAV_BAR_RIGHT;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.graphics.PixelFormat;
+import android.platform.test.annotations.Presubmit;
+import android.view.WindowManager;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+@SmallTest
+@Presubmit
+public class DisplayPolicyTests extends WindowTestsBase {
+
+ private WindowState createOpaqueFullscreen(boolean hasLightNavBar) {
+ final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, "opaqueFullscreen");
+ final WindowManager.LayoutParams attrs = win.mAttrs;
+ attrs.width = MATCH_PARENT;
+ attrs.height = MATCH_PARENT;
+ attrs.flags =
+ FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR | FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+ attrs.format = PixelFormat.OPAQUE;
+ attrs.systemUiVisibility = attrs.subtreeSystemUiVisibility = win.mSystemUiVisibility =
+ hasLightNavBar ? SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR : 0;
+ return win;
+ }
+
+ private WindowState createDimmingDialogWindow(boolean canBeImTarget) {
+ final WindowState win = spy(createWindow(null, TYPE_APPLICATION, "dimmingDialog"));
+ final WindowManager.LayoutParams attrs = win.mAttrs;
+ attrs.width = WRAP_CONTENT;
+ attrs.height = WRAP_CONTENT;
+ attrs.flags = FLAG_DIM_BEHIND | (canBeImTarget ? 0 : FLAG_ALT_FOCUSABLE_IM);
+ attrs.format = PixelFormat.TRANSLUCENT;
+ when(win.isDimming()).thenReturn(true);
+ return win;
+ }
+
+ private WindowState createInputMethodWindow(boolean visible, boolean drawNavBar,
+ boolean hasLightNavBar) {
+ final WindowState win = createWindow(null, TYPE_INPUT_METHOD, "inputMethod");
+ final WindowManager.LayoutParams attrs = win.mAttrs;
+ attrs.width = MATCH_PARENT;
+ attrs.height = MATCH_PARENT;
+ attrs.flags = FLAG_NOT_FOCUSABLE | FLAG_LAYOUT_IN_SCREEN
+ | (drawNavBar ? FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS : 0);
+ attrs.format = PixelFormat.TRANSPARENT;
+ attrs.systemUiVisibility = attrs.subtreeSystemUiVisibility = win.mSystemUiVisibility =
+ hasLightNavBar ? SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR : 0;
+ win.mHasSurface = visible;
+ return win;
+ }
+
+ @Test
+ public void testChooseNavigationColorWindowLw() {
+ final WindowState opaque = createOpaqueFullscreen(false);
+
+ final WindowState dimmingImTarget = createDimmingDialogWindow(true);
+ final WindowState dimmingNonImTarget = createDimmingDialogWindow(false);
+
+ final WindowState visibleIme = createInputMethodWindow(true, true, false);
+ final WindowState invisibleIme = createInputMethodWindow(false, true, false);
+ final WindowState imeNonDrawNavBar = createInputMethodWindow(true, false, false);
+
+ // If everything is null, return null
+ assertNull(null, DisplayPolicy.chooseNavigationColorWindowLw(
+ null, null, null, NAV_BAR_BOTTOM));
+
+ assertEquals(opaque, DisplayPolicy.chooseNavigationColorWindowLw(
+ opaque, opaque, null, NAV_BAR_BOTTOM));
+ assertEquals(dimmingImTarget, DisplayPolicy.chooseNavigationColorWindowLw(
+ opaque, dimmingImTarget, null, NAV_BAR_BOTTOM));
+ assertEquals(dimmingNonImTarget, DisplayPolicy.chooseNavigationColorWindowLw(
+ opaque, dimmingNonImTarget, null, NAV_BAR_BOTTOM));
+
+ assertEquals(visibleIme, DisplayPolicy.chooseNavigationColorWindowLw(
+ null, null, visibleIme, NAV_BAR_BOTTOM));
+ assertEquals(visibleIme, DisplayPolicy.chooseNavigationColorWindowLw(
+ null, dimmingImTarget, visibleIme, NAV_BAR_BOTTOM));
+ assertEquals(dimmingNonImTarget, DisplayPolicy.chooseNavigationColorWindowLw(
+ null, dimmingNonImTarget, visibleIme, NAV_BAR_BOTTOM));
+ assertEquals(visibleIme, DisplayPolicy.chooseNavigationColorWindowLw(
+ opaque, opaque, visibleIme, NAV_BAR_BOTTOM));
+ assertEquals(visibleIme, DisplayPolicy.chooseNavigationColorWindowLw(
+ opaque, dimmingImTarget, visibleIme, NAV_BAR_BOTTOM));
+ assertEquals(dimmingNonImTarget, DisplayPolicy.chooseNavigationColorWindowLw(
+ opaque, dimmingNonImTarget, visibleIme, NAV_BAR_BOTTOM));
+
+ assertEquals(opaque, DisplayPolicy.chooseNavigationColorWindowLw(
+ opaque, opaque, invisibleIme, NAV_BAR_BOTTOM));
+ assertEquals(opaque, DisplayPolicy.chooseNavigationColorWindowLw(
+ opaque, opaque, invisibleIme, NAV_BAR_BOTTOM));
+ assertEquals(opaque, DisplayPolicy.chooseNavigationColorWindowLw(
+ opaque, opaque, visibleIme, NAV_BAR_RIGHT));
+
+ // Only IME windows that have FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS should be navigation color
+ // window.
+ assertEquals(opaque, DisplayPolicy.chooseNavigationColorWindowLw(
+ opaque, opaque, imeNonDrawNavBar, NAV_BAR_BOTTOM));
+ assertEquals(dimmingImTarget, DisplayPolicy.chooseNavigationColorWindowLw(
+ opaque, dimmingImTarget, imeNonDrawNavBar, NAV_BAR_BOTTOM));
+ assertEquals(dimmingNonImTarget, DisplayPolicy.chooseNavigationColorWindowLw(
+ opaque, dimmingNonImTarget, imeNonDrawNavBar, NAV_BAR_BOTTOM));
+ }
+
+ @Test
+ public void testUpdateLightNavigationBarLw() {
+ final WindowState opaqueDarkNavBar = createOpaqueFullscreen(false);
+ final WindowState opaqueLightNavBar = createOpaqueFullscreen(true);
+
+ final WindowState dimming = createDimmingDialogWindow(false);
+
+ final WindowState imeDrawDarkNavBar = createInputMethodWindow(true, true, false);
+ final WindowState imeDrawLightNavBar = createInputMethodWindow(true, true, true);
+
+ assertEquals(SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR,
+ DisplayPolicy.updateLightNavigationBarLw(
+ SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR, null, null,
+ null, null));
+
+ // Opaque top fullscreen window overrides SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR flag.
+ assertEquals(0, DisplayPolicy.updateLightNavigationBarLw(
+ 0, opaqueDarkNavBar, opaqueDarkNavBar, null, opaqueDarkNavBar));
+ assertEquals(0, DisplayPolicy.updateLightNavigationBarLw(
+ SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR, opaqueDarkNavBar, opaqueDarkNavBar, null,
+ opaqueDarkNavBar));
+ assertEquals(SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR,
+ DisplayPolicy.updateLightNavigationBarLw(0, opaqueLightNavBar,
+ opaqueLightNavBar, null, opaqueLightNavBar));
+ assertEquals(SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR,
+ DisplayPolicy.updateLightNavigationBarLw(SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR,
+ opaqueLightNavBar, opaqueLightNavBar, null, opaqueLightNavBar));
+
+ // Dimming window clears SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.
+ assertEquals(0, DisplayPolicy.updateLightNavigationBarLw(
+ 0, opaqueDarkNavBar, dimming, null, dimming));
+ assertEquals(0, DisplayPolicy.updateLightNavigationBarLw(
+ 0, opaqueLightNavBar, dimming, null, dimming));
+ assertEquals(0, DisplayPolicy.updateLightNavigationBarLw(
+ SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR, opaqueDarkNavBar, dimming, null, dimming));
+ assertEquals(0, DisplayPolicy.updateLightNavigationBarLw(
+ SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR, opaqueLightNavBar, dimming, null, dimming));
+ assertEquals(0, DisplayPolicy.updateLightNavigationBarLw(
+ SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR, opaqueLightNavBar, dimming, imeDrawLightNavBar,
+ dimming));
+
+ // IME window clears SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
+ assertEquals(0, DisplayPolicy.updateLightNavigationBarLw(
+ SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR, null, null, imeDrawDarkNavBar,
+ imeDrawDarkNavBar));
+
+ // Even if the top fullscreen has SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR, IME window wins.
+ assertEquals(0, DisplayPolicy.updateLightNavigationBarLw(
+ SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR, opaqueLightNavBar, opaqueLightNavBar,
+ imeDrawDarkNavBar, imeDrawDarkNavBar));
+
+ // IME window should be able to use SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.
+ assertEquals(SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR,
+ DisplayPolicy.updateLightNavigationBarLw(0, opaqueDarkNavBar,
+ opaqueDarkNavBar, imeDrawLightNavBar, imeDrawLightNavBar));
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTestsBase.java
new file mode 100644
index 0000000..1d63c57
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTestsBase.java
@@ -0,0 +1,184 @@
+/*
+ * 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.util.DisplayMetrics.DENSITY_DEFAULT;
+import static android.view.DisplayCutout.BOUNDS_POSITION_BOTTOM;
+import static android.view.DisplayCutout.BOUNDS_POSITION_LEFT;
+import static android.view.DisplayCutout.BOUNDS_POSITION_RIGHT;
+import static android.view.DisplayCutout.BOUNDS_POSITION_TOP;
+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.junit.Assert.assertEquals;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.os.IBinder;
+import android.testing.TestableResources;
+import android.util.Pair;
+import android.view.DisplayCutout;
+import android.view.DisplayInfo;
+import android.view.Gravity;
+import android.view.View;
+import android.view.WindowManagerGlobal;
+
+import com.android.internal.R;
+import com.android.server.wm.utils.WmDisplayCutout;
+
+import org.junit.Before;
+
+public class DisplayPolicyTestsBase extends WindowTestsBase {
+
+ static final int DISPLAY_WIDTH = 500;
+ static final int DISPLAY_HEIGHT = 1000;
+ static final int DISPLAY_DENSITY = 320;
+
+ static final int STATUS_BAR_HEIGHT = 10;
+ static final int NAV_BAR_HEIGHT = 15;
+ static final int DISPLAY_CUTOUT_HEIGHT = 8;
+
+ DisplayPolicy mDisplayPolicy;
+
+ @Before
+ public void setUpBase() {
+ super.setUpBase();
+ mDisplayPolicy = spy(mDisplayContent.getDisplayPolicy());
+
+ final TestContextWrapper context =
+ new TestContextWrapper(mDisplayPolicy.getSystemUiContext());
+ final TestableResources resources = context.getResourceMocker();
+ resources.addOverride(R.dimen.status_bar_height_portrait, STATUS_BAR_HEIGHT);
+ resources.addOverride(R.dimen.status_bar_height_landscape, STATUS_BAR_HEIGHT);
+ resources.addOverride(R.dimen.navigation_bar_height, NAV_BAR_HEIGHT);
+ resources.addOverride(R.dimen.navigation_bar_height_landscape, NAV_BAR_HEIGHT);
+ resources.addOverride(R.dimen.navigation_bar_width, NAV_BAR_HEIGHT);
+ when(mDisplayPolicy.getSystemUiContext()).thenReturn(context);
+ when(mDisplayPolicy.hasNavigationBar()).thenReturn(true);
+
+ final int shortSizeDp =
+ Math.min(DISPLAY_WIDTH, DISPLAY_HEIGHT) * DENSITY_DEFAULT / DISPLAY_DENSITY;
+ final int longSizeDp =
+ Math.min(DISPLAY_WIDTH, DISPLAY_HEIGHT) * DENSITY_DEFAULT / DISPLAY_DENSITY;
+ mDisplayContent.getDisplayRotation().configure(
+ DISPLAY_WIDTH, DISPLAY_HEIGHT, shortSizeDp, longSizeDp);
+ mDisplayPolicy.configure(DISPLAY_WIDTH, DISPLAY_HEIGHT, shortSizeDp);
+ mDisplayPolicy.onConfigurationChanged();
+
+ mStatusBarWindow.mAttrs.gravity = Gravity.TOP;
+ addWindow(mStatusBarWindow);
+ mDisplayPolicy.mLastSystemUiFlags |= View.STATUS_BAR_TRANSPARENT;
+
+ mNavBarWindow.mAttrs.gravity = Gravity.BOTTOM;
+ addWindow(mNavBarWindow);
+ mDisplayPolicy.mLastSystemUiFlags |= View.NAVIGATION_BAR_TRANSPARENT;
+ }
+
+ void addWindow(WindowState win) {
+ mDisplayPolicy.adjustWindowParamsLw(win, win.mAttrs, true /* hasStatusBarPermission */);
+ assertEquals(WindowManagerGlobal.ADD_OKAY,
+ mDisplayPolicy.prepareAddWindowLw(win, win.mAttrs));
+ win.mHasSurface = true;
+ }
+
+ static Pair<DisplayInfo, WmDisplayCutout> displayInfoAndCutoutForRotation(int rotation,
+ boolean withDisplayCutout) {
+ final DisplayInfo info = new DisplayInfo();
+ WmDisplayCutout cutout = null;
+
+ final boolean flippedDimensions = rotation == ROTATION_90 || rotation == ROTATION_270;
+ info.logicalWidth = flippedDimensions ? DISPLAY_HEIGHT : DISPLAY_WIDTH;
+ info.logicalHeight = flippedDimensions ? DISPLAY_WIDTH : DISPLAY_HEIGHT;
+ info.rotation = rotation;
+ if (withDisplayCutout) {
+ cutout = WmDisplayCutout.computeSafeInsets(
+ displayCutoutForRotation(rotation), info.logicalWidth,
+ info.logicalHeight);
+ info.displayCutout = cutout.getDisplayCutout();
+ } else {
+ info.displayCutout = null;
+ }
+ return Pair.create(info, cutout);
+ }
+
+ private static DisplayCutout displayCutoutForRotation(int rotation) {
+ final RectF rectF =
+ new RectF(DISPLAY_WIDTH / 4, 0, DISPLAY_WIDTH * 3 / 4, DISPLAY_CUTOUT_HEIGHT);
+
+ final Matrix m = new Matrix();
+ transformPhysicalToLogicalCoordinates(rotation, DISPLAY_WIDTH, DISPLAY_HEIGHT, m);
+ m.mapRect(rectF);
+
+ int pos = -1;
+ switch (rotation) {
+ case ROTATION_0:
+ pos = BOUNDS_POSITION_TOP;
+ break;
+ case ROTATION_90:
+ pos = BOUNDS_POSITION_LEFT;
+ break;
+ case ROTATION_180:
+ pos = BOUNDS_POSITION_BOTTOM;
+ break;
+ case ROTATION_270:
+ pos = BOUNDS_POSITION_RIGHT;
+ break;
+ }
+
+ return DisplayCutout.fromBoundingRect((int) rectF.left, (int) rectF.top,
+ (int) rectF.right, (int) rectF.bottom, pos);
+ }
+
+ static class TestContextWrapper extends ContextWrapper {
+ private final TestableResources mResourceMocker;
+
+ TestContextWrapper(Context targetContext) {
+ super(targetContext);
+ mResourceMocker = new TestableResources(targetContext.getResources());
+ }
+
+ @Override
+ public int checkPermission(String permission, int pid, int uid) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+
+ @Override
+ public int checkPermission(String permission, int pid, int uid, IBinder callerToken) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+
+ @Override
+ public Resources getResources() {
+ return mResourceMocker.getResources();
+ }
+
+ TestableResources getResourceMocker() {
+ return mResourceMocker;
+ }
+ }
+
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
new file mode 100644
index 0000000..8e881b5
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
@@ -0,0 +1,446 @@
+/*
+ * 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.REMOVE_CONTENT_MODE_DESTROY;
+import static android.view.WindowManager.REMOVE_CONTENT_MODE_MOVE_TO_PRIMARY;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Matchers.eq;
+
+import android.app.WindowConfiguration;
+import android.platform.test.annotations.Presubmit;
+import android.view.Display;
+import android.view.DisplayInfo;
+import android.view.Surface;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.policy.WindowManagerPolicy;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+
+/**
+ * Tests for the {@link DisplayWindowSettings} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:DisplayWindowSettingsTests
+ */
+@SmallTest
+@Presubmit
+public class DisplayWindowSettingsTests extends WindowTestsBase {
+
+ private static final File TEST_FOLDER = getInstrumentation().getTargetContext().getCacheDir();
+ private DisplayWindowSettings mTarget;
+
+ DisplayInfo mPrivateDisplayInfo;
+
+ private DisplayContent mPrimaryDisplay;
+ private DisplayContent mSecondaryDisplay;
+ private DisplayContent mPrivateDisplay;
+
+ @Before
+ public void setUp() throws Exception {
+ deleteRecursively(TEST_FOLDER);
+
+ mWm.setSupportsFreeformWindowManagement(false);
+ mWm.setIsPc(false);
+ mWm.setForceDesktopModeOnExternalDisplays(false);
+
+ mTarget = new DisplayWindowSettings(mWm, TEST_FOLDER);
+
+ mPrimaryDisplay = mWm.getDefaultDisplayContentLocked();
+ mSecondaryDisplay = mDisplayContent;
+ assertNotEquals(Display.DEFAULT_DISPLAY, mSecondaryDisplay.getDisplayId());
+
+ mPrivateDisplayInfo = new DisplayInfo(mDisplayInfo);
+ mPrivateDisplayInfo.flags |= Display.FLAG_PRIVATE;
+ mPrivateDisplay = createNewDisplay(mPrivateDisplayInfo);
+ assertNotEquals(Display.DEFAULT_DISPLAY, mPrivateDisplay.getDisplayId());
+ assertNotEquals(mSecondaryDisplay.getDisplayId(), mPrivateDisplay.getDisplayId());
+ }
+
+ @After
+ public void tearDown() {
+ deleteRecursively(TEST_FOLDER);
+ }
+
+ @Test
+ public void testPrimaryDisplayDefaultToFullscreen_NoFreeformSupport() {
+ mTarget.applySettingsToDisplayLocked(mPrimaryDisplay);
+
+ assertEquals(WindowConfiguration.WINDOWING_MODE_FULLSCREEN,
+ mPrimaryDisplay.getWindowingMode());
+ }
+
+ @Test
+ public void testPrimaryDisplayDefaultToFullscreen_HasFreeformSupport_NonPc_NoDesktopMode() {
+ mWm.setSupportsFreeformWindowManagement(true);
+
+ mTarget.applySettingsToDisplayLocked(mPrimaryDisplay);
+
+ assertEquals(WindowConfiguration.WINDOWING_MODE_FULLSCREEN,
+ mPrimaryDisplay.getWindowingMode());
+ }
+
+ @Test
+ public void testPrimaryDisplayDefaultToFullscreen_HasFreeformSupport_NonPc_HasDesktopMode() {
+ mWm.setSupportsFreeformWindowManagement(true);
+ mWm.setForceDesktopModeOnExternalDisplays(true);
+
+ mTarget.applySettingsToDisplayLocked(mPrimaryDisplay);
+
+ assertEquals(WindowConfiguration.WINDOWING_MODE_FULLSCREEN,
+ mPrimaryDisplay.getWindowingMode());
+ }
+
+ @Test
+ public void testPrimaryDisplayDefaultToFreeform_HasFreeformSupport_IsPc() {
+ mWm.setSupportsFreeformWindowManagement(true);
+ mWm.setIsPc(true);
+
+ mTarget.applySettingsToDisplayLocked(mPrimaryDisplay);
+
+ assertEquals(WindowConfiguration.WINDOWING_MODE_FREEFORM,
+ mPrimaryDisplay.getWindowingMode());
+ }
+
+ @Test
+ public void testSecondaryDisplayDefaultToFullscreen_NoFreeformSupport() {
+ mTarget.applySettingsToDisplayLocked(mSecondaryDisplay);
+
+ assertEquals(WindowConfiguration.WINDOWING_MODE_FULLSCREEN,
+ mSecondaryDisplay.getWindowingMode());
+ }
+
+ @Test
+ public void testSecondaryDisplayDefaultToFreeform_HasFreeformSupport_NonPc_NoDesktopMode() {
+ mWm.setSupportsFreeformWindowManagement(true);
+
+ mTarget.applySettingsToDisplayLocked(mSecondaryDisplay);
+
+ assertEquals(WindowConfiguration.WINDOWING_MODE_FULLSCREEN,
+ mSecondaryDisplay.getWindowingMode());
+ }
+
+ @Test
+ public void testSecondaryDisplayDefaultToFreeform_HasFreeformSupport_NonPc_HasDesktopMode() {
+ mWm.setSupportsFreeformWindowManagement(true);
+ mWm.setForceDesktopModeOnExternalDisplays(true);
+
+ mTarget.applySettingsToDisplayLocked(mSecondaryDisplay);
+
+ assertEquals(WindowConfiguration.WINDOWING_MODE_FREEFORM,
+ mSecondaryDisplay.getWindowingMode());
+ }
+
+ @Test
+ public void testSecondaryDisplayDefaultToFreeform_HasFreeformSupport_IsPc() {
+ mWm.setSupportsFreeformWindowManagement(true);
+ mWm.setIsPc(true);
+
+ mTarget.applySettingsToDisplayLocked(mSecondaryDisplay);
+
+ assertEquals(WindowConfiguration.WINDOWING_MODE_FREEFORM,
+ mSecondaryDisplay.getWindowingMode());
+ }
+
+ @Test
+ public void testDefaultToOriginalMetrics() {
+ final int originalWidth = mSecondaryDisplay.mBaseDisplayWidth;
+ final int originalHeight = mSecondaryDisplay.mBaseDisplayHeight;
+ final int originalDensity = mSecondaryDisplay.mBaseDisplayDensity;
+ final boolean originalScalingDisabled = mSecondaryDisplay.mDisplayScalingDisabled;
+
+ mTarget.applySettingsToDisplayLocked(mSecondaryDisplay);
+
+ assertEquals(originalWidth, mSecondaryDisplay.mBaseDisplayWidth);
+ assertEquals(originalHeight, mSecondaryDisplay.mBaseDisplayHeight);
+ assertEquals(originalDensity, mSecondaryDisplay.mBaseDisplayDensity);
+ assertEquals(originalScalingDisabled, mSecondaryDisplay.mDisplayScalingDisabled);
+ }
+
+ @Test
+ public void testSetForcedSize() {
+ final DisplayInfo originalInfo = new DisplayInfo(mSecondaryDisplay.getDisplayInfo());
+ // Provides the orginal display info to avoid changing initial display size.
+ doAnswer(invocation -> {
+ ((DisplayInfo) invocation.getArguments()[1]).copyFrom(originalInfo);
+ return null;
+ }).when(mWm.mDisplayManagerInternal).getNonOverrideDisplayInfo(anyInt(), any());
+
+ mTarget.setForcedSize(mSecondaryDisplay, 1000 /* width */, 2000 /* height */);
+ applySettingsToDisplayByNewInstance(mSecondaryDisplay);
+
+ assertEquals(1000 /* width */, mSecondaryDisplay.mBaseDisplayWidth);
+ assertEquals(2000 /* height */, mSecondaryDisplay.mBaseDisplayHeight);
+
+ mWm.clearForcedDisplaySize(mSecondaryDisplay.getDisplayId());
+ assertEquals(mSecondaryDisplay.mInitialDisplayWidth, mSecondaryDisplay.mBaseDisplayWidth);
+ assertEquals(mSecondaryDisplay.mInitialDisplayHeight, mSecondaryDisplay.mBaseDisplayHeight);
+ }
+
+ @Test
+ public void testSetForcedDensity() {
+ mTarget.setForcedDensity(mSecondaryDisplay, 600 /* density */, 0 /* userId */);
+ applySettingsToDisplayByNewInstance(mSecondaryDisplay);
+
+ assertEquals(600 /* density */, mSecondaryDisplay.mBaseDisplayDensity);
+
+ mWm.clearForcedDisplayDensityForUser(mSecondaryDisplay.getDisplayId(), 0 /* userId */);
+ assertEquals(mSecondaryDisplay.mInitialDisplayDensity,
+ mSecondaryDisplay.mBaseDisplayDensity);
+ }
+
+ @Test
+ public void testSetForcedScalingMode() {
+ mTarget.setForcedScalingMode(mSecondaryDisplay, DisplayContent.FORCE_SCALING_MODE_DISABLED);
+ applySettingsToDisplayByNewInstance(mSecondaryDisplay);
+
+ assertTrue(mSecondaryDisplay.mDisplayScalingDisabled);
+
+ mWm.setForcedDisplayScalingMode(mSecondaryDisplay.getDisplayId(),
+ DisplayContent.FORCE_SCALING_MODE_AUTO);
+ assertFalse(mSecondaryDisplay.mDisplayScalingDisabled);
+ }
+
+ @Test
+ public void testDefaultToZeroOverscan() {
+ mTarget.applySettingsToDisplayLocked(mPrimaryDisplay);
+
+ assertOverscan(mPrimaryDisplay, 0 /* left */, 0 /* top */, 0 /* right */, 0 /* bottom */);
+ }
+
+ @Test
+ public void testPersistOverscanInSameInstance() {
+ final DisplayInfo info = mPrimaryDisplay.getDisplayInfo();
+ mTarget.setOverscanLocked(info, 1 /* left */, 2 /* top */, 3 /* right */, 4 /* bottom */);
+
+ mTarget.applySettingsToDisplayLocked(mPrimaryDisplay);
+
+ assertOverscan(mPrimaryDisplay, 1 /* left */, 2 /* top */, 3 /* right */, 4 /* bottom */);
+ }
+
+ @Test
+ public void testPersistOverscanAcrossInstances() {
+ final DisplayInfo info = mPrimaryDisplay.getDisplayInfo();
+ mTarget.setOverscanLocked(info, 1 /* left */, 2 /* top */, 3 /* right */, 4 /* bottom */);
+
+ applySettingsToDisplayByNewInstance(mPrimaryDisplay);
+
+ assertOverscan(mPrimaryDisplay, 1 /* left */, 2 /* top */, 3 /* right */, 4 /* bottom */);
+ }
+
+ @Test
+ public void testDefaultToFreeUserRotation() {
+ mTarget.applySettingsToDisplayLocked(mSecondaryDisplay);
+
+ final DisplayRotation rotation = mSecondaryDisplay.getDisplayRotation();
+ assertEquals(WindowManagerPolicy.USER_ROTATION_FREE, rotation.getUserRotationMode());
+ assertFalse(rotation.isRotationFrozen());
+ }
+
+ @Test
+ public void testDefaultTo0DegRotation() {
+ mTarget.applySettingsToDisplayLocked(mSecondaryDisplay);
+
+ assertEquals(Surface.ROTATION_0, mSecondaryDisplay.getDisplayRotation().getUserRotation());
+ }
+
+ @Test
+ public void testPrivateDisplayDefaultToDestroyContent() {
+ assertEquals(REMOVE_CONTENT_MODE_DESTROY,
+ mTarget.getRemoveContentModeLocked(mPrivateDisplay));
+ }
+
+ @Test
+ public void testPublicDisplayDefaultToMoveToPrimary() {
+ assertEquals(REMOVE_CONTENT_MODE_MOVE_TO_PRIMARY,
+ mTarget.getRemoveContentModeLocked(mSecondaryDisplay));
+ }
+
+ @Test
+ public void testDefaultToNotShowWithInsecureKeyguard() {
+ assertFalse(mTarget.shouldShowWithInsecureKeyguardLocked(mPrivateDisplay));
+ assertFalse(mTarget.shouldShowWithInsecureKeyguardLocked(mSecondaryDisplay));
+ }
+
+ @Test
+ public void testPublicDisplayNotAllowSetShouldShowWithInsecureKeyguard() {
+ mTarget.setShouldShowWithInsecureKeyguardLocked(mSecondaryDisplay, true);
+
+ assertFalse(mTarget.shouldShowWithInsecureKeyguardLocked(mSecondaryDisplay));
+ }
+
+ @Test
+ public void testPrivateDisplayAllowSetShouldShowWithInsecureKeyguard() {
+ mTarget.setShouldShowWithInsecureKeyguardLocked(mPrivateDisplay, true);
+
+ assertTrue(mTarget.shouldShowWithInsecureKeyguardLocked(mPrivateDisplay));
+ }
+
+ @Test
+ public void testPrimaryDisplayShouldShowSystemDecors() {
+ assertTrue(mTarget.shouldShowSystemDecorsLocked(mPrimaryDisplay));
+
+ mTarget.setShouldShowSystemDecorsLocked(mPrimaryDisplay, false);
+
+ // Default display should show system decors
+ assertTrue(mTarget.shouldShowSystemDecorsLocked(mPrimaryDisplay));
+ }
+
+ @Test
+ public void testSecondaryDisplayDefaultToNotShowSystemDecors() {
+ assertFalse(mTarget.shouldShowSystemDecorsLocked(mSecondaryDisplay));
+ }
+
+ @Test
+ public void testPrimaryDisplayShouldShowIme() {
+ assertTrue(mTarget.shouldShowImeLocked(mPrimaryDisplay));
+
+ mTarget.setShouldShowImeLocked(mPrimaryDisplay, false);
+
+ assertTrue(mTarget.shouldShowImeLocked(mPrimaryDisplay));
+ }
+
+ @Test
+ public void testSecondaryDisplayDefaultToNotShowIme() {
+ assertFalse(mTarget.shouldShowImeLocked(mSecondaryDisplay));
+ }
+
+ @Test
+ public void testPersistUserRotationModeInSameInstance() {
+ mTarget.setUserRotation(mSecondaryDisplay, WindowManagerPolicy.USER_ROTATION_LOCKED,
+ Surface.ROTATION_90);
+
+ mTarget.applySettingsToDisplayLocked(mSecondaryDisplay);
+
+ final DisplayRotation rotation = mSecondaryDisplay.getDisplayRotation();
+ assertEquals(WindowManagerPolicy.USER_ROTATION_LOCKED, rotation.getUserRotationMode());
+ assertTrue(rotation.isRotationFrozen());
+ }
+
+ @Test
+ public void testPersistUserRotationInSameInstance() {
+ mTarget.setUserRotation(mSecondaryDisplay, WindowManagerPolicy.USER_ROTATION_LOCKED,
+ Surface.ROTATION_90);
+
+ mTarget.applySettingsToDisplayLocked(mSecondaryDisplay);
+
+ assertEquals(Surface.ROTATION_90, mSecondaryDisplay.getDisplayRotation().getUserRotation());
+ }
+
+ @Test
+ public void testPersistUserRotationModeAcrossInstances() {
+ mTarget.setUserRotation(mSecondaryDisplay, WindowManagerPolicy.USER_ROTATION_LOCKED,
+ Surface.ROTATION_270);
+
+ applySettingsToDisplayByNewInstance(mSecondaryDisplay);
+
+ final DisplayRotation rotation = mSecondaryDisplay.getDisplayRotation();
+ assertEquals(WindowManagerPolicy.USER_ROTATION_LOCKED, rotation.getUserRotationMode());
+ assertTrue(rotation.isRotationFrozen());
+ }
+
+ @Test
+ public void testPersistUserRotationAcrossInstances() {
+ mTarget.setUserRotation(mSecondaryDisplay, WindowManagerPolicy.USER_ROTATION_LOCKED,
+ Surface.ROTATION_270);
+
+ applySettingsToDisplayByNewInstance(mSecondaryDisplay);
+
+ assertEquals(Surface.ROTATION_270,
+ mSecondaryDisplay.getDisplayRotation().getUserRotation());
+ }
+
+ @Test
+ public void testNotFixedToUserRotationByDefault() {
+ mTarget.setUserRotation(mPrimaryDisplay, WindowManagerPolicy.USER_ROTATION_LOCKED,
+ Surface.ROTATION_0);
+
+ final DisplayRotation displayRotation = mock(DisplayRotation.class);
+ mPrimaryDisplay = spy(mPrimaryDisplay);
+ when(mPrimaryDisplay.getDisplayRotation()).thenReturn(displayRotation);
+
+ mTarget.applySettingsToDisplayLocked(mPrimaryDisplay);
+
+ verify(displayRotation).restoreSettings(anyInt(), anyInt(), eq(false));
+ }
+
+ @Test
+ public void testSetFixedToUserRotation() {
+ mTarget.setFixedToUserRotation(mPrimaryDisplay, true);
+
+ final DisplayRotation displayRotation = mock(DisplayRotation.class);
+ mPrimaryDisplay = spy(mPrimaryDisplay);
+ when(mPrimaryDisplay.getDisplayRotation()).thenReturn(displayRotation);
+
+ applySettingsToDisplayByNewInstance(mPrimaryDisplay);
+
+ verify(displayRotation).restoreSettings(anyInt(), anyInt(), eq(true));
+ }
+
+ private static void assertOverscan(DisplayContent display, int left, int top, int right,
+ int bottom) {
+ final DisplayInfo info = display.getDisplayInfo();
+
+ assertEquals(left, info.overscanLeft);
+ assertEquals(top, info.overscanTop);
+ assertEquals(right, info.overscanRight);
+ assertEquals(bottom, info.overscanBottom);
+ }
+
+ /**
+ * This method helps to ensure read and write persistent settings successfully because the
+ * constructor of {@link DisplayWindowSettings} should read the persistent file from the given
+ * path that also means the previous state must be written correctly.
+ */
+ private void applySettingsToDisplayByNewInstance(DisplayContent display) {
+ new DisplayWindowSettings(mWm, TEST_FOLDER).applySettingsToDisplayLocked(display);
+ }
+
+ private static boolean deleteRecursively(File file) {
+ boolean fullyDeleted = true;
+ if (file.isFile()) {
+ return file.delete();
+ } else if (file.isDirectory()) {
+ final File[] files = file.listFiles();
+ for (File child : files) {
+ fullyDeleted &= deleteRecursively(child);
+ }
+ fullyDeleted &= file.delete();
+ }
+ return fullyDeleted;
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DockedStackDividerControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DockedStackDividerControllerTests.java
new file mode 100644
index 0000000..3206208
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DockedStackDividerControllerTests.java
@@ -0,0 +1,88 @@
+/*
+ * 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.DOCKED_BOTTOM;
+import static android.view.WindowManager.DOCKED_LEFT;
+import static android.view.WindowManager.DOCKED_RIGHT;
+import static android.view.WindowManager.DOCKED_TOP;
+import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM;
+import static android.view.WindowManagerPolicyConstants.NAV_BAR_LEFT;
+import static android.view.WindowManagerPolicyConstants.NAV_BAR_RIGHT;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+@SmallTest
+@Presubmit
+public class DockedStackDividerControllerTests {
+
+ @Test
+ public void testIsDockSideAllowedDockTop() {
+ // Docked top is always allowed
+ assertTrue(DockedStackDividerController.isDockSideAllowed(DOCKED_TOP, DOCKED_LEFT,
+ NAV_BAR_BOTTOM, true /* navigationBarCanMove */));
+ assertTrue(DockedStackDividerController.isDockSideAllowed(DOCKED_TOP, DOCKED_LEFT,
+ NAV_BAR_BOTTOM, false /* navigationBarCanMove */));
+ }
+
+ @Test
+ public void testIsDockSideAllowedDockBottom() {
+ // Cannot dock bottom
+ assertFalse(DockedStackDividerController.isDockSideAllowed(DOCKED_BOTTOM, DOCKED_LEFT,
+ NAV_BAR_BOTTOM, true /* navigationBarCanMove */));
+ }
+
+ @Test
+ public void testIsDockSideAllowedNavigationBarMovable() {
+ assertFalse(DockedStackDividerController.isDockSideAllowed(DOCKED_LEFT, DOCKED_LEFT,
+ NAV_BAR_BOTTOM, true /* navigationBarCanMove */));
+ assertFalse(DockedStackDividerController.isDockSideAllowed(DOCKED_LEFT, DOCKED_LEFT,
+ NAV_BAR_LEFT, true /* navigationBarCanMove */));
+ assertTrue(DockedStackDividerController.isDockSideAllowed(DOCKED_LEFT, DOCKED_LEFT,
+ NAV_BAR_RIGHT, true /* navigationBarCanMove */));
+ assertFalse(DockedStackDividerController.isDockSideAllowed(DOCKED_RIGHT, DOCKED_LEFT,
+ NAV_BAR_BOTTOM, true /* navigationBarCanMove */));
+ assertFalse(DockedStackDividerController.isDockSideAllowed(DOCKED_RIGHT, DOCKED_LEFT,
+ NAV_BAR_RIGHT, true /* navigationBarCanMove */));
+ assertTrue(DockedStackDividerController.isDockSideAllowed(DOCKED_RIGHT, DOCKED_LEFT,
+ NAV_BAR_LEFT, true /* navigationBarCanMove */));
+ }
+
+ @Test
+ public void testIsDockSideAllowedNavigationBarNotMovable() {
+ // Navigation bar is not movable such as tablets
+ assertTrue(DockedStackDividerController.isDockSideAllowed(DOCKED_LEFT, DOCKED_LEFT,
+ NAV_BAR_BOTTOM, false /* navigationBarCanMove */));
+ assertTrue(DockedStackDividerController.isDockSideAllowed(DOCKED_LEFT, DOCKED_TOP,
+ NAV_BAR_BOTTOM, false /* navigationBarCanMove */));
+ assertFalse(DockedStackDividerController.isDockSideAllowed(DOCKED_LEFT, DOCKED_RIGHT,
+ NAV_BAR_BOTTOM, false /* navigationBarCanMove */));
+ assertFalse(DockedStackDividerController.isDockSideAllowed(DOCKED_RIGHT, DOCKED_LEFT,
+ NAV_BAR_BOTTOM, false /* navigationBarCanMove */));
+ assertFalse(DockedStackDividerController.isDockSideAllowed(DOCKED_RIGHT, DOCKED_TOP,
+ NAV_BAR_BOTTOM, false /* navigationBarCanMove */));
+ assertTrue(DockedStackDividerController.isDockSideAllowed(DOCKED_RIGHT, DOCKED_RIGHT,
+ NAV_BAR_BOTTOM, false /* navigationBarCanMove */));
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
new file mode 100644
index 0000000..f1c6eab
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
@@ -0,0 +1,191 @@
+/*
+ * 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 com.android.dx.mockito.inline.extended.ExtendedMockito.any;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+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.view.InputChannel;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+import android.view.View;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.LocalServices;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests for the {@link DragDropController} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:DragDropControllerTests
+ */
+@SmallTest
+@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 {
+ 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;
+ }
+
+ @BeforeClass
+ public static void setUpOnce() {
+ final UserManagerInternal userManager = mock(UserManagerInternal.class);
+ LocalServices.addService(UserManagerInternal.class, userManager);
+ }
+
+ @AfterClass
+ public static void tearDownOnce() {
+ LocalServices.removeServiceForTest(UserManagerInternal.class);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ mTarget = new TestDragDropController(mWm, mWm.mH.getLooper());
+ mDisplayContent = spy(mDisplayContent);
+ mWindow = createDropTargetWindow("Drag test window", 0);
+ doReturn(mWindow).when(mDisplayContent).getTouchableWinAtPointLocked(0, 0);
+ when(mWm.mInputManager.transferTouchFocus(any(), any())).thenReturn(true);
+
+ synchronized (mWm.mGlobalLock) {
+ mWm.mWindowMap.put(mWindow.mClient.asBinder(), mWindow);
+ }
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ final CountDownLatch latch;
+ synchronized (mWm.mGlobalLock) {
+ 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() {
+ dragFlow(0, ClipData.newPlainText("label", "Test"), 0, 0);
+ }
+
+ @Test
+ public void testPerformDrag_NullDataWithGrantUri() {
+ dragFlow(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, null, 0, 0);
+ }
+
+ @Test
+ public void testPerformDrag_NullDataToOtherUser() {
+ final WindowState otherUsersWindow =
+ createDropTargetWindow("Other user's window", 1 * UserHandle.PER_USER_RANGE);
+ doReturn(otherUsersWindow).when(mDisplayContent).getTouchableWinAtPointLocked(10, 10);
+
+ 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")
+ .setBufferSize(100, 100)
+ .setFormat(PixelFormat.TRANSLUCENT)
+ .build();
+
+ assertTrue(mWm.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/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java
new file mode 100644
index 0000000..c11e606
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.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.view.InsetsState.TYPE_TOP_BAR;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+
+import static org.junit.Assert.assertEquals;
+
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.view.InsetsSource;
+
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+@SmallTest
+@FlakyTest(detail = "Promote once confirmed non-flaky")
+@Presubmit
+public class InsetsSourceProviderTest extends WindowTestsBase {
+
+ private InsetsSourceProvider mProvider = new InsetsSourceProvider(
+ new InsetsSource(TYPE_TOP_BAR));
+
+ @Test
+ public void testPostLayout() {
+ final WindowState topBar = createWindow(null, TYPE_APPLICATION, "parentWindow");
+ topBar.getFrameLw().set(0, 0, 500, 100);
+ topBar.mHasSurface = true;
+ mProvider.setWindow(topBar, null);
+ mProvider.onPostLayout();
+ assertEquals(new Rect(0, 0, 500, 100), mProvider.getSource().getFrame());
+ assertEquals(Insets.of(0, 100, 0, 0),
+ mProvider.getSource().calculateInsets(new Rect(0, 0, 500, 500),
+ false /* ignoreVisibility */));
+ }
+
+ @Test
+ public void testPostLayout_invisible() {
+ final WindowState topBar = createWindow(null, TYPE_APPLICATION, "parentWindow");
+ topBar.getFrameLw().set(0, 0, 500, 100);
+ mProvider.setWindow(topBar, null);
+ mProvider.onPostLayout();
+ assertEquals(Insets.NONE, mProvider.getSource().calculateInsets(new Rect(0, 0, 500, 500),
+ false /* ignoreVisibility */));
+ }
+
+ @Test
+ public void testPostLayout_frameProvider() {
+ final WindowState topBar = createWindow(null, TYPE_APPLICATION, "parentWindow");
+ topBar.getFrameLw().set(0, 0, 500, 100);
+ mProvider.setWindow(topBar,
+ (displayFrames, windowState, rect) -> {
+ rect.set(10, 10, 20, 20);
+ });
+ mProvider.onPostLayout();
+ assertEquals(new Rect(10, 10, 20, 20), mProvider.getSource().getFrame());
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
new file mode 100644
index 0000000..331622c
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.InsetsState.TYPE_IME;
+import static android.view.InsetsState.TYPE_NAVIGATION_BAR;
+import static android.view.InsetsState.TYPE_TOP_BAR;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import android.platform.test.annotations.Presubmit;
+import android.view.InsetsState;
+
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+@SmallTest
+@FlakyTest(detail = "Promote once confirmed non-flaky")
+@Presubmit
+public class InsetsStateControllerTest extends WindowTestsBase {
+
+ @Test
+ public void testStripForDispatch_notOwn() {
+ final WindowState topBar = createWindow(null, TYPE_APPLICATION, "parentWindow");
+ final WindowState app = createWindow(null, TYPE_APPLICATION, "parentWindow");
+ mDisplayContent.getInsetsStateController().getSourceProvider(TYPE_TOP_BAR)
+ .setWindow(topBar, null);
+ topBar.setInsetProvider(
+ mDisplayContent.getInsetsStateController().getSourceProvider(TYPE_TOP_BAR));
+ assertNotNull(mDisplayContent.getInsetsStateController().getInsetsForDispatch(app)
+ .getSource(TYPE_TOP_BAR));
+ }
+
+ @Test
+ public void testStripForDispatch_own() {
+ final WindowState topBar = createWindow(null, TYPE_APPLICATION, "parentWindow");
+ mDisplayContent.getInsetsStateController().getSourceProvider(TYPE_TOP_BAR)
+ .setWindow(topBar, null);
+ topBar.setInsetProvider(
+ mDisplayContent.getInsetsStateController().getSourceProvider(TYPE_TOP_BAR));
+ assertEquals(new InsetsState(),
+ mDisplayContent.getInsetsStateController().getInsetsForDispatch(topBar));
+ }
+
+ @Test
+ public void testStripForDispatch_navBar() {
+ final WindowState navBar = createWindow(null, TYPE_APPLICATION, "parentWindow");
+ final WindowState topBar = createWindow(null, TYPE_APPLICATION, "parentWindow");
+ final WindowState ime = createWindow(null, TYPE_APPLICATION, "parentWindow");
+ mDisplayContent.getInsetsStateController().getSourceProvider(TYPE_TOP_BAR)
+ .setWindow(topBar, null);
+ mDisplayContent.getInsetsStateController().getSourceProvider(TYPE_NAVIGATION_BAR)
+ .setWindow(navBar, null);
+ mDisplayContent.getInsetsStateController().getSourceProvider(TYPE_IME)
+ .setWindow(ime, null);
+ assertEquals(new InsetsState(),
+ mDisplayContent.getInsetsStateController().getInsetsForDispatch(navBar));
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/PinnedStackControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/PinnedStackControllerTest.java
new file mode 100644
index 0000000..63d9fb9
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/PinnedStackControllerTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.Display.DEFAULT_DISPLAY;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.reset;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+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 android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+import android.view.IPinnedStackListener;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:PinnedStackControllerTest
+ */
+@SmallTest
+@Presubmit
+public class PinnedStackControllerTest extends WindowTestsBase {
+
+ private static final int SHELF_HEIGHT = 300;
+
+ @Mock private IPinnedStackListener mIPinnedStackListener;
+ @Mock private IPinnedStackListener.Stub mIPinnedStackListenerStub;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ when(mIPinnedStackListener.asBinder()).thenReturn(mIPinnedStackListenerStub);
+ }
+
+ @Test
+ public void setShelfHeight_shelfVisibilityChangedTriggered() throws RemoteException {
+ mWm.mSupportsPictureInPicture = true;
+ mWm.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);
+
+ mWm.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/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
new file mode 100644
index 0000000..cc6a58a
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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_HOME;
+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.dx.mockito.inline.extended.ExtendedMockito.atLeast;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyNoMoreInteractions;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+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.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.eq;
+
+import android.os.Binder;
+import android.os.IInterface;
+import android.platform.test.annotations.Presubmit;
+import android.util.SparseBooleanArray;
+import android.view.IRecentsAnimationRunner;
+import android.view.SurfaceControl;
+
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:RecentsAnimationControllerTest
+ */
+@SmallTest
+@Presubmit
+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 {
+ MockitoAnnotations.initMocks(this);
+
+ when(mMockRunner.asBinder()).thenReturn(new Binder());
+ mController = new RecentsAnimationController(mWm, 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() {
+ 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");
+ }
+ }
+
+ @FlakyTest(bugId = 117117823)
+ @Test
+ public void testIncludedApps_expectTargetAndVisible() {
+ mWm.setRecentsAnimationController(mController);
+ final AppWindowToken homeAppWindow = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_HOME);
+ final AppWindowToken appWindow = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ final AppWindowToken hiddenAppWindow = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ hiddenAppWindow.setHidden(true);
+ mDisplayContent.getConfiguration().windowConfiguration.setRotation(
+ mDisplayContent.getRotation());
+ mController.initialize(mDisplayContent, ACTIVITY_TYPE_HOME, new SparseBooleanArray());
+
+ // Ensure that we are animating the target activity as well
+ assertTrue(mController.isAnimatingTask(homeAppWindow.getTask()));
+ assertTrue(mController.isAnimatingTask(appWindow.getTask()));
+ assertFalse(mController.isAnimatingTask(hiddenAppWindow.getTask()));
+ }
+
+ private static void verifyNoMoreInteractionsExceptAsBinder(IInterface binder) {
+ verify(binder, atLeast(0)).asBinder();
+ verifyNoMoreInteractions(binder);
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
new file mode 100644
index 0000000..ad2a708
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
@@ -0,0 +1,221 @@
+/*
+ * 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 com.android.dx.mockito.inline.extended.ExtendedMockito.atLeast;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyNoMoreInteractions;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.os.IInterface;
+import android.platform.test.annotations.Presubmit;
+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 androidx.test.filters.SmallTest;
+
+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.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:RemoteAnimationControllerTest
+ */
+@SmallTest
+@Presubmit
+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 {
+ MockitoAnnotations.initMocks(this);
+
+ when(mMockRunner.asBinder()).thenReturn(new Binder());
+ mAdapter = new RemoteAnimationAdapter(mMockRunner, 100, 50);
+ mAdapter.setCallingPid(123);
+ mWm.mH.runWithScissors(() -> mHandler = new TestHandler(null, mClock), 0);
+ mController = new RemoteAnimationController(mWm, mAdapter, mHandler);
+ }
+
+ @Test
+ public void testRun() throws Exception {
+ final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin");
+ mDisplayContent.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();
+ mWm.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 {
+ mDisplayContent.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 {
+ mWm.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 {
+ mWm.setAnimationScale(2, 1.0f);
+ }
+ }
+
+ @Test
+ public void testZeroAnimations() {
+ mController.goodToGo();
+ verifyNoMoreInteractionsExceptAsBinder(mMockRunner);
+ }
+
+ @Test
+ public void testNotReallyStarted() {
+ 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();
+ mWm.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() {
+ 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/wmtests/src/com/android/server/wm/StackWindowControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/StackWindowControllerTests.java
new file mode 100644
index 0000000..ce5b13c
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/StackWindowControllerTests.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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Test class for {@link StackWindowController}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:StackWindowControllerTests
+ */
+@SmallTest
+@Presubmit
+public class StackWindowControllerTests extends WindowTestsBase {
+ @Test
+ public void testRemoveContainer() {
+ 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() {
+ 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() {
+ // 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/wmtests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java
new file mode 100644
index 0000000..83e7ee7
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java
@@ -0,0 +1,251 @@
+/*
+ * 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.dx.mockito.inline.extended.ExtendedMockito.any;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.atLeast;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.atLeastOnce;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.animation.AnimationHandler.AnimationFrameCallbackProvider;
+import android.animation.ValueAnimator;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.os.PowerManagerInternal;
+import android.platform.test.annotations.Presubmit;
+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 androidx.test.filters.FlakyTest;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.LocalAnimationAdapter.AnimationSpec;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * Test class for {@link SurfaceAnimationRunner}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:SurfaceAnimationRunnerTest
+ */
+@SmallTest
+@Presubmit
+public class SurfaceAnimationRunnerTest extends WindowTestsBase {
+
+ @Mock SurfaceControl mMockSurface;
+ @Mock Transaction mMockTransaction;
+ @Mock AnimationSpec mMockAnimationSpec;
+ @Mock PowerManagerInternal mMockPowerManager;
+ @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ private SurfaceAnimationRunner mSurfaceAnimationRunner;
+ private CountDownLatch mFinishCallbackLatch;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mFinishCallbackLatch = new CountDownLatch(1);
+ mSurfaceAnimationRunner = new SurfaceAnimationRunner(null /* callbackProvider */, null,
+ mMockTransaction, mMockPowerManager);
+ }
+
+ 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() {
+ mSurfaceAnimationRunner = new SurfaceAnimationRunner(new NoOpFrameCallbackProvider(), null,
+ mMockTransaction, mMockPowerManager);
+ 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, mMockPowerManager);
+ 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, mMockPowerManager);
+ 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();
+ }
+
+ @Test
+ public void testPowerHint() throws Exception {
+ mSurfaceAnimationRunner = new SurfaceAnimationRunner(new NoOpFrameCallbackProvider(), null,
+ mMockTransaction, mMockPowerManager);
+ mSurfaceAnimationRunner.startAnimation(createTranslateAnimation(), mMockSurface,
+ mMockTransaction, this::finishedCallback);
+ waitUntilNextFrame();
+
+ // TODO: For some reason we don't have access to PowerHint definition from the tests. For
+ // now let's just verify that we got some kind of hint.
+ verify(mMockPowerManager).powerHint(anyInt(), anyInt());
+ }
+
+ 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/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
new file mode 100644
index 0000000..d14f30d
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
@@ -0,0 +1,310 @@
+/*
+ * 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.dx.mockito.inline.extended.ExtendedMockito.any;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions;
+
+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 android.platform.test.annotations.Presubmit;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Builder;
+import android.view.SurfaceControl.Transaction;
+import android.view.SurfaceSession;
+
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.SurfaceAnimator.Animatable;
+import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Test class for {@link SurfaceAnimatorTest}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:SurfaceAnimatorTest
+ */
+@SmallTest
+@Presubmit
+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 {
+ MockitoAnnotations.initMocks(this);
+
+ mAnimatable = new MyAnimatable(mWm, mSession, mTransaction);
+ mAnimatable2 = new MyAnimatable(mWm, mSession, mTransaction);
+ mDeferFinishAnimatable = new DeferFinishAnimatable(mWm, mSession, mTransaction);
+ }
+
+ @After
+ public void tearDown() {
+ mAnimatable = null;
+ mAnimatable2 = null;
+ mDeferFinishAnimatable = null;
+ mSession.kill();
+ mSession = null;
+ }
+
+ @Test
+ public void testRunAnimation() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+
+ // 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.mEndDeferFinishCallback.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 static class MyAnimatable implements Animatable {
+
+ private final SurfaceSession mSession;
+ private final Transaction mTransaction;
+ final SurfaceControl mParent;
+ final SurfaceControl mSurface;
+ final SurfaceAnimator mSurfaceAnimator;
+ SurfaceControl mLeash;
+ boolean mFinishedCallbackCalled;
+
+ MyAnimatable(WindowManagerService wm, SurfaceSession session, Transaction transaction) {
+ mSession = session;
+ mTransaction = transaction;
+ mParent = wm.makeSurfaceBuilder(mSession)
+ .setName("test surface parent")
+ .build();
+ mSurface = wm.makeSurfaceBuilder(mSession)
+ .setName("test surface")
+ .build();
+ mFinishedCallbackCalled = false;
+ mLeash = null;
+ mSurfaceAnimator = new SurfaceAnimator(this, mFinishedCallback, wm);
+ }
+
+ @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 static class DeferFinishAnimatable extends MyAnimatable {
+
+ Runnable mEndDeferFinishCallback;
+
+ DeferFinishAnimatable(WindowManagerService wm, SurfaceSession session,
+ Transaction transaction) {
+ super(wm, session, transaction);
+ }
+
+ @Override
+ public boolean shouldDeferAnimationFinish(Runnable endDeferFinishCallback) {
+ mEndDeferFinishCallback = endDeferFinishCallback;
+ return true;
+ }
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskPositionerTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskPositionerTests.java
new file mode 100644
index 0000000..b996bfb
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskPositionerTests.java
@@ -0,0 +1,496 @@
+/*
+ * 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.WINDOWING_MODE_FREEFORM;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+
+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;
+
+import android.app.IActivityTaskManager;
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Display;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+/**
+ * Tests for the {@link TaskPositioner} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:TaskPositionerTests
+ */
+@SmallTest
+public class TaskPositionerTests extends WindowTestsBase {
+
+ private static final boolean DEBUGGING = false;
+ private static final String TAG = "TaskPositionerTest";
+
+ private static final int MOUSE_DELTA_X = 5;
+ private static final int MOUSE_DELTA_Y = 5;
+
+ private int mMinVisibleWidth;
+ private int mMinVisibleHeight;
+ private TaskPositioner mPositioner;
+ private WindowState mWindow;
+ private Rect mDimBounds = new Rect();
+
+ @Before
+ public void setUp() throws Exception {
+ 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 = new TaskPositioner(mWm, Mockito.mock(IActivityTaskManager.class));
+ mPositioner.register(mDisplayContent);
+
+ mWindow = Mockito.spy(createWindow(null, TYPE_BASE_APPLICATION, "window"));
+ final Task task = Mockito.spy(mWindow.getTask());
+ Mockito.when(mWindow.getTask()).thenReturn(task);
+
+ Mockito.doAnswer(invocation -> {
+ final Rect rect = (Rect) invocation.getArguments()[0];
+ rect.set(mDimBounds);
+ return (Void) null;
+ }).when(task).getDimBounds(Mockito.any(Rect.class));
+
+ mWindow.getStack().setWindowingMode(WINDOWING_MODE_FREEFORM);
+ }
+
+ @Test
+ public void testOverrideFactory() {
+ 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(mWm));
+ 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
+ public void testBasicFreeWindowResizing() {
+ final Rect r = new Rect(100, 220, 700, 520);
+ final int midY = (r.top + r.bottom) / 2;
+ mDimBounds.set(r);
+
+ // Start a drag resize starting upper left.
+ mPositioner.startDrag(mWindow, true /*resizing*/,
+ false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y);
+ 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(mWindow, true /*resizing*/,
+ false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, midY);
+
+ // 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
+ public void testFreeWindowResizingTestAllEdges() {
+ 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;
+ mDimBounds.set(r);
+
+ // Drag upper left.
+ mPositioner.startDrag(mWindow, true /*resizing*/,
+ false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y);
+ 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(mWindow, true /*resizing*/,
+ false /*preserveOrientation*/, midX, r.top - MOUSE_DELTA_Y);
+ 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(mWindow, true /*resizing*/,
+ false /*preserveOrientation*/, r.right + MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y);
+ 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(mWindow, true /*resizing*/,
+ false /*preserveOrientation*/, r.right + MOUSE_DELTA_X, midY);
+ 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(mWindow, true /*resizing*/,
+ false /*preserveOrientation*/,
+ r.right + MOUSE_DELTA_X, r.bottom + MOUSE_DELTA_Y);
+ 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(mWindow, true /*resizing*/,
+ false /*preserveOrientation*/, midX, r.bottom + MOUSE_DELTA_Y);
+ 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(mWindow, true /*resizing*/,
+ false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.bottom + MOUSE_DELTA_Y);
+ 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(mWindow, true /*resizing*/,
+ false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, midX);
+ 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
+ public void testLandscapePreservedWindowResizingDragTopLeft() {
+ final Rect r = new Rect(100, 220, 700, 520);
+ mDimBounds.set(r);
+
+ mPositioner.startDrag(mWindow, true /*resizing*/,
+ true /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y);
+ 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
+ public void testLandscapePreservedWindowResizingDragLeft() {
+ final Rect r = new Rect(100, 220, 700, 520);
+ final int midY = (r.top + r.bottom) / 2;
+ mDimBounds.set(r);
+
+ mPositioner.startDrag(mWindow, true /*resizing*/,
+ true /*preserveOrientation*/, r.left - MOUSE_DELTA_X, midY);
+
+ // 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
+ public void testLandscapePreservedWindowResizingDragTop() {
+ final Rect r = new Rect(100, 220, 700, 520);
+ final int midX = (r.left + r.right) / 2;
+ mDimBounds.set(r);
+
+ mPositioner.startDrag(mWindow, true /*resizing*/,
+ true /*preserveOrientation*/, midX, r.top - MOUSE_DELTA_Y);
+
+ // 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
+ public void testPortraitPreservedWindowResizingDragTopLeft() {
+ final Rect r = new Rect(330, 100, 630, 600);
+ mDimBounds.set(r);
+
+ mPositioner.startDrag(mWindow, true /*resizing*/,
+ true /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y);
+ 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(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 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
+ public void testPortraitPreservedWindowResizingDragLeft() {
+ final Rect r = new Rect(330, 100, 630, 600);
+ final int midY = (r.top + r.bottom) / 2;
+ mDimBounds.set(r);
+
+ mPositioner.startDrag(mWindow, true /*resizing*/,
+ true /*preserveOrientation*/, r.left - MOUSE_DELTA_X, midY);
+
+ // 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
+ public void testPortraitPreservedWindowResizingDragTop() {
+ final Rect r = new Rect(330, 100, 630, 600);
+ final int midX = (r.left + r.right) / 2;
+ mDimBounds.set(r);
+
+ mPositioner.startDrag(mWindow, true /*resizing*/,
+ true /*preserveOrientation*/, midX, r.top - MOUSE_DELTA_Y);
+
+ // 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 static 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("left", expected.left, actual.left);
+ assertEquals("right", expected.right, actual.right);
+ assertEquals("top", expected.top, actual.top);
+ assertEquals("bottom", expected.bottom, actual.bottom);
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskPositioningControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskPositioningControllerTests.java
new file mode 100644
index 0000000..3991e06
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskPositioningControllerTests.java
@@ -0,0 +1,120 @@
+/*
+ * 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 com.android.dx.mockito.inline.extended.ExtendedMockito.any;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+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 android.platform.test.annotations.Presubmit;
+import android.view.InputChannel;
+
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for the {@link TaskPositioningController} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:TaskPositioningControllerTests
+ */
+@FlakyTest(bugId = 117924387)
+@SmallTest
+@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 {
+ assertNotNull(mWm.mTaskPositioningController);
+ mTarget = mWm.mTaskPositioningController;
+
+ when(mWm.mInputManager.transferTouchFocus(
+ any(InputChannel.class),
+ any(InputChannel.class))).thenReturn(true);
+
+ mWindow = createWindow(null, TYPE_BASE_APPLICATION, "window");
+ mWindow.mInputChannel = new InputChannel();
+ synchronized (mWm.mGlobalLock) {
+ mWm.mWindowMap.put(mWindow.mClient.asBinder(), mWindow);
+ }
+ }
+
+ @Test
+ public void testStartAndFinishPositioning() {
+ synchronized (mWm.mGlobalLock) {
+ assertFalse(mTarget.isPositioningLocked());
+ assertNull(mTarget.getDragWindowHandleLocked());
+ }
+
+ assertTrue(mTarget.startMovingTask(mWindow.mClient, 0, 0));
+
+ synchronized (mWm.mGlobalLock) {
+ assertTrue(mTarget.isPositioningLocked());
+ assertNotNull(mTarget.getDragWindowHandleLocked());
+ }
+
+ mTarget.finishTaskPositioning();
+ // Wait until the looper processes finishTaskPositioning.
+ assertTrue(mWm.mH.runWithScissors(() -> { }, TIMEOUT_MS));
+
+ assertFalse(mTarget.isPositioningLocked());
+ assertNull(mTarget.getDragWindowHandleLocked());
+ }
+
+ @Test
+ public void testHandleTapOutsideTask() {
+ synchronized (mWm.mGlobalLock) {
+ 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(mWm.mH.runWithScissors(() -> { }, TIMEOUT_MS));
+
+ synchronized (mWm.mGlobalLock) {
+ assertTrue(mTarget.isPositioningLocked());
+ assertNotNull(mTarget.getDragWindowHandleLocked());
+ }
+
+ mTarget.finishTaskPositioning();
+ // Wait until the looper processes finishTaskPositioning.
+ assertTrue(mWm.mH.runWithScissors(() -> { }, TIMEOUT_MS));
+
+ assertFalse(mTarget.isPositioningLocked());
+ assertNull(mTarget.getDragWindowHandleLocked());
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java
new file mode 100644
index 0000000..1c6afd54
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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 androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for {@link TaskSnapshotCache}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:TaskSnapshotCacheTest
+ */
+@SmallTest
+@Presubmit
+public class TaskSnapshotCacheTest extends TaskSnapshotPersisterTestBase {
+
+ private TaskSnapshotCache mCache;
+
+ @Override
+ @Before
+ public void setUp() {
+ super.setUp();
+
+ mCache = new TaskSnapshotCache(mWm, mLoader);
+ }
+
+ @Test
+ public void testAppRemoved() {
+ 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() {
+ 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() {
+ 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() {
+ final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ mPersister.persistSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId, createSnapshot());
+ mPersister.waitForQueueEmpty();
+ assertNull(mCache.getSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+
+ // Load it from disk
+ assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId,
+ true /* restoreFromDisk */, true /* reducedResolution */));
+
+ // Make sure it's not in the cache now.
+ assertNull(mCache.getSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+ }
+
+ @Test
+ public void testRestoreFromDisk() {
+ final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ mPersister.persistSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId, createSnapshot());
+ mPersister.waitForQueueEmpty();
+ assertNull(mCache.getSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+
+ // Load it from disk
+ assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId,
+ true /* restoreFromDisk */, false /* reducedResolution */));
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java
new file mode 100644
index 0000000..792e8a6
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.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.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.SNAPSHOT_MODE_APP_THEME;
+import static com.android.server.wm.TaskSnapshotController.SNAPSHOT_MODE_REAL;
+
+import static junit.framework.Assert.assertEquals;
+
+import android.platform.test.annotations.Presubmit;
+import android.util.ArraySet;
+
+import androidx.test.filters.SmallTest;
+
+import com.google.android.collect.Sets;
+
+import org.junit.Test;
+
+/**
+ * Test class for {@link TaskSnapshotController}.
+ *
+ * Build/Install/Run:
+ * * atest FrameworksServicesTests:TaskSnapshotControllerTest
+ */
+@SmallTest
+@Presubmit
+public class TaskSnapshotControllerTest extends WindowTestsBase {
+
+ @Test
+ public void testGetClosingApps_closing() {
+ final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
+ "closingWindow");
+ closingWindow.mAppToken.commitVisibility(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<>();
+ mWm.mTaskSnapshotController.getClosingTasks(closingApps, closingTasks);
+ assertEquals(1, closingTasks.size());
+ assertEquals(closingWindow.mAppToken.getTask(), closingTasks.valueAt(0));
+ }
+
+ @Test
+ public void testGetClosingApps_notClosing() {
+ final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
+ "closingWindow");
+ final WindowState openingWindow = createAppWindow(closingWindow.getTask(),
+ FIRST_APPLICATION_WINDOW, "openingWindow");
+ closingWindow.mAppToken.commitVisibility(null, false /* visible */, TRANSIT_UNSET,
+ true /* performLayout */, false /* isVoiceInteraction */);
+ openingWindow.mAppToken.commitVisibility(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<>();
+ mWm.mTaskSnapshotController.getClosingTasks(closingApps, closingTasks);
+ assertEquals(0, closingTasks.size());
+ }
+
+ @Test
+ public void testGetClosingApps_skipClosingAppsSnapshotTasks() {
+ final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
+ "closingWindow");
+ closingWindow.mAppToken.commitVisibility(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<>();
+ mWm.mTaskSnapshotController.addSkipClosingAppSnapshotTasks(
+ Sets.newArraySet(closingWindow.mAppToken.getTask()));
+ mWm.mTaskSnapshotController.getClosingTasks(closingApps, closingTasks);
+ assertEquals(0, closingTasks.size());
+ }
+
+ @Test
+ public void testGetSnapshotMode() {
+ final WindowState disabledWindow = createWindow(null,
+ FIRST_APPLICATION_WINDOW, mDisplayContent, "disabledWindow");
+ disabledWindow.mAppToken.setDisablePreviewScreenshots(true);
+ assertEquals(SNAPSHOT_MODE_APP_THEME,
+ mWm.mTaskSnapshotController.getSnapshotMode(disabledWindow.getTask()));
+
+ final WindowState normalWindow = createWindow(null,
+ FIRST_APPLICATION_WINDOW, mDisplayContent, "normalWindow");
+ assertEquals(SNAPSHOT_MODE_REAL,
+ mWm.mTaskSnapshotController.getSnapshotMode(normalWindow.getTask()));
+
+ final WindowState secureWindow = createWindow(null,
+ FIRST_APPLICATION_WINDOW, mDisplayContent, "secureWindow");
+ secureWindow.mAttrs.flags |= FLAG_SECURE;
+ assertEquals(SNAPSHOT_MODE_APP_THEME,
+ mWm.mTaskSnapshotController.getSnapshotMode(secureWindow.getTask()));
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java
new file mode 100644
index 0000000..b0eafee
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java
@@ -0,0 +1,292 @@
+/*
+ * 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 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.util.ArraySet;
+import android.view.View;
+
+import androidx.test.filters.MediumTest;
+
+import com.android.server.wm.TaskSnapshotPersister.RemoveObsoleteFilesQueueItem;
+
+import org.junit.Test;
+
+import java.io.File;
+import java.util.function.Predicate;
+
+/**
+ * Test class for {@link TaskSnapshotPersister} and {@link TaskSnapshotLoader}
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:TaskSnapshotPersisterLoaderTest
+ */
+@MediumTest
+@Presubmit
+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(FILES_DIR.getPath() + "/snapshots/1.proto"),
+ new File(FILES_DIR.getPath() + "/snapshots/1.jpg"),
+ new File(FILES_DIR.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 static void assertTrueForFiles(File[] files, Predicate<File> predicate,
+ String message) {
+ for (File file : files) {
+ assertTrue(file.getName() + message, predicate.test(file));
+ }
+ }
+
+ @Test
+ public void testTaskRemovedFromRecents() {
+ mPersister.persistSnapshot(1, mTestUserId, createSnapshot());
+ mPersister.onTaskRemovedFromRecents(1, mTestUserId);
+ mPersister.waitForQueueEmpty();
+ assertFalse(new File(FILES_DIR.getPath() + "/snapshots/1.proto").exists());
+ assertFalse(new File(FILES_DIR.getPath() + "/snapshots/1.jpg").exists());
+ assertFalse(new File(FILES_DIR.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(FILES_DIR.getPath() + "/snapshots/3.proto"),
+ new File(FILES_DIR.getPath() + "/snapshots/4.proto")};
+ final File[] nonExistsFiles = new File[] {
+ new File(FILES_DIR.getPath() + "/snapshots/100.proto"),
+ new File(FILES_DIR.getPath() + "/snapshots/1.proto"),
+ new File(FILES_DIR.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(FILES_DIR.getPath() + "/snapshots/1.proto"),
+ new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg")};
+ final File[] nonExistsFiles = new File[] {
+ new File(FILES_DIR.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();
+ assertEquals(WINDOWING_MODE_FULLSCREEN, a.getWindowingMode());
+ assertEquals(WINDOWING_MODE_PINNED, b.getWindowingMode());
+ 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);
+ assertEquals(WINDOWING_MODE_FULLSCREEN, snapshotA.getWindowingMode());
+ assertEquals(WINDOWING_MODE_PINNED, snapshotB.getWindowingMode());
+ }
+
+ @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();
+ assertEquals(0, a.getSystemUiVisibility());
+ assertEquals(lightBarFlags, b.getSystemUiVisibility());
+ 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);
+ assertEquals(0, snapshotA.getSystemUiVisibility());
+ assertEquals(lightBarFlags, snapshotB.getSystemUiVisibility());
+ }
+
+ @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(FILES_DIR.getPath() + "/snapshots/1.proto"),
+ new File(FILES_DIR.getPath() + "/snapshots/1.jpg"),
+ new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg") };
+ final File[] nonExistsFiles = new File[] {
+ new File(FILES_DIR.getPath() + "/snapshots/2.proto"),
+ new File(FILES_DIR.getPath() + "/snapshots/2.jpg"),
+ new File(FILES_DIR.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(FILES_DIR.getPath() + "/snapshots/1.proto"),
+ new File(FILES_DIR.getPath() + "/snapshots/1.jpg"),
+ new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg"),
+ new File(FILES_DIR.getPath() + "/snapshots/2.proto"),
+ new File(FILES_DIR.getPath() + "/snapshots/2.jpg"),
+ new File(FILES_DIR.getPath() + "/snapshots/2_reduced.jpg")};
+ assertTrueForFiles(existsFiles, File::exists, " must exist");
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
new file mode 100644
index 0000000..946ffb60
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
@@ -0,0 +1,135 @@
+/*
+ * 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 static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import android.app.ActivityManager.TaskSnapshot;
+import android.content.ComponentName;
+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 org.junit.After;
+import org.junit.Before;
+
+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);
+ static final File FILES_DIR = getInstrumentation().getTargetContext().getFilesDir();
+
+ TaskSnapshotPersister mPersister;
+ TaskSnapshotLoader mLoader;
+ int mTestUserId;
+
+ @Before
+ public void setUp() {
+ final UserManager um = UserManager.get(getInstrumentation().getTargetContext());
+ mTestUserId = um.getUserHandle();
+ mPersister = new TaskSnapshotPersister(userId -> FILES_DIR);
+ mLoader = new TaskSnapshotLoader(mPersister);
+ mPersister.start();
+ }
+
+ @After
+ public void tearDown() {
+ cleanDirectory();
+ }
+
+ private void cleanDirectory() {
+ final File[] files = new File(FILES_DIR, "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.
+ */
+ static class TaskSnapshotBuilder {
+
+ private float mScale = 1f;
+ private boolean mIsRealSnapshot = true;
+ private boolean mIsTranslucent = false;
+ private int mWindowingMode = WINDOWING_MODE_FULLSCREEN;
+ private int mSystemUiVisibility = 0;
+
+ TaskSnapshotBuilder setScale(float scale) {
+ mScale = scale;
+ return this;
+ }
+
+ TaskSnapshotBuilder setIsRealSnapshot(boolean isRealSnapshot) {
+ mIsRealSnapshot = isRealSnapshot;
+ return this;
+ }
+
+ TaskSnapshotBuilder setIsTranslucent(boolean isTranslucent) {
+ mIsTranslucent = isTranslucent;
+ return this;
+ }
+
+ TaskSnapshotBuilder setWindowingMode(int windowingMode) {
+ mWindowingMode = windowingMode;
+ return this;
+ }
+
+ TaskSnapshotBuilder setSystemUiVisibility(int systemUiVisibility) {
+ mSystemUiVisibility = systemUiVisibility;
+ return this;
+ }
+
+ 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(new ComponentName("", ""), buffer, ORIENTATION_PORTRAIT,
+ TEST_INSETS, mScale < 1f /* reducedResolution */, mScale, mIsRealSnapshot,
+ mWindowingMode, mSystemUiVisibility, mIsTranslucent);
+ }
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotSurfaceTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotSurfaceTest.java
new file mode 100644
index 0000000..624ef9b
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotSurfaceTest.java
@@ -0,0 +1,248 @@
+/*
+ * 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 com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+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 android.app.ActivityManager.TaskSnapshot;
+import android.content.ComponentName;
+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.view.Surface;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.TaskSnapshotSurface.Window;
+
+import org.junit.Test;
+
+/**
+ * Test class for {@link TaskSnapshotSurface}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:TaskSnapshotSurfaceTest
+ */
+@SmallTest
+@Presubmit
+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_RARELY | GraphicBuffer.USAGE_SW_WRITE_NEVER);
+ final TaskSnapshot snapshot = new TaskSnapshot(new ComponentName("", ""), buffer,
+ ORIENTATION_PORTRAIT, contentInsets, false, 1.0f, true /* isRealSnapshot */,
+ WINDOWING_MODE_FULLSCREEN, 0 /* systemUiVisibility */, false /* isTranslucent */);
+ mSurface = new TaskSnapshotSurface(mWm, 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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/wmtests/src/com/android/server/wm/TaskStackContainersTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskStackContainersTests.java
new file mode 100644
index 0000000..f01e9f0
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskStackContainersTests.java
@@ -0,0 +1,128 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for the {@link DisplayContent.TaskStackContainers} container in {@link DisplayContent}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:TaskStackContainersTests
+ */
+@SmallTest
+@Presubmit
+public class TaskStackContainersTests extends WindowTestsBase {
+
+ private TaskStack mPinnedStack;
+
+ @Before
+ public void setUp() throws Exception {
+ 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() {
+ // 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);
+ assertThat(pinnedStackPos).isGreaterThan(stack2Pos);
+ assertThat(stack2Pos).isGreaterThan(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() {
+ // 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);
+ assertThat(pinnedStackPos).isGreaterThan(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);
+ }
+
+ @Test
+ public void testDisplayPositionWithPinnedStack() {
+ // The display contains pinned stack that was added in {@link #setUp}.
+ final TaskStack stack = createTaskStackOnDisplay(mDisplayContent);
+ final Task task = createTaskInStack(stack, 0 /* userId */);
+
+ // Add another display at top.
+ mWm.mRoot.positionChildAt(WindowContainer.POSITION_TOP, createNewDisplay(),
+ false /* includingParents */);
+
+ // Move the task of {@code mDisplayContent} to top.
+ stack.positionChildAt(WindowContainer.POSITION_TOP, task, true /* includingParents */);
+ final int indexOfDisplayWithPinnedStack = mWm.mRoot.mChildren.indexOf(mDisplayContent);
+
+ assertEquals("The testing DisplayContent should be moved to top with task",
+ mWm.mRoot.getChildCount() - 1, indexOfDisplayWithPinnedStack);
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskStackTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskStackTests.java
new file mode 100644
index 0000000..7ac3318
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskStackTests.java
@@ -0,0 +1,109 @@
+/*
+ * 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.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.assertNull;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Tests for the {@link TaskStack} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:TaskStackTests
+ */
+@SmallTest
+@Presubmit
+public class TaskStackTests extends WindowTestsBase {
+
+ @Test
+ public void testStackPositionChildAt() {
+ 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() {
+ 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());
+ mDisplayContent.mClosingApps.add(appWindowToken2);
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, stack.getOrientation());
+ }
+
+ @Test
+ public void testMoveTaskToBackDifferentStackOrientation() {
+ 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() {
+ 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/wmtests/src/com/android/server/wm/TaskWindowContainerControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskWindowContainerControllerTests.java
new file mode 100644
index 0000000..bbf508d
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskWindowContainerControllerTests.java
@@ -0,0 +1,145 @@
+/*
+ * 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.assertTrue;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Test class for {@link TaskWindowContainerController}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:TaskWindowContainerControllerTests
+ */
+@SmallTest
+@Presubmit
+public class TaskWindowContainerControllerTests extends WindowTestsBase {
+
+ /* Comment out due to removal of AppWindowContainerController
+ @Test
+ public void testRemoveContainer() {
+ 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);
+ }
+ */
+
+ /* Comment out due to removal of AppWindowContainerController
+ @Test
+ public void testRemoveContainer_deferRemoval() {
+ 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() {
+ 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() {
+ // 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/wmtests/src/com/android/server/wm/TestIWindow.java b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
new file mode 100644
index 0000000..432af0d
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
@@ -0,0 +1,103 @@
+/*
+ * 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.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;
+import android.view.InsetsState;
+
+import com.android.internal.os.IResultReceiver;
+
+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 insetsChanged(InsetsState insetsState) 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, boolean reportToClient)
+ 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/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
new file mode 100644
index 0000000..ba81bd1
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
@@ -0,0 +1,404 @@
+/*
+ * 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 com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.CompatibilityInfo;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.proto.ProtoOutputStream;
+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 final Supplier<WindowManagerService> mWmSupplier;
+
+ int mRotationToReport = 0;
+ boolean mKeyguardShowingAndNotOccluded = false;
+
+ private Runnable mRunnableWhenAddingSplashScreen;
+
+ 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) {
+ }
+
+ public void setDefaultDisplay(DisplayContentInfo displayContentInfo) {
+ }
+
+ @Override
+ public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {
+ return 0;
+ }
+
+ @Override
+ public boolean checkShowToOwnerOnly(WindowManager.LayoutParams attrs) {
+ return false;
+ }
+
+ @Override
+ public void adjustConfigurationLw(Configuration config, int keyboardPresence,
+ int navigationPresence) {
+ }
+
+ @Override
+ public int getMaxWallpaperLayer() {
+ 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.mGlobalLock) {
+ atoken = wm.mRoot.getAppWindowToken(appToken);
+ IWindow iWindow = mock(IWindow.class);
+ doReturn(mock(IBinder.class)).when(iWindow).asBinder();
+ window = WindowTestsBase.createWindow(null, TYPE_APPLICATION_STARTING, atoken,
+ "Starting window", 0 /* ownerId */, false /* internalWindows */, wm,
+ mock(Session.class), iWindow);
+ atoken.startingWindow = window;
+ }
+ if (mRunnableWhenAddingSplashScreen != null) {
+ mRunnableWhenAddingSplashScreen.run();
+ mRunnableWhenAddingSplashScreen = null;
+ }
+ return () -> {
+ synchronized (wm.mGlobalLock) {
+ atoken.removeChild(window);
+ atoken.startingWindow = null;
+ }
+ };
+ }
+
+ @Override
+ public void setKeyguardCandidateLw(WindowState win) {
+ }
+
+ @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 void applyKeyguardPolicyLw(WindowState win, WindowState imeTarget) {
+ }
+
+ @Override
+ public void setAllowLockscreenWhenOn(int displayId, boolean allow) {
+ }
+
+ @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 mKeyguardShowingAndNotOccluded;
+ }
+
+ @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 mKeyguardShowingAndNotOccluded;
+ }
+
+ @Override
+ public boolean inKeyguardRestrictedKeyInputMode() {
+ return false;
+ }
+
+ @Override
+ public void dismissKeyguardLw(@Nullable IKeyguardDismissCallback callback,
+ CharSequence message) {
+ }
+
+ @Override
+ public boolean isKeyguardDrawnLw() {
+ return false;
+ }
+
+ @Override
+ public void onKeyguardOccludedChangedLw(boolean occluded) {
+ }
+
+ 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 boolean performHapticFeedbackLw(WindowState win, int effectId, boolean always,
+ String reason) {
+ return false;
+ }
+
+ @Override
+ public void keepScreenOnStartedLw() {
+ }
+
+ @Override
+ public void keepScreenOnStoppedLw() {
+ }
+
+ @Override
+ public boolean hasNavigationBar() {
+ return false;
+ }
+
+ @Override
+ public void lockNow(Bundle options) {
+ }
+
+ @Override
+ public void showRecentApps() {
+ }
+
+ @Override
+ public void showGlobalActions() {
+ }
+
+ @Override
+ public boolean isUserSetupComplete() {
+ return false;
+ }
+
+ @Override
+ public int getUiMode() {
+ return 0;
+ }
+
+ @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 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 boolean setAodShowing(boolean aodShowing) {
+ return false;
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java
new file mode 100644
index 0000000..612f9ad
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.os.SystemClock;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for {@link AppTransition}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:UnknownAppVisibilityControllerTest
+ */
+@SmallTest
+@Presubmit
+public class UnknownAppVisibilityControllerTest extends WindowTestsBase {
+
+ @Before
+ public void setUp() throws Exception {
+ mDisplayContent.mUnknownAppVisibilityController.clear();
+ }
+
+ @Test
+ public void testFlow() {
+ final AppWindowToken token = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ mDisplayContent.mUnknownAppVisibilityController.notifyLaunched(token);
+ mDisplayContent.mUnknownAppVisibilityController.notifyAppResumedFinished(token);
+ mDisplayContent.mUnknownAppVisibilityController.notifyRelayouted(token);
+
+ // Make sure our handler processed the message.
+ SystemClock.sleep(100);
+ assertTrue(mDisplayContent.mUnknownAppVisibilityController.allResolved());
+ }
+
+ @Test
+ public void testMultiple() {
+ final AppWindowToken token1 = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ final AppWindowToken token2 = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ mDisplayContent.mUnknownAppVisibilityController.notifyLaunched(token1);
+ mDisplayContent.mUnknownAppVisibilityController.notifyAppResumedFinished(token1);
+ mDisplayContent.mUnknownAppVisibilityController.notifyLaunched(token2);
+ mDisplayContent.mUnknownAppVisibilityController.notifyRelayouted(token1);
+ mDisplayContent.mUnknownAppVisibilityController.notifyAppResumedFinished(token2);
+ mDisplayContent.mUnknownAppVisibilityController.notifyRelayouted(token2);
+
+ // Make sure our handler processed the message.
+ SystemClock.sleep(100);
+ assertTrue(mDisplayContent.mUnknownAppVisibilityController.allResolved());
+ }
+
+ @Test
+ public void testClear() {
+ final AppWindowToken token = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ mDisplayContent.mUnknownAppVisibilityController.notifyLaunched(token);
+ mDisplayContent.mUnknownAppVisibilityController.clear();
+ assertTrue(mDisplayContent.mUnknownAppVisibilityController.allResolved());
+ }
+
+ @Test
+ public void testAppRemoved() {
+ final AppWindowToken token = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ mDisplayContent.mUnknownAppVisibilityController.notifyLaunched(token);
+ mDisplayContent.mUnknownAppVisibilityController.appRemovedOrHidden(token);
+ assertTrue(mDisplayContent.mUnknownAppVisibilityController.allResolved());
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
new file mode 100644
index 0000000..d07230e
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
@@ -0,0 +1,82 @@
+/*
+ * 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_WALLPAPER;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static junit.framework.TestCase.assertNotNull;
+
+import static org.junit.Assert.assertNull;
+
+import android.graphics.Bitmap;
+import android.os.IBinder;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Tests for the {@link WallpaperController} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:WallpaperControllerTests
+ */
+@SmallTest
+@Presubmit
+public class WallpaperControllerTests extends WindowTestsBase {
+ @Test
+ public void testWallpaperScreenshot() {
+ WindowSurfaceController windowSurfaceController = mock(WindowSurfaceController.class);
+
+ synchronized (mWm.mGlobalLock) {
+ // No wallpaper
+ final DisplayContent dc = createNewDisplay();
+ Bitmap wallpaperBitmap = dc.mWallpaperController.screenshotWallpaperLocked();
+ assertNull(wallpaperBitmap);
+
+ // No wallpaper WSA Surface
+ WindowToken wallpaperWindowToken = new WallpaperWindowToken(mWm, mock(IBinder.class),
+ true, dc, true /* ownerCanManageAppTokens */);
+ WindowState wallpaperWindow = createWindow(null /* parent */, TYPE_WALLPAPER,
+ wallpaperWindowToken, "wallpaperWindow");
+ wallpaperBitmap = dc.mWallpaperController.screenshotWallpaperLocked();
+ assertNull(wallpaperBitmap);
+
+ // Wallpaper with not visible WSA surface.
+ wallpaperWindow.mWinAnimator.mSurfaceController = windowSurfaceController;
+ wallpaperWindow.mWinAnimator.mLastAlpha = 1;
+ wallpaperBitmap = dc.mWallpaperController.screenshotWallpaperLocked();
+ assertNull(wallpaperBitmap);
+
+ when(windowSurfaceController.getShown()).thenReturn(true);
+
+ // Wallpaper with WSA alpha set to 0.
+ wallpaperWindow.mWinAnimator.mLastAlpha = 0;
+ wallpaperBitmap = dc.mWallpaperController.screenshotWallpaperLocked();
+ assertNull(wallpaperBitmap);
+
+ // Wallpaper window with WSA Surface
+ wallpaperWindow.mWinAnimator.mLastAlpha = 1;
+ wallpaperBitmap = dc.mWallpaperController.screenshotWallpaperLocked();
+ assertNotNull(wallpaperBitmap);
+ }
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowConfigurationTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowConfigurationTests.java
new file mode 100644
index 0000000..3643457
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowConfigurationTests.java
@@ -0,0 +1,243 @@
+/*
+ * 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_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOW_CONFIG_ALWAYS_ON_TOP;
+import static android.app.WindowConfiguration.WINDOW_CONFIG_APP_BOUNDS;
+import static android.app.WindowConfiguration.WINDOW_CONFIG_ROTATION;
+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 android.app.WindowConfiguration;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.view.DisplayInfo;
+import android.view.Surface;
+
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class to for {@link android.app.WindowConfiguration}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:WindowConfigurationTests
+ */
+@FlakyTest(bugId = 74078662)
+@SmallTest
+@Presubmit
+public class WindowConfigurationTests extends WindowTestsBase {
+ private Rect mParentBounds;
+
+ @Before
+ public void setUp() throws Exception {
+ 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;
+ final Configuration config4 = new Configuration();
+ final WindowConfiguration winConfig4 = config4.windowConfiguration;
+
+ winConfig1.setAppBounds(0, 1, 1, 0);
+ winConfig2.setAppBounds(1, 2, 2, 1);
+ winConfig3.setAppBounds(winConfig1.getAppBounds());
+ winConfig4.setRotation(Surface.ROTATION_90);
+
+ 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 */));
+
+ winConfig2.setAlwaysOnTop(true);
+ assertEquals(WINDOW_CONFIG_APP_BOUNDS | WINDOW_CONFIG_WINDOWING_MODE
+ | WINDOW_CONFIG_ALWAYS_ON_TOP,
+ winConfig1.diff(winConfig2, false /* compareUndefined */));
+
+ assertEquals(WINDOW_CONFIG_ROTATION,
+ winConfig1.diff(winConfig4, 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() {
+ 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 always on top state
+ winConfig2.setAlwaysOnTop(true);
+ assertNotEquals(config1.compareTo(config2), 0);
+ assertNotEquals(winConfig1.compareTo(winConfig2), 0);
+ winConfig2.setAlwaysOnTop(winConfig1.isAlwaysOnTop());
+
+ // Different bounds
+ winConfig2.setAppBounds(0, 2, 3, 4);
+ assertNotEquals(config1.compareTo(config2), 0);
+ assertNotEquals(winConfig1.compareTo(winConfig2), 0);
+ winConfig2.setAppBounds(winConfig1.getAppBounds());
+
+ // No bounds
+ assertEquals(config1.compareTo(blankConfig), -1);
+ assertEquals(winConfig1.compareTo(blankWinConfig), -1);
+
+ // Different rotation
+ winConfig2.setRotation(Surface.ROTATION_180);
+ assertNotEquals(config1.compareTo(config2), 0);
+ assertNotEquals(winConfig1.compareTo(winConfig2), 0);
+ winConfig2.setRotation(winConfig1.getRotation());
+
+ assertEquals(blankConfig.compareTo(config1), 1);
+ assertEquals(blankWinConfig.compareTo(winConfig1), 1);
+ }
+
+ @Test
+ public void testSetActivityType() {
+ 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() {
+ final DisplayInfo info = mDisplayContent.getDisplayInfo();
+ info.appWidth = 1024;
+ info.appHeight = 768;
+
+ final Rect appBounds = mWm.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() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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/wmtests/src/com/android/server/wm/WindowContainerControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerControllerTests.java
new file mode 100644
index 0000000..7592f1c
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerControllerTests.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.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;
+
+import android.content.res.Configuration;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Test class for {@link WindowContainerController}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:WindowContainerControllerTests
+ */
+@FlakyTest(bugId = 74078662)
+@SmallTest
+@Presubmit
+public class WindowContainerControllerTests extends WindowTestsBase {
+
+ @Test
+ public void testCreation() {
+ final WindowContainerController controller = new WindowContainerController<>(null, mWm);
+ final WindowContainer container = new WindowContainer(mWm);
+
+ container.setController(controller);
+ assertEquals(controller, container.getController());
+ assertEquals(controller.mContainer, container);
+ }
+
+ @Test
+ public void testSetContainer() {
+ final WindowContainerController controller = new WindowContainerController<>(null, mWm);
+ final WindowContainer container = new WindowContainer(mWm);
+
+ 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(mWm));
+ } 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() {
+ final WindowContainerController controller = new WindowContainerController<>(null, mWm);
+ final WindowContainer container = new WindowContainer(mWm);
+
+ controller.setContainer(container);
+ assertEquals(controller.mContainer, container);
+
+ controller.removeContainer();
+ assertNull(controller.mContainer);
+ }
+
+ @Test
+ public void testOnOverrideConfigurationChanged() {
+ final WindowContainerController controller = new WindowContainerController<>(null, mWm);
+ final WindowContainer container = new WindowContainer(mWm);
+
+ 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/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
new file mode 100644
index 0000000..a2e0ed9
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -0,0 +1,898 @@
+/*
+ * 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.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.dx.mockito.inline.extended.ExtendedMockito.any;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyFloat;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+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 android.content.res.Configuration;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+import java.util.Comparator;
+
+/**
+ * Test class for {@link WindowContainer}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:WindowContainerTests
+ */
+@SmallTest
+@Presubmit
+@FlakyTest(bugId = 74078662)
+public class WindowContainerTests extends WindowTestsBase {
+
+ @Test
+ public void testCreation() {
+ final TestWindowContainer w = new TestWindowContainerBuilder(mWm).setLayer(0).build();
+ assertNull("window must have no parent", w.getParentWindow());
+ assertEquals("window must have no children", 0, w.getChildrenCount());
+ }
+
+ @Test
+ public void testAdd() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ 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() {
+ try (MockSurfaceBuildingContainer top = new MockSurfaceBuildingContainer(mWm)) {
+
+ final SurfaceControl.Transaction transaction = mock(SurfaceControl.Transaction.class);
+ mWm.mTransactionFactory = () -> transaction;
+
+ WindowContainer child = new WindowContainer(mWm);
+ 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() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ 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() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ 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() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ 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() {
+ final WindowContainer container = new WindowContainer(mWm);
+ final WindowContainerController controller = new WindowContainerController<>(null, mWm);
+
+ container.setController(controller);
+ assertEquals(controller, container.getController());
+ assertEquals(container, controller.mContainer);
+
+ container.removeImmediately();
+ assertNull(container.getController());
+ assertNull(controller.mContainer);
+ }
+
+ @Test
+ public void testSetController() {
+ final WindowContainerController controller = new WindowContainerController<>(null, mWm);
+ final WindowContainer container = new WindowContainer(mWm);
+
+ 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, mWm));
+ } 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 testAddChildByIndex() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child = root.addChildWindow();
+
+ final TestWindowContainer child2 = builder.setLayer(1).build();
+ final TestWindowContainer child3 = builder.setLayer(2).build();
+ final TestWindowContainer child4 = builder.setLayer(3).build();
+
+ // Test adding at top.
+ root.addChild(child2, POSITION_TOP);
+ assertEquals(child2, root.getChildAt(root.getChildrenCount() - 1));
+
+ // Test adding at bottom.
+ root.addChild(child3, POSITION_BOTTOM);
+ assertEquals(child3, root.getChildAt(0));
+
+ // Test adding in the middle.
+ root.addChild(child4, 1);
+ assertEquals(child3, root.getChildAt(0));
+ assertEquals(child4, root.getChildAt(1));
+ assertEquals(child, root.getChildAt(2));
+ assertEquals(child2, root.getChildAt(3));
+ }
+
+ @Test
+ public void testPositionChildAt() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ 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() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ 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() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = 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() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ 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() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ 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(mWm);
+ 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() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ 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() {
+ 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(mWm);
+ 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() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ 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() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ 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() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ 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() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ 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() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ 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 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() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ 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() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ 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() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ 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() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+ 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 static 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 static final Comparator<TestWindowContainer> SUBLAYER_COMPARATOR = (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;
+ }
+ if (layer1 == layer2) return 0;
+ return 1;
+ };
+
+ TestWindowContainer(WindowManagerService wm, int layer, boolean isAnimating,
+ boolean isVisible, Integer orientation) {
+ super(wm);
+
+ 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, SUBLAYER_COMPARATOR);
+ return child;
+ }
+
+ TestWindowContainer addChildWindow(TestWindowContainerBuilder childBuilder) {
+ TestWindowContainer child = childBuilder.build();
+ addChild(child, SUBLAYER_COMPARATOR);
+ return child;
+ }
+
+ TestWindowContainer addChildWindow() {
+ return addChildWindow(new TestWindowContainerBuilder(mService).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 static class TestWindowContainerBuilder {
+ private final WindowManagerService mWm;
+ private int mLayer;
+ private boolean mIsAnimating;
+ private boolean mIsVisible;
+ private Integer mOrientation;
+
+ TestWindowContainerBuilder(WindowManagerService wm) {
+ mWm = wm;
+ mLayer = 0;
+ mIsAnimating = false;
+ mIsVisible = false;
+ mOrientation = null;
+ }
+
+ 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;
+ }
+
+ TestWindowContainer build() {
+ return new TestWindowContainer(mWm, mLayer, mIsAnimating, mIsVisible, mOrientation);
+ }
+ }
+
+ private static class MockSurfaceBuildingContainer extends WindowContainer<WindowContainer>
+ implements AutoCloseable {
+ private final SurfaceSession mSession = new SurfaceSession();
+
+ MockSurfaceBuildingContainer(WindowManagerService wm) {
+ super(wm);
+ }
+
+ static 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 void close() {
+ mSession.kill();
+ }
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java
new file mode 100644
index 0000000..4b666f5
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java
@@ -0,0 +1,64 @@
+/*
+ * 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 com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static org.mockito.Matchers.eq;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+import java.util.function.Consumer;
+
+/**
+ * Tests for {@link WindowContainer#forAllWindows} and various implementations.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:WindowContainerTraversalTests
+ */
+@SmallTest
+@Presubmit
+public class WindowContainerTraversalTests extends WindowTestsBase {
+
+ @Test
+ public void testDockedDividerPosition() {
+ 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");
+
+ mDisplayContent.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/wmtests/src/com/android/server/wm/WindowFrameTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowFrameTests.java
new file mode 100644
index 0000000..b3e90de
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowFrameTests.java
@@ -0,0 +1,522 @@
+/*
+ * 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.DisplayCutout.BOUNDS_POSITION_TOP;
+import static android.view.DisplayCutout.fromBoundingRect;
+import static android.view.WindowManager.LayoutParams.MATCH_PARENT;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+
+import static org.junit.Assert.assertEquals;
+
+import android.app.ActivityManager.TaskDescription;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.view.DisplayInfo;
+import android.view.Gravity;
+import android.view.IWindow;
+import android.view.WindowManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.utils.WmDisplayCutout;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for the {@link WindowState#computeFrameLw} method and other window frame machinery.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:WindowFrameTests
+ */
+@SmallTest
+@Presubmit
+public class WindowFrameTests extends WindowTestsBase {
+
+ private WindowToken mWindowToken;
+ private final IWindow mIWindow = new TestIWindow();
+ private final Rect mEmptyRect = new Rect();
+
+ static class WindowStateWithTask extends WindowState {
+ final Task mTask;
+ boolean mDockedResizingForTest = false;
+ WindowStateWithTask(WindowManagerService wm, IWindow iWindow, WindowToken windowToken,
+ WindowManager.LayoutParams attrs, Task t) {
+ super(wm, null, iWindow, windowToken, null, 0, 0, attrs, 0, 0,
+ false /* ownerCanAddInternalSystemWindow */);
+ mTask = t;
+ }
+
+ @Override
+ Task getTask() {
+ return mTask;
+ }
+
+ @Override
+ boolean isDockedResizing() {
+ return mDockedResizingForTest;
+ }
+ }
+
+ private static class TaskWithBounds extends Task {
+ final Rect mBounds;
+ final Rect mInsetBounds = new Rect();
+ boolean mFullscreenForTest = true;
+
+ TaskWithBounds(TaskStack stack, WindowManagerService wm, Rect bounds) {
+ super(0, stack, 0, wm, 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 {
+ mWindowToken = WindowTestUtils.createTestAppWindowToken(
+ mWm.getDefaultDisplayContentLocked());
+ mStubStack = new TaskStack(mWm, 0, null);
+ }
+
+ // Do not use this function directly in the tests below. Instead, use more explicit function
+ // such as assertFlame().
+ private 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);
+ }
+
+ private void assertContentInset(WindowState w, int left, int top, int right, int bottom) {
+ assertRect(w.getContentInsets(), left, top, right, bottom);
+ }
+
+ private void assertVisibleInset(WindowState w, int left, int top, int right, int bottom) {
+ assertRect(w.getVisibleInsets(), left, top, right, bottom);
+ }
+
+ private void assertStableInset(WindowState w, int left, int top, int right, int bottom) {
+ assertRect(w.getStableInsets(), left, top, right, bottom);
+ }
+
+ private void assertFrame(WindowState w, int left, int top, int right, int bottom) {
+ assertRect(w.getFrameLw(), left, top, right, bottom);
+ }
+
+ private void assertContentFrame(WindowState w, Rect expectedRect) {
+ assertRect(w.getContentFrameLw(), expectedRect.left, expectedRect.top, expectedRect.right,
+ expectedRect.bottom);
+ }
+
+ private void assertVisibleFrame(WindowState w, Rect expectedRect) {
+ assertRect(w.getVisibleFrameLw(), expectedRect.left, expectedRect.top, expectedRect.right,
+ expectedRect.bottom);
+ }
+
+ private void assertStableFrame(WindowState w, Rect expectedRect) {
+ assertRect(w.getStableFrameLw(), expectedRect.left, expectedRect.top, expectedRect.right,
+ expectedRect.bottom);
+ }
+
+ private void assertPolicyCrop(WindowStateWithTask w, int left, int top, int right, int bottom) {
+ Rect policyCrop = new Rect();
+ w.calculatePolicyCrop(policyCrop);
+ assertRect(policyCrop, left, top, right, bottom);
+ }
+
+ @Test
+ public void testLayoutInFullscreenTaskInsets() {
+ // fullscreen task doesn't use bounds for computeFrame
+ final Task task = new TaskWithBounds(mStubStack, mWm, null);
+ WindowState w = createWindow(task, MATCH_PARENT, MATCH_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.getWindowFrames().setFrames(pf, df, of, cf, vf, dcf, sf, mEmptyRect);
+ w.computeFrameLw();
+ assertFrame(w, 0, 0, 1000, 1000);
+ assertContentInset(w, 0, topContentInset, 0, bottomContentInset);
+ assertVisibleInset(w, 0, topVisibleInset, 0, bottomVisibleInset);
+ assertStableInset(w, leftStableInset, 0, rightStableInset, 0);
+ assertContentFrame(w, cf);
+ assertVisibleFrame(w, vf);
+ assertStableFrame(w, sf);
+ // On the other hand getFrame() 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();
+ assertFrame(w, 100, 100, 200, 200);
+ assertContentInset(w, 0, 0, 0, 0);
+ // In this case the frames are shrunk to the window frame.
+ assertContentFrame(w, w.getFrameLw());
+ assertVisibleFrame(w, w.getFrameLw());
+ assertStableFrame(w, w.getFrameLw());
+ }
+
+ @Test
+ public void testLayoutInFullscreenTaskNoInsets() {
+ // fullscreen task doesn't use bounds for computeFrame
+ final Task task = new TaskWithBounds(mStubStack, mWm, null);
+ WindowState w = createWindow(task, MATCH_PARENT, MATCH_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.getWindowFrames().setFrames(pf, pf, pf, pf, pf, pf, pf, pf);
+ w.computeFrameLw();
+ assertFrame(w, 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();
+ // Explicit width and height without requested width/height
+ // gets us nothing.
+ assertFrame(w, 0, 0, 0, 0);
+
+ w.mRequestedWidth = 300;
+ w.mRequestedHeight = 300;
+ w.computeFrameLw();
+ // With requestedWidth/Height we can freely choose our size within the
+ // parent bounds.
+ assertFrame(w, 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();
+ assertFrame(w, 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();
+ assertFrame(w, 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();
+ assertFrame(w, 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();
+ assertFrame(w, 700, 0, 1000, 300);
+ w.mAttrs.gravity = Gravity.RIGHT | Gravity.BOTTOM;
+ w.computeFrameLw();
+ assertFrame(w, 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();
+ assertFrame(w, 600, 600, 900, 900);
+ }
+
+ @Test
+ public void testLayoutNonfullscreenTask() {
+ final DisplayInfo displayInfo = mWm.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);
+ final TaskWithBounds task = new TaskWithBounds(mStubStack, mWm, taskBounds);
+ task.mFullscreenForTest = false;
+ WindowState w = createWindow(task, MATCH_PARENT, MATCH_PARENT);
+ w.mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
+
+ final Rect pf = new Rect(0, 0, logicalWidth, logicalHeight);
+ final WindowFrames windowFrames = w.getWindowFrames();
+ windowFrames.setFrames(pf, pf, pf, pf, pf, pf, pf, mEmptyRect);
+ w.computeFrameLw();
+ // For non fullscreen tasks the containing frame is based off the
+ // task bounds not the parent frame.
+ assertFrame(w, taskLeft, taskTop, taskRight, taskBottom);
+ assertContentFrame(w, taskBounds);
+ assertContentInset(w, 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);
+ windowFrames.setFrames(pf, pf, pf, cf, cf, pf, cf, mEmptyRect);
+ w.computeFrameLw();
+ assertFrame(w, taskLeft, taskTop, taskRight, taskBottom);
+ int contentInsetRight = taskRight - cfRight;
+ int contentInsetBottom = taskBottom - cfBottom;
+ assertContentInset(w, 0, 0, contentInsetRight, contentInsetBottom);
+ assertContentFrame(w, new Rect(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);
+ windowFrames.setFrames(pf, pf, pf, cf, cf, pf, cf, mEmptyRect);
+ w.computeFrameLw();
+ assertFrame(w, taskLeft, taskTop, taskRight, taskBottom);
+ contentInsetRight = insetRight - cfRight;
+ contentInsetBottom = insetBottom - cfBottom;
+ assertContentInset(w, 0, 0, contentInsetRight, contentInsetBottom);
+ assertContentFrame(w, new Rect(taskLeft, taskTop, taskRight - contentInsetRight,
+ taskBottom - contentInsetBottom));
+ }
+
+ @Test
+ public void testCalculatePolicyCrop() {
+ final WindowStateWithTask w = createWindow(
+ new TaskWithBounds(mStubStack, mWm, null), MATCH_PARENT, MATCH_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 = new Rect(cf);
+
+ final WindowFrames windowFrames = w.getWindowFrames();
+ windowFrames.setFrames(pf, df, of, cf, vf, dcf, sf, mEmptyRect);
+ w.computeFrameLw();
+ assertPolicyCrop(w, 0, cf.top, logicalWidth, cf.bottom);
+
+ windowFrames.mDecorFrame.setEmpty();
+ // Likewise with no decor frame we would get no crop
+ w.computeFrameLw();
+ assertPolicyCrop(w, 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.
+
+ windowFrames.setFrames(pf, pf, pf, pf, pf, pf, pf, pf);
+ 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();
+
+ // Normally the crop is shrunk from the decor frame
+ // to the computed window frame.
+ assertPolicyCrop(w, 0, 0, logicalWidth / 2, logicalHeight / 2);
+
+ w.mDockedResizingForTest = true;
+ // But if we are docked resizing it won't be, however we will still be
+ // shrunk to the decor frame and the display.
+ assertPolicyCrop(w, 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 = mWm.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);
+ final TaskWithBounds task = new TaskWithBounds(mStubStack, mWm, taskBounds);
+ task.mInsetBounds.set(taskLeft, taskTop, taskRight, taskBottom);
+ task.mFullscreenForTest = false;
+ WindowState w = createWindow(task, MATCH_PARENT, MATCH_PARENT);
+ w.mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
+
+ final Rect pf = new Rect(0, 0, logicalWidth, logicalHeight);
+ final WindowFrames windowFrames = w.getWindowFrames();
+ windowFrames.setFrames(pf, pf, pf, pf, pf, pf, pf, mEmptyRect);
+ w.computeFrameLw();
+ // For non fullscreen tasks the containing frame is based off the
+ // task bounds not the parent frame.
+ assertFrame(w, taskLeft, taskTop, taskRight, taskBottom);
+ assertContentFrame(w, taskBounds);
+ assertContentInset(w, 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;
+ windowFrames.setFrames(pf, pf, pf, cf, cf, pf, cf, mEmptyRect);
+ w.computeFrameLw();
+ assertFrame(w, cf.left, cf.top, cf.right, cf.bottom);
+ assertContentFrame(w, cf);
+ assertContentInset(w, 0, 0, 0, 0);
+ }
+
+ @Test
+ public void testDisplayCutout() {
+ // Regular fullscreen task and window
+ final Task task = new TaskWithBounds(mStubStack, mWm, null);
+ WindowState w = createWindow(task, MATCH_PARENT, MATCH_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, BOUNDS_POSITION_TOP),
+ pf.width(), pf.height());
+
+ final WindowFrames windowFrames = w.getWindowFrames();
+ windowFrames.setFrames(pf, pf, pf, pf, pf, pf, pf, pf);
+ windowFrames.setDisplayCutout(cutout);
+ w.computeFrameLw();
+
+ assertEquals(w.getWmDisplayCutout().getDisplayCutout().getSafeInsetTop(), 50);
+ assertEquals(w.getWmDisplayCutout().getDisplayCutout().getSafeInsetBottom(), 0);
+ assertEquals(w.getWmDisplayCutout().getDisplayCutout().getSafeInsetLeft(), 0);
+ assertEquals(w.getWmDisplayCutout().getDisplayCutout().getSafeInsetRight(), 0);
+ }
+
+ @Test
+ public void testDisplayCutout_tempInsetBounds() {
+ // Regular fullscreen task and window
+ final TaskWithBounds task = new TaskWithBounds(mStubStack, mWm,
+ new Rect(0, -500, 1000, 1500));
+ task.mFullscreenForTest = false;
+ task.mInsetBounds.set(0, 0, 1000, 2000);
+ WindowState w = createWindow(task, MATCH_PARENT, MATCH_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, BOUNDS_POSITION_TOP),
+ pf.width(), pf.height());
+
+ final WindowFrames windowFrames = w.getWindowFrames();
+ windowFrames.setFrames(pf, pf, pf, pf, pf, pf, pf, pf);
+ windowFrames.setDisplayCutout(cutout);
+ w.computeFrameLw();
+
+ assertEquals(w.getWmDisplayCutout().getDisplayCutout().getSafeInsetTop(), 50);
+ assertEquals(w.getWmDisplayCutout().getDisplayCutout().getSafeInsetBottom(), 0);
+ assertEquals(w.getWmDisplayCutout().getDisplayCutout().getSafeInsetLeft(), 0);
+ assertEquals(w.getWmDisplayCutout().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(mWm, mIWindow, mWindowToken, attrs, task);
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceRule.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceRule.java
new file mode 100644
index 0000000..4a99172
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceRule.java
@@ -0,0 +1,212 @@
+/*
+ * 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 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+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 android.app.ActivityManagerInternal;
+import android.content.Context;
+import android.hardware.display.DisplayManagerInternal;
+import android.os.PowerManagerInternal;
+import android.os.PowerSaveState;
+import android.view.Display;
+import android.view.InputChannel;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+
+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;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 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;
+ // Record all {@link SurfaceControl.Transaction} created while testing and releases native
+ // resources when test finishes.
+ private final List<WeakReference<Transaction>> mSurfaceTransactions = new ArrayList<>();
+ // Record all {@link SurfaceControl} created while testing and releases native resources when
+ // test finishes.
+ private final List<WeakReference<SurfaceControl>> mSurfaceControls = new ArrayList<>();
+
+ @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 = getInstrumentation().getTargetContext();
+
+ removeServices();
+
+ LocalServices.addService(DisplayManagerInternal.class,
+ mock(DisplayManagerInternal.class));
+
+ LocalServices.addService(PowerManagerInternal.class,
+ mock(PowerManagerInternal.class));
+ final PowerManagerInternal pm =
+ LocalServices.getService(PowerManagerInternal.class);
+ doNothing().when(pm).registerLowPowerModeObserver(any());
+ PowerSaveState state = new PowerSaveState.Builder().build();
+ doReturn(state).when(pm).getLowPowerState(anyInt());
+
+ LocalServices.addService(ActivityManagerInternal.class,
+ mock(ActivityManagerInternal.class));
+ LocalServices.addService(ActivityTaskManagerInternal.class,
+ mock(ActivityTaskManagerInternal.class));
+ final ActivityTaskManagerInternal atm =
+ LocalServices.getService(ActivityTaskManagerInternal.class);
+ doAnswer((InvocationOnMock invocationOnMock) -> {
+ final Runnable runnable = invocationOnMock.<Runnable>getArgument(0);
+ if (runnable != null) {
+ runnable.run();
+ }
+ return null;
+ }).when(atm).notifyKeyguardFlagsChanged(any(), anyInt());
+
+ 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(), anyInt());
+ }
+
+ mService = WindowManagerService.main(context, ims, false, false,
+ mPolicy = new TestWindowManagerPolicy(
+ WindowManagerServiceRule.this::getWindowManagerService),
+ new WindowManagerGlobalLock());
+ mService.mTransactionFactory = () -> {
+ final SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+ mSurfaceTransactions.add(new WeakReference<>(transaction));
+ return transaction;
+ };
+ mService.mSurfaceBuilderFactory = session -> new SurfaceControl.Builder(session) {
+ @Override
+ public SurfaceControl build() {
+ final SurfaceControl control = super.build();
+ mSurfaceControls.add(new WeakReference<>(control));
+ return control;
+ }
+ };
+
+ mService.onInitReady();
+
+ final Display display = mService.mDisplayManager.getDisplay(DEFAULT_DISPLAY);
+ final DisplayWindowController dcw = new DisplayWindowController(display, mService);
+ // Display creation is driven by the ActivityManagerService via
+ // ActivityStackSupervisor. We emulate those steps here.
+ mService.mRoot.createDisplayContent(display, dcw);
+ }
+
+ private void removeServices() {
+ LocalServices.removeServiceForTest(DisplayManagerInternal.class);
+ LocalServices.removeServiceForTest(PowerManagerInternal.class);
+ LocalServices.removeServiceForTest(ActivityManagerInternal.class);
+ LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class);
+ LocalServices.removeServiceForTest(WindowManagerInternal.class);
+ LocalServices.removeServiceForTest(WindowManagerPolicy.class);
+ }
+
+ private void tearDown() {
+ waitUntilWindowManagerHandlersIdle();
+ destroyAllSurfaceTransactions();
+ destroyAllSurfaceControls();
+ 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);
+ }
+ }
+
+ private void destroyAllSurfaceTransactions() {
+ for (final WeakReference<Transaction> reference : mSurfaceTransactions) {
+ final Transaction transaction = reference.get();
+ if (transaction != null) {
+ reference.clear();
+ transaction.close();
+ }
+ }
+ }
+
+ private void destroyAllSurfaceControls() {
+ for (final WeakReference<SurfaceControl> reference : mSurfaceControls) {
+ final SurfaceControl control = reference.get();
+ if (control != null) {
+ reference.clear();
+ control.destroy();
+ }
+ }
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceRuleTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceRuleTest.java
new file mode 100644
index 0000000..343d359
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceRuleTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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 androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+/**
+ * Build/InstallRun:
+ * atest FrameworksServicesTests:WindowManagerServiceRuleTest
+ */
+@Presubmit
+@SmallTest
+public class WindowManagerServiceRuleTest {
+
+ @Rule
+ public final WindowManagerServiceRule mRule = new WindowManagerServiceRule();
+
+ @Test
+ public void testWindowManagerSetUp() {
+ assertThat(mRule.getWindowManagerService(), notNullValue());
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
new file mode 100644
index 0000000..7f78034
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -0,0 +1,423 @@
+/*
+ * 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.hardware.camera2.params.OutputConfiguration.ROTATION_90;
+import static android.view.Surface.ROTATION_0;
+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 com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.reset;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+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.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+
+import android.graphics.Insets;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.util.Size;
+import android.view.DisplayCutout;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.utils.WmDisplayCutout;
+
+import org.junit.Test;
+
+import java.util.LinkedList;
+
+/**
+ * Tests for the {@link WindowState} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:WindowStateTests
+ */
+@FlakyTest(bugId = 74078662)
+@SmallTest
+@Presubmit
+public class WindowStateTests extends WindowTestsBase {
+
+ @Test
+ public void testIsParentWindowHidden() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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(sPowerManagerWrapper);
+ first.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ verify(sPowerManagerWrapper, never()).wakeUp(anyLong(), anyString());
+ assertTrue(appWindowToken.canTurnScreenOn());
+
+ reset(sPowerManagerWrapper);
+ second.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ verify(sPowerManagerWrapper).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(sPowerManagerWrapper);
+ first.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ verify(sPowerManagerWrapper).wakeUp(anyLong(), anyString());
+ assertFalse(appWindowToken.canTurnScreenOn());
+
+ reset(sPowerManagerWrapper);
+ second.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ verify(sPowerManagerWrapper, 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(sPowerManagerWrapper);
+ firstWindow.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ verify(sPowerManagerWrapper).wakeUp(anyLong(), anyString());
+
+ reset(sPowerManagerWrapper);
+ secondWindow.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ verify(sPowerManagerWrapper).wakeUp(anyLong(), anyString());
+ }
+
+ @Test
+ public void testCanAffectSystemUiFlags() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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()));
+ }
+
+ @Test
+ public void testSeamlesslyRotateWindow() {
+ final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+
+ app.mHasSurface = true;
+ app.mSurfaceControl = mock(SurfaceControl.class);
+ try {
+ app.getFrameLw().set(10, 20, 60, 80);
+ app.updateSurfacePosition(t);
+
+ app.seamlesslyRotateIfAllowed(t, ROTATION_0, ROTATION_90, true);
+
+ assertTrue(app.mSeamlesslyRotated);
+
+ // Verify we un-rotate the window state surface.
+ Matrix matrix = new Matrix();
+ // Un-rotate 90 deg
+ matrix.setRotate(270);
+ // Translate it back to origin
+ matrix.postTranslate(0, mDisplayInfo.logicalWidth);
+ verify(t).setMatrix(eq(app.mSurfaceControl), eq(matrix), any(float[].class));
+
+ // Verify we update the position as well.
+ float[] currentSurfacePos = {app.mLastSurfacePosition.x, app.mLastSurfacePosition.y};
+ matrix.mapPoints(currentSurfacePos);
+ verify(t).setPosition(eq(app.mSurfaceControl), eq(currentSurfacePos[0]),
+ eq(currentSurfacePos[1]));
+ } finally {
+ app.mSurfaceControl = null;
+ app.mHasSurface = false;
+ }
+ }
+
+ @Test
+ public void testDisplayCutoutIsCalculatedRelativeToFrame() {
+ final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ WindowFrames wf = app.getWindowFrames();
+ wf.mParentFrame.set(7, 10, 185, 380);
+ wf.mDisplayFrame.set(wf.mParentFrame);
+ final DisplayCutout cutout = new DisplayCutout(
+ Insets.of(0, 15, 0, 22) /* safeInset */,
+ null /* boundLeft */,
+ new Rect(95, 0, 105, 15),
+ null /* boundRight */,
+ new Rect(95, 378, 105, 400));
+ wf.setDisplayCutout(new WmDisplayCutout(cutout, new Size(200, 400)));
+
+ app.computeFrameLw();
+ assertThat(app.getWmDisplayCutout().getDisplayCutout(), is(cutout.inset(7, 10, 5, 20)));
+ }
+
+ private void testPrepareWindowToDisplayDuringRelayout(boolean wasVisible) {
+ reset(sPowerManagerWrapper);
+ final WindowState root = createWindow(null, TYPE_APPLICATION, "root");
+ root.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
+
+ root.prepareWindowToDisplayDuringRelayout(wasVisible /*wasVisible*/);
+ verify(sPowerManagerWrapper).wakeUp(anyLong(), anyString());
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestUtils.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestUtils.java
index 3c87721..3bd9a89 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestUtils.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestUtils.java
@@ -16,6 +16,9 @@
package com.android.server.wm;
+import static android.app.AppOpsManager.OP_NONE;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
+
import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyFloat;
@@ -23,12 +26,21 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static com.android.server.wm.WindowContainer.POSITION_TOP;
+import android.app.ActivityManager;
+import android.content.ComponentName;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
+import android.os.Binder;
+import android.os.IBinder;
import android.view.Display;
+import android.view.IApplicationToken;
+import android.view.IWindow;
import android.view.Surface;
+import android.view.SurfaceControl.Transaction;
+import android.view.WindowManager;
import org.mockito.invocation.InvocationOnMock;
@@ -37,6 +49,7 @@
* to WindowManager related test functionality.
*/
public class WindowTestUtils {
+ private static int sNextTaskId = 0;
/** An extension of {@link DisplayContent} to gain package scoped access. */
public static class TestDisplayContent extends DisplayContent {
@@ -54,6 +67,14 @@
return null;
}
+ /**
+ * Stubbing method of non-public parent class isn't supported, so here explicitly overrides.
+ */
+ @Override
+ DockedStackDividerController getDockedDividerController() {
+ return null;
+ }
+
/** Create a mocked default {@link DisplayContent}. */
public static TestDisplayContent create(Context context) {
final TestDisplayContent displayContent = mock(TestDisplayContent.class);
@@ -106,6 +127,17 @@
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.mGlobalLock) {
+ 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.
@@ -120,4 +152,233 @@
// Do nothing.
}
}
+
+ static TestAppWindowToken createTestAppWindowToken(DisplayContent dc) {
+ synchronized (dc.mService.mGlobalLock) {
+ 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 Transaction mPendingTransactionOverride;
+
+ private TestAppWindowToken(DisplayContent dc) {
+ super(dc.mService, new IApplicationToken.Stub() {
+ @Override
+ public String getName() {
+ return null;
+ }
+ }, new ComponentName("", ""), false, dc, true /* fillsParent */);
+ }
+
+ TestAppWindowToken(WindowManagerService service, IApplicationToken token,
+ ComponentName activityComponent, boolean voiceInteraction, DisplayContent dc,
+ long inputDispatchingTimeoutNanos, boolean fullscreen, boolean showForAllUsers,
+ int targetSdk, int orientation, int rotationAnimationHint, int configChanges,
+ boolean launchTaskBehind, boolean alwaysFocusable, ActivityRecord activityRecord) {
+ super(service, token, activityComponent, voiceInteraction, dc,
+ inputDispatchingTimeoutNanos, fullscreen, showForAllUsers, targetSdk,
+ orientation, rotationAnimationHint, configChanges, launchTaskBehind,
+ alwaysFocusable, activityRecord);
+ }
+
+ 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;
+ }
+
+ void setPendingTransaction(Transaction transaction) {
+ mPendingTransactionOverride = transaction;
+ }
+
+ @Override
+ public Transaction getPendingTransaction() {
+ return mPendingTransactionOverride == null
+ ? super.getPendingTransaction()
+ : mPendingTransactionOverride;
+ }
+ }
+
+ 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.mGlobalLock) {
+ 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 {
+
+ static final TaskWindowContainerListener NOP_LISTENER = 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) {
+ }
+ };
+
+ TestTaskWindowContainerController(WindowTestsBase testsBase) {
+ this(testsBase.createStackControllerOnDisplay(testsBase.mDisplayContent));
+ }
+
+ TestTaskWindowContainerController(StackWindowController stackController) {
+ super(sNextTaskId++, NOP_LISTENER, 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 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 mResizeReported;
+
+ 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();
+ mResizeReported = 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/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
new file mode 100644
index 0000000..b3f56e7
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -0,0 +1,400 @@
+/*
+ * 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.AppOpsManager.OP_NONE;
+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.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
+import static android.view.View.VISIBLE;
+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 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+
+import android.content.Context;
+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 android.view.IWindow;
+import android.view.WindowManager;
+
+import com.android.server.AttributeCache;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+
+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 mWm;
+ 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;
+
+ /** Non-default display. */
+ 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;
+
+ @Rule
+ public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
+ new DexmakerShareClassLoaderRule();
+
+ @Rule
+ public final WindowManagerServiceRule mWmRule = new WindowManagerServiceRule();
+
+ static WindowState.PowerManagerWrapper sPowerManagerWrapper; // TODO(roosa): make non-static.
+
+ @BeforeClass
+ public static void setUpOnceBase() {
+ AttributeCache.init(getInstrumentation().getTargetContext());
+ sPowerManagerWrapper = mock(WindowState.PowerManagerWrapper.class);
+ }
+
+ @Before
+ public void setUpBase() {
+ // 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);
+
+ final Context context = getInstrumentation().getTargetContext();
+
+ mWm = mWmRule.getWindowManagerService();
+ beforeCreateDisplay();
+
+ context.getDisplay().getDisplayInfo(mDisplayInfo);
+ mDisplayContent = createNewDisplay();
+ mWm.mDisplayEnabled = true;
+ mWm.mDisplayReady = true;
+
+ // Set-up some common windows.
+ mCommonWindows = new HashSet<>();
+ synchronized (mWm.mGlobalLock) {
+ mWallpaperWindow = createCommonWindow(null, TYPE_WALLPAPER, "wallpaperWindow");
+ mImeWindow = createCommonWindow(null, TYPE_INPUT_METHOD, "mImeWindow");
+ mDisplayContent.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 tearDownBase() {
+ // 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 {
+ // Test may schedule to perform surface placement or other messages. Wait until a
+ // stable state to clean up for consistency.
+ waitUntilHandlersIdle();
+
+ final LinkedList<WindowState> nonCommonWindows = new LinkedList<>();
+
+ synchronized (mWm.mGlobalLock) {
+ mWm.mRoot.forAllWindows(w -> {
+ if (!mCommonWindows.contains(w)) {
+ nonCommonWindows.addLast(w);
+ }
+ }, true /* traverseTopToBottom */);
+
+ while (!nonCommonWindows.isEmpty()) {
+ nonCommonWindows.pollLast().removeImmediately();
+ }
+
+ for (int i = mWm.mRoot.mChildren.size() - 1; i >= 0; --i) {
+ final DisplayContent displayContent = mWm.mRoot.mChildren.get(i);
+ if (!displayContent.isDefaultDisplay) {
+ displayContent.removeImmediately();
+ }
+ }
+ // Remove app transition & window freeze timeout callbacks to prevent unnecessary
+ // actions after test.
+ mWm.getDefaultDisplayContentLocked().mAppTransition
+ .removeAppTransitionTimeoutCallbacks();
+ mWm.mH.removeMessages(WindowManagerService.H.WINDOW_FREEZE_TIMEOUT);
+ mDisplayContent.mInputMethodTarget = null;
+ }
+
+ // Wait until everything is really cleaned up.
+ waitUntilHandlersIdle();
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to tear down test", e);
+ throw e;
+ }
+ }
+
+ private WindowState createCommonWindow(WindowState parent, int type, String name) {
+ synchronized (mWm.mGlobalLock) {
+ 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;
+ }
+ }
+
+ /**
+ * 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 (mWm.mGlobalLock) {
+ 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) {
+ return createTestAppWindowToken(dc, windowingMode, activityType);
+ }
+
+ WindowTestUtils.TestAppWindowToken createTestAppWindowToken(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 (mWm.mGlobalLock) {
+ 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 (mWm.mGlobalLock) {
+ final WindowToken token = createWindowToken(dc, windowingMode, activityType, type);
+ return createWindow(parent, type, token, name);
+ }
+ }
+
+ WindowState createAppWindow(Task task, int type, String name) {
+ synchronized (mWm.mGlobalLock) {
+ 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 (mWm.mGlobalLock) {
+ 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 (mWm.mGlobalLock) {
+ 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 (mWm.mGlobalLock) {
+ 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,
+ mWm, 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.mGlobalLock) {
+ 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,
+ sPowerManagerWrapper);
+ // 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 (mWm.mGlobalLock) {
+ return createStackControllerOnDisplay(dc).mContainer;
+ }
+ }
+
+ StackWindowController createStackControllerOnDisplay(DisplayContent dc) {
+ synchronized (mWm.mGlobalLock) {
+ return createStackControllerOnStackOnDisplay(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, dc);
+ }
+ }
+
+ StackWindowController createStackControllerOnStackOnDisplay(
+ int windowingMode, int activityType, DisplayContent dc) {
+ synchronized (mWm.mGlobalLock) {
+ 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(), mWm);
+ 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(mWm, stack, userId);
+ }
+
+ /** Creates a {@link DisplayContent} and adds it to the system. */
+ DisplayContent createNewDisplay() {
+ return createNewDisplay(mDisplayInfo);
+ }
+
+ /** Creates a {@link DisplayContent} and adds it to the system. */
+ DisplayContent createNewDisplay(DisplayInfo displayInfo) {
+ final int displayId = sNextDisplayId++;
+ final Display display = new Display(DisplayManagerGlobal.getInstance(), displayId,
+ displayInfo, DEFAULT_DISPLAY_ADJUSTMENTS);
+ synchronized (mWm.mGlobalLock) {
+ return new DisplayContent(display, mWm, mock(DisplayWindowController.class));
+ }
+ }
+
+ /**
+ * Creates a {@link DisplayContent} with given display state and adds it to the system.
+ *
+ * Unlike {@link #createNewDisplay()} that uses a mock {@link DisplayWindowController} to
+ * initialize {@link DisplayContent}, this method used real controller object when the test
+ * need to verify its related flows.
+ *
+ * @param displayState For initializing the state of the display. See
+ * {@link Display#getState()}.
+ */
+ DisplayContent createNewDisplayWithController(int displayState) {
+ // Leverage main display info & initialize it with display state for given displayId.
+ DisplayInfo displayInfo = new DisplayInfo();
+ displayInfo.copyFrom(mDisplayInfo);
+ displayInfo.state = displayState;
+ final int displayId = sNextDisplayId++;
+ final Display display = new Display(DisplayManagerGlobal.getInstance(), displayId,
+ displayInfo, DEFAULT_DISPLAY_ADJUSTMENTS);
+ final DisplayWindowController dcw = new DisplayWindowController(display, mWm);
+ synchronized (mWm.mGlobalLock) {
+ // Display creation is driven by DisplayWindowController via ActivityStackSupervisor.
+ // We skip those steps here.
+ return mWm.mRoot.createDisplayContent(display, dcw);
+ }
+ }
+
+ /** Creates a {@link com.android.server.wm.WindowTestUtils.TestWindowState} */
+ WindowTestUtils.TestWindowState createWindowState(WindowManager.LayoutParams attrs,
+ WindowToken token) {
+ synchronized (mWm.mGlobalLock) {
+ return new WindowTestUtils.TestWindowState(mWm, mMockSession, mIWindow, attrs, token);
+ }
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java
new file mode 100644
index 0000000..3048f1a
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java
@@ -0,0 +1,132 @@
+/*
+ * 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_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 android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Tests for the {@link WindowToken} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:WindowTokenTests
+ */
+@FlakyTest(bugId = 74078662)
+@SmallTest
+@Presubmit
+public class WindowTokenTests extends WindowTestsBase {
+
+ @Test
+ public void testAddWindow() {
+ 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() {
+ 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() {
+ 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/wmtests/src/com/android/server/wm/ZOrderingTests.java b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
new file mode 100644
index 0000000..3dcea75
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
@@ -0,0 +1,422 @@
+/*
+ * 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_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 static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+
+/**
+ * Tests for the {@link DisplayContent#assignChildLayers(SurfaceControl.Transaction)} method.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:ZOrderingTests
+ */
+@FlakyTest(bugId = 74078662)
+@SmallTest
+@Presubmit
+public class ZOrderingTests extends WindowTestsBase {
+
+ private static class LayerRecordingTransaction extends SurfaceControl.Transaction {
+ // 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 final HashMap<SurfaceControl, SurfaceControl> mParentFor = new HashMap<>();
+ 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);
+ }
+
+ void addParentFor(SurfaceControl child, SurfaceControl parent) {
+ mParentFor.put(child, parent);
+ }
+
+ SurfaceControl getParentFor(SurfaceControl child) {
+ return mParentFor.get(child);
+ }
+
+ @Override
+ public void close() {
+
+ }
+ }
+
+ private static class HierarchyRecorder extends SurfaceControl.Builder {
+ private LayerRecordingTransaction mTransaction;
+ private SurfaceControl mPendingParent;
+
+ HierarchyRecorder(SurfaceSession s, LayerRecordingTransaction transaction) {
+ super(s);
+ mTransaction = transaction;
+ }
+
+ @Override
+ public SurfaceControl.Builder setParent(SurfaceControl sc) {
+ mPendingParent = sc;
+ return super.setParent(sc);
+ }
+
+ @Override
+ public SurfaceControl build() {
+ final SurfaceControl sc = super.build();
+ mTransaction.addParentFor(sc, mPendingParent);
+ mPendingParent = null;
+ return sc;
+ }
+ }
+
+ private static class HierarchyRecordingBuilderFactory implements SurfaceBuilderFactory {
+ private LayerRecordingTransaction mTransaction;
+
+ HierarchyRecordingBuilderFactory(LayerRecordingTransaction transaction) {
+ mTransaction = transaction;
+ }
+
+ @Override
+ public SurfaceControl.Builder make(SurfaceSession s) {
+ final LayerRecordingTransaction transaction = mTransaction;
+ return new HierarchyRecorder(s, transaction);
+ }
+ }
+
+ 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();
+ mWm.mSurfaceBuilderFactory = new HierarchyRecordingBuilderFactory(mTransaction);
+ mWm.mTransactionFactory = () -> mTransaction;
+ }
+
+ @After
+ public void tearDown() {
+ mTransaction.close();
+ }
+
+ private static 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 = t.getParentFor(current);
+ }
+ } while (current != null);
+ return p;
+ }
+
+
+ private static void assertZOrderGreaterThan(LayerRecordingTransaction t, SurfaceControl left,
+ SurfaceControl right) {
+ final LinkedList<SurfaceControl> leftParentChain = getAncestors(t, left);
+ final LinkedList<SurfaceControl> rightParentChain = getAncestors(t, right);
+
+ SurfaceControl leftTop = leftParentChain.peekLast();
+ SurfaceControl rightTop = rightParentChain.peekLast();
+ while (leftTop != null && rightTop != null && leftTop == rightTop) {
+ leftParentChain.removeLast();
+ rightParentChain.removeLast();
+ leftTop = leftParentChain.peekLast();
+ rightTop = rightParentChain.peekLast();
+ }
+
+ if (rightTop == null) { // right is the parent of left.
+ assertThat(t.getLayer(leftTop)).isGreaterThan(0);
+ } else if (leftTop == null) { // left is the parent of right.
+ assertThat(t.getLayer(rightTop)).isLessThan(0);
+ } else {
+ assertThat(t.getLayer(leftTop)).isGreaterThan(t.getLayer(rightTop));
+ }
+ }
+
+ void assertWindowHigher(WindowState left, WindowState right) {
+ assertZOrderGreaterThan(mTransaction, left.getSurfaceControl(), right.getSurfaceControl());
+ }
+
+ WindowState createWindow(String name) {
+ return createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, name);
+ }
+
+ @Test
+ public void testAssignWindowLayers_ForImeWithNoTarget() {
+ mDisplayContent.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() {
+ final WindowState imeAppTarget = createWindow("imeAppTarget");
+ mDisplayContent.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() {
+ 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");
+
+ mDisplayContent.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() {
+ final WindowState appBelowImeTarget = createWindow("appBelowImeTarget");
+ final WindowState imeAppTarget = createWindow("imeAppTarget");
+ final WindowState appAboveImeTarget = createWindow("appAboveImeTarget");
+
+ mDisplayContent.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() {
+ final WindowState imeSystemOverlayTarget = createWindow(null, TYPE_SYSTEM_OVERLAY,
+ mDisplayContent, "imeSystemOverlayTarget",
+ true /* ownerCanAddInternalSystemWindow */);
+
+ mDisplayContent.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() {
+ mDisplayContent.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() {
+ 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() {
+ 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() {
+ // 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() {
+ 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);
+ }
+}