Revert "DO NOT MERGE: Move AM/WM unit tests out of FrameworksServicesTests to WmTests"
This reverts commit dee5a4dc2a5a6829e5509fc0e94903e30dcc238f.
Reason for revert: CLs on upstream branch are on hold.
Change-Id: I253294d080efdfd1dbf377812f12880e03dff1f6
diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityLaunchParamsModifierTests.java b/services/tests/servicestests/src/com/android/server/am/ActivityLaunchParamsModifierTests.java
new file mode 100644
index 0000000..f741c70
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityLaunchParamsModifierTests.java
@@ -0,0 +1,143 @@
+/*
+ * 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.am;
+
+import android.app.ActivityOptions;
+import android.content.pm.ActivityInfo;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.server.am.LaunchParamsController.LaunchParams;
+import org.junit.runner.RunWith;
+import org.junit.Before;
+import org.junit.Test;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static com.android.server.am.LaunchParamsController.LaunchParamsModifier.RESULT_DONE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.doAnswer;
+
+import static com.android.server.am.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP;
+
+/**
+ * Tests for exercising resizing bounds due to activity options.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:ActivityLaunchParamsModifierTests
+ */
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class ActivityLaunchParamsModifierTests extends ActivityTestsBase {
+ private ActivityLaunchParamsModifier mModifier;
+ private ActivityManagerService mService;
+ private ActivityStack mStack;
+ private TaskRecord mTask;
+ private ActivityRecord mActivity;
+
+ private LaunchParams mCurrent;
+ private LaunchParams mResult;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mService = createActivityManagerService();
+ mModifier = new ActivityLaunchParamsModifier(mService.mStackSupervisor);
+ mCurrent = new LaunchParams();
+ mResult = new LaunchParams();
+
+
+ mStack = mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ mTask = new TaskBuilder(mService.mStackSupervisor).setStack(mStack).build();
+ mActivity = new ActivityBuilder(mService).setTask(mTask).build();
+ }
+
+
+ @Test
+ public void testSkippedInvocations() throws Exception {
+ // No specified activity should be ignored
+ assertEquals(RESULT_SKIP, mModifier.onCalculate(null /*task*/, null /*layout*/,
+ null /*activity*/, null /*source*/, null /*options*/, mCurrent, mResult));
+
+ // No specified activity options should be ignored
+ assertEquals(RESULT_SKIP, mModifier.onCalculate(null /*task*/, null /*layout*/,
+ mActivity, null /*source*/, null /*options*/, mCurrent, mResult));
+
+ // launch bounds specified should be ignored.
+ final ActivityOptions options = ActivityOptions.makeBasic();
+ assertEquals(RESULT_SKIP, mModifier.onCalculate(null /*task*/, null /*layout*/,
+ mActivity, null /*source*/, options /*options*/, mCurrent, mResult));
+
+ // Non-resizeable records should be ignored
+ mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
+ assertFalse(mActivity.isResizeable());
+ assertEquals(RESULT_SKIP, mModifier.onCalculate(null /*task*/, null /*layout*/,
+ mActivity, null /*source*/, options /*options*/, mCurrent, mResult));
+
+ // make record resizeable
+ mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_RESIZEABLE;
+ assertTrue(mActivity.isResizeable());
+
+ assertEquals(RESULT_SKIP, mModifier.onCalculate(null /*task*/, null /*layout*/,
+ mActivity, null /*source*/, options /*options*/, mCurrent, mResult));
+
+ // Does not support freeform
+ mService.mSupportsFreeformWindowManagement = false;
+ assertFalse(mService.mStackSupervisor.canUseActivityOptionsLaunchBounds(options));
+ assertEquals(RESULT_SKIP, mModifier.onCalculate(null /*task*/, null /*layout*/,
+ mActivity, null /*source*/, options /*options*/, mCurrent, mResult));
+
+ mService.mSupportsFreeformWindowManagement = true;
+ options.setLaunchBounds(new Rect());
+ assertTrue(mService.mStackSupervisor.canUseActivityOptionsLaunchBounds(options));
+
+ // Invalid bounds
+ assertEquals(RESULT_SKIP, mModifier.onCalculate(null /*task*/, null /*layout*/,
+ mActivity, null /*source*/, options /*options*/, mCurrent, mResult));
+ options.setLaunchBounds(new Rect(0, 0, -1, -1));
+ assertEquals(RESULT_SKIP, mModifier.onCalculate(null /*task*/, null /*layout*/,
+ mActivity, null /*source*/, options /*options*/, mCurrent, mResult));
+
+ // Valid bounds should cause the positioner to be applied.
+ options.setLaunchBounds(new Rect(0, 0, 100, 100));
+ assertEquals(RESULT_DONE, mModifier.onCalculate(null /*task*/, null /*layout*/,
+ mActivity, null /*source*/, options /*options*/, mCurrent, mResult));
+ }
+
+ @Test
+ public void testBoundsExtraction() throws Exception {
+ // Make activity resizeable and enable freeform mode.
+ mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_RESIZEABLE;
+ mService.mSupportsFreeformWindowManagement = true;
+
+ ActivityOptions options = ActivityOptions.makeBasic();
+ final Rect proposedBounds = new Rect(20, 30, 45, 40);
+ options.setLaunchBounds(proposedBounds);
+
+ assertEquals(RESULT_DONE, mModifier.onCalculate(null /*task*/, null /*layout*/,
+ mActivity, null /*source*/, options /*options*/, mCurrent, mResult));
+ assertEquals(mResult.mBounds, proposedBounds);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerInternalTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerInternalTest.java
new file mode 100644
index 0000000..bce87dc
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerInternalTest.java
@@ -0,0 +1,214 @@
+/*
+ * 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.am;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.app.ActivityManagerInternal;
+import android.os.SystemClock;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Test class for {@link ActivityManagerInternal}.
+ *
+ * To run the tests, use
+ *
+ * runtest -c com.android.server.am.ActivityManagerInternalTest frameworks-services
+ *
+ * or the following steps:
+ *
+ * Build: m FrameworksServicesTests
+ * Install: adb install -r \
+ * ${ANDROID_PRODUCT_OUT}/data/app/FrameworksServicesTests/FrameworksServicesTests.apk
+ * Run: adb shell am instrument -e class com.android.server.am.ActivityManagerInternalTest -w \
+ * com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
+ */
+@RunWith(AndroidJUnit4.class)
+public class ActivityManagerInternalTest {
+ private static final int TEST_UID1 = 111;
+ private static final int TEST_UID2 = 112;
+
+ private static final long TEST_PROC_STATE_SEQ1 = 1111;
+ private static final long TEST_PROC_STATE_SEQ2 = 1112;
+ private static final long TEST_PROC_STATE_SEQ3 = 1113;
+
+ @Mock private ActivityManagerService.Injector mMockInjector;
+
+ private ActivityManagerService mAms;
+ private ActivityManagerInternal mAmi;
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mAms = new ActivityManagerService(mMockInjector);
+ mAmi = mAms.new LocalService();
+ }
+
+ @MediumTest
+ @Test
+ public void testNotifyNetworkPolicyRulesUpdated() throws Exception {
+ // Check there is no crash when there are no active uid records.
+ mAmi.notifyNetworkPolicyRulesUpdated(TEST_UID1, TEST_PROC_STATE_SEQ1);
+
+ // Notify that network policy rules are updated for TEST_UID1 and verify that
+ // UidRecord.lastNetworkUpdateProcStateSeq is updated and any blocked threads are notified.
+ verifyNetworkUpdatedProcStateSeq(
+ TEST_PROC_STATE_SEQ2, // curProcStateSeq
+ TEST_PROC_STATE_SEQ1, // lastNetworkUpdateProcStateSeq
+ TEST_PROC_STATE_SEQ2, // procStateSeq to notify
+ true); // expectNotify
+
+ // Notify that network policy rules are updated for TEST_UID1 with already handled
+ // procStateSeq and verify that there is no notify call.
+ verifyNetworkUpdatedProcStateSeq(
+ TEST_PROC_STATE_SEQ1, // curProcStateSeq
+ TEST_PROC_STATE_SEQ1, // lastNetworkUpdateProcStateSeq
+ TEST_PROC_STATE_SEQ1, // procStateSeq to notify
+ false); // expectNotify
+
+ // Notify that network policy rules are updated for TEST_UID1 with procStateSeq older
+ // than it's UidRecord.curProcStateSeq and verify that there is no notify call.
+ verifyNetworkUpdatedProcStateSeq(
+ TEST_PROC_STATE_SEQ3, // curProcStateSeq
+ TEST_PROC_STATE_SEQ1, // lastNetworkUpdateProcStateSeq
+ TEST_PROC_STATE_SEQ2, // procStateSeq to notify
+ false); // expectNotify
+ }
+
+ private void verifyNetworkUpdatedProcStateSeq(long curProcStateSeq,
+ long lastNetworkUpdatedProcStateSeq, long expectedProcStateSeq, boolean expectNotify)
+ throws Exception {
+ final UidRecord record1 = addActiveUidRecord(TEST_UID1, curProcStateSeq,
+ lastNetworkUpdatedProcStateSeq);
+ final UidRecord record2 = addActiveUidRecord(TEST_UID2, curProcStateSeq,
+ lastNetworkUpdatedProcStateSeq);
+
+ final CustomThread thread1 = new CustomThread(record1.networkStateLock);
+ thread1.startAndWait("Unexpected state for " + record1);
+ final CustomThread thread2 = new CustomThread(record2.networkStateLock);
+ thread2.startAndWait("Unexpected state for " + record2);
+
+ mAmi.notifyNetworkPolicyRulesUpdated(TEST_UID1, expectedProcStateSeq);
+ assertEquals(record1 + " should be updated",
+ expectedProcStateSeq, record1.lastNetworkUpdatedProcStateSeq);
+ assertEquals(record2 + " should not be updated",
+ lastNetworkUpdatedProcStateSeq, record2.lastNetworkUpdatedProcStateSeq);
+
+ if (expectNotify) {
+ thread1.assertTerminated("Unexpected state for " + record1);
+ assertTrue("Threads waiting for network should be notified: " + record1,
+ thread1.mNotified);
+ } else {
+ thread1.assertWaiting("Unexpected state for " + record1);
+ thread1.interrupt();
+ }
+ thread2.assertWaiting("Unexpected state for " + record2);
+ thread2.interrupt();
+
+ mAms.mActiveUids.clear();
+ }
+
+ private UidRecord addActiveUidRecord(int uid, long curProcStateSeq,
+ long lastNetworkUpdatedProcStateSeq) {
+ final UidRecord record = new UidRecord(uid);
+ record.lastNetworkUpdatedProcStateSeq = lastNetworkUpdatedProcStateSeq;
+ record.curProcStateSeq = curProcStateSeq;
+ record.waitingForNetwork = true;
+ mAms.mActiveUids.put(uid, record);
+ return record;
+ }
+
+ static class CustomThread extends Thread {
+ private static final long WAIT_TIMEOUT_MS = 1000;
+ private static final long WAIT_INTERVAL_MS = 100;
+
+ private final Object mLock;
+ private Runnable mRunnable;
+ boolean mNotified;
+
+ public CustomThread(Object lock) {
+ mLock = lock;
+ }
+
+ public CustomThread(Object lock, Runnable runnable) {
+ super(runnable);
+ mLock = lock;
+ mRunnable = runnable;
+ }
+
+ @Override
+ public void run() {
+ if (mRunnable != null) {
+ mRunnable.run();
+ } else {
+ synchronized (mLock) {
+ try {
+ mLock.wait();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupted();
+ }
+ }
+ }
+ mNotified = !Thread.interrupted();
+ }
+
+ public void startAndWait(String errMsg) throws Exception {
+ startAndWait(errMsg, false);
+ }
+
+ public void startAndWait(String errMsg, boolean timedWaiting) throws Exception {
+ start();
+ final long endTime = SystemClock.elapsedRealtime() + WAIT_TIMEOUT_MS;
+ final Thread.State stateToReach = timedWaiting
+ ? Thread.State.TIMED_WAITING : Thread.State.WAITING;
+ while (getState() != stateToReach
+ && SystemClock.elapsedRealtime() < endTime) {
+ Thread.sleep(WAIT_INTERVAL_MS);
+ }
+ if (timedWaiting) {
+ assertTimedWaiting(errMsg);
+ } else {
+ assertWaiting(errMsg);
+ }
+ }
+
+ public void assertWaiting(String errMsg) {
+ assertEquals(errMsg, Thread.State.WAITING, getState());
+ }
+
+ public void assertTimedWaiting(String errMsg) {
+ assertEquals(errMsg, Thread.State.TIMED_WAITING, getState());
+ }
+
+ public void assertTerminated(String errMsg) throws Exception {
+ final long endTime = SystemClock.elapsedRealtime() + WAIT_TIMEOUT_MS;
+ while (getState() != Thread.State.TERMINATED
+ && SystemClock.elapsedRealtime() < endTime) {
+ Thread.sleep(WAIT_INTERVAL_MS);
+ }
+ assertEquals(errMsg, Thread.State.TERMINATED, getState());
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java
new file mode 100644
index 0000000..c70d1e1
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java
@@ -0,0 +1,833 @@
+/*
+ * 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.am;
+
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_CACHED_ACTIVITY;
+import static android.app.ActivityManager.PROCESS_STATE_CACHED_EMPTY;
+import static android.app.ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND;
+import static android.app.ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND;
+import static android.app.ActivityManager.PROCESS_STATE_LAST_ACTIVITY;
+import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT;
+import static android.app.ActivityManager.PROCESS_STATE_RECEIVER;
+import static android.app.ActivityManager.PROCESS_STATE_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_TOP;
+import static android.util.DebugUtils.valueToString;
+import static com.android.server.am.ActivityManagerInternalTest.CustomThread;
+import static com.android.server.am.ActivityManagerService.DISPATCH_UIDS_CHANGED_UI_MSG;
+import static com.android.server.am.ActivityManagerService.Injector;
+import static com.android.server.am.ActivityManagerService.NETWORK_STATE_BLOCK;
+import static com.android.server.am.ActivityManagerService.NETWORK_STATE_NO_CHANGE;
+import static com.android.server.am.ActivityManagerService.NETWORK_STATE_UNBLOCK;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
+import android.app.IApplicationThread;
+import android.app.IUidObserver;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.internal.os.BatteryStatsImpl;
+import com.android.server.AppOpsService;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * Test class for {@link ActivityManagerService}.
+ *
+ * To run the tests, use
+ *
+ * runtest -c com.android.server.am.ActivityManagerServiceTest frameworks-services
+ *
+ * or the following steps:
+ *
+ * Build: m FrameworksServicesTests
+ * Install: adb install -r \
+ * ${ANDROID_PRODUCT_OUT}/data/app/FrameworksServicesTests/FrameworksServicesTests.apk
+ * Run: adb shell am instrument -e class com.android.server.am.ActivityManagerServiceTest -w \
+ * com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActivityManagerServiceTest {
+ private static final String TAG = ActivityManagerServiceTest.class.getSimpleName();
+
+ private static final int TEST_UID = 11111;
+
+ private static final long TEST_PROC_STATE_SEQ1 = 555;
+ private static final long TEST_PROC_STATE_SEQ2 = 556;
+
+ private static final int[] UID_RECORD_CHANGES = {
+ UidRecord.CHANGE_PROCSTATE,
+ UidRecord.CHANGE_GONE,
+ UidRecord.CHANGE_GONE | UidRecord.CHANGE_IDLE,
+ UidRecord.CHANGE_IDLE,
+ UidRecord.CHANGE_ACTIVE
+ };
+
+ @Mock private Context mContext;
+ @Mock private AppOpsService mAppOpsService;
+ @Mock private PackageManager mPackageManager;
+ @Mock private BatteryStatsImpl mBatteryStatsImpl;
+
+ private TestInjector mInjector;
+ private ActivityManagerService mAms;
+ private HandlerThread mHandlerThread;
+ private TestHandler mHandler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mHandlerThread = new HandlerThread(TAG);
+ mHandlerThread.start();
+ mHandler = new TestHandler(mHandlerThread.getLooper());
+ mInjector = new TestInjector();
+ mAms = new ActivityManagerService(mInjector);
+ mAms.mWaitForNetworkTimeoutMs = 2000;
+
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ }
+
+ @After
+ public void tearDown() {
+ mHandlerThread.quit();
+ }
+
+ @MediumTest
+ @Test
+ public void incrementProcStateSeqAndNotifyAppsLocked() throws Exception {
+
+ final UidRecord uidRec = addUidRecord(TEST_UID);
+ addUidRecord(TEST_UID + 1);
+
+ // Uid state is not moving from background to foreground or vice versa.
+ verifySeqCounterAndInteractions(uidRec,
+ PROCESS_STATE_TOP, // prevState
+ PROCESS_STATE_TOP, // curState
+ 0, // expectedGlobalCounter
+ 0, // exptectedCurProcStateSeq
+ NETWORK_STATE_NO_CHANGE, // expectedBlockState
+ false); // expectNotify
+
+ // Uid state is moving from foreground to background.
+ verifySeqCounterAndInteractions(uidRec,
+ PROCESS_STATE_FOREGROUND_SERVICE, // prevState
+ PROCESS_STATE_SERVICE, // curState
+ 1, // expectedGlobalCounter
+ 1, // exptectedCurProcStateSeq
+ NETWORK_STATE_UNBLOCK, // expectedBlockState
+ true); // expectNotify
+
+ // Explicitly setting the seq counter for more verification.
+ mAms.mProcStateSeqCounter = 42;
+
+ // Uid state is not moving from background to foreground or vice versa.
+ verifySeqCounterAndInteractions(uidRec,
+ PROCESS_STATE_IMPORTANT_BACKGROUND, // prevState
+ PROCESS_STATE_IMPORTANT_FOREGROUND, // curState
+ 42, // expectedGlobalCounter
+ 1, // exptectedCurProcStateSeq
+ NETWORK_STATE_NO_CHANGE, // expectedBlockState
+ false); // expectNotify
+
+ // Uid state is moving from background to foreground.
+ verifySeqCounterAndInteractions(uidRec,
+ PROCESS_STATE_LAST_ACTIVITY, // prevState
+ PROCESS_STATE_TOP, // curState
+ 43, // expectedGlobalCounter
+ 43, // exptectedCurProcStateSeq
+ NETWORK_STATE_BLOCK, // expectedBlockState
+ false); // expectNotify
+
+ // verify waiting threads are not notified.
+ uidRec.waitingForNetwork = false;
+ // Uid state is moving from foreground to background.
+ verifySeqCounterAndInteractions(uidRec,
+ PROCESS_STATE_FOREGROUND_SERVICE, // prevState
+ PROCESS_STATE_SERVICE, // curState
+ 44, // expectedGlobalCounter
+ 44, // exptectedCurProcStateSeq
+ NETWORK_STATE_UNBLOCK, // expectedBlockState
+ false); // expectNotify
+
+ // Verify when uid is not restricted, procStateSeq is not incremented.
+ uidRec.waitingForNetwork = true;
+ mInjector.setNetworkRestrictedForUid(false);
+ verifySeqCounterAndInteractions(uidRec,
+ PROCESS_STATE_IMPORTANT_BACKGROUND, // prevState
+ PROCESS_STATE_TOP, // curState
+ 44, // expectedGlobalCounter
+ 44, // exptectedCurProcStateSeq
+ -1, // expectedBlockState, -1 to verify there are no interactions with main thread.
+ false); // expectNotify
+
+ // Verify when waitForNetworkTimeout is 0, then procStateSeq is not incremented.
+ mAms.mWaitForNetworkTimeoutMs = 0;
+ mInjector.setNetworkRestrictedForUid(true);
+ verifySeqCounterAndInteractions(uidRec,
+ PROCESS_STATE_TOP, // prevState
+ PROCESS_STATE_IMPORTANT_BACKGROUND, // curState
+ 44, // expectedGlobalCounter
+ 44, // exptectedCurProcStateSeq
+ -1, // expectedBlockState, -1 to verify there are no interactions with main thread.
+ false); // expectNotify
+
+ // Verify when the uid doesn't have internet permission, then procStateSeq is not
+ // incremented.
+ uidRec.hasInternetPermission = false;
+ mAms.mWaitForNetworkTimeoutMs = 111;
+ mInjector.setNetworkRestrictedForUid(true);
+ verifySeqCounterAndInteractions(uidRec,
+ PROCESS_STATE_CACHED_ACTIVITY, // prevState
+ PROCESS_STATE_FOREGROUND_SERVICE, // curState
+ 44, // expectedGlobalCounter
+ 44, // exptectedCurProcStateSeq
+ -1, // expectedBlockState, -1 to verify there are no interactions with main thread.
+ false); // expectNotify
+
+ // Verify procStateSeq is not incremented when the uid is not an application, regardless
+ // of the process state.
+ final int notAppUid = 111;
+ final UidRecord uidRec2 = addUidRecord(notAppUid);
+ verifySeqCounterAndInteractions(uidRec2,
+ PROCESS_STATE_CACHED_EMPTY, // prevState
+ PROCESS_STATE_TOP, // curState
+ 44, // expectedGlobalCounter
+ 0, // exptectedCurProcStateSeq
+ -1, // expectedBlockState, -1 to verify there are no interactions with main thread.
+ false); // expectNotify
+ }
+
+ private UidRecord addUidRecord(int uid) {
+ final UidRecord uidRec = new UidRecord(uid);
+ uidRec.waitingForNetwork = true;
+ uidRec.hasInternetPermission = true;
+ mAms.mActiveUids.put(uid, uidRec);
+
+ final ProcessRecord appRec = new ProcessRecord(null, mBatteryStatsImpl,
+ new ApplicationInfo(), TAG, uid);
+ appRec.thread = Mockito.mock(IApplicationThread.class);
+ mAms.mLruProcesses.add(appRec);
+
+ return uidRec;
+ }
+
+ private void verifySeqCounterAndInteractions(UidRecord uidRec, int prevState, int curState,
+ int expectedGlobalCounter, int expectedCurProcStateSeq, int expectedBlockState,
+ boolean expectNotify) throws Exception {
+ CustomThread thread = new CustomThread(uidRec.networkStateLock);
+ thread.startAndWait("Unexpected state for " + uidRec);
+
+ uidRec.setProcState = prevState;
+ uidRec.curProcState = curState;
+ mAms.incrementProcStateSeqAndNotifyAppsLocked();
+
+ assertEquals(expectedGlobalCounter, mAms.mProcStateSeqCounter);
+ assertEquals(expectedCurProcStateSeq, uidRec.curProcStateSeq);
+
+ for (int i = mAms.mLruProcesses.size() - 1; i >= 0; --i) {
+ final ProcessRecord app = mAms.mLruProcesses.get(i);
+ // AMS should notify apps only for block states other than NETWORK_STATE_NO_CHANGE.
+ if (app.uid == uidRec.uid && expectedBlockState == NETWORK_STATE_BLOCK) {
+ verify(app.thread).setNetworkBlockSeq(uidRec.curProcStateSeq);
+ } else {
+ verifyZeroInteractions(app.thread);
+ }
+ Mockito.reset(app.thread);
+ }
+
+ if (expectNotify) {
+ thread.assertTerminated("Unexpected state for " + uidRec);
+ } else {
+ thread.assertWaiting("Unexpected state for " + uidRec);
+ thread.interrupt();
+ }
+ }
+
+ @Test
+ public void testBlockStateForUid() {
+ final UidRecord uidRec = new UidRecord(TEST_UID);
+ int expectedBlockState;
+
+ final String errorTemplate = "Block state should be %s, prevState: %s, curState: %s";
+ Function<Integer, String> errorMsg = (blockState) -> {
+ return String.format(errorTemplate,
+ valueToString(ActivityManagerService.class, "NETWORK_STATE_", blockState),
+ valueToString(ActivityManager.class, "PROCESS_STATE_", uidRec.setProcState),
+ valueToString(ActivityManager.class, "PROCESS_STATE_", uidRec.curProcState));
+ };
+
+ // No change in uid state
+ uidRec.setProcState = PROCESS_STATE_RECEIVER;
+ uidRec.curProcState = PROCESS_STATE_RECEIVER;
+ expectedBlockState = NETWORK_STATE_NO_CHANGE;
+ assertEquals(errorMsg.apply(expectedBlockState),
+ expectedBlockState, mAms.getBlockStateForUid(uidRec));
+
+ // Foreground to foreground
+ uidRec.setProcState = PROCESS_STATE_FOREGROUND_SERVICE;
+ uidRec.curProcState = PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+ expectedBlockState = NETWORK_STATE_NO_CHANGE;
+ assertEquals(errorMsg.apply(expectedBlockState),
+ expectedBlockState, mAms.getBlockStateForUid(uidRec));
+
+ // Background to background
+ uidRec.setProcState = PROCESS_STATE_CACHED_ACTIVITY;
+ uidRec.curProcState = PROCESS_STATE_CACHED_EMPTY;
+ expectedBlockState = NETWORK_STATE_NO_CHANGE;
+ assertEquals(errorMsg.apply(expectedBlockState),
+ expectedBlockState, mAms.getBlockStateForUid(uidRec));
+
+ // Background to background
+ uidRec.setProcState = PROCESS_STATE_NONEXISTENT;
+ uidRec.curProcState = PROCESS_STATE_CACHED_ACTIVITY;
+ expectedBlockState = NETWORK_STATE_NO_CHANGE;
+ assertEquals(errorMsg.apply(expectedBlockState),
+ expectedBlockState, mAms.getBlockStateForUid(uidRec));
+
+ // Background to foreground
+ uidRec.setProcState = PROCESS_STATE_SERVICE;
+ uidRec.curProcState = PROCESS_STATE_FOREGROUND_SERVICE;
+ expectedBlockState = NETWORK_STATE_BLOCK;
+ assertEquals(errorMsg.apply(expectedBlockState),
+ expectedBlockState, mAms.getBlockStateForUid(uidRec));
+
+ // Foreground to background
+ uidRec.setProcState = PROCESS_STATE_TOP;
+ uidRec.curProcState = PROCESS_STATE_LAST_ACTIVITY;
+ expectedBlockState = NETWORK_STATE_UNBLOCK;
+ assertEquals(errorMsg.apply(expectedBlockState),
+ expectedBlockState, mAms.getBlockStateForUid(uidRec));
+ }
+
+ /**
+ * This test verifies that process state changes are dispatched to observers based on the
+ * changes they wanted to listen (this is specified when registering the observer).
+ */
+ @Test
+ public void testDispatchUids_dispatchNeededChanges() throws RemoteException {
+ when(mAppOpsService.noteOperation(AppOpsManager.OP_GET_USAGE_STATS, Process.myUid(), null))
+ .thenReturn(AppOpsManager.MODE_ALLOWED);
+
+ final int[] changesToObserve = {
+ ActivityManager.UID_OBSERVER_PROCSTATE,
+ ActivityManager.UID_OBSERVER_GONE,
+ ActivityManager.UID_OBSERVER_IDLE,
+ ActivityManager.UID_OBSERVER_ACTIVE,
+ ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE
+ | ActivityManager.UID_OBSERVER_ACTIVE | ActivityManager.UID_OBSERVER_IDLE
+ };
+ final IUidObserver[] observers = new IUidObserver.Stub[changesToObserve.length];
+ for (int i = 0; i < observers.length; ++i) {
+ observers[i] = Mockito.mock(IUidObserver.Stub.class);
+ when(observers[i].asBinder()).thenReturn((IBinder) observers[i]);
+ mAms.registerUidObserver(observers[i], changesToObserve[i] /* which */,
+ ActivityManager.PROCESS_STATE_UNKNOWN /* cutpoint */, null /* caller */);
+
+ // When we invoke AMS.registerUidObserver, there are some interactions with observers[i]
+ // mock in RemoteCallbackList class. We don't want to test those interactions and
+ // at the same time, we don't want those to interfere with verifyNoMoreInteractions.
+ // So, resetting the mock here.
+ Mockito.reset(observers[i]);
+ }
+
+ // Add pending uid records each corresponding to a different change type UidRecord.CHANGE_*
+ final int[] changesForPendingUidRecords = UID_RECORD_CHANGES;
+
+ final int[] procStatesForPendingUidRecords = {
+ ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE,
+ ActivityManager.PROCESS_STATE_NONEXISTENT,
+ ActivityManager.PROCESS_STATE_CACHED_EMPTY,
+ ActivityManager.PROCESS_STATE_CACHED_ACTIVITY,
+ ActivityManager.PROCESS_STATE_TOP
+ };
+ final Map<Integer, UidRecord.ChangeItem> changeItems = new HashMap<>();
+ for (int i = 0; i < changesForPendingUidRecords.length; ++i) {
+ final UidRecord.ChangeItem pendingChange = new UidRecord.ChangeItem();
+ pendingChange.change = changesForPendingUidRecords[i];
+ pendingChange.uid = i;
+ pendingChange.processState = procStatesForPendingUidRecords[i];
+ pendingChange.procStateSeq = i;
+ changeItems.put(changesForPendingUidRecords[i], pendingChange);
+ mAms.mPendingUidChanges.add(pendingChange);
+ }
+
+ mAms.dispatchUidsChanged();
+ // Verify the required changes have been dispatched to observers.
+ for (int i = 0; i < observers.length; ++i) {
+ final int changeToObserve = changesToObserve[i];
+ final IUidObserver observerToTest = observers[i];
+ if ((changeToObserve & ActivityManager.UID_OBSERVER_IDLE) != 0) {
+ // Observer listens to uid idle changes, so change items corresponding to
+ // UidRecord.CHANGE_IDLE or UidRecord.CHANGE_IDLE_GONE needs to be
+ // delivered to this observer.
+ final int[] changesToVerify = {
+ UidRecord.CHANGE_IDLE,
+ UidRecord.CHANGE_GONE | UidRecord.CHANGE_IDLE
+ };
+ verifyObserverReceivedChanges(observerToTest, changesToVerify, changeItems,
+ (observer, changeItem) -> {
+ verify(observer).onUidIdle(changeItem.uid, changeItem.ephemeral);
+ });
+ }
+ if ((changeToObserve & ActivityManager.UID_OBSERVER_ACTIVE) != 0) {
+ // Observer listens to uid active changes, so change items corresponding to
+ // UidRecord.CHANGE_ACTIVE needs to be delivered to this observer.
+ final int[] changesToVerify = { UidRecord.CHANGE_ACTIVE };
+ verifyObserverReceivedChanges(observerToTest, changesToVerify, changeItems,
+ (observer, changeItem) -> {
+ verify(observer).onUidActive(changeItem.uid);
+ });
+ }
+ if ((changeToObserve & ActivityManager.UID_OBSERVER_GONE) != 0) {
+ // Observer listens to uid gone changes, so change items corresponding to
+ // UidRecord.CHANGE_GONE or UidRecord.CHANGE_IDLE_GONE needs to be
+ // delivered to this observer.
+ final int[] changesToVerify = {
+ UidRecord.CHANGE_GONE,
+ UidRecord.CHANGE_GONE | UidRecord.CHANGE_IDLE
+ };
+ verifyObserverReceivedChanges(observerToTest, changesToVerify, changeItems,
+ (observer, changeItem) -> {
+ verify(observer).onUidGone(changeItem.uid, changeItem.ephemeral);
+ });
+ }
+ if ((changeToObserve & ActivityManager.UID_OBSERVER_PROCSTATE) != 0) {
+ // Observer listens to uid procState changes, so change items corresponding to
+ // UidRecord.CHANGE_PROCSTATE or UidRecord.CHANGE_IDLE or UidRecord.CHANGE_ACTIVE
+ // needs to be delivered to this observer.
+ final int[] changesToVerify = {
+ UidRecord.CHANGE_PROCSTATE,
+ UidRecord.CHANGE_ACTIVE,
+ UidRecord.CHANGE_IDLE
+ };
+ verifyObserverReceivedChanges(observerToTest, changesToVerify, changeItems,
+ (observer, changeItem) -> {
+ verify(observer).onUidStateChanged(changeItem.uid,
+ changeItem.processState, changeItem.procStateSeq);
+ });
+ }
+ // Verify there are no other callbacks for this observer.
+ verifyNoMoreInteractions(observerToTest);
+ }
+ }
+
+ private interface ObserverChangesVerifier {
+ void verify(IUidObserver observer, UidRecord.ChangeItem changeItem) throws RemoteException;
+ }
+
+ private void verifyObserverReceivedChanges(IUidObserver observer, int[] changesToVerify,
+ Map<Integer, UidRecord.ChangeItem> changeItems, ObserverChangesVerifier verifier)
+ throws RemoteException {
+ for (int change : changesToVerify) {
+ final UidRecord.ChangeItem changeItem = changeItems.get(change);
+ verifier.verify(observer, changeItem);
+ }
+ }
+
+ /**
+ * This test verifies that process state changes are dispatched to observers only when they
+ * change across the cutpoint (this is specified when registering the observer).
+ */
+ @Test
+ public void testDispatchUidChanges_procStateCutpoint() throws RemoteException {
+ final IUidObserver observer = Mockito.mock(IUidObserver.Stub.class);
+
+ when(observer.asBinder()).thenReturn((IBinder) observer);
+ mAms.registerUidObserver(observer, ActivityManager.UID_OBSERVER_PROCSTATE /* which */,
+ ActivityManager.PROCESS_STATE_SERVICE /* cutpoint */, null /* callingPackage */);
+ // When we invoke AMS.registerUidObserver, there are some interactions with observer
+ // mock in RemoteCallbackList class. We don't want to test those interactions and
+ // at the same time, we don't want those to interfere with verifyNoMoreInteractions.
+ // So, resetting the mock here.
+ Mockito.reset(observer);
+
+ final UidRecord.ChangeItem changeItem = new UidRecord.ChangeItem();
+ changeItem.uid = TEST_UID;
+ changeItem.change = UidRecord.CHANGE_PROCSTATE;
+ changeItem.processState = ActivityManager.PROCESS_STATE_LAST_ACTIVITY;
+ changeItem.procStateSeq = 111;
+ mAms.mPendingUidChanges.add(changeItem);
+ mAms.dispatchUidsChanged();
+ // First process state message is always delivered regardless of whether the process state
+ // change is above or below the cutpoint (PROCESS_STATE_SERVICE).
+ verify(observer).onUidStateChanged(TEST_UID,
+ changeItem.processState, changeItem.procStateSeq);
+ verifyNoMoreInteractions(observer);
+
+ changeItem.processState = ActivityManager.PROCESS_STATE_RECEIVER;
+ mAms.mPendingUidChanges.add(changeItem);
+ mAms.dispatchUidsChanged();
+ // Previous process state change is below cutpoint (PROCESS_STATE_SERVICE) and
+ // the current process state change is also below cutpoint, so no callback will be invoked.
+ verifyNoMoreInteractions(observer);
+
+ changeItem.processState = ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
+ mAms.mPendingUidChanges.add(changeItem);
+ mAms.dispatchUidsChanged();
+ // Previous process state change is below cutpoint (PROCESS_STATE_SERVICE) and
+ // the current process state change is above cutpoint, so callback will be invoked with the
+ // current process state change.
+ verify(observer).onUidStateChanged(TEST_UID,
+ changeItem.processState, changeItem.procStateSeq);
+ verifyNoMoreInteractions(observer);
+
+ changeItem.processState = ActivityManager.PROCESS_STATE_TOP;
+ mAms.mPendingUidChanges.add(changeItem);
+ mAms.dispatchUidsChanged();
+ // Previous process state change is above cutpoint (PROCESS_STATE_SERVICE) and
+ // the current process state change is also above cutpoint, so no callback will be invoked.
+ verifyNoMoreInteractions(observer);
+
+ changeItem.processState = ActivityManager.PROCESS_STATE_CACHED_EMPTY;
+ mAms.mPendingUidChanges.add(changeItem);
+ mAms.dispatchUidsChanged();
+ // Previous process state change is above cutpoint (PROCESS_STATE_SERVICE) and
+ // the current process state change is below cutpoint, so callback will be invoked with the
+ // current process state change.
+ verify(observer).onUidStateChanged(TEST_UID,
+ changeItem.processState, changeItem.procStateSeq);
+ verifyNoMoreInteractions(observer);
+ }
+
+ /**
+ * This test verifies that {@link ActivityManagerService#mValidateUids} which is a
+ * part of dumpsys is correctly updated.
+ */
+ @Test
+ public void testDispatchUidChanges_validateUidsUpdated() {
+ final int[] changesForPendingItems = UID_RECORD_CHANGES;
+
+ final int[] procStatesForPendingItems = {
+ ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE,
+ ActivityManager.PROCESS_STATE_CACHED_EMPTY,
+ ActivityManager.PROCESS_STATE_CACHED_ACTIVITY,
+ ActivityManager.PROCESS_STATE_SERVICE,
+ ActivityManager.PROCESS_STATE_RECEIVER
+ };
+ final ArrayList<UidRecord.ChangeItem> pendingItemsForUids
+ = new ArrayList<>(changesForPendingItems.length);
+ for (int i = 0; i < changesForPendingItems.length; ++i) {
+ final UidRecord.ChangeItem item = new UidRecord.ChangeItem();
+ item.uid = i;
+ item.change = changesForPendingItems[i];
+ item.processState = procStatesForPendingItems[i];
+ pendingItemsForUids.add(i, item);
+ }
+
+ // Verify that when there no observers listening to uid state changes, then there will
+ // be no changes to validateUids.
+ mAms.mPendingUidChanges.addAll(pendingItemsForUids);
+ mAms.dispatchUidsChanged();
+ assertEquals("No observers registered, so validateUids should be empty",
+ 0, mAms.mValidateUids.size());
+
+ final IUidObserver observer = Mockito.mock(IUidObserver.Stub.class);
+ when(observer.asBinder()).thenReturn((IBinder) observer);
+ mAms.registerUidObserver(observer, 0, 0, null);
+ // Verify that when observers are registered, then validateUids is correctly updated.
+ mAms.mPendingUidChanges.addAll(pendingItemsForUids);
+ mAms.dispatchUidsChanged();
+ for (int i = 0; i < pendingItemsForUids.size(); ++i) {
+ final UidRecord.ChangeItem item = pendingItemsForUids.get(i);
+ final UidRecord validateUidRecord = mAms.mValidateUids.get(item.uid);
+ if ((item.change & UidRecord.CHANGE_GONE) != 0) {
+ assertNull("validateUidRecord should be null since the change is either "
+ + "CHANGE_GONE or CHANGE_GONE_IDLE", validateUidRecord);
+ } else {
+ assertNotNull("validateUidRecord should not be null since the change is neither "
+ + "CHANGE_GONE nor CHANGE_GONE_IDLE", validateUidRecord);
+ assertEquals("processState: " + item.processState + " curProcState: "
+ + validateUidRecord.curProcState + " should have been equal",
+ item.processState, validateUidRecord.curProcState);
+ assertEquals("processState: " + item.processState + " setProcState: "
+ + validateUidRecord.curProcState + " should have been equal",
+ item.processState, validateUidRecord.setProcState);
+ if (item.change == UidRecord.CHANGE_IDLE) {
+ assertTrue("UidRecord.idle should be updated to true for CHANGE_IDLE",
+ validateUidRecord.idle);
+ } else if (item.change == UidRecord.CHANGE_ACTIVE) {
+ assertFalse("UidRecord.idle should be updated to false for CHANGE_ACTIVE",
+ validateUidRecord.idle);
+ }
+ }
+ }
+
+ // Verify that when uid state changes to CHANGE_GONE or CHANGE_GONE_IDLE, then it
+ // will be removed from validateUids.
+ assertNotEquals("validateUids should not be empty", 0, mAms.mValidateUids.size());
+ for (int i = 0; i < pendingItemsForUids.size(); ++i) {
+ final UidRecord.ChangeItem item = pendingItemsForUids.get(i);
+ // Assign CHANGE_GONE_IDLE to some items and CHANGE_GONE to the others, using even/odd
+ // distribution for this assignment.
+ item.change = (i % 2) == 0 ? (UidRecord.CHANGE_GONE | UidRecord.CHANGE_IDLE)
+ : UidRecord.CHANGE_GONE;
+ }
+ mAms.mPendingUidChanges.addAll(pendingItemsForUids);
+ mAms.dispatchUidsChanged();
+ assertEquals("validateUids should be empty, validateUids: " + mAms.mValidateUids,
+ 0, mAms.mValidateUids.size());
+ }
+
+ @Test
+ public void testEnqueueUidChangeLocked_procStateSeqUpdated() {
+ final UidRecord uidRecord = new UidRecord(TEST_UID);
+ uidRecord.curProcStateSeq = TEST_PROC_STATE_SEQ1;
+
+ // Verify with no pending changes for TEST_UID.
+ verifyLastProcStateSeqUpdated(uidRecord, -1, TEST_PROC_STATE_SEQ1);
+
+ // Add a pending change for TEST_UID and verify enqueueUidChangeLocked still works as
+ // expected.
+ final UidRecord.ChangeItem changeItem = new UidRecord.ChangeItem();
+ uidRecord.pendingChange = changeItem;
+ uidRecord.curProcStateSeq = TEST_PROC_STATE_SEQ2;
+ verifyLastProcStateSeqUpdated(uidRecord, -1, TEST_PROC_STATE_SEQ2);
+ }
+
+ @Test
+ public void testEnqueueUidChangeLocked_nullUidRecord() {
+ // Use "null" uidRecord to make sure there is no crash.
+ mAms.enqueueUidChangeLocked(null, TEST_UID, UidRecord.CHANGE_ACTIVE);
+ }
+
+ private void verifyLastProcStateSeqUpdated(UidRecord uidRecord, int uid, long curProcstateSeq) {
+ // Test enqueueUidChangeLocked with every UidRecord.CHANGE_*
+ for (int i = 0; i < UID_RECORD_CHANGES.length; ++i) {
+ final int changeToDispatch = UID_RECORD_CHANGES[i];
+ // Reset lastProcStateSeqDispatchToObservers after every test.
+ uidRecord.lastDispatchedProcStateSeq = 0;
+ mAms.enqueueUidChangeLocked(uidRecord, uid, changeToDispatch);
+ // Verify there is no effect on curProcStateSeq.
+ assertEquals(curProcstateSeq, uidRecord.curProcStateSeq);
+ if ((changeToDispatch & UidRecord.CHANGE_GONE) != 0) {
+ // Since the change is CHANGE_GONE or CHANGE_GONE_IDLE, verify that
+ // lastProcStateSeqDispatchedToObservers is not updated.
+ assertNotEquals(uidRecord.curProcStateSeq,
+ uidRecord.lastDispatchedProcStateSeq);
+ } else {
+ // Since the change is neither CHANGE_GONE nor CHANGE_GONE_IDLE, verify that
+ // lastProcStateSeqDispatchedToObservers has been updated to curProcStateSeq.
+ assertEquals(uidRecord.curProcStateSeq,
+ uidRecord.lastDispatchedProcStateSeq);
+ }
+ }
+ }
+
+ @MediumTest
+ @Test
+ public void testEnqueueUidChangeLocked_dispatchUidsChanged() {
+ final UidRecord uidRecord = new UidRecord(TEST_UID);
+ final int expectedProcState = PROCESS_STATE_SERVICE;
+ uidRecord.setProcState = expectedProcState;
+ uidRecord.curProcStateSeq = TEST_PROC_STATE_SEQ1;
+
+ // Test with no pending uid records.
+ for (int i = 0; i < UID_RECORD_CHANGES.length; ++i) {
+ final int changeToDispatch = UID_RECORD_CHANGES[i];
+
+ // Reset the current state
+ mHandler.reset();
+ uidRecord.pendingChange = null;
+ mAms.mPendingUidChanges.clear();
+
+ mAms.enqueueUidChangeLocked(uidRecord, -1, changeToDispatch);
+
+ // Verify that UidRecord.pendingChange is updated correctly.
+ assertNotNull(uidRecord.pendingChange);
+ assertEquals(TEST_UID, uidRecord.pendingChange.uid);
+ assertEquals(expectedProcState, uidRecord.pendingChange.processState);
+ assertEquals(TEST_PROC_STATE_SEQ1, uidRecord.pendingChange.procStateSeq);
+
+ // Verify that DISPATCH_UIDS_CHANGED_UI_MSG is posted to handler.
+ mHandler.waitForMessage(DISPATCH_UIDS_CHANGED_UI_MSG);
+ }
+ }
+
+ @MediumTest
+ @Test
+ public void testWaitForNetworkStateUpdate() throws Exception {
+ // Check there is no crash when there is no UidRecord for myUid
+ mAms.waitForNetworkStateUpdate(TEST_PROC_STATE_SEQ1);
+
+ // Verify there is no waiting when UidRecord.curProcStateSeq is greater than
+ // the procStateSeq in the request to wait.
+ verifyWaitingForNetworkStateUpdate(
+ TEST_PROC_STATE_SEQ1, // curProcStateSeq
+ TEST_PROC_STATE_SEQ1, // lastDsipatchedProcStateSeq
+ TEST_PROC_STATE_SEQ1 - 4, // lastNetworkUpdatedProcStateSeq
+ TEST_PROC_STATE_SEQ1 - 2, // procStateSeqToWait
+ false); // expectWait
+
+ // Verify there is no waiting when the procStateSeq in the request to wait is
+ // not dispatched to NPMS.
+ verifyWaitingForNetworkStateUpdate(
+ TEST_PROC_STATE_SEQ1, // curProcStateSeq
+ TEST_PROC_STATE_SEQ1 - 1, // lastDsipatchedProcStateSeq
+ TEST_PROC_STATE_SEQ1 - 1, // lastNetworkUpdatedProcStateSeq
+ TEST_PROC_STATE_SEQ1, // procStateSeqToWait
+ false); // expectWait
+
+ // Verify there is not waiting when the procStateSeq in the request already has
+ // an updated network state.
+ verifyWaitingForNetworkStateUpdate(
+ TEST_PROC_STATE_SEQ1, // curProcStateSeq
+ TEST_PROC_STATE_SEQ1, // lastDsipatchedProcStateSeq
+ TEST_PROC_STATE_SEQ1, // lastNetworkUpdatedProcStateSeq
+ TEST_PROC_STATE_SEQ1, // procStateSeqToWait
+ false); // expectWait
+
+ // Verify waiting for network works
+ verifyWaitingForNetworkStateUpdate(
+ TEST_PROC_STATE_SEQ1, // curProcStateSeq
+ TEST_PROC_STATE_SEQ1, // lastDsipatchedProcStateSeq
+ TEST_PROC_STATE_SEQ1 - 1, // lastNetworkUpdatedProcStateSeq
+ TEST_PROC_STATE_SEQ1, // procStateSeqToWait
+ true); // expectWait
+ }
+
+ private void verifyWaitingForNetworkStateUpdate(long curProcStateSeq,
+ long lastDispatchedProcStateSeq, long lastNetworkUpdatedProcStateSeq,
+ final long procStateSeqToWait, boolean expectWait) throws Exception {
+ final UidRecord record = new UidRecord(Process.myUid());
+ record.curProcStateSeq = curProcStateSeq;
+ record.lastDispatchedProcStateSeq = lastDispatchedProcStateSeq;
+ record.lastNetworkUpdatedProcStateSeq = lastNetworkUpdatedProcStateSeq;
+ mAms.mActiveUids.put(Process.myUid(), record);
+
+ CustomThread thread = new CustomThread(record.networkStateLock, new Runnable() {
+ @Override
+ public void run() {
+ mAms.waitForNetworkStateUpdate(procStateSeqToWait);
+ }
+ });
+ final String errMsg = "Unexpected state for " + record;
+ if (expectWait) {
+ thread.startAndWait(errMsg, true);
+ thread.assertTimedWaiting(errMsg);
+ synchronized (record.networkStateLock) {
+ record.networkStateLock.notifyAll();
+ }
+ thread.assertTerminated(errMsg);
+ assertTrue(thread.mNotified);
+ assertFalse(record.waitingForNetwork);
+ } else {
+ thread.start();
+ thread.assertTerminated(errMsg);
+ }
+
+ mAms.mActiveUids.clear();
+ }
+
+ private class TestHandler extends Handler {
+ private static final long WAIT_FOR_MSG_TIMEOUT_MS = 4000; // 4 sec
+ private static final long WAIT_FOR_MSG_INTERVAL_MS = 400; // 0.4 sec
+
+ private Set<Integer> mMsgsHandled = new HashSet<>();
+
+ TestHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ mMsgsHandled.add(msg.what);
+ }
+
+ public void waitForMessage(int msg) {
+ final long endTime = System.currentTimeMillis() + WAIT_FOR_MSG_TIMEOUT_MS;
+ while (!mMsgsHandled.contains(msg) && System.currentTimeMillis() < endTime) {
+ SystemClock.sleep(WAIT_FOR_MSG_INTERVAL_MS);
+ }
+ if (!mMsgsHandled.contains(msg)) {
+ fail("Timed out waiting for the message to be handled, msg: " + msg);
+ }
+ }
+
+ public void reset() {
+ mMsgsHandled.clear();
+ }
+ }
+
+ private class TestInjector extends Injector {
+ private boolean mRestricted = true;
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ @Override
+ public AppOpsService getAppOpsService(File file, Handler handler) {
+ return mAppOpsService;
+ }
+
+ @Override
+ public Handler getUiHandler(ActivityManagerService service) {
+ return mHandler;
+ }
+
+ @Override
+ public boolean isNetworkRestrictedForUid(int uid) {
+ return mRestricted;
+ }
+
+ public void setNetworkRestrictedForUid(boolean restricted) {
+ mRestricted = restricted;
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java
new file mode 100644
index 0000000..ba25b16
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.am;
+
+import android.app.ActivityManager;
+import android.app.IActivityManager;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.os.RemoteException;
+import android.test.AndroidTestCase;
+
+import java.util.List;
+
+public class ActivityManagerTest extends AndroidTestCase {
+
+ IActivityManager service;
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ service = ActivityManager.getService();
+ }
+
+ public void testTaskIdsForRunningUsers() throws RemoteException {
+ for(int userId : service.getRunningUserIds()) {
+ testTaskIdsForUser(userId);
+ }
+ }
+
+ private void testTaskIdsForUser(int userId) throws RemoteException {
+ List<ActivityManager.RecentTaskInfo> recentTasks = service.getRecentTasks(
+ 100, 0, userId).getList();
+ if(recentTasks != null) {
+ for(ActivityManager.RecentTaskInfo recentTask : recentTasks) {
+ int taskId = recentTask.persistentId;
+ assertEquals("The task id " + taskId + " should not belong to user " + userId,
+ taskId / UserHandle.PER_USER_RANGE, userId);
+ }
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityRecordTests.java b/services/tests/servicestests/src/com/android/server/am/ActivityRecordTests.java
new file mode 100644
index 0000000..5ee1c40
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityRecordTests.java
@@ -0,0 +1,258 @@
+/*
+ * 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.am;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.server.am.ActivityStack.ActivityState.DESTROYED;
+import static com.android.server.am.ActivityStack.ActivityState.DESTROYING;
+import static com.android.server.am.ActivityStack.ActivityState.FINISHING;
+import static com.android.server.am.ActivityStack.ActivityState.INITIALIZING;
+import static com.android.server.am.ActivityStack.ActivityState.PAUSED;
+import static com.android.server.am.ActivityStack.ActivityState.PAUSING;
+import static com.android.server.am.ActivityStack.ActivityState.STOPPED;
+import static com.android.server.am.ActivityStack.REMOVE_TASK_MODE_MOVING;
+import static com.android.server.policy.WindowManagerPolicy.NAV_BAR_BOTTOM;
+import static com.android.server.policy.WindowManagerPolicy.NAV_BAR_LEFT;
+import static com.android.server.policy.WindowManagerPolicy.NAV_BAR_RIGHT;
+
+import static junit.framework.TestCase.assertNotNull;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityOptions;
+import android.app.servertransaction.ClientTransaction;
+import android.app.servertransaction.PauseActivityItem;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.MutableBoolean;
+
+import org.junit.runner.RunWith;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+
+/**
+ * Tests for the {@link ActivityRecord} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.am.ActivityRecordTests
+ */
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class ActivityRecordTests extends ActivityTestsBase {
+ private ActivityManagerService mService;
+ private TestActivityStack mStack;
+ private TaskRecord mTask;
+ private ActivityRecord mActivity;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mService = createActivityManagerService();
+ mStack = mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ mTask = new TaskBuilder(mService.mStackSupervisor).setStack(mStack).build();
+ mActivity = new ActivityBuilder(mService).setTask(mTask).build();
+ }
+
+ @Test
+ public void testStackCleanupOnClearingTask() throws Exception {
+ mActivity.setTask(null);
+ assertEquals(mStack.onActivityRemovedFromStackInvocationCount(), 1);
+ }
+
+ @Test
+ public void testStackCleanupOnActivityRemoval() throws Exception {
+ mTask.removeActivity(mActivity);
+ assertEquals(mStack.onActivityRemovedFromStackInvocationCount(), 1);
+ }
+
+ @Test
+ public void testStackCleanupOnTaskRemoval() throws Exception {
+ mStack.removeTask(mTask, null /*reason*/, REMOVE_TASK_MODE_MOVING);
+ // Stack should be gone on task removal.
+ assertNull(mService.mStackSupervisor.getStack(mStack.mStackId));
+ }
+
+ @Test
+ public void testNoCleanupMovingActivityInSameStack() throws Exception {
+ final TaskRecord newTask = new TaskBuilder(mService.mStackSupervisor).setStack(mStack)
+ .build();
+ mActivity.reparent(newTask, 0, null /*reason*/);
+ assertEquals(mStack.onActivityRemovedFromStackInvocationCount(), 0);
+ }
+
+ @Test
+ public void testPausingWhenVisibleFromStopped() throws Exception {
+ final MutableBoolean pauseFound = new MutableBoolean(false);
+ doAnswer((InvocationOnMock invocationOnMock) -> {
+ final ClientTransaction transaction = invocationOnMock.getArgument(0);
+ if (transaction.getLifecycleStateRequest() instanceof PauseActivityItem) {
+ pauseFound.value = true;
+ }
+ return null;
+ }).when(mActivity.app.thread).scheduleTransaction(any());
+
+ mActivity.setState(STOPPED, "testPausingWhenVisibleFromStopped");
+
+ // The activity is in the focused stack so it should not move to paused.
+ mActivity.makeVisibleIfNeeded(null /* starting */, true /* reportToClient */);
+ assertTrue(mActivity.isState(STOPPED));
+ assertFalse(pauseFound.value);
+
+ // Clear focused stack
+ mActivity.mStackSupervisor.mFocusedStack = null;
+
+ // In the unfocused stack, the activity should move to paused.
+ mActivity.makeVisibleIfNeeded(null /* starting */, true /* reportToClient */);
+ assertTrue(mActivity.isState(PAUSING));
+ assertTrue(pauseFound.value);
+
+ // Make sure that the state does not change for current non-stopping states.
+ mActivity.setState(INITIALIZING, "testPausingWhenVisibleFromStopped");
+
+ mActivity.makeVisibleIfNeeded(null /* starting */, true /* reportToClient */);
+
+ assertTrue(mActivity.isState(INITIALIZING));
+
+ // Make sure the state does not change if we are not the current top activity.
+ mActivity.setState(STOPPED, "testPausingWhenVisibleFromStopped behind");
+
+ // Make sure that the state does not change when we have an activity becoming translucent
+ final ActivityRecord topActivity = new ActivityBuilder(mService).setTask(mTask).build();
+ mStack.mTranslucentActivityWaiting = topActivity;
+ mActivity.makeVisibleIfNeeded(null /* starting */, true /* reportToClient */);
+
+ assertTrue(mActivity.isState(STOPPED));
+ }
+
+ @Test
+ public void testPositionLimitedAspectRatioNavBarBottom() throws Exception {
+ verifyPositionWithLimitedAspectRatio(NAV_BAR_BOTTOM, new Rect(0, 0, 1000, 2000), 1.5f,
+ new Rect(0, 0, 1000, 1500));
+ }
+
+ @Test
+ public void testPositionLimitedAspectRatioNavBarLeft() throws Exception {
+ verifyPositionWithLimitedAspectRatio(NAV_BAR_LEFT, new Rect(0, 0, 2000, 1000), 1.5f,
+ new Rect(500, 0, 2000, 1000));
+ }
+
+ @Test
+ public void testPositionLimitedAspectRatioNavBarRight() throws Exception {
+ verifyPositionWithLimitedAspectRatio(NAV_BAR_RIGHT, new Rect(0, 0, 2000, 1000), 1.5f,
+ new Rect(0, 0, 1500, 1000));
+ }
+
+ private void verifyPositionWithLimitedAspectRatio(int navBarPosition, Rect taskBounds,
+ float aspectRatio, Rect expectedActivityBounds) {
+ // Verify with nav bar on the right.
+ when(mService.mWindowManager.getNavBarPosition()).thenReturn(navBarPosition);
+ mTask.getConfiguration().windowConfiguration.setAppBounds(taskBounds);
+ mActivity.info.maxAspectRatio = aspectRatio;
+ mActivity.ensureActivityConfiguration(
+ 0 /* globalChanges */, false /* preserveWindow */);
+ assertEquals(expectedActivityBounds, mActivity.getBounds());
+ }
+
+ @Test
+ public void testCanBeLaunchedOnDisplay() throws Exception {
+ testSupportsLaunchingResizeable(false /*taskPresent*/, true /*taskResizeable*/,
+ true /*activityResizeable*/, true /*expected*/);
+
+ testSupportsLaunchingResizeable(false /*taskPresent*/, true /*taskResizeable*/,
+ false /*activityResizeable*/, false /*expected*/);
+
+ testSupportsLaunchingResizeable(true /*taskPresent*/, false /*taskResizeable*/,
+ true /*activityResizeable*/, false /*expected*/);
+
+ testSupportsLaunchingResizeable(true /*taskPresent*/, true /*taskResizeable*/,
+ false /*activityResizeable*/, true /*expected*/);
+ }
+
+ @Test
+ public void testsApplyOptionsLocked() {
+ ActivityOptions activityOptions = ActivityOptions.makeBasic();
+
+ // Set and apply options for ActivityRecord. Pending options should be cleared
+ mActivity.updateOptionsLocked(activityOptions);
+ mActivity.applyOptionsLocked();
+ assertNull(mActivity.pendingOptions);
+
+ // Set options for two ActivityRecords in same Task. Apply one ActivityRecord options.
+ // Pending options should be cleared for both ActivityRecords
+ ActivityRecord activity2 = new ActivityBuilder(mService).setTask(mTask).build();
+ activity2.updateOptionsLocked(activityOptions);
+ mActivity.updateOptionsLocked(activityOptions);
+ mActivity.applyOptionsLocked();
+ assertNull(mActivity.pendingOptions);
+ assertNull(activity2.pendingOptions);
+
+ // Set options for two ActivityRecords in separate Tasks. Apply one ActivityRecord options.
+ // Pending options should be cleared for only ActivityRecord that was applied
+ TaskRecord task2 = new TaskBuilder(mService.mStackSupervisor).setStack(mStack).build();
+ activity2 = new ActivityBuilder(mService).setTask(task2).build();
+ activity2.updateOptionsLocked(activityOptions);
+ mActivity.updateOptionsLocked(activityOptions);
+ mActivity.applyOptionsLocked();
+ assertNull(mActivity.pendingOptions);
+ assertNotNull(activity2.pendingOptions);
+ }
+
+ private void testSupportsLaunchingResizeable(boolean taskPresent, boolean taskResizeable,
+ boolean activityResizeable, boolean expected) {
+ mService.mSupportsMultiWindow = true;
+
+ final TaskRecord task = taskPresent
+ ? new TaskBuilder(mService.mStackSupervisor).setStack(mStack).build() : null;
+
+ if (task != null) {
+ task.setResizeMode(taskResizeable ? RESIZE_MODE_RESIZEABLE : RESIZE_MODE_UNRESIZEABLE);
+ }
+
+ final ActivityRecord record = new ActivityBuilder(mService).setTask(task).build();
+ record.info.resizeMode = activityResizeable
+ ? RESIZE_MODE_RESIZEABLE : RESIZE_MODE_UNRESIZEABLE;
+
+ record.canBeLaunchedOnDisplay(DEFAULT_DISPLAY);
+
+
+ verify(mService.mStackSupervisor, times(1)).canPlaceEntityOnDisplay(anyInt(), eq(expected),
+ anyInt(), anyInt(), eq(record.info));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityStackSupervisorTests.java b/services/tests/servicestests/src/com/android/server/am/ActivityStackSupervisorTests.java
new file mode 100644
index 0000000..1ce41a6
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityStackSupervisorTests.java
@@ -0,0 +1,410 @@
+/*
+ * 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.am;
+
+import static android.app.ActivityManager.START_DELIVERED_TO_TOP;
+import static android.app.ActivityManager.START_TASK_TO_FRONT;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+import static android.content.pm.ActivityInfo.FLAG_ALWAYS_FOCUSABLE;
+import static android.content.pm.ActivityInfo.FLAG_SHOW_WHEN_LOCKED;
+
+import static com.android.server.am.ActivityStack.REMOVE_TASK_MODE_DESTROYING;
+import static com.android.server.am.ActivityStackSupervisor
+ .MATCH_TASK_IN_STACKS_OR_RECENT_TASKS_AND_RESTORE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.ActivityOptions;
+import android.app.WaitResult;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.SparseIntArray;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.invocation.InvocationOnMock;
+
+import java.util.ArrayList;
+
+/**
+ * Tests for the {@link ActivityStackSupervisor} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.am.ActivityStackSupervisorTests
+ */
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class ActivityStackSupervisorTests extends ActivityTestsBase {
+ private ActivityManagerService mService;
+ private ActivityStackSupervisor mSupervisor;
+ private ActivityStack mFullscreenStack;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mService = createActivityManagerService();
+ mSupervisor = mService.mStackSupervisor;
+ mFullscreenStack = mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ }
+
+ /**
+ * This test ensures that we do not try to restore a task based off an invalid task id. The
+ * stack supervisor is a test version so there will be no tasks present. We should expect
+ * {@code null} to be returned in this case.
+ */
+ @Test
+ public void testRestoringInvalidTask() throws Exception {
+ TaskRecord task = mSupervisor.anyTaskForIdLocked(0 /*taskId*/,
+ MATCH_TASK_IN_STACKS_OR_RECENT_TASKS_AND_RESTORE, null, false /* onTop */);
+ assertNull(task);
+ }
+
+ /**
+ * This test ensures that an existing task in the pinned stack is moved to the fullscreen
+ * activity stack when a new task is added.
+ */
+ @Test
+ public void testReplacingTaskInPinnedStack() throws Exception {
+ final ActivityRecord firstActivity = new ActivityBuilder(mService).setCreateTask(true)
+ .setStack(mFullscreenStack).build();
+ final TaskRecord firstTask = firstActivity.getTask();
+
+ final ActivityRecord secondActivity = new ActivityBuilder(mService).setCreateTask(true)
+ .setStack(mFullscreenStack).build();
+ final TaskRecord secondTask = secondActivity.getTask();
+
+ mSupervisor.setFocusStackUnchecked("testReplacingTaskInPinnedStack", mFullscreenStack);
+
+ // Ensure full screen stack has both tasks.
+ ensureStackPlacement(mFullscreenStack, firstTask, secondTask);
+
+ // Move first activity to pinned stack.
+ final Rect sourceBounds = new Rect();
+ mSupervisor.moveActivityToPinnedStackLocked(firstActivity, sourceBounds,
+ 0f /*aspectRatio*/, "initialMove");
+
+ final ActivityDisplay display = mFullscreenStack.getDisplay();
+ ActivityStack pinnedStack = display.getPinnedStack();
+ // Ensure a task has moved over.
+ ensureStackPlacement(pinnedStack, firstTask);
+ ensureStackPlacement(mFullscreenStack, secondTask);
+
+ // Move second activity to pinned stack.
+ mSupervisor.moveActivityToPinnedStackLocked(secondActivity, sourceBounds,
+ 0f /*aspectRatio*/, "secondMove");
+
+ // Need to get stacks again as a new instance might have been created.
+ pinnedStack = display.getPinnedStack();
+ mFullscreenStack = display.getStack(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ // Ensure stacks have swapped tasks.
+ ensureStackPlacement(pinnedStack, secondTask);
+ ensureStackPlacement(mFullscreenStack, firstTask);
+ }
+
+ private static void ensureStackPlacement(ActivityStack stack, TaskRecord... tasks) {
+ final ArrayList<TaskRecord> stackTasks = stack.getAllTasks();
+ assertEquals(stackTasks.size(), tasks != null ? tasks.length : 0);
+
+ if (tasks == null) {
+ return;
+ }
+
+ for (TaskRecord task : tasks) {
+ assertTrue(stackTasks.contains(task));
+ }
+ }
+
+ /**
+ * Ensures that an activity is removed from the stopping activities list once it is resumed.
+ */
+ @Test
+ public void testStoppingActivityRemovedWhenResumed() throws Exception {
+ final ActivityRecord firstActivity = new ActivityBuilder(mService).setCreateTask(true)
+ .setStack(mFullscreenStack).build();
+ mSupervisor.mStoppingActivities.add(firstActivity);
+
+ firstActivity.completeResumeLocked();
+
+ assertFalse(mSupervisor.mStoppingActivities.contains(firstActivity));
+ }
+
+ /**
+ * Ensures that waiting results are notified of launches.
+ */
+ @Test
+ public void testReportWaitingActivityLaunchedIfNeeded() throws Exception {
+ final ActivityRecord firstActivity = new ActivityBuilder(mService).setCreateTask(true)
+ .setStack(mFullscreenStack).build();
+
+ // #notifyAll will be called on the ActivityManagerService. we must hold the object lock
+ // when this happens.
+ synchronized (mSupervisor.mService) {
+ final WaitResult taskToFrontWait = new WaitResult();
+ mSupervisor.mWaitingActivityLaunched.add(taskToFrontWait);
+ mSupervisor.reportWaitingActivityLaunchedIfNeeded(firstActivity, START_TASK_TO_FRONT);
+
+ assertTrue(mSupervisor.mWaitingActivityLaunched.isEmpty());
+ assertEquals(taskToFrontWait.result, START_TASK_TO_FRONT);
+ assertEquals(taskToFrontWait.who, null);
+
+ final WaitResult deliverToTopWait = new WaitResult();
+ mSupervisor.mWaitingActivityLaunched.add(deliverToTopWait);
+ mSupervisor.reportWaitingActivityLaunchedIfNeeded(firstActivity,
+ START_DELIVERED_TO_TOP);
+
+ assertTrue(mSupervisor.mWaitingActivityLaunched.isEmpty());
+ assertEquals(deliverToTopWait.result, START_DELIVERED_TO_TOP);
+ assertEquals(deliverToTopWait.who, firstActivity.realActivity);
+ }
+ }
+
+ @Test
+ public void testApplySleepTokensLocked() throws Exception {
+ final ActivityDisplay display = mSupervisor.getDefaultDisplay();
+ final KeyguardController keyguard = mSupervisor.getKeyguardController();
+ final ActivityStack stack = mock(ActivityStack.class);
+ display.addChild(stack, 0 /* position */);
+
+ // Make sure we wake and resume in the case the display is turning on and the keyguard is
+ // not showing.
+ verifySleepTokenBehavior(display, keyguard, stack, true /*displaySleeping*/,
+ false /* displayShouldSleep */, true /* isFocusedStack */,
+ false /* keyguardShowing */, true /* expectWakeFromSleep */,
+ true /* expectResumeTopActivity */);
+
+ // Make sure we wake and don't resume when the display is turning on and the keyguard is
+ // showing.
+ verifySleepTokenBehavior(display, keyguard, stack, true /*displaySleeping*/,
+ false /* displayShouldSleep */, true /* isFocusedStack */,
+ true /* keyguardShowing */, true /* expectWakeFromSleep */,
+ false /* expectResumeTopActivity */);
+
+ // Make sure we wake and don't resume when the display is turning on and the keyguard is
+ // not showing as unfocused.
+ verifySleepTokenBehavior(display, keyguard, stack, true /*displaySleeping*/,
+ false /* displayShouldSleep */, false /* isFocusedStack */,
+ false /* keyguardShowing */, true /* expectWakeFromSleep */,
+ false /* expectResumeTopActivity */);
+
+ // Should not do anything if the display state hasn't changed.
+ verifySleepTokenBehavior(display, keyguard, stack, false /*displaySleeping*/,
+ false /* displayShouldSleep */, true /* isFocusedStack */,
+ false /* keyguardShowing */, false /* expectWakeFromSleep */,
+ false /* expectResumeTopActivity */);
+ }
+
+ private void verifySleepTokenBehavior(ActivityDisplay display, KeyguardController keyguard,
+ ActivityStack stack, boolean displaySleeping, boolean displayShouldSleep,
+ boolean isFocusedStack, boolean keyguardShowing, boolean expectWakeFromSleep,
+ boolean expectResumeTopActivity) {
+ reset(stack);
+
+ doReturn(displayShouldSleep).when(display).shouldSleep();
+ doReturn(displaySleeping).when(display).isSleeping();
+ doReturn(keyguardShowing).when(keyguard).isKeyguardOrAodShowing(anyInt());
+
+ mSupervisor.mFocusedStack = isFocusedStack ? stack : null;
+ mSupervisor.applySleepTokensLocked(true);
+ verify(stack, times(expectWakeFromSleep ? 1 : 0)).awakeFromSleepingLocked();
+ verify(stack, times(expectResumeTopActivity ? 1 : 0)).resumeTopActivityUncheckedLocked(
+ null /* target */, null /* targetOptions */);
+ }
+
+ @Test
+ public void testTopRunningActivityLockedWithNonExistentDisplay() throws Exception {
+ // Create display that ActivityManagerService does not know about
+ final int unknownDisplayId = 100;
+
+ doAnswer((InvocationOnMock invocationOnMock) -> {
+ final SparseIntArray displayIds = invocationOnMock.<SparseIntArray>getArgument(0);
+ displayIds.put(0, unknownDisplayId);
+ return null;
+ }).when(mSupervisor.mWindowManager).getDisplaysInFocusOrder(any());
+
+ mSupervisor.mFocusedStack = mock(ActivityStack.class);
+
+ // Supervisor should skip over the non-existent display.
+ assertEquals(null, mSupervisor.topRunningActivityLocked());
+ }
+
+ /**
+ * Verifies that removal of activity with task and stack is done correctly.
+ */
+ @Test
+ public void testRemovingStackOnAppCrash() throws Exception {
+ final ActivityDisplay defaultDisplay = mService.mStackSupervisor.getDefaultDisplay();
+ final int originalStackCount = defaultDisplay.getChildCount();
+ final ActivityStack stack = mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, false /* onTop */);
+ final ActivityRecord firstActivity = new ActivityBuilder(mService).setCreateTask(true)
+ .setStack(stack).build();
+
+ assertEquals(originalStackCount + 1, defaultDisplay.getChildCount());
+
+ // Let's pretend that the app has crashed.
+ firstActivity.app.thread = null;
+ mService.mStackSupervisor.finishTopCrashedActivitiesLocked(firstActivity.app, "test");
+
+ // Verify that the stack was removed.
+ assertEquals(originalStackCount, defaultDisplay.getChildCount());
+ }
+
+ @Test
+ public void testFocusability() throws Exception {
+ final ActivityStack stack = mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ final ActivityRecord activity = new ActivityBuilder(mService).setCreateTask(true)
+ .setStack(stack).build();
+
+ // Under split screen primary we should be focusable when not minimized
+ mService.mStackSupervisor.setDockedStackMinimized(false);
+ assertTrue(stack.isFocusable());
+ assertTrue(activity.isFocusable());
+
+ // Under split screen primary we should not be focusable when minimized
+ mService.mStackSupervisor.setDockedStackMinimized(true);
+ assertFalse(stack.isFocusable());
+ assertFalse(activity.isFocusable());
+
+ final ActivityStack pinnedStack = mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ final ActivityRecord pinnedActivity = new ActivityBuilder(mService).setCreateTask(true)
+ .setStack(pinnedStack).build();
+
+ // We should not be focusable when in pinned mode
+ assertFalse(pinnedStack.isFocusable());
+ assertFalse(pinnedActivity.isFocusable());
+
+ // Add flag forcing focusability.
+ pinnedActivity.info.flags |= FLAG_ALWAYS_FOCUSABLE;
+
+ // We should not be focusable when in pinned mode
+ assertTrue(pinnedStack.isFocusable());
+ assertTrue(pinnedActivity.isFocusable());
+
+ // Without the overridding activity, stack should not be focusable.
+ pinnedStack.removeTask(pinnedActivity.getTask(), "testFocusability",
+ REMOVE_TASK_MODE_DESTROYING);
+ assertFalse(pinnedStack.isFocusable());
+ }
+
+ /**
+ * Verifies the correct activity is returned when querying the top running activity.
+ */
+ @Test
+ public void testTopRunningActivity() throws Exception {
+ // Create stack to hold focus
+ final ActivityStack emptyStack = mService.mStackSupervisor.getDefaultDisplay()
+ .createStack(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+
+ final KeyguardController keyguard = mSupervisor.getKeyguardController();
+ final ActivityStack stack = mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ final ActivityRecord activity = new ActivityBuilder(mService).setCreateTask(true)
+ .setStack(stack).build();
+
+ mSupervisor.mFocusedStack = emptyStack;
+
+ doAnswer((InvocationOnMock invocationOnMock) -> {
+ final SparseIntArray displayIds = invocationOnMock.<SparseIntArray>getArgument(0);
+ displayIds.put(0, mSupervisor.getDefaultDisplay().mDisplayId);
+ return null;
+ }).when(mSupervisor.mWindowManager).getDisplaysInFocusOrder(any());
+
+ // Make sure the top running activity is not affected when keyguard is not locked
+ assertEquals(activity, mService.mStackSupervisor.topRunningActivityLocked());
+ assertEquals(activity, mService.mStackSupervisor.topRunningActivityLocked(
+ true /* considerKeyguardState */));
+
+ // Check to make sure activity not reported when it cannot show on lock and lock is on.
+ doReturn(true).when(keyguard).isKeyguardLocked();
+ assertEquals(activity, mService.mStackSupervisor.topRunningActivityLocked());
+ assertEquals(null, mService.mStackSupervisor.topRunningActivityLocked(
+ true /* considerKeyguardState */));
+
+ // Change focus to stack with activity.
+ mSupervisor.mFocusedStack = stack;
+ assertEquals(activity, mService.mStackSupervisor.topRunningActivityLocked());
+ assertEquals(null, mService.mStackSupervisor.topRunningActivityLocked(
+ true /* considerKeyguardState */));
+
+ // Add activity that should be shown on the keyguard.
+ final ActivityRecord showWhenLockedActivity = new ActivityBuilder(mService)
+ .setCreateTask(true)
+ .setStack(stack)
+ .setActivityFlags(FLAG_SHOW_WHEN_LOCKED)
+ .build();
+
+ // Ensure the show when locked activity is returned.
+ assertEquals(showWhenLockedActivity, mService.mStackSupervisor.topRunningActivityLocked());
+ assertEquals(showWhenLockedActivity, mService.mStackSupervisor.topRunningActivityLocked(
+ true /* considerKeyguardState */));
+
+ // Change focus back to empty stack
+ mSupervisor.mFocusedStack = emptyStack;
+ // Ensure the show when locked activity is returned when not the focused stack
+ assertEquals(showWhenLockedActivity, mService.mStackSupervisor.topRunningActivityLocked());
+ assertEquals(showWhenLockedActivity, mService.mStackSupervisor.topRunningActivityLocked(
+ true /* considerKeyguardState */));
+ }
+
+ /**
+ * Verify that split-screen primary stack will be chosen if activity is launched that targets
+ * split-screen secondary, but a matching existing instance is found on top of split-screen
+ * primary stack.
+ */
+ @Test
+ public void testSplitScreenPrimaryChosenWhenTopActivityLaunchedToSecondary() throws Exception {
+ // Create primary split-screen stack with a task and an activity.
+ final ActivityStack primaryStack = mService.mStackSupervisor.getDefaultDisplay()
+ .createStack(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD,
+ true /* onTop */);
+ final TaskRecord task = new TaskBuilder(mSupervisor).setStack(primaryStack).build();
+ final ActivityRecord r = new ActivityBuilder(mService).setTask(task).build();
+
+ // Find a launch stack for the top activity in split-screen primary, while requesting
+ // split-screen secondary.
+ final ActivityOptions options = ActivityOptions.makeBasic();
+ options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
+ final ActivityStack result = mSupervisor.getLaunchStack(r, options, task, true /* onTop */);
+
+ // Assert that the primary stack is returned.
+ assertEquals(primaryStack, result);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityStackTests.java b/services/tests/servicestests/src/com/android/server/am/ActivityStackTests.java
new file mode 100644
index 0000000..01425ed
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityStackTests.java
@@ -0,0 +1,631 @@
+/*
+ * 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.am;
+
+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 com.android.server.am.ActivityStack.ActivityState.PAUSING;
+import static com.android.server.am.ActivityStack.ActivityState.RESUMED;
+import static com.android.server.am.ActivityStack.REMOVE_TASK_MODE_DESTROYING;
+
+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.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.pm.ActivityInfo;
+import android.os.UserHandle;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.runner.RunWith;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for the {@link ActivityStack} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.am.ActivityStackTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class ActivityStackTests extends ActivityTestsBase {
+ private ActivityManagerService mService;
+ private ActivityStackSupervisor mSupervisor;
+ private ActivityDisplay mDefaultDisplay;
+ private ActivityStack mStack;
+ private TaskRecord mTask;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mService = createActivityManagerService();
+ mSupervisor = mService.mStackSupervisor;
+ mDefaultDisplay = mService.mStackSupervisor.getDefaultDisplay();
+ mStack = mDefaultDisplay.createStack(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD,
+ true /* onTop */);
+ mTask = new TaskBuilder(mSupervisor).setStack(mStack).build();
+ }
+
+ @Test
+ public void testEmptyTaskCleanupOnRemove() throws Exception {
+ assertNotNull(mTask.getWindowContainerController());
+ mStack.removeTask(mTask, "testEmptyTaskCleanupOnRemove", REMOVE_TASK_MODE_DESTROYING);
+ assertNull(mTask.getWindowContainerController());
+ }
+
+ @Test
+ public void testOccupiedTaskCleanupOnRemove() throws Exception {
+ final ActivityRecord r = new ActivityBuilder(mService).setTask(mTask).build();
+ assertNotNull(mTask.getWindowContainerController());
+ mStack.removeTask(mTask, "testOccupiedTaskCleanupOnRemove", REMOVE_TASK_MODE_DESTROYING);
+ assertNotNull(mTask.getWindowContainerController());
+ }
+
+ @Test
+ public void testResumedActivity() throws Exception {
+ final ActivityRecord r = new ActivityBuilder(mService).setTask(mTask).build();
+ assertEquals(mStack.getResumedActivity(), null);
+ r.setState(RESUMED, "testResumedActivity");
+ assertEquals(mStack.getResumedActivity(), r);
+ r.setState(PAUSING, "testResumedActivity");
+ assertEquals(mStack.getResumedActivity(), null);
+ }
+
+ @Test
+ public void testResumedActivityFromTaskReparenting() {
+ final ActivityRecord r = new ActivityBuilder(mService).setTask(mTask).build();
+ // Ensure moving task between two stacks updates resumed activity
+ r.setState(RESUMED, "testResumedActivityFromTaskReparenting");
+ assertEquals(mStack.getResumedActivity(), r);
+
+ final ActivityStack destStack = mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+
+ mTask.reparent(destStack, true /* toTop */, TaskRecord.REPARENT_KEEP_STACK_AT_FRONT,
+ false /* animate */, true /* deferResume*/,
+ "testResumedActivityFromTaskReparenting");
+
+ assertEquals(mStack.getResumedActivity(), null);
+ assertEquals(destStack.getResumedActivity(), r);
+ }
+
+ @Test
+ public void testResumedActivityFromActivityReparenting() {
+ final ActivityRecord r = new ActivityBuilder(mService).setTask(mTask).build();
+ // Ensure moving task between two stacks updates resumed activity
+ r.setState(RESUMED, "testResumedActivityFromActivityReparenting");
+ assertEquals(mStack.getResumedActivity(), r);
+
+ final ActivityStack destStack = mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ final TaskRecord destTask = new TaskBuilder(mSupervisor).setStack(destStack).build();
+
+ mTask.removeActivity(r);
+ destTask.addActivityToTop(r);
+
+ assertEquals(mStack.getResumedActivity(), null);
+ assertEquals(destStack.getResumedActivity(), r);
+ }
+
+ @Test
+ public void testPrimarySplitScreenToFullscreenWhenMovedToBack() throws Exception {
+ // Create primary splitscreen stack. This will create secondary stacks and places the
+ // existing fullscreen stack on the bottom.
+ final ActivityStack primarySplitScreen = mDefaultDisplay.createStack(
+ WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+
+ // Assert windowing mode.
+ assertEquals(primarySplitScreen.getWindowingMode(), WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
+
+ // Move primary to back.
+ primarySplitScreen.moveToBack("testPrimarySplitScreenToFullscreenWhenMovedToBack",
+ null /* task */);
+
+ // Assert that stack is at the bottom.
+ assertEquals(mDefaultDisplay.getIndexOf(primarySplitScreen), 0);
+
+ // Ensure no longer in splitscreen.
+ assertEquals(primarySplitScreen.getWindowingMode(), WINDOWING_MODE_FULLSCREEN);
+ }
+
+ @Test
+ public void testStopActivityWhenActivityDestroyed() throws Exception {
+ final ActivityRecord r = new ActivityBuilder(mService).setTask(mTask).build();
+ r.info.flags |= ActivityInfo.FLAG_NO_HISTORY;
+ mSupervisor.setFocusStackUnchecked("testStopActivityWithDestroy", mStack);
+ mStack.stopActivityLocked(r);
+ // Mostly testing to make sure there is a crash in the call part, so if we get here we are
+ // good-to-go!
+ }
+
+ @Test
+ public void testFindTaskWithOverlay() throws Exception {
+ final ActivityRecord r = new ActivityBuilder(mService)
+ .setCreateTask(true)
+ .setStack(mStack)
+ .setUid(0)
+ .build();
+ final TaskRecord task = r.getTask();
+ // Overlay must be for a different user to prevent recognizing a matching top activity
+ final ActivityRecord taskOverlay = new ActivityBuilder(mService).setTask(task)
+ .setUid(UserHandle.PER_USER_RANGE * 2).build();
+ taskOverlay.mTaskOverlay = true;
+
+ final ActivityStackSupervisor.FindTaskResult result =
+ new ActivityStackSupervisor.FindTaskResult();
+ mStack.findTaskLocked(r, result);
+
+ assertEquals(task.getTopActivity(false /* includeOverlays */), r);
+ assertEquals(task.getTopActivity(true /* includeOverlays */), taskOverlay);
+ assertNotNull(result.r);
+ }
+
+ @Test
+ public void testShouldBeVisible_Fullscreen() throws Exception {
+ final TestActivityStack homeStack = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_HOME, true /* onTop */);
+ final ActivityStack pinnedStack = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+
+ assertTrue(homeStack.shouldBeVisible(null /* starting */));
+ assertTrue(pinnedStack.shouldBeVisible(null /* starting */));
+
+ final TestActivityStack fullscreenStack = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ // Home stack shouldn't be visible behind an opaque fullscreen stack, but pinned stack
+ // should be visible since it is always on-top.
+ fullscreenStack.setIsTranslucent(false);
+ assertFalse(homeStack.shouldBeVisible(null /* starting */));
+ assertTrue(pinnedStack.shouldBeVisible(null /* starting */));
+ assertTrue(fullscreenStack.shouldBeVisible(null /* starting */));
+
+ // Home stack should be visible behind a translucent fullscreen stack.
+ fullscreenStack.setIsTranslucent(true);
+ assertTrue(homeStack.shouldBeVisible(null /* starting */));
+ assertTrue(pinnedStack.shouldBeVisible(null /* starting */));
+ }
+
+ @Test
+ public void testShouldBeVisible_SplitScreen() throws Exception {
+ final TestActivityStack homeStack = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_HOME, true /* onTop */);
+ // Home stack should always be fullscreen for this test.
+ homeStack.setSupportsSplitScreen(false);
+ final TestActivityStack splitScreenPrimary = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ final TestActivityStack splitScreenSecondary = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+
+ // Home stack shouldn't be visible if both halves of split-screen are opaque.
+ splitScreenPrimary.setIsTranslucent(false);
+ splitScreenSecondary.setIsTranslucent(false);
+ assertFalse(homeStack.shouldBeVisible(null /* starting */));
+ assertTrue(splitScreenPrimary.shouldBeVisible(null /* starting */));
+ assertTrue(splitScreenSecondary.shouldBeVisible(null /* starting */));
+
+ // Home stack should be visible if one of the halves of split-screen is translucent.
+ splitScreenPrimary.setIsTranslucent(true);
+ assertTrue(homeStack.shouldBeVisible(null /* starting */));
+ assertTrue(splitScreenPrimary.shouldBeVisible(null /* starting */));
+ assertTrue(splitScreenSecondary.shouldBeVisible(null /* starting */));
+
+ final TestActivityStack splitScreenSecondary2 = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ // First split-screen secondary shouldn't be visible behind another opaque split-split
+ // secondary.
+ splitScreenSecondary2.setIsTranslucent(false);
+ assertFalse(splitScreenSecondary.shouldBeVisible(null /* starting */));
+ assertTrue(splitScreenSecondary2.shouldBeVisible(null /* starting */));
+
+ // First split-screen secondary should be visible behind another translucent split-screen
+ // secondary.
+ splitScreenSecondary2.setIsTranslucent(true);
+ assertTrue(splitScreenSecondary.shouldBeVisible(null /* starting */));
+ assertTrue(splitScreenSecondary2.shouldBeVisible(null /* starting */));
+
+ final TestActivityStack assistantStack = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_ASSISTANT, true /* onTop */);
+
+ // Split-screen stacks shouldn't be visible behind an opaque fullscreen stack.
+ assistantStack.setIsTranslucent(false);
+ assertTrue(assistantStack.shouldBeVisible(null /* starting */));
+ assertFalse(splitScreenPrimary.shouldBeVisible(null /* starting */));
+ assertFalse(splitScreenSecondary.shouldBeVisible(null /* starting */));
+ assertFalse(splitScreenSecondary2.shouldBeVisible(null /* starting */));
+
+ // Split-screen stacks should be visible behind a translucent fullscreen stack.
+ assistantStack.setIsTranslucent(true);
+ assertTrue(assistantStack.shouldBeVisible(null /* starting */));
+ assertTrue(splitScreenPrimary.shouldBeVisible(null /* starting */));
+ assertTrue(splitScreenSecondary.shouldBeVisible(null /* starting */));
+ assertTrue(splitScreenSecondary2.shouldBeVisible(null /* starting */));
+
+ // Assistant stack shouldn't be visible behind translucent split-screen stack
+ assistantStack.setIsTranslucent(false);
+ splitScreenPrimary.setIsTranslucent(true);
+ splitScreenSecondary2.setIsTranslucent(true);
+ splitScreenSecondary2.moveToFront("testShouldBeVisible_SplitScreen");
+ splitScreenPrimary.moveToFront("testShouldBeVisible_SplitScreen");
+ assertFalse(assistantStack.shouldBeVisible(null /* starting */));
+ assertTrue(splitScreenPrimary.shouldBeVisible(null /* starting */));
+ assertTrue(splitScreenSecondary2.shouldBeVisible(null /* starting */));
+ }
+
+ @Test
+ public void testShouldBeVisible_Finishing() throws Exception {
+ final TestActivityStack homeStack = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_HOME, true /* onTop */);
+ final TestActivityStack translucentStack = createStackForShouldBeVisibleTest(
+ mDefaultDisplay, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD,
+ true /* onTop */);
+ translucentStack.setIsTranslucent(true);
+
+ assertTrue(homeStack.shouldBeVisible(null /* starting */));
+ assertTrue(translucentStack.shouldBeVisible(null /* starting */));
+
+ final ActivityRecord topRunningHomeActivity = homeStack.topRunningActivityLocked();
+ topRunningHomeActivity.finishing = true;
+ final ActivityRecord topRunningTranslucentActivity =
+ translucentStack.topRunningActivityLocked();
+ topRunningTranslucentActivity.finishing = true;
+
+ // Home shouldn't be visible since its activity is marked as finishing and it isn't the top
+ // of the stack list.
+ assertFalse(homeStack.shouldBeVisible(null /* starting */));
+ // Home should be visible if we are starting an activity within it.
+ assertTrue(homeStack.shouldBeVisible(topRunningHomeActivity /* starting */));
+ // The translucent stack should be visible since it is the top of the stack list even though
+ // it has its activity marked as finishing.
+ assertTrue(translucentStack.shouldBeVisible(null /* starting */));
+ }
+
+ @Test
+ public void testMoveHomeStackBehindBottomMostVisibleStack_NoMoveHomeBehindFullscreen() {
+ mDefaultDisplay.removeChild(mStack);
+
+ final TestActivityStack homeStack = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_HOME, true /* onTop */);
+ final TestActivityStack fullscreenStack = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+
+ homeStack.setIsTranslucent(false);
+ fullscreenStack.setIsTranslucent(false);
+
+ // Ensure that we don't move the home stack if it is already behind the top fullscreen stack
+ int homeStackIndex = mDefaultDisplay.getIndexOf(homeStack);
+ assertTrue(mDefaultDisplay.getStackAbove(homeStack) == fullscreenStack);
+ mDefaultDisplay.moveStackBehindBottomMostVisibleStack(homeStack);
+ assertTrue(mDefaultDisplay.getIndexOf(homeStack) == homeStackIndex);
+ }
+
+ @Test
+ public void testMoveHomeStackBehindBottomMostVisibleStack_NoMoveHomeBehindTranslucent() {
+ mDefaultDisplay.removeChild(mStack);
+
+ final TestActivityStack homeStack = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_HOME, true /* onTop */);
+ final TestActivityStack fullscreenStack = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+
+ homeStack.setIsTranslucent(false);
+ fullscreenStack.setIsTranslucent(true);
+
+ // Ensure that we don't move the home stack if it is already behind the top fullscreen stack
+ int homeStackIndex = mDefaultDisplay.getIndexOf(homeStack);
+ assertTrue(mDefaultDisplay.getStackAbove(homeStack) == fullscreenStack);
+ mDefaultDisplay.moveStackBehindBottomMostVisibleStack(homeStack);
+ assertTrue(mDefaultDisplay.getIndexOf(homeStack) == homeStackIndex);
+ }
+
+ @Test
+ public void testMoveHomeStackBehindBottomMostVisibleStack_NoMoveHomeOnTop() {
+ mDefaultDisplay.removeChild(mStack);
+
+ final TestActivityStack fullscreenStack = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ final TestActivityStack homeStack = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_HOME, true /* onTop */);
+
+ homeStack.setIsTranslucent(false);
+ fullscreenStack.setIsTranslucent(false);
+
+ // Ensure we don't move the home stack if it is already on top
+ int homeStackIndex = mDefaultDisplay.getIndexOf(homeStack);
+ assertTrue(mDefaultDisplay.getStackAbove(homeStack) == null);
+ mDefaultDisplay.moveStackBehindBottomMostVisibleStack(homeStack);
+ assertTrue(mDefaultDisplay.getIndexOf(homeStack) == homeStackIndex);
+ }
+
+ @Test
+ public void testMoveHomeStackBehindBottomMostVisibleStack_MoveHomeBehindFullscreen() {
+ mDefaultDisplay.removeChild(mStack);
+
+ final TestActivityStack homeStack = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_HOME, true /* onTop */);
+ final TestActivityStack fullscreenStack1 = createStackForShouldBeVisibleTest(
+ mDefaultDisplay, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD,
+ true /* onTop */);
+ final TestActivityStack fullscreenStack2 = createStackForShouldBeVisibleTest(
+ mDefaultDisplay, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD,
+ true /* onTop */);
+ final TestActivityStack pinnedStack = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+
+ homeStack.setIsTranslucent(false);
+ fullscreenStack1.setIsTranslucent(false);
+ fullscreenStack2.setIsTranslucent(false);
+
+ // Ensure that we move the home stack behind the bottom most fullscreen stack, ignoring the
+ // pinned stack
+ assertTrue(mDefaultDisplay.getStackAbove(homeStack) == fullscreenStack1);
+ mDefaultDisplay.moveStackBehindBottomMostVisibleStack(homeStack);
+ assertTrue(mDefaultDisplay.getStackAbove(homeStack) == fullscreenStack2);
+ }
+
+ @Test
+ public void testMoveHomeStackBehindBottomMostVisibleStack_MoveHomeBehindFullscreenAndTranslucent() {
+ mDefaultDisplay.removeChild(mStack);
+
+ final TestActivityStack homeStack = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_HOME, true /* onTop */);
+ final TestActivityStack fullscreenStack1 = createStackForShouldBeVisibleTest(
+ mDefaultDisplay, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD,
+ true /* onTop */);
+ final TestActivityStack fullscreenStack2 = createStackForShouldBeVisibleTest(
+ mDefaultDisplay, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD,
+ true /* onTop */);
+
+ homeStack.setIsTranslucent(false);
+ fullscreenStack1.setIsTranslucent(false);
+ fullscreenStack2.setIsTranslucent(true);
+
+ // Ensure that we move the home stack behind the bottom most non-translucent fullscreen
+ // stack
+ assertTrue(mDefaultDisplay.getStackAbove(homeStack) == fullscreenStack1);
+ mDefaultDisplay.moveStackBehindBottomMostVisibleStack(homeStack);
+ assertTrue(mDefaultDisplay.getStackAbove(homeStack) == fullscreenStack1);
+ }
+
+ @Test
+ public void testMoveHomeStackBehindStack_BehindHomeStack() {
+ mDefaultDisplay.removeChild(mStack);
+
+ final TestActivityStack fullscreenStack1 = createStackForShouldBeVisibleTest(
+ mDefaultDisplay, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD,
+ true /* onTop */);
+ final TestActivityStack fullscreenStack2 = createStackForShouldBeVisibleTest(
+ mDefaultDisplay, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD,
+ true /* onTop */);
+ final TestActivityStack homeStack = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_HOME, true /* onTop */);
+
+ homeStack.setIsTranslucent(false);
+ fullscreenStack1.setIsTranslucent(false);
+ fullscreenStack2.setIsTranslucent(false);
+
+ // Ensure we don't move the home stack behind itself
+ int homeStackIndex = mDefaultDisplay.getIndexOf(homeStack);
+ mDefaultDisplay.moveStackBehindStack(homeStack, homeStack);
+ assertTrue(mDefaultDisplay.getIndexOf(homeStack) == homeStackIndex);
+ }
+
+ @Test
+ public void testMoveHomeStackBehindStack() {
+ mDefaultDisplay.removeChild(mStack);
+
+ final TestActivityStack fullscreenStack1 = createStackForShouldBeVisibleTest(
+ mDefaultDisplay, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD,
+ true /* onTop */);
+ final TestActivityStack fullscreenStack2 = createStackForShouldBeVisibleTest(
+ mDefaultDisplay, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD,
+ true /* onTop */);
+ final TestActivityStack fullscreenStack3 = createStackForShouldBeVisibleTest(
+ mDefaultDisplay, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD,
+ true /* onTop */);
+ final TestActivityStack fullscreenStack4 = createStackForShouldBeVisibleTest(
+ mDefaultDisplay, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD,
+ true /* onTop */);
+ final TestActivityStack homeStack = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_HOME, true /* onTop */);
+
+ mDefaultDisplay.moveStackBehindStack(homeStack, fullscreenStack1);
+ assertTrue(mDefaultDisplay.getStackAbove(homeStack) == fullscreenStack1);
+ mDefaultDisplay.moveStackBehindStack(homeStack, fullscreenStack2);
+ assertTrue(mDefaultDisplay.getStackAbove(homeStack) == fullscreenStack2);
+ mDefaultDisplay.moveStackBehindStack(homeStack, fullscreenStack4);
+ assertTrue(mDefaultDisplay.getStackAbove(homeStack) == fullscreenStack4);
+ mDefaultDisplay.moveStackBehindStack(homeStack, fullscreenStack2);
+ assertTrue(mDefaultDisplay.getStackAbove(homeStack) == fullscreenStack2);
+ }
+
+ @Test
+ public void testSplitScreenMoveToFront() throws Exception {
+ final TestActivityStack splitScreenPrimary = createStackForShouldBeVisibleTest(
+ mDefaultDisplay, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD,
+ true /* onTop */);
+ final TestActivityStack splitScreenSecondary = createStackForShouldBeVisibleTest(
+ mDefaultDisplay, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_STANDARD,
+ true /* onTop */);
+ final TestActivityStack assistantStack = createStackForShouldBeVisibleTest(mDefaultDisplay,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_ASSISTANT, true /* onTop */);
+
+ splitScreenPrimary.setIsTranslucent(false);
+ splitScreenSecondary.setIsTranslucent(false);
+ assistantStack.setIsTranslucent(false);
+
+ assertFalse(splitScreenPrimary.shouldBeVisible(null /* starting */));
+ assertFalse(splitScreenSecondary.shouldBeVisible(null /* starting */));
+ assertTrue(assistantStack.shouldBeVisible(null /* starting */));
+
+ splitScreenSecondary.moveToFront("testSplitScreenMoveToFront");
+
+ assertTrue(splitScreenPrimary.shouldBeVisible(null /* starting */));
+ assertTrue(splitScreenSecondary.shouldBeVisible(null /* starting */));
+ assertFalse(assistantStack.shouldBeVisible(null /* starting */));
+ }
+
+ private <T extends ActivityStack> T createStackForShouldBeVisibleTest(
+ ActivityDisplay display, int windowingMode, int activityType, boolean onTop) {
+ final T stack = display.createStack(windowingMode, activityType, onTop);
+ final ActivityRecord r = new ActivityBuilder(mService).setUid(0).setStack(stack)
+ .setCreateTask(true).build();
+ return stack;
+ }
+
+ @Test
+ public void testFinishDisabledPackageActivities() throws Exception {
+ final ActivityRecord firstActivity = new ActivityBuilder(mService).setTask(mTask).build();
+ final ActivityRecord secondActivity = new ActivityBuilder(mService).setTask(mTask).build();
+
+ // Making the second activity a task overlay without an app means it will be removed from
+ // the task's activities as well once first activity is removed.
+ secondActivity.mTaskOverlay = true;
+ secondActivity.app = null;
+
+ assertEquals(mTask.mActivities.size(), 2);
+
+ mStack.finishDisabledPackageActivitiesLocked(firstActivity.packageName, null,
+ true /* doit */, true /* evenPersistent */, UserHandle.USER_ALL);
+
+ assertTrue(mTask.mActivities.isEmpty());
+ assertTrue(mStack.getAllTasks().isEmpty());
+ }
+
+ @Test
+ public void testHandleAppDied() throws Exception {
+ final ActivityRecord firstActivity = new ActivityBuilder(mService).setTask(mTask).build();
+ final ActivityRecord secondActivity = new ActivityBuilder(mService).setTask(mTask).build();
+
+ // Making the first activity a task overlay means it will be removed from the task's
+ // activities as well once second activity is removed as handleAppDied processes the
+ // activity list in reverse.
+ firstActivity.mTaskOverlay = true;
+ firstActivity.app = null;
+
+ // second activity will be immediately removed as it has no state.
+ secondActivity.haveState = false;
+
+ assertEquals(mTask.mActivities.size(), 2);
+
+ mStack.handleAppDiedLocked(secondActivity.app);
+
+ assertTrue(mTask.mActivities.isEmpty());
+ assertTrue(mStack.getAllTasks().isEmpty());
+ }
+
+ @Test
+ public void testShouldSleepActivities() throws Exception {
+ // When focused activity and keyguard is going away, we should not sleep regardless
+ // of the display state
+ verifyShouldSleepActivities(true /* focusedStack */, true /*keyguardGoingAway*/,
+ true /* displaySleeping */, false /* expected*/);
+
+ // When not the focused stack, defer to display sleeping state.
+ verifyShouldSleepActivities(false /* focusedStack */, true /*keyguardGoingAway*/,
+ true /* displaySleeping */, true /* expected*/);
+
+ // If keyguard is going away, defer to the display sleeping state.
+ verifyShouldSleepActivities(true /* focusedStack */, false /*keyguardGoingAway*/,
+ true /* displaySleeping */, true /* expected*/);
+ verifyShouldSleepActivities(true /* focusedStack */, false /*keyguardGoingAway*/,
+ false /* displaySleeping */, false /* expected*/);
+ }
+
+ @Test
+ public void testStackOrderChangedOnRemoveStack() throws Exception {
+ StackOrderChangedListener listener = new StackOrderChangedListener();
+ mDefaultDisplay.registerStackOrderChangedListener(listener);
+ try {
+ mDefaultDisplay.removeChild(mStack);
+ } finally {
+ mDefaultDisplay.unregisterStackOrderChangedListener(listener);
+ }
+ assertTrue(listener.changed);
+ }
+
+ @Test
+ public void testStackOrderChangedOnAddPositionStack() throws Exception {
+ mDefaultDisplay.removeChild(mStack);
+
+ StackOrderChangedListener listener = new StackOrderChangedListener();
+ mDefaultDisplay.registerStackOrderChangedListener(listener);
+ try {
+ mDefaultDisplay.addChild(mStack, 0);
+ } finally {
+ mDefaultDisplay.unregisterStackOrderChangedListener(listener);
+ }
+ assertTrue(listener.changed);
+ }
+
+ @Test
+ public void testStackOrderChangedOnPositionStack() throws Exception {
+ StackOrderChangedListener listener = new StackOrderChangedListener();
+ try {
+ final TestActivityStack fullscreenStack1 = createStackForShouldBeVisibleTest(
+ mDefaultDisplay, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD,
+ true /* onTop */);
+ mDefaultDisplay.registerStackOrderChangedListener(listener);
+ mDefaultDisplay.positionChildAtBottom(fullscreenStack1);
+ } finally {
+ mDefaultDisplay.unregisterStackOrderChangedListener(listener);
+ }
+ assertTrue(listener.changed);
+ }
+
+ private void verifyShouldSleepActivities(boolean focusedStack,
+ boolean keyguardGoingAway, boolean displaySleeping, boolean expected) {
+ mSupervisor.mFocusedStack = focusedStack ? mStack : null;
+
+ final ActivityDisplay display = mock(ActivityDisplay.class);
+ final KeyguardController keyguardController = mSupervisor.getKeyguardController();
+
+ doReturn(display).when(mSupervisor).getActivityDisplay(anyInt());
+ doReturn(keyguardGoingAway).when(keyguardController).isKeyguardGoingAway();
+ doReturn(displaySleeping).when(display).isSleeping();
+
+ assertEquals(expected, mStack.shouldSleepActivities());
+ }
+
+ private class StackOrderChangedListener implements ActivityDisplay.OnStackOrderChangedListener {
+ boolean changed = false;
+
+ @Override
+ public void onStackOrderChanged() {
+ changed = true;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityStartControllerTests.java b/services/tests/servicestests/src/com/android/server/am/ActivityStartControllerTests.java
new file mode 100644
index 0000000..7948e4c
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityStartControllerTests.java
@@ -0,0 +1,107 @@
+/*
+ * 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.am;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+
+import android.content.Intent;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.server.am.ActivityStackSupervisor.PendingActivityLaunch;
+import com.android.server.am.ActivityStarter.Factory;
+
+import org.junit.runner.RunWith;
+import org.junit.Test;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.times;
+
+import java.util.Random;
+
+/**
+ * Tests for the {@link ActivityStartController} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:ActivityStartControllerTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class ActivityStartControllerTests extends ActivityTestsBase {
+ private ActivityManagerService mService;
+ private ActivityStartController mController;
+ private Factory mFactory;
+ private ActivityStarter mStarter;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mService = createActivityManagerService();
+ mFactory = mock(Factory.class);
+ mController = new ActivityStartController(mService, mService.mStackSupervisor, mFactory);
+ mStarter = spy(new ActivityStarter(mController, mService, mService.mStackSupervisor,
+ mock(ActivityStartInterceptor.class)));
+ doReturn(mStarter).when(mFactory).obtain();
+ }
+
+ /**
+ * Ensures that pending launches are processed.
+ */
+ @Test
+ public void testPendingActivityLaunches() {
+ final Random random = new Random();
+
+ final ActivityRecord activity = new ActivityBuilder(mService).build();
+ final ActivityRecord source = new ActivityBuilder(mService).build();
+ final int startFlags = random.nextInt();
+ final ActivityStack stack = mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ final ProcessRecord process= new ProcessRecord(null, null,
+ mService.mContext.getApplicationInfo(), "name", 12345);
+
+ mController.addPendingActivityLaunch(
+ new PendingActivityLaunch(activity, source, startFlags, stack, process));
+ final boolean resume = random.nextBoolean();
+ mController.doPendingActivityLaunches(resume);
+
+ verify(mStarter, times(1)).startResolvedActivity(eq(activity), eq(source), eq(null),
+ eq(null), eq(startFlags), eq(resume), eq(null), eq(null), eq(null));
+ }
+
+
+ /**
+ * Ensures instances are recycled after execution.
+ */
+ @Test
+ public void testRecycling() throws Exception {
+ final Intent intent = new Intent();
+ final ActivityStarter optionStarter = new ActivityStarter(mController, mService,
+ mService.mStackSupervisor, mock(ActivityStartInterceptor.class));
+ optionStarter
+ .setIntent(intent)
+ .setReason("Test")
+ .execute();
+ verify(mFactory, times(1)).recycle(eq(optionStarter));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityStartInterceptorTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityStartInterceptorTest.java
new file mode 100644
index 0000000..b4b34c5
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityStartInterceptorTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 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.am;
+
+import static android.content.pm.ApplicationInfo.FLAG_SUSPENDED;
+
+import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.when;
+
+import android.app.KeyguardManager;
+import android.app.admin.DevicePolicyManagerInternal;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.UserInfo;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.support.test.filters.SmallTest;
+import android.testing.DexmakerShareClassLoaderRule;
+
+import com.android.internal.app.SuspendedAppActivity;
+import com.android.internal.app.UnlaunchableAppActivity;
+import com.android.server.LocalServices;
+import com.android.server.pm.PackageManagerService;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link ActivityStartInterceptorTest}.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.am.ActivityStartInterceptorTest
+ */
+@SmallTest
+public class ActivityStartInterceptorTest {
+ private static final int TEST_USER_ID = 1;
+ private static final int TEST_REAL_CALLING_UID = 2;
+ private static final int TEST_REAL_CALLING_PID = 3;
+ private static final String TEST_CALLING_PACKAGE = "com.test.caller";
+ private static final int TEST_START_FLAGS = 4;
+ private static final Intent ADMIN_SUPPORT_INTENT =
+ new Intent("com.test.ADMIN_SUPPORT");
+ private static final Intent CONFIRM_CREDENTIALS_INTENT =
+ new Intent("com.test.CONFIRM_CREDENTIALS");
+ private static final UserInfo PARENT_USER_INFO = new UserInfo(0 /* userId */, "parent",
+ 0 /* flags */);
+ private static final String TEST_PACKAGE_NAME = "com.test.package";
+
+ @Rule
+ public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
+ new DexmakerShareClassLoaderRule();
+
+ @Mock
+ private Context mContext;
+ @Mock
+ private ActivityManagerService mService;
+ @Mock
+ private ActivityStackSupervisor mSupervisor;
+ @Mock
+ private DevicePolicyManagerInternal mDevicePolicyManager;
+ @Mock
+ private PackageManagerInternal mPackageManagerInternal;
+ @Mock
+ private UserManager mUserManager;
+ @Mock
+ private UserController mUserController;
+ @Mock
+ private KeyguardManager mKeyguardManager;
+ @Mock
+ private PackageManagerService mPackageManager;
+
+ private ActivityStartInterceptor mInterceptor;
+ private ActivityInfo mAInfo = new ActivityInfo();
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mInterceptor = new ActivityStartInterceptor(mService, mSupervisor, mContext,
+ mUserController);
+ mInterceptor.setStates(TEST_USER_ID, TEST_REAL_CALLING_PID, TEST_REAL_CALLING_UID,
+ TEST_START_FLAGS, TEST_CALLING_PACKAGE);
+
+ // Mock DevicePolicyManagerInternal
+ LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class);
+ LocalServices.addService(DevicePolicyManagerInternal.class,
+ mDevicePolicyManager);
+ when(mDevicePolicyManager
+ .createShowAdminSupportIntent(TEST_USER_ID, true))
+ .thenReturn(ADMIN_SUPPORT_INTENT);
+ when(mService.getPackageManagerInternalLocked()).thenReturn(mPackageManagerInternal);
+
+ // Mock UserManager
+ when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
+ when(mUserManager.getProfileParent(TEST_USER_ID)).thenReturn(PARENT_USER_INFO);
+
+ // Mock KeyguardManager
+ when(mContext.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(mKeyguardManager);
+ when(mKeyguardManager.createConfirmDeviceCredentialIntent(
+ nullable(CharSequence.class), nullable(CharSequence.class), eq(TEST_USER_ID))).
+ thenReturn(CONFIRM_CREDENTIALS_INTENT);
+
+ // Mock PackageManager
+ when(mService.getPackageManager()).thenReturn(mPackageManager);
+ when(mPackageManager.getHarmfulAppWarning(TEST_PACKAGE_NAME, TEST_USER_ID))
+ .thenReturn(null);
+
+ // Initialise activity info
+ mAInfo.applicationInfo = new ApplicationInfo();
+ mAInfo.packageName = mAInfo.applicationInfo.packageName = TEST_PACKAGE_NAME;
+ }
+
+ @Test
+ public void testSuspendedByAdminPackage() {
+ // GIVEN the package we're about to launch is currently suspended
+ mAInfo.applicationInfo.flags = FLAG_SUSPENDED;
+
+ when(mPackageManagerInternal.getSuspendingPackage(TEST_PACKAGE_NAME, TEST_USER_ID))
+ .thenReturn(PLATFORM_PACKAGE_NAME);
+
+ // THEN calling intercept returns true
+ assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null));
+
+ // THEN the returned intent is the admin support intent
+ assertEquals(ADMIN_SUPPORT_INTENT, mInterceptor.mIntent);
+ }
+
+ @Test
+ public void testSuspendedPackage() {
+ mAInfo.applicationInfo.flags = FLAG_SUSPENDED;
+ final String suspendingPackage = "com.test.suspending.package";
+ final String dialogMessage = "Test Message";
+ when(mPackageManagerInternal.getSuspendingPackage(TEST_PACKAGE_NAME, TEST_USER_ID))
+ .thenReturn(suspendingPackage);
+ when(mPackageManagerInternal.getSuspendedDialogMessage(TEST_PACKAGE_NAME, TEST_USER_ID))
+ .thenReturn(dialogMessage);
+ // THEN calling intercept returns true
+ assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null));
+
+ // Check intent parameters
+ assertEquals(dialogMessage,
+ mInterceptor.mIntent.getStringExtra(SuspendedAppActivity.EXTRA_DIALOG_MESSAGE));
+ assertEquals(suspendingPackage,
+ mInterceptor.mIntent.getStringExtra(SuspendedAppActivity.EXTRA_SUSPENDING_PACKAGE));
+ assertEquals(TEST_PACKAGE_NAME,
+ mInterceptor.mIntent.getStringExtra(SuspendedAppActivity.EXTRA_SUSPENDED_PACKAGE));
+ assertEquals(TEST_USER_ID, mInterceptor.mIntent.getIntExtra(Intent.EXTRA_USER_ID, -1000));
+ }
+
+ @Test
+ public void testInterceptQuietProfile() {
+ // GIVEN that the user the activity is starting as is currently in quiet mode
+ when(mUserManager.isQuietModeEnabled(eq(UserHandle.of(TEST_USER_ID)))).thenReturn(true);
+
+ // THEN calling intercept returns true
+ assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null));
+
+ // THEN the returned intent is the quiet mode intent
+ assertTrue(UnlaunchableAppActivity.createInQuietModeDialogIntent(TEST_USER_ID)
+ .filterEquals(mInterceptor.mIntent));
+ }
+
+ @Test
+ public void testWorkChallenge() {
+ // GIVEN that the user the activity is starting as is currently locked
+ when(mUserController.shouldConfirmCredentials(TEST_USER_ID)).thenReturn(true);
+
+ // THEN calling intercept returns true
+ mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null);
+
+ // THEN the returned intent is the quiet mode intent
+ assertTrue(CONFIRM_CREDENTIALS_INTENT.filterEquals(mInterceptor.mIntent));
+ }
+
+ @Test
+ public void testNoInterception() {
+ // GIVEN that none of the interception conditions are met
+
+ // THEN calling intercept returns false
+ assertFalse(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityStarterTests.java b/services/tests/servicestests/src/com/android/server/am/ActivityStarterTests.java
new file mode 100644
index 0000000..1520859
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityStarterTests.java
@@ -0,0 +1,474 @@
+/*
+ * 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.am;
+
+import static android.app.ActivityManager.START_ABORTED;
+import static android.app.ActivityManager.START_CLASS_NOT_FOUND;
+import static android.app.ActivityManager.START_DELIVERED_TO_TOP;
+import static android.app.ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT;
+import static android.app.ActivityManager.START_INTENT_NOT_RESOLVED;
+import static android.app.ActivityManager.START_NOT_VOICE_COMPATIBLE;
+import static android.app.ActivityManager.START_PERMISSION_DENIED;
+import static android.app.ActivityManager.START_RETURN_LOCK_TASK_MODE_VIOLATION;
+import static android.app.ActivityManager.START_SUCCESS;
+import static android.app.ActivityManager.START_SWITCHES_CANCELED;
+import static android.app.ActivityManager.START_TASK_TO_FRONT;
+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 android.app.ActivityOptions;
+import android.app.IApplicationThread;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ActivityInfo.WindowLayout;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+import android.service.voice.IVoiceInteractionSession;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.Gravity;
+
+import org.junit.runner.RunWith;
+import org.junit.Test;
+
+import static android.content.Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED;
+import static com.android.server.am.ActivityManagerService.ANIMATE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyObject;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.times;
+
+import com.android.internal.os.BatteryStatsImpl;
+import com.android.server.am.ActivityStarter.Factory;
+import com.android.server.am.LaunchParamsController.LaunchParamsModifier;
+import com.android.server.am.TaskRecord.TaskRecordFactory;
+
+import java.util.ArrayList;
+
+/**
+ * Tests for the {@link ActivityStarter} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:ActivityStarterTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class ActivityStarterTests extends ActivityTestsBase {
+ private ActivityManagerService mService;
+ private ActivityStarter mStarter;
+ private ActivityStartController mController;
+
+ private static final int PRECONDITION_NO_CALLER_APP = 1;
+ private static final int PRECONDITION_NO_INTENT_COMPONENT = 1 << 1;
+ private static final int PRECONDITION_NO_ACTIVITY_INFO = 1 << 2;
+ private static final int PRECONDITION_SOURCE_PRESENT = 1 << 3;
+ private static final int PRECONDITION_REQUEST_CODE = 1 << 4;
+ private static final int PRECONDITION_SOURCE_VOICE_SESSION = 1 << 5;
+ private static final int PRECONDITION_NO_VOICE_SESSION_SUPPORT = 1 << 6;
+ private static final int PRECONDITION_DIFFERENT_UID = 1 << 7;
+ private static final int PRECONDITION_ACTIVITY_SUPPORTS_INTENT_EXCEPTION = 1 << 8;
+ private static final int PRECONDITION_CANNOT_START_ANY_ACTIVITY = 1 << 9;
+ private static final int PRECONDITION_DISALLOW_APP_SWITCHING = 1 << 10;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mService = createActivityManagerService();
+ mController = mock(ActivityStartController.class);
+ mStarter = new ActivityStarter(mController, mService, mService.mStackSupervisor,
+ mock(ActivityStartInterceptor.class));
+ }
+
+ @Test
+ public void testUpdateLaunchBounds() throws Exception {
+ // When in a non-resizeable stack, the task bounds should be updated.
+ final TaskRecord task = new TaskBuilder(mService.mStackSupervisor)
+ .setStack(mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */))
+ .build();
+ final Rect bounds = new Rect(10, 10, 100, 100);
+
+ mStarter.updateBounds(task, bounds);
+ assertEquals(task.getOverrideBounds(), bounds);
+ assertEquals(new Rect(), task.getStack().getOverrideBounds());
+
+ // When in a resizeable stack, the stack bounds should be updated as well.
+ final TaskRecord task2 = new TaskBuilder(mService.mStackSupervisor)
+ .setStack(mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD, true /* onTop */))
+ .build();
+ assertTrue(task2.getStack() instanceof PinnedActivityStack);
+ mStarter.updateBounds(task2, bounds);
+
+ verify(mService, times(1)).resizeStack(eq(task2.getStack().mStackId),
+ eq(bounds), anyBoolean(), anyBoolean(), anyBoolean(), anyInt());
+
+ // In the case of no animation, the stack and task bounds should be set immediately.
+ if (!ANIMATE) {
+ assertEquals(task2.getStack().getOverrideBounds(), bounds);
+ assertEquals(task2.getOverrideBounds(), bounds);
+ } else {
+ assertEquals(task2.getOverrideBounds(), new Rect());
+ }
+ }
+
+ @Test
+ public void testStartActivityPreconditions() throws Exception {
+ verifyStartActivityPreconditions(PRECONDITION_NO_CALLER_APP, START_PERMISSION_DENIED);
+ verifyStartActivityPreconditions(PRECONDITION_NO_INTENT_COMPONENT,
+ START_INTENT_NOT_RESOLVED);
+ verifyStartActivityPreconditions(PRECONDITION_NO_ACTIVITY_INFO, START_CLASS_NOT_FOUND);
+ verifyStartActivityPreconditions(PRECONDITION_SOURCE_PRESENT | PRECONDITION_REQUEST_CODE,
+ Intent.FLAG_ACTIVITY_FORWARD_RESULT, START_FORWARD_AND_REQUEST_CONFLICT);
+ verifyStartActivityPreconditions(
+ PRECONDITION_SOURCE_PRESENT | PRECONDITION_NO_VOICE_SESSION_SUPPORT
+ | PRECONDITION_SOURCE_VOICE_SESSION | PRECONDITION_DIFFERENT_UID,
+ START_NOT_VOICE_COMPATIBLE);
+ verifyStartActivityPreconditions(
+ PRECONDITION_SOURCE_PRESENT | PRECONDITION_NO_VOICE_SESSION_SUPPORT
+ | PRECONDITION_SOURCE_VOICE_SESSION | PRECONDITION_DIFFERENT_UID
+ | PRECONDITION_ACTIVITY_SUPPORTS_INTENT_EXCEPTION,
+ START_NOT_VOICE_COMPATIBLE);
+ verifyStartActivityPreconditions(PRECONDITION_CANNOT_START_ANY_ACTIVITY, START_ABORTED);
+ verifyStartActivityPreconditions(PRECONDITION_DISALLOW_APP_SWITCHING,
+ START_SWITCHES_CANCELED);
+ }
+
+ private static boolean containsConditions(int preconditions, int mask) {
+ return (preconditions & mask) == mask;
+ }
+
+ private void verifyStartActivityPreconditions(int preconditions, int expectedResult) {
+ verifyStartActivityPreconditions(preconditions, 0 /*launchFlags*/, expectedResult);
+ }
+
+ /**
+ * Excercises how the {@link ActivityStarter} reacts to various preconditions. The caller
+ * provides a bitmask of all the set conditions (such as {@link #PRECONDITION_NO_CALLER_APP})
+ * and the launch flags specified in the intent. The method constructs a call to
+ * {@link ActivityStarter#execute} based on these preconditions and ensures the result matches
+ * the expected. It is important to note that the method also checks side effects of the start,
+ * such as ensuring {@link ActivityOptions#abort()} is called in the relevant scenarios.
+ * @param preconditions A bitmask representing the preconditions for the launch
+ * @param launchFlags The launch flags to be provided by the launch {@link Intent}.
+ * @param expectedResult The expected result from the launch.
+ */
+ private void verifyStartActivityPreconditions(int preconditions, int launchFlags,
+ int expectedResult) {
+ final ActivityManagerService service = createActivityManagerService();
+ final IPackageManager packageManager = mock(IPackageManager.class);
+ final ActivityStartController controller = mock(ActivityStartController.class);
+
+ final ActivityStarter starter = new ActivityStarter(controller, service,
+ service.mStackSupervisor, mock(ActivityStartInterceptor.class));
+ final IApplicationThread caller = mock(IApplicationThread.class);
+
+ // If no caller app, return {@code null} {@link ProcessRecord}.
+ final ProcessRecord record = containsConditions(preconditions, PRECONDITION_NO_CALLER_APP)
+ ? null : new ProcessRecord(null, mock(BatteryStatsImpl.class),
+ mock(ApplicationInfo.class), null, 0);
+
+ doReturn(record).when(service).getRecordForAppLocked(anyObject());
+
+ final Intent intent = new Intent();
+ intent.setFlags(launchFlags);
+
+ final ActivityInfo aInfo = containsConditions(preconditions, PRECONDITION_NO_ACTIVITY_INFO)
+ ? null : new ActivityInfo();
+
+ IVoiceInteractionSession voiceSession =
+ containsConditions(preconditions, PRECONDITION_SOURCE_VOICE_SESSION)
+ ? mock(IVoiceInteractionSession.class) : null;
+
+ // Create source token
+ final ActivityBuilder builder = new ActivityBuilder(service).setTask(
+ new TaskBuilder(service.mStackSupervisor).setVoiceSession(voiceSession).build());
+
+ if (aInfo != null) {
+ aInfo.applicationInfo = new ApplicationInfo();
+ aInfo.applicationInfo.packageName =
+ ActivityBuilder.getDefaultComponent().getPackageName();
+ }
+
+ // Offset uid by one from {@link ActivityInfo} to simulate different uids.
+ if (containsConditions(preconditions, PRECONDITION_DIFFERENT_UID)) {
+ builder.setUid(aInfo.applicationInfo.uid + 1);
+ }
+
+ final ActivityRecord source = builder.build();
+
+ if (!containsConditions(preconditions, PRECONDITION_NO_INTENT_COMPONENT)) {
+ intent.setComponent(source.realActivity);
+ }
+
+ if (containsConditions(preconditions, PRECONDITION_DISALLOW_APP_SWITCHING)) {
+ doReturn(false).when(service).checkAppSwitchAllowedLocked(anyInt(), anyInt(), anyInt(),
+ anyInt(), any());
+ }
+
+ if (containsConditions(preconditions,PRECONDITION_CANNOT_START_ANY_ACTIVITY)) {
+ doReturn(false).when(service.mStackSupervisor).checkStartAnyActivityPermission(
+ any(), any(), any(), anyInt(), anyInt(), anyInt(), any(),
+ anyBoolean(), anyBoolean(), any(), any(), any());
+ }
+
+ try {
+ if (containsConditions(preconditions,
+ PRECONDITION_ACTIVITY_SUPPORTS_INTENT_EXCEPTION)) {
+ doAnswer((inv) -> {
+ throw new RemoteException();
+ }).when(packageManager).activitySupportsIntent(eq(source.realActivity), eq(intent),
+ any());
+ } else {
+ doReturn(!containsConditions(preconditions, PRECONDITION_NO_VOICE_SESSION_SUPPORT))
+ .when(packageManager).activitySupportsIntent(eq(source.realActivity),
+ eq(intent), any());
+ }
+ } catch (RemoteException e) {
+ }
+
+ final IBinder resultTo = containsConditions(preconditions, PRECONDITION_SOURCE_PRESENT)
+ || containsConditions(preconditions, PRECONDITION_SOURCE_VOICE_SESSION)
+ ? source.appToken : null;
+
+ final int requestCode = containsConditions(preconditions, PRECONDITION_REQUEST_CODE)
+ ? 1 : 0;
+
+ final int result = starter.setCaller(caller)
+ .setIntent(intent)
+ .setActivityInfo(aInfo)
+ .setResultTo(resultTo)
+ .setRequestCode(requestCode)
+ .setReason("testLaunchActivityPermissionDenied")
+ .execute();
+
+ // In some cases the expected result internally is different than the published result. We
+ // must use ActivityStarter#getExternalResult to translate.
+ assertEquals(ActivityStarter.getExternalResult(expectedResult), result);
+
+ // Ensure that {@link ActivityOptions} are aborted with unsuccessful result.
+ if (expectedResult != START_SUCCESS) {
+ final ActivityStarter optionStarter = new ActivityStarter(mController, mService,
+ mService.mStackSupervisor, mock(ActivityStartInterceptor.class));
+ final ActivityOptions options = spy(ActivityOptions.makeBasic());
+
+ final int optionResult = optionStarter.setCaller(caller)
+ .setIntent(intent)
+ .setActivityInfo(aInfo)
+ .setResultTo(resultTo)
+ .setRequestCode(requestCode)
+ .setReason("testLaunchActivityPermissionDenied")
+ .setActivityOptions(new SafeActivityOptions(options))
+ .execute();
+ verify(options, times(1)).abort();
+ }
+ }
+
+ private ActivityStarter prepareStarter(int launchFlags) {
+ // always allow test to start activity.
+ doReturn(true).when(mService.mStackSupervisor).checkStartAnyActivityPermission(
+ any(), any(), any(), anyInt(), anyInt(), anyInt(), any(),
+ anyBoolean(), anyBoolean(), any(), any(), any());
+
+ // instrument the stack and task used.
+ final ActivityStack stack = mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ final TaskRecord task = new TaskBuilder(mService.mStackSupervisor)
+ .setCreateStack(false)
+ .build();
+
+ // supervisor needs a focused stack.
+ mService.mStackSupervisor.mFocusedStack = stack;
+
+ // use factory that only returns spy task.
+ final TaskRecordFactory factory = mock(TaskRecordFactory.class);
+ TaskRecord.setTaskRecordFactory(factory);
+
+ // return task when created.
+ doReturn(task).when(factory).create(any(), anyInt(), any(), any(), any(), any());
+
+ // direct starter to use spy stack.
+ doReturn(stack).when(mService.mStackSupervisor)
+ .getLaunchStack(any(), any(), any(), anyBoolean());
+ doReturn(stack).when(mService.mStackSupervisor)
+ .getLaunchStack(any(), any(), any(), anyBoolean(), anyInt());
+
+ final Intent intent = new Intent();
+ intent.addFlags(launchFlags);
+ intent.setComponent(ActivityBuilder.getDefaultComponent());
+
+ final ActivityInfo info = new ActivityInfo();
+
+ info.applicationInfo = new ApplicationInfo();
+ info.applicationInfo.packageName = ActivityBuilder.getDefaultComponent().getPackageName();
+
+ return new ActivityStarter(mController, mService,
+ mService.mStackSupervisor, mock(ActivityStartInterceptor.class))
+ .setIntent(intent)
+ .setActivityInfo(info);
+ }
+
+ /**
+ * Ensures that values specified at launch time are passed to {@link LaunchParamsModifier}
+ * when we are laying out a new task.
+ */
+ @Test
+ public void testCreateTaskLayout() {
+ // modifier for validating passed values.
+ final LaunchParamsModifier modifier = mock(LaunchParamsModifier.class);
+ mService.mStackSupervisor.getLaunchParamsController().registerModifier(modifier);
+
+ // add custom values to activity info to make unique.
+ final ActivityInfo info = new ActivityInfo();
+ final Rect launchBounds = new Rect(0, 0, 20, 30);
+
+ final WindowLayout windowLayout =
+ new WindowLayout(10, .5f, 20, 1.0f, Gravity.NO_GRAVITY, 1, 1);
+
+ info.windowLayout = windowLayout;
+ info.applicationInfo = new ApplicationInfo();
+ info.applicationInfo.packageName = ActivityBuilder.getDefaultComponent().getPackageName();
+
+ // create starter.
+ final ActivityStarter optionStarter = prepareStarter(0 /* launchFlags */);
+
+ final ActivityOptions options = ActivityOptions.makeBasic();
+ options.setLaunchBounds(launchBounds);
+
+ // run starter.
+ optionStarter
+ .setReason("testCreateTaskLayout")
+ .setActivityInfo(info)
+ .setActivityOptions(new SafeActivityOptions(options))
+ .execute();
+
+ // verify that values are passed to the modifier.
+ verify(modifier, times(1)).onCalculate(any(), eq(windowLayout), any(), any(), eq(options),
+ any(), any());
+ }
+
+ /**
+ * This test ensures that if the intent is being delivered to a
+ */
+ @Test
+ public void testSplitScreenDeliverToTop() {
+ final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+
+ final ActivityRecord focusActivity = new ActivityBuilder(mService)
+ .setCreateTask(true)
+ .build();
+
+ focusActivity.getStack().setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
+
+ final ActivityRecord reusableActivity = new ActivityBuilder(mService)
+ .setCreateTask(true)
+ .build();
+
+ // Create reusable activity after entering split-screen so that it is the top secondary
+ // stack.
+ reusableActivity.getStack().setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
+
+ // Set focus back to primary.
+ mService.mStackSupervisor.setFocusStackUnchecked("testSplitScreenDeliverToTop",
+ focusActivity.getStack());
+
+ doReturn(reusableActivity).when(mService.mStackSupervisor).findTaskLocked(any(), anyInt());
+
+ final int result = starter.setReason("testSplitScreenDeliverToTop").execute();
+
+ // Ensure result is delivering intent to top.
+ assertEquals(result, START_DELIVERED_TO_TOP);
+ }
+
+ /**
+ * This test ensures that if the intent is being delivered to a split-screen unfocused task
+ * reports it is brought to front instead of delivering to top.
+ */
+ @Test
+ public void testSplitScreenTaskToFront() {
+ final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+
+ // Create reusable activity here first. Setting the windowing mode of the primary stack
+ // will move the existing standard full screen stack to secondary, putting this one on the
+ // bottom.
+ final ActivityRecord reusableActivity = new ActivityBuilder(mService)
+ .setCreateTask(true)
+ .build();
+
+ reusableActivity.getStack().setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
+
+ final ActivityRecord focusActivity = new ActivityBuilder(mService)
+ .setCreateTask(true)
+ .build();
+
+ // Enter split-screen. Primary stack should have focus.
+ focusActivity.getStack().setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
+
+ doReturn(reusableActivity).when(mService.mStackSupervisor).findTaskLocked(any(), anyInt());
+
+ final int result = starter.setReason("testSplitScreenMoveToFront").execute();
+
+ // Ensure result is moving task to front.
+ assertEquals(result, START_TASK_TO_FRONT);
+ }
+
+ /**
+ * Tests activity is cleaned up properly in a task mode violation.
+ */
+ @Test
+ public void testTaskModeViolation() {
+ final ActivityDisplay display = mService.mStackSupervisor.getDefaultDisplay();
+ assertNoTasks(display);
+
+ final ActivityStarter starter = prepareStarter(0);
+
+ final LockTaskController lockTaskController = mService.getLockTaskController();
+ doReturn(true).when(lockTaskController).isLockTaskModeViolation(any());
+
+ final int result = starter.setReason("testTaskModeViolation").execute();
+
+ assertEquals(START_RETURN_LOCK_TASK_MODE_VIOLATION, result);
+ assertNoTasks(display);
+ }
+
+ private void assertNoTasks(ActivityDisplay display) {
+ for (int i = display.getChildCount() - 1; i >= 0; --i) {
+ final ActivityStack stack = display.getChildAt(i);
+ assertTrue(stack.getAllTasks().isEmpty());
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityTestsBase.java b/services/tests/servicestests/src/com/android/server/am/ActivityTestsBase.java
new file mode 100644
index 0000000..1cd111f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityTestsBase.java
@@ -0,0 +1,593 @@
+/*
+ * 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.am;
+
+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.view.Display.DEFAULT_DISPLAY;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.spy;
+
+import android.app.ActivityOptions;
+import com.android.server.wm.DisplayWindowController;
+
+import org.junit.Rule;
+import org.mockito.invocation.InvocationOnMock;
+
+import android.app.IApplicationThread;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.service.voice.IVoiceInteractionSession;
+import android.support.test.InstrumentationRegistry;
+import android.testing.DexmakerShareClassLoaderRule;
+
+
+import com.android.internal.app.IVoiceInteractor;
+
+import com.android.server.AttributeCache;
+import com.android.server.wm.AppWindowContainerController;
+import com.android.server.wm.PinnedStackWindowController;
+import com.android.server.wm.StackWindowController;
+import com.android.server.wm.TaskWindowContainerController;
+import com.android.server.wm.WindowManagerService;
+import com.android.server.wm.WindowTestUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.mockito.MockitoAnnotations;
+
+
+/**
+ * A base class to handle common operations in activity related unit tests.
+ */
+public class ActivityTestsBase {
+ private static boolean sOneTimeSetupDone = false;
+
+ @Rule
+ public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
+ new DexmakerShareClassLoaderRule();
+
+ private final Context mContext = InstrumentationRegistry.getContext();
+ private HandlerThread mHandlerThread;
+
+ // Default package name
+ static final String DEFAULT_COMPONENT_PACKAGE_NAME = "com.foo";
+
+ // Default base activity name
+ private static final String DEFAULT_COMPONENT_CLASS_NAME = ".BarActivity";
+
+ @Before
+ public void setUp() throws Exception {
+ if (!sOneTimeSetupDone) {
+ sOneTimeSetupDone = true;
+ MockitoAnnotations.initMocks(this);
+ }
+ mHandlerThread = new HandlerThread("ActivityTestsBaseThread");
+ mHandlerThread.start();
+ }
+
+ @After
+ public void tearDown() {
+ mHandlerThread.quitSafely();
+ }
+
+ protected ActivityManagerService createActivityManagerService() {
+ final ActivityManagerService service =
+ setupActivityManagerService(new TestActivityManagerService(mContext));
+ AttributeCache.init(mContext);
+ return service;
+ }
+
+ protected ActivityManagerService setupActivityManagerService(ActivityManagerService service) {
+ service = spy(service);
+ doReturn(mock(IPackageManager.class)).when(service).getPackageManager();
+ doNothing().when(service).grantEphemeralAccessLocked(anyInt(), any(), anyInt(), anyInt());
+ service.mWindowManager = prepareMockWindowManager();
+ return service;
+ }
+
+ /**
+ * Builder for creating new activities.
+ */
+ protected static class ActivityBuilder {
+ // An id appended to the end of the component name to make it unique
+ private static int sCurrentActivityId = 0;
+
+
+
+ private final ActivityManagerService mService;
+
+ private ComponentName mComponent;
+ private TaskRecord mTaskRecord;
+ private int mUid;
+ private boolean mCreateTask;
+ private ActivityStack mStack;
+ private int mActivityFlags;
+
+ ActivityBuilder(ActivityManagerService service) {
+ mService = service;
+ }
+
+ ActivityBuilder setComponent(ComponentName component) {
+ mComponent = component;
+ return this;
+ }
+
+ static ComponentName getDefaultComponent() {
+ return ComponentName.createRelative(DEFAULT_COMPONENT_PACKAGE_NAME,
+ DEFAULT_COMPONENT_PACKAGE_NAME);
+ }
+
+ ActivityBuilder setTask(TaskRecord task) {
+ mTaskRecord = task;
+ return this;
+ }
+
+ ActivityBuilder setActivityFlags(int flags) {
+ mActivityFlags = flags;
+ return this;
+ }
+
+ ActivityBuilder setStack(ActivityStack stack) {
+ mStack = stack;
+ return this;
+ }
+
+ ActivityBuilder setCreateTask(boolean createTask) {
+ mCreateTask = createTask;
+ return this;
+ }
+
+ ActivityBuilder setUid(int uid) {
+ mUid = uid;
+ return this;
+ }
+
+ ActivityRecord build() {
+ if (mComponent == null) {
+ final int id = sCurrentActivityId++;
+ mComponent = ComponentName.createRelative(DEFAULT_COMPONENT_PACKAGE_NAME,
+ DEFAULT_COMPONENT_CLASS_NAME + id);
+ }
+
+ if (mCreateTask) {
+ mTaskRecord = new TaskBuilder(mService.mStackSupervisor)
+ .setComponent(mComponent)
+ .setStack(mStack).build();
+ }
+
+ Intent intent = new Intent();
+ intent.setComponent(mComponent);
+ final ActivityInfo aInfo = new ActivityInfo();
+ aInfo.applicationInfo = new ApplicationInfo();
+ aInfo.applicationInfo.packageName = mComponent.getPackageName();
+ aInfo.applicationInfo.uid = mUid;
+ aInfo.flags |= mActivityFlags;
+
+ final ActivityRecord activity = new ActivityRecord(mService, null /* caller */,
+ 0 /* launchedFromPid */, 0, null, intent, null,
+ aInfo /*aInfo*/, new Configuration(), null /* resultTo */, null /* resultWho */,
+ 0 /* reqCode */, false /*componentSpecified*/, false /* rootVoiceInteraction */,
+ mService.mStackSupervisor, null /* options */, null /* sourceRecord */);
+ activity.mWindowContainerController = mock(AppWindowContainerController.class);
+
+ if (mTaskRecord != null) {
+ mTaskRecord.addActivityToTop(activity);
+ }
+
+ activity.setProcess(new ProcessRecord(null, null,
+ mService.mContext.getApplicationInfo(), "name", 12345));
+ activity.app.thread = mock(IApplicationThread.class);
+
+ return activity;
+ }
+ }
+
+ /**
+ * Builder for creating new tasks.
+ */
+ protected static class TaskBuilder {
+ // Default package name
+ static final String DEFAULT_PACKAGE = "com.bar";
+
+ private final ActivityStackSupervisor mSupervisor;
+
+ private ComponentName mComponent;
+ private String mPackage;
+ private int mFlags = 0;
+ private int mTaskId = 0;
+ private int mUserId = 0;
+ private IVoiceInteractionSession mVoiceSession;
+ private boolean mCreateStack = true;
+
+ private ActivityStack mStack;
+
+ TaskBuilder(ActivityStackSupervisor supervisor) {
+ mSupervisor = supervisor;
+ }
+
+ TaskBuilder setComponent(ComponentName component) {
+ mComponent = component;
+ return this;
+ }
+
+ TaskBuilder setPackage(String packageName) {
+ mPackage = packageName;
+ return this;
+ }
+
+ /**
+ * Set to {@code true} by default, set to {@code false} to prevent the task from
+ * automatically creating a parent stack.
+ */
+ TaskBuilder setCreateStack(boolean createStack) {
+ mCreateStack = createStack;
+ return this;
+ }
+
+ TaskBuilder setVoiceSession(IVoiceInteractionSession session) {
+ mVoiceSession = session;
+ return this;
+ }
+
+ TaskBuilder setFlags(int flags) {
+ mFlags = flags;
+ return this;
+ }
+
+ TaskBuilder setTaskId(int taskId) {
+ mTaskId = taskId;
+ return this;
+ }
+
+ TaskBuilder setUserId(int userId) {
+ mUserId = userId;
+ return this;
+ }
+
+ TaskBuilder setStack(ActivityStack stack) {
+ mStack = stack;
+ return this;
+ }
+
+ TaskRecord build() {
+ if (mStack == null && mCreateStack) {
+ mStack = mSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ }
+
+ final ActivityInfo aInfo = new ActivityInfo();
+ aInfo.applicationInfo = new ApplicationInfo();
+ aInfo.applicationInfo.packageName = mPackage;
+
+ Intent intent = new Intent();
+ if (mComponent == null) {
+ mComponent = ComponentName.createRelative(DEFAULT_COMPONENT_PACKAGE_NAME,
+ DEFAULT_COMPONENT_CLASS_NAME);
+ }
+
+ intent.setComponent(mComponent);
+ intent.setFlags(mFlags);
+
+ final TestTaskRecord task = new TestTaskRecord(mSupervisor.mService, mTaskId, aInfo,
+ intent /*intent*/, mVoiceSession, null /*_voiceInteractor*/);
+ task.userId = mUserId;
+
+ if (mStack != null) {
+ mSupervisor.setFocusStackUnchecked("test", mStack);
+ mStack.addTask(task, true, "creating test task");
+ task.setStack(mStack);
+ task.setWindowContainerController();
+ }
+
+ task.touchActiveTime();
+
+ return task;
+ }
+
+ private static class TestTaskRecord extends TaskRecord {
+ TestTaskRecord(ActivityManagerService service, int _taskId, ActivityInfo info,
+ Intent _intent, IVoiceInteractionSession _voiceSession,
+ IVoiceInteractor _voiceInteractor) {
+ super(service, _taskId, info, _intent, _voiceSession, _voiceInteractor);
+ }
+
+ @Override
+ void createWindowContainer(boolean onTop, boolean showForAllUsers) {
+ setWindowContainerController();
+ }
+
+ private void setWindowContainerController() {
+ setWindowContainerController(mock(TaskWindowContainerController.class));
+ }
+ }
+ }
+
+ /**
+ * An {@link ActivityManagerService} subclass which provides a test
+ * {@link ActivityStackSupervisor}.
+ */
+ protected static class TestActivityManagerService extends ActivityManagerService {
+ private ClientLifecycleManager mLifecycleManager;
+ private LockTaskController mLockTaskController;
+
+ TestActivityManagerService(Context context) {
+ super(context);
+ mSupportsMultiWindow = true;
+ mSupportsMultiDisplay = true;
+ mSupportsSplitScreenMultiWindow = true;
+ mSupportsFreeformWindowManagement = true;
+ mSupportsPictureInPicture = true;
+ mWindowManager = WindowTestUtils.getMockWindowManagerService();
+ }
+
+ @Override
+ public ClientLifecycleManager getLifecycleManager() {
+ if (mLifecycleManager == null) {
+ return super.getLifecycleManager();
+ }
+ return mLifecycleManager;
+ }
+
+ public LockTaskController getLockTaskController() {
+ if (mLockTaskController == null) {
+ mLockTaskController = spy(super.getLockTaskController());
+ }
+
+ return mLockTaskController;
+ }
+
+ void setLifecycleManager(ClientLifecycleManager manager) {
+ mLifecycleManager = manager;
+ }
+
+ @Override
+ final protected ActivityStackSupervisor createStackSupervisor() {
+ final ActivityStackSupervisor supervisor = spy(createTestSupervisor());
+ final KeyguardController keyguardController = mock(KeyguardController.class);
+
+ // No home stack is set.
+ doNothing().when(supervisor).moveHomeStackToFront(any());
+ doReturn(true).when(supervisor).moveHomeStackTaskToTop(any());
+ // Invoked during {@link ActivityStack} creation.
+ doNothing().when(supervisor).updateUIDsPresentOnDisplay();
+ // Always keep things awake.
+ doReturn(true).when(supervisor).hasAwakeDisplay();
+ // Called when moving activity to pinned stack.
+ doNothing().when(supervisor).ensureActivitiesVisibleLocked(any(), anyInt(), anyBoolean());
+ // Do not schedule idle timeouts
+ doNothing().when(supervisor).scheduleIdleTimeoutLocked(any());
+ // unit test version does not handle launch wake lock
+ doNothing().when(supervisor).acquireLaunchWakelock();
+ doReturn(keyguardController).when(supervisor).getKeyguardController();
+
+ supervisor.initialize();
+
+ return supervisor;
+ }
+
+ protected ActivityStackSupervisor createTestSupervisor() {
+ return new TestActivityStackSupervisor(this, mHandlerThread.getLooper());
+ }
+
+ @Override
+ void updateUsageStats(ActivityRecord component, boolean resumed) {
+ }
+ }
+
+ /**
+ * An {@link ActivityStackSupervisor} which stubs out certain methods that depend on
+ * setup not available in the test environment. Also specifies an injector for
+ */
+ protected static class TestActivityStackSupervisor extends ActivityStackSupervisor {
+ private ActivityDisplay mDisplay;
+ private KeyguardController mKeyguardController;
+
+ public TestActivityStackSupervisor(ActivityManagerService service, Looper looper) {
+ super(service, looper);
+ mDisplayManager =
+ (DisplayManager) mService.mContext.getSystemService(Context.DISPLAY_SERVICE);
+ mWindowManager = prepareMockWindowManager();
+ mKeyguardController = mock(KeyguardController.class);
+ }
+
+ @Override
+ public void initialize() {
+ super.initialize();
+ mDisplay = spy(new TestActivityDisplay(this, DEFAULT_DISPLAY));
+ attachDisplay(mDisplay);
+ }
+
+ @Override
+ public KeyguardController getKeyguardController() {
+ return mKeyguardController;
+ }
+
+ @Override
+ ActivityDisplay getDefaultDisplay() {
+ return mDisplay;
+ }
+
+ // Just return the current front task. This is called internally so we cannot use spy to mock this out.
+ @Override
+ ActivityStack getNextFocusableStackLocked(ActivityStack currentFocus,
+ boolean ignoreCurrent) {
+ return mFocusedStack;
+ }
+ }
+
+ protected static class TestActivityDisplay extends ActivityDisplay {
+
+ private final ActivityStackSupervisor mSupervisor;
+ TestActivityDisplay(ActivityStackSupervisor supervisor, int displayId) {
+ super(supervisor, displayId);
+ mSupervisor = supervisor;
+ }
+
+ @Override
+ <T extends ActivityStack> T createStackUnchecked(int windowingMode, int activityType,
+ int stackId, boolean onTop) {
+ if (windowingMode == WINDOWING_MODE_PINNED) {
+ return (T) new PinnedActivityStack(this, stackId, mSupervisor, onTop) {
+ @Override
+ Rect getDefaultPictureInPictureBounds(float aspectRatio) {
+ return new Rect(50, 50, 100, 100);
+ }
+
+ @Override
+ PinnedStackWindowController createStackWindowController(int displayId,
+ boolean onTop, Rect outBounds) {
+ return mock(PinnedStackWindowController.class);
+ }
+ };
+ } else {
+ return (T) new TestActivityStack(
+ this, stackId, mSupervisor, windowingMode, activityType, onTop);
+ }
+ }
+
+ @Override
+ protected DisplayWindowController createWindowContainerController() {
+ return mock(DisplayWindowController.class);
+ }
+ }
+
+ private static WindowManagerService prepareMockWindowManager() {
+ final WindowManagerService service = WindowTestUtils.getMockWindowManagerService();
+
+ doAnswer((InvocationOnMock invocationOnMock) -> {
+ final Runnable runnable = invocationOnMock.<Runnable>getArgument(0);
+ if (runnable != null) {
+ runnable.run();
+ }
+ return null;
+ }).when(service).inSurfaceTransaction(any());
+
+ return service;
+ }
+
+ /**
+ * Overridden {@link ActivityStack} that tracks test metrics, such as the number of times a
+ * method is called. Note that its functionality depends on the implementations of the
+ * construction arguments.
+ */
+ protected static class TestActivityStack<T extends StackWindowController>
+ extends ActivityStack<T> {
+ private int mOnActivityRemovedFromStackCount = 0;
+ private T mContainerController;
+
+ static final int IS_TRANSLUCENT_UNSET = 0;
+ static final int IS_TRANSLUCENT_FALSE = 1;
+ static final int IS_TRANSLUCENT_TRUE = 2;
+ private int mIsTranslucent = IS_TRANSLUCENT_UNSET;
+
+ static final int SUPPORTS_SPLIT_SCREEN_UNSET = 0;
+ static final int SUPPORTS_SPLIT_SCREEN_FALSE = 1;
+ static final int SUPPORTS_SPLIT_SCREEN_TRUE = 2;
+ private int mSupportsSplitScreen = SUPPORTS_SPLIT_SCREEN_UNSET;
+
+ TestActivityStack(ActivityDisplay display, int stackId, ActivityStackSupervisor supervisor,
+ int windowingMode, int activityType, boolean onTop) {
+ super(display, stackId, supervisor, windowingMode, activityType, onTop);
+ }
+
+ @Override
+ void onActivityRemovedFromStack(ActivityRecord r) {
+ mOnActivityRemovedFromStackCount++;
+ super.onActivityRemovedFromStack(r);
+ }
+
+ // Returns the number of times {@link #onActivityRemovedFromStack} has been called
+ int onActivityRemovedFromStackInvocationCount() {
+ return mOnActivityRemovedFromStackCount;
+ }
+
+ @Override
+ protected T createStackWindowController(int displayId, boolean onTop, Rect outBounds) {
+ mContainerController = (T) WindowTestUtils.createMockStackWindowContainerController();
+
+ // Primary pinned stacks require a non-empty out bounds to be set or else all tasks
+ // will be moved to the full screen stack.
+ if (getWindowingMode() == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) {
+ outBounds.set(0, 0, 100, 100);
+ }
+ return mContainerController;
+ }
+
+ @Override
+ T getWindowContainerController() {
+ return mContainerController;
+ }
+
+ void setIsTranslucent(boolean isTranslucent) {
+ mIsTranslucent = isTranslucent ? IS_TRANSLUCENT_TRUE : IS_TRANSLUCENT_FALSE;
+ }
+
+ @Override
+ boolean isStackTranslucent(ActivityRecord starting) {
+ switch (mIsTranslucent) {
+ case IS_TRANSLUCENT_TRUE:
+ return true;
+ case IS_TRANSLUCENT_FALSE:
+ return false;
+ case IS_TRANSLUCENT_UNSET:
+ default:
+ return super.isStackTranslucent(starting);
+ }
+ }
+
+ void setSupportsSplitScreen(boolean supportsSplitScreen) {
+ mSupportsSplitScreen = supportsSplitScreen
+ ? SUPPORTS_SPLIT_SCREEN_TRUE : SUPPORTS_SPLIT_SCREEN_FALSE;
+ }
+
+ @Override
+ public boolean supportsSplitScreenWindowingMode() {
+ switch (mSupportsSplitScreen) {
+ case SUPPORTS_SPLIT_SCREEN_TRUE:
+ return true;
+ case SUPPORTS_SPLIT_SCREEN_FALSE:
+ return false;
+ case SUPPORTS_SPLIT_SCREEN_UNSET:
+ default:
+ return super.supportsSplitScreenWindowingMode();
+ }
+ }
+
+ @Override
+ void startActivityLocked(ActivityRecord r, ActivityRecord focusedTopActivity,
+ boolean newTask, boolean keepCurTransition,
+ ActivityOptions options) {
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/AppErrorDialogTest.java b/services/tests/servicestests/src/com/android/server/am/AppErrorDialogTest.java
new file mode 100644
index 0000000..3d11c4c
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/AppErrorDialogTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.am;
+
+import android.content.Context;
+import android.os.Handler;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.server.AppOpsService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+/**
+ * runtest -c com.android.server.am.AppErrorDialogTest frameworks-services
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class AppErrorDialogTest {
+
+ private Context mContext;
+ private ActivityManagerService mService;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getTargetContext();
+ mService = new ActivityManagerService(new ActivityManagerService.Injector() {
+ @Override
+ public AppOpsService getAppOpsService(File file, Handler handler) {
+ return null;
+ }
+
+ @Override
+ public Handler getUiHandler(ActivityManagerService service) {
+ return null;
+ }
+
+ @Override
+ public boolean isNetworkRestrictedForUid(int uid) {
+ return false;
+ }
+ });
+ }
+
+ @Test
+ @UiThreadTest
+ public void testCreateWorks() throws Exception {
+ AppErrorDialog.Data data = new AppErrorDialog.Data();
+ data.proc = new ProcessRecord(null, null, mContext.getApplicationInfo(), "name", 12345);
+ data.result = new AppErrorResult();
+
+ AppErrorDialog dialog = new AppErrorDialog(mContext, mService, data);
+
+ dialog.create();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/AssistDataRequesterTest.java b/services/tests/servicestests/src/com/android/server/am/AssistDataRequesterTest.java
new file mode 100644
index 0000000..ce88d84
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/AssistDataRequesterTest.java
@@ -0,0 +1,374 @@
+/*
+ * 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.am;
+
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.AppOpsManager.MODE_ERRORED;
+import static android.app.AppOpsManager.OP_ASSIST_SCREENSHOT;
+import static android.app.AppOpsManager.OP_ASSIST_STRUCTURE;
+import static android.graphics.Bitmap.Config.ARGB_8888;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import android.app.AppOpsManager;
+import android.app.IActivityManager;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Log;
+import android.view.IWindowManager;
+
+import com.android.server.am.AssistDataRequester.AssistDataRequesterCallbacks;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Note: Currently, we only support fetching the screenshot for the current application, so the
+ * screenshot checks are hardcoded accordingly.
+ *
+ * runtest --path frameworks/base/services/tests/servicestests/src/com/android/server/am/AssistDataRequesterTest.java
+ */
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class AssistDataRequesterTest extends ActivityTestsBase {
+
+ private static final String TAG = AssistDataRequesterTest.class.getSimpleName();
+
+ private static final boolean CURRENT_ACTIVITY_ASSIST_ALLOWED = true;
+ private static final boolean CALLER_ASSIST_STRUCTURE_ALLOWED = true;
+ private static final boolean CALLER_ASSIST_SCREENSHOT_ALLOWED = true;
+ private static final boolean FETCH_DATA = true;
+ private static final boolean FETCH_SCREENSHOTS = true;
+ private static final boolean ALLOW_FETCH_DATA = true;
+ private static final boolean ALLOW_FETCH_SCREENSHOTS = true;
+
+ private static final int TEST_UID = 0;
+ private static final String TEST_PACKAGE = "";
+
+ private Context mContext;
+ private AssistDataRequester mDataRequester;
+ private Callbacks mCallbacks;
+ private Object mCallbacksLock;
+ private Handler mHandler;
+ private IActivityManager mAm;
+ private IWindowManager mWm;
+ private AppOpsManager mAppOpsManager;
+
+ /**
+ * The requests to fetch assist data are done incrementally from the text thread, and we
+ * immediately post onto the main thread handler below, which would immediately make the
+ * callback and decrement the pending counts. In order to assert the pending counts, we defer
+ * the callbacks on the test-side until after we flip the gate, after which we can drain the
+ * main thread handler and make assertions on the actual callbacks
+ */
+ private CountDownLatch mGate;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mAm = mock(IActivityManager.class);
+ mWm = mock(IWindowManager.class);
+ mAppOpsManager = mock(AppOpsManager.class);
+ mContext = InstrumentationRegistry.getContext();
+ mHandler = new Handler(Looper.getMainLooper());
+ mCallbacksLock = new Object();
+ mCallbacks = new Callbacks();
+ mDataRequester = new AssistDataRequester(mContext, mAm, mWm, mAppOpsManager, mCallbacks,
+ mCallbacksLock, OP_ASSIST_STRUCTURE, OP_ASSIST_SCREENSHOT);
+
+ // Gate the continuation of the assist data callbacks until we are ready within the tests
+ mGate = new CountDownLatch(1);
+ doAnswer(invocation -> {
+ mHandler.post(() -> {
+ try {
+ mGate.await(10, TimeUnit.SECONDS);
+ mDataRequester.onHandleAssistData(new Bundle());
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Failed to wait", e);
+ }
+ });
+ return true;
+ }).when(mAm).requestAssistContextExtras(anyInt(), any(), any(), any(), anyBoolean(),
+ anyBoolean());
+ doAnswer(invocation -> {
+ mHandler.post(() -> {
+ try {
+ mGate.await(10, TimeUnit.SECONDS);
+ mDataRequester.onHandleAssistScreenshot(Bitmap.createBitmap(1, 1, ARGB_8888));
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Failed to wait", e);
+ }
+ });
+ return true;
+ }).when(mWm).requestAssistScreenshot(any());
+ }
+
+ private void setupMocks(boolean currentActivityAssistAllowed, boolean assistStructureAllowed,
+ boolean assistScreenshotAllowed) throws Exception {
+ doReturn(currentActivityAssistAllowed).when(mAm).isAssistDataAllowedOnCurrentActivity();
+ doReturn(assistStructureAllowed ? MODE_ALLOWED : MODE_ERRORED).when(mAppOpsManager)
+ .checkOpNoThrow(eq(OP_ASSIST_STRUCTURE), anyInt(), anyString());
+ doReturn(assistScreenshotAllowed ? MODE_ALLOWED : MODE_ERRORED).when(mAppOpsManager)
+ .checkOpNoThrow(eq(OP_ASSIST_SCREENSHOT), anyInt(), anyString());
+ }
+
+ @Test
+ public void testRequestData() throws Exception {
+ setupMocks(CURRENT_ACTIVITY_ASSIST_ALLOWED, CALLER_ASSIST_STRUCTURE_ALLOWED,
+ CALLER_ASSIST_SCREENSHOT_ALLOWED);
+
+ mDataRequester.requestAssistData(createActivityList(5), FETCH_DATA, FETCH_SCREENSHOTS,
+ ALLOW_FETCH_DATA, ALLOW_FETCH_SCREENSHOTS, TEST_UID, TEST_PACKAGE);
+ assertReceivedDataCount(5, 5, 1, 1);
+ }
+
+ @Test
+ public void testEmptyActivities_expectNoCallbacks() throws Exception {
+ setupMocks(CURRENT_ACTIVITY_ASSIST_ALLOWED, CALLER_ASSIST_STRUCTURE_ALLOWED,
+ CALLER_ASSIST_SCREENSHOT_ALLOWED);
+
+ mDataRequester.requestAssistData(createActivityList(0), FETCH_DATA, FETCH_SCREENSHOTS,
+ ALLOW_FETCH_DATA, ALLOW_FETCH_SCREENSHOTS, TEST_UID, TEST_PACKAGE);
+ assertReceivedDataCount(0, 0, 0, 0);
+ }
+
+ @Test
+ public void testCurrentAppDisallow_expectNullCallbacks() throws Exception {
+ setupMocks(!CURRENT_ACTIVITY_ASSIST_ALLOWED, CALLER_ASSIST_STRUCTURE_ALLOWED,
+ CALLER_ASSIST_SCREENSHOT_ALLOWED);
+
+ mDataRequester.requestAssistData(createActivityList(5), FETCH_DATA, FETCH_SCREENSHOTS,
+ ALLOW_FETCH_DATA, ALLOW_FETCH_SCREENSHOTS, TEST_UID, TEST_PACKAGE);
+ assertReceivedDataCount(0, 1, 0, 1);
+ }
+
+ @Test
+ public void testProcessPendingData() throws Exception {
+ setupMocks(CURRENT_ACTIVITY_ASSIST_ALLOWED, CALLER_ASSIST_STRUCTURE_ALLOWED,
+ CALLER_ASSIST_SCREENSHOT_ALLOWED);
+
+ mCallbacks.canHandleReceivedData = false;
+ mDataRequester.requestAssistData(createActivityList(5), FETCH_DATA, FETCH_SCREENSHOTS,
+ ALLOW_FETCH_DATA, ALLOW_FETCH_SCREENSHOTS, TEST_UID, TEST_PACKAGE);
+ assertTrue(mDataRequester.getPendingDataCount() == 5);
+ assertTrue(mDataRequester.getPendingScreenshotCount() == 1);
+ mGate.countDown();
+ waitForIdle(mHandler);
+
+ // Callbacks still not ready to receive, but all pending data is received
+ assertTrue(mDataRequester.getPendingDataCount() == 0);
+ assertTrue(mDataRequester.getPendingScreenshotCount() == 0);
+ assertTrue(mCallbacks.receivedData.isEmpty());
+ assertTrue(mCallbacks.receivedScreenshots.isEmpty());
+ assertFalse(mCallbacks.requestCompleted);
+
+ mCallbacks.canHandleReceivedData = true;
+ mDataRequester.processPendingAssistData();
+ // Since we are posting the callback for the request-complete, flush the handler as well
+ mGate.countDown();
+ waitForIdle(mHandler);
+ assertTrue(mCallbacks.receivedData.size() == 5);
+ assertTrue(mCallbacks.receivedScreenshots.size() == 1);
+ assertTrue(mCallbacks.requestCompleted);
+
+ // Clear the state and ensure that we only process pending data once
+ mCallbacks.reset();
+ mDataRequester.processPendingAssistData();
+ assertTrue(mCallbacks.receivedData.isEmpty());
+ assertTrue(mCallbacks.receivedScreenshots.isEmpty());
+ }
+
+ @Test
+ public void testNoFetchData_expectNoDataCallbacks() throws Exception {
+ setupMocks(CURRENT_ACTIVITY_ASSIST_ALLOWED, CALLER_ASSIST_STRUCTURE_ALLOWED,
+ CALLER_ASSIST_SCREENSHOT_ALLOWED);
+
+ mDataRequester.requestAssistData(createActivityList(5), !FETCH_DATA, FETCH_SCREENSHOTS,
+ ALLOW_FETCH_DATA, ALLOW_FETCH_SCREENSHOTS, TEST_UID, TEST_PACKAGE);
+ assertReceivedDataCount(0, 0, 0, 1);
+ }
+
+ @Test
+ public void testDisallowAssistStructure_expectNullDataCallbacks() throws Exception {
+ setupMocks(CURRENT_ACTIVITY_ASSIST_ALLOWED, !CALLER_ASSIST_STRUCTURE_ALLOWED,
+ CALLER_ASSIST_SCREENSHOT_ALLOWED);
+
+ mDataRequester.requestAssistData(createActivityList(5), FETCH_DATA, FETCH_SCREENSHOTS,
+ ALLOW_FETCH_DATA, ALLOW_FETCH_SCREENSHOTS, TEST_UID, TEST_PACKAGE);
+ // Expect a single null data when the appops is denied
+ assertReceivedDataCount(0, 1, 0, 1);
+ }
+
+ @Test
+ public void testDisallowAssistContextExtras_expectNullDataCallbacks() throws Exception {
+ setupMocks(CURRENT_ACTIVITY_ASSIST_ALLOWED, CALLER_ASSIST_STRUCTURE_ALLOWED,
+ CALLER_ASSIST_SCREENSHOT_ALLOWED);
+ doReturn(false).when(mAm).requestAssistContextExtras(anyInt(), any(), any(), any(),
+ anyBoolean(), anyBoolean());
+
+ mDataRequester.requestAssistData(createActivityList(5), FETCH_DATA, FETCH_SCREENSHOTS,
+ ALLOW_FETCH_DATA, ALLOW_FETCH_SCREENSHOTS, TEST_UID, TEST_PACKAGE);
+ // Expect a single null data when requestAssistContextExtras() fails
+ assertReceivedDataCount(0, 1, 0, 1);
+ }
+
+ @Test
+ public void testNoFetchScreenshots_expectNoScreenshotCallbacks() throws Exception {
+ setupMocks(CURRENT_ACTIVITY_ASSIST_ALLOWED, CALLER_ASSIST_STRUCTURE_ALLOWED,
+ CALLER_ASSIST_SCREENSHOT_ALLOWED);
+
+ mDataRequester.requestAssistData(createActivityList(5), FETCH_DATA, !FETCH_SCREENSHOTS,
+ ALLOW_FETCH_DATA, ALLOW_FETCH_SCREENSHOTS, TEST_UID, TEST_PACKAGE);
+ assertReceivedDataCount(5, 5, 0, 0);
+ }
+
+ @Test
+ public void testDisallowAssistScreenshot_expectNullScreenshotCallback() throws Exception {
+ setupMocks(CURRENT_ACTIVITY_ASSIST_ALLOWED, CALLER_ASSIST_STRUCTURE_ALLOWED,
+ !CALLER_ASSIST_SCREENSHOT_ALLOWED);
+
+ mDataRequester.requestAssistData(createActivityList(5), FETCH_DATA, FETCH_SCREENSHOTS,
+ ALLOW_FETCH_DATA, ALLOW_FETCH_SCREENSHOTS, TEST_UID, TEST_PACKAGE);
+ // Expect a single null screenshot when the appops is denied
+ assertReceivedDataCount(5, 5, 0, 1);
+ }
+
+ @Test
+ public void testCanNotHandleReceivedData_expectNoCallbacks() throws Exception {
+ setupMocks(CURRENT_ACTIVITY_ASSIST_ALLOWED, !CALLER_ASSIST_STRUCTURE_ALLOWED,
+ !CALLER_ASSIST_SCREENSHOT_ALLOWED);
+
+ mCallbacks.canHandleReceivedData = false;
+ mDataRequester.requestAssistData(createActivityList(5), FETCH_DATA, FETCH_SCREENSHOTS,
+ ALLOW_FETCH_DATA, ALLOW_FETCH_SCREENSHOTS, TEST_UID, TEST_PACKAGE);
+ mGate.countDown();
+ waitForIdle(mHandler);
+ assertTrue(mCallbacks.receivedData.isEmpty());
+ assertTrue(mCallbacks.receivedScreenshots.isEmpty());
+ }
+
+ @Test
+ public void testRequestDataNoneAllowed_expectNullCallbacks() throws Exception {
+ setupMocks(CURRENT_ACTIVITY_ASSIST_ALLOWED, CALLER_ASSIST_STRUCTURE_ALLOWED,
+ CALLER_ASSIST_SCREENSHOT_ALLOWED);
+
+ mDataRequester.requestAssistData(createActivityList(5), FETCH_DATA, FETCH_SCREENSHOTS,
+ !ALLOW_FETCH_DATA, !ALLOW_FETCH_SCREENSHOTS, TEST_UID, TEST_PACKAGE);
+ assertReceivedDataCount(0, 1, 0, 1);
+ }
+
+ private void assertReceivedDataCount(int numPendingData, int numReceivedData,
+ int numPendingScreenshots, int numReceivedScreenshots) throws Exception {
+ assertTrue("Expected " + numPendingData + " pending data, got "
+ + mDataRequester.getPendingDataCount(),
+ mDataRequester.getPendingDataCount() == numPendingData);
+ assertTrue("Expected " + numPendingScreenshots + " pending screenshots, got "
+ + mDataRequester.getPendingScreenshotCount(),
+ mDataRequester.getPendingScreenshotCount() == numPendingScreenshots);
+ assertFalse("Expected request NOT completed", mCallbacks.requestCompleted);
+ mGate.countDown();
+ waitForIdle(mHandler);
+ assertTrue("Expected " + numReceivedData + " data, received "
+ + mCallbacks.receivedData.size(),
+ mCallbacks.receivedData.size() == numReceivedData);
+ assertTrue("Expected " + numReceivedScreenshots + " screenshots, received "
+ + mCallbacks.receivedScreenshots.size(),
+ mCallbacks.receivedScreenshots.size() == numReceivedScreenshots);
+ assertTrue("Expected request completed", mCallbacks.requestCompleted);
+ }
+
+ private List<IBinder> createActivityList(int size) {
+ ArrayList<IBinder> activities = new ArrayList<>();
+ for (int i = 0; i < size; i++) {
+ activities.add(mock(IBinder.class));
+ }
+ return activities;
+ }
+
+ public void waitForIdle(Handler h) throws Exception {
+ if (Looper.myLooper() == h.getLooper()) {
+ throw new RuntimeException("This method can not be called from the waiting looper");
+ }
+ CountDownLatch latch = new CountDownLatch(1);
+ h.post(() -> latch.countDown());
+ latch.await(2, TimeUnit.SECONDS);
+ }
+
+ private class Callbacks implements AssistDataRequesterCallbacks {
+
+ boolean canHandleReceivedData = true;
+ boolean requestCompleted = false;
+ ArrayList<Bundle> receivedData = new ArrayList<>();
+ ArrayList<Bitmap> receivedScreenshots = new ArrayList<>();
+
+ void reset() {
+ canHandleReceivedData = true;
+ receivedData.clear();
+ receivedScreenshots.clear();
+ }
+
+ @Override
+ public boolean canHandleReceivedAssistDataLocked() {
+ return canHandleReceivedData;
+ }
+
+ @Override
+ public void onAssistDataReceivedLocked(Bundle data, int activityIndex, int activityCount) {
+ receivedData.add(data);
+ }
+
+ @Override
+ public void onAssistScreenshotReceivedLocked(Bitmap screenshot) {
+ receivedScreenshots.add(screenshot);
+ }
+
+ @Override
+ public void onAssistRequestCompleted() {
+ mHandler.post(() -> {
+ try {
+ mGate.await(10, TimeUnit.SECONDS);
+ requestCompleted = true;
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Failed to wait", e);
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/am/ClientLifecycleManagerTests.java b/services/tests/servicestests/src/com/android/server/am/ClientLifecycleManagerTests.java
new file mode 100644
index 0000000..ef6d5e8
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/ClientLifecycleManagerTests.java
@@ -0,0 +1,44 @@
+package com.android.server.am;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.IApplicationThread;
+import android.app.servertransaction.ClientTransaction;
+import android.os.Binder;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class ClientLifecycleManagerTests {
+
+ @Test
+ public void testScheduleAndRecycleBinderClientTransaction() throws Exception {
+ ClientTransaction item = spy(ClientTransaction.obtain(mock(IApplicationThread.class),
+ new Binder()));
+
+ ClientLifecycleManager clientLifecycleManager = new ClientLifecycleManager();
+ clientLifecycleManager.scheduleTransaction(item);
+
+ verify(item, times(1)).recycle();
+ }
+
+ @Test
+ public void testScheduleNoRecycleNonBinderClientTransaction() throws Exception {
+ ClientTransaction item = spy(ClientTransaction.obtain(mock(IApplicationThread.Stub.class),
+ new Binder()));
+
+ ClientLifecycleManager clientLifecycleManager = new ClientLifecycleManager();
+ clientLifecycleManager.scheduleTransaction(item);
+
+ verify(item, times(0)).recycle();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/CoreSettingsObserverTest.java b/services/tests/servicestests/src/com/android/server/am/CoreSettingsObserverTest.java
new file mode 100644
index 0000000..da30c11
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/CoreSettingsObserverTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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.am;
+
+import static com.android.server.am.ActivityManagerService.Injector;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.Settings;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.mock.MockContentResolver;
+
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.server.AppOpsService;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+
+/**
+ * Test class for {@link CoreSettingsObserver}.
+ *
+ * To run the tests, use
+ *
+ * runtest -c com.android.server.am.CoreSettingsObserverTest frameworks-services
+ *
+ * or the following steps:
+ *
+ * Build: m FrameworksServicesTests
+ * Install: adb install -r \
+ * ${ANDROID_PRODUCT_OUT}/data/app/FrameworksServicesTests/FrameworksServicesTests.apk
+ * Run: adb shell am instrument -e class com.android.server.am.CoreSettingsObserverTest -w \
+ * com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CoreSettingsObserverTest {
+ private static final String TEST_SETTING_SECURE_INT = "secureInt";
+ private static final String TEST_SETTING_GLOBAL_FLOAT = "globalFloat";
+ private static final String TEST_SETTING_SYSTEM_STRING = "systemString";
+
+ private static final int TEST_INT = 111;
+ private static final float TEST_FLOAT = 3.14f;
+ private static final String TEST_STRING = "testString";
+
+ private ActivityManagerService mAms;
+ @Mock private Context mContext;
+
+ private MockContentResolver mContentResolver;
+ private CoreSettingsObserver mCoreSettingsObserver;
+
+ @BeforeClass
+ public static void setupOnce() {
+ FakeSettingsProvider.clearSettingsProvider();
+ CoreSettingsObserver.sSecureSettingToTypeMap.put(TEST_SETTING_SECURE_INT, int.class);
+ CoreSettingsObserver.sGlobalSettingToTypeMap.put(TEST_SETTING_GLOBAL_FLOAT, float.class);
+ CoreSettingsObserver.sSystemSettingToTypeMap.put(TEST_SETTING_SYSTEM_STRING, String.class);
+ }
+
+ @AfterClass
+ public static void tearDownOnce() {
+ FakeSettingsProvider.clearSettingsProvider();
+ }
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ final Context originalContext = InstrumentationRegistry.getContext();
+ when(mContext.getApplicationInfo()).thenReturn(originalContext.getApplicationInfo());
+ mContentResolver = new MockContentResolver(mContext);
+ mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+ when(mContext.getContentResolver()).thenReturn(mContentResolver);
+
+ mAms = new ActivityManagerService(new TestInjector());
+ mCoreSettingsObserver = new CoreSettingsObserver(mAms);
+ }
+
+ @Test
+ public void testPopulateSettings() {
+ Settings.Secure.putInt(mContentResolver, TEST_SETTING_SECURE_INT, TEST_INT);
+ Settings.Global.putFloat(mContentResolver, TEST_SETTING_GLOBAL_FLOAT, TEST_FLOAT);
+ Settings.System.putString(mContentResolver, TEST_SETTING_SYSTEM_STRING, TEST_STRING);
+
+ final Bundle settingsBundle = getPopulatedBundle();
+
+ assertEquals("Unexpected value of " + TEST_SETTING_SECURE_INT,
+ TEST_INT, settingsBundle.getInt(TEST_SETTING_SECURE_INT));
+ assertEquals("Unexpected value of " + TEST_SETTING_GLOBAL_FLOAT,
+ TEST_FLOAT, settingsBundle.getFloat(TEST_SETTING_GLOBAL_FLOAT), 0);
+ assertEquals("Unexpected value of " + TEST_SETTING_SYSTEM_STRING,
+ TEST_STRING, settingsBundle.getString(TEST_SETTING_SYSTEM_STRING));
+ }
+
+ @Test
+ public void testPopulateSettings_settingNotSet() {
+ final Bundle settingsBundle = getPopulatedBundle();
+
+ assertFalse("Bundle should not contain " + TEST_SETTING_SECURE_INT,
+ settingsBundle.containsKey(TEST_SETTING_SECURE_INT));
+ assertFalse("Bundle should not contain " + TEST_SETTING_GLOBAL_FLOAT,
+ settingsBundle.containsKey(TEST_SETTING_GLOBAL_FLOAT));
+ assertFalse("Bundle should not contain " + TEST_SETTING_SYSTEM_STRING,
+ settingsBundle.containsKey(TEST_SETTING_SYSTEM_STRING));
+ }
+
+ private Bundle getPopulatedBundle() {
+ final Bundle settingsBundle = new Bundle();
+ mCoreSettingsObserver.populateSettings(settingsBundle,
+ CoreSettingsObserver.sGlobalSettingToTypeMap);
+ mCoreSettingsObserver.populateSettings(settingsBundle,
+ CoreSettingsObserver.sSecureSettingToTypeMap);
+ mCoreSettingsObserver.populateSettings(settingsBundle,
+ CoreSettingsObserver.sSystemSettingToTypeMap);
+ return settingsBundle;
+ }
+
+ private class TestInjector extends Injector {
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ public AppOpsService getAppOpsService(File file, Handler handler) {
+ return null;
+ }
+
+ @Override
+ public Handler getUiHandler(ActivityManagerService service) {
+ return null;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/GlobalSettingsToPropertiesMapperTest.java b/services/tests/servicestests/src/com/android/server/am/GlobalSettingsToPropertiesMapperTest.java
new file mode 100644
index 0000000..d9b3e1c
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/GlobalSettingsToPropertiesMapperTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.am;
+
+import android.content.ContentResolver;
+import android.provider.Settings;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.mock.MockContentResolver;
+
+import com.android.internal.util.Preconditions;
+import com.android.internal.util.test.FakeSettingsProvider;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Tests for {@link GlobalSettingsToPropertiesMapper}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class GlobalSettingsToPropertiesMapperTest {
+ private static final String[][] TEST_MAPPING = new String[][] {
+ {Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, "TestProperty"}
+ };
+
+ private TestMapper mTestMapper;
+ private MockContentResolver mMockContentResolver;
+
+ @Before
+ public void setup() {
+ // Use FakeSettingsProvider to not affect global state
+ mMockContentResolver = new MockContentResolver(InstrumentationRegistry.getContext());
+ mMockContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+ mTestMapper = new TestMapper(mMockContentResolver);
+ }
+
+ @Test
+ public void testUpdatePropertiesFromGlobalSettings() {
+ Settings.Global.putString(mMockContentResolver,
+ Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, "testValue");
+
+ mTestMapper.updatePropertiesFromGlobalSettings();
+ String propValue = mTestMapper.systemPropertiesGet("TestProperty");
+ Assert.assertEquals("testValue", propValue);
+
+ Settings.Global.putString(mMockContentResolver,
+ Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, "testValue2");
+ mTestMapper.updatePropertyFromSetting(Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS,
+ "TestProperty");
+ propValue = mTestMapper.systemPropertiesGet("TestProperty");
+ Assert.assertEquals("testValue2", propValue);
+
+ Settings.Global.putString(mMockContentResolver,
+ Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, null);
+ mTestMapper.updatePropertyFromSetting(Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS,
+ "TestProperty");
+ propValue = mTestMapper.systemPropertiesGet("TestProperty");
+ Assert.assertEquals("", propValue);
+ }
+
+ @Test
+ public void testUpdatePropertiesFromGlobalSettings_PropertyAndSettingNotPresent() {
+ // Test that empty property will not not be set if setting is not set
+ mTestMapper.updatePropertiesFromGlobalSettings();
+ String propValue = mTestMapper.systemPropertiesGet("TestProperty");
+ Assert.assertNull("Property should not be set if setting is null", propValue);
+ }
+
+ private static class TestMapper extends GlobalSettingsToPropertiesMapper {
+ private final Map<String, String> mProps = new HashMap<>();
+
+ TestMapper(ContentResolver contentResolver) {
+ super(contentResolver, TEST_MAPPING);
+ }
+
+ @Override
+ protected String systemPropertiesGet(String key) {
+ Preconditions.checkNotNull(key);
+ return mProps.get(key);
+ }
+
+ @Override
+ protected void systemPropertiesSet(String key, String value) {
+ Preconditions.checkNotNull(value);
+ Preconditions.checkNotNull(key);
+ mProps.put(key, value);
+ }
+ }
+
+}
+
diff --git a/services/tests/servicestests/src/com/android/server/am/LaunchParamsControllerTests.java b/services/tests/servicestests/src/com/android/server/am/LaunchParamsControllerTests.java
new file mode 100644
index 0000000..161c287
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/LaunchParamsControllerTests.java
@@ -0,0 +1,255 @@
+/*
+ * 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.am;
+
+import android.app.ActivityOptions;
+import android.content.pm.ActivityInfo.WindowLayout;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.server.am.LaunchParamsController.LaunchParams;
+import org.junit.runner.RunWith;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.android.server.am.LaunchParamsController.LaunchParamsModifier;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import static com.android.server.am.LaunchParamsController.LaunchParamsModifier.RESULT_DONE;
+import static com.android.server.am.LaunchParamsController.LaunchParamsModifier.RESULT_CONTINUE;
+import static com.android.server.am.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+/**
+ * Tests for exercising {@link LaunchParamsController}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:LaunchParamsControllerTests
+ */
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class LaunchParamsControllerTests extends ActivityTestsBase {
+ private ActivityManagerService mService;
+ private LaunchParamsController mController;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mService = createActivityManagerService();
+ mController = new LaunchParamsController(mService);
+ }
+
+ /**
+ * Makes sure positioners get values passed to controller.
+ */
+ @Test
+ public void testArgumentPropagation() {
+ final LaunchParamsModifier
+ positioner = mock(LaunchParamsModifier.class);
+ mController.registerModifier(positioner);
+
+ final ActivityRecord record = new ActivityBuilder(mService).build();
+ final ActivityRecord source = new ActivityBuilder(mService).build();
+ final WindowLayout layout = new WindowLayout(0, 0, 0, 0, 0, 0, 0);
+ final ActivityOptions options = mock(ActivityOptions.class);
+
+ mController.calculate(record.getTask(), layout, record, source, options,
+ new LaunchParams());
+ verify(positioner, times(1)).onCalculate(eq(record.getTask()), eq(layout), eq(record),
+ eq(source), eq(options), any(), any());
+ }
+
+ /**
+ * Ensures positioners further down the chain are not called when RESULT_DONE is returned.
+ */
+ @Test
+ public void testEarlyExit() {
+ final LaunchParamsModifier
+ ignoredPositioner = mock(LaunchParamsModifier.class);
+ final LaunchParamsModifier earlyExitPositioner =
+ (task, layout, activity, source, options, currentParams, outParams) -> RESULT_DONE;
+
+ mController.registerModifier(ignoredPositioner);
+ mController.registerModifier(earlyExitPositioner);
+
+ mController.calculate(null /*task*/, null /*layout*/, null /*activity*/,
+ null /*source*/, null /*options*/, new LaunchParams());
+ verify(ignoredPositioner, never()).onCalculate(any(), any(), any(), any(), any(),
+ any(), any());
+ }
+
+ /**
+ * Ensures that positioners are called in the correct order.
+ */
+ @Test
+ public void testRegistration() {
+ LaunchParamsModifier earlyExitPositioner =
+ new InstrumentedPositioner(RESULT_DONE, new LaunchParams());
+
+ final LaunchParamsModifier firstPositioner = spy(earlyExitPositioner);
+
+ mController.registerModifier(firstPositioner);
+
+ mController.calculate(null /*task*/, null /*layout*/, null /*activity*/,
+ null /*source*/, null /*options*/, new LaunchParams());
+ verify(firstPositioner, times(1)).onCalculate(any(), any(), any(), any(), any(), any(),
+ any());
+
+ final LaunchParamsModifier secondPositioner = spy(earlyExitPositioner);
+
+ mController.registerModifier(secondPositioner);
+
+ mController.calculate(null /*task*/, null /*layout*/, null /*activity*/,
+ null /*source*/, null /*options*/, new LaunchParams());
+ verify(firstPositioner, times(1)).onCalculate(any(), any(), any(), any(), any(), any(),
+ any());
+ verify(secondPositioner, times(1)).onCalculate(any(), any(), any(), any(), any(), any(),
+ any());
+ }
+
+ /**
+ * Makes sure positioners further down the registration chain are called.
+ */
+ @Test
+ public void testPassThrough() {
+ final LaunchParamsModifier
+ positioner1 = mock(LaunchParamsModifier.class);
+ final LaunchParams params = new LaunchParams();
+ params.mWindowingMode = WINDOWING_MODE_FREEFORM;
+ params.mBounds.set(0, 0, 30, 20);
+ params.mPreferredDisplayId = 3;
+
+ final InstrumentedPositioner positioner2 = new InstrumentedPositioner(RESULT_CONTINUE,
+ params);
+
+ mController.registerModifier(positioner1);
+ mController.registerModifier(positioner2);
+
+ mController.calculate(null /*task*/, null /*layout*/, null /*activity*/, null /*source*/,
+ null /*options*/, new LaunchParams());
+
+ verify(positioner1, times(1)).onCalculate(any(), any(), any(), any(), any(),
+ eq(positioner2.getLaunchParams()), any());
+ }
+
+ /**
+ * Ensures skipped results are not propagated.
+ */
+ @Test
+ public void testSkip() {
+ final LaunchParams params1 = new LaunchParams();
+ params1.mBounds.set(0, 0, 10, 10);
+ final InstrumentedPositioner positioner1 = new InstrumentedPositioner(RESULT_SKIP, params1);
+
+ final LaunchParams params2 = new LaunchParams();
+ params2.mBounds.set(0, 0, 20, 30);
+ final InstrumentedPositioner positioner2 =
+ new InstrumentedPositioner(RESULT_CONTINUE, params2);
+
+ mController.registerModifier(positioner1);
+ mController.registerModifier(positioner2);
+
+ final LaunchParams
+ result = new LaunchParams();
+
+ mController.calculate(null /*task*/, null /*layout*/, null /*activity*/, null /*source*/,
+ null /*options*/, result);
+
+ assertEquals(result, positioner2.getLaunchParams());
+ }
+
+ /**
+ * Ensures that {@link LaunchParamsModifier} requests specifying display id during
+ * layout are honored.
+ */
+ @Test
+ public void testLayoutTaskPreferredDisplayChange() {
+ final LaunchParams params = new LaunchParams();
+ params.mPreferredDisplayId = 2;
+ final InstrumentedPositioner positioner = new InstrumentedPositioner(RESULT_DONE, params);
+ final TaskRecord task = new TaskBuilder(mService.mStackSupervisor).build();
+
+ mController.registerModifier(positioner);
+
+ doNothing().when(mService).moveStackToDisplay(anyInt(), anyInt());
+ mController.layoutTask(task, null /* windowLayout */);
+ verify(mService, times(1)).moveStackToDisplay(eq(task.getStackId()),
+ eq(params.mPreferredDisplayId));
+ }
+
+ /**
+ * Ensures that {@link LaunchParamsModifier} requests specifying windowingMode during
+ * layout are honored.
+ */
+ @Test
+ public void testLayoutTaskWindowingModeChange() {
+ final LaunchParams params = new LaunchParams();
+ final int windowingMode = WINDOWING_MODE_FREEFORM;
+ params.mWindowingMode = windowingMode;
+ final InstrumentedPositioner positioner = new InstrumentedPositioner(RESULT_DONE, params);
+ final TaskRecord task = new TaskBuilder(mService.mStackSupervisor).build();
+
+ mController.registerModifier(positioner);
+
+ final int beforeWindowMode = task.getStack().getWindowingMode();
+ assertNotEquals(beforeWindowMode, windowingMode);
+
+ mController.layoutTask(task, null /* windowLayout */);
+
+ final int afterWindowMode = task.getStack().getWindowingMode();
+ assertEquals(afterWindowMode, windowingMode);
+ }
+
+ public static class InstrumentedPositioner implements
+ LaunchParamsModifier {
+
+ final private int mReturnVal;
+ final private LaunchParams mParams;
+
+ InstrumentedPositioner(int returnVal, LaunchParams params) {
+ mReturnVal = returnVal;
+ mParams = params;
+ }
+
+ @Override
+ public int onCalculate(TaskRecord task, WindowLayout layout, ActivityRecord activity,
+ ActivityRecord source, ActivityOptions options,
+ LaunchParams currentParams, LaunchParams outParams) {
+ outParams.set(mParams);
+ return mReturnVal;
+ }
+
+ LaunchParams getLaunchParams() {
+ return mParams;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/LockTaskControllerTest.java b/services/tests/servicestests/src/com/android/server/am/LockTaskControllerTest.java
new file mode 100644
index 0000000..f46d712
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/LockTaskControllerTest.java
@@ -0,0 +1,680 @@
+/*
+ * Copyright 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.am;
+
+import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED;
+import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
+import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED;
+import static android.app.StatusBarManager.DISABLE2_MASK;
+import static android.app.StatusBarManager.DISABLE2_NONE;
+import static android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE;
+import static android.app.StatusBarManager.DISABLE_HOME;
+import static android.app.StatusBarManager.DISABLE_NOTIFICATION_ALERTS;
+import static android.app.StatusBarManager.DISABLE_NOTIFICATION_ICONS;
+import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_HOME;
+import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_KEYGUARD;
+import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_NONE;
+import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_NOTIFICATIONS;
+import static android.os.Process.SYSTEM_UID;
+import static android.telecom.TelecomManager.EMERGENCY_DIALER_COMPONENT;
+
+import static com.android.server.am.LockTaskController.STATUS_BAR_MASK_LOCKED;
+import static com.android.server.am.LockTaskController.STATUS_BAR_MASK_PINNED;
+
+import static org.junit.Assert.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import android.app.StatusBarManager;
+import android.app.admin.DevicePolicyManager;
+import android.app.admin.IDevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.platform.test.annotations.Presubmit;
+import android.provider.Settings;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.telecom.TelecomManager;
+import android.testing.DexmakerShareClassLoaderRule;
+import android.util.Pair;
+
+import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.server.LocalServices;
+import com.android.server.statusbar.StatusBarManagerInternal;
+import com.android.server.wm.WindowManagerService;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.verification.VerificationMode;
+
+/**
+ * Unit tests for {@link LockTaskController}.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.am.LockTaskControllerTest
+ */
+@Presubmit
+@SmallTest
+public class LockTaskControllerTest {
+ private static final String TEST_PACKAGE_NAME = "com.test.package";
+ private static final String TEST_PACKAGE_NAME_2 = "com.test.package2";
+ private static final String TEST_CLASS_NAME = ".TestClass";
+ private static final int TEST_USER_ID = 123;
+ private static final int TEST_UID = 10467;
+
+ @Rule
+ public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
+ new DexmakerShareClassLoaderRule();
+
+ @Mock private ActivityStackSupervisor mSupervisor;
+ @Mock private IDevicePolicyManager mDevicePolicyManager;
+ @Mock private IStatusBarService mStatusBarService;
+ @Mock private WindowManagerService mWindowManager;
+ @Mock private LockPatternUtils mLockPatternUtils;
+ @Mock private StatusBarManagerInternal mStatusBarManagerInternal;
+ @Mock private TelecomManager mTelecomManager;
+ @Mock private RecentTasks mRecentTasks;
+
+ private LockTaskController mLockTaskController;
+ private Context mContext;
+ private String mLockToAppSetting;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = InstrumentationRegistry.getTargetContext();
+ mLockToAppSetting = Settings.Secure.getString(mContext.getContentResolver(),
+ Settings.Secure.LOCK_TO_APP_EXIT_LOCKED);
+
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+
+ mSupervisor.mRecentTasks = mRecentTasks;
+
+ mLockTaskController = new LockTaskController(mContext, mSupervisor,
+ new ImmediatelyExecuteHandler());
+ mLockTaskController.setWindowManager(mWindowManager);
+ mLockTaskController.mStatusBarService = mStatusBarService;
+ mLockTaskController.mDevicePolicyManager = mDevicePolicyManager;
+ mLockTaskController.mTelecomManager = mTelecomManager;
+ mLockTaskController.mLockPatternUtils = mLockPatternUtils;
+
+ LocalServices.removeServiceForTest(StatusBarManagerInternal.class);
+ LocalServices.addService(StatusBarManagerInternal.class, mStatusBarManagerInternal);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ Settings.Secure.putString(mContext.getContentResolver(),
+ Settings.Secure.LOCK_TO_APP_EXIT_LOCKED, mLockToAppSetting);
+ }
+
+ @Test
+ public void testPreconditions() {
+ // GIVEN nothing has happened
+
+ // THEN current lock task mode should be NONE
+ assertEquals(LOCK_TASK_MODE_NONE, mLockTaskController.getLockTaskModeState());
+ }
+
+ @Test
+ public void testStartLockTaskMode_once() throws Exception {
+ // GIVEN a task record with whitelisted auth
+ TaskRecord tr = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_WHITELISTED);
+
+ // WHEN calling setLockTaskMode for LOCKED mode without resuming
+ mLockTaskController.startLockTaskMode(tr, false, TEST_UID);
+
+ // THEN the lock task mode state should be LOCKED
+ assertEquals(LOCK_TASK_MODE_LOCKED, mLockTaskController.getLockTaskModeState());
+ // THEN the task should be locked
+ assertTrue(mLockTaskController.isTaskLocked(tr));
+
+ // THEN lock task mode should be started
+ verifyLockTaskStarted(STATUS_BAR_MASK_LOCKED, DISABLE2_MASK);
+ }
+
+ @Test
+ public void testStartLockTaskMode_twice() throws Exception {
+ // GIVEN two task records with whitelisted auth
+ TaskRecord tr1 = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_WHITELISTED);
+ TaskRecord tr2 = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_WHITELISTED);
+
+ // WHEN calling setLockTaskMode for LOCKED mode on both tasks
+ mLockTaskController.startLockTaskMode(tr1, false, TEST_UID);
+ mLockTaskController.startLockTaskMode(tr2, false, TEST_UID);
+
+ // THEN the lock task mode state should be LOCKED
+ assertEquals(LOCK_TASK_MODE_LOCKED, mLockTaskController.getLockTaskModeState());
+ // THEN neither of the tasks should be able to move to back of stack
+ assertTrue(mLockTaskController.isTaskLocked(tr1));
+ assertTrue(mLockTaskController.isTaskLocked(tr2));
+
+ // THEN lock task mode should be started
+ verifyLockTaskStarted(STATUS_BAR_MASK_LOCKED, DISABLE2_MASK);
+ }
+
+ @Test
+ public void testStartLockTaskMode_pinningRequest() throws Exception {
+ // GIVEN a task record that is not whitelisted, i.e. with pinned auth
+ TaskRecord tr = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_PINNABLE);
+
+ // WHEN calling startLockTaskMode
+ mLockTaskController.startLockTaskMode(tr, false, TEST_UID);
+
+ // THEN a pinning request should be shown
+ verify(mStatusBarManagerInternal).showScreenPinningRequest(anyInt());
+ }
+
+ @Test
+ public void testStartLockTaskMode_pinnedBySystem() throws Exception {
+ // GIVEN a task record with pinned auth
+ TaskRecord tr = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_PINNABLE);
+
+ // WHEN the system calls startLockTaskMode
+ mLockTaskController.startLockTaskMode(tr, true, SYSTEM_UID);
+
+ // THEN the lock task mode state should be PINNED
+ assertEquals(LOCK_TASK_MODE_PINNED, mLockTaskController.getLockTaskModeState());
+ // THEN the task should be locked
+ assertTrue(mLockTaskController.isTaskLocked(tr));
+
+ // THEN lock task mode should be started
+ verifyLockTaskStarted(STATUS_BAR_MASK_PINNED, DISABLE2_NONE);
+ // THEN screen pinning toast should be shown
+ verify(mStatusBarService).showPinningEnterExitToast(true /* entering */);
+ }
+
+ @Test
+ public void testLockTaskViolation() throws Exception {
+ // GIVEN one task record with whitelisted auth that is in lock task mode
+ TaskRecord tr = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_WHITELISTED);
+ mLockTaskController.startLockTaskMode(tr, false, TEST_UID);
+
+ // THEN it's not a lock task violation to try and launch this task without clearing
+ assertFalse(mLockTaskController.isLockTaskModeViolation(tr, false));
+
+ // THEN it's a lock task violation to launch another task that is not whitelisted
+ assertTrue(mLockTaskController.isLockTaskModeViolation(getTaskRecord(
+ TaskRecord.LOCK_TASK_AUTH_PINNABLE)));
+ // THEN it's a lock task violation to launch another task that is disallowed from lock task
+ assertTrue(mLockTaskController.isLockTaskModeViolation(getTaskRecord(
+ TaskRecord.LOCK_TASK_AUTH_DONT_LOCK)));
+
+ // THEN it's no a lock task violation to launch another task that is whitelisted
+ assertFalse(mLockTaskController.isLockTaskModeViolation(getTaskRecord(
+ TaskRecord.LOCK_TASK_AUTH_WHITELISTED)));
+ assertFalse(mLockTaskController.isLockTaskModeViolation(getTaskRecord(
+ TaskRecord.LOCK_TASK_AUTH_LAUNCHABLE)));
+ // THEN it's not a lock task violation to launch another task that is priv launchable
+ assertFalse(mLockTaskController.isLockTaskModeViolation(getTaskRecord(
+ TaskRecord.LOCK_TASK_AUTH_LAUNCHABLE_PRIV)));
+ }
+
+ @Test
+ public void testLockTaskViolation_emergencyCall() throws Exception {
+ // GIVEN one task record with whitelisted auth that is in lock task mode
+ TaskRecord tr = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_WHITELISTED);
+ mLockTaskController.startLockTaskMode(tr, false, TEST_UID);
+
+ // GIVEN tasks necessary for emergency calling
+ TaskRecord keypad = getTaskRecord(new Intent().setComponent(EMERGENCY_DIALER_COMPONENT),
+ TaskRecord.LOCK_TASK_AUTH_PINNABLE);
+ TaskRecord callAction = getTaskRecord(new Intent(Intent.ACTION_CALL_EMERGENCY),
+ TaskRecord.LOCK_TASK_AUTH_PINNABLE);
+ TaskRecord dialer = getTaskRecord("com.example.dialer", TaskRecord.LOCK_TASK_AUTH_PINNABLE);
+ when(mTelecomManager.getSystemDialerPackage())
+ .thenReturn(dialer.intent.getComponent().getPackageName());
+
+ // GIVEN keyguard is allowed for lock task mode
+ mLockTaskController.updateLockTaskFeatures(TEST_USER_ID, LOCK_TASK_FEATURE_KEYGUARD);
+
+ // THEN the above tasks should all be allowed
+ assertFalse(mLockTaskController.isLockTaskModeViolation(keypad));
+ assertFalse(mLockTaskController.isLockTaskModeViolation(callAction));
+ assertFalse(mLockTaskController.isLockTaskModeViolation(dialer));
+
+ // GIVEN keyguard is disallowed for lock task mode (default)
+ mLockTaskController.updateLockTaskFeatures(TEST_USER_ID, LOCK_TASK_FEATURE_NONE);
+
+ // THEN the above tasks should all be blocked
+ assertTrue(mLockTaskController.isLockTaskModeViolation(keypad));
+ assertTrue(mLockTaskController.isLockTaskModeViolation(callAction));
+ assertTrue(mLockTaskController.isLockTaskModeViolation(dialer));
+ }
+
+ @Test
+ public void testStopLockTaskMode() throws Exception {
+ // GIVEN one task record with whitelisted auth that is in lock task mode
+ TaskRecord tr = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_WHITELISTED);
+ mLockTaskController.startLockTaskMode(tr, false, TEST_UID);
+
+ // WHEN the same caller calls stopLockTaskMode
+ mLockTaskController.stopLockTaskMode(tr, false, TEST_UID);
+
+ // THEN the lock task mode should be NONE
+ assertEquals(LOCK_TASK_MODE_NONE, mLockTaskController.getLockTaskModeState());
+ // THEN the task should no longer be locked
+ assertFalse(mLockTaskController.isTaskLocked(tr));
+ // THEN lock task mode should have been finished
+ verifyLockTaskStopped(times(1));
+ }
+
+ @Test(expected = SecurityException.class)
+ public void testStopLockTaskMode_differentCaller() throws Exception {
+ // GIVEN one task record with whitelisted auth that is in lock task mode
+ TaskRecord tr = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_WHITELISTED);
+ mLockTaskController.startLockTaskMode(tr, false, TEST_UID);
+
+ // WHEN a different caller calls stopLockTaskMode
+ mLockTaskController.stopLockTaskMode(tr, false, TEST_UID + 1);
+
+ // THEN security exception should be thrown, because different caller tried to unlock
+ }
+
+ @Test
+ public void testStopLockTaskMode_systemCaller() throws Exception {
+ // GIVEN one task record with whitelisted auth that is in lock task mode
+ TaskRecord tr = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_WHITELISTED);
+ mLockTaskController.startLockTaskMode(tr, false, TEST_UID);
+
+ // WHEN system calls stopLockTaskMode
+ mLockTaskController.stopLockTaskMode(tr, true, SYSTEM_UID);
+
+ // THEN lock task mode should still be active
+ assertEquals(LOCK_TASK_MODE_LOCKED, mLockTaskController.getLockTaskModeState());
+ }
+
+ @Test
+ public void testStopLockTaskMode_twoTasks() throws Exception {
+ // GIVEN two task records with whitelisted auth that is in lock task mode
+ TaskRecord tr1 = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_WHITELISTED);
+ TaskRecord tr2 = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_WHITELISTED);
+ mLockTaskController.startLockTaskMode(tr1, false, TEST_UID);
+ mLockTaskController.startLockTaskMode(tr2, false, TEST_UID);
+
+ // WHEN calling stopLockTaskMode
+ mLockTaskController.stopLockTaskMode(tr2, false, TEST_UID);
+
+ // THEN the lock task mode should still be active
+ assertEquals(LOCK_TASK_MODE_LOCKED, mLockTaskController.getLockTaskModeState());
+ // THEN the first task should still be locked
+ assertTrue(mLockTaskController.isTaskLocked(tr1));
+ // THEN the top task should no longer be locked
+ assertFalse(mLockTaskController.isTaskLocked(tr2));
+ // THEN lock task mode should not have been finished
+ verifyLockTaskStopped(never());
+ }
+
+ @Test
+ public void testStopLockTaskMode_rootTask() throws Exception {
+ // GIVEN two task records with whitelisted auth that is in lock task mode
+ TaskRecord tr1 = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_WHITELISTED);
+ TaskRecord tr2 = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_WHITELISTED);
+ mLockTaskController.startLockTaskMode(tr1, false, TEST_UID);
+ mLockTaskController.startLockTaskMode(tr2, false, TEST_UID);
+
+ // WHEN calling stopLockTaskMode on the root task
+ mLockTaskController.stopLockTaskMode(tr1, false, TEST_UID);
+
+ // THEN the lock task mode should be inactive
+ assertEquals(LOCK_TASK_MODE_NONE, mLockTaskController.getLockTaskModeState());
+ // THEN the first task should no longer be locked
+ assertFalse(mLockTaskController.isTaskLocked(tr1));
+ // THEN the top task should no longer be locked
+ assertFalse(mLockTaskController.isTaskLocked(tr2));
+ // THEN lock task mode should be finished
+ verifyLockTaskStopped(times(1));
+ }
+
+ @Test
+ public void testStopLockTaskMode_pinned() throws Exception {
+ // GIVEN one task records that is in pinned mode
+ TaskRecord tr = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_PINNABLE);
+ mLockTaskController.startLockTaskMode(tr, true, SYSTEM_UID);
+ // GIVEN that the keyguard is required to show after unlocking
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.LOCK_TO_APP_EXIT_LOCKED, 1);
+
+ // reset invocation counter
+ reset(mStatusBarService);
+
+ // WHEN calling stopLockTask
+ mLockTaskController.stopLockTaskMode(null, true, SYSTEM_UID);
+
+ // THEN the lock task mode should no longer be active
+ assertEquals(LOCK_TASK_MODE_NONE, mLockTaskController.getLockTaskModeState());
+ // THEN the task should no longer be locked
+ assertFalse(mLockTaskController.isTaskLocked(tr));
+ // THEN lock task mode should have been finished
+ verifyLockTaskStopped(times(1));
+ // THEN the keyguard should be shown
+ verify(mLockPatternUtils).requireCredentialEntry(UserHandle.USER_ALL);
+ // THEN screen pinning toast should be shown
+ verify(mStatusBarService).showPinningEnterExitToast(false /* entering */);
+ }
+
+ @Test
+ public void testClearLockedTasks() throws Exception {
+ // GIVEN two task records with whitelisted auth that is in lock task mode
+ TaskRecord tr1 = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_WHITELISTED);
+ TaskRecord tr2 = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_WHITELISTED);
+ mLockTaskController.startLockTaskMode(tr1, false, TEST_UID);
+ mLockTaskController.startLockTaskMode(tr2, false, TEST_UID);
+
+ // WHEN calling stopLockTaskMode on the root task
+ mLockTaskController.clearLockedTasks("testClearLockedTasks");
+
+ // THEN the lock task mode should be inactive
+ assertEquals(LOCK_TASK_MODE_NONE, mLockTaskController.getLockTaskModeState());
+ // THEN the first task should no longer be locked
+ assertFalse(mLockTaskController.isTaskLocked(tr1));
+ // THEN the top task should no longer be locked
+ assertFalse(mLockTaskController.isTaskLocked(tr2));
+ // THEN lock task mode should be finished
+ verifyLockTaskStopped(times(1));
+ }
+
+ @Test
+ public void testUpdateLockTaskPackages() throws Exception {
+ String[] whitelist1 = {TEST_PACKAGE_NAME, TEST_PACKAGE_NAME_2};
+ String[] whitelist2 = {TEST_PACKAGE_NAME};
+
+ // No package is whitelisted initially
+ for (String pkg : whitelist1) {
+ assertFalse("Package shouldn't be whitelisted: " + pkg,
+ mLockTaskController.isPackageWhitelisted(TEST_USER_ID, pkg));
+ assertFalse("Package shouldn't be whitelisted for user 0: " + pkg,
+ mLockTaskController.isPackageWhitelisted(0, pkg));
+ }
+
+ // Apply whitelist
+ mLockTaskController.updateLockTaskPackages(TEST_USER_ID, whitelist1);
+
+ // Assert the whitelist is applied to the correct user
+ for (String pkg : whitelist1) {
+ assertTrue("Package should be whitelisted: " + pkg,
+ mLockTaskController.isPackageWhitelisted(TEST_USER_ID, pkg));
+ assertFalse("Package shouldn't be whitelisted for user 0: " + pkg,
+ mLockTaskController.isPackageWhitelisted(0, pkg));
+ }
+
+ // Update whitelist
+ mLockTaskController.updateLockTaskPackages(TEST_USER_ID, whitelist2);
+
+ // Assert the new whitelist is applied
+ assertTrue("Package should remain whitelisted: " + TEST_PACKAGE_NAME,
+ mLockTaskController.isPackageWhitelisted(TEST_USER_ID, TEST_PACKAGE_NAME));
+ assertFalse("Package should no longer be whitelisted: " + TEST_PACKAGE_NAME_2,
+ mLockTaskController.isPackageWhitelisted(TEST_USER_ID, TEST_PACKAGE_NAME_2));
+ }
+
+ @Test
+ public void testUpdateLockTaskPackages_taskRemoved() throws Exception {
+ // GIVEN two tasks which are whitelisted initially
+ TaskRecord tr1 = getTaskRecordForUpdate(TEST_PACKAGE_NAME, true);
+ TaskRecord tr2 = getTaskRecordForUpdate(TEST_PACKAGE_NAME_2, false);
+ String[] whitelist = {TEST_PACKAGE_NAME, TEST_PACKAGE_NAME_2};
+ mLockTaskController.updateLockTaskPackages(TEST_USER_ID, whitelist);
+
+ // GIVEN the tasks are launched into LockTask mode
+ mLockTaskController.startLockTaskMode(tr1, false, TEST_UID);
+ mLockTaskController.startLockTaskMode(tr2, false, TEST_UID);
+ assertEquals(LOCK_TASK_MODE_LOCKED, mLockTaskController.getLockTaskModeState());
+ assertTrue(mLockTaskController.isTaskLocked(tr1));
+ assertTrue(mLockTaskController.isTaskLocked(tr2));
+ verifyLockTaskStarted(STATUS_BAR_MASK_LOCKED, DISABLE2_MASK);
+
+ // WHEN removing one package from whitelist
+ whitelist = new String[] {TEST_PACKAGE_NAME};
+ mLockTaskController.updateLockTaskPackages(TEST_USER_ID, whitelist);
+
+ // THEN the task running that package should be stopped
+ verify(tr2).performClearTaskLocked();
+ assertFalse(mLockTaskController.isTaskLocked(tr2));
+ // THEN the other task should remain locked
+ assertEquals(LOCK_TASK_MODE_LOCKED, mLockTaskController.getLockTaskModeState());
+ assertTrue(mLockTaskController.isTaskLocked(tr1));
+ verifyLockTaskStarted(STATUS_BAR_MASK_LOCKED, DISABLE2_MASK);
+
+ // WHEN removing the last package from whitelist
+ whitelist = new String[] {};
+ mLockTaskController.updateLockTaskPackages(TEST_USER_ID, whitelist);
+
+ // THEN the last task should be cleared, and the system should quit LockTask mode
+ verify(tr1).performClearTaskLocked();
+ assertFalse(mLockTaskController.isTaskLocked(tr1));
+ assertEquals(LOCK_TASK_MODE_NONE, mLockTaskController.getLockTaskModeState());
+ verifyLockTaskStopped(times(1));
+ }
+
+ @Test
+ public void testUpdateLockTaskFeatures() throws Exception {
+ // GIVEN a locked task
+ TaskRecord tr = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_WHITELISTED);
+ mLockTaskController.startLockTaskMode(tr, false, TEST_UID);
+
+ // THEN lock task mode should be started with default status bar masks
+ verifyLockTaskStarted(STATUS_BAR_MASK_LOCKED, DISABLE2_MASK);
+
+ // reset invocation counter
+ reset(mStatusBarService);
+
+ // WHEN home button is enabled for lock task mode
+ mLockTaskController.updateLockTaskFeatures(TEST_USER_ID, LOCK_TASK_FEATURE_HOME);
+
+ // THEN status bar should be updated to reflect this change
+ int expectedFlags = STATUS_BAR_MASK_LOCKED
+ & ~DISABLE_HOME;
+ int expectedFlags2 = DISABLE2_MASK;
+ verify(mStatusBarService).disable(eq(expectedFlags), any(IBinder.class),
+ eq(mContext.getPackageName()));
+ verify(mStatusBarService).disable2(eq(expectedFlags2), any(IBinder.class),
+ eq(mContext.getPackageName()));
+
+ // reset invocation counter
+ reset(mStatusBarService);
+
+ // WHEN notifications are enabled for lock task mode
+ mLockTaskController.updateLockTaskFeatures(TEST_USER_ID, LOCK_TASK_FEATURE_NOTIFICATIONS);
+
+ // THEN status bar should be updated to reflect this change
+ expectedFlags = STATUS_BAR_MASK_LOCKED
+ & ~DISABLE_NOTIFICATION_ICONS
+ & ~DISABLE_NOTIFICATION_ALERTS;
+ expectedFlags2 = DISABLE2_MASK
+ & ~DISABLE2_NOTIFICATION_SHADE;
+ verify(mStatusBarService).disable(eq(expectedFlags), any(IBinder.class),
+ eq(mContext.getPackageName()));
+ verify(mStatusBarService).disable2(eq(expectedFlags2), any(IBinder.class),
+ eq(mContext.getPackageName()));
+ }
+
+ @Test
+ public void testUpdateLockTaskFeatures_differentUser() throws Exception {
+ // GIVEN a locked task
+ TaskRecord tr = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_WHITELISTED);
+ mLockTaskController.startLockTaskMode(tr, false, TEST_UID);
+
+ // THEN lock task mode should be started with default status bar masks
+ verifyLockTaskStarted(STATUS_BAR_MASK_LOCKED, DISABLE2_MASK);
+
+ // reset invocation counter
+ reset(mStatusBarService);
+
+ // WHEN home button is enabled for lock task mode for another user
+ mLockTaskController.updateLockTaskFeatures(TEST_USER_ID + 1, LOCK_TASK_FEATURE_HOME);
+
+ // THEN status bar shouldn't change
+ verify(mStatusBarService, never()).disable(anyInt(), any(IBinder.class),
+ eq(mContext.getPackageName()));
+ verify(mStatusBarService, never()).disable2(anyInt(), any(IBinder.class),
+ eq(mContext.getPackageName()));
+ }
+
+ @Test
+ public void testUpdateLockTaskFeatures_keyguard() throws Exception {
+ // GIVEN a locked task
+ TaskRecord tr = getTaskRecord(TaskRecord.LOCK_TASK_AUTH_WHITELISTED);
+ mLockTaskController.startLockTaskMode(tr, false, TEST_UID);
+
+ // THEN keyguard should be disabled
+ verify(mWindowManager).disableKeyguard(any(IBinder.class), anyString());
+
+ // WHEN keyguard is enabled for lock task mode
+ mLockTaskController.updateLockTaskFeatures(TEST_USER_ID, LOCK_TASK_FEATURE_KEYGUARD);
+
+ // THEN keyguard should be enabled
+ verify(mWindowManager).reenableKeyguard(any(IBinder.class));
+
+ // WHEN keyguard is disabled again for lock task mode
+ mLockTaskController.updateLockTaskFeatures(TEST_USER_ID, LOCK_TASK_FEATURE_NONE);
+
+ // THEN keyguard should be disabled
+ verify(mWindowManager, times(2)).disableKeyguard(any(IBinder.class), anyString());
+ }
+
+ @Test
+ public void testGetStatusBarDisableFlags() {
+ // Note that we don't enumerate all StatusBarManager flags, but only choose a subset to test
+
+ // WHEN nothing is enabled
+ Pair<Integer, Integer> flags = mLockTaskController.getStatusBarDisableFlags(
+ LOCK_TASK_FEATURE_NONE);
+ // THEN unsupported feature flags should still be untouched
+ assertTrue((~STATUS_BAR_MASK_LOCKED & flags.first) == 0);
+ // THEN everything else should be disabled
+ assertTrue((StatusBarManager.DISABLE_CLOCK & flags.first) != 0);
+ assertTrue((StatusBarManager.DISABLE2_QUICK_SETTINGS & flags.second) != 0);
+
+ // WHEN only home button is enabled
+ flags = mLockTaskController.getStatusBarDisableFlags(
+ LOCK_TASK_FEATURE_HOME);
+ // THEN unsupported feature flags should still be untouched
+ assertTrue((~STATUS_BAR_MASK_LOCKED & flags.first) == 0);
+ // THEN home button should indeed be enabled
+ assertTrue((StatusBarManager.DISABLE_HOME & flags.first) == 0);
+ // THEN other feature flags should remain disabled
+ assertTrue((StatusBarManager.DISABLE2_NOTIFICATION_SHADE & flags.second) != 0);
+
+ // WHEN only global actions menu and notifications are enabled
+ flags = mLockTaskController.getStatusBarDisableFlags(
+ DevicePolicyManager.LOCK_TASK_FEATURE_GLOBAL_ACTIONS
+ | DevicePolicyManager.LOCK_TASK_FEATURE_NOTIFICATIONS);
+ // THEN unsupported feature flags should still be untouched
+ assertTrue((~STATUS_BAR_MASK_LOCKED & flags.first) == 0);
+ // THEN notifications should be enabled
+ assertTrue((StatusBarManager.DISABLE_NOTIFICATION_ICONS & flags.first) == 0);
+ assertTrue((StatusBarManager.DISABLE_NOTIFICATION_ALERTS & flags.first) == 0);
+ assertTrue((StatusBarManager.DISABLE2_NOTIFICATION_SHADE & flags.second) == 0);
+ // THEN global actions should be enabled
+ assertTrue((StatusBarManager.DISABLE2_GLOBAL_ACTIONS & flags.second) == 0);
+ // THEN quick settings should still be disabled
+ assertTrue((StatusBarManager.DISABLE2_QUICK_SETTINGS & flags.second) != 0);
+ }
+
+ private TaskRecord getTaskRecord(int lockTaskAuth) {
+ return getTaskRecord(TEST_PACKAGE_NAME, lockTaskAuth);
+ }
+
+ private TaskRecord getTaskRecord(String pkg, int lockTaskAuth) {
+ final Intent intent = new Intent()
+ .setComponent(ComponentName.createRelative(pkg, TEST_CLASS_NAME));
+ return getTaskRecord(intent, lockTaskAuth);
+ }
+
+ private TaskRecord getTaskRecord(Intent intent, int lockTaskAuth) {
+ TaskRecord tr = mock(TaskRecord.class);
+ tr.mLockTaskAuth = lockTaskAuth;
+ tr.intent = intent;
+ tr.userId = TEST_USER_ID;
+ return tr;
+ }
+
+ /**
+ * @param isAppAware {@code true} if the app has marked if_whitelisted in its manifest
+ */
+ private TaskRecord getTaskRecordForUpdate(String pkg, boolean isAppAware) {
+ final int authIfWhitelisted = isAppAware
+ ? TaskRecord.LOCK_TASK_AUTH_LAUNCHABLE
+ : TaskRecord.LOCK_TASK_AUTH_WHITELISTED;
+ TaskRecord tr = getTaskRecord(pkg, authIfWhitelisted);
+ doAnswer((invocation) -> {
+ boolean isWhitelisted =
+ mLockTaskController.isPackageWhitelisted(TEST_USER_ID, pkg);
+ tr.mLockTaskAuth = isWhitelisted
+ ? authIfWhitelisted
+ : TaskRecord.LOCK_TASK_AUTH_PINNABLE;
+ return null;
+ }).when(tr).setLockTaskAuth();
+ return tr;
+ }
+
+ private void verifyLockTaskStarted(int statusBarMask, int statusBarMask2) throws Exception {
+ // THEN the keyguard should have been disabled
+ verify(mWindowManager).disableKeyguard(any(IBinder.class), anyString());
+ // THEN the status bar should have been disabled
+ verify(mStatusBarService).disable(eq(statusBarMask), any(IBinder.class),
+ eq(mContext.getPackageName()));
+ verify(mStatusBarService).disable2(eq(statusBarMask2), any(IBinder.class),
+ eq(mContext.getPackageName()));
+ // THEN recents should have been notified
+ verify(mRecentTasks).onLockTaskModeStateChanged(anyInt(), eq(TEST_USER_ID));
+ // THEN the DO/PO should be informed about the operation
+ verify(mDevicePolicyManager).notifyLockTaskModeChanged(true, TEST_PACKAGE_NAME,
+ TEST_USER_ID);
+ }
+
+ private void verifyLockTaskStopped(VerificationMode mode) throws Exception {
+ // THEN the keyguard should have been disabled
+ verify(mWindowManager, mode).reenableKeyguard(any(IBinder.class));
+ // THEN the status bar should have been disabled
+ verify(mStatusBarService, mode).disable(eq(StatusBarManager.DISABLE_NONE),
+ any(IBinder.class), eq(mContext.getPackageName()));
+ verify(mStatusBarService, mode).disable2(eq(StatusBarManager.DISABLE2_NONE),
+ any(IBinder.class), eq(mContext.getPackageName()));
+ // THEN the DO/PO should be informed about the operation
+ verify(mDevicePolicyManager, mode).notifyLockTaskModeChanged(false, null, TEST_USER_ID);
+ }
+
+ /**
+ * Special handler implementation that executes any message / runnable posted immediately on the
+ * thread that it's posted on rather than enqueuing them on its looper.
+ */
+ private static class ImmediatelyExecuteHandler extends Handler {
+ @Override
+ public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
+ if (msg.getCallback() != null) {
+ msg.getCallback().run();
+ }
+ return true;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/MemoryStatUtilTest.java b/services/tests/servicestests/src/com/android/server/am/MemoryStatUtilTest.java
new file mode 100644
index 0000000..5518ca5
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/MemoryStatUtilTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.am;
+
+import static com.android.server.am.MemoryStatUtil.parseMemoryStatFromMemcg;
+import static com.android.server.am.MemoryStatUtil.parseMemoryStatFromProcfs;
+import static com.android.server.am.MemoryStatUtil.MemoryStat;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MemoryStatUtilTest {
+ private String MEMORY_STAT_CONTENTS = String.join(
+ "\n",
+ "cache 96", // keep different from total_cache to catch reading wrong value
+ "rss 97", // keep different from total_rss to catch reading wrong value
+ "rss_huge 0",
+ "mapped_file 524288",
+ "writeback 0",
+ "swap 95", // keep different from total_rss to catch reading wrong value
+ "pgpgin 16717",
+ "pgpgout 5037",
+ "pgfault 99", // keep different from total_pgfault to catch reading wrong value
+ "pgmajfault 98", // keep different from total_pgmajfault to catch reading wrong value
+ "inactive_anon 503808",
+ "active_anon 46309376",
+ "inactive_file 876544",
+ "active_file 81920",
+ "unevictable 0",
+ "hierarchical_memory_limit 18446744073709551615",
+ "hierarchical_memsw_limit 18446744073709551615",
+ "total_cache 4",
+ "total_rss 3",
+ "total_rss_huge 0",
+ "total_mapped_file 524288",
+ "total_writeback 0",
+ "total_swap 5",
+ "total_pgpgin 16717",
+ "total_pgpgout 5037",
+ "total_pgfault 1",
+ "total_pgmajfault 2",
+ "total_inactive_anon 503808",
+ "total_active_anon 46309376",
+ "total_inactive_file 876544",
+ "total_active_file 81920",
+ "total_unevictable 0");
+
+ private String PROC_STAT_CONTENTS = String.join(
+ " ",
+ "1040",
+ "(system_server)",
+ "S",
+ "544",
+ "544",
+ "0",
+ "0",
+ "-1",
+ "1077936448",
+ "1", // this is pgfault
+ "0",
+ "2", // this is pgmajfault
+ "0",
+ "44533",
+ "13471",
+ "0",
+ "0",
+ "18",
+ "-2",
+ "117",
+ "0",
+ "2206",
+ "1257177088",
+ "3", // this is rss in bytes
+ "4294967295",
+ "2936971264",
+ "2936991289",
+ "3198888320",
+ "3198879848",
+ "2903927664",
+ "0",
+ "4612",
+ "0",
+ "1073775864",
+ "4294967295",
+ "0",
+ "0",
+ "17",
+ "0",
+ "0",
+ "0",
+ "0",
+ "0",
+ "0",
+ "2936999088",
+ "2936999936",
+ "2958692352",
+ "3198888595",
+ "3198888671",
+ "3198888671",
+ "3198889956",
+ "0");
+
+ @Test
+ public void testParseMemoryStatFromMemcg_parsesCorrectValues() throws Exception {
+ MemoryStat stat = parseMemoryStatFromMemcg(MEMORY_STAT_CONTENTS);
+ assertEquals(stat.pgfault, 1);
+ assertEquals(stat.pgmajfault, 2);
+ assertEquals(stat.rssInBytes, 3);
+ assertEquals(stat.cacheInBytes, 4);
+ assertEquals(stat.swapInBytes, 5);
+ }
+
+ @Test
+ public void testParseMemoryStatFromMemcg_emptyMemoryStatContents() throws Exception {
+ MemoryStat stat = parseMemoryStatFromMemcg("");
+ assertNull(stat);
+
+ stat = parseMemoryStatFromMemcg(null);
+ assertNull(stat);
+ }
+
+ @Test
+ public void testParseMemoryStatFromProcfs_parsesCorrectValues() throws Exception {
+ MemoryStat stat = parseMemoryStatFromProcfs(PROC_STAT_CONTENTS);
+ assertEquals(1, stat.pgfault);
+ assertEquals(2, stat.pgmajfault);
+ assertEquals(3, stat.rssInBytes);
+ assertEquals(0, stat.cacheInBytes);
+ assertEquals(0, stat.swapInBytes);
+ }
+
+ @Test
+ public void testParseMemoryStatFromProcfs_emptyContents() throws Exception {
+ MemoryStat stat = parseMemoryStatFromProcfs("");
+ assertNull(stat);
+
+ stat = parseMemoryStatFromProcfs(null);
+ assertNull(stat);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/PendingRemoteAnimationRegistryTest.java b/services/tests/servicestests/src/com/android/server/am/PendingRemoteAnimationRegistryTest.java
new file mode 100644
index 0000000..2baf995
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/PendingRemoteAnimationRegistryTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.am;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.annotation.Nullable;
+import android.app.ActivityOptions;
+import android.os.Handler;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.ArrayMap;
+import android.view.RemoteAnimationAdapter;
+
+import com.android.server.testutils.OffsettableClock;
+import com.android.server.testutils.TestHandler;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * atest PendingRemoteAnimationRegistryTest
+ */
+@SmallTest
+@Presubmit
+@FlakyTest
+@RunWith(AndroidJUnit4.class)
+public class PendingRemoteAnimationRegistryTest extends ActivityTestsBase {
+
+ @Mock RemoteAnimationAdapter mAdapter;
+ private PendingRemoteAnimationRegistry mRegistry;
+ private final OffsettableClock mClock = new OffsettableClock.Stopped();
+ private TestHandler mHandler;
+ private ActivityManagerService mService;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ mService = createActivityManagerService();
+ mService.mHandlerThread.getThreadHandler().runWithScissors(() -> {
+ mHandler = new TestHandler(null, mClock);
+ }, 0);
+ mRegistry = new PendingRemoteAnimationRegistry(mService, mHandler);
+ }
+
+ @Test
+ public void testOverrideActivityOptions() {
+ mRegistry.addPendingAnimation("com.android.test", mAdapter);
+ ActivityOptions opts = ActivityOptions.makeBasic();
+ opts = mRegistry.overrideOptionsIfNeeded("com.android.test", opts);
+ assertEquals(mAdapter, opts.getRemoteAnimationAdapter());
+ }
+
+ @Test
+ public void testOverrideActivityOptions_null() {
+ mRegistry.addPendingAnimation("com.android.test", mAdapter);
+ final ActivityOptions opts = mRegistry.overrideOptionsIfNeeded("com.android.test", null);
+ assertNotNull(opts);
+ assertEquals(mAdapter, opts.getRemoteAnimationAdapter());
+ }
+
+ @Test
+ public void testTimeout() {
+ mRegistry.addPendingAnimation("com.android.test", mAdapter);
+ mClock.fastForward(5000);
+ mHandler.timeAdvance();
+ assertNull(mRegistry.overrideOptionsIfNeeded("com.android.test", null));
+ }
+
+ @Test
+ public void testTimeout_overridenEntry() {
+ mRegistry.addPendingAnimation("com.android.test", mAdapter);
+ mClock.fastForward(2500);
+ mHandler.timeAdvance();
+ mRegistry.addPendingAnimation("com.android.test", mAdapter);
+ mClock.fastForward(1000);
+ mHandler.timeAdvance();
+ final ActivityOptions opts = mRegistry.overrideOptionsIfNeeded("com.android.test", null);
+ assertEquals(mAdapter, opts.getRemoteAnimationAdapter());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/PersistentConnectionTest.java b/services/tests/servicestests/src/com/android/server/am/PersistentConnectionTest.java
new file mode 100644
index 0000000..54f93a8
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/PersistentConnectionTest.java
@@ -0,0 +1,354 @@
+/*
+ * 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.am;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.admin.IDeviceAdminService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Pair;
+
+import org.mockito.ArgumentMatchers;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+
+@SmallTest
+public class PersistentConnectionTest extends AndroidTestCase {
+ private static class MyConnection extends PersistentConnection<IDeviceAdminService> {
+ public long uptimeMillis = 12345;
+
+ public ArrayList<Pair<Runnable, Long>> scheduledRunnables = new ArrayList<>();
+
+ public MyConnection(String tag, Context context, Handler handler, int userId,
+ ComponentName componentName, long rebindBackoffSeconds,
+ double rebindBackoffIncrease, long rebindMaxBackoffSeconds) {
+ super(tag, context, handler, userId, componentName,
+ rebindBackoffSeconds, rebindBackoffIncrease, rebindMaxBackoffSeconds);
+ }
+
+ @Override
+ protected IDeviceAdminService asInterface(IBinder binder) {
+ return (IDeviceAdminService) binder;
+ }
+
+ @Override
+ long injectUptimeMillis() {
+ return uptimeMillis;
+ }
+
+ @Override
+ void injectPostAtTime(Runnable r, long uptimeMillis) {
+ scheduledRunnables.add(Pair.create(r, uptimeMillis));
+ }
+
+ @Override
+ void injectRemoveCallbacks(Runnable r) {
+ for (int i = scheduledRunnables.size() - 1; i >= 0; i--) {
+ if (scheduledRunnables.get(i).first.equals(r)) {
+ scheduledRunnables.remove(i);
+ }
+ }
+ }
+
+ void elapse(long milliSeconds) {
+ uptimeMillis += milliSeconds;
+
+ // Fire the scheduled runnables.
+
+ // Note we collect first and then run all, because sometimes a scheduled runnable
+ // calls removeCallbacks.
+ final ArrayList<Runnable> list = new ArrayList<>();
+
+ for (int i = scheduledRunnables.size() - 1; i >= 0; i--) {
+ if (scheduledRunnables.get(i).second <= uptimeMillis) {
+ list.add(scheduledRunnables.get(i).first);
+ scheduledRunnables.remove(i);
+ }
+ }
+
+ Collections.reverse(list);
+ for (Runnable r : list) {
+ r.run();
+ }
+ }
+ }
+
+ public void testAll() {
+ final Context context = mock(Context.class);
+ final int userId = 11;
+ final ComponentName cn = ComponentName.unflattenFromString("a.b.c/def");
+ final Handler handler = new Handler(Looper.getMainLooper());
+
+ final MyConnection conn = new MyConnection("tag", context, handler, userId, cn,
+ /* rebindBackoffSeconds= */ 5,
+ /* rebindBackoffIncrease= */ 1.5,
+ /* rebindMaxBackoffSeconds= */ 11);
+
+ assertFalse(conn.isBound());
+ assertFalse(conn.isConnected());
+ assertFalse(conn.isRebindScheduled());
+ assertEquals(5000, conn.getNextBackoffMsForTest());
+ assertNull(conn.getServiceBinder());
+
+ when(context.bindServiceAsUser(any(Intent.class), any(ServiceConnection.class), anyInt(),
+ any(Handler.class), any(UserHandle.class)))
+ .thenReturn(true);
+
+ // Call bind.
+ conn.bind();
+
+ assertTrue(conn.isBound());
+ assertTrue(conn.shouldBeBoundForTest());
+ assertFalse(conn.isConnected());
+ assertFalse(conn.isRebindScheduled());
+ assertNull(conn.getServiceBinder());
+
+ assertEquals(5000, conn.getNextBackoffMsForTest());
+
+ verify(context).bindServiceAsUser(
+ ArgumentMatchers.argThat(intent -> cn.equals(intent.getComponent())),
+ eq(conn.getServiceConnectionForTest()),
+ eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE),
+ eq(handler), eq(UserHandle.of(userId)));
+
+ // AM responds...
+ conn.getServiceConnectionForTest().onServiceConnected(cn,
+ new IDeviceAdminService.Stub() {});
+
+ assertTrue(conn.isBound());
+ assertTrue(conn.shouldBeBoundForTest());
+ assertTrue(conn.isConnected());
+ assertNotNull(conn.getServiceBinder());
+ assertFalse(conn.isRebindScheduled());
+
+ assertEquals(5000, conn.getNextBackoffMsForTest());
+
+
+ // Now connected.
+
+ // Call unbind...
+ conn.unbind();
+ assertFalse(conn.isBound());
+ assertFalse(conn.shouldBeBoundForTest());
+ assertFalse(conn.isConnected());
+ assertNull(conn.getServiceBinder());
+ assertFalse(conn.isRebindScheduled());
+
+ // Caller bind again...
+ conn.bind();
+
+ assertTrue(conn.isBound());
+ assertTrue(conn.shouldBeBoundForTest());
+ assertFalse(conn.isConnected());
+ assertFalse(conn.isRebindScheduled());
+ assertNull(conn.getServiceBinder());
+
+ assertEquals(5000, conn.getNextBackoffMsForTest());
+
+
+ // Now connected again.
+
+ // The service got killed...
+ conn.getServiceConnectionForTest().onServiceDisconnected(cn);
+
+ assertTrue(conn.isBound());
+ assertTrue(conn.shouldBeBoundForTest());
+ assertFalse(conn.isConnected());
+ assertNull(conn.getServiceBinder());
+ assertFalse(conn.isRebindScheduled());
+
+ assertEquals(5000, conn.getNextBackoffMsForTest());
+
+ // Connected again...
+ conn.getServiceConnectionForTest().onServiceConnected(cn,
+ new IDeviceAdminService.Stub() {});
+
+ assertTrue(conn.isBound());
+ assertTrue(conn.shouldBeBoundForTest());
+ assertTrue(conn.isConnected());
+ assertNotNull(conn.getServiceBinder());
+ assertFalse(conn.isRebindScheduled());
+
+ assertEquals(5000, conn.getNextBackoffMsForTest());
+
+
+ // Then the binding is "died"...
+ conn.getServiceConnectionForTest().onBindingDied(cn);
+
+ assertFalse(conn.isBound());
+ assertTrue(conn.shouldBeBoundForTest());
+ assertFalse(conn.isConnected());
+ assertNull(conn.getServiceBinder());
+ assertTrue(conn.isRebindScheduled());
+
+ assertEquals(7500, conn.getNextBackoffMsForTest());
+
+ assertEquals(
+ Arrays.asList(Pair.create(conn.getBindForBackoffRunnableForTest(),
+ conn.uptimeMillis + 5000)),
+ conn.scheduledRunnables);
+
+ // 5000 ms later...
+ conn.elapse(5000);
+
+ assertTrue(conn.isBound());
+ assertTrue(conn.shouldBeBoundForTest());
+ assertFalse(conn.isConnected());
+ assertNull(conn.getServiceBinder());
+ assertFalse(conn.isRebindScheduled());
+
+ assertEquals(7500, conn.getNextBackoffMsForTest());
+
+ // Connected.
+ conn.getServiceConnectionForTest().onServiceConnected(cn,
+ new IDeviceAdminService.Stub() {});
+
+ assertTrue(conn.isBound());
+ assertTrue(conn.shouldBeBoundForTest());
+ assertTrue(conn.isConnected());
+ assertNotNull(conn.getServiceBinder());
+ assertFalse(conn.isRebindScheduled());
+
+ assertEquals(7500, conn.getNextBackoffMsForTest());
+
+ // Then the binding is "died"...
+ conn.getServiceConnectionForTest().onBindingDied(cn);
+
+ assertFalse(conn.isBound());
+ assertTrue(conn.shouldBeBoundForTest());
+ assertFalse(conn.isConnected());
+ assertNull(conn.getServiceBinder());
+ assertTrue(conn.isRebindScheduled());
+
+ assertEquals(11000, conn.getNextBackoffMsForTest());
+
+ assertEquals(
+ Arrays.asList(Pair.create(conn.getBindForBackoffRunnableForTest(),
+ conn.uptimeMillis + 7500)),
+ conn.scheduledRunnables);
+
+ // Later...
+ conn.elapse(7500);
+
+ assertTrue(conn.isBound());
+ assertTrue(conn.shouldBeBoundForTest());
+ assertFalse(conn.isConnected());
+ assertNull(conn.getServiceBinder());
+ assertFalse(conn.isRebindScheduled());
+
+ assertEquals(11000, conn.getNextBackoffMsForTest());
+
+
+ // Then the binding is "died"...
+ conn.getServiceConnectionForTest().onBindingDied(cn);
+
+ assertFalse(conn.isBound());
+ assertTrue(conn.shouldBeBoundForTest());
+ assertFalse(conn.isConnected());
+ assertNull(conn.getServiceBinder());
+ assertTrue(conn.isRebindScheduled());
+
+ assertEquals(11000, conn.getNextBackoffMsForTest());
+
+ assertEquals(
+ Arrays.asList(Pair.create(conn.getBindForBackoffRunnableForTest(),
+ conn.uptimeMillis + 11000)),
+ conn.scheduledRunnables);
+
+ // Call unbind...
+ conn.unbind();
+ assertFalse(conn.isBound());
+ assertFalse(conn.shouldBeBoundForTest());
+ assertFalse(conn.isConnected());
+ assertNull(conn.getServiceBinder());
+ assertFalse(conn.isRebindScheduled());
+
+ // Call bind again... And now the backoff is reset to 5000.
+ conn.bind();
+
+ assertTrue(conn.isBound());
+ assertTrue(conn.shouldBeBoundForTest());
+ assertFalse(conn.isConnected());
+ assertFalse(conn.isRebindScheduled());
+ assertNull(conn.getServiceBinder());
+
+ assertEquals(5000, conn.getNextBackoffMsForTest());
+ }
+
+ public void testReconnectFiresAfterUnbind() {
+ final Context context = mock(Context.class);
+ final int userId = 11;
+ final ComponentName cn = ComponentName.unflattenFromString("a.b.c/def");
+ final Handler handler = new Handler(Looper.getMainLooper());
+
+ final MyConnection conn = new MyConnection("tag", context, handler, userId, cn,
+ /* rebindBackoffSeconds= */ 5,
+ /* rebindBackoffIncrease= */ 1.5,
+ /* rebindMaxBackoffSeconds= */ 11);
+
+ when(context.bindServiceAsUser(any(Intent.class), any(ServiceConnection.class), anyInt(),
+ any(Handler.class), any(UserHandle.class)))
+ .thenReturn(true);
+
+ // Bind.
+ conn.bind();
+
+ assertTrue(conn.isBound());
+ assertTrue(conn.shouldBeBoundForTest());
+ assertFalse(conn.isRebindScheduled());
+
+ conn.elapse(1000);
+
+ // Service crashes.
+ conn.getServiceConnectionForTest().onBindingDied(cn);
+
+ assertFalse(conn.isBound());
+ assertTrue(conn.shouldBeBoundForTest());
+ assertTrue(conn.isRebindScheduled());
+
+ assertEquals(7500, conn.getNextBackoffMsForTest());
+
+ // Call unbind.
+ conn.unbind();
+ assertFalse(conn.isBound());
+ assertFalse(conn.shouldBeBoundForTest());
+
+ // Now, at this point, it's possible that the scheduled runnable had already been fired
+ // before during the unbind() call, and waiting on mLock.
+ // To simulate it, we just call the runnable here.
+ conn.getBindForBackoffRunnableForTest().run();
+
+ // Should still not be bound.
+ assertFalse(conn.isBound());
+ assertFalse(conn.shouldBeBoundForTest());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/RecentTasksTest.java b/services/tests/servicestests/src/com/android/server/am/RecentTasksTest.java
new file mode 100644
index 0000000..b73ac89
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/RecentTasksTest.java
@@ -0,0 +1,1026 @@
+/*
+ * 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.am;
+
+import static android.app.ActivityManager.SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
+import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import static java.lang.Integer.MAX_VALUE;
+
+import android.annotation.TestApi;
+import android.app.ActivityManager;
+import android.app.ActivityManager.RecentTaskInfo;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.WindowConfiguration;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
+import android.content.pm.UserInfo;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.MutableLong;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+
+import com.android.server.am.RecentTasks.Callbacks;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+/**
+ * atest FrameworksServicesTests:RecentTasksTest
+ */
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class RecentTasksTest extends ActivityTestsBase {
+ private static final int TEST_USER_0_ID = 0;
+ private static final int TEST_USER_1_ID = 10;
+ private static final int TEST_QUIET_USER_ID = 20;
+ private static final UserInfo DEFAULT_USER_INFO = new UserInfo();
+ private static final UserInfo QUIET_USER_INFO = new UserInfo();
+ private static int LAST_TASK_ID = 1;
+ private static int LAST_STACK_ID = 1;
+ private static int INVALID_STACK_ID = 999;
+
+ private Context mContext = InstrumentationRegistry.getContext();
+ private ActivityManagerService mService;
+ private ActivityDisplay mDisplay;
+ private ActivityDisplay mOtherDisplay;
+ private ActivityStack mStack;
+ private ActivityStack mHomeStack;
+ private TestTaskPersister mTaskPersister;
+ private TestRecentTasks mRecentTasks;
+ private TestRunningTasks mRunningTasks;
+
+ private ArrayList<TaskRecord> mTasks;
+ private ArrayList<TaskRecord> mSameDocumentTasks;
+
+ private CallbacksRecorder mCallbacksRecorder;
+
+ class TestUserController extends UserController {
+ TestUserController(ActivityManagerService service) {
+ super(service);
+ }
+
+ @Override
+ int[] getCurrentProfileIds() {
+ return new int[] { TEST_USER_0_ID, TEST_QUIET_USER_ID };
+ }
+
+ @Override
+ Set<Integer> getProfileIds(int userId) {
+ Set<Integer> profileIds = new HashSet<>();
+ profileIds.add(TEST_USER_0_ID);
+ profileIds.add(TEST_QUIET_USER_ID);
+ return profileIds;
+ }
+
+ @Override
+ UserInfo getUserInfo(int userId) {
+ switch (userId) {
+ case TEST_USER_0_ID:
+ case TEST_USER_1_ID:
+ return DEFAULT_USER_INFO;
+ case TEST_QUIET_USER_ID:
+ return QUIET_USER_INFO;
+ }
+ return null;
+ }
+ }
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mTaskPersister = new TestTaskPersister(mContext.getFilesDir());
+ mService = setupActivityManagerService(new MyTestActivityManagerService(mContext));
+ mRecentTasks = (TestRecentTasks) mService.getRecentTasks();
+ mRecentTasks.loadParametersFromResources(mContext.getResources());
+ mHomeStack = mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_HOME, true /* onTop */);
+ mStack = mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ ((MyTestActivityStackSupervisor) mService.mStackSupervisor).setHomeStack(mHomeStack);
+ mCallbacksRecorder = new CallbacksRecorder();
+ mRecentTasks.registerCallback(mCallbacksRecorder);
+ QUIET_USER_INFO.flags = UserInfo.FLAG_MANAGED_PROFILE | UserInfo.FLAG_QUIET_MODE;
+
+ mTasks = new ArrayList<>();
+ mTasks.add(createTaskBuilder(".Task1").build());
+ mTasks.add(createTaskBuilder(".Task2").build());
+ mTasks.add(createTaskBuilder(".Task3").build());
+ mTasks.add(createTaskBuilder(".Task4").build());
+ mTasks.add(createTaskBuilder(".Task5").build());
+
+ mSameDocumentTasks = new ArrayList<>();
+ mSameDocumentTasks.add(createDocumentTask(".DocumentTask1"));
+ mSameDocumentTasks.add(createDocumentTask(".DocumentTask1"));
+ }
+
+ @Test
+ public void testCallbacks() throws Exception {
+ // Add some tasks
+ mRecentTasks.add(mTasks.get(0));
+ mRecentTasks.add(mTasks.get(1));
+ assertTrue(mCallbacksRecorder.added.contains(mTasks.get(0))
+ && mCallbacksRecorder.added.contains(mTasks.get(1)));
+ assertTrue(mCallbacksRecorder.trimmed.isEmpty());
+ assertTrue(mCallbacksRecorder.removed.isEmpty());
+ mCallbacksRecorder.clear();
+
+ // Remove some tasks
+ mRecentTasks.remove(mTasks.get(0));
+ mRecentTasks.remove(mTasks.get(1));
+ assertTrue(mCallbacksRecorder.added.isEmpty());
+ assertTrue(mCallbacksRecorder.trimmed.isEmpty());
+ assertTrue(mCallbacksRecorder.removed.contains(mTasks.get(0)));
+ assertTrue(mCallbacksRecorder.removed.contains(mTasks.get(1)));
+ mCallbacksRecorder.clear();
+
+ // Remove the callback, ensure we don't get any calls
+ mRecentTasks.unregisterCallback(mCallbacksRecorder);
+ mRecentTasks.add(mTasks.get(0));
+ mRecentTasks.remove(mTasks.get(0));
+ assertTrue(mCallbacksRecorder.added.isEmpty());
+ assertTrue(mCallbacksRecorder.trimmed.isEmpty());
+ assertTrue(mCallbacksRecorder.removed.isEmpty());
+ }
+
+ @Test
+ public void testAddTasksNoMultiple_expectNoTrim() throws Exception {
+ // Add same non-multiple-task document tasks will remove the task (to re-add it) but not
+ // trim it
+ TaskRecord documentTask1 = createDocumentTask(".DocumentTask1");
+ TaskRecord documentTask2 = createDocumentTask(".DocumentTask1");
+ mRecentTasks.add(documentTask1);
+ mRecentTasks.add(documentTask2);
+ assertTrue(mCallbacksRecorder.added.contains(documentTask1));
+ assertTrue(mCallbacksRecorder.added.contains(documentTask2));
+ assertTrue(mCallbacksRecorder.trimmed.isEmpty());
+ assertTrue(mCallbacksRecorder.removed.contains(documentTask1));
+ }
+
+ @Test
+ public void testAddTasksMaxTaskRecents_expectNoTrim() throws Exception {
+ // Add a task hitting max-recents for that app will remove the task (to add the next one)
+ // but not trim it
+ TaskRecord documentTask1 = createDocumentTask(".DocumentTask1");
+ TaskRecord documentTask2 = createDocumentTask(".DocumentTask1");
+ documentTask1.maxRecents = 1;
+ documentTask2.maxRecents = 1;
+ mRecentTasks.add(documentTask1);
+ mRecentTasks.add(documentTask2);
+ assertTrue(mCallbacksRecorder.added.contains(documentTask1));
+ assertTrue(mCallbacksRecorder.added.contains(documentTask2));
+ assertTrue(mCallbacksRecorder.trimmed.isEmpty());
+ assertTrue(mCallbacksRecorder.removed.contains(documentTask1));
+ }
+
+ @Test
+ public void testAddTasksSameTask_expectNoTrim() throws Exception {
+ // Add a task that is already in the task list does not trigger any callbacks, it just
+ // moves in the list
+ TaskRecord documentTask1 = createDocumentTask(".DocumentTask1");
+ mRecentTasks.add(documentTask1);
+ mRecentTasks.add(documentTask1);
+ assertTrue(mCallbacksRecorder.added.size() == 1);
+ assertTrue(mCallbacksRecorder.added.contains(documentTask1));
+ assertTrue(mCallbacksRecorder.trimmed.isEmpty());
+ assertTrue(mCallbacksRecorder.removed.isEmpty());
+ }
+
+ @Test
+ public void testAddTasksMultipleDocumentTasks_expectNoTrim() throws Exception {
+ // Add same multiple-task document tasks does not trim the first tasks
+ TaskRecord documentTask1 = createDocumentTask(".DocumentTask1",
+ FLAG_ACTIVITY_MULTIPLE_TASK);
+ TaskRecord documentTask2 = createDocumentTask(".DocumentTask1",
+ FLAG_ACTIVITY_MULTIPLE_TASK);
+ mRecentTasks.add(documentTask1);
+ mRecentTasks.add(documentTask2);
+ assertTrue(mCallbacksRecorder.added.size() == 2);
+ assertTrue(mCallbacksRecorder.added.contains(documentTask1));
+ assertTrue(mCallbacksRecorder.added.contains(documentTask2));
+ assertTrue(mCallbacksRecorder.trimmed.isEmpty());
+ assertTrue(mCallbacksRecorder.removed.isEmpty());
+ }
+
+ @Test
+ public void testAddTasksMultipleTasks_expectRemovedNoTrim() throws Exception {
+ // Add multiple same-affinity non-document tasks, ensure that it removes the other task,
+ // but that it does not trim it
+ TaskRecord task1 = createTaskBuilder(".Task1")
+ .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
+ .build();
+ TaskRecord task2 = createTaskBuilder(".Task1")
+ .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
+ .build();
+ mRecentTasks.add(task1);
+ assertTrue(mCallbacksRecorder.added.size() == 1);
+ assertTrue(mCallbacksRecorder.added.contains(task1));
+ assertTrue(mCallbacksRecorder.trimmed.isEmpty());
+ assertTrue(mCallbacksRecorder.removed.isEmpty());
+ mCallbacksRecorder.clear();
+ mRecentTasks.add(task2);
+ assertTrue(mCallbacksRecorder.added.size() == 1);
+ assertTrue(mCallbacksRecorder.added.contains(task2));
+ assertTrue(mCallbacksRecorder.trimmed.isEmpty());
+ assertTrue(mCallbacksRecorder.removed.size() == 1);
+ assertTrue(mCallbacksRecorder.removed.contains(task1));
+ }
+
+ @Test
+ public void testAddTasksDifferentStacks_expectNoRemove() throws Exception {
+ // Adding the same task with different activity types should not trigger removal of the
+ // other task
+ TaskRecord task1 = createTaskBuilder(".Task1")
+ .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
+ .setStack(mHomeStack).build();
+ TaskRecord task2 = createTaskBuilder(".Task1")
+ .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
+ .setStack(mStack).build();
+ mRecentTasks.add(task1);
+ mRecentTasks.add(task2);
+ assertTrue(mCallbacksRecorder.added.size() == 2);
+ assertTrue(mCallbacksRecorder.added.contains(task1));
+ assertTrue(mCallbacksRecorder.added.contains(task2));
+ assertTrue(mCallbacksRecorder.trimmed.isEmpty());
+ assertTrue(mCallbacksRecorder.removed.isEmpty());
+ }
+
+ @Test
+ public void testAddTaskCompatibleActivityType_expectRemove() throws Exception {
+ // Test with undefined activity type since the type is not persisted by the task persister
+ // and we want to ensure that a new task will match a restored task
+ Configuration config1 = new Configuration();
+ config1.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED);
+ TaskRecord task1 = createTaskBuilder(".Task1")
+ .setFlags(FLAG_ACTIVITY_NEW_TASK)
+ .setStack(mStack)
+ .build();
+ task1.onConfigurationChanged(config1);
+ assertTrue(task1.getActivityType() == ACTIVITY_TYPE_UNDEFINED);
+ mRecentTasks.add(task1);
+ mCallbacksRecorder.clear();
+
+ TaskRecord task2 = createTaskBuilder(".Task1")
+ .setFlags(FLAG_ACTIVITY_NEW_TASK)
+ .setStack(mStack)
+ .build();
+ assertTrue(task2.getActivityType() == ACTIVITY_TYPE_STANDARD);
+ mRecentTasks.add(task2);
+ assertTrue(mCallbacksRecorder.added.size() == 1);
+ assertTrue(mCallbacksRecorder.added.contains(task2));
+ assertTrue(mCallbacksRecorder.trimmed.isEmpty());
+ assertTrue(mCallbacksRecorder.removed.size() == 1);
+ assertTrue(mCallbacksRecorder.removed.contains(task1));
+ }
+
+ @Test
+ public void testAddTaskCompatibleActivityTypeDifferentUser_expectNoRemove() throws Exception {
+ Configuration config1 = new Configuration();
+ config1.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED);
+ TaskRecord task1 = createTaskBuilder(".Task1")
+ .setFlags(FLAG_ACTIVITY_NEW_TASK)
+ .setStack(mStack)
+ .setUserId(TEST_USER_0_ID)
+ .build();
+ task1.onConfigurationChanged(config1);
+ assertTrue(task1.getActivityType() == ACTIVITY_TYPE_UNDEFINED);
+ mRecentTasks.add(task1);
+ mCallbacksRecorder.clear();
+
+ TaskRecord task2 = createTaskBuilder(".Task1")
+ .setFlags(FLAG_ACTIVITY_NEW_TASK)
+ .setStack(mStack)
+ .setUserId(TEST_USER_1_ID)
+ .build();
+ assertTrue(task2.getActivityType() == ACTIVITY_TYPE_STANDARD);
+ mRecentTasks.add(task2);
+ assertTrue(mCallbacksRecorder.added.size() == 1);
+ assertTrue(mCallbacksRecorder.added.contains(task2));
+ assertTrue(mCallbacksRecorder.trimmed.isEmpty());
+ assertTrue(mCallbacksRecorder.removed.isEmpty());
+ }
+
+ @Test
+ public void testAddTaskCompatibleWindowingMode_expectRemove() throws Exception {
+ Configuration config1 = new Configuration();
+ config1.windowConfiguration.setWindowingMode(WINDOWING_MODE_UNDEFINED);
+ TaskRecord task1 = createTaskBuilder(".Task1")
+ .setFlags(FLAG_ACTIVITY_NEW_TASK)
+ .setStack(mStack)
+ .build();
+ task1.onConfigurationChanged(config1);
+ assertTrue(task1.getWindowingMode() == WINDOWING_MODE_UNDEFINED);
+ mRecentTasks.add(task1);
+ mCallbacksRecorder.clear();
+
+ Configuration config2 = new Configuration();
+ config2.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ TaskRecord task2 = createTaskBuilder(".Task1")
+ .setFlags(FLAG_ACTIVITY_NEW_TASK)
+ .setStack(mStack)
+ .build();
+ task2.onConfigurationChanged(config2);
+ assertTrue(task2.getWindowingMode() == WINDOWING_MODE_FULLSCREEN);
+ mRecentTasks.add(task2);
+
+ assertTrue(mCallbacksRecorder.added.size() == 1);
+ assertTrue(mCallbacksRecorder.added.contains(task2));
+ assertTrue(mCallbacksRecorder.trimmed.isEmpty());
+ assertTrue(mCallbacksRecorder.removed.size() == 1);
+ assertTrue(mCallbacksRecorder.removed.contains(task1));
+ }
+
+ @Test
+ public void testAddTaskIncompatibleWindowingMode_expectNoRemove() throws Exception {
+ Configuration config1 = new Configuration();
+ config1.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ TaskRecord task1 = createTaskBuilder(".Task1")
+ .setFlags(FLAG_ACTIVITY_NEW_TASK)
+ .setStack(mStack)
+ .build();
+ task1.onConfigurationChanged(config1);
+ assertTrue(task1.getWindowingMode() == WINDOWING_MODE_FULLSCREEN);
+ mRecentTasks.add(task1);
+
+ Configuration config2 = new Configuration();
+ config2.windowConfiguration.setWindowingMode(WINDOWING_MODE_PINNED);
+ TaskRecord task2 = createTaskBuilder(".Task1")
+ .setFlags(FLAG_ACTIVITY_NEW_TASK)
+ .setStack(mStack)
+ .build();
+ task2.onConfigurationChanged(config2);
+ assertTrue(task2.getWindowingMode() == WINDOWING_MODE_PINNED);
+ mRecentTasks.add(task2);
+
+ assertTrue(mCallbacksRecorder.added.size() == 2);
+ assertTrue(mCallbacksRecorder.added.contains(task1));
+ assertTrue(mCallbacksRecorder.added.contains(task2));
+ assertTrue(mCallbacksRecorder.trimmed.isEmpty());
+ assertTrue(mCallbacksRecorder.removed.isEmpty());
+ }
+
+ @Test
+ public void testUsersTasks() throws Exception {
+ mRecentTasks.setOnlyTestVisibleRange();
+
+ // Setup some tasks for the users
+ mTaskPersister.userTaskIdsOverride = new SparseBooleanArray();
+ mTaskPersister.userTaskIdsOverride.put(1, true);
+ mTaskPersister.userTaskIdsOverride.put(2, true);
+ mTaskPersister.userTasksOverride = new ArrayList<>();
+ mTaskPersister.userTasksOverride.add(createTaskBuilder(".UserTask1").build());
+ mTaskPersister.userTasksOverride.add(createTaskBuilder(".UserTask2").build());
+
+ // Assert no user tasks are initially loaded
+ assertTrue(mRecentTasks.usersWithRecentsLoadedLocked().length == 0);
+
+ // Load user 0 tasks
+ mRecentTasks.loadUserRecentsLocked(TEST_USER_0_ID);
+ assertTrue(arrayContainsUser(mRecentTasks.usersWithRecentsLoadedLocked(), TEST_USER_0_ID));
+ assertTrue(mRecentTasks.containsTaskId(1, TEST_USER_0_ID));
+ assertTrue(mRecentTasks.containsTaskId(2, TEST_USER_0_ID));
+
+ // Load user 1 tasks
+ mRecentTasks.loadUserRecentsLocked(TEST_USER_1_ID);
+ assertTrue(arrayContainsUser(mRecentTasks.usersWithRecentsLoadedLocked(), TEST_USER_0_ID));
+ assertTrue(arrayContainsUser(mRecentTasks.usersWithRecentsLoadedLocked(), TEST_USER_1_ID));
+ assertTrue(mRecentTasks.containsTaskId(1, TEST_USER_0_ID));
+ assertTrue(mRecentTasks.containsTaskId(2, TEST_USER_0_ID));
+ assertTrue(mRecentTasks.containsTaskId(1, TEST_USER_1_ID));
+ assertTrue(mRecentTasks.containsTaskId(2, TEST_USER_1_ID));
+
+ // Unload user 1 tasks
+ mRecentTasks.unloadUserDataFromMemoryLocked(TEST_USER_1_ID);
+ assertTrue(arrayContainsUser(mRecentTasks.usersWithRecentsLoadedLocked(), TEST_USER_0_ID));
+ assertFalse(arrayContainsUser(mRecentTasks.usersWithRecentsLoadedLocked(), TEST_USER_1_ID));
+ assertTrue(mRecentTasks.containsTaskId(1, TEST_USER_0_ID));
+ assertTrue(mRecentTasks.containsTaskId(2, TEST_USER_0_ID));
+
+ // Unload user 0 tasks
+ mRecentTasks.unloadUserDataFromMemoryLocked(TEST_USER_0_ID);
+ assertFalse(arrayContainsUser(mRecentTasks.usersWithRecentsLoadedLocked(), TEST_USER_0_ID));
+ assertFalse(arrayContainsUser(mRecentTasks.usersWithRecentsLoadedLocked(), TEST_USER_1_ID));
+ }
+
+ @Test
+ public void testOrderedIteration() throws Exception {
+ mRecentTasks.setOnlyTestVisibleRange();
+ TaskRecord task1 = createTaskBuilder(".Task1").build();
+ task1.lastActiveTime = new Random().nextInt();
+ TaskRecord task2 = createTaskBuilder(".Task1").build();
+ task2.lastActiveTime = new Random().nextInt();
+ TaskRecord task3 = createTaskBuilder(".Task1").build();
+ task3.lastActiveTime = new Random().nextInt();
+ TaskRecord task4 = createTaskBuilder(".Task1").build();
+ task4.lastActiveTime = new Random().nextInt();
+ mRecentTasks.add(task1);
+ mRecentTasks.add(task2);
+ mRecentTasks.add(task3);
+ mRecentTasks.add(task4);
+
+ MutableLong prevLastActiveTime = new MutableLong(0);
+ final ArrayList<TaskRecord> tasks = mRecentTasks.getRawTasks();
+ for (int i = 0; i < tasks.size(); i++) {
+ final TaskRecord task = tasks.get(i);
+ assertTrue(task.lastActiveTime >= prevLastActiveTime.value);
+ prevLastActiveTime.value = task.lastActiveTime;
+ }
+ }
+
+ @Test
+ public void testTrimToGlobalMaxNumRecents() throws Exception {
+ mRecentTasks.setOnlyTestVisibleRange();
+
+ // Limit the global maximum number of recent tasks to a fixed size
+ mRecentTasks.setGlobalMaxNumTasks(2 /* globalMaxNumTasks */);
+
+ // Add N+1 tasks
+ mRecentTasks.add(mTasks.get(0));
+ mRecentTasks.add(mTasks.get(1));
+ mRecentTasks.add(mTasks.get(2));
+
+ // Ensure that the last task was trimmed as an inactive task
+ assertTrimmed(mTasks.get(0));
+ }
+
+ @Test
+ public void testTrimQuietProfileTasks() throws Exception {
+ mRecentTasks.setOnlyTestVisibleRange();
+ TaskRecord qt1 = createTaskBuilder(".QuietTask1").setUserId(TEST_QUIET_USER_ID).build();
+ TaskRecord qt2 = createTaskBuilder(".QuietTask2").setUserId(TEST_QUIET_USER_ID).build();
+ mRecentTasks.add(qt1);
+ mRecentTasks.add(qt2);
+
+ mRecentTasks.add(mTasks.get(0));
+ mRecentTasks.add(mTasks.get(1));
+
+ // Ensure that the quiet user's tasks was trimmed once the new tasks were added
+ assertTrimmed(qt1, qt2);
+ }
+
+ @Test
+ public void testSessionDuration() throws Exception {
+ mRecentTasks.setOnlyTestVisibleRange();
+ mRecentTasks.setParameters(-1 /* min */, -1 /* max */, 50 /* ms */);
+
+ TaskRecord t1 = createTaskBuilder(".Task1").build();
+ t1.touchActiveTime();
+ mRecentTasks.add(t1);
+
+ // Force a small sleep just beyond the session duration
+ SystemClock.sleep(75);
+
+ TaskRecord t2 = createTaskBuilder(".Task2").build();
+ t2.touchActiveTime();
+ mRecentTasks.add(t2);
+
+ // Assert that the old task has been removed due to being out of the active session
+ assertTrimmed(t1);
+ }
+
+ @Test
+ public void testVisibleTasks_excludedFromRecents() throws Exception {
+ mRecentTasks.setOnlyTestVisibleRange();
+ mRecentTasks.setParameters(-1 /* min */, 4 /* max */, -1 /* ms */);
+
+ TaskRecord excludedTask1 = createTaskBuilder(".ExcludedTask1")
+ .setFlags(FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
+ .build();
+ TaskRecord excludedTask2 = createTaskBuilder(".ExcludedTask2")
+ .setFlags(FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
+ .build();
+
+ mRecentTasks.add(excludedTask1);
+ mRecentTasks.add(mTasks.get(0));
+ mRecentTasks.add(mTasks.get(1));
+ mRecentTasks.add(mTasks.get(2));
+ mRecentTasks.add(excludedTask2);
+
+ // The last excluded task should be trimmed, while the first-most excluded task should not
+ assertTrimmed(excludedTask1);
+ }
+
+ @Test
+ public void testVisibleTasks_minNum() throws Exception {
+ mRecentTasks.setOnlyTestVisibleRange();
+ mRecentTasks.setParameters(5 /* min */, -1 /* max */, 25 /* ms */);
+
+ for (int i = 0; i < 4; i++) {
+ final TaskRecord task = mTasks.get(i);
+ task.touchActiveTime();
+ mRecentTasks.add(task);
+ }
+
+ // Force a small sleep just beyond the session duration
+ SystemClock.sleep(50);
+
+ // Add a new task to trigger tasks to be trimmed
+ mRecentTasks.add(mTasks.get(4));
+
+ // Ensure that there are a minimum number of tasks regardless of session length
+ assertNoTasksTrimmed();
+ }
+
+ @Test
+ public void testVisibleTasks_maxNum() throws Exception {
+ mRecentTasks.setOnlyTestVisibleRange();
+ mRecentTasks.setParameters(-1 /* min */, 3 /* max */, -1 /* ms */);
+
+ for (int i = 0; i < 5; i++) {
+ final TaskRecord task = mTasks.get(i);
+ task.touchActiveTime();
+ mRecentTasks.add(task);
+ }
+
+ // Ensure that only the last number of max tasks are kept
+ assertTrimmed(mTasks.get(0), mTasks.get(1));
+ }
+
+ @Test
+ public void testBackStackTasks_expectNoTrim() throws Exception {
+ mRecentTasks.setParameters(-1 /* min */, 1 /* max */, -1 /* ms */);
+
+ final MyTestActivityStackSupervisor supervisor =
+ (MyTestActivityStackSupervisor) mService.mStackSupervisor;
+ final ActivityStack homeStack = new MyTestActivityStack(mDisplay, supervisor);
+ final ActivityStack aboveHomeStack = new MyTestActivityStack(mDisplay, supervisor);
+ supervisor.setHomeStack(homeStack);
+
+ // Add a number of tasks (beyond the max) but ensure that nothing is trimmed because all
+ // the tasks belong in stacks above the home stack
+ mRecentTasks.add(createTaskBuilder(".HomeTask1").setStack(homeStack).build());
+ mRecentTasks.add(createTaskBuilder(".Task1").setStack(aboveHomeStack).build());
+ mRecentTasks.add(createTaskBuilder(".Task2").setStack(aboveHomeStack).build());
+ mRecentTasks.add(createTaskBuilder(".Task3").setStack(aboveHomeStack).build());
+
+ assertNoTasksTrimmed();
+ }
+
+ @Test
+ public void testBehindHomeStackTasks_expectTaskTrimmed() throws Exception {
+ mRecentTasks.setParameters(-1 /* min */, 1 /* max */, -1 /* ms */);
+
+ final MyTestActivityStackSupervisor supervisor =
+ (MyTestActivityStackSupervisor) mService.mStackSupervisor;
+ final ActivityStack behindHomeStack = new MyTestActivityStack(mDisplay, supervisor);
+ final ActivityStack homeStack = new MyTestActivityStack(mDisplay, supervisor);
+ final ActivityStack aboveHomeStack = new MyTestActivityStack(mDisplay, supervisor);
+ supervisor.setHomeStack(homeStack);
+
+ // Add a number of tasks (beyond the max) but ensure that only the task in the stack behind
+ // the home stack is trimmed once a new task is added
+ final TaskRecord behindHomeTask = createTaskBuilder(".Task1")
+ .setStack(behindHomeStack)
+ .build();
+ mRecentTasks.add(behindHomeTask);
+ mRecentTasks.add(createTaskBuilder(".HomeTask1").setStack(homeStack).build());
+ mRecentTasks.add(createTaskBuilder(".Task2").setStack(aboveHomeStack).build());
+
+ assertTrimmed(behindHomeTask);
+ }
+
+ @Test
+ public void testOtherDisplayTasks_expectNoTrim() throws Exception {
+ mRecentTasks.setParameters(-1 /* min */, 1 /* max */, -1 /* ms */);
+
+ final MyTestActivityStackSupervisor supervisor =
+ (MyTestActivityStackSupervisor) mService.mStackSupervisor;
+ final ActivityStack homeStack = new MyTestActivityStack(mDisplay, supervisor);
+ final ActivityStack otherDisplayStack = new MyTestActivityStack(mOtherDisplay, supervisor);
+ supervisor.setHomeStack(homeStack);
+
+ // Add a number of tasks (beyond the max) on each display, ensure that the tasks are not
+ // removed
+ mRecentTasks.add(createTaskBuilder(".HomeTask1").setStack(homeStack).build());
+ mRecentTasks.add(createTaskBuilder(".Task1").setStack(otherDisplayStack).build());
+ mRecentTasks.add(createTaskBuilder(".Task2").setStack(otherDisplayStack).build());
+ mRecentTasks.add(createTaskBuilder(".HomeTask2").setStack(homeStack).build());
+
+ assertNoTasksTrimmed();
+ }
+
+ @Test
+ public void testRemovePackageByName() throws Exception {
+ // Add a number of tasks with the same package name
+ mRecentTasks.add(createTaskBuilder("com.android.pkg1", ".Task1").build());
+ mRecentTasks.add(createTaskBuilder("com.android.pkg2", ".Task2").build());
+ mRecentTasks.add(createTaskBuilder("com.android.pkg3", ".Task3").build());
+ mRecentTasks.add(createTaskBuilder("com.android.pkg1", ".Task4").build());
+ mRecentTasks.removeTasksByPackageName("com.android.pkg1", TEST_USER_0_ID);
+
+ final ArrayList<TaskRecord> tasks = mRecentTasks.getRawTasks();
+ for (int i = 0; i < tasks.size(); i++) {
+ if (tasks.get(i).intent.getComponent().getPackageName().equals("com.android.pkg1")) {
+ fail("Expected com.android.pkg1 tasks to be removed");
+ }
+ }
+ }
+
+ @Test
+ public void testNotRecentsComponent_denyApiAccess() throws Exception {
+ doReturn(PackageManager.PERMISSION_DENIED).when(mService).checkPermission(anyString(),
+ anyInt(), anyInt());
+
+ // Expect the following methods to fail due to recents component not being set
+ mRecentTasks.setIsCallerRecentsOverride(TestRecentTasks.DENY_THROW_SECURITY_EXCEPTION);
+ testRecentTasksApis(false /* expectNoSecurityException */);
+ // Don't throw for the following tests
+ mRecentTasks.setIsCallerRecentsOverride(TestRecentTasks.DENY);
+ testGetTasksApis(false /* expectNoSecurityException */);
+ }
+
+ @Test
+ public void testRecentsComponent_allowApiAccessWithoutPermissions() throws Exception {
+ doReturn(PackageManager.PERMISSION_DENIED).when(mService).checkPermission(anyString(),
+ anyInt(), anyInt());
+
+ // Set the recents component and ensure that the following calls do not fail
+ mRecentTasks.setIsCallerRecentsOverride(TestRecentTasks.GRANT);
+ testRecentTasksApis(true /* expectNoSecurityException */);
+ testGetTasksApis(true /* expectNoSecurityException */);
+ }
+
+ private void testRecentTasksApis(boolean expectCallable) {
+ assertSecurityException(expectCallable, () -> mService.removeStack(INVALID_STACK_ID));
+ assertSecurityException(expectCallable,
+ () -> mService.removeStacksInWindowingModes(new int[] {WINDOWING_MODE_UNDEFINED}));
+ assertSecurityException(expectCallable,
+ () -> mService.removeStacksWithActivityTypes(new int[] {ACTIVITY_TYPE_UNDEFINED}));
+ assertSecurityException(expectCallable, () -> mService.removeTask(0));
+ assertSecurityException(expectCallable,
+ () -> mService.setTaskWindowingMode(0, WINDOWING_MODE_UNDEFINED, true));
+ assertSecurityException(expectCallable,
+ () -> mService.moveTaskToStack(0, INVALID_STACK_ID, true));
+ assertSecurityException(expectCallable,
+ () -> mService.setTaskWindowingModeSplitScreenPrimary(0,
+ SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT, true, true, new Rect(), true));
+ assertSecurityException(expectCallable, () -> mService.dismissSplitScreenMode(true));
+ assertSecurityException(expectCallable, () -> mService.dismissPip(true, 0));
+ assertSecurityException(expectCallable,
+ () -> mService.moveTopActivityToPinnedStack(INVALID_STACK_ID, new Rect()));
+ assertSecurityException(expectCallable,
+ () -> mService.resizeStack(INVALID_STACK_ID, new Rect(), true, true, true, 0));
+ assertSecurityException(expectCallable,
+ () -> mService.resizeDockedStack(new Rect(), new Rect(), new Rect(), new Rect(),
+ new Rect()));
+ assertSecurityException(expectCallable,
+ () -> mService.resizePinnedStack(new Rect(), new Rect()));
+ assertSecurityException(expectCallable, () -> mService.getAllStackInfos());
+ assertSecurityException(expectCallable,
+ () -> mService.getStackInfo(WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_UNDEFINED));
+ assertSecurityException(expectCallable, () -> {
+ try {
+ mService.getFocusedStackInfo();
+ } catch (RemoteException e) {
+ // Ignore
+ }
+ });
+ assertSecurityException(expectCallable,
+ () -> mService.moveTasksToFullscreenStack(INVALID_STACK_ID, true));
+ assertSecurityException(expectCallable,
+ () -> mService.startActivityFromRecents(0, new Bundle()));
+ assertSecurityException(expectCallable,
+ () -> mService.getTaskSnapshot(0, true));
+ assertSecurityException(expectCallable, () -> {
+ try {
+ mService.registerTaskStackListener(null);
+ } catch (RemoteException e) {
+ // Ignore
+ }
+ });
+ assertSecurityException(expectCallable, () -> {
+ try {
+ mService.unregisterTaskStackListener(null);
+ } catch (RemoteException e) {
+ // Ignore
+ }
+ });
+ assertSecurityException(expectCallable, () -> mService.getTaskDescription(0));
+ assertSecurityException(expectCallable, () -> mService.cancelTaskWindowTransition(0));
+ assertSecurityException(expectCallable, () -> mService.startRecentsActivity(null, null,
+ null));
+ assertSecurityException(expectCallable, () -> mService.cancelRecentsAnimation(true));
+ assertSecurityException(expectCallable, () -> mService.stopAppSwitches());
+ assertSecurityException(expectCallable, () -> mService.resumeAppSwitches());
+ }
+
+ private void testGetTasksApis(boolean expectCallable) {
+ mService.getRecentTasks(MAX_VALUE, 0, TEST_USER_0_ID);
+ mService.getTasks(MAX_VALUE);
+ if (expectCallable) {
+ assertTrue(mRecentTasks.lastAllowed);
+ assertTrue(mRunningTasks.lastAllowed);
+ } else {
+ assertFalse(mRecentTasks.lastAllowed);
+ assertFalse(mRunningTasks.lastAllowed);
+ }
+ }
+
+ private TaskBuilder createTaskBuilder(String className) {
+ return createTaskBuilder(mContext.getPackageName(), className);
+ }
+
+ private TaskBuilder createTaskBuilder(String packageName, String className) {
+ return new TaskBuilder(mService.mStackSupervisor)
+ .setComponent(new ComponentName(packageName, className))
+ .setStack(mStack)
+ .setTaskId(LAST_TASK_ID++)
+ .setUserId(TEST_USER_0_ID);
+ }
+
+ private TaskRecord createDocumentTask(String className) {
+ return createDocumentTask(className, 0);
+ }
+
+ private TaskRecord createDocumentTask(String className, int flags) {
+ TaskRecord task = createTaskBuilder(className)
+ .setFlags(FLAG_ACTIVITY_NEW_DOCUMENT | flags)
+ .build();
+ task.affinity = null;
+ task.maxRecents = ActivityManager.getMaxAppRecentsLimitStatic();
+ return task;
+ }
+
+ private boolean arrayContainsUser(int[] userIds, int targetUserId) {
+ Arrays.sort(userIds);
+ return Arrays.binarySearch(userIds, targetUserId) >= 0;
+ }
+
+ private void assertNoTasksTrimmed() {
+ assertTrimmed();
+ }
+
+ private void assertTrimmed(TaskRecord... tasks) {
+ final ArrayList<TaskRecord> trimmed = mCallbacksRecorder.trimmed;
+ final ArrayList<TaskRecord> removed = mCallbacksRecorder.removed;
+ assertTrue("Expected " + tasks.length + " trimmed tasks, got " + trimmed.size(),
+ trimmed.size() == tasks.length);
+ assertTrue("Expected " + tasks.length + " removed tasks, got " + removed.size(),
+ removed.size() == tasks.length);
+ for (TaskRecord task : tasks) {
+ assertTrue("Expected trimmed task: " + task, trimmed.contains(task));
+ assertTrue("Expected removed task: " + task, removed.contains(task));
+ }
+ }
+
+ private void assertSecurityException(boolean expectCallable, Runnable runnable) {
+ boolean noSecurityException = true;
+ try {
+ runnable.run();
+ } catch (SecurityException se) {
+ noSecurityException = false;
+ } catch (Exception e) {
+ // We only care about SecurityExceptions, fall through here
+ e.printStackTrace();
+ }
+ if (noSecurityException != expectCallable) {
+ fail("Expected callable: " + expectCallable + " but got no security exception: "
+ + noSecurityException);
+ }
+ }
+
+ private class MyTestActivityManagerService extends TestActivityManagerService {
+ MyTestActivityManagerService(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected ActivityStackSupervisor createTestSupervisor() {
+ return new MyTestActivityStackSupervisor(this, mHandlerThread.getLooper());
+ }
+
+ @Override
+ protected RecentTasks createRecentTasks() {
+ return new TestRecentTasks(this, mTaskPersister, new TestUserController(this));
+ }
+
+ @Override
+ public boolean isUserRunning(int userId, int flags) {
+ return true;
+ }
+ }
+
+ private class MyTestActivityStackSupervisor extends TestActivityStackSupervisor {
+ public MyTestActivityStackSupervisor(ActivityManagerService service, Looper looper) {
+ super(service, looper);
+ }
+
+ @Override
+ public void initialize() {
+ super.initialize();
+ mDisplay = new TestActivityDisplay(this, DEFAULT_DISPLAY);
+ mOtherDisplay = new TestActivityDisplay(this, DEFAULT_DISPLAY);
+ attachDisplay(mOtherDisplay);
+ attachDisplay(mDisplay);
+ }
+
+ @Override
+ RunningTasks createRunningTasks() {
+ mRunningTasks = new TestRunningTasks();
+ return mRunningTasks;
+ }
+
+ void setHomeStack(ActivityStack stack) {
+ mHomeStack = stack;
+ }
+ }
+
+ private class MyTestActivityStack extends TestActivityStack {
+ private ActivityDisplay mDisplay = null;
+
+ MyTestActivityStack(ActivityDisplay display, ActivityStackSupervisor supervisor) {
+ super(display, LAST_STACK_ID++, supervisor, WINDOWING_MODE_FULLSCREEN,
+ ACTIVITY_TYPE_STANDARD, true);
+ mDisplay = display;
+ }
+
+ @Override
+ ActivityDisplay getDisplay() {
+ if (mDisplay != null) {
+ return mDisplay;
+ }
+ return super.getDisplay();
+ }
+ }
+
+ private static class CallbacksRecorder implements Callbacks {
+ ArrayList<TaskRecord> added = new ArrayList<>();
+ ArrayList<TaskRecord> trimmed = new ArrayList<>();
+ ArrayList<TaskRecord> removed = new ArrayList<>();
+
+ void clear() {
+ added.clear();
+ trimmed.clear();
+ removed.clear();
+ }
+
+ @Override
+ public void onRecentTaskAdded(TaskRecord task) {
+ added.add(task);
+ }
+
+ @Override
+ public void onRecentTaskRemoved(TaskRecord task, boolean wasTrimmed) {
+ if (wasTrimmed) {
+ trimmed.add(task);
+ }
+ removed.add(task);
+ }
+ }
+
+ private static class TestTaskPersister extends TaskPersister {
+ SparseBooleanArray userTaskIdsOverride;
+ ArrayList<TaskRecord> userTasksOverride;
+
+ TestTaskPersister(File workingDir) {
+ super(workingDir);
+ }
+
+ @Override
+ SparseBooleanArray loadPersistedTaskIdsForUser(int userId) {
+ if (userTaskIdsOverride != null) {
+ return userTaskIdsOverride;
+ }
+ return super.loadPersistedTaskIdsForUser(userId);
+ }
+
+ @Override
+ List<TaskRecord> restoreTasksForUserLocked(int userId, SparseBooleanArray preaddedTasks) {
+ if (userTasksOverride != null) {
+ return userTasksOverride;
+ }
+ return super.restoreTasksForUserLocked(userId, preaddedTasks);
+ }
+ }
+
+ private static class TestRecentTasks extends RecentTasks {
+ static final int GRANT = 0;
+ static final int DENY = 1;
+ static final int DENY_THROW_SECURITY_EXCEPTION = 2;
+
+ private boolean mOverrideIsCallerRecents;
+ private boolean mIsTrimmableOverride;
+ private int mIsCallerRecentsPolicy;
+
+ boolean lastAllowed;
+
+ TestRecentTasks(ActivityManagerService service, TaskPersister taskPersister,
+ UserController userController) {
+ super(service, taskPersister, userController);
+ }
+
+ @Override
+ boolean isCallerRecents(int callingUid) {
+ if (mOverrideIsCallerRecents) {
+ switch (mIsCallerRecentsPolicy) {
+ case GRANT:
+ return true;
+ case DENY:
+ return false;
+ case DENY_THROW_SECURITY_EXCEPTION:
+ throw new SecurityException();
+ }
+ }
+ return super.isCallerRecents(callingUid);
+ }
+
+ void setIsCallerRecentsOverride(int policy) {
+ mOverrideIsCallerRecents = true;
+ mIsCallerRecentsPolicy = policy;
+ }
+
+ /**
+ * To simplify the setup for some tests, the caller can request that we only rely on the
+ * visible range test to determine what is trimmable. In this case, we don't try to
+ * use the stack order to determine additionally if the task is trimmable when it is not
+ * in the visible range.
+ */
+ void setOnlyTestVisibleRange() {
+ mIsTrimmableOverride = true;
+ }
+
+ @Override
+ ParceledListSlice<RecentTaskInfo> getRecentTasks(int maxNum, int flags,
+ boolean getTasksAllowed,
+ boolean getDetailedTasks, int userId, int callingUid) {
+ lastAllowed = getTasksAllowed;
+ return super.getRecentTasks(maxNum, flags, getTasksAllowed, getDetailedTasks, userId,
+ callingUid);
+ }
+
+ @Override
+ protected boolean isTrimmable(TaskRecord task) {
+ return mIsTrimmableOverride || super.isTrimmable(task);
+ }
+ }
+
+ private static class TestRunningTasks extends RunningTasks {
+ boolean lastAllowed;
+
+ @Override
+ void getTasks(int maxNum, List<RunningTaskInfo> list, int ignoreActivityType,
+ int ignoreWindowingMode, SparseArray<ActivityDisplay> activityDisplays,
+ int callingUid, boolean allowed) {
+ lastAllowed = allowed;
+ super.getTasks(maxNum, list, ignoreActivityType, ignoreWindowingMode, activityDisplays,
+ callingUid, allowed);
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/RecentsAnimationTest.java b/services/tests/servicestests/src/com/android/server/am/RecentsAnimationTest.java
new file mode 100644
index 0000000..eefd973
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/RecentsAnimationTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.am;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static com.android.server.wm.RecentsAnimationController.REORDER_KEEP_IN_PLACE;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.IRecentsAnimationRunner;
+import com.android.server.AttributeCache;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * atest FrameworksServicesTests:RecentsAnimationTest
+ */
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class RecentsAnimationTest extends ActivityTestsBase {
+ private static final int TEST_CALLING_PID = 3;
+
+ private Context mContext = InstrumentationRegistry.getContext();
+ private ActivityManagerService mService;
+ private ComponentName mRecentsComponent;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mRecentsComponent = new ComponentName(mContext.getPackageName(), "RecentsActivity");
+ mService = setupActivityManagerService(new MyTestActivityManagerService(mContext));
+ AttributeCache.init(mContext);
+ }
+
+ @Test
+ public void testCancelAnimationOnStackOrderChange() throws Exception {
+ ActivityStack fullscreenStack = mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ ActivityStack recentsStack = mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_RECENTS, true /* onTop */);
+ ActivityRecord recentsActivity = new ActivityBuilder(mService)
+ .setComponent(mRecentsComponent)
+ .setCreateTask(true)
+ .setStack(recentsStack)
+ .build();
+ ActivityStack fullscreenStack2 = mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ ActivityRecord fsActivity = new ActivityBuilder(mService)
+ .setComponent(new ComponentName(mContext.getPackageName(), "App1"))
+ .setCreateTask(true)
+ .setStack(fullscreenStack2)
+ .build();
+ doReturn(true).when(mService.mWindowManager).canStartRecentsAnimation();
+
+ // Start the recents animation
+ Intent recentsIntent = new Intent();
+ recentsIntent.setComponent(mRecentsComponent);
+ mService.startRecentsActivity(recentsIntent, null, mock(IRecentsAnimationRunner.class));
+
+ fullscreenStack.moveToFront("Activity start");
+
+ // Ensure that the recents animation was canceled
+ verify(mService.mWindowManager, times(1)).cancelRecentsAnimationSynchronously(
+ eq(REORDER_KEEP_IN_PLACE), any());
+ }
+
+ private class MyTestActivityManagerService extends TestActivityManagerService {
+ MyTestActivityManagerService(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected RecentTasks createRecentTasks() {
+ RecentTasks recents = mock(RecentTasks.class);
+ doReturn(mRecentsComponent).when(recents).getRecentsComponent();
+ System.out.println(mRecentsComponent);
+ return recents;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/RunningTasksTest.java b/services/tests/servicestests/src/com/android/server/am/RunningTasksTest.java
new file mode 100644
index 0000000..c6ce7e1
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/RunningTasksTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.am;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.server.am.ActivityDisplay.POSITION_BOTTOM;
+
+import static org.junit.Assert.assertTrue;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.SparseArray;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+/**
+ * runtest --path frameworks/base/services/tests/servicestests/src/com/android/server/am/RunningTasksTest.java
+ */
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class RunningTasksTest extends ActivityTestsBase {
+
+ private Context mContext = InstrumentationRegistry.getContext();
+ private ActivityManagerService mService;
+
+ private RunningTasks mRunningTasks;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mService = createActivityManagerService();
+ mRunningTasks = new RunningTasks();
+ }
+
+ @Test
+ public void testCollectTasksByLastActiveTime() throws Exception {
+ // Create a number of stacks with tasks (of incrementing active time)
+ final ActivityStackSupervisor supervisor = mService.mStackSupervisor;
+ final SparseArray<ActivityDisplay> displays = new SparseArray<>();
+ final ActivityDisplay display = new TestActivityDisplay(supervisor, DEFAULT_DISPLAY);
+ displays.put(DEFAULT_DISPLAY, display);
+
+ final int numStacks = 2;
+ for (int stackIndex = 0; stackIndex < numStacks; stackIndex++) {
+ final ActivityStack stack = new TestActivityStack(display, stackIndex, supervisor,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true);
+ display.addChild(stack, POSITION_BOTTOM);
+ }
+
+ final int numTasks = 10;
+ int activeTime = 0;
+ for (int i = 0; i < numTasks; i++) {
+ createTask(display.getChildAt(i % numStacks), ".Task" + i, i, activeTime++);
+ }
+
+ // Ensure that the latest tasks were returned in order of decreasing last active time,
+ // collected from all tasks across all the stacks
+ final int numFetchTasks = 5;
+ ArrayList<RunningTaskInfo> tasks = new ArrayList<>();
+ mRunningTasks.getTasks(5, tasks, ACTIVITY_TYPE_UNDEFINED, WINDOWING_MODE_UNDEFINED,
+ displays, -1 /* callingUid */, true /* allowed */);
+ assertTrue(tasks.size() == numFetchTasks);
+ for (int i = 0; i < numFetchTasks; i++) {
+ assertTrue(tasks.get(i).id == (numTasks - i - 1));
+ }
+
+ // Ensure that requesting more than the total number of tasks only returns the subset
+ // and does not crash
+ tasks.clear();
+ mRunningTasks.getTasks(100, tasks, ACTIVITY_TYPE_UNDEFINED, WINDOWING_MODE_UNDEFINED,
+ displays, -1 /* callingUid */, true /* allowed */);
+ assertTrue(tasks.size() == numTasks);
+ for (int i = 0; i < numTasks; i++) {
+ assertTrue(tasks.get(i).id == (numTasks - i - 1));
+ }
+ }
+
+ /**
+ * Create a task with a single activity in it, with the given last active time.
+ */
+ private TaskRecord createTask(ActivityStack stack, String className, int taskId,
+ int lastActiveTime) {
+ final TaskRecord task = new TaskBuilder(mService.mStackSupervisor)
+ .setComponent(new ComponentName(mContext.getPackageName(), className))
+ .setTaskId(taskId)
+ .setStack(stack)
+ .build();
+ task.lastActiveTime = lastActiveTime;
+ final ActivityRecord activity = new ActivityBuilder(mService)
+ .setTask(task)
+ .setComponent(new ComponentName(mContext.getPackageName(), ".TaskActivity"))
+ .build();
+ return task;
+ }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/am/SafeActivityOptionsTest.java b/services/tests/servicestests/src/com/android/server/am/SafeActivityOptionsTest.java
new file mode 100644
index 0000000..168bc17
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/SafeActivityOptionsTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.am;
+
+import static org.junit.Assert.assertEquals;
+
+import android.app.ActivityOptions;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@Presubmit
+@FlakyTest
+@RunWith(AndroidJUnit4.class)
+public class SafeActivityOptionsTest {
+
+ @Test
+ public void testMerge() {
+ final ActivityOptions opts1 = ActivityOptions.makeBasic();
+ opts1.setLaunchDisplayId(5);
+ final ActivityOptions opts2 = ActivityOptions.makeBasic();
+ opts2.setLaunchDisplayId(6);
+ final SafeActivityOptions options = new SafeActivityOptions(opts1);
+ final ActivityOptions result = options.mergeActivityOptions(opts1, opts2);
+ assertEquals(6, result.getLaunchDisplayId());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/TaskLaunchParamsModifierTests.java b/services/tests/servicestests/src/com/android/server/am/TaskLaunchParamsModifierTests.java
new file mode 100644
index 0000000..3d323f0
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/TaskLaunchParamsModifierTests.java
@@ -0,0 +1,255 @@
+/*
+ * 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.am;
+
+import android.content.pm.ActivityInfo.WindowLayout;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import android.view.Gravity;
+
+import org.junit.runner.RunWith;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.mockito.invocation.InvocationOnMock;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+
+import static com.android.server.am.LaunchParamsController.LaunchParamsModifier.RESULT_CONTINUE;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.doAnswer;
+import static org.junit.Assert.assertEquals;
+
+
+/**
+ * Tests for exercising resizing task bounds.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:TaskLaunchParamsModifierTests
+ */
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class TaskLaunchParamsModifierTests extends ActivityTestsBase {
+ private final static int STACK_WIDTH = 100;
+ private final static int STACK_HEIGHT = 200;
+
+ private final static Rect STACK_BOUNDS = new Rect(0, 0, STACK_WIDTH, STACK_HEIGHT);
+
+ private ActivityManagerService mService;
+ private ActivityStack mStack;
+ private TaskRecord mTask;
+
+ private TaskLaunchParamsModifier mPositioner;
+
+ private LaunchParamsController.LaunchParams mCurrent;
+ private LaunchParamsController.LaunchParams mResult;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mService = createActivityManagerService();
+ mStack = mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_FREEFORM, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ mStack.requestResize(STACK_BOUNDS);
+
+ // We must create the task after resizing to make sure it does not inherit the stack
+ // dimensions on resize.
+ mTask = new TaskBuilder(mService.mStackSupervisor).setStack(mStack).build();
+
+ mPositioner = new TaskLaunchParamsModifier();
+
+ mResult = new LaunchParamsController.LaunchParams();
+ mCurrent = new LaunchParamsController.LaunchParams();
+ }
+
+ /**
+ * Ensures that the setup bounds are set as expected with the stack bounds set and the task
+ * bounds still {@code null}.
+ * @throws Exception
+ */
+ @Test
+ public void testInitialBounds() throws Exception {
+ assertEquals(mStack.getOverrideBounds(), STACK_BOUNDS);
+ assertEquals(mTask.getOverrideBounds(), new Rect());
+ }
+
+ /**
+ * Ensures that a task positioned with no {@link WindowLayout} receives the default launch
+ * position.
+ * @throws Exception
+ */
+ @Test
+ public void testLaunchNoWindowLayout() throws Exception {
+ assertEquals(RESULT_CONTINUE, mPositioner.onCalculate(mTask, null /*layout*/,
+ null /*record*/, null /*source*/, null /*options*/, mCurrent, mResult));
+ assertEquals(getDefaultBounds(Gravity.NO_GRAVITY), mResult.mBounds);
+ }
+
+ /**
+ * Ensures that a task positioned with an empty {@link WindowLayout} receives the default launch
+ * position.
+ * @throws Exception
+ */
+ @Test
+ public void testlaunchEmptyWindowLayout() throws Exception {
+ assertEquals(RESULT_CONTINUE, mPositioner.onCalculate(mTask,
+ new WindowLayout(0, 0, 0, 0, Gravity.NO_GRAVITY, 0, 0), null /*activity*/,
+ null /*source*/, null /*options*/, mCurrent, mResult));
+ assertEquals(mResult.mBounds, getDefaultBounds(Gravity.NO_GRAVITY));
+ }
+
+ /**
+ * Ensures that a task positioned with a {@link WindowLayout} gravity specified is positioned
+ * according to specification.
+ * @throws Exception
+ */
+ @Test
+ public void testlaunchWindowLayoutGravity() throws Exception {
+ // Unspecified gravity should be ignored
+ testGravity(Gravity.NO_GRAVITY);
+
+ // Unsupported gravity should be ignored
+ testGravity(Gravity.LEFT);
+ testGravity(Gravity.RIGHT);
+
+ // Test defaults for vertical gravities
+ testGravity(Gravity.TOP);
+ testGravity(Gravity.BOTTOM);
+
+ // Test corners
+ testGravity(Gravity.TOP | Gravity.LEFT);
+ testGravity(Gravity.TOP | Gravity.RIGHT);
+ testGravity(Gravity.BOTTOM | Gravity.LEFT);
+ testGravity(Gravity.BOTTOM | Gravity.RIGHT);
+ }
+
+ private void testGravity(int gravity) {
+ try {
+ assertEquals(RESULT_CONTINUE, mPositioner.onCalculate(mTask,
+ new WindowLayout(0, 0, 0, 0, gravity, 0, 0), null /*activity*/,
+ null /*source*/, null /*options*/, mCurrent, mResult));
+ assertEquals(mResult.mBounds, getDefaultBounds(gravity));
+ } finally {
+ mCurrent.reset();
+ mResult.reset();
+ }
+ }
+
+ /**
+ * Ensures that a task which causes a conflict with another task when positioned is adjusted as
+ * expected.
+ * @throws Exception
+ */
+ @Test
+ public void testLaunchWindowCenterConflict() throws Exception {
+ testConflict(Gravity.NO_GRAVITY);
+ testConflict(Gravity.TOP);
+ testConflict(Gravity.BOTTOM);
+ testConflict(Gravity.TOP | Gravity.LEFT);
+ testConflict(Gravity.TOP | Gravity.RIGHT);
+ testConflict(Gravity.BOTTOM | Gravity.LEFT);
+ testConflict(Gravity.BOTTOM | Gravity.RIGHT);
+ }
+
+ private void testConflict(int gravity) {
+ final WindowLayout layout = new WindowLayout(0, 0, 0, 0, gravity, 0, 0);
+
+ // layout first task
+ mService.mStackSupervisor.getLaunchParamsController().layoutTask(mTask, layout);
+
+ // Second task will be laid out on top of the first so starting bounds is the same.
+ final Rect expectedBounds = new Rect(mTask.getOverrideBounds());
+
+ ActivityRecord activity = null;
+ TaskRecord secondTask = null;
+
+ // wrap with try/finally to ensure cleanup of activity/stack.
+ try {
+ // empty tasks are ignored in conflicts
+ activity = new ActivityBuilder(mService).setTask(mTask).build();
+
+ // Create secondary task
+ secondTask = new TaskBuilder(mService.mStackSupervisor).setStack(mStack).build();
+
+ // layout second task
+ assertEquals(RESULT_CONTINUE,
+ mPositioner.onCalculate(secondTask, layout, null /*activity*/,
+ null /*source*/, null /*options*/, mCurrent, mResult));
+
+ if ((gravity & (Gravity.TOP | Gravity.RIGHT)) == (Gravity.TOP | Gravity.RIGHT)
+ || (gravity & (Gravity.BOTTOM | Gravity.RIGHT))
+ == (Gravity.BOTTOM | Gravity.RIGHT)) {
+ expectedBounds.offset(-TaskLaunchParamsModifier.getHorizontalStep(
+ mStack.getOverrideBounds()), 0);
+ } else if ((gravity & Gravity.TOP) == Gravity.TOP
+ || (gravity & Gravity.BOTTOM) == Gravity.BOTTOM) {
+ expectedBounds.offset(
+ TaskLaunchParamsModifier.getHorizontalStep(mStack.getOverrideBounds()), 0);
+ } else {
+ expectedBounds.offset(
+ TaskLaunchParamsModifier.getHorizontalStep(mStack.getOverrideBounds()),
+ TaskLaunchParamsModifier.getVerticalStep(mStack.getOverrideBounds()));
+ }
+
+ assertEquals(mResult.mBounds, expectedBounds);
+ } finally {
+ // Remove task and activity to prevent influencing future tests
+ if (activity != null) {
+ mTask.removeActivity(activity);
+ }
+
+ if (secondTask != null) {
+ mStack.removeTask(secondTask, "cleanup", ActivityStack.REMOVE_TASK_MODE_DESTROYING);
+ }
+ }
+ }
+
+ private Rect getDefaultBounds(int gravity) {
+ final Rect bounds = new Rect();
+ bounds.set(mStack.getOverrideBounds());
+
+ final int verticalInset =
+ TaskLaunchParamsModifier.getFreeformStartTop(mStack.getOverrideBounds());
+ final int horizontalInset =
+ TaskLaunchParamsModifier.getFreeformStartLeft(mStack.getOverrideBounds());
+
+ bounds.inset(horizontalInset, verticalInset);
+
+ if ((gravity & (Gravity.TOP | Gravity.RIGHT)) == (Gravity.TOP | Gravity.RIGHT)) {
+ bounds.offsetTo(horizontalInset * 2, 0);
+ } else if ((gravity & Gravity.TOP) == Gravity.TOP) {
+ bounds.offsetTo(0, 0);
+ } else if ((gravity & (Gravity.BOTTOM | Gravity.RIGHT))
+ == (Gravity.BOTTOM | Gravity.RIGHT)) {
+ bounds.offsetTo(horizontalInset * 2, verticalInset * 2);
+ } else if ((gravity & Gravity.BOTTOM) == Gravity.BOTTOM) {
+ bounds.offsetTo(0, verticalInset * 2);
+ }
+
+ return bounds;
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/TaskPersisterTest.java b/services/tests/servicestests/src/com/android/server/am/TaskPersisterTest.java
new file mode 100644
index 0000000..9e6055d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/TaskPersisterTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import android.content.pm.UserInfo;
+import android.os.Environment;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.test.AndroidTestCase;
+import android.util.Log;
+import android.util.SparseBooleanArray;
+
+import com.android.server.am.TaskPersister;
+
+import java.io.File;
+import java.util.Random;
+
+/**
+ * atest FrameworksServicesTests:TaskPersisterTest
+ */
+public class TaskPersisterTest extends AndroidTestCase {
+ private static final String TEST_USER_NAME = "AM-Test-User";
+
+ private TaskPersister mTaskPersister;
+ private int testUserId;
+ private UserManager mUserManager;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mUserManager = UserManager.get(getContext());
+ mTaskPersister = new TaskPersister(getContext().getFilesDir());
+ testUserId = createUser(TEST_USER_NAME, 0);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ mTaskPersister.unloadUserDataFromMemory(testUserId);
+ removeUser(testUserId);
+ }
+
+ private int getRandomTaskIdForUser(int userId) {
+ int taskId = (int) (Math.random() * UserHandle.PER_USER_RANGE);
+ taskId += UserHandle.PER_USER_RANGE * userId;
+ return taskId;
+ }
+
+ public void testTaskIdsPersistence() {
+ SparseBooleanArray taskIdsOnFile = new SparseBooleanArray();
+ for (int i = 0; i < 100; i++) {
+ taskIdsOnFile.put(getRandomTaskIdForUser(testUserId), true);
+ }
+ mTaskPersister.writePersistedTaskIdsForUser(taskIdsOnFile, testUserId);
+ SparseBooleanArray newTaskIdsOnFile = mTaskPersister
+ .loadPersistedTaskIdsForUser(testUserId);
+ assertTrue("TaskIds written differ from TaskIds read back from file",
+ taskIdsOnFile.equals(newTaskIdsOnFile));
+ }
+
+ private int createUser(String name, int flags) {
+ UserInfo user = mUserManager.createUser(name, flags);
+ if (user == null) {
+ fail("Error while creating the test user: " + TEST_USER_NAME);
+ }
+ return user.id;
+ }
+
+ private void removeUser(int userId) {
+ if (!mUserManager.removeUser(userId)) {
+ fail("Error while removing the test user: " + TEST_USER_NAME);
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/am/TaskRecordTests.java b/services/tests/servicestests/src/com/android/server/am/TaskRecordTests.java
new file mode 100644
index 0000000..057fdc8
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/TaskRecordTests.java
@@ -0,0 +1,207 @@
+/*
+ * 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.am;
+
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.res.XmlResourceParser;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.service.voice.IVoiceInteractionSession;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Xml;
+
+import com.android.frameworks.servicestests.R;
+import com.android.internal.app.IVoiceInteractor;
+import com.android.server.am.TaskRecord.TaskRecordFactory;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Comparator;
+
+/**
+ * Tests for exercising {@link TaskRecord}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.am.TaskRecordTests
+ */
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class TaskRecordTests extends ActivityTestsBase {
+
+ private static final String TASK_TAG = "task";
+
+ private ActivityManagerService mService;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ TaskRecord.setTaskRecordFactory(null);
+ mService = createActivityManagerService();
+ }
+
+ @Test
+ public void testRestoreWindowedTask() throws Exception {
+ final TaskRecord expected = createTaskRecord(64);
+ expected.mLastNonFullscreenBounds = new Rect(50, 50, 100, 100);
+
+ final File serializedFile = serializeToFile(expected);
+
+ try {
+ final TaskRecord actual = restoreFromFile(serializedFile);
+ assertEquals(expected.taskId, actual.taskId);
+ assertEquals(expected.mLastNonFullscreenBounds, actual.mLastNonFullscreenBounds);
+ } finally {
+ serializedFile.delete();
+ }
+ }
+
+ @Test
+ public void testDefaultTaskFactoryNotNull() throws Exception {
+ assertNotNull(TaskRecord.getTaskRecordFactory());
+ }
+
+ @Test
+ public void testCreateTestRecordUsingCustomizedFactory() throws Exception {
+ TestTaskRecordFactory factory = new TestTaskRecordFactory();
+ TaskRecord.setTaskRecordFactory(factory);
+
+ assertFalse(factory.mCreated);
+
+ TaskRecord.create(null, 0, null, null, null, null);
+
+ assertTrue(factory.mCreated);
+ }
+
+ @Test
+ public void testReturnsToHomeStack() throws Exception {
+ final TaskRecord task = createTaskRecord(1);
+ assertFalse(task.returnsToHomeStack());
+ task.intent = null;
+ assertFalse(task.returnsToHomeStack());
+ task.intent = new Intent();
+ assertFalse(task.returnsToHomeStack());
+ task.intent.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_TASK_ON_HOME);
+ assertTrue(task.returnsToHomeStack());
+ }
+
+ private File serializeToFile(TaskRecord r) throws IOException, XmlPullParserException {
+ final File tmpFile = File.createTempFile(r.taskId + "_task_", "xml");
+
+ try (final OutputStream os = new FileOutputStream(tmpFile)) {
+ final XmlSerializer serializer = Xml.newSerializer();
+ serializer.setOutput(os, "UTF-8");
+ serializer.startDocument(null, true);
+ serializer.startTag(null, TASK_TAG);
+ r.saveToXml(serializer);
+ serializer.endTag(null, TASK_TAG);
+ serializer.endDocument();
+ }
+
+ return tmpFile;
+ }
+
+ private TaskRecord restoreFromFile(File file) throws IOException, XmlPullParserException {
+ try (final Reader reader = new BufferedReader(new FileReader(file))) {
+ final XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(reader);
+ assertEquals(XmlPullParser.START_TAG, parser.next());
+ assertEquals(TASK_TAG, parser.getName());
+ return TaskRecord.restoreFromXml(parser, mService.mStackSupervisor);
+ }
+ }
+
+ private TaskRecord createTaskRecord(int taskId) {
+ return new TaskRecord(mService, taskId, new Intent(), null, null, null, null, null, false,
+ false, false, 0, 10050, null, new ArrayList<>(), 0, false, null, 0, 0, 0, 0, 0,
+ null, 0, false, false, false, 0, 0);
+ }
+
+ private static class TestTaskRecordFactory extends TaskRecordFactory {
+ private boolean mCreated = false;
+
+ @Override
+ TaskRecord create(ActivityManagerService service, int taskId, ActivityInfo info,
+ Intent intent,
+ IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor) {
+ mCreated = true;
+ return null;
+ }
+
+ @Override
+ TaskRecord create(ActivityManagerService service, int taskId, ActivityInfo info,
+ Intent intent,
+ ActivityManager.TaskDescription taskDescription) {
+ mCreated = true;
+ return null;
+ }
+
+ @Override
+ TaskRecord create(ActivityManagerService service, int taskId, Intent intent,
+ Intent affinityIntent, String affinity, String rootAffinity,
+ ComponentName realActivity,
+ ComponentName origActivity, boolean rootWasReset, boolean autoRemoveRecents,
+ boolean askedCompatMode, int userId, int effectiveUid, String lastDescription,
+ ArrayList<ActivityRecord> activities, long lastTimeMoved,
+ boolean neverRelinquishIdentity,
+ ActivityManager.TaskDescription lastTaskDescription,
+ int taskAffiliation, int prevTaskId, int nextTaskId, int taskAffiliationColor,
+ int callingUid, String callingPackage, int resizeMode,
+ boolean supportsPictureInPicture,
+ boolean realActivitySuspended, boolean userSetupComplete, int minWidth,
+ int minHeight) {
+ mCreated = true;
+ return null;
+ }
+
+ @Override
+ TaskRecord restoreFromXml(XmlPullParser in, ActivityStackSupervisor stackSupervisor)
+ throws IOException, XmlPullParserException {
+ mCreated = true;
+ return null;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/TaskStackChangedListenerTest.java b/services/tests/servicestests/src/com/android/server/am/TaskStackChangedListenerTest.java
new file mode 100644
index 0000000..0359096
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/TaskStackChangedListenerTest.java
@@ -0,0 +1,297 @@
+/*
+ * 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.am;
+
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.ActivityManager.TaskDescription;
+import android.app.IActivityManager;
+import android.app.ITaskStackListener;
+import android.app.Instrumentation.ActivityMonitor;
+import android.app.TaskStackListener;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.res.Resources.Theme;
+import android.os.RemoteException;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.test.uiautomator.UiDevice;
+import android.text.TextUtils;
+import android.util.Pair;
+import com.android.internal.annotations.GuardedBy;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class TaskStackChangedListenerTest {
+
+ private IActivityManager mService;
+ private ITaskStackListener mTaskStackListener;
+
+ private static final Object sLock = new Object();
+ @GuardedBy("sLock")
+ private static boolean sTaskStackChangedCalled;
+ private static boolean sActivityBResumed;
+
+ @Before
+ public void setUp() throws Exception {
+ mService = ActivityManager.getService();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mService.unregisterTaskStackListener(mTaskStackListener);
+ mTaskStackListener = null;
+ }
+
+ @Test
+ public void testTaskStackChanged_afterFinish() throws Exception {
+ registerTaskStackChangedListener(new TaskStackListener() {
+ @Override
+ public void onTaskStackChanged() throws RemoteException {
+ synchronized (sLock) {
+ sTaskStackChangedCalled = true;
+ }
+ }
+ });
+
+ Context ctx = InstrumentationRegistry.getContext();
+ ctx.startActivity(new Intent(ctx, ActivityA.class));
+ UiDevice.getInstance(getInstrumentation()).waitForIdle();
+ synchronized (sLock) {
+ Assert.assertTrue(sTaskStackChangedCalled);
+ }
+ Assert.assertTrue(sActivityBResumed);
+ }
+
+ @Test
+ public void testTaskDescriptionChanged() throws Exception {
+ final Object[] params = new Object[2];
+ final CountDownLatch latch = new CountDownLatch(1);
+ registerTaskStackChangedListener(new TaskStackListener() {
+ int taskId = -1;
+
+ @Override
+ public void onTaskCreated(int taskId, ComponentName componentName)
+ throws RemoteException {
+ this.taskId = taskId;
+ }
+ @Override
+ public void onTaskDescriptionChanged(int taskId, TaskDescription td)
+ throws RemoteException {
+ if (this.taskId == taskId && !TextUtils.isEmpty(td.getLabel())) {
+ params[0] = taskId;
+ params[1] = td;
+ latch.countDown();
+ }
+ }
+ });
+ final Activity activity = startTestActivity(ActivityTaskDescriptionChange.class);
+ waitForCallback(latch);
+ assertEquals(activity.getTaskId(), params[0]);
+ assertEquals("Test Label", ((TaskDescription) params[1]).getLabel());
+ }
+
+ @Test
+ public void testActivityRequestedOrientationChanged() throws Exception {
+ final int[] params = new int[2];
+ final CountDownLatch latch = new CountDownLatch(1);
+ registerTaskStackChangedListener(new TaskStackListener() {
+ @Override
+ public void onActivityRequestedOrientationChanged(int taskId,
+ int requestedOrientation) {
+ params[0] = taskId;
+ params[1] = requestedOrientation;
+ latch.countDown();
+ }
+ });
+ final Activity activity = startTestActivity(ActivityRequestedOrientationChange.class);
+ waitForCallback(latch);
+ assertEquals(activity.getTaskId(), params[0]);
+ assertEquals(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, params[1]);
+ }
+
+ @Test
+ /**
+ * Tests for onTaskCreated, onTaskMovedToFront, onTaskRemoved and onTaskRemovalStarted.
+ */
+ public void testTaskChangeCallBacks() throws Exception {
+ final Object[] params = new Object[2];
+ final CountDownLatch taskCreatedLaunchLatch = new CountDownLatch(1);
+ final CountDownLatch taskMovedToFrontLatch = new CountDownLatch(1);
+ final CountDownLatch taskRemovedLatch = new CountDownLatch(1);
+ final CountDownLatch taskRemovalStartedLatch = new CountDownLatch(1);
+ final CountDownLatch onDetachedFromWindowLatch = new CountDownLatch(1);
+ registerTaskStackChangedListener(new TaskStackListener() {
+ @Override
+ public void onTaskCreated(int taskId, ComponentName componentName)
+ throws RemoteException {
+ params[0] = taskId;
+ params[1] = componentName;
+ taskCreatedLaunchLatch.countDown();
+ }
+
+ @Override
+ public void onTaskMovedToFront(int taskId) throws RemoteException {
+ params[0] = taskId;
+ taskMovedToFrontLatch.countDown();
+ }
+
+ @Override
+ public void onTaskRemovalStarted(int taskId) {
+ params[0] = taskId;
+ taskRemovalStartedLatch.countDown();
+ }
+
+ @Override
+ public void onTaskRemoved(int taskId) throws RemoteException {
+ params[0] = taskId;
+ taskRemovedLatch.countDown();
+ }
+ });
+
+ final ActivityTaskChangeCallbacks activity =
+ (ActivityTaskChangeCallbacks) startTestActivity(ActivityTaskChangeCallbacks.class);
+ final int id = activity.getTaskId();
+
+ // Test for onTaskCreated.
+ waitForCallback(taskCreatedLaunchLatch);
+ assertEquals(id, params[0]);
+ ComponentName componentName = (ComponentName) params[1];
+ assertEquals(ActivityTaskChangeCallbacks.class.getName(), componentName.getClassName());
+
+ // Test for onTaskMovedToFront.
+ assertEquals(1, taskMovedToFrontLatch.getCount());
+ mService.moveTaskToFront(id, 0, null);
+ waitForCallback(taskMovedToFrontLatch);
+ assertEquals(activity.getTaskId(), params[0]);
+
+ // Test for onTaskRemovalStarted.
+ assertEquals(1, taskRemovalStartedLatch.getCount());
+ activity.finishAndRemoveTask();
+ waitForCallback(taskRemovalStartedLatch);
+ // onTaskRemovalStarted happens before the activity's window is removed.
+ assertFalse(activity.onDetachedFromWindowCalled);
+ assertEquals(id, params[0]);
+
+ // Test for onTaskRemoved.
+ assertEquals(1, taskRemovedLatch.getCount());
+ waitForCallback(taskRemovedLatch);
+ assertEquals(id, params[0]);
+ assertTrue(activity.onDetachedFromWindowCalled);
+ }
+
+ /**
+ * Starts the provided activity and returns the started instance.
+ */
+ private Activity startTestActivity(Class<?> activityClass) {
+ final Context context = InstrumentationRegistry.getContext();
+ final ActivityMonitor monitor =
+ new ActivityMonitor(activityClass.getName(), null, false);
+ InstrumentationRegistry.getInstrumentation().addMonitor(monitor);
+ context.startActivity(new Intent(context, activityClass));
+ final Activity activity = monitor.waitForActivityWithTimeout(1000);
+ if (activity == null) {
+ throw new RuntimeException("Timed out waiting for Activity");
+ }
+ return activity;
+ }
+
+ private void registerTaskStackChangedListener(ITaskStackListener listener) throws Exception {
+ mTaskStackListener = listener;
+ mService.registerTaskStackListener(listener);
+ }
+
+ private void waitForCallback(CountDownLatch latch) {
+ try {
+ final boolean result = latch.await(2, TimeUnit.SECONDS);
+ if (!result) {
+ throw new RuntimeException("Timed out waiting for task stack change notification");
+ }
+ }catch (InterruptedException e) {}
+ }
+
+ public static class ActivityA extends Activity {
+
+ private boolean mActivityBLaunched = false;
+
+ @Override
+ protected void onPostResume() {
+ super.onPostResume();
+ if (mActivityBLaunched) {
+ return;
+ }
+ mActivityBLaunched = true;
+ finish();
+ startActivity(new Intent(this, ActivityB.class));
+ }
+ }
+
+ public static class ActivityB extends Activity {
+
+ @Override
+ protected void onPostResume() {
+ super.onPostResume();
+ synchronized (sLock) {
+ sTaskStackChangedCalled = false;
+ }
+ sActivityBResumed = true;
+ finish();
+ }
+ }
+
+ public static class ActivityRequestedOrientationChange extends Activity {
+ @Override
+ protected void onPostResume() {
+ super.onPostResume();
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ finish();
+ }
+ }
+
+ public static class ActivityTaskDescriptionChange extends Activity {
+ @Override
+ protected void onPostResume() {
+ super.onPostResume();
+ setTaskDescription(new TaskDescription("Test Label"));
+ finish();
+ }
+ }
+
+ public static class ActivityTaskChangeCallbacks extends Activity {
+ public boolean onDetachedFromWindowCalled = false;;
+
+ @Override
+ public void onDetachedFromWindow() {
+ onDetachedFromWindowCalled = true;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
new file mode 100644
index 0000000..cc4f519
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -0,0 +1,441 @@
+/*
+ * 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.am;
+
+import android.app.IUserSwitchObserver;
+import android.content.Context;
+import android.content.IIntentReceiver;
+import android.content.Intent;
+import android.content.pm.UserInfo;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IRemoteCallback;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.UserManagerInternal;
+import android.platform.test.annotations.Presubmit;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+
+import com.android.server.pm.UserManagerService;
+import com.android.server.wm.WindowManagerService;
+
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.testing.DexmakerShareClassLoaderRule.runWithDexmakerShareClassLoader;
+import static com.android.server.am.UserController.CONTINUE_USER_SWITCH_MSG;
+import static com.android.server.am.UserController.REPORT_LOCKED_BOOT_COMPLETE_MSG;
+import static com.android.server.am.UserController.REPORT_USER_SWITCH_COMPLETE_MSG;
+import static com.android.server.am.UserController.REPORT_USER_SWITCH_MSG;
+import static com.android.server.am.UserController.SYSTEM_USER_CURRENT_MSG;
+import static com.android.server.am.UserController.SYSTEM_USER_START_MSG;
+import static com.android.server.am.UserController.USER_SWITCH_TIMEOUT_MSG;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+
+/**
+ * Usage: bit FrameworksServicesTests:com.android.server.am.UserControllerTest
+ */
+@Presubmit
+public class UserControllerTest extends AndroidTestCase {
+ private static final int TEST_USER_ID = 10;
+ private static final int NONEXIST_USER_ID = 2;
+ private static String TAG = UserControllerTest.class.getSimpleName();
+ private UserController mUserController;
+ private TestInjector mInjector;
+
+ private static final List<String> START_FOREGROUND_USER_ACTIONS =
+ Arrays.asList(
+ Intent.ACTION_USER_STARTED,
+ Intent.ACTION_USER_SWITCHED,
+ Intent.ACTION_USER_STARTING);
+
+ private static final List<String> START_BACKGROUND_USER_ACTIONS =
+ Arrays.asList(
+ Intent.ACTION_USER_STARTED,
+ Intent.ACTION_LOCKED_BOOT_COMPLETED,
+ Intent.ACTION_USER_STARTING);
+
+ private static final Set<Integer> START_FOREGROUND_USER_MESSAGE_CODES =
+ new HashSet<>(Arrays.asList(REPORT_USER_SWITCH_MSG, USER_SWITCH_TIMEOUT_MSG,
+ SYSTEM_USER_START_MSG, SYSTEM_USER_CURRENT_MSG));
+
+ private static final Set<Integer> START_BACKGROUND_USER_MESSAGE_CODES =
+ new HashSet<>(Arrays.asList(SYSTEM_USER_START_MSG, REPORT_LOCKED_BOOT_COMPLETE_MSG));
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ runWithDexmakerShareClassLoader(() -> {
+ mInjector = Mockito.spy(new TestInjector(getContext()));
+ doNothing().when(mInjector).clearAllLockedTasks(anyString());
+ doNothing().when(mInjector).startHomeActivity(anyInt(), anyString());
+ doReturn(false).when(mInjector).stackSupervisorSwitchUser(anyInt(), any());
+ doNothing().when(mInjector).stackSupervisorResumeFocusedStackTopActivity();
+ mUserController = new UserController(mInjector);
+ setUpUser(TEST_USER_ID, 0);
+ });
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ mInjector.handlerThread.quit();
+ Mockito.validateMockitoUsage();
+ }
+
+ @SmallTest
+ public void testStartUser_foreground() throws RemoteException {
+ mUserController.startUser(TEST_USER_ID, true /* foreground */);
+ Mockito.verify(mInjector.getWindowManager()).startFreezingScreen(anyInt(), anyInt());
+ Mockito.verify(mInjector.getWindowManager(), never()).stopFreezingScreen();
+ Mockito.verify(mInjector.getWindowManager(), times(1)).setSwitchingUser(anyBoolean());
+ Mockito.verify(mInjector.getWindowManager()).setSwitchingUser(true);
+ Mockito.verify(mInjector).clearAllLockedTasks(anyString());
+ startForegroundUserAssertions();
+ }
+
+ @SmallTest
+ public void testStartUser_background() throws RemoteException {
+ mUserController.startUser(TEST_USER_ID, false /* foreground */);
+ Mockito.verify(
+ mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt());
+ Mockito.verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean());
+ Mockito.verify(mInjector, never()).clearAllLockedTasks(anyString());
+ startBackgroundUserAssertions();
+ }
+
+ @SmallTest
+ public void testStartUserUIDisabled() throws RemoteException {
+ mUserController.mUserSwitchUiEnabled = false;
+ mUserController.startUser(TEST_USER_ID, true /* foreground */);
+ Mockito.verify(mInjector.getWindowManager(), never())
+ .startFreezingScreen(anyInt(), anyInt());
+ Mockito.verify(mInjector.getWindowManager(), never()).stopFreezingScreen();
+ Mockito.verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean());
+ startForegroundUserAssertions();
+ }
+
+ private void startUserAssertions(
+ List<String> expectedActions, Set<Integer> expectedMessageCodes)
+ throws RemoteException {
+ assertEquals(expectedActions, getActions(mInjector.sentIntents));
+ Set<Integer> actualCodes = mInjector.handler.getMessageCodes();
+ assertEquals("Unexpected message sent", expectedMessageCodes, actualCodes);
+ }
+
+ private void startBackgroundUserAssertions() throws RemoteException {
+ startUserAssertions(START_BACKGROUND_USER_ACTIONS, START_BACKGROUND_USER_MESSAGE_CODES);
+ }
+
+ private void startForegroundUserAssertions() throws RemoteException {
+ startUserAssertions(START_FOREGROUND_USER_ACTIONS, START_FOREGROUND_USER_MESSAGE_CODES);
+ Message reportMsg = mInjector.handler.getMessageForCode(REPORT_USER_SWITCH_MSG);
+ assertNotNull(reportMsg);
+ UserState userState = (UserState) reportMsg.obj;
+ assertNotNull(userState);
+ assertEquals(TEST_USER_ID, userState.mHandle.getIdentifier());
+ assertEquals("User must be in STATE_BOOTING", UserState.STATE_BOOTING, userState.state);
+ assertEquals("Unexpected old user id", 0, reportMsg.arg1);
+ assertEquals("Unexpected new user id", TEST_USER_ID, reportMsg.arg2);
+ }
+
+ @SmallTest
+ public void testFailedStartUserInForeground() throws RemoteException {
+ mUserController.mUserSwitchUiEnabled = false;
+ mUserController.startUserInForeground(NONEXIST_USER_ID);
+ Mockito.verify(mInjector.getWindowManager(), times(1)).setSwitchingUser(anyBoolean());
+ Mockito.verify(mInjector.getWindowManager()).setSwitchingUser(false);
+ }
+
+ @SmallTest
+ public void testDispatchUserSwitch() throws RemoteException {
+ // Prepare mock observer and register it
+ IUserSwitchObserver observer = mock(IUserSwitchObserver.class);
+ when(observer.asBinder()).thenReturn(new Binder());
+ doAnswer(invocation -> {
+ IRemoteCallback callback = (IRemoteCallback) invocation.getArguments()[1];
+ callback.sendResult(null);
+ return null;
+ }).when(observer).onUserSwitching(anyInt(), any());
+ mUserController.registerUserSwitchObserver(observer, "mock");
+ // Start user -- this will update state of mUserController
+ mUserController.startUser(TEST_USER_ID, true);
+ Message reportMsg = mInjector.handler.getMessageForCode(REPORT_USER_SWITCH_MSG);
+ assertNotNull(reportMsg);
+ UserState userState = (UserState) reportMsg.obj;
+ int oldUserId = reportMsg.arg1;
+ int newUserId = reportMsg.arg2;
+ // Call dispatchUserSwitch and verify that observer was called only once
+ mInjector.handler.clearAllRecordedMessages();
+ mUserController.dispatchUserSwitch(userState, oldUserId, newUserId);
+ Mockito.verify(observer, times(1)).onUserSwitching(eq(TEST_USER_ID), any());
+ Set<Integer> expectedCodes = Collections.singleton(CONTINUE_USER_SWITCH_MSG);
+ Set<Integer> actualCodes = mInjector.handler.getMessageCodes();
+ assertEquals("Unexpected message sent", expectedCodes, actualCodes);
+ Message conMsg = mInjector.handler.getMessageForCode(CONTINUE_USER_SWITCH_MSG);
+ assertNotNull(conMsg);
+ userState = (UserState) conMsg.obj;
+ assertNotNull(userState);
+ assertEquals(TEST_USER_ID, userState.mHandle.getIdentifier());
+ assertEquals("User must be in STATE_BOOTING", UserState.STATE_BOOTING, userState.state);
+ assertEquals("Unexpected old user id", 0, conMsg.arg1);
+ assertEquals("Unexpected new user id", TEST_USER_ID, conMsg.arg2);
+ }
+
+ @SmallTest
+ public void testDispatchUserSwitchBadReceiver() throws RemoteException {
+ // Prepare mock observer which doesn't notify the callback and register it
+ IUserSwitchObserver observer = mock(IUserSwitchObserver.class);
+ when(observer.asBinder()).thenReturn(new Binder());
+ mUserController.registerUserSwitchObserver(observer, "mock");
+ // Start user -- this will update state of mUserController
+ mUserController.startUser(TEST_USER_ID, true);
+ Message reportMsg = mInjector.handler.getMessageForCode(REPORT_USER_SWITCH_MSG);
+ assertNotNull(reportMsg);
+ UserState userState = (UserState) reportMsg.obj;
+ int oldUserId = reportMsg.arg1;
+ int newUserId = reportMsg.arg2;
+ // Call dispatchUserSwitch and verify that observer was called only once
+ mInjector.handler.clearAllRecordedMessages();
+ mUserController.dispatchUserSwitch(userState, oldUserId, newUserId);
+ Mockito.verify(observer, times(1)).onUserSwitching(eq(TEST_USER_ID), any());
+ // Verify that CONTINUE_USER_SWITCH_MSG is not sent (triggers timeout)
+ Set<Integer> actualCodes = mInjector.handler.getMessageCodes();
+ assertTrue("No messages should be sent", actualCodes.isEmpty());
+ }
+
+ @SmallTest
+ public void testContinueUserSwitch() throws RemoteException {
+ // Start user -- this will update state of mUserController
+ mUserController.startUser(TEST_USER_ID, true);
+ Message reportMsg = mInjector.handler.getMessageForCode(REPORT_USER_SWITCH_MSG);
+ assertNotNull(reportMsg);
+ UserState userState = (UserState) reportMsg.obj;
+ int oldUserId = reportMsg.arg1;
+ int newUserId = reportMsg.arg2;
+ mInjector.handler.clearAllRecordedMessages();
+ // Verify that continueUserSwitch worked as expected
+ mUserController.continueUserSwitch(userState, oldUserId, newUserId);
+ Mockito.verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen();
+ continueUserSwitchAssertions();
+ }
+
+ @SmallTest
+ public void testContinueUserSwitchUIDisabled() throws RemoteException {
+ mUserController.mUserSwitchUiEnabled = false;
+ // Start user -- this will update state of mUserController
+ mUserController.startUser(TEST_USER_ID, true);
+ Message reportMsg = mInjector.handler.getMessageForCode(REPORT_USER_SWITCH_MSG);
+ assertNotNull(reportMsg);
+ UserState userState = (UserState) reportMsg.obj;
+ int oldUserId = reportMsg.arg1;
+ int newUserId = reportMsg.arg2;
+ mInjector.handler.clearAllRecordedMessages();
+ // Verify that continueUserSwitch worked as expected
+ mUserController.continueUserSwitch(userState, oldUserId, newUserId);
+ Mockito.verify(mInjector.getWindowManager(), never()).stopFreezingScreen();
+ continueUserSwitchAssertions();
+ }
+
+ private void continueUserSwitchAssertions() throws RemoteException {
+ Set<Integer> expectedCodes = Collections.singleton(REPORT_USER_SWITCH_COMPLETE_MSG);
+ Set<Integer> actualCodes = mInjector.handler.getMessageCodes();
+ assertEquals("Unexpected message sent", expectedCodes, actualCodes);
+ Message msg = mInjector.handler.getMessageForCode(REPORT_USER_SWITCH_COMPLETE_MSG);
+ assertNotNull(msg);
+ assertEquals("Unexpected userId", TEST_USER_ID, msg.arg1);
+ }
+
+ @SmallTest
+ public void testDispatchUserSwitchComplete() throws RemoteException {
+ // Prepare mock observer and register it
+ IUserSwitchObserver observer = mock(IUserSwitchObserver.class);
+ when(observer.asBinder()).thenReturn(new Binder());
+ mUserController.registerUserSwitchObserver(observer, "mock");
+ // Start user -- this will update state of mUserController
+ mUserController.startUser(TEST_USER_ID, true);
+ Message reportMsg = mInjector.handler.getMessageForCode(REPORT_USER_SWITCH_MSG);
+ assertNotNull(reportMsg);
+ int newUserId = reportMsg.arg2;
+ mInjector.handler.clearAllRecordedMessages();
+ // Mockito can't reset only interactions, so just verify that this hasn't been
+ // called with 'false' until after dispatchUserSwitchComplete.
+ Mockito.verify(mInjector.getWindowManager(), never()).setSwitchingUser(false);
+ // Call dispatchUserSwitchComplete
+ mUserController.dispatchUserSwitchComplete(newUserId);
+ Mockito.verify(observer, times(1)).onUserSwitchComplete(anyInt());
+ Mockito.verify(observer).onUserSwitchComplete(TEST_USER_ID);
+ Mockito.verify(mInjector.getWindowManager(), times(1)).setSwitchingUser(false);
+ }
+
+ private void setUpUser(int userId, int flags) {
+ UserInfo userInfo = new UserInfo(userId, "User" + userId, flags);
+ when(mInjector.userManagerMock.getUserInfo(eq(userId))).thenReturn(userInfo);
+ }
+
+ private static List<String> getActions(List<Intent> intents) {
+ List<String> result = new ArrayList<>();
+ for (Intent intent : intents) {
+ result.add(intent.getAction());
+ }
+ return result;
+ }
+
+ // Should be public to allow mocking
+ public static class TestInjector extends UserController.Injector {
+ TestHandler handler;
+ TestHandler uiHandler;
+ HandlerThread handlerThread;
+ UserManagerService userManagerMock;
+ UserManagerInternal userManagerInternalMock;
+ WindowManagerService windowManagerMock;
+ private Context mCtx;
+ List<Intent> sentIntents = new ArrayList<>();
+
+ TestInjector(Context ctx) {
+ super(null);
+ mCtx = ctx;
+ handlerThread = new HandlerThread(TAG);
+ handlerThread.start();
+ handler = new TestHandler(handlerThread.getLooper());
+ uiHandler = new TestHandler(handlerThread.getLooper());
+ userManagerMock = mock(UserManagerService.class);
+ userManagerInternalMock = mock(UserManagerInternal.class);
+ windowManagerMock = mock(WindowManagerService.class);
+ }
+
+ @Override
+ protected Handler getHandler(Handler.Callback callback) {
+ return handler;
+ }
+
+ @Override
+ protected Handler getUiHandler(Handler.Callback callback) {
+ return uiHandler;
+ }
+
+ @Override
+ protected UserManagerService getUserManager() {
+ return userManagerMock;
+ }
+
+ @Override
+ UserManagerInternal getUserManagerInternal() {
+ return userManagerInternalMock;
+ }
+
+ @Override
+ protected Context getContext() {
+ return mCtx;
+ }
+
+ @Override
+ int checkCallingPermission(String permission) {
+ Log.i(TAG, "checkCallingPermission " + permission);
+ return PERMISSION_GRANTED;
+ }
+
+ @Override
+ WindowManagerService getWindowManager() {
+ return windowManagerMock;
+ }
+
+ @Override
+ void updateUserConfiguration() {
+ Log.i(TAG, "updateUserConfiguration");
+ }
+
+ @Override
+ protected int broadcastIntent(Intent intent, String resolvedType,
+ IIntentReceiver resultTo, int resultCode, String resultData, Bundle resultExtras,
+ String[] requiredPermissions, int appOp, Bundle bOptions, boolean ordered,
+ boolean sticky, int callingPid, int callingUid, int userId) {
+ Log.i(TAG, "broadcastIntentLocked " + intent);
+ sentIntents.add(intent);
+ return 0;
+ }
+
+ @Override
+ void reportGlobalUsageEventLocked(int event) {
+ }
+
+ @Override
+ void reportCurWakefulnessUsageEvent() {
+ }
+ }
+
+ private static class TestHandler extends Handler {
+ private final List<Message> mMessages = new ArrayList<>();
+
+ TestHandler(Looper looper) {
+ super(looper);
+ }
+
+ Set<Integer> getMessageCodes() {
+ Set<Integer> result = new LinkedHashSet<>();
+ for (Message msg : mMessages) {
+ result.add(msg.what);
+ }
+ return result;
+ }
+
+ Message getMessageForCode(int what) {
+ for (Message msg : mMessages) {
+ if (msg.what == what) {
+ return msg;
+ }
+ }
+ return null;
+ }
+
+ void clearAllRecordedMessages() {
+ mMessages.clear();
+ }
+
+ @Override
+ public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
+ Message copy = new Message();
+ copy.copyFrom(msg);
+ mMessages.add(copy);
+ return super.sendMessageAtTime(msg, uptimeMillis);
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/policy/FakeWindowState.java b/services/tests/servicestests/src/com/android/server/policy/FakeWindowState.java
new file mode 100644
index 0000000..7487d44
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/policy/FakeWindowState.java
@@ -0,0 +1,266 @@
+/*
+ * 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.policy;
+
+import android.annotation.Nullable;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.proto.ProtoOutputStream;
+import android.view.Display;
+import android.view.DisplayCutout;
+import android.view.IApplicationToken;
+import android.view.WindowManager;
+
+import com.android.server.wm.utils.WmDisplayCutout;
+
+public class FakeWindowState implements WindowManagerPolicy.WindowState {
+
+ public final Rect parentFrame = new Rect();
+ public final Rect displayFrame = new Rect();
+ public final Rect overscanFrame = new Rect();
+ public final Rect contentFrame = new Rect();
+ public final Rect visibleFrame = new Rect();
+ public final Rect decorFrame = new Rect();
+ public final Rect stableFrame = new Rect();
+ public Rect outsetFrame = new Rect();
+
+ public WmDisplayCutout displayCutout;
+
+ public WindowManager.LayoutParams attrs;
+ public int displayId;
+ public boolean isVoiceInteraction;
+ public boolean inMultiWindowMode;
+ public boolean visible = true;
+ public int surfaceLayer = 1;
+ public boolean isDimming = false;
+
+ public boolean policyVisible = true;
+
+ @Override
+ public int getOwningUid() {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public String getOwningPackage() {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public void computeFrameLw(Rect parentFrame, Rect displayFrame, Rect overlayFrame,
+ Rect contentFrame, Rect visibleFrame, Rect decorFrame, Rect stableFrame,
+ @Nullable Rect outsetFrame, WmDisplayCutout displayCutout,
+ boolean parentFrameWasClippedByDisplayCutout) {
+ this.parentFrame.set(parentFrame);
+ this.displayFrame.set(displayFrame);
+ this.overscanFrame.set(overlayFrame);
+ this.contentFrame.set(contentFrame);
+ this.visibleFrame.set(visibleFrame);
+ this.decorFrame.set(decorFrame);
+ this.stableFrame.set(stableFrame);
+ this.outsetFrame = outsetFrame == null ? null : new Rect(outsetFrame);
+ this.displayCutout = displayCutout;
+ }
+
+ @Override
+ public Rect getFrameLw() {
+ return parentFrame;
+ }
+
+ @Override
+ public Rect getDisplayFrameLw() {
+ return displayFrame;
+ }
+
+ @Override
+ public Rect getOverscanFrameLw() {
+ return overscanFrame;
+ }
+
+ @Override
+ public Rect getContentFrameLw() {
+ return contentFrame;
+ }
+
+ @Override
+ public Rect getVisibleFrameLw() {
+ return visibleFrame;
+ }
+
+ @Override
+ public boolean getGivenInsetsPendingLw() {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public Rect getGivenContentInsetsLw() {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public Rect getGivenVisibleInsetsLw() {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public WindowManager.LayoutParams getAttrs() {
+ return attrs;
+ }
+
+ @Override
+ public boolean getNeedsMenuLw(WindowManagerPolicy.WindowState bottom) {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public int getSystemUiVisibility() {
+ return attrs.systemUiVisibility | attrs.subtreeSystemUiVisibility;
+ }
+
+ @Override
+ public int getSurfaceLayer() {
+ return surfaceLayer;
+ }
+
+ @Override
+ public int getBaseType() {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public IApplicationToken getAppToken() {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public boolean isVoiceInteraction() {
+ return isVoiceInteraction;
+ }
+
+ @Override
+ public boolean hasAppShownWindows() {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public boolean isVisibleLw() {
+ return visible && policyVisible;
+ }
+
+ @Override
+ public boolean isDisplayedLw() {
+ return isVisibleLw();
+ }
+
+ @Override
+ public boolean isAnimatingLw() {
+ return false;
+ }
+
+ @Override
+ public boolean canAffectSystemUiFlags() {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public boolean isGoneForLayoutLw() {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public boolean isDrawnLw() {
+ return true;
+ }
+
+ @Override
+ public boolean hasDrawnLw() {
+ return true;
+ }
+
+ @Override
+ public boolean hideLw(boolean doAnimation) {
+ if (!policyVisible) {
+ return false;
+ }
+ policyVisible = false;
+ return true;
+ }
+
+ @Override
+ public boolean showLw(boolean doAnimation) {
+ if (policyVisible) {
+ return false;
+ }
+ policyVisible = true;
+ return true;
+ }
+
+ @Override
+ public boolean isAlive() {
+ return true;
+ }
+
+ @Override
+ public boolean isDefaultDisplay() {
+ return displayId == Display.DEFAULT_DISPLAY;
+ }
+
+ @Override
+ public boolean isDimming() {
+ return isDimming;
+ }
+
+ @Override
+ public int getWindowingMode() {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public boolean isInMultiWindowMode() {
+ return inMultiWindowMode;
+ }
+
+ @Override
+ public int getRotationAnimationHint() {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public boolean isInputMethodWindow() {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public int getDisplayId() {
+ return displayId;
+ }
+
+ @Override
+ public boolean canAcquireSleepToken() {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public void writeIdentifierToProto(ProtoOutputStream proto, long fieldId){
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public boolean isInputMethodTarget() {
+ return false;
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/policy/PhoneWindowManagerInsetsTest.java b/services/tests/servicestests/src/com/android/server/policy/PhoneWindowManagerInsetsTest.java
new file mode 100644
index 0000000..7e18ce7
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/policy/PhoneWindowManagerInsetsTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.policy;
+
+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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.Display;
+import android.view.DisplayInfo;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ErrorCollector;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class PhoneWindowManagerInsetsTest extends PhoneWindowManagerTestBase {
+
+ @Rule
+ public final ErrorCollector mErrorCollector = new ErrorCollector();
+
+ @Before
+ public void setUp() throws Exception {
+ addStatusBar();
+ addNavigationBar();
+ }
+
+ @Test
+ public void portrait() throws Exception {
+ 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() throws Exception {
+ 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() throws Exception {
+ 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() throws Exception {
+ 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() throws Exception {
+ 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() throws Exception {
+ 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() throws Exception {
+ 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() throws Exception {
+ 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();
+ mPolicy.getStableInsetsLw(di.rotation, di.logicalWidth, di.logicalHeight,
+ di.displayCutout, result);
+ return result;
+ }
+
+ private Rect getNonDecorInsetsLw(DisplayInfo di) {
+ Rect result = new Rect();
+ mPolicy.getNonDecorInsetsLw(di.rotation, di.logicalWidth, di.logicalHeight,
+ di.displayCutout, result);
+ return result;
+ }
+
+ private int getNonDecorDisplayWidth(DisplayInfo di) {
+ return mPolicy.getNonDecorDisplayWidth(di.logicalWidth, di.logicalHeight, di.rotation,
+ 0 /* ui */, Display.DEFAULT_DISPLAY, di.displayCutout);
+ }
+
+ private int getNonDecorDisplayHeight(DisplayInfo di) {
+ return mPolicy.getNonDecorDisplayHeight(di.logicalWidth, di.logicalHeight, di.rotation,
+ 0 /* ui */, Display.DEFAULT_DISPLAY, di.displayCutout);
+ }
+
+ private int getConfigDisplayWidth(DisplayInfo di) {
+ return mPolicy.getConfigDisplayWidth(di.logicalWidth, di.logicalHeight, di.rotation,
+ 0 /* ui */, Display.DEFAULT_DISPLAY, di.displayCutout);
+ }
+
+ private int getConfigDisplayHeight(DisplayInfo di) {
+ return mPolicy.getConfigDisplayHeight(di.logicalWidth, di.logicalHeight, di.rotation,
+ 0 /* ui */, Display.DEFAULT_DISPLAY, di.displayCutout);
+ }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/policy/PhoneWindowManagerLayoutTest.java b/services/tests/servicestests/src/com/android/server/policy/PhoneWindowManagerLayoutTest.java
new file mode 100644
index 0000000..97a716f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/policy/PhoneWindowManagerLayoutTest.java
@@ -0,0 +1,367 @@
+/*
+ * 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.policy;
+
+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_FULLSCREEN;
+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.PRIVATE_FLAG_IS_SCREEN_DECOR;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.DisplayCutout;
+import android.view.WindowManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class PhoneWindowManagerLayoutTest extends PhoneWindowManagerTestBase {
+
+ private FakeWindowState mAppWindow;
+
+ @Before
+ public void setUp() throws Exception {
+ mAppWindow = new FakeWindowState();
+ mAppWindow.attrs = new WindowManager.LayoutParams(MATCH_PARENT, MATCH_PARENT,
+ TYPE_APPLICATION,
+ FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR | FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
+ PixelFormat.TRANSLUCENT);
+
+ addStatusBar();
+ addNavigationBar();
+ }
+
+ @Test
+ public void layoutWindowLw_appDrawsBars() throws Exception {
+ mAppWindow.attrs.flags |= FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+ mPolicy.addWindow(mAppWindow);
+
+ mPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mPolicy.layoutWindowLw(mAppWindow, null, mFrames);
+
+ assertInsetByTopBottom(mAppWindow.parentFrame, 0, 0);
+ assertInsetByTopBottom(mAppWindow.stableFrame, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.contentFrame, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.decorFrame, 0, 0);
+ assertInsetBy(mAppWindow.displayFrame, 0, 0, 0, 0);
+ }
+
+ @Test
+ public void layoutWindowLw_appWontDrawBars() throws Exception {
+ mAppWindow.attrs.flags &= ~FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+ mPolicy.addWindow(mAppWindow);
+
+ mPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mPolicy.layoutWindowLw(mAppWindow, null, mFrames);
+
+ assertInsetByTopBottom(mAppWindow.parentFrame, 0, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.stableFrame, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.contentFrame, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.decorFrame, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.displayFrame, 0, NAV_BAR_HEIGHT);
+ }
+
+ @Test
+ public void layoutWindowLw_appWontDrawBars_forceStatus() throws Exception {
+ mAppWindow.attrs.flags &= ~FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+ mAppWindow.attrs.privateFlags |= PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND;
+ mPolicy.addWindow(mAppWindow);
+
+ mPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mPolicy.layoutWindowLw(mAppWindow, null, mFrames);
+
+ assertInsetByTopBottom(mAppWindow.parentFrame, 0, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.stableFrame, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.contentFrame, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.decorFrame, 0, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.displayFrame, 0, NAV_BAR_HEIGHT);
+ }
+
+ @Test
+ public void addingWindow_doesNotTamperWithSysuiFlags() {
+ mAppWindow.attrs.flags |= FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+ mPolicy.addWindow(mAppWindow);
+
+ assertEquals(0, mAppWindow.attrs.systemUiVisibility);
+ assertEquals(0, mAppWindow.attrs.subtreeSystemUiVisibility);
+ }
+
+ @Test
+ public void layoutWindowLw_withDisplayCutout() {
+ addDisplayCutout();
+
+ mPolicy.addWindow(mAppWindow);
+
+ mPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mPolicy.layoutWindowLw(mAppWindow, null, mFrames);
+
+ assertInsetByTopBottom(mAppWindow.parentFrame, 0, 0);
+ assertInsetByTopBottom(mAppWindow.stableFrame, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.contentFrame, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.decorFrame, 0, 0);
+ assertInsetByTopBottom(mAppWindow.displayFrame, 0, 0);
+ }
+
+ @Test
+ public void layoutWindowLw_withhDisplayCutout_never() {
+ addDisplayCutout();
+
+ mAppWindow.attrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
+ mPolicy.addWindow(mAppWindow);
+
+ mPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mPolicy.layoutWindowLw(mAppWindow, null, mFrames);
+
+ assertInsetByTopBottom(mAppWindow.parentFrame, STATUS_BAR_HEIGHT, 0);
+ assertInsetByTopBottom(mAppWindow.stableFrame, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.contentFrame, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.decorFrame, 0, 0);
+ assertInsetByTopBottom(mAppWindow.displayFrame, STATUS_BAR_HEIGHT, 0);
+ }
+
+ @Test
+ public void layoutWindowLw_withDisplayCutout_layoutFullscreen() {
+ addDisplayCutout();
+
+ mAppWindow.attrs.subtreeSystemUiVisibility |= SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
+ mPolicy.addWindow(mAppWindow);
+
+ mPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mPolicy.layoutWindowLw(mAppWindow, null, mFrames);
+
+ assertInsetByTopBottom(mAppWindow.parentFrame, 0, 0);
+ assertInsetByTopBottom(mAppWindow.stableFrame, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.contentFrame, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.decorFrame, 0, 0);
+ assertInsetBy(mAppWindow.displayFrame, 0, 0, 0, 0);
+ }
+
+ @Test
+ public void layoutWindowLw_withDisplayCutout_fullscreen() {
+ addDisplayCutout();
+
+ mAppWindow.attrs.subtreeSystemUiVisibility |= SYSTEM_UI_FLAG_FULLSCREEN;
+ mPolicy.addWindow(mAppWindow);
+
+ mPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mPolicy.layoutWindowLw(mAppWindow, null, mFrames);
+
+ assertInsetByTopBottom(mAppWindow.parentFrame, STATUS_BAR_HEIGHT, 0);
+ assertInsetByTopBottom(mAppWindow.stableFrame, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.contentFrame, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.decorFrame, 0, 0);
+ assertInsetByTopBottom(mAppWindow.displayFrame, STATUS_BAR_HEIGHT, 0);
+ }
+
+ @Test
+ public void layoutWindowLw_withDisplayCutout_fullscreenInCutout() {
+ addDisplayCutout();
+
+ mAppWindow.attrs.subtreeSystemUiVisibility |= SYSTEM_UI_FLAG_FULLSCREEN;
+ mAppWindow.attrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+ mPolicy.addWindow(mAppWindow);
+
+ mPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mPolicy.layoutWindowLw(mAppWindow, null, mFrames);
+
+ assertInsetByTopBottom(mAppWindow.parentFrame, 0, 0);
+ assertInsetByTopBottom(mAppWindow.stableFrame, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.contentFrame, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.decorFrame, 0, 0);
+ assertInsetByTopBottom(mAppWindow.displayFrame, 0, 0);
+ }
+
+
+ @Test
+ public void layoutWindowLw_withDisplayCutout_landscape() {
+ addDisplayCutout();
+ setRotation(ROTATION_90);
+ mPolicy.addWindow(mAppWindow);
+
+ mPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mPolicy.layoutWindowLw(mAppWindow, null, mFrames);
+
+ assertInsetBy(mAppWindow.parentFrame, DISPLAY_CUTOUT_HEIGHT, 0, 0, 0);
+ assertInsetBy(mAppWindow.stableFrame, 0, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT, 0);
+ assertInsetBy(mAppWindow.contentFrame,
+ DISPLAY_CUTOUT_HEIGHT, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT, 0);
+ assertInsetBy(mAppWindow.decorFrame, 0, 0, 0, 0);
+ assertInsetBy(mAppWindow.displayFrame, DISPLAY_CUTOUT_HEIGHT, 0, 0, 0);
+ }
+
+ @Test
+ public void layoutWindowLw_withDisplayCutout_seascape() {
+ addDisplayCutout();
+ setRotation(ROTATION_270);
+ mPolicy.addWindow(mAppWindow);
+
+ mPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mPolicy.layoutWindowLw(mAppWindow, null, mFrames);
+
+ assertInsetBy(mAppWindow.parentFrame, 0, 0, DISPLAY_CUTOUT_HEIGHT, 0);
+ assertInsetBy(mAppWindow.stableFrame, NAV_BAR_HEIGHT, STATUS_BAR_HEIGHT, 0, 0);
+ assertInsetBy(mAppWindow.contentFrame,
+ NAV_BAR_HEIGHT, STATUS_BAR_HEIGHT, DISPLAY_CUTOUT_HEIGHT, 0);
+ assertInsetBy(mAppWindow.decorFrame, 0, 0, 0, 0);
+ assertInsetBy(mAppWindow.displayFrame, 0, 0, DISPLAY_CUTOUT_HEIGHT, 0);
+ }
+
+ @Test
+ public void layoutWindowLw_withDisplayCutout_fullscreen_landscape() {
+ addDisplayCutout();
+ setRotation(ROTATION_90);
+
+ mAppWindow.attrs.subtreeSystemUiVisibility |= SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
+ mPolicy.addWindow(mAppWindow);
+
+ mPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mPolicy.layoutWindowLw(mAppWindow, null, mFrames);
+
+ assertInsetBy(mAppWindow.parentFrame, DISPLAY_CUTOUT_HEIGHT, 0, 0, 0);
+ assertInsetBy(mAppWindow.stableFrame, 0, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT, 0);
+ assertInsetBy(mAppWindow.contentFrame,
+ DISPLAY_CUTOUT_HEIGHT, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT, 0);
+ assertInsetBy(mAppWindow.decorFrame, 0, 0, 0, 0);
+ }
+
+ @Test
+ public void layoutWindowLw_withDisplayCutout_floatingInScreen() {
+ addDisplayCutout();
+
+ mAppWindow.attrs.flags = FLAG_LAYOUT_IN_SCREEN;
+ mAppWindow.attrs.type = TYPE_APPLICATION_OVERLAY;
+ mAppWindow.attrs.width = DISPLAY_WIDTH;
+ mAppWindow.attrs.height = DISPLAY_HEIGHT;
+ mPolicy.addWindow(mAppWindow);
+
+ mPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mPolicy.layoutWindowLw(mAppWindow, null, mFrames);
+
+ assertInsetByTopBottom(mAppWindow.parentFrame, 0, NAV_BAR_HEIGHT);
+ assertInsetByTopBottom(mAppWindow.displayFrame, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT);
+ }
+
+ @Test
+ public void layoutWindowLw_withDisplayCutout_fullscreenInCutout_landscape() {
+ addDisplayCutout();
+ setRotation(ROTATION_90);
+
+ mAppWindow.attrs.subtreeSystemUiVisibility |= SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
+ mAppWindow.attrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+ mPolicy.addWindow(mAppWindow);
+
+ mPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+ mPolicy.layoutWindowLw(mAppWindow, null, mFrames);
+
+ assertInsetBy(mAppWindow.parentFrame, 0, 0, 0, 0);
+ assertInsetBy(mAppWindow.stableFrame, 0, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT, 0);
+ assertInsetBy(mAppWindow.contentFrame,
+ DISPLAY_CUTOUT_HEIGHT, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT, 0);
+ assertInsetBy(mAppWindow.decorFrame, 0, 0, 0, 0);
+ }
+
+ @Test
+ public void layoutHint_screenDecorWindow() {
+ addDisplayCutout();
+ mAppWindow.attrs.privateFlags |= PRIVATE_FLAG_IS_SCREEN_DECOR;
+
+ mPolicy.beginLayoutLw(mFrames, 0 /* UI mode */);
+
+ final Rect frame = new Rect();
+ final Rect content = new Rect();
+ final Rect stable = new Rect();
+ final Rect outsets = new Rect();
+ final DisplayCutout.ParcelableWrapper cutout = new DisplayCutout.ParcelableWrapper();
+ mPolicy.getLayoutHintLw(mAppWindow.attrs, null /* taskBounds */, mFrames, frame, content,
+ stable, outsets, cutout);
+
+ assertThat(frame, equalTo(mFrames.mUnrestricted));
+ assertThat(content, equalTo(new Rect()));
+ assertThat(stable, equalTo(new Rect()));
+ assertThat(outsets, equalTo(new Rect()));
+ assertThat(cutout.get(), equalTo(DisplayCutout.NO_CUTOUT));
+ }
+
+ @Test
+ public void layoutHint_appWindow() {
+ // Initialize DisplayFrames
+ mPolicy.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();
+
+ mPolicy.getLayoutHintLw(mAppWindow.attrs, null, mFrames, 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
+ mPolicy.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();
+
+ mPolicy.getLayoutHintLw(mAppWindow.attrs, taskBounds, mFrames, 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()));
+ }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/policy/PhoneWindowManagerTest.java b/services/tests/servicestests/src/com/android/server/policy/PhoneWindowManagerTest.java
new file mode 100644
index 0000000..30665b5
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/policy/PhoneWindowManagerTest.java
@@ -0,0 +1,253 @@
+/*
+ * 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.policy;
+
+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 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 com.android.server.policy.WindowManagerPolicy.NAV_BAR_LEFT;
+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.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.PixelFormat;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.WindowManager;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class PhoneWindowManagerTest {
+
+ private static FakeWindowState createOpaqueFullscreen(boolean hasLightNavBar) {
+ final FakeWindowState state = new FakeWindowState();
+ state.attrs = new WindowManager.LayoutParams(MATCH_PARENT, MATCH_PARENT,
+ TYPE_BASE_APPLICATION,
+ FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR | FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
+ PixelFormat.OPAQUE);
+ state.attrs.subtreeSystemUiVisibility =
+ hasLightNavBar ? SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR : 0;
+ return state;
+ }
+
+ private static FakeWindowState createDimmingDialogWindow(boolean canBeImTarget) {
+ final FakeWindowState state = new FakeWindowState();
+ state.attrs = new WindowManager.LayoutParams(WRAP_CONTENT, WRAP_CONTENT,
+ TYPE_APPLICATION,
+ FLAG_DIM_BEHIND | (canBeImTarget ? 0 : FLAG_ALT_FOCUSABLE_IM),
+ PixelFormat.TRANSLUCENT);
+ state.isDimming = true;
+ return state;
+ }
+
+ private static FakeWindowState createInputMethodWindow(boolean visible, boolean drawNavBar,
+ boolean hasLightNavBar) {
+ final FakeWindowState state = new FakeWindowState();
+ state.attrs = new WindowManager.LayoutParams(MATCH_PARENT, MATCH_PARENT,
+ TYPE_INPUT_METHOD,
+ FLAG_NOT_FOCUSABLE | FLAG_LAYOUT_IN_SCREEN
+ | (drawNavBar ? FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS : 0),
+ PixelFormat.TRANSPARENT);
+ state.attrs.subtreeSystemUiVisibility =
+ hasLightNavBar ? SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR : 0;
+ state.visible = visible;
+ state.policyVisible = visible;
+ return state;
+ }
+
+
+ @Test
+ public void testChooseNavigationColorWindowLw() throws Exception {
+ final FakeWindowState opaque = createOpaqueFullscreen(false);
+
+ final FakeWindowState dimmingImTarget = createDimmingDialogWindow(true);
+ final FakeWindowState dimmingNonImTarget = createDimmingDialogWindow(false);
+
+ final FakeWindowState visibleIme = createInputMethodWindow(true, true, false);
+ final FakeWindowState invisibleIme = createInputMethodWindow(false, true, false);
+ final FakeWindowState imeNonDrawNavBar = createInputMethodWindow(true, false, false);
+
+ // If everything is null, return null
+ assertNull(null, PhoneWindowManager.chooseNavigationColorWindowLw(
+ null, null, null, NAV_BAR_BOTTOM));
+
+ assertEquals(opaque, PhoneWindowManager.chooseNavigationColorWindowLw(
+ opaque, opaque, null, NAV_BAR_BOTTOM));
+ assertEquals(dimmingImTarget, PhoneWindowManager.chooseNavigationColorWindowLw(
+ opaque, dimmingImTarget, null, NAV_BAR_BOTTOM));
+ assertEquals(dimmingNonImTarget, PhoneWindowManager.chooseNavigationColorWindowLw(
+ opaque, dimmingNonImTarget, null, NAV_BAR_BOTTOM));
+
+ assertEquals(visibleIme, PhoneWindowManager.chooseNavigationColorWindowLw(
+ null, null, visibleIme, NAV_BAR_BOTTOM));
+ assertEquals(visibleIme, PhoneWindowManager.chooseNavigationColorWindowLw(
+ null, dimmingImTarget, visibleIme, NAV_BAR_BOTTOM));
+ assertEquals(dimmingNonImTarget, PhoneWindowManager.chooseNavigationColorWindowLw(
+ null, dimmingNonImTarget, visibleIme, NAV_BAR_BOTTOM));
+ assertEquals(visibleIme, PhoneWindowManager.chooseNavigationColorWindowLw(
+ opaque, opaque, visibleIme, NAV_BAR_BOTTOM));
+ assertEquals(visibleIme, PhoneWindowManager.chooseNavigationColorWindowLw(
+ opaque, dimmingImTarget, visibleIme, NAV_BAR_BOTTOM));
+ assertEquals(dimmingNonImTarget, PhoneWindowManager.chooseNavigationColorWindowLw(
+ opaque, dimmingNonImTarget, visibleIme, NAV_BAR_BOTTOM));
+
+ assertEquals(opaque, PhoneWindowManager.chooseNavigationColorWindowLw(
+ opaque, opaque, invisibleIme, NAV_BAR_BOTTOM));
+ assertEquals(opaque, PhoneWindowManager.chooseNavigationColorWindowLw(
+ opaque, opaque, invisibleIme, NAV_BAR_BOTTOM));
+ assertEquals(opaque, PhoneWindowManager.chooseNavigationColorWindowLw(
+ opaque, opaque, visibleIme, NAV_BAR_RIGHT));
+
+ // Only IME windows that have FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS should be navigation color
+ // window.
+ assertEquals(opaque, PhoneWindowManager.chooseNavigationColorWindowLw(
+ opaque, opaque, imeNonDrawNavBar, NAV_BAR_BOTTOM));
+ assertEquals(dimmingImTarget, PhoneWindowManager.chooseNavigationColorWindowLw(
+ opaque, dimmingImTarget, imeNonDrawNavBar, NAV_BAR_BOTTOM));
+ assertEquals(dimmingNonImTarget, PhoneWindowManager.chooseNavigationColorWindowLw(
+ opaque, dimmingNonImTarget, imeNonDrawNavBar, NAV_BAR_BOTTOM));
+ }
+
+ @Test
+ public void testUpdateLightNavigationBarLw() throws Exception {
+ final FakeWindowState opaqueDarkNavBar = createOpaqueFullscreen(false);
+ final FakeWindowState opaqueLightNavBar = createOpaqueFullscreen(true);
+
+ final FakeWindowState dimming = createDimmingDialogWindow(false);
+
+ final FakeWindowState imeDrawDarkNavBar = createInputMethodWindow(true,true, false);
+ final FakeWindowState imeDrawLightNavBar = createInputMethodWindow(true,true, true);
+
+ assertEquals(SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR,
+ PhoneWindowManager.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, PhoneWindowManager.updateLightNavigationBarLw(
+ 0, opaqueDarkNavBar, opaqueDarkNavBar, null, opaqueDarkNavBar));
+ assertEquals(0, PhoneWindowManager.updateLightNavigationBarLw(
+ SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR, opaqueDarkNavBar, opaqueDarkNavBar, null,
+ opaqueDarkNavBar));
+ assertEquals(SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR,
+ PhoneWindowManager.updateLightNavigationBarLw(0, opaqueLightNavBar,
+ opaqueLightNavBar, null, opaqueLightNavBar));
+ assertEquals(SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR,
+ PhoneWindowManager.updateLightNavigationBarLw(SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR,
+ opaqueLightNavBar, opaqueLightNavBar, null, opaqueLightNavBar));
+
+ // Dimming window clears SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.
+ assertEquals(0, PhoneWindowManager.updateLightNavigationBarLw(
+ 0, opaqueDarkNavBar, dimming, null, dimming));
+ assertEquals(0, PhoneWindowManager.updateLightNavigationBarLw(
+ 0, opaqueLightNavBar, dimming, null, dimming));
+ assertEquals(0, PhoneWindowManager.updateLightNavigationBarLw(
+ SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR, opaqueDarkNavBar, dimming, null, dimming));
+ assertEquals(0, PhoneWindowManager.updateLightNavigationBarLw(
+ SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR, opaqueLightNavBar, dimming, null, dimming));
+ assertEquals(0, PhoneWindowManager.updateLightNavigationBarLw(
+ SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR, opaqueLightNavBar, dimming, imeDrawLightNavBar,
+ dimming));
+
+ // IME window clears SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
+ assertEquals(0, PhoneWindowManager.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, PhoneWindowManager.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,
+ PhoneWindowManager.updateLightNavigationBarLw(0, opaqueDarkNavBar,
+ opaqueDarkNavBar, imeDrawLightNavBar, imeDrawLightNavBar));
+ }
+
+ @Test
+ public void testIsDockSideAllowedDockTop() throws Exception {
+ // Docked top is always allowed
+ assertTrue(PhoneWindowManager.isDockSideAllowed(DOCKED_TOP, DOCKED_LEFT, NAV_BAR_BOTTOM,
+ true /* navigationBarCanMove */));
+ assertTrue(PhoneWindowManager.isDockSideAllowed(DOCKED_TOP, DOCKED_LEFT, NAV_BAR_BOTTOM,
+ false /* navigationBarCanMove */));
+ }
+
+ @Test
+ public void testIsDockSideAllowedDockBottom() throws Exception {
+ // Cannot dock bottom
+ assertFalse(PhoneWindowManager.isDockSideAllowed(DOCKED_BOTTOM, DOCKED_LEFT, NAV_BAR_BOTTOM,
+ true /* navigationBarCanMove */));
+ }
+
+ @Test
+ public void testIsDockSideAllowedNavigationBarMovable() throws Exception {
+ assertFalse(PhoneWindowManager.isDockSideAllowed(DOCKED_LEFT, DOCKED_LEFT, NAV_BAR_BOTTOM,
+ true /* navigationBarCanMove */));
+ assertFalse(PhoneWindowManager.isDockSideAllowed(DOCKED_LEFT, DOCKED_LEFT, NAV_BAR_LEFT,
+ true /* navigationBarCanMove */));
+ assertTrue(PhoneWindowManager.isDockSideAllowed(DOCKED_LEFT, DOCKED_LEFT, NAV_BAR_RIGHT,
+ true /* navigationBarCanMove */));
+ assertFalse(PhoneWindowManager.isDockSideAllowed(DOCKED_RIGHT, DOCKED_LEFT, NAV_BAR_BOTTOM,
+ true /* navigationBarCanMove */));
+ assertFalse(PhoneWindowManager.isDockSideAllowed(DOCKED_RIGHT, DOCKED_LEFT, NAV_BAR_RIGHT,
+ true /* navigationBarCanMove */));
+ assertTrue(PhoneWindowManager.isDockSideAllowed(DOCKED_RIGHT, DOCKED_LEFT, NAV_BAR_LEFT,
+ true /* navigationBarCanMove */));
+ }
+
+ @Test
+ public void testIsDockSideAllowedNavigationBarNotMovable() throws Exception {
+ // Navigation bar is not movable such as tablets
+ assertTrue(PhoneWindowManager.isDockSideAllowed(DOCKED_LEFT, DOCKED_LEFT, NAV_BAR_BOTTOM,
+ false /* navigationBarCanMove */));
+ assertTrue(PhoneWindowManager.isDockSideAllowed(DOCKED_LEFT, DOCKED_TOP, NAV_BAR_BOTTOM,
+ false /* navigationBarCanMove */));
+ assertFalse(PhoneWindowManager.isDockSideAllowed(DOCKED_LEFT, DOCKED_RIGHT, NAV_BAR_BOTTOM,
+ false /* navigationBarCanMove */));
+ assertFalse(PhoneWindowManager.isDockSideAllowed(DOCKED_RIGHT, DOCKED_LEFT, NAV_BAR_BOTTOM,
+ false /* navigationBarCanMove */));
+ assertFalse(PhoneWindowManager.isDockSideAllowed(DOCKED_RIGHT, DOCKED_TOP, NAV_BAR_BOTTOM,
+ false /* navigationBarCanMove */));
+ assertTrue(PhoneWindowManager.isDockSideAllowed(DOCKED_RIGHT, DOCKED_RIGHT, NAV_BAR_BOTTOM,
+ false /* navigationBarCanMove */));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/policy/PhoneWindowManagerTestBase.java b/services/tests/servicestests/src/com/android/server/policy/PhoneWindowManagerTestBase.java
new file mode 100644
index 0000000..2c47a94
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/policy/PhoneWindowManagerTestBase.java
@@ -0,0 +1,258 @@
+/*
+ * 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.policy;
+
+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 android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
+import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
+
+import static com.android.server.wm.utils.CoordinateTransforms.transformPhysicalToLogicalCoordinates;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+
+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.Path;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.os.UserHandle;
+import android.support.test.InstrumentationRegistry;
+import android.testing.TestableResources;
+import android.util.Pair;
+import android.view.Display;
+import android.view.DisplayCutout;
+import android.view.DisplayInfo;
+import android.view.Gravity;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.IAccessibilityManager;
+
+import com.android.server.policy.keyguard.KeyguardServiceDelegate;
+import com.android.server.wm.DisplayFrames;
+import com.android.server.wm.utils.WmDisplayCutout;
+
+import org.junit.Before;
+
+public class PhoneWindowManagerTestBase {
+ static final int DISPLAY_WIDTH = 500;
+ static final int DISPLAY_HEIGHT = 1000;
+
+ static final int STATUS_BAR_HEIGHT = 10;
+ static final int NAV_BAR_HEIGHT = 15;
+ static final int DISPLAY_CUTOUT_HEIGHT = 8;
+
+ TestablePhoneWindowManager mPolicy;
+ TestContextWrapper mContext;
+ DisplayFrames mFrames;
+
+ FakeWindowState mStatusBar;
+ FakeWindowState mNavigationBar;
+ private boolean mHasDisplayCutout;
+ private int mRotation = ROTATION_0;
+
+ @Before
+ public void setUpBase() throws Exception {
+ mContext = new TestContextWrapper(InstrumentationRegistry.getTargetContext());
+ mContext.getResourceMocker().addOverride(
+ com.android.internal.R.dimen.status_bar_height_portrait, STATUS_BAR_HEIGHT);
+ mContext.getResourceMocker().addOverride(
+ com.android.internal.R.dimen.status_bar_height_landscape, STATUS_BAR_HEIGHT);
+ mContext.getResourceMocker().addOverride(
+ com.android.internal.R.dimen.navigation_bar_height, NAV_BAR_HEIGHT);
+ mContext.getResourceMocker().addOverride(
+ com.android.internal.R.dimen.navigation_bar_height_landscape, NAV_BAR_HEIGHT);
+ mContext.getResourceMocker().addOverride(
+ com.android.internal.R.dimen.navigation_bar_width, NAV_BAR_HEIGHT);
+
+ mPolicy = TestablePhoneWindowManager.create(mContext);
+
+ updateDisplayFrames();
+ }
+
+ public void setRotation(int rotation) {
+ mRotation = rotation;
+ updateDisplayFrames();
+ }
+
+ private void updateDisplayFrames() {
+ Pair<DisplayInfo, WmDisplayCutout> info = displayInfoAndCutoutForRotation(mRotation,
+ mHasDisplayCutout);
+ mFrames = new DisplayFrames(Display.DEFAULT_DISPLAY, info.first, info.second);
+ }
+
+ public void addStatusBar() {
+ mStatusBar = new FakeWindowState();
+ mStatusBar.attrs = new WindowManager.LayoutParams(MATCH_PARENT, STATUS_BAR_HEIGHT,
+ TYPE_STATUS_BAR, 0 /* flags */, PixelFormat.TRANSLUCENT);
+ mStatusBar.attrs.gravity = Gravity.TOP;
+
+ mPolicy.addWindow(mStatusBar);
+ mPolicy.mLastSystemUiFlags |= View.STATUS_BAR_TRANSPARENT;
+ }
+
+ public void addNavigationBar() {
+ mNavigationBar = new FakeWindowState();
+ mNavigationBar.attrs = new WindowManager.LayoutParams(MATCH_PARENT, NAV_BAR_HEIGHT,
+ TYPE_NAVIGATION_BAR, 0 /* flags */, PixelFormat.TRANSLUCENT);
+ mNavigationBar.attrs.gravity = Gravity.BOTTOM;
+
+ mPolicy.addWindow(mNavigationBar);
+ mPolicy.mHasNavigationBar = true;
+ mPolicy.mLastSystemUiFlags |= View.NAVIGATION_BAR_TRANSPARENT;
+ }
+
+ public void addDisplayCutout() {
+ mHasDisplayCutout = true;
+ updateDisplayFrames();
+ }
+
+ /** Asserts that {@code actual} is inset by the given amounts from the full display rect. */
+ public void assertInsetBy(Rect actual, int expectedInsetLeft, int expectedInsetTop,
+ int expectedInsetRight, int expectedInsetBottom) {
+ assertEquals(new Rect(expectedInsetLeft, expectedInsetTop,
+ mFrames.mDisplayWidth - expectedInsetRight,
+ mFrames.mDisplayHeight - expectedInsetBottom), actual);
+ }
+
+ /**
+ * 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.
+ */
+ public void assertInsetByTopBottom(Rect actual, int expectedInsetTop, int expectedInsetBottom) {
+ assertInsetBy(actual, 0, expectedInsetTop, 0, expectedInsetBottom);
+ }
+
+ public static DisplayInfo displayInfoForRotation(int rotation, boolean withDisplayCutout) {
+ return displayInfoAndCutoutForRotation(rotation, withDisplayCutout).first;
+ }
+ public static Pair<DisplayInfo, WmDisplayCutout> displayInfoAndCutoutForRotation(int rotation,
+ boolean withDisplayCutout) {
+ 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) {
+ Path p = new Path();
+ p.addRect(DISPLAY_WIDTH / 4, 0, DISPLAY_WIDTH * 3 / 4, DISPLAY_CUTOUT_HEIGHT,
+ Path.Direction.CCW);
+
+ Matrix m = new Matrix();
+ transformPhysicalToLogicalCoordinates(rotation, DISPLAY_WIDTH, DISPLAY_HEIGHT, m);
+ p.transform(m);
+
+ return DisplayCutout.fromBounds(p);
+ }
+
+ static class TestContextWrapper extends ContextWrapper {
+ private final TestableResources mResourceMocker;
+
+ public 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();
+ }
+
+ public TestableResources getResourceMocker() {
+ return mResourceMocker;
+ }
+ }
+
+ static class TestablePhoneWindowManager extends PhoneWindowManager {
+
+ public TestablePhoneWindowManager() {
+ }
+
+ @Override
+ void initializeHdmiState() {
+ // Do nothing.
+ }
+
+ @Override
+ Context getSystemUiContext() {
+ return mContext;
+ }
+
+ void addWindow(WindowState state) {
+ if (state instanceof FakeWindowState) {
+ ((FakeWindowState) state).surfaceLayer =
+ getWindowLayerFromTypeLw(state.getAttrs().type,
+ true /* canAddInternalSystemWindow */);
+ }
+ adjustWindowParamsLw(state, state.getAttrs(), true /* hasStatusBarPermission */);
+ assertEquals(WindowManagerGlobal.ADD_OKAY, prepareAddWindowLw(state, state.getAttrs()));
+ }
+
+ public static TestablePhoneWindowManager create(Context context) {
+ TestablePhoneWindowManager[] policy = new TestablePhoneWindowManager[1];
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ policy[0] = new TestablePhoneWindowManager();
+ policy[0].mContext = context;
+ policy[0].mKeyguardDelegate = mock(KeyguardServiceDelegate.class);
+ policy[0].mAccessibilityManager = new AccessibilityManager(context,
+ mock(IAccessibilityManager.class), UserHandle.USER_CURRENT);
+ policy[0].mSystemGestures = mock(SystemGesturesPointerEventListener.class);
+ policy[0].mNavigationBarCanMove = true;
+ policy[0].mPortraitRotation = ROTATION_0;
+ policy[0].mLandscapeRotation = ROTATION_90;
+ policy[0].mUpsideDownRotation = ROTATION_180;
+ policy[0].mSeascapeRotation = ROTATION_270;
+ policy[0].onConfigurationChanged();
+ });
+ return policy[0];
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/AnimatingAppWindowTokenRegistryTest.java b/services/tests/servicestests/src/com/android/server/wm/AnimatingAppWindowTokenRegistryTest.java
new file mode 100644
index 0000000..164c80b
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/AnimatingAppWindowTokenRegistryTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for the {@link TaskStack} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.wm.AnimatingAppWindowTokenRegistryTest
+ */
+@SmallTest
+@Presubmit
+@FlakyTest(detail = "Promote once confirmed non-flaky")
+@RunWith(AndroidJUnit4.class)
+public class AnimatingAppWindowTokenRegistryTest extends WindowTestsBase {
+
+ @Mock
+ AnimationAdapter mAdapter;
+
+ @Mock
+ Runnable mMockEndDeferFinishCallback1;
+ @Mock
+ Runnable mMockEndDeferFinishCallback2;
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void testDeferring() throws Exception {
+ final AppWindowToken window1 = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ final AppWindowToken window2 = createAppWindow(window1.getTask(), ACTIVITY_TYPE_STANDARD,
+ "window2").mAppToken;
+ final AnimatingAppWindowTokenRegistry registry =
+ window1.getStack().getAnimatingAppWindowTokenRegistry();
+
+ window1.startAnimation(window1.getPendingTransaction(), mAdapter, false /* hidden */);
+ window2.startAnimation(window1.getPendingTransaction(), mAdapter, false /* hidden */);
+ assertTrue(window1.isSelfAnimating());
+ assertTrue(window2.isSelfAnimating());
+
+ // Make sure that first animation finish is deferred, second one is not deferred, and first
+ // one gets cancelled.
+ assertTrue(registry.notifyAboutToFinish(window1, mMockEndDeferFinishCallback1));
+ assertFalse(registry.notifyAboutToFinish(window2, mMockEndDeferFinishCallback2));
+ verify(mMockEndDeferFinishCallback1).run();
+ verifyZeroInteractions(mMockEndDeferFinishCallback2);
+ }
+
+ @Test
+ public void testContainerRemoved() throws Exception {
+ final AppWindowToken window1 = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ final AppWindowToken window2 = createAppWindow(window1.getTask(), ACTIVITY_TYPE_STANDARD,
+ "window2").mAppToken;
+ final AnimatingAppWindowTokenRegistry registry =
+ window1.getStack().getAnimatingAppWindowTokenRegistry();
+
+ window1.startAnimation(window1.getPendingTransaction(), mAdapter, false /* hidden */);
+ window2.startAnimation(window1.getPendingTransaction(), mAdapter, false /* hidden */);
+ assertTrue(window1.isSelfAnimating());
+ assertTrue(window2.isSelfAnimating());
+
+ // Make sure that first animation finish is deferred, and removing the second window stops
+ // finishes all pending deferred finishings.
+ registry.notifyAboutToFinish(window1, mMockEndDeferFinishCallback1);
+ window2.setParent(null);
+ verify(mMockEndDeferFinishCallback1).run();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/servicestests/src/com/android/server/wm/AppTransitionTests.java
new file mode 100644
index 0000000..be7d781
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/AppTransitionTests.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.view.WindowManager.TRANSIT_ACTIVITY_OPEN;
+import static android.view.WindowManager.TRANSIT_CRASHING_ACTIVITY_CLOSE;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test class for {@link AppTransition}.
+ *
+ * atest AppTransitionTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AppTransitionTests {
+
+ @Rule
+ public final WindowManagerServiceRule mRule = new WindowManagerServiceRule();
+ private WindowManagerService mWm;
+
+ @Before
+ public void setUp() throws Exception {
+ final Context context = InstrumentationRegistry.getTargetContext();
+ mWm = mRule.getWindowManagerService();
+ }
+
+ @Test
+ public void testKeyguardOverride() throws Exception {
+ mWm.prepareAppTransition(TRANSIT_ACTIVITY_OPEN, false /* alwaysKeepCurrent */);
+ mWm.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY, false /* alwaysKeepCurrent */);
+ assertEquals(TRANSIT_KEYGUARD_GOING_AWAY, mWm.mAppTransition.getAppTransition());
+ }
+
+ @Test
+ public void testKeyguardKeep() throws Exception {
+ mWm.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY, false /* alwaysKeepCurrent */);
+ mWm.prepareAppTransition(TRANSIT_ACTIVITY_OPEN, false /* alwaysKeepCurrent */);
+ assertEquals(TRANSIT_KEYGUARD_GOING_AWAY, mWm.mAppTransition.getAppTransition());
+ }
+
+ @Test
+ public void testForceOverride() throws Exception {
+ mWm.prepareAppTransition(TRANSIT_KEYGUARD_UNOCCLUDE, false /* alwaysKeepCurrent */);
+ mWm.prepareAppTransition(TRANSIT_ACTIVITY_OPEN, false /* alwaysKeepCurrent */,
+ 0 /* flags */, true /* forceOverride */);
+ assertEquals(TRANSIT_ACTIVITY_OPEN, mWm.mAppTransition.getAppTransition());
+ }
+
+ @Test
+ public void testCrashing() throws Exception {
+ mWm.prepareAppTransition(TRANSIT_ACTIVITY_OPEN, false /* alwaysKeepCurrent */);
+ mWm.prepareAppTransition(TRANSIT_CRASHING_ACTIVITY_CLOSE, false /* alwaysKeepCurrent */);
+ assertEquals(TRANSIT_CRASHING_ACTIVITY_CLOSE, mWm.mAppTransition.getAppTransition());
+ }
+
+ @Test
+ public void testKeepKeyguard_withCrashing() throws Exception {
+ mWm.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY, false /* alwaysKeepCurrent */);
+ mWm.prepareAppTransition(TRANSIT_CRASHING_ACTIVITY_CLOSE, false /* alwaysKeepCurrent */);
+ assertEquals(TRANSIT_KEYGUARD_GOING_AWAY, mWm.mAppTransition.getAppTransition());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/AppWindowContainerControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/AppWindowContainerControllerTests.java
new file mode 100644
index 0000000..e0645b1
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/AppWindowContainerControllerTests.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import android.support.test.filters.FlakyTest;
+import org.junit.Test;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.res.Configuration.EMPTY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import com.android.server.wm.WindowTestUtils.TestTaskWindowContainerController;
+
+/**
+ * Test class for {@link AppWindowContainerController}.
+ *
+ * atest FrameworksServicesTests:com.android.server.wm.AppWindowContainerControllerTests
+ */
+@SmallTest
+@Presubmit
+@FlakyTest(bugId = 74078662)
+@org.junit.runner.RunWith(AndroidJUnit4.class)
+public class AppWindowContainerControllerTests extends WindowTestsBase {
+
+ @Test
+ public void testRemoveContainer() throws Exception {
+ final WindowTestUtils.TestAppWindowContainerController controller =
+ createAppWindowController();
+
+ // Assert token was added to display.
+ assertNotNull(mDisplayContent.getWindowToken(controller.mToken.asBinder()));
+ // Assert that the container was created and linked.
+ assertNotNull(controller.mContainer);
+
+ controller.removeContainer(mDisplayContent.getDisplayId());
+
+ // Assert token was remove from display.
+ assertNull(mDisplayContent.getWindowToken(controller.mToken.asBinder()));
+ // Assert that the container was removed.
+ assertNull(controller.mContainer);
+ }
+
+ @Test
+ public void testSetOrientation() throws Exception {
+ final WindowTestUtils.TestAppWindowContainerController controller =
+ createAppWindowController();
+
+ // Assert orientation is unspecified to start.
+ assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, controller.getOrientation());
+
+ controller.setOrientation(SCREEN_ORIENTATION_LANDSCAPE, mDisplayContent.getDisplayId(),
+ EMPTY /* displayConfig */, false /* freezeScreenIfNeeded */);
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, controller.getOrientation());
+
+ controller.removeContainer(mDisplayContent.getDisplayId());
+ // Assert orientation is unspecified to after container is removed.
+ assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, controller.getOrientation());
+
+ // Reset display frozen state
+ sWm.mDisplayFrozen = false;
+ }
+
+ private void assertHasStartingWindow(AppWindowToken atoken) {
+ assertNotNull(atoken.startingSurface);
+ assertNotNull(atoken.startingData);
+ assertNotNull(atoken.startingWindow);
+ }
+
+ private void assertNoStartingWindow(AppWindowToken atoken) {
+ assertNull(atoken.startingSurface);
+ assertNull(atoken.startingWindow);
+ assertNull(atoken.startingData);
+ atoken.forAllWindows(windowState -> {
+ assertFalse(windowState.getBaseType() == TYPE_APPLICATION_STARTING);
+ }, true);
+ }
+
+ @Test
+ public void testCreateRemoveStartingWindow() throws Exception {
+ final WindowTestUtils.TestAppWindowContainerController controller =
+ createAppWindowController();
+ controller.addStartingWindow(InstrumentationRegistry.getContext().getPackageName(),
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
+ false, false);
+ waitUntilHandlersIdle();
+ final AppWindowToken atoken = controller.getAppWindowToken(mDisplayContent);
+ assertHasStartingWindow(atoken);
+ controller.removeStartingWindow();
+ waitUntilHandlersIdle();
+ assertNoStartingWindow(atoken);
+ }
+
+ @Test
+ public void testAddRemoveRace() throws Exception {
+
+ // There was once a race condition between adding and removing starting windows
+ for (int i = 0; i < 1000; i++) {
+ final WindowTestUtils.TestAppWindowContainerController controller =
+ createAppWindowController();
+ controller.addStartingWindow(InstrumentationRegistry.getContext().getPackageName(),
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
+ false, false);
+ controller.removeStartingWindow();
+ waitUntilHandlersIdle();
+ assertNoStartingWindow(controller.getAppWindowToken(mDisplayContent));
+
+ controller.getAppWindowToken(mDisplayContent).getParent().getParent().removeImmediately();
+ }
+ }
+
+ @Test
+ public void testTransferStartingWindow() throws Exception {
+ final WindowTestUtils.TestAppWindowContainerController controller1 =
+ createAppWindowController();
+ final WindowTestUtils.TestAppWindowContainerController controller2 =
+ createAppWindowController();
+ controller1.addStartingWindow(InstrumentationRegistry.getContext().getPackageName(),
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
+ false, false);
+ waitUntilHandlersIdle();
+ controller2.addStartingWindow(InstrumentationRegistry.getContext().getPackageName(),
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0, controller1.mToken.asBinder(),
+ true, true, false, true, false, false);
+ waitUntilHandlersIdle();
+ assertNoStartingWindow(controller1.getAppWindowToken(mDisplayContent));
+ assertHasStartingWindow(controller2.getAppWindowToken(mDisplayContent));
+ }
+
+ @Test
+ public void testTransferStartingWindowWhileCreating() throws Exception {
+ final WindowTestUtils.TestAppWindowContainerController controller1 =
+ createAppWindowController();
+ final WindowTestUtils.TestAppWindowContainerController controller2 =
+ createAppWindowController();
+ ((TestWindowManagerPolicy) sWm.mPolicy).setRunnableWhenAddingSplashScreen(() -> {
+
+ // Surprise, ...! Transfer window in the middle of the creation flow.
+ controller2.addStartingWindow(InstrumentationRegistry.getContext().getPackageName(),
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0, controller1.mToken.asBinder(),
+ true, true, false, true, false, false);
+ });
+ controller1.addStartingWindow(InstrumentationRegistry.getContext().getPackageName(),
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
+ false, false);
+ waitUntilHandlersIdle();
+ assertNoStartingWindow(controller1.getAppWindowToken(mDisplayContent));
+ assertHasStartingWindow(controller2.getAppWindowToken(mDisplayContent));
+ }
+
+ @Test
+ public void testTryTransferStartingWindowFromHiddenAboveToken() throws Exception {
+
+ // Add two tasks on top of each other.
+ TestTaskWindowContainerController taskController =
+ new WindowTestUtils.TestTaskWindowContainerController(this);
+ final WindowTestUtils.TestAppWindowContainerController controllerTop =
+ createAppWindowController(taskController);
+ final WindowTestUtils.TestAppWindowContainerController controllerBottom =
+ createAppWindowController(taskController);
+
+ // Add a starting window.
+ controllerTop.addStartingWindow(InstrumentationRegistry.getContext().getPackageName(),
+ android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
+ false, false);
+ waitUntilHandlersIdle();
+
+ // Make the top one invisible, and try transfering the starting window from the top to the
+ // bottom one.
+ controllerTop.setVisibility(false, false);
+ controllerBottom.mContainer.transferStartingWindowFromHiddenAboveTokenIfNeeded();
+
+ // Assert that the bottom window now has the starting window.
+ assertNoStartingWindow(controllerTop.getAppWindowToken(mDisplayContent));
+ assertHasStartingWindow(controllerBottom.getAppWindowToken(mDisplayContent));
+ }
+
+ @Test
+ public void testReparent() throws Exception {
+ final StackWindowController stackController =
+ createStackControllerOnDisplay(mDisplayContent);
+ final WindowTestUtils.TestTaskWindowContainerController taskController1 =
+ new WindowTestUtils.TestTaskWindowContainerController(stackController);
+ final WindowTestUtils.TestAppWindowContainerController appWindowController1 =
+ createAppWindowController(taskController1);
+ final WindowTestUtils.TestTaskWindowContainerController taskController2 =
+ new WindowTestUtils.TestTaskWindowContainerController(stackController);
+ final WindowTestUtils.TestAppWindowContainerController appWindowController2 =
+ createAppWindowController(taskController2);
+ final WindowTestUtils.TestTaskWindowContainerController taskController3 =
+ new WindowTestUtils.TestTaskWindowContainerController(stackController);
+
+ try {
+ appWindowController1.reparent(taskController1, 0);
+ fail("Should not be able to reparent to the same parent");
+ } catch (IllegalArgumentException e) {
+ // Expected
+ }
+
+ try {
+ taskController3.setContainer(null);
+ appWindowController1.reparent(taskController3, 0);
+ fail("Should not be able to reparent to a task that doesn't have a container");
+ } catch (IllegalArgumentException e) {
+ // Expected
+ }
+
+ // Reparent the app window and ensure that it is moved
+ appWindowController1.reparent(taskController2, 0);
+ assertEquals(taskController2.mContainer, appWindowController1.mContainer.getParent());
+ assertEquals(0, ((WindowTestUtils.TestAppWindowToken) appWindowController1.mContainer)
+ .positionInParent());
+ assertEquals(1, ((WindowTestUtils.TestAppWindowToken) appWindowController2.mContainer)
+ .positionInParent());
+ }
+
+ private WindowTestUtils.TestAppWindowContainerController createAppWindowController() {
+ return createAppWindowController(
+ new WindowTestUtils.TestTaskWindowContainerController(this));
+ }
+
+ private WindowTestUtils.TestAppWindowContainerController createAppWindowController(
+ WindowTestUtils.TestTaskWindowContainerController taskController) {
+ return new WindowTestUtils.TestAppWindowContainerController(taskController);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/AppWindowTokenTests.java b/services/tests/servicestests/src/com/android/server/wm/AppWindowTokenTests.java
new file mode 100644
index 0000000..f6599dc
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/AppWindowTokenTests.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
+import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
+import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
+import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
+import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static android.view.WindowManager.TRANSIT_UNSET;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+/**
+ * Tests for the {@link AppWindowToken} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.wm.AppWindowTokenTests
+ */
+@SmallTest
+// TODO: b/68267650
+// @Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AppWindowTokenTests extends WindowTestsBase {
+
+ TaskStack mStack;
+ Task mTask;
+ WindowTestUtils.TestAppWindowToken mToken;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mStack = createTaskStackOnDisplay(mDisplayContent);
+ mTask = createTaskInStack(mStack, 0 /* userId */);
+ mToken = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+
+ mTask.addChild(mToken, 0);
+ }
+
+ @Test
+ @Presubmit
+ public void testAddWindow_Order() throws Exception {
+ assertEquals(0, mToken.getWindowsCount());
+
+ final WindowState win1 = createWindow(null, TYPE_APPLICATION, mToken, "win1");
+ final WindowState startingWin = createWindow(null, TYPE_APPLICATION_STARTING, mToken,
+ "startingWin");
+ final WindowState baseWin = createWindow(null, TYPE_BASE_APPLICATION, mToken, "baseWin");
+ final WindowState win4 = createWindow(null, TYPE_APPLICATION, mToken, "win4");
+
+ // Should not contain the windows that were added above.
+ assertEquals(4, mToken.getWindowsCount());
+ assertTrue(mToken.hasWindow(win1));
+ assertTrue(mToken.hasWindow(startingWin));
+ assertTrue(mToken.hasWindow(baseWin));
+ assertTrue(mToken.hasWindow(win4));
+
+ // The starting window should be on-top of all other windows.
+ assertEquals(startingWin, mToken.getLastChild());
+
+ // The base application window should be below all other windows.
+ assertEquals(baseWin, mToken.getFirstChild());
+ mToken.removeImmediately();
+ }
+
+ @Test
+ @Presubmit
+ public void testFindMainWindow() throws Exception {
+ assertNull(mToken.findMainWindow());
+
+ final WindowState window1 = createWindow(null, TYPE_BASE_APPLICATION, mToken, "window1");
+ final WindowState window11 = createWindow(window1, FIRST_SUB_WINDOW, mToken, "window11");
+ final WindowState window12 = createWindow(window1, FIRST_SUB_WINDOW, mToken, "window12");
+ assertEquals(window1, mToken.findMainWindow());
+ window1.mAnimatingExit = true;
+ assertEquals(window1, mToken.findMainWindow());
+ final WindowState window2 = createWindow(null, TYPE_APPLICATION_STARTING, mToken, "window2");
+ assertEquals(window2, mToken.findMainWindow());
+ mToken.removeImmediately();
+ }
+
+ @Test
+ @Presubmit
+ public void testGetTopFullscreenWindow() throws Exception {
+ assertNull(mToken.getTopFullscreenWindow());
+
+ final WindowState window1 = createWindow(null, TYPE_BASE_APPLICATION, mToken, "window1");
+ final WindowState window11 = createWindow(null, TYPE_APPLICATION, mToken, "window11");
+ final WindowState window12 = createWindow(null, TYPE_APPLICATION, mToken, "window12");
+ assertEquals(window12, mToken.getTopFullscreenWindow());
+ window12.mAttrs.width = 500;
+ assertEquals(window11, mToken.getTopFullscreenWindow());
+ window11.mAttrs.width = 500;
+ assertEquals(window1, mToken.getTopFullscreenWindow());
+ mToken.removeImmediately();
+ }
+
+ @Test
+ public void testLandscapeSeascapeRotationByApp() throws Exception {
+ // Some plumbing to get the service ready for rotation updates.
+ sWm.mDisplayReady = true;
+ sWm.mDisplayEnabled = true;
+
+ final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(
+ TYPE_BASE_APPLICATION);
+ attrs.setTitle("AppWindow");
+ final WindowTestUtils.TestWindowState appWindow = createWindowState(attrs, mToken);
+ mToken.addWindow(appWindow);
+
+ // Set initial orientation and update.
+ mToken.setOrientation(SCREEN_ORIENTATION_LANDSCAPE);
+ sWm.updateOrientationFromAppTokens(mDisplayContent.getOverrideConfiguration(), null,
+ mDisplayContent.getDisplayId());
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, mDisplayContent.getLastOrientation());
+ appWindow.resizeReported = false;
+
+ // Update the orientation to perform 180 degree rotation and check that resize was reported.
+ mToken.setOrientation(SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
+ sWm.updateOrientationFromAppTokens(mDisplayContent.getOverrideConfiguration(), null,
+ mDisplayContent.getDisplayId());
+ sWm.mRoot.performSurfacePlacement(false /* recoveringMemory */);
+ assertEquals(SCREEN_ORIENTATION_REVERSE_LANDSCAPE, mDisplayContent.getLastOrientation());
+ assertTrue(appWindow.resizeReported);
+ appWindow.removeImmediately();
+ }
+
+ @Test
+ public void testLandscapeSeascapeRotationByPolicy() throws Exception {
+ // Some plumbing to get the service ready for rotation updates.
+ sWm.mDisplayReady = true;
+ sWm.mDisplayEnabled = true;
+
+ final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(
+ TYPE_BASE_APPLICATION);
+ attrs.setTitle("AppWindow");
+ final WindowTestUtils.TestWindowState appWindow = createWindowState(attrs, mToken);
+ mToken.addWindow(appWindow);
+
+ // Set initial orientation and update.
+ performRotation(Surface.ROTATION_90);
+ appWindow.resizeReported = false;
+
+ // Update the rotation to perform 180 degree rotation and check that resize was reported.
+ performRotation(Surface.ROTATION_270);
+ assertTrue(appWindow.resizeReported);
+ appWindow.removeImmediately();
+ }
+
+ private void performRotation(int rotationToReport) {
+ ((TestWindowManagerPolicy) sWm.mPolicy).rotationToReport = rotationToReport;
+ sWm.updateRotation(false, false);
+ // Simulate animator finishing orientation change
+ sWm.mRoot.mOrientationChangeComplete = true;
+ sWm.mRoot.performSurfacePlacement(false /* recoveringMemory */);
+ }
+
+ @Test
+ @Presubmit
+ public void testGetOrientation() throws Exception {
+ mToken.setOrientation(SCREEN_ORIENTATION_LANDSCAPE);
+
+ mToken.setFillsParent(false);
+ // Can specify orientation if app doesn't fill parent.
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, mToken.getOrientation());
+
+ mToken.setFillsParent(true);
+ mToken.setHidden(true);
+ mToken.sendingToBottom = true;
+ // Can not specify orientation if app isn't visible even though it fills parent.
+ assertEquals(SCREEN_ORIENTATION_UNSET, mToken.getOrientation());
+ // Can specify orientation if the current orientation candidate is orientation behind.
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, mToken.getOrientation(SCREEN_ORIENTATION_BEHIND));
+ }
+
+ @Test
+ @Presubmit
+ public void testKeyguardFlagsDuringRelaunch() throws Exception {
+ final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(
+ TYPE_BASE_APPLICATION);
+ attrs.flags |= FLAG_SHOW_WHEN_LOCKED | FLAG_DISMISS_KEYGUARD;
+ attrs.setTitle("AppWindow");
+ final WindowTestUtils.TestWindowState appWindow = createWindowState(attrs, mToken);
+
+ // Add window with show when locked flag
+ mToken.addWindow(appWindow);
+ assertTrue(mToken.containsShowWhenLockedWindow() && mToken.containsDismissKeyguardWindow());
+
+ // Start relaunching
+ mToken.startRelaunching();
+ assertTrue(mToken.containsShowWhenLockedWindow() && mToken.containsDismissKeyguardWindow());
+
+ // Remove window and make sure that we still report back flag
+ mToken.removeChild(appWindow);
+ assertTrue(mToken.containsShowWhenLockedWindow() && mToken.containsDismissKeyguardWindow());
+
+ // Finish relaunching and ensure flag is now not reported
+ mToken.finishRelaunching();
+ assertFalse(mToken.containsShowWhenLockedWindow() || mToken.containsDismissKeyguardWindow());
+ }
+
+ @Test
+ @FlakyTest(detail = "Promote once confirmed non-flaky")
+ public void testStuckExitingWindow() throws Exception {
+ final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
+ "closingWindow");
+ closingWindow.mAnimatingExit = true;
+ closingWindow.mRemoveOnExit = true;
+ closingWindow.mAppToken.setVisibility(null, false /* visible */, TRANSIT_UNSET,
+ true /* performLayout */, false /* isVoiceInteraction */);
+
+ // We pretended that we were running an exit animation, but that should have been cleared up
+ // by changing visibility of AppWindowToken
+ closingWindow.removeIfPossible();
+ assertTrue(closingWindow.mRemoved);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/BoundsAnimationControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/BoundsAnimationControllerTests.java
new file mode 100644
index 0000000..ff631e7
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/BoundsAnimationControllerTests.java
@@ -0,0 +1,595 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static com.android.server.wm.BoundsAnimationController.NO_PIP_MODE_CHANGED_CALLBACKS;
+import static com.android.server.wm.BoundsAnimationController.SCHEDULE_PIP_MODE_CHANGED_ON_END;
+import static com.android.server.wm.BoundsAnimationController.SCHEDULE_PIP_MODE_CHANGED_ON_START;
+import static com.android.server.wm.BoundsAnimationController.SchedulePipModeChangedState;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.server.wm.WindowManagerInternal.AppTransitionListener;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.server.wm.BoundsAnimationController.BoundsAnimator;
+
+/**
+ * Test class for {@link BoundsAnimationController} to ensure that it sends the right callbacks
+ * depending on the various interactions.
+ *
+ * We are really concerned about only three of the transition states [F = fullscreen, !F = floating]
+ * F->!F, !F->!F, and !F->F. Each animation can only be cancelled from the target mid-transition,
+ * or if a new animation starts on the same target. The tests below verifies that the target is
+ * notified of all the cases where it is animating and cancelled so that it can respond
+ * appropriately.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.BoundsAnimationControllerTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class BoundsAnimationControllerTests extends WindowTestsBase {
+
+ /**
+ * Mock value animator to simulate updates with.
+ */
+ private class MockValueAnimator extends ValueAnimator {
+
+ private float mFraction;
+
+ public MockValueAnimator getWithValue(float fraction) {
+ mFraction = fraction;
+ return this;
+ }
+
+ @Override
+ public Object getAnimatedValue() {
+ return mFraction;
+ }
+ }
+
+ /**
+ * Mock app transition to fire notifications to the bounds animator.
+ */
+ private class MockAppTransition extends AppTransition {
+
+ private AppTransitionListener mListener;
+
+ MockAppTransition(Context context) {
+ super(context, null);
+ }
+
+ @Override
+ void registerListenerLocked(AppTransitionListener listener) {
+ mListener = listener;
+ }
+
+ public void notifyTransitionPending() {
+ mListener.onAppTransitionPendingLocked();
+ }
+
+ public void notifyTransitionCancelled(int transit) {
+ mListener.onAppTransitionCancelledLocked(transit);
+ }
+
+ public void notifyTransitionStarting(int transit) {
+ mListener.onAppTransitionStartingLocked(transit, null, null, 0, 0, 0);
+ }
+
+ public void notifyTransitionFinished() {
+ mListener.onAppTransitionFinishedLocked(null);
+ }
+ }
+
+ /**
+ * A test animate bounds user to track callbacks from the bounds animation.
+ */
+ private class TestBoundsAnimationTarget implements BoundsAnimationTarget {
+
+ boolean mAwaitingAnimationStart;
+ boolean mMovedToFullscreen;
+ boolean mAnimationStarted;
+ boolean mSchedulePipModeChangedOnStart;
+ boolean mForcePipModeChangedCallback;
+ boolean mAnimationEnded;
+ Rect mAnimationEndFinalStackBounds;
+ boolean mSchedulePipModeChangedOnEnd;
+ boolean mBoundsUpdated;
+ boolean mCancelRequested;
+ Rect mStackBounds;
+ Rect mTaskBounds;
+
+ void initialize(Rect from) {
+ mAwaitingAnimationStart = true;
+ mMovedToFullscreen = false;
+ mAnimationStarted = false;
+ mAnimationEnded = false;
+ mAnimationEndFinalStackBounds = null;
+ mForcePipModeChangedCallback = false;
+ mSchedulePipModeChangedOnStart = false;
+ mSchedulePipModeChangedOnEnd = false;
+ mStackBounds = from;
+ mTaskBounds = null;
+ mBoundsUpdated = false;
+ }
+
+ @Override
+ public void onAnimationStart(boolean schedulePipModeChangedCallback, boolean forceUpdate) {
+ mAwaitingAnimationStart = false;
+ mAnimationStarted = true;
+ mSchedulePipModeChangedOnStart = schedulePipModeChangedCallback;
+ mForcePipModeChangedCallback = forceUpdate;
+ }
+
+ @Override
+ public boolean shouldDeferStartOnMoveToFullscreen() {
+ return true;
+ }
+
+ @Override
+ public boolean setPinnedStackSize(Rect stackBounds, Rect taskBounds) {
+ // TODO: Once we break the runs apart, we should fail() here if this is called outside
+ // of onAnimationStart() and onAnimationEnd()
+ if (mCancelRequested) {
+ mCancelRequested = false;
+ return false;
+ } else {
+ mBoundsUpdated = true;
+ mStackBounds = stackBounds;
+ mTaskBounds = taskBounds;
+ return true;
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(boolean schedulePipModeChangedCallback, Rect finalStackBounds,
+ boolean moveToFullscreen) {
+ mAnimationEnded = true;
+ mAnimationEndFinalStackBounds = finalStackBounds;
+ mSchedulePipModeChangedOnEnd = schedulePipModeChangedCallback;
+ mMovedToFullscreen = moveToFullscreen;
+ mTaskBounds = null;
+ }
+ }
+
+ /**
+ * Drives the animations, makes common assertions along the way.
+ */
+ private class BoundsAnimationDriver {
+
+ private BoundsAnimationController mController;
+ private TestBoundsAnimationTarget mTarget;
+ private BoundsAnimator mAnimator;
+
+ private Rect mFrom;
+ private Rect mTo;
+ private Rect mLargerBounds;
+ private Rect mExpectedFinalBounds;
+
+ BoundsAnimationDriver(BoundsAnimationController controller,
+ TestBoundsAnimationTarget target) {
+ mController = controller;
+ mTarget = target;
+ }
+
+ BoundsAnimationDriver start(Rect from, Rect to) {
+ if (mAnimator != null) {
+ throw new IllegalArgumentException("Call restart() to restart an animation");
+ }
+
+ boolean fromFullscreen = from.equals(BOUNDS_FULL);
+ boolean toFullscreen = to.equals(BOUNDS_FULL);
+
+ mTarget.initialize(from);
+
+ // Started, not running
+ assertTrue(mTarget.mAwaitingAnimationStart);
+ assertTrue(!mTarget.mAnimationStarted);
+
+ startImpl(from, to);
+
+ // Ensure that the animator is paused for the all windows drawn signal when animating
+ // to/from fullscreen
+ if (fromFullscreen || toFullscreen) {
+ assertTrue(mAnimator.isPaused());
+ mController.onAllWindowsDrawn();
+ } else {
+ assertTrue(!mAnimator.isPaused());
+ }
+
+ // Started and running
+ assertTrue(!mTarget.mAwaitingAnimationStart);
+ assertTrue(mTarget.mAnimationStarted);
+
+ return this;
+ }
+
+ BoundsAnimationDriver restart(Rect to, boolean expectStartedAndPipModeChangedCallback) {
+ if (mAnimator == null) {
+ throw new IllegalArgumentException("Call start() to start a new animation");
+ }
+
+ BoundsAnimator oldAnimator = mAnimator;
+ boolean toSameBounds = mAnimator.isStarted() && to.equals(mTo);
+
+ // Reset the animation start state
+ mTarget.mAnimationStarted = false;
+
+ // Start animation
+ startImpl(mTarget.mStackBounds, to);
+
+ if (toSameBounds) {
+ // Same animator if same final bounds
+ assertSame(oldAnimator, mAnimator);
+ }
+
+ if (expectStartedAndPipModeChangedCallback) {
+ // Replacing animation with pending pip mode changed callback, ensure we update
+ assertTrue(mTarget.mAnimationStarted);
+ assertTrue(mTarget.mSchedulePipModeChangedOnStart);
+ assertTrue(mTarget.mForcePipModeChangedCallback);
+ } else {
+ // No animation start for replacing animation
+ assertTrue(!mTarget.mAnimationStarted);
+ }
+ mTarget.mAnimationStarted = true;
+ return this;
+ }
+
+ private BoundsAnimationDriver startImpl(Rect from, Rect to) {
+ boolean fromFullscreen = from.equals(BOUNDS_FULL);
+ boolean toFullscreen = to.equals(BOUNDS_FULL);
+ mFrom = new Rect(from);
+ mTo = new Rect(to);
+ mExpectedFinalBounds = new Rect(to);
+ mLargerBounds = getLargerBounds(mFrom, mTo);
+
+ // Start animation
+ final @SchedulePipModeChangedState int schedulePipModeChangedState = toFullscreen
+ ? SCHEDULE_PIP_MODE_CHANGED_ON_START
+ : fromFullscreen
+ ? SCHEDULE_PIP_MODE_CHANGED_ON_END
+ : NO_PIP_MODE_CHANGED_CALLBACKS;
+ mAnimator = mController.animateBoundsImpl(mTarget, from, to, DURATION,
+ schedulePipModeChangedState, fromFullscreen, toFullscreen);
+
+ // Original stack bounds, frozen task bounds
+ assertEquals(mFrom, mTarget.mStackBounds);
+ assertEqualSizeAtOffset(mLargerBounds, mTarget.mTaskBounds);
+
+ // Animating to larger size
+ if (mFrom.equals(mLargerBounds)) {
+ assertTrue(!mAnimator.animatingToLargerSize());
+ } else if (mTo.equals(mLargerBounds)) {
+ assertTrue(mAnimator.animatingToLargerSize());
+ }
+
+ return this;
+ }
+
+ BoundsAnimationDriver expectStarted(boolean schedulePipModeChanged) {
+ // Callback made
+ assertTrue(mTarget.mAnimationStarted);
+
+ assertEquals(schedulePipModeChanged, mTarget.mSchedulePipModeChangedOnStart);
+ return this;
+ }
+
+ BoundsAnimationDriver update(float t) {
+ mAnimator.onAnimationUpdate(mMockAnimator.getWithValue(t));
+
+ // Temporary stack bounds, frozen task bounds
+ if (t == 0f) {
+ assertEquals(mFrom, mTarget.mStackBounds);
+ } else if (t == 1f) {
+ assertEquals(mTo, mTarget.mStackBounds);
+ } else {
+ assertNotEquals(mFrom, mTarget.mStackBounds);
+ assertNotEquals(mTo, mTarget.mStackBounds);
+ }
+ assertEqualSizeAtOffset(mLargerBounds, mTarget.mTaskBounds);
+ return this;
+ }
+
+ BoundsAnimationDriver cancel() {
+ // Cancel
+ mTarget.mCancelRequested = true;
+ mTarget.mBoundsUpdated = false;
+ mExpectedFinalBounds = null;
+
+ // Update
+ mAnimator.onAnimationUpdate(mMockAnimator.getWithValue(0.5f));
+
+ // Not started, not running, cancel reset
+ assertTrue(!mTarget.mCancelRequested);
+
+ // Stack/task bounds not updated
+ assertTrue(!mTarget.mBoundsUpdated);
+
+ // Callback made
+ assertTrue(mTarget.mAnimationEnded);
+ assertNull(mTarget.mAnimationEndFinalStackBounds);
+
+ return this;
+ }
+
+ BoundsAnimationDriver end() {
+ mAnimator.end();
+
+ // Final stack bounds
+ assertEquals(mTo, mTarget.mStackBounds);
+ assertEquals(mExpectedFinalBounds, mTarget.mAnimationEndFinalStackBounds);
+ assertNull(mTarget.mTaskBounds);
+
+ return this;
+ }
+
+ BoundsAnimationDriver expectEnded(boolean schedulePipModeChanged,
+ boolean moveToFullscreen) {
+ // Callback made
+ assertTrue(mTarget.mAnimationEnded);
+
+ assertEquals(schedulePipModeChanged, mTarget.mSchedulePipModeChangedOnEnd);
+ assertEquals(moveToFullscreen, mTarget.mMovedToFullscreen);
+ return this;
+ }
+
+ private Rect getLargerBounds(Rect r1, Rect r2) {
+ int r1Area = r1.width() * r1.height();
+ int r2Area = r2.width() * r2.height();
+ if (r1Area <= r2Area) {
+ return r2;
+ } else {
+ return r1;
+ }
+ }
+ }
+
+ // Constants
+ private static final boolean SCHEDULE_PIP_MODE_CHANGED = true;
+ private static final boolean MOVE_TO_FULLSCREEN = true;
+ private static final int DURATION = 100;
+
+ // Some dummy bounds to represent fullscreen and floating bounds
+ private static final Rect BOUNDS_FULL = new Rect(0, 0, 100, 100);
+ private static final Rect BOUNDS_FLOATING = new Rect(60, 60, 95, 95);
+ private static final Rect BOUNDS_SMALLER_FLOATING = new Rect(80, 80, 95, 95);
+
+ // Common
+ private MockAppTransition mMockAppTransition;
+ private MockValueAnimator mMockAnimator;
+ private TestBoundsAnimationTarget mTarget;
+ private BoundsAnimationController mController;
+ private BoundsAnimationDriver mDriver;
+
+ // Temp
+ private Rect mTmpRect = new Rect();
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ final Context context = InstrumentationRegistry.getTargetContext();
+ final Handler handler = new Handler(Looper.getMainLooper());
+ mMockAppTransition = new MockAppTransition(context);
+ mMockAnimator = new MockValueAnimator();
+ mTarget = new TestBoundsAnimationTarget();
+ mController = new BoundsAnimationController(context, mMockAppTransition, handler, null);
+ mDriver = new BoundsAnimationDriver(mController, mTarget);
+ }
+
+ /** BASE TRANSITIONS **/
+
+ @UiThreadTest
+ @Test
+ public void testFullscreenToFloatingTransition() throws Exception {
+ mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING)
+ .expectStarted(!SCHEDULE_PIP_MODE_CHANGED)
+ .update(0f)
+ .update(0.5f)
+ .update(1f)
+ .end()
+ .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testFloatingToFullscreenTransition() throws Exception {
+ mDriver.start(BOUNDS_FLOATING, BOUNDS_FULL)
+ .expectStarted(SCHEDULE_PIP_MODE_CHANGED)
+ .update(0f)
+ .update(0.5f)
+ .update(1f)
+ .end()
+ .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, MOVE_TO_FULLSCREEN);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testFloatingToSmallerFloatingTransition() throws Exception {
+ mDriver.start(BOUNDS_FLOATING, BOUNDS_SMALLER_FLOATING)
+ .expectStarted(!SCHEDULE_PIP_MODE_CHANGED)
+ .update(0f)
+ .update(0.5f)
+ .update(1f)
+ .end()
+ .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testFloatingToLargerFloatingTransition() throws Exception {
+ mDriver.start(BOUNDS_SMALLER_FLOATING, BOUNDS_FLOATING)
+ .expectStarted(!SCHEDULE_PIP_MODE_CHANGED)
+ .update(0f)
+ .update(0.5f)
+ .update(1f)
+ .end()
+ .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN);
+ }
+
+ /** F->!F w/ CANCEL **/
+
+ @UiThreadTest
+ @Test
+ public void testFullscreenToFloatingCancelFromTarget() throws Exception {
+ mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING)
+ .expectStarted(!SCHEDULE_PIP_MODE_CHANGED)
+ .update(0.25f)
+ .cancel()
+ .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testFullscreenToFloatingCancelFromAnimationToSameBounds() throws Exception {
+ mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING)
+ .expectStarted(!SCHEDULE_PIP_MODE_CHANGED)
+ .update(0.25f)
+ .restart(BOUNDS_FLOATING, false /* expectStartedAndPipModeChangedCallback */)
+ .end()
+ .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testFullscreenToFloatingCancelFromAnimationToFloatingBounds() throws Exception {
+ mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING)
+ .expectStarted(!SCHEDULE_PIP_MODE_CHANGED)
+ .update(0.25f)
+ .restart(BOUNDS_SMALLER_FLOATING,
+ false /* expectStartedAndPipModeChangedCallback */)
+ .end()
+ .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testFullscreenToFloatingCancelFromAnimationToFullscreenBounds() throws Exception {
+ // When animating from fullscreen and the animation is interruped, we expect the animation
+ // start callback to be made, with a forced pip mode change callback
+ mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING)
+ .expectStarted(!SCHEDULE_PIP_MODE_CHANGED)
+ .update(0.25f)
+ .restart(BOUNDS_FULL, true /* expectStartedAndPipModeChangedCallback */)
+ .end()
+ .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, MOVE_TO_FULLSCREEN);
+ }
+
+ /** !F->F w/ CANCEL **/
+
+ @UiThreadTest
+ @Test
+ public void testFloatingToFullscreenCancelFromTarget() throws Exception {
+ mDriver.start(BOUNDS_FLOATING, BOUNDS_FULL)
+ .expectStarted(SCHEDULE_PIP_MODE_CHANGED)
+ .update(0.25f)
+ .cancel()
+ .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testFloatingToFullscreenCancelFromAnimationToSameBounds() throws Exception {
+ mDriver.start(BOUNDS_FLOATING, BOUNDS_FULL)
+ .expectStarted(SCHEDULE_PIP_MODE_CHANGED)
+ .update(0.25f)
+ .restart(BOUNDS_FULL, false /* expectStartedAndPipModeChangedCallback */)
+ .end()
+ .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, MOVE_TO_FULLSCREEN);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testFloatingToFullscreenCancelFromAnimationToFloatingBounds() throws Exception {
+ mDriver.start(BOUNDS_FLOATING, BOUNDS_FULL)
+ .expectStarted(SCHEDULE_PIP_MODE_CHANGED)
+ .update(0.25f)
+ .restart(BOUNDS_SMALLER_FLOATING,
+ false /* expectStartedAndPipModeChangedCallback */)
+ .end()
+ .expectEnded(SCHEDULE_PIP_MODE_CHANGED, MOVE_TO_FULLSCREEN);
+ }
+
+ /** !F->!F w/ CANCEL **/
+
+ @UiThreadTest
+ @Test
+ public void testFloatingToSmallerFloatingCancelFromTarget() throws Exception {
+ mDriver.start(BOUNDS_FLOATING, BOUNDS_SMALLER_FLOATING)
+ .expectStarted(!SCHEDULE_PIP_MODE_CHANGED)
+ .update(0.25f)
+ .cancel()
+ .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testFloatingToLargerFloatingCancelFromTarget() throws Exception {
+ mDriver.start(BOUNDS_SMALLER_FLOATING, BOUNDS_FLOATING)
+ .expectStarted(!SCHEDULE_PIP_MODE_CHANGED)
+ .update(0.25f)
+ .cancel()
+ .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN);
+ }
+
+ /** MISC **/
+
+ @UiThreadTest
+ @Test
+ public void testBoundsAreCopied() throws Exception {
+ Rect from = new Rect(0, 0, 100, 100);
+ Rect to = new Rect(25, 25, 75, 75);
+ mDriver.start(from, to)
+ .update(0.25f)
+ .end();
+ assertEquals(new Rect(0, 0, 100, 100), from);
+ assertEquals(new Rect(25, 25, 75, 75), to);
+ }
+
+ /**
+ * @return whether the task and stack bounds would be the same if they were at the same offset.
+ */
+ private boolean assertEqualSizeAtOffset(Rect stackBounds, Rect taskBounds) {
+ mTmpRect.set(taskBounds);
+ mTmpRect.offsetTo(stackBounds.left, stackBounds.top);
+ return stackBounds.equals(mTmpRect);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/ConfigurationContainerTests.java b/services/tests/servicestests/src/com/android/server/wm/ConfigurationContainerTests.java
new file mode 100644
index 0000000..192e156
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/ConfigurationContainerTests.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+import static android.content.res.Configuration.EMPTY;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.res.Configuration;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test class for {@link ConfigurationContainer}.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.ConfigurationContainerTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class ConfigurationContainerTests {
+
+ @Test
+ public void testConfigurationInit() throws Exception {
+ // Check root container initial config.
+ final TestConfigurationContainer root = new TestConfigurationContainer();
+ assertEquals(EMPTY, root.getOverrideConfiguration());
+ assertEquals(EMPTY, root.getMergedOverrideConfiguration());
+ assertEquals(EMPTY, root.getConfiguration());
+
+ // Check child initial config.
+ final TestConfigurationContainer child1 = root.addChild();
+ assertEquals(EMPTY, child1.getOverrideConfiguration());
+ assertEquals(EMPTY, child1.getMergedOverrideConfiguration());
+ assertEquals(EMPTY, child1.getConfiguration());
+
+ // Check child initial config if root has overrides.
+ final Configuration rootOverrideConfig = new Configuration();
+ rootOverrideConfig.fontScale = 1.3f;
+ root.onOverrideConfigurationChanged(rootOverrideConfig);
+ final TestConfigurationContainer child2 = root.addChild();
+ assertEquals(EMPTY, child2.getOverrideConfiguration());
+ assertEquals(rootOverrideConfig, child2.getMergedOverrideConfiguration());
+ assertEquals(rootOverrideConfig, child2.getConfiguration());
+
+ // Check child initial config if root has parent config set.
+ final Configuration rootParentConfig = new Configuration();
+ rootParentConfig.fontScale = 0.8f;
+ rootParentConfig.orientation = SCREEN_ORIENTATION_LANDSCAPE;
+ root.onConfigurationChanged(rootParentConfig);
+ final Configuration rootFullConfig = new Configuration(rootParentConfig);
+ rootFullConfig.updateFrom(rootOverrideConfig);
+
+ final TestConfigurationContainer child3 = root.addChild();
+ assertEquals(EMPTY, child3.getOverrideConfiguration());
+ assertEquals(rootOverrideConfig, child3.getMergedOverrideConfiguration());
+ assertEquals(rootFullConfig, child3.getConfiguration());
+ }
+
+ @Test
+ public void testConfigurationChangeOnAddRemove() throws Exception {
+ // Init root's config.
+ final TestConfigurationContainer root = new TestConfigurationContainer();
+ final Configuration rootOverrideConfig = new Configuration();
+ rootOverrideConfig.fontScale = 1.3f;
+ root.onOverrideConfigurationChanged(rootOverrideConfig);
+
+ // Init child's config.
+ final TestConfigurationContainer child = root.addChild();
+ final Configuration childOverrideConfig = new Configuration();
+ childOverrideConfig.densityDpi = 320;
+ child.onOverrideConfigurationChanged(childOverrideConfig);
+ final Configuration mergedOverrideConfig = new Configuration(root.getConfiguration());
+ mergedOverrideConfig.updateFrom(childOverrideConfig);
+
+ // Check configuration update when child is removed from parent.
+ root.removeChild(child);
+ assertEquals(childOverrideConfig, child.getOverrideConfiguration());
+ assertEquals(mergedOverrideConfig, child.getMergedOverrideConfiguration());
+ assertEquals(mergedOverrideConfig, child.getConfiguration());
+
+ // It may be paranoia... but let's check if parent's config didn't change after removal.
+ assertEquals(rootOverrideConfig, root.getOverrideConfiguration());
+ assertEquals(rootOverrideConfig, root.getMergedOverrideConfiguration());
+ assertEquals(rootOverrideConfig, root.getConfiguration());
+
+ // Init different root
+ final TestConfigurationContainer root2 = new TestConfigurationContainer();
+ final Configuration rootOverrideConfig2 = new Configuration();
+ rootOverrideConfig2.fontScale = 1.1f;
+ root2.onOverrideConfigurationChanged(rootOverrideConfig2);
+
+ // Check configuration update when child is added to different parent.
+ mergedOverrideConfig.setTo(rootOverrideConfig2);
+ mergedOverrideConfig.updateFrom(childOverrideConfig);
+ root2.addChild(child);
+ assertEquals(childOverrideConfig, child.getOverrideConfiguration());
+ assertEquals(mergedOverrideConfig, child.getMergedOverrideConfiguration());
+ assertEquals(mergedOverrideConfig, child.getConfiguration());
+ }
+
+ @Test
+ public void testConfigurationChangePropagation() throws Exception {
+ // Builds 3-level vertical hierarchy with one configuration container on each level.
+ // In addition to different overrides on each level, everyone in hierarchy will have one
+ // common overridden value - orientation;
+
+ // Init root's config.
+ final TestConfigurationContainer root = new TestConfigurationContainer();
+ final Configuration rootOverrideConfig = new Configuration();
+ rootOverrideConfig.fontScale = 1.3f;
+ rootOverrideConfig.orientation = SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+ root.onOverrideConfigurationChanged(rootOverrideConfig);
+
+ // Init children.
+ final TestConfigurationContainer child1 = root.addChild();
+ final Configuration childOverrideConfig1 = new Configuration();
+ childOverrideConfig1.densityDpi = 320;
+ childOverrideConfig1.orientation = SCREEN_ORIENTATION_LANDSCAPE;
+ child1.onOverrideConfigurationChanged(childOverrideConfig1);
+
+ final TestConfigurationContainer child2 = child1.addChild();
+ final Configuration childOverrideConfig2 = new Configuration();
+ childOverrideConfig2.screenWidthDp = 150;
+ childOverrideConfig2.orientation = SCREEN_ORIENTATION_PORTRAIT;
+ child2.onOverrideConfigurationChanged(childOverrideConfig2);
+
+ // Check configuration on all levels when root override is updated.
+ rootOverrideConfig.smallestScreenWidthDp = 200;
+ root.onOverrideConfigurationChanged(rootOverrideConfig);
+
+ final Configuration mergedOverrideConfig1 = new Configuration(rootOverrideConfig);
+ mergedOverrideConfig1.updateFrom(childOverrideConfig1);
+ final Configuration mergedConfig1 = new Configuration(mergedOverrideConfig1);
+
+ final Configuration mergedOverrideConfig2 = new Configuration(mergedOverrideConfig1);
+ mergedOverrideConfig2.updateFrom(childOverrideConfig2);
+ final Configuration mergedConfig2 = new Configuration(mergedOverrideConfig2);
+
+ assertEquals(rootOverrideConfig, root.getOverrideConfiguration());
+ assertEquals(rootOverrideConfig, root.getMergedOverrideConfiguration());
+ assertEquals(rootOverrideConfig, root.getConfiguration());
+
+ assertEquals(childOverrideConfig1, child1.getOverrideConfiguration());
+ assertEquals(mergedOverrideConfig1, child1.getMergedOverrideConfiguration());
+ assertEquals(mergedConfig1, child1.getConfiguration());
+
+ assertEquals(childOverrideConfig2, child2.getOverrideConfiguration());
+ assertEquals(mergedOverrideConfig2, child2.getMergedOverrideConfiguration());
+ assertEquals(mergedConfig2, child2.getConfiguration());
+
+ // Check configuration on all levels when root parent config is updated.
+ final Configuration rootParentConfig = new Configuration();
+ rootParentConfig.screenHeightDp = 100;
+ rootParentConfig.orientation = SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+ root.onConfigurationChanged(rootParentConfig);
+ final Configuration mergedRootConfig = new Configuration(rootParentConfig);
+ mergedRootConfig.updateFrom(rootOverrideConfig);
+
+ mergedConfig1.setTo(mergedRootConfig);
+ mergedConfig1.updateFrom(mergedOverrideConfig1);
+
+ mergedConfig2.setTo(mergedConfig1);
+ mergedConfig2.updateFrom(mergedOverrideConfig2);
+
+ assertEquals(rootOverrideConfig, root.getOverrideConfiguration());
+ assertEquals(rootOverrideConfig, root.getMergedOverrideConfiguration());
+ assertEquals(mergedRootConfig, root.getConfiguration());
+
+ assertEquals(childOverrideConfig1, child1.getOverrideConfiguration());
+ assertEquals(mergedOverrideConfig1, child1.getMergedOverrideConfiguration());
+ assertEquals(mergedConfig1, child1.getConfiguration());
+
+ assertEquals(childOverrideConfig2, child2.getOverrideConfiguration());
+ assertEquals(mergedOverrideConfig2, child2.getMergedOverrideConfiguration());
+ assertEquals(mergedConfig2, child2.getConfiguration());
+ }
+
+ @Test
+ public void testSetWindowingMode() throws Exception {
+ final TestConfigurationContainer root = new TestConfigurationContainer();
+ root.setWindowingMode(WINDOWING_MODE_UNDEFINED);
+ final TestConfigurationContainer child = root.addChild();
+ child.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ assertEquals(WINDOWING_MODE_UNDEFINED, root.getWindowingMode());
+ assertEquals(WINDOWING_MODE_FREEFORM, child.getWindowingMode());
+
+ root.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ assertEquals(WINDOWING_MODE_FULLSCREEN, root.getWindowingMode());
+ assertEquals(WINDOWING_MODE_FREEFORM, child.getWindowingMode());
+ }
+
+ @Test
+ public void testSetActivityType() throws Exception {
+ final TestConfigurationContainer root = new TestConfigurationContainer();
+ root.setActivityType(ACTIVITY_TYPE_UNDEFINED);
+ final TestConfigurationContainer child = root.addChild();
+ child.setActivityType(ACTIVITY_TYPE_STANDARD);
+ assertEquals(ACTIVITY_TYPE_UNDEFINED, root.getActivityType());
+ assertEquals(ACTIVITY_TYPE_STANDARD, child.getActivityType());
+
+ boolean gotException = false;
+ try {
+ // Can't change activity type once set.
+ child.setActivityType(ACTIVITY_TYPE_HOME);
+ } catch (IllegalStateException e) {
+ gotException = true;
+ }
+ assertTrue("Can't change activity type once set.", gotException);
+
+ // TODO: Commenting out for now until we figure-out a good way to test these rules that
+ // should only apply to system process.
+ /*
+ gotException = false;
+ try {
+ // Parent can't change child's activity type once set.
+ root.setActivityType(ACTIVITY_TYPE_HOME);
+ } catch (IllegalStateException e) {
+ gotException = true;
+ }
+ assertTrue("Parent can't change activity type once set.", gotException);
+ assertEquals(ACTIVITY_TYPE_HOME, root.getActivityType());
+
+ final TestConfigurationContainer child2 = new TestConfigurationContainer();
+ child2.setActivityType(ACTIVITY_TYPE_RECENTS);
+
+ gotException = false;
+ try {
+ // Can't re-parent to a different activity type.
+ root.addChild(child2);
+ } catch (IllegalStateException e) {
+ gotException = true;
+ }
+ assertTrue("Can't re-parent to a different activity type.", gotException);
+ */
+
+ }
+
+ @Test
+ public void testRegisterConfigurationChangeListener() throws Exception {
+ final TestConfigurationContainer container = new TestConfigurationContainer();
+ final TestConfigurationChangeListener listener = new TestConfigurationChangeListener();
+ final Configuration config = new Configuration();
+ config.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ config.windowConfiguration.setAppBounds(10, 10, 10, 10);
+ container.onOverrideConfigurationChanged(config);
+ container.registerConfigurationChangeListener(listener);
+ // Assert listener got the current config. of the container after it was registered.
+ assertEquals(config, listener.mOverrideConfiguration);
+ // Assert listener gets changes to override configuration.
+ container.onOverrideConfigurationChanged(EMPTY);
+ assertEquals(EMPTY, listener.mOverrideConfiguration);
+ }
+
+ /**
+ * Contains minimal implementation of {@link ConfigurationContainer}'s abstract behavior needed
+ * for testing.
+ */
+ private class TestConfigurationContainer
+ extends ConfigurationContainer<TestConfigurationContainer> {
+ private List<TestConfigurationContainer> mChildren = new ArrayList<>();
+ private TestConfigurationContainer mParent;
+
+ TestConfigurationContainer addChild(TestConfigurationContainer childContainer) {
+ childContainer.mParent = this;
+ childContainer.onParentChanged();
+ mChildren.add(childContainer);
+ return childContainer;
+ }
+
+ TestConfigurationContainer addChild() {
+ return addChild(new TestConfigurationContainer());
+ }
+
+ void removeChild(TestConfigurationContainer child) {
+ child.mParent = null;
+ child.onParentChanged();
+ }
+
+ @Override
+ protected int getChildCount() {
+ return mChildren.size();
+ }
+
+ @Override
+ protected TestConfigurationContainer getChildAt(int index) {
+ return mChildren.get(index);
+ }
+
+ @Override
+ protected ConfigurationContainer getParent() {
+ return mParent;
+ }
+ }
+
+ private class TestConfigurationChangeListener implements ConfigurationContainerListener {
+
+ final Configuration mOverrideConfiguration = new Configuration();
+
+ public void onOverrideConfigurationChanged(Configuration overrideConfiguration) {
+ mOverrideConfiguration.setTo(overrideConfiguration);
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/DimmerTests.java b/services/tests/servicestests/src/com/android/server/wm/DimmerTests.java
new file mode 100644
index 0000000..6769e40
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/DimmerTests.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.wm.DimmerTests;
+ */
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class DimmerTests extends WindowTestsBase {
+
+ private class TestWindowContainer extends WindowContainer<TestWindowContainer> {
+ final SurfaceControl mControl = mock(SurfaceControl.class);
+ final SurfaceControl.Transaction mTransaction = mock(SurfaceControl.Transaction.class);
+
+ TestWindowContainer() {
+ super(sWm);
+ }
+
+ @Override
+ public SurfaceControl getSurfaceControl() {
+ return mControl;
+ }
+
+ @Override
+ public SurfaceControl.Transaction getPendingTransaction() {
+ return mTransaction;
+ }
+ }
+
+ private class MockSurfaceBuildingContainer extends WindowContainer<TestWindowContainer> {
+ final SurfaceSession mSession = new SurfaceSession();
+ final SurfaceControl mHostControl = mock(SurfaceControl.class);
+ final SurfaceControl.Transaction mHostTransaction = mock(SurfaceControl.Transaction.class);
+
+ MockSurfaceBuildingContainer() {
+ super(sWm);
+ }
+
+ class MockSurfaceBuilder extends SurfaceControl.Builder {
+ MockSurfaceBuilder(SurfaceSession ss) {
+ super(ss);
+ }
+
+ @Override
+ public SurfaceControl build() {
+ return mock(SurfaceControl.class);
+ }
+ }
+
+ @Override
+ SurfaceControl.Builder makeChildSurface(WindowContainer child) {
+ return new MockSurfaceBuilder(mSession);
+ }
+
+ @Override
+ public SurfaceControl getSurfaceControl() {
+ return mHostControl;
+ }
+
+ @Override
+ public SurfaceControl.Transaction getPendingTransaction() {
+ return mHostTransaction;
+ }
+ }
+
+ private MockSurfaceBuildingContainer mHost;
+ private Dimmer mDimmer;
+ private SurfaceControl.Transaction mTransaction;
+ private Dimmer.SurfaceAnimatorStarter mSurfaceAnimatorStarter;
+
+ private static class SurfaceAnimatorStarterImpl implements Dimmer.SurfaceAnimatorStarter {
+ @Override
+ public void startAnimation(SurfaceAnimator surfaceAnimator, SurfaceControl.Transaction t,
+ AnimationAdapter anim, boolean hidden) {
+ surfaceAnimator.mAnimationFinishedCallback.run();
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mHost = new MockSurfaceBuildingContainer();
+ mSurfaceAnimatorStarter = spy(new SurfaceAnimatorStarterImpl());
+ mTransaction = mock(SurfaceControl.Transaction.class);
+ mDimmer = new Dimmer(mHost, mSurfaceAnimatorStarter);
+ }
+
+ @Test
+ public void testDimAboveNoChildCreatesSurface() throws Exception {
+ final float alpha = 0.8f;
+ mDimmer.dimAbove(mTransaction, alpha);
+
+ SurfaceControl dimLayer = getDimLayer();
+
+ assertNotNull("Dimmer should have created a surface", dimLayer);
+
+ verify(mTransaction).setAlpha(dimLayer, alpha);
+ verify(mTransaction).setLayer(dimLayer, Integer.MAX_VALUE);
+ }
+
+ @Test
+ public void testDimAboveNoChildRedundantlyUpdatesAlphaOnExistingSurface() throws Exception {
+ float alpha = 0.8f;
+ mDimmer.dimAbove(mTransaction, alpha);
+ final SurfaceControl firstSurface = getDimLayer();
+
+ alpha = 0.9f;
+ mDimmer.dimAbove(mTransaction, alpha);
+
+ assertEquals(firstSurface, getDimLayer());
+ verify(mTransaction).setAlpha(firstSurface, 0.9f);
+ }
+
+ @Test
+ public void testUpdateDimsAppliesSize() throws Exception {
+ mDimmer.dimAbove(mTransaction, 0.8f);
+
+ int width = 100;
+ int height = 300;
+ Rect bounds = new Rect(0, 0, width, height);
+ mDimmer.updateDims(mTransaction, bounds);
+
+ verify(mTransaction).setSize(getDimLayer(), width, height);
+ verify(mTransaction).show(getDimLayer());
+ }
+
+ @Test
+ public void testDimAboveNoChildNotReset() throws Exception {
+ mDimmer.dimAbove(mTransaction, 0.8f);
+ SurfaceControl dimLayer = getDimLayer();
+ mDimmer.resetDimStates();
+
+ mDimmer.updateDims(mTransaction, new Rect());
+ verify(mTransaction).show(getDimLayer());
+ verify(dimLayer, never()).destroy();
+ }
+
+ @Test
+ public void testDimAboveWithChildCreatesSurfaceAboveChild() throws Exception {
+ TestWindowContainer child = new TestWindowContainer();
+ mHost.addChild(child, 0);
+
+ final float alpha = 0.8f;
+ mDimmer.dimAbove(mTransaction, child, alpha);
+ SurfaceControl dimLayer = getDimLayer();
+
+ assertNotNull("Dimmer should have created a surface", dimLayer);
+
+ verify(mTransaction).setAlpha(dimLayer, alpha);
+ verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, 1);
+ }
+
+ @Test
+ public void testDimBelowWithChildSurfaceCreatesSurfaceBelowChild() throws Exception {
+ TestWindowContainer child = new TestWindowContainer();
+ mHost.addChild(child, 0);
+
+ final float alpha = 0.8f;
+ mDimmer.dimBelow(mTransaction, child, alpha);
+ SurfaceControl dimLayer = getDimLayer();
+
+ assertNotNull("Dimmer should have created a surface", dimLayer);
+
+ verify(mTransaction).setAlpha(dimLayer, alpha);
+ verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, -1);
+ }
+
+ @Test
+ public void testDimBelowWithChildSurfaceDestroyedWhenReset() throws Exception {
+ TestWindowContainer child = new TestWindowContainer();
+ mHost.addChild(child, 0);
+
+ final float alpha = 0.8f;
+ mDimmer.dimAbove(mTransaction, child, alpha);
+ SurfaceControl dimLayer = getDimLayer();
+ mDimmer.resetDimStates();
+
+ mDimmer.updateDims(mTransaction, new Rect());
+ verify(mSurfaceAnimatorStarter).startAnimation(any(SurfaceAnimator.class), any(
+ SurfaceControl.Transaction.class), any(AnimationAdapter.class), anyBoolean());
+ verify(dimLayer).destroy();
+ }
+
+ @Test
+ public void testDimBelowWithChildSurfaceNotDestroyedWhenPersisted() throws Exception {
+ TestWindowContainer child = new TestWindowContainer();
+ mHost.addChild(child, 0);
+
+ final float alpha = 0.8f;
+ mDimmer.dimAbove(mTransaction, child, alpha);
+ SurfaceControl dimLayer = getDimLayer();
+ mDimmer.resetDimStates();
+ mDimmer.dimAbove(mTransaction, child, alpha);
+
+ mDimmer.updateDims(mTransaction, new Rect());
+ verify(mTransaction).show(dimLayer);
+ verify(dimLayer, never()).destroy();
+ }
+
+ @Test
+ public void testDimUpdateWhileDimming() throws Exception {
+ Rect bounds = new Rect();
+ TestWindowContainer child = new TestWindowContainer();
+ mHost.addChild(child, 0);
+
+ final float alpha = 0.8f;
+ mDimmer.dimAbove(mTransaction, child, alpha);
+
+ SurfaceControl dimLayer = getDimLayer();
+ bounds.set(0, 0, 10, 10);
+ mDimmer.updateDims(mTransaction, bounds);
+ verify(mTransaction, times(1)).show(dimLayer);
+ verify(mTransaction).setSize(dimLayer, bounds.width(), bounds.height());
+ verify(mTransaction).setPosition(dimLayer, 0, 0);
+
+ bounds.set(10, 10, 30, 30);
+ mDimmer.updateDims(mTransaction, bounds);
+ verify(mTransaction).setSize(dimLayer, bounds.width(), bounds.height());
+ verify(mTransaction).setPosition(dimLayer, 10, 10);
+ }
+
+ @Test
+ public void testRemoveDimImmediately() throws Exception {
+ TestWindowContainer child = new TestWindowContainer();
+ mHost.addChild(child, 0);
+
+ mDimmer.dimAbove(mTransaction, child, 1);
+ SurfaceControl dimLayer = getDimLayer();
+ mDimmer.updateDims(mTransaction, new Rect());
+ verify(mTransaction, times(1)).show(dimLayer);
+
+ reset(mSurfaceAnimatorStarter);
+ mDimmer.dontAnimateExit();
+ mDimmer.resetDimStates();
+ mDimmer.updateDims(mTransaction, new Rect());
+ verify(mSurfaceAnimatorStarter, never()).startAnimation(any(SurfaceAnimator.class), any(
+ SurfaceControl.Transaction.class), any(AnimationAdapter.class), anyBoolean());
+ verify(mTransaction).destroy(dimLayer);
+ }
+
+ private SurfaceControl getDimLayer() {
+ return mDimmer.mDimState.mDimLayer;
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/servicestests/src/com/android/server/wm/DisplayContentTests.java
new file mode 100644
index 0000000..ac196f9
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/DisplayContentTests.java
@@ -0,0 +1,550 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.DisplayCutout.fromBoundingRect;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
+import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION;
+import static com.android.server.wm.WindowContainer.POSITION_TOP;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.annotation.SuppressLint;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.DisplayMetrics;
+import android.util.SparseIntArray;
+import android.view.DisplayCutout;
+import android.view.MotionEvent;
+import android.view.Surface;
+
+import com.android.server.wm.utils.WmDisplayCutout;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Tests for the {@link DisplayContent} class.
+ *
+ * Build/Install/Run:
+ * atest com.android.server.wm.DisplayContentTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class DisplayContentTests extends WindowTestsBase {
+
+ @Test
+ @FlakyTest(bugId = 77772044)
+ public void testForAllWindows() throws Exception {
+ final WindowState exitingAppWindow = createWindow(null, TYPE_BASE_APPLICATION,
+ mDisplayContent, "exiting app");
+ final AppWindowToken exitingAppToken = exitingAppWindow.mAppToken;
+ exitingAppToken.mIsExiting = true;
+ exitingAppToken.getTask().mStack.mExitingAppTokens.add(exitingAppToken);
+
+ assertForAllWindowsOrder(Arrays.asList(
+ mWallpaperWindow,
+ exitingAppWindow,
+ mChildAppWindowBelow,
+ mAppWindow,
+ mChildAppWindowAbove,
+ mDockedDividerWindow,
+ mStatusBarWindow,
+ mNavBarWindow,
+ mImeWindow,
+ mImeDialogWindow));
+ }
+
+ @Test
+ public void testForAllWindows_WithAppImeTarget() throws Exception {
+ final WindowState imeAppTarget =
+ createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, "imeAppTarget");
+
+ sWm.mInputMethodTarget = imeAppTarget;
+
+ assertForAllWindowsOrder(Arrays.asList(
+ mWallpaperWindow,
+ mChildAppWindowBelow,
+ mAppWindow,
+ mChildAppWindowAbove,
+ imeAppTarget,
+ mImeWindow,
+ mImeDialogWindow,
+ mDockedDividerWindow,
+ mStatusBarWindow,
+ mNavBarWindow));
+ }
+
+ @Test
+ public void testForAllWindows_WithChildWindowImeTarget() throws Exception {
+ sWm.mInputMethodTarget = mChildAppWindowAbove;
+
+ assertForAllWindowsOrder(Arrays.asList(
+ mWallpaperWindow,
+ mChildAppWindowBelow,
+ mAppWindow,
+ mChildAppWindowAbove,
+ mImeWindow,
+ mImeDialogWindow,
+ mDockedDividerWindow,
+ mStatusBarWindow,
+ mNavBarWindow));
+ }
+
+ @Test
+ public void testForAllWindows_WithStatusBarImeTarget() throws Exception {
+ sWm.mInputMethodTarget = mStatusBarWindow;
+
+ assertForAllWindowsOrder(Arrays.asList(
+ mWallpaperWindow,
+ mChildAppWindowBelow,
+ mAppWindow,
+ mChildAppWindowAbove,
+ mDockedDividerWindow,
+ mStatusBarWindow,
+ mImeWindow,
+ mImeDialogWindow,
+ mNavBarWindow));
+ }
+
+ @Test
+ public void testForAllWindows_WithInBetweenWindowToken() throws Exception {
+ // This window is set-up to be z-ordered between some windows that go in the same token like
+ // the nav bar and status bar.
+ final WindowState voiceInteractionWindow = createWindow(null, TYPE_VOICE_INTERACTION,
+ mDisplayContent, "voiceInteractionWindow");
+
+ assertForAllWindowsOrder(Arrays.asList(
+ mWallpaperWindow,
+ mChildAppWindowBelow,
+ mAppWindow,
+ mChildAppWindowAbove,
+ mDockedDividerWindow,
+ voiceInteractionWindow,
+ mStatusBarWindow,
+ mNavBarWindow,
+ mImeWindow,
+ mImeDialogWindow));
+ }
+
+ @Test
+ public void testComputeImeTarget() throws Exception {
+ // Verify that an app window can be an ime target.
+ final WindowState appWin = createWindow(null, TYPE_APPLICATION, mDisplayContent, "appWin");
+ appWin.setHasSurface(true);
+ assertTrue(appWin.canBeImeTarget());
+ WindowState imeTarget = mDisplayContent.computeImeTarget(false /* updateImeTarget */);
+ assertEquals(appWin, imeTarget);
+ appWin.mHidden = false;
+
+ // Verify that an child window can be an ime target.
+ final WindowState childWin = createWindow(appWin,
+ TYPE_APPLICATION_ATTACHED_DIALOG, "childWin");
+ childWin.setHasSurface(true);
+ assertTrue(childWin.canBeImeTarget());
+ imeTarget = mDisplayContent.computeImeTarget(false /* updateImeTarget */);
+ assertEquals(childWin, imeTarget);
+ }
+
+ /**
+ * This tests stack movement between displays and proper stack's, task's and app token's display
+ * container references updates.
+ */
+ @Test
+ public void testMoveStackBetweenDisplays() throws Exception {
+ // Create a second display.
+ final DisplayContent dc = createNewDisplay();
+
+ // Add stack with activity.
+ final TaskStack stack = createTaskStackOnDisplay(dc);
+ assertEquals(dc.getDisplayId(), stack.getDisplayContent().getDisplayId());
+ assertEquals(dc, stack.getParent().getParent());
+ assertEquals(dc, stack.getDisplayContent());
+
+ final Task task = createTaskInStack(stack, 0 /* userId */);
+ final WindowTestUtils.TestAppWindowToken token = WindowTestUtils.createTestAppWindowToken(
+ dc);
+ task.addChild(token, 0);
+ assertEquals(dc, task.getDisplayContent());
+ assertEquals(dc, token.getDisplayContent());
+
+ // Move stack to first display.
+ mDisplayContent.moveStackToDisplay(stack, true /* onTop */);
+ assertEquals(mDisplayContent.getDisplayId(), stack.getDisplayContent().getDisplayId());
+ assertEquals(mDisplayContent, stack.getParent().getParent());
+ assertEquals(mDisplayContent, stack.getDisplayContent());
+ assertEquals(mDisplayContent, task.getDisplayContent());
+ assertEquals(mDisplayContent, token.getDisplayContent());
+ }
+
+ /**
+ * This tests override configuration updates for display content.
+ */
+ @Test
+ public void testDisplayOverrideConfigUpdate() throws Exception {
+ final int displayId = mDisplayContent.getDisplayId();
+ final Configuration currentOverrideConfig = mDisplayContent.getOverrideConfiguration();
+
+ // Create new, slightly changed override configuration and apply it to the display.
+ final Configuration newOverrideConfig = new Configuration(currentOverrideConfig);
+ newOverrideConfig.densityDpi += 120;
+ newOverrideConfig.fontScale += 0.3;
+
+ sWm.setNewDisplayOverrideConfiguration(newOverrideConfig, displayId);
+
+ // Check that override config is applied.
+ assertEquals(newOverrideConfig, mDisplayContent.getOverrideConfiguration());
+ }
+
+ /**
+ * This tests global configuration updates when default display config is updated.
+ */
+ @Test
+ public void testDefaultDisplayOverrideConfigUpdate() throws Exception {
+ final Configuration currentConfig = mDisplayContent.getConfiguration();
+
+ // Create new, slightly changed override configuration and apply it to the display.
+ final Configuration newOverrideConfig = new Configuration(currentConfig);
+ newOverrideConfig.densityDpi += 120;
+ newOverrideConfig.fontScale += 0.3;
+
+ sWm.setNewDisplayOverrideConfiguration(newOverrideConfig, DEFAULT_DISPLAY);
+
+ // Check that global configuration is updated, as we've updated default display's config.
+ Configuration globalConfig = sWm.mRoot.getConfiguration();
+ assertEquals(newOverrideConfig.densityDpi, globalConfig.densityDpi);
+ assertEquals(newOverrideConfig.fontScale, globalConfig.fontScale, 0.1 /* delta */);
+
+ // Return back to original values.
+ sWm.setNewDisplayOverrideConfiguration(currentConfig, DEFAULT_DISPLAY);
+ globalConfig = sWm.mRoot.getConfiguration();
+ assertEquals(currentConfig.densityDpi, globalConfig.densityDpi);
+ assertEquals(currentConfig.fontScale, globalConfig.fontScale, 0.1 /* delta */);
+ }
+
+ /**
+ * Tests tapping on a stack in different display results in window gaining focus.
+ */
+ @Test
+ public void testInputEventBringsCorrectDisplayInFocus() throws Exception {
+ DisplayContent dc0 = sWm.getDefaultDisplayContentLocked();
+ // Create a second display
+ final DisplayContent dc1 = createNewDisplay();
+
+ // Add stack with activity.
+ final TaskStack stack0 = createTaskStackOnDisplay(dc0);
+ final Task task0 = createTaskInStack(stack0, 0 /* userId */);
+ final WindowTestUtils.TestAppWindowToken token =
+ WindowTestUtils.createTestAppWindowToken(dc0);
+ task0.addChild(token, 0);
+ dc0.mTapDetector = new TaskTapPointerEventListener(sWm, dc0);
+ sWm.registerPointerEventListener(dc0.mTapDetector);
+ final TaskStack stack1 = createTaskStackOnDisplay(dc1);
+ final Task task1 = createTaskInStack(stack1, 0 /* userId */);
+ final WindowTestUtils.TestAppWindowToken token1 =
+ WindowTestUtils.createTestAppWindowToken(dc0);
+ task1.addChild(token1, 0);
+ dc1.mTapDetector = new TaskTapPointerEventListener(sWm, dc0);
+ sWm.registerPointerEventListener(dc1.mTapDetector);
+
+ // tap on primary display (by sending ACTION_DOWN followed by ACTION_UP)
+ DisplayMetrics dm0 = dc0.getDisplayMetrics();
+ dc0.mTapDetector.onPointerEvent(
+ createTapEvent(dm0.widthPixels / 2, dm0.heightPixels / 2, true));
+ dc0.mTapDetector.onPointerEvent(
+ createTapEvent(dm0.widthPixels / 2, dm0.heightPixels / 2, false));
+
+ // Check focus is on primary display.
+ assertEquals(sWm.mCurrentFocus, dc0.findFocusedWindow());
+
+ // Tap on secondary display
+ DisplayMetrics dm1 = dc1.getDisplayMetrics();
+ dc1.mTapDetector.onPointerEvent(
+ createTapEvent(dm1.widthPixels / 2, dm1.heightPixels / 2, true));
+ dc1.mTapDetector.onPointerEvent(
+ createTapEvent(dm1.widthPixels / 2, dm1.heightPixels / 2, false));
+
+ // Check focus is on secondary.
+ assertEquals(sWm.mCurrentFocus, dc1.findFocusedWindow());
+ }
+
+ @Test
+ public void testFocusedWindowMultipleDisplays() throws Exception {
+ // Create a focusable window and check that focus is calculated correctly
+ final WindowState window1 =
+ createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, "window1");
+ assertEquals(window1, sWm.mRoot.computeFocusedWindow());
+
+ // Check that a new display doesn't affect focus
+ final DisplayContent dc = createNewDisplay();
+ assertEquals(window1, sWm.mRoot.computeFocusedWindow());
+
+ // Add a window to the second display, and it should be focused
+ final WindowState window2 = createWindow(null, TYPE_BASE_APPLICATION, dc, "window2");
+ assertEquals(window2, sWm.mRoot.computeFocusedWindow());
+
+ // Move the first window to the to including parents, and make sure focus is updated
+ window1.getParent().positionChildAt(POSITION_TOP, window1, true);
+ assertEquals(window1, sWm.mRoot.computeFocusedWindow());
+ }
+
+ @Test
+ public void testKeyguard_preventsSecondaryDisplayFocus() throws Exception {
+ final WindowState keyguard = createWindow(null, TYPE_STATUS_BAR,
+ sWm.getDefaultDisplayContentLocked(), "keyguard");
+ assertEquals(keyguard, sWm.mRoot.computeFocusedWindow());
+
+ // Add a window to a second display, and it should be focused
+ final DisplayContent dc = createNewDisplay();
+ final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, dc, "win");
+ assertEquals(win, sWm.mRoot.computeFocusedWindow());
+
+ mWmRule.getWindowManagerPolicy().keyguardShowingAndNotOccluded = true;
+ assertEquals(keyguard, sWm.mRoot.computeFocusedWindow());
+ }
+
+ /**
+ * This tests setting the maximum ui width on a display.
+ */
+ @Test
+ public void testMaxUiWidth() throws Exception {
+ final int baseWidth = 1440;
+ final int baseHeight = 2560;
+ final int baseDensity = 300;
+
+ mDisplayContent.updateBaseDisplayMetrics(baseWidth, baseHeight, baseDensity);
+
+ final int maxWidth = 300;
+ final int resultingHeight = (maxWidth * baseHeight) / baseWidth;
+ final int resultingDensity = (maxWidth * baseDensity) / baseWidth;
+
+ mDisplayContent.setMaxUiWidth(maxWidth);
+ verifySizes(mDisplayContent, maxWidth, resultingHeight, resultingDensity);
+
+ // Assert setting values again does not change;
+ mDisplayContent.updateBaseDisplayMetrics(baseWidth, baseHeight, baseDensity);
+ verifySizes(mDisplayContent, maxWidth, resultingHeight, resultingDensity);
+
+ final int smallerWidth = 200;
+ final int smallerHeight = 400;
+ final int smallerDensity = 100;
+
+ // Specify smaller dimension, verify that it is honored
+ mDisplayContent.updateBaseDisplayMetrics(smallerWidth, smallerHeight, smallerDensity);
+ verifySizes(mDisplayContent, smallerWidth, smallerHeight, smallerDensity);
+
+ // Verify that setting the max width to a greater value than the base width has no effect
+ mDisplayContent.setMaxUiWidth(maxWidth);
+ verifySizes(mDisplayContent, smallerWidth, smallerHeight, smallerDensity);
+ }
+
+ /**
+ * This test enforces that the pinned stack is always kept as the top stack.
+ */
+ @Test
+ public void testPinnedStackLocation() {
+ final TaskStack pinnedStack = createStackControllerOnStackOnDisplay(
+ WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD, mDisplayContent).mContainer;
+ // Ensure that the pinned stack is the top stack
+ assertEquals(pinnedStack, mDisplayContent.getPinnedStack());
+ assertEquals(pinnedStack, mDisplayContent.getTopStack());
+ // By default, this should try to create a new stack on top
+ final TaskStack otherStack = createTaskStackOnDisplay(mDisplayContent);
+ // Ensure that the other stack is on the display.
+ assertEquals(mDisplayContent, otherStack.getDisplayContent());
+ // Ensure that the pinned stack is still on top
+ assertEquals(pinnedStack, mDisplayContent.getTopStack());
+ }
+
+ /**
+ * Test that WM does not report displays to AM that are pending to be removed.
+ */
+ @Test
+ public void testDontReportDeferredRemoval() {
+ // Create a display and add an animating window to it.
+ final DisplayContent dc = createNewDisplay();
+ final WindowState window = createWindow(null /* parent */, TYPE_BASE_APPLICATION, dc, "w");
+ window.mAnimatingExit = true;
+ // Request display removal, it should be deferred.
+ dc.removeIfPossible();
+ // Request ordered display ids from WM.
+ final SparseIntArray orderedDisplayIds = new SparseIntArray();
+ sWm.getDisplaysInFocusOrder(orderedDisplayIds);
+ // Make sure that display that is marked for removal is not reported.
+ assertEquals(-1, orderedDisplayIds.indexOfValue(dc.getDisplayId()));
+ }
+
+ @Test
+ public void testDisplayCutout_rot0() throws Exception {
+ synchronized (sWm.getWindowManagerLock()) {
+ final DisplayContent dc = createNewDisplay();
+ dc.mInitialDisplayWidth = 200;
+ dc.mInitialDisplayHeight = 400;
+ Rect r = new Rect(80, 0, 120, 10);
+ final DisplayCutout cutout = new WmDisplayCutout(
+ fromBoundingRect(r.left, r.top, r.right, r.bottom), null)
+ .computeSafeInsets(200, 400).getDisplayCutout();
+
+ dc.mInitialDisplayCutout = cutout;
+ dc.setRotation(Surface.ROTATION_0);
+ dc.computeScreenConfiguration(new Configuration()); // recomputes dc.mDisplayInfo.
+
+ assertEquals(cutout, dc.getDisplayInfo().displayCutout);
+ }
+ }
+
+ @Test
+ public void testDisplayCutout_rot90() throws Exception {
+ synchronized (sWm.getWindowManagerLock()) {
+ final DisplayContent dc = createNewDisplay();
+ dc.mInitialDisplayWidth = 200;
+ dc.mInitialDisplayHeight = 400;
+ Rect r1 = new Rect(80, 0, 120, 10);
+ final DisplayCutout cutout = new WmDisplayCutout(
+ fromBoundingRect(r1.left, r1.top, r1.right, r1.bottom), null)
+ .computeSafeInsets(200, 400).getDisplayCutout();
+
+ dc.mInitialDisplayCutout = cutout;
+ dc.setRotation(Surface.ROTATION_90);
+ dc.computeScreenConfiguration(new Configuration()); // recomputes dc.mDisplayInfo.
+
+ final Rect r = new Rect(0, 80, 10, 120);
+ assertEquals(new WmDisplayCutout(
+ fromBoundingRect(r.left, r.top, r.right, r.bottom), null)
+ .computeSafeInsets(400, 200).getDisplayCutout(), dc.getDisplayInfo().displayCutout);
+ }
+ }
+
+ @Test
+ public void testLayoutSeq_assignedDuringLayout() throws Exception {
+ synchronized (sWm.getWindowManagerLock()) {
+
+ final DisplayContent dc = createNewDisplay();
+ final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, dc, "w");
+
+ dc.setLayoutNeeded();
+ dc.performLayout(true /* initial */, false /* updateImeWindows */);
+
+ assertThat(win.mLayoutSeq, is(dc.mLayoutSeq));
+ }
+ }
+
+ @Test
+ @SuppressLint("InlinedApi")
+ public void testOrientationDefinedByKeyguard() {
+ final DisplayContent dc = createNewDisplay();
+ // Create a window that requests landscape orientation. It will define device orientation
+ // by default.
+ final WindowState window = createWindow(null /* parent */, TYPE_BASE_APPLICATION, dc, "w");
+ window.mAppToken.setOrientation(SCREEN_ORIENTATION_LANDSCAPE);
+
+ final WindowState keyguard = createWindow(null, TYPE_STATUS_BAR, dc, "keyguard");
+ keyguard.mHasSurface = true;
+ keyguard.mAttrs.screenOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
+
+ assertEquals("Screen orientation must be defined by the app window by default",
+ SCREEN_ORIENTATION_LANDSCAPE, dc.getOrientation());
+
+ keyguard.mAttrs.screenOrientation = SCREEN_ORIENTATION_PORTRAIT;
+ assertEquals("Visible keyguard must influence device orientation",
+ SCREEN_ORIENTATION_PORTRAIT, dc.getOrientation());
+
+ sWm.setKeyguardGoingAway(true);
+ assertEquals("Keyguard that is going away must not influence device orientation",
+ SCREEN_ORIENTATION_LANDSCAPE, dc.getOrientation());
+ }
+
+ @Test
+ public void testDisableDisplayInfoOverrideFromWindowManager() {
+ final DisplayContent dc = createNewDisplay();
+
+ assertTrue(dc.mShouldOverrideDisplayConfiguration);
+ sWm.dontOverrideDisplayInfo(dc.getDisplayId());
+
+ assertFalse(dc.mShouldOverrideDisplayConfiguration);
+ verify(sWm.mDisplayManagerInternal, times(1))
+ .setDisplayInfoOverrideFromWindowManager(dc.getDisplayId(), null);
+ }
+
+ private static void verifySizes(DisplayContent displayContent, int expectedBaseWidth,
+ int expectedBaseHeight, int expectedBaseDensity) {
+ assertEquals(displayContent.mBaseDisplayWidth, expectedBaseWidth);
+ assertEquals(displayContent.mBaseDisplayHeight, expectedBaseHeight);
+ assertEquals(displayContent.mBaseDisplayDensity, expectedBaseDensity);
+ }
+
+ private void assertForAllWindowsOrder(List<WindowState> expectedWindowsBottomToTop) {
+ final LinkedList<WindowState> actualWindows = new LinkedList<>();
+
+ // Test forward traversal.
+ mDisplayContent.forAllWindows(actualWindows::addLast, false /* traverseTopToBottom */);
+ assertThat("bottomToTop", actualWindows, is(expectedWindowsBottomToTop));
+
+ actualWindows.clear();
+
+ // Test backward traversal.
+ mDisplayContent.forAllWindows(actualWindows::addLast, true /* traverseTopToBottom */);
+ assertThat("topToBottom", actualWindows, is(reverseList(expectedWindowsBottomToTop)));
+ }
+
+ private static List<WindowState> reverseList(List<WindowState> list) {
+ final ArrayList<WindowState> result = new ArrayList<>(list);
+ Collections.reverse(result);
+ return result;
+ }
+
+ private MotionEvent createTapEvent(float x, float y, boolean isDownEvent) {
+ final long downTime = SystemClock.uptimeMillis();
+ final long eventTime = SystemClock.uptimeMillis() + 100;
+ final int metaState = 0;
+
+ return MotionEvent.obtain(
+ downTime,
+ eventTime,
+ isDownEvent ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP,
+ x,
+ y,
+ metaState);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/DragDropControllerTests.java
new file mode 100644
index 0000000..a09656c
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/DragDropControllerTests.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.ClipData;
+import android.graphics.PixelFormat;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.os.UserManagerInternal;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.InputChannel;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+import android.view.View;
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.LocalServices;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for the {@link DragDropController} class.
+ *
+ * atest com.android.server.wm.DragDropControllerTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@Presubmit
+public class DragDropControllerTests extends WindowTestsBase {
+ private static final int TIMEOUT_MS = 3000;
+ private TestDragDropController mTarget;
+ private WindowState mWindow;
+ private IBinder mToken;
+
+ static class TestDragDropController extends DragDropController {
+ @GuardedBy("sWm.mWindowMap")
+ private Runnable mCloseCallback;
+
+ TestDragDropController(WindowManagerService service, Looper looper) {
+ super(service, looper);
+ }
+
+ void setOnClosedCallbackLocked(Runnable runnable) {
+ assertTrue(dragDropActiveLocked());
+ mCloseCallback = runnable;
+ }
+
+ @Override
+ void onDragStateClosedLocked(DragState dragState) {
+ super.onDragStateClosedLocked(dragState);
+ if (mCloseCallback != null) {
+ mCloseCallback.run();
+ mCloseCallback = null;
+ }
+ }
+ }
+
+ /**
+ * Creates a window state which can be used as a drop target.
+ */
+ private WindowState createDropTargetWindow(String name, int ownerId) {
+ final WindowTestUtils.TestAppWindowToken token = WindowTestUtils.createTestAppWindowToken(
+ mDisplayContent);
+ final TaskStack stack = createStackControllerOnStackOnDisplay(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, mDisplayContent).mContainer;
+ final Task task = createTaskInStack(stack, ownerId);
+ task.addChild(token, 0);
+
+ final WindowState window = createWindow(
+ null, TYPE_BASE_APPLICATION, token, name, ownerId, false);
+ window.mInputChannel = new InputChannel();
+ window.mHasSurface = true;
+ return window;
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ final UserManagerInternal userManager = mock(UserManagerInternal.class);
+ LocalServices.addService(UserManagerInternal.class, userManager);
+
+ super.setUp();
+
+ mTarget = new TestDragDropController(sWm, sWm.mH.getLooper());
+ mDisplayContent = spy(mDisplayContent);
+ mWindow = createDropTargetWindow("Drag test window", 0);
+ when(mDisplayContent.getTouchableWinAtPointLocked(0, 0)).thenReturn(mWindow);
+ when(sWm.mInputManager.transferTouchFocus(any(), any())).thenReturn(true);
+
+ synchronized (sWm.mWindowMap) {
+ sWm.mWindowMap.put(mWindow.mClient.asBinder(), mWindow);
+ }
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ LocalServices.removeServiceForTest(UserManagerInternal.class);
+ final CountDownLatch latch;
+ synchronized (sWm.mWindowMap) {
+ if (!mTarget.dragDropActiveLocked()) {
+ return;
+ }
+ if (mToken != null) {
+ mTarget.cancelDragAndDrop(mToken);
+ }
+ latch = new CountDownLatch(1);
+ mTarget.setOnClosedCallbackLocked(() -> {
+ latch.countDown();
+ });
+ }
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testDragFlow() throws Exception {
+ dragFlow(0, ClipData.newPlainText("label", "Test"), 0, 0);
+ }
+
+ @Test
+ public void testPerformDrag_NullDataWithGrantUri() throws Exception {
+ dragFlow(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, null, 0, 0);
+ }
+
+ @Test
+ public void testPerformDrag_NullDataToOtherUser() throws Exception {
+ final WindowState otherUsersWindow =
+ createDropTargetWindow("Other user's window", 1 * UserHandle.PER_USER_RANGE);
+ when(mDisplayContent.getTouchableWinAtPointLocked(10, 10))
+ .thenReturn(otherUsersWindow);
+
+ dragFlow(0, null, 10, 10);
+ }
+
+ private void dragFlow(int flag, ClipData data, float dropX, float dropY) {
+ final SurfaceSession appSession = new SurfaceSession();
+ try {
+ final SurfaceControl surface = new SurfaceControl.Builder(appSession)
+ .setName("drag surface")
+ .setSize(100, 100)
+ .setFormat(PixelFormat.TRANSLUCENT)
+ .build();
+
+ assertTrue(sWm.mInputManager.transferTouchFocus(null, null));
+ mToken = mTarget.performDrag(
+ new SurfaceSession(), 0, 0, mWindow.mClient, flag, surface, 0, 0, 0, 0, 0,
+ data);
+ assertNotNull(mToken);
+
+ mTarget.handleMotionEvent(false, dropX, dropY);
+ mToken = mWindow.mClient.asBinder();
+ } finally {
+ appSession.kill();
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/PinnedStackControllerTest.java b/services/tests/servicestests/src/com/android/server/wm/PinnedStackControllerTest.java
new file mode 100644
index 0000000..96745fa
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/PinnedStackControllerTest.java
@@ -0,0 +1,63 @@
+package com.android.server.wm;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.IPinnedStackListener;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class PinnedStackControllerTest extends WindowTestsBase {
+
+ @Mock private IPinnedStackListener mIPinnedStackListener;
+ @Mock private IPinnedStackListener.Stub mIPinnedStackListenerStub;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ when(mIPinnedStackListener.asBinder()).thenReturn(mIPinnedStackListenerStub);
+ }
+
+ @Test
+ public void setShelfHeight_shelfVisibilityChangedTriggered() throws RemoteException {
+ sWm.mSupportsPictureInPicture = true;
+ sWm.registerPinnedStackListener(DEFAULT_DISPLAY, mIPinnedStackListener);
+
+ verify(mIPinnedStackListener).onImeVisibilityChanged(false, 0);
+ verify(mIPinnedStackListener).onShelfVisibilityChanged(false, 0);
+ verify(mIPinnedStackListener).onMovementBoundsChanged(any(), any(), any(), eq(false),
+ eq(false), anyInt());
+ verify(mIPinnedStackListener).onActionsChanged(any());
+ verify(mIPinnedStackListener).onMinimizedStateChanged(anyBoolean());
+
+ reset(mIPinnedStackListener);
+
+ final int SHELF_HEIGHT = 300;
+
+ sWm.setShelfHeight(true, SHELF_HEIGHT);
+ verify(mIPinnedStackListener).onShelfVisibilityChanged(true, SHELF_HEIGHT);
+ verify(mIPinnedStackListener).onMovementBoundsChanged(any(), any(), any(), eq(false),
+ eq(true), anyInt());
+ verify(mIPinnedStackListener, never()).onImeVisibilityChanged(anyBoolean(), anyInt());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/RecentsAnimationControllerTest.java b/services/tests/servicestests/src/com/android/server/wm/RecentsAnimationControllerTest.java
new file mode 100644
index 0000000..a2af9b8
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/RecentsAnimationControllerTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static com.android.server.wm.RecentsAnimationController.REORDER_KEEP_IN_PLACE;
+import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_ORIGINAL_POSITION;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.os.Binder;
+import android.os.IInterface;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.IRecentsAnimationRunner;
+import android.view.SurfaceControl;
+import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * atest FrameworksServicesTests:com.android.server.wm.RecentsAnimationControllerTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class RecentsAnimationControllerTest extends WindowTestsBase {
+
+ @Mock SurfaceControl mMockLeash;
+ @Mock SurfaceControl.Transaction mMockTransaction;
+ @Mock OnAnimationFinishedCallback mFinishedCallback;
+ @Mock IRecentsAnimationRunner mMockRunner;
+ @Mock RecentsAnimationController.RecentsAnimationCallbacks mAnimationCallbacks;
+ private RecentsAnimationController mController;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ when(mMockRunner.asBinder()).thenReturn(new Binder());
+ mController = new RecentsAnimationController(sWm, mMockRunner, mAnimationCallbacks,
+ DEFAULT_DISPLAY);
+ }
+
+ @Test
+ public void testRemovedBeforeStarted_expectCanceled() throws Exception {
+ final AppWindowToken appWindow = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ AnimationAdapter adapter = mController.addAnimation(appWindow.getTask(),
+ false /* isRecentTaskInvisible */);
+ adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback);
+
+ // Remove the app window so that the animation target can not be created
+ appWindow.removeImmediately();
+ mController.startAnimation();
+
+ // Verify that the finish callback to reparent the leash is called
+ verify(mFinishedCallback).onAnimationFinished(eq(adapter));
+ // Verify the animation canceled callback to the app was made
+ verify(mMockRunner).onAnimationCanceled();
+ verifyNoMoreInteractionsExceptAsBinder(mMockRunner);
+ }
+
+ @Test
+ public void testCancelAfterRemove_expectIgnored() throws Exception {
+ final AppWindowToken appWindow = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ AnimationAdapter adapter = mController.addAnimation(appWindow.getTask(),
+ false /* isRecentTaskInvisible */);
+ adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback);
+
+ // Remove the app window so that the animation target can not be created
+ appWindow.removeImmediately();
+ mController.startAnimation();
+ mController.cleanupAnimation(REORDER_KEEP_IN_PLACE);
+ try {
+ mController.cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "test");
+ } catch (Exception e) {
+ fail("Unexpected failure when canceling animation after finishing it");
+ }
+ }
+
+ private static void verifyNoMoreInteractionsExceptAsBinder(IInterface binder) {
+ verify(binder, atLeast(0)).asBinder();
+ verifyNoMoreInteractions(binder);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/servicestests/src/com/android/server/wm/RemoteAnimationControllerTest.java
new file mode 100644
index 0000000..95361f0
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/RemoteAnimationControllerTest.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.os.IInterface;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+
+import com.android.server.testutils.OffsettableClock;
+import com.android.server.testutils.TestHandler;
+import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * atest FrameworksServicesTests:com.android.server.wm.RemoteAnimationControllerTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class RemoteAnimationControllerTest extends WindowTestsBase {
+
+ @Mock SurfaceControl mMockLeash;
+ @Mock Transaction mMockTransaction;
+ @Mock OnAnimationFinishedCallback mFinishedCallback;
+ @Mock IRemoteAnimationRunner mMockRunner;
+ private RemoteAnimationAdapter mAdapter;
+ private RemoteAnimationController mController;
+ private final OffsettableClock mClock = new OffsettableClock.Stopped();
+ private TestHandler mHandler;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ when(mMockRunner.asBinder()).thenReturn(new Binder());
+ mAdapter = new RemoteAnimationAdapter(mMockRunner, 100, 50);
+ mAdapter.setCallingPid(123);
+ sWm.mH.runWithScissors(() -> {
+ mHandler = new TestHandler(null, mClock);
+ }, 0);
+ mController = new RemoteAnimationController(sWm, mAdapter, mHandler);
+ }
+
+ @Test
+ public void testRun() throws Exception {
+ final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin");
+ sWm.mOpeningApps.add(win.mAppToken);
+ try {
+ final AnimationAdapter adapter = mController.createAnimationAdapter(win.mAppToken,
+ new Point(50, 100), new Rect(50, 100, 150, 150));
+ adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback);
+ mController.goodToGo();
+ sWm.mAnimator.executeAfterPrepareSurfacesRunnables();
+ final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
+ ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
+ final ArgumentCaptor<IRemoteAnimationFinishedCallback> finishedCaptor =
+ ArgumentCaptor.forClass(IRemoteAnimationFinishedCallback.class);
+ verify(mMockRunner).onAnimationStart(appsCaptor.capture(), finishedCaptor.capture());
+ assertEquals(1, appsCaptor.getValue().length);
+ final RemoteAnimationTarget app = appsCaptor.getValue()[0];
+ assertEquals(new Point(50, 100), app.position);
+ assertEquals(new Rect(50, 100, 150, 150), app.sourceContainerBounds);
+ assertEquals(win.mAppToken.getPrefixOrderIndex(), app.prefixOrderIndex);
+ assertEquals(win.mAppToken.getTask().mTaskId, app.taskId);
+ assertEquals(mMockLeash, app.leash);
+ assertEquals(win.mWinAnimator.mLastClipRect, app.clipRect);
+ assertEquals(false, app.isTranslucent);
+ verify(mMockTransaction).setLayer(mMockLeash, app.prefixOrderIndex);
+ verify(mMockTransaction).setPosition(mMockLeash, app.position.x, app.position.y);
+ verify(mMockTransaction).setWindowCrop(mMockLeash, new Rect(0, 0, 100, 50));
+
+ finishedCaptor.getValue().onAnimationFinished();
+ verify(mFinishedCallback).onAnimationFinished(eq(adapter));
+ } finally {
+ sWm.mOpeningApps.clear();
+ }
+ }
+
+ @Test
+ public void testCancel() throws Exception {
+ final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin");
+ final AnimationAdapter adapter = mController.createAnimationAdapter(win.mAppToken,
+ new Point(50, 100), new Rect(50, 100, 150, 150));
+ adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback);
+ mController.goodToGo();
+
+ adapter.onAnimationCancelled(mMockLeash);
+ verify(mMockRunner).onAnimationCancelled();
+ }
+
+ @Test
+ public void testTimeout() throws Exception {
+ final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin");
+ final AnimationAdapter adapter = mController.createAnimationAdapter(win.mAppToken,
+ new Point(50, 100), new Rect(50, 100, 150, 150));
+ adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback);
+ mController.goodToGo();
+
+ mClock.fastForward(2500);
+ mHandler.timeAdvance();
+
+ verify(mMockRunner).onAnimationCancelled();
+ verify(mFinishedCallback).onAnimationFinished(eq(adapter));
+ }
+
+ @Test
+ public void testTimeout_scaled() throws Exception {
+ sWm.setAnimationScale(2, 5.0f);
+ try{
+ final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin");
+ final AnimationAdapter adapter = mController.createAnimationAdapter(win.mAppToken,
+ new Point(50, 100), new Rect(50, 100, 150, 150));
+ adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback);
+ mController.goodToGo();
+
+ mClock.fastForward(2500);
+ mHandler.timeAdvance();
+
+ verify(mMockRunner, never()).onAnimationCancelled();
+
+ mClock.fastForward(10000);
+ mHandler.timeAdvance();
+
+ verify(mMockRunner).onAnimationCancelled();
+ verify(mFinishedCallback).onAnimationFinished(eq(adapter));
+ } finally {
+ sWm.setAnimationScale(2, 1.0f);
+ }
+
+ }
+
+ @Test
+ public void testZeroAnimations() throws Exception {
+ mController.goodToGo();
+ verifyNoMoreInteractionsExceptAsBinder(mMockRunner);
+ }
+
+ @Test
+ public void testNotReallyStarted() throws Exception {
+ final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin");
+ mController.createAnimationAdapter(win.mAppToken,
+ new Point(50, 100), new Rect(50, 100, 150, 150));
+ mController.goodToGo();
+ verifyNoMoreInteractionsExceptAsBinder(mMockRunner);
+ }
+
+ @Test
+ public void testOneNotStarted() throws Exception {
+ final WindowState win1 = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin1");
+ final WindowState win2 = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin2");
+ mController.createAnimationAdapter(win1.mAppToken,
+ new Point(50, 100), new Rect(50, 100, 150, 150));
+ final AnimationAdapter adapter = mController.createAnimationAdapter(win2.mAppToken,
+ new Point(50, 100), new Rect(50, 100, 150, 150));
+ adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback);
+ mController.goodToGo();
+ sWm.mAnimator.executeAfterPrepareSurfacesRunnables();
+ final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
+ ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
+ final ArgumentCaptor<IRemoteAnimationFinishedCallback> finishedCaptor =
+ ArgumentCaptor.forClass(IRemoteAnimationFinishedCallback.class);
+ verify(mMockRunner).onAnimationStart(appsCaptor.capture(), finishedCaptor.capture());
+ assertEquals(1, appsCaptor.getValue().length);
+ assertEquals(mMockLeash, appsCaptor.getValue()[0].leash);
+ }
+
+ @Test
+ public void testRemovedBeforeStarted() throws Exception {
+ final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin");
+ final AnimationAdapter adapter = mController.createAnimationAdapter(win.mAppToken,
+ new Point(50, 100), new Rect(50, 100, 150, 150));
+ adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback);
+ win.mAppToken.removeImmediately();
+ mController.goodToGo();
+ verifyNoMoreInteractionsExceptAsBinder(mMockRunner);
+ verify(mFinishedCallback).onAnimationFinished(eq(adapter));
+ }
+
+ private static void verifyNoMoreInteractionsExceptAsBinder(IInterface binder) {
+ verify(binder, atLeast(0)).asBinder();
+ verifyNoMoreInteractions(binder);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/servicestests/src/com/android/server/wm/RootWindowContainerTests.java
new file mode 100644
index 0000000..204e26c
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/RootWindowContainerTests.java
@@ -0,0 +1,52 @@
+package com.android.server.wm;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.content.res.Configuration;
+import android.graphics.Rect;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for the {@link RootWindowContainer} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.wm.RootWindowContainerTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class RootWindowContainerTests extends WindowTestsBase {
+ @Test
+ public void testSetDisplayOverrideConfigurationIfNeeded() throws Exception {
+ synchronized (sWm.mWindowMap) {
+ // Add first stack we expect to be updated with configuration change.
+ final TaskStack stack = createTaskStackOnDisplay(mDisplayContent);
+ stack.getOverrideConfiguration().windowConfiguration.setBounds(new Rect(0, 0, 5, 5));
+
+ // Add second task that will be set for deferred removal that should not be returned
+ // with the configuration change.
+ final TaskStack deferredDeletedStack = createTaskStackOnDisplay(mDisplayContent);
+ deferredDeletedStack.getOverrideConfiguration().windowConfiguration.setBounds(
+ new Rect(0, 0, 5, 5));
+ deferredDeletedStack.mDeferRemoval = true;
+
+ final Configuration override = new Configuration(
+ mDisplayContent.getOverrideConfiguration());
+ override.windowConfiguration.setBounds(new Rect(0, 0, 10, 10));
+
+ // Set display override.
+ final int[] results = sWm.mRoot.setDisplayOverrideConfigurationIfNeeded(override,
+ mDisplayContent.getDisplayId());
+
+ // Ensure only first stack is returned.
+ assertTrue(results.length == 1);
+ assertTrue(results[0] == stack.mStackId);
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/ScreenDecorWindowTests.java b/services/tests/servicestests/src/com/android/server/wm/ScreenDecorWindowTests.java
new file mode 100644
index 0000000..a2ccee4
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/ScreenDecorWindowTests.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.graphics.Color.RED;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Gravity.BOTTOM;
+import static android.view.Gravity.LEFT;
+import static android.view.Gravity.RIGHT;
+import static android.view.Gravity.TOP;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IS_SCREEN_DECOR;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+import static org.junit.Assert.assertEquals;
+
+import android.app.Activity;
+import android.app.ActivityOptions;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.VirtualDisplay;
+import android.media.ImageReader;
+import android.os.Handler;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Pair;
+import android.view.Display;
+import android.view.DisplayInfo;
+import android.view.View;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.function.BooleanSupplier;
+
+/**
+ * Tests for the {@link android.view.WindowManager.LayoutParams#PRIVATE_FLAG_IS_SCREEN_DECOR} flag.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.wm.ScreenDecorWindowTests
+ */
+// TODO: Add test for FLAG_FULLSCREEN which hides the status bar and also other flags.
+// TODO: Test non-Activity windows.
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class ScreenDecorWindowTests {
+
+ private final Context mContext = InstrumentationRegistry.getTargetContext();
+ private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
+
+ private WindowManager mWm;
+ private ArrayList<View> mWindows = new ArrayList<>();
+
+ private Activity mTestActivity;
+ private VirtualDisplay mDisplay;
+ private ImageReader mImageReader;
+
+ private int mDecorThickness;
+ private int mHalfDecorThickness;
+
+ @Before
+ public void setUp() {
+ final Pair<VirtualDisplay, ImageReader> result = createDisplay();
+ mDisplay = result.first;
+ mImageReader = result.second;
+ final Display display = mDisplay.getDisplay();
+ final Context dContext = mContext.createDisplayContext(display);
+ mWm = dContext.getSystemService(WindowManager.class);
+ mTestActivity = startActivityOnDisplay(TestActivity.class, display.getDisplayId());
+ final Point size = new Point();
+ mDisplay.getDisplay().getRealSize(size);
+ mDecorThickness = Math.min(size.x, size.y) / 3;
+ mHalfDecorThickness = mDecorThickness / 2;
+ }
+
+ @After
+ public void tearDown() {
+ while (!mWindows.isEmpty()) {
+ removeWindow(mWindows.get(0));
+ }
+ finishActivity(mTestActivity);
+ mDisplay.release();
+ mImageReader.close();
+ }
+
+ @Test
+ public void testScreenSides() throws Exception {
+ // Decor on top
+ final View decorWindow = createDecorWindow(TOP, MATCH_PARENT, mDecorThickness);
+ assertInsetGreaterOrEqual(mTestActivity, TOP, mDecorThickness);
+
+ // Decor at the bottom
+ updateWindow(decorWindow, BOTTOM, MATCH_PARENT, mDecorThickness, 0, 0);
+ assertInsetGreaterOrEqual(mTestActivity, BOTTOM, mDecorThickness);
+
+ // Decor to the left
+ updateWindow(decorWindow, LEFT, mDecorThickness, MATCH_PARENT, 0, 0);
+ assertInsetGreaterOrEqual(mTestActivity, LEFT, mDecorThickness);
+
+ // Decor to the right
+ updateWindow(decorWindow, RIGHT, mDecorThickness, MATCH_PARENT, 0, 0);
+ assertInsetGreaterOrEqual(mTestActivity, RIGHT, mDecorThickness);
+ }
+
+ @Test
+ public void testMultipleDecors() throws Exception {
+ // Test 2 decor windows on-top.
+ createDecorWindow(TOP, MATCH_PARENT, mHalfDecorThickness);
+ assertInsetGreaterOrEqual(mTestActivity, TOP, mHalfDecorThickness);
+ createDecorWindow(TOP, MATCH_PARENT, mDecorThickness);
+ assertInsetGreaterOrEqual(mTestActivity, TOP, mDecorThickness);
+
+ // And one at the bottom.
+ createDecorWindow(BOTTOM, MATCH_PARENT, mHalfDecorThickness);
+ assertInsetGreaterOrEqual(mTestActivity, TOP, mDecorThickness);
+ assertInsetGreaterOrEqual(mTestActivity, BOTTOM, mHalfDecorThickness);
+ }
+
+ @Test
+ public void testFlagChange() throws Exception {
+ WindowInsets initialInsets = getInsets(mTestActivity);
+
+ final View decorWindow = createDecorWindow(TOP, MATCH_PARENT, mDecorThickness);
+ assertTopInsetEquals(mTestActivity, mDecorThickness);
+
+ updateWindow(decorWindow, TOP, MATCH_PARENT, mDecorThickness,
+ 0, PRIVATE_FLAG_IS_SCREEN_DECOR);
+
+ // TODO: fix test and re-enable assertion.
+ // initialInsets was not actually immutable and just updated to the current insets,
+ // meaning this assertion never actually tested anything. Now that WindowInsets actually is
+ // immutable, it turns out the test was broken.
+ // assertTopInsetEquals(mTestActivity, initialInsets.getSystemWindowInsetTop());
+
+ updateWindow(decorWindow, TOP, MATCH_PARENT, mDecorThickness,
+ PRIVATE_FLAG_IS_SCREEN_DECOR, PRIVATE_FLAG_IS_SCREEN_DECOR);
+ assertTopInsetEquals(mTestActivity, mDecorThickness);
+ }
+
+ @Test
+ public void testRemoval() throws Exception {
+ WindowInsets initialInsets = getInsets(mTestActivity);
+
+ final View decorWindow = createDecorWindow(TOP, MATCH_PARENT, mDecorThickness);
+ assertInsetGreaterOrEqual(mTestActivity, TOP, mDecorThickness);
+
+ removeWindow(decorWindow);
+ assertTopInsetEquals(mTestActivity, initialInsets.getSystemWindowInsetTop());
+ }
+
+ private View createDecorWindow(int gravity, int width, int height) {
+ return createWindow("decorWindow", gravity, width, height, RED,
+ FLAG_LAYOUT_IN_SCREEN, PRIVATE_FLAG_IS_SCREEN_DECOR);
+ }
+
+ private View createWindow(String name, int gravity, int width, int height, int color, int flags,
+ int privateFlags) {
+
+ final View[] viewHolder = new View[1];
+ final int finalFlag = flags
+ | FLAG_NOT_FOCUSABLE | FLAG_WATCH_OUTSIDE_TOUCH | FLAG_NOT_TOUCHABLE;
+
+ // Needs to run on the UI thread.
+ Handler.getMain().runWithScissors(() -> {
+ final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ width, height, TYPE_APPLICATION_OVERLAY, finalFlag, PixelFormat.OPAQUE);
+ lp.gravity = gravity;
+ lp.privateFlags |= privateFlags;
+
+ final TextView view = new TextView(mContext);
+ view.setText("ScreenDecorWindowTests - " + name);
+ view.setBackgroundColor(color);
+ mWm.addView(view, lp);
+ mWindows.add(view);
+ viewHolder[0] = view;
+ }, 0);
+
+ waitForIdle();
+ return viewHolder[0];
+ }
+
+ private void updateWindow(View v, int gravity, int width, int height,
+ int privateFlags, int privateFlagsMask) {
+ // Needs to run on the UI thread.
+ Handler.getMain().runWithScissors(() -> {
+ final WindowManager.LayoutParams lp = (WindowManager.LayoutParams) v.getLayoutParams();
+ lp.gravity = gravity;
+ lp.width = width;
+ lp.height = height;
+ setPrivateFlags(lp, privateFlags, privateFlagsMask);
+
+ mWm.updateViewLayout(v, lp);
+ }, 0);
+
+ waitForIdle();
+ }
+
+ private void removeWindow(View v) {
+ Handler.getMain().runWithScissors(() -> mWm.removeView(v), 0);
+ mWindows.remove(v);
+ waitForIdle();
+ }
+
+ private WindowInsets getInsets(Activity a) {
+ return new WindowInsets(a.getWindow().getDecorView().getRootWindowInsets());
+ }
+
+ /**
+ * Set the flags of the window, as per the
+ * {@link WindowManager.LayoutParams WindowManager.LayoutParams}
+ * flags.
+ *
+ * @param flags The new window flags (see WindowManager.LayoutParams).
+ * @param mask Which of the window flag bits to modify.
+ */
+ public void setPrivateFlags(WindowManager.LayoutParams lp, int flags, int mask) {
+ lp.flags = (lp.flags & ~mask) | (flags & mask);
+ }
+
+ /**
+ * Asserts the top inset of {@param activity} is equal to {@param expected} waiting as needed.
+ */
+ private void assertTopInsetEquals(Activity activity, int expected) throws Exception {
+ waitFor(() -> getInsets(activity).getSystemWindowInsetTop() == expected);
+ assertEquals(expected, getInsets(activity).getSystemWindowInsetTop());
+ }
+
+ /**
+ * Asserts the inset at {@param side} of {@param activity} is equal to {@param expected}
+ * waiting as needed.
+ */
+ private void assertInsetGreaterOrEqual(Activity activity, int side, int expected)
+ throws Exception {
+ waitFor(() -> {
+ final WindowInsets insets = getInsets(activity);
+ switch (side) {
+ case TOP: return insets.getSystemWindowInsetTop() >= expected;
+ case BOTTOM: return insets.getSystemWindowInsetBottom() >= expected;
+ case LEFT: return insets.getSystemWindowInsetLeft() >= expected;
+ case RIGHT: return insets.getSystemWindowInsetRight() >= expected;
+ default: return true;
+ }
+ });
+
+ final WindowInsets insets = getInsets(activity);
+ switch (side) {
+ case TOP: assertGreaterOrEqual(insets.getSystemWindowInsetTop(), expected); break;
+ case BOTTOM: assertGreaterOrEqual(insets.getSystemWindowInsetBottom(), expected); break;
+ case LEFT: assertGreaterOrEqual(insets.getSystemWindowInsetLeft(), expected); break;
+ case RIGHT: assertGreaterOrEqual(insets.getSystemWindowInsetRight(), expected); break;
+ }
+ }
+
+ /** Asserts that the first entry is greater than or equal to the second entry. */
+ private void assertGreaterOrEqual(int first, int second) throws Exception {
+ Assert.assertTrue("Excepted " + first + " >= " + second, first >= second);
+ }
+
+ private void waitFor(BooleanSupplier waitCondition) {
+ int retriesLeft = 5;
+ do {
+ if (waitCondition.getAsBoolean()) {
+ break;
+ }
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ // Well I guess we are not waiting...
+ }
+ } while (retriesLeft-- > 0);
+ }
+
+ private void finishActivity(Activity a) {
+ if (a == null) {
+ return;
+ }
+ a.finish();
+ waitForIdle();
+ }
+
+ private void waitForIdle() {
+ mInstrumentation.waitForIdleSync();
+ }
+
+ private Activity startActivityOnDisplay(Class<?> cls, int displayId) {
+ final Intent intent = new Intent(mContext, cls);
+ intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
+ final ActivityOptions options = ActivityOptions.makeBasic();
+ options.setLaunchDisplayId(displayId);
+ final Activity activity = mInstrumentation.startActivitySync(intent, options.toBundle());
+ waitForIdle();
+
+ assertEquals(displayId, activity.getDisplay().getDisplayId());
+ return activity;
+ }
+
+ private Pair<VirtualDisplay, ImageReader> createDisplay() {
+ final DisplayManager dm = mContext.getSystemService(DisplayManager.class);
+ final DisplayInfo displayInfo = new DisplayInfo();
+ final Display defaultDisplay = dm.getDisplay(DEFAULT_DISPLAY);
+ defaultDisplay.getDisplayInfo(displayInfo);
+ final String name = "ScreenDecorWindowTests";
+ int flags = VIRTUAL_DISPLAY_FLAG_PRESENTATION | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
+
+ final ImageReader imageReader = ImageReader.newInstance(
+ displayInfo.logicalWidth, displayInfo.logicalHeight, PixelFormat.RGBA_8888, 2);
+
+ final VirtualDisplay display = dm.createVirtualDisplay(name, displayInfo.logicalWidth,
+ displayInfo.logicalHeight, displayInfo.logicalDensityDpi, imageReader.getSurface(),
+ flags);
+
+ return Pair.create(display, imageReader);
+ }
+
+ public static class TestActivity extends Activity {
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/StackWindowControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/StackWindowControllerTests.java
new file mode 100644
index 0000000..ab0a2bd
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/StackWindowControllerTests.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import android.graphics.Rect;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test class for {@link StackWindowController}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:StackWindowControllerTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class StackWindowControllerTests extends WindowTestsBase {
+ @Test
+ public void testRemoveContainer() throws Exception {
+ final StackWindowController stackController =
+ createStackControllerOnDisplay(mDisplayContent);
+ final WindowTestUtils.TestTaskWindowContainerController taskController =
+ new WindowTestUtils.TestTaskWindowContainerController(stackController);
+
+ final TaskStack stack = stackController.mContainer;
+ final Task task = taskController.mContainer;
+ assertNotNull(stack);
+ assertNotNull(task);
+ stackController.removeContainer();
+ // Assert that the container was removed.
+ assertNull(stackController.mContainer);
+ assertNull(taskController.mContainer);
+ assertNull(stack.getDisplayContent());
+ assertNull(task.getDisplayContent());
+ assertNull(task.mStack);
+ }
+
+ @Test
+ public void testRemoveContainer_deferRemoval() throws Exception {
+ final StackWindowController stackController =
+ createStackControllerOnDisplay(mDisplayContent);
+ final WindowTestUtils.TestTaskWindowContainerController taskController =
+ new WindowTestUtils.TestTaskWindowContainerController(stackController);
+
+ final TaskStack stack = stackController.mContainer;
+ final WindowTestUtils.TestTask task = (WindowTestUtils.TestTask) taskController.mContainer;
+ // Stack removal is deferred if one of its child is animating.
+ task.setLocalIsAnimating(true);
+
+ stackController.removeContainer();
+ // For the case of deferred removal the stack controller will no longer be connected to the
+ // container, but the task controller will still be connected to the its container until
+ // the stack window container is removed.
+ assertNull(stackController.mContainer);
+ assertNull(stack.getController());
+ assertNotNull(taskController.mContainer);
+ assertNotNull(task.getController());
+
+ stack.removeImmediately();
+ assertNull(taskController.mContainer);
+ assertNull(task.getController());
+ }
+
+ @Test
+ public void testReparent() throws Exception {
+ // Create first stack on primary display.
+ final StackWindowController stack1Controller =
+ createStackControllerOnDisplay(mDisplayContent);
+ final TaskStack stack1 = stack1Controller.mContainer;
+ final WindowTestUtils.TestTaskWindowContainerController taskController =
+ new WindowTestUtils.TestTaskWindowContainerController(stack1Controller);
+ final WindowTestUtils.TestTask task1 = (WindowTestUtils.TestTask) taskController.mContainer;
+ task1.mOnDisplayChangedCalled = false;
+
+ // Create second display and put second stack on it.
+ final DisplayContent dc = createNewDisplay();
+ final StackWindowController stack2Controller =
+ createStackControllerOnDisplay(dc);
+ final TaskStack stack2 = stack2Controller.mContainer;
+
+ // Reparent
+ stack1Controller.reparent(dc.getDisplayId(), new Rect(), true /* onTop */);
+ assertEquals(dc, stack1.getDisplayContent());
+ final int stack1PositionInParent = stack1.getParent().mChildren.indexOf(stack1);
+ final int stack2PositionInParent = stack1.getParent().mChildren.indexOf(stack2);
+ assertEquals(stack1PositionInParent, stack2PositionInParent + 1);
+ assertTrue(task1.mOnDisplayChangedCalled);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java b/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java
new file mode 100644
index 0000000..edac8a5
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.animation.AnimationHandler;
+import android.animation.AnimationHandler.AnimationFrameCallbackProvider;
+import android.animation.ValueAnimator;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Log;
+import android.view.Choreographer;
+import android.view.Choreographer.FrameCallback;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+import android.view.animation.Animation;
+import android.view.animation.TranslateAnimation;
+
+import com.android.server.wm.LocalAnimationAdapter.AnimationSpec;
+import com.android.server.wm.SurfaceAnimationRunner.AnimatorFactory;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * Test class for {@link SurfaceAnimationRunner}.
+ *
+ * atest FrameworksServicesTests:com.android.server.wm.SurfaceAnimationRunnerTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class SurfaceAnimationRunnerTest extends WindowTestsBase {
+
+ @Mock SurfaceControl mMockSurface;
+ @Mock Transaction mMockTransaction;
+ @Mock AnimationSpec mMockAnimationSpec;
+ @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ private SurfaceAnimationRunner mSurfaceAnimationRunner;
+ private CountDownLatch mFinishCallbackLatch;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mFinishCallbackLatch = new CountDownLatch(1);
+ mSurfaceAnimationRunner = new SurfaceAnimationRunner(null /* callbackProvider */, null,
+ mMockTransaction);
+ }
+
+ private void finishedCallback() {
+ mFinishCallbackLatch.countDown();
+ }
+
+ @Test
+ public void testAnimation() throws Exception {
+ mSurfaceAnimationRunner
+ .startAnimation(createTranslateAnimation(), mMockSurface, mMockTransaction,
+ this::finishedCallback);
+
+ // Ensure that the initial transformation has been applied.
+ final Matrix m = new Matrix();
+ m.setTranslate(-10, 0);
+ verify(mMockTransaction, atLeastOnce()).setMatrix(eq(mMockSurface), eq(m), any());
+ verify(mMockTransaction, atLeastOnce()).setAlpha(eq(mMockSurface), eq(1.0f));
+
+ mFinishCallbackLatch.await(1, SECONDS);
+ assertFinishCallbackCalled();
+
+ m.setTranslate(10, 0);
+ verify(mMockTransaction, atLeastOnce()).setMatrix(eq(mMockSurface), eq(m), any());
+
+ // At least 3 times: After initialization, first frame, last frame.
+ verify(mMockTransaction, atLeast(3)).setAlpha(eq(mMockSurface), eq(1.0f));
+ }
+
+ @Test
+ public void testCancel_notStarted() throws Exception {
+ mSurfaceAnimationRunner = new SurfaceAnimationRunner(new NoOpFrameCallbackProvider(), null,
+ mMockTransaction);
+ mSurfaceAnimationRunner
+ .startAnimation(createTranslateAnimation(), mMockSurface, mMockTransaction,
+ this::finishedCallback);
+ mSurfaceAnimationRunner.onAnimationCancelled(mMockSurface);
+ waitUntilHandlersIdle();
+ assertTrue(mSurfaceAnimationRunner.mPendingAnimations.isEmpty());
+ assertFinishCallbackNotCalled();
+ }
+
+ @Test
+ public void testCancel_running() throws Exception {
+ mSurfaceAnimationRunner = new SurfaceAnimationRunner(new NoOpFrameCallbackProvider(), null,
+ mMockTransaction);
+ mSurfaceAnimationRunner.startAnimation(createTranslateAnimation(), mMockSurface,
+ mMockTransaction, this::finishedCallback);
+ waitUntilNextFrame();
+ assertFalse(mSurfaceAnimationRunner.mRunningAnimations.isEmpty());
+ mSurfaceAnimationRunner.onAnimationCancelled(mMockSurface);
+ assertTrue(mSurfaceAnimationRunner.mRunningAnimations.isEmpty());
+ waitUntilHandlersIdle();
+ assertFinishCallbackNotCalled();
+ }
+
+ @FlakyTest(bugId = 71719744)
+ @Test
+ public void testCancel_sneakyCancelBeforeUpdate() throws Exception {
+ mSurfaceAnimationRunner = new SurfaceAnimationRunner(null, () -> new ValueAnimator() {
+ {
+ setFloatValues(0f, 1f);
+ }
+
+ @Override
+ public void addUpdateListener(AnimatorUpdateListener listener) {
+ super.addUpdateListener(animation -> {
+ // Sneaky test cancels animation just before applying frame to simulate
+ // interleaving of multiple threads. Muahahaha
+ if (animation.getCurrentPlayTime() > 0) {
+ mSurfaceAnimationRunner.onAnimationCancelled(mMockSurface);
+ }
+ listener.onAnimationUpdate(animation);
+ });
+ }
+ }, mMockTransaction);
+ when(mMockAnimationSpec.getDuration()).thenReturn(200L);
+ mSurfaceAnimationRunner.startAnimation(mMockAnimationSpec, mMockSurface, mMockTransaction,
+ this::finishedCallback);
+
+ // We need to wait for two frames: The first frame starts the animation, the second frame
+ // actually cancels the animation.
+ waitUntilNextFrame();
+ waitUntilNextFrame();
+ assertTrue(mSurfaceAnimationRunner.mRunningAnimations.isEmpty());
+ verify(mMockAnimationSpec, atLeastOnce()).apply(any(), any(), eq(0L));
+ }
+
+ @FlakyTest(bugId = 74780584)
+ @Test
+ public void testDeferStartingAnimations() throws Exception {
+ mSurfaceAnimationRunner.deferStartingAnimations();
+ mSurfaceAnimationRunner.startAnimation(createTranslateAnimation(), mMockSurface,
+ mMockTransaction, this::finishedCallback);
+ waitUntilNextFrame();
+ assertTrue(mSurfaceAnimationRunner.mRunningAnimations.isEmpty());
+ mSurfaceAnimationRunner.continueStartingAnimations();
+ waitUntilNextFrame();
+ assertFalse(mSurfaceAnimationRunner.mRunningAnimations.isEmpty());
+ mFinishCallbackLatch.await(1, SECONDS);
+ assertFinishCallbackCalled();
+ }
+
+ private void waitUntilNextFrame() throws Exception {
+ final CountDownLatch latch = new CountDownLatch(1);
+ mSurfaceAnimationRunner.mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT,
+ latch::countDown, null /* token */);
+ latch.await();
+ }
+
+ private void assertFinishCallbackCalled() {
+ assertEquals(0, mFinishCallbackLatch.getCount());
+ }
+
+ private void assertFinishCallbackNotCalled() {
+ assertEquals(1, mFinishCallbackLatch.getCount());
+ }
+
+ private AnimationSpec createTranslateAnimation() {
+ final Animation a = new TranslateAnimation(-10, 10, 0, 0);
+ a.initialize(0, 0, 0, 0);
+ a.setDuration(50);
+ return new WindowAnimationSpec(a, new Point(0, 0), false /* canSkipFirstFrame */);
+ }
+
+ /**
+ * Callback provider that doesn't animate at all.
+ */
+ private static final class NoOpFrameCallbackProvider implements AnimationFrameCallbackProvider {
+
+ @Override
+ public void postFrameCallback(FrameCallback callback) {
+ }
+
+ @Override
+ public void postCommitCallback(Runnable runnable) {
+ }
+
+ @Override
+ public long getFrameTime() {
+ return 0;
+ }
+
+ @Override
+ public long getFrameDelay() {
+ return 0;
+ }
+
+ @Override
+ public void setFrameDelay(long delay) {
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimatorTest.java b/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimatorTest.java
new file mode 100644
index 0000000..16b8458
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimatorTest.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Builder;
+import android.view.SurfaceControl.Transaction;
+import android.view.SurfaceSession;
+
+import com.android.server.wm.SurfaceAnimator.Animatable;
+import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+
+/**
+ * Test class for {@link SurfaceAnimatorTest}.
+ *
+ * atest FrameworksServicesTests:com.android.server.wm.SurfaceAnimatorTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class SurfaceAnimatorTest extends WindowTestsBase {
+
+ @Mock AnimationAdapter mSpec;
+ @Mock AnimationAdapter mSpec2;
+ @Mock Transaction mTransaction;
+
+ private SurfaceSession mSession = new SurfaceSession();
+ private MyAnimatable mAnimatable;
+ private MyAnimatable mAnimatable2;
+ private DeferFinishAnimatable mDeferFinishAnimatable;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ mAnimatable = new MyAnimatable();
+ mAnimatable2 = new MyAnimatable();
+ mDeferFinishAnimatable = new DeferFinishAnimatable();
+ }
+
+ @Test
+ public void testRunAnimation() throws Exception {
+ mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+ final ArgumentCaptor<OnAnimationFinishedCallback> callbackCaptor = ArgumentCaptor.forClass(
+ OnAnimationFinishedCallback.class);
+ assertAnimating(mAnimatable);
+ verify(mTransaction).reparent(eq(mAnimatable.mSurface), eq(mAnimatable.mLeash.getHandle()));
+ verify(mSpec).startAnimation(any(), any(), callbackCaptor.capture());
+
+ callbackCaptor.getValue().onAnimationFinished(mSpec);
+ assertNotAnimating(mAnimatable);
+ assertTrue(mAnimatable.mFinishedCallbackCalled);
+ verify(mTransaction).destroy(eq(mAnimatable.mLeash));
+ // TODO: Verify reparenting once we use mPendingTransaction to reparent it back
+ }
+
+ @Test
+ public void testOverrideAnimation() throws Exception {
+ mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+ final SurfaceControl firstLeash = mAnimatable.mLeash;
+ mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec2, true /* hidden */);
+
+ verify(mTransaction).destroy(eq(firstLeash));
+ assertFalse(mAnimatable.mFinishedCallbackCalled);
+
+ final ArgumentCaptor<OnAnimationFinishedCallback> callbackCaptor = ArgumentCaptor.forClass(
+ OnAnimationFinishedCallback.class);
+ assertAnimating(mAnimatable);
+ verify(mSpec).startAnimation(any(), any(), callbackCaptor.capture());
+
+ // First animation was finished, but this shouldn't cancel the second animation
+ callbackCaptor.getValue().onAnimationFinished(mSpec);
+ assertTrue(mAnimatable.mSurfaceAnimator.isAnimating());
+
+ // Second animation was finished
+ verify(mSpec2).startAnimation(any(), any(), callbackCaptor.capture());
+ callbackCaptor.getValue().onAnimationFinished(mSpec2);
+ assertNotAnimating(mAnimatable);
+ assertTrue(mAnimatable.mFinishedCallbackCalled);
+ }
+
+ @Test
+ public void testCancelAnimation() throws Exception {
+ mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+ assertAnimating(mAnimatable);
+ mAnimatable.mSurfaceAnimator.cancelAnimation();
+ assertNotAnimating(mAnimatable);
+ verify(mSpec).onAnimationCancelled(any());
+ assertTrue(mAnimatable.mFinishedCallbackCalled);
+ verify(mTransaction).destroy(eq(mAnimatable.mLeash));
+ }
+
+ @Test
+ public void testDelayingAnimationStart() throws Exception {
+ mAnimatable.mSurfaceAnimator.startDelayingAnimationStart();
+ mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+ verifyZeroInteractions(mSpec);
+ assertAnimating(mAnimatable);
+ assertTrue(mAnimatable.mSurfaceAnimator.isAnimationStartDelayed());
+ mAnimatable.mSurfaceAnimator.endDelayingAnimationStart();
+ verify(mSpec).startAnimation(any(), any(), any());
+ }
+
+ @Test
+ public void testDelayingAnimationStartAndCancelled() throws Exception {
+ mAnimatable.mSurfaceAnimator.startDelayingAnimationStart();
+ mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+ mAnimatable.mSurfaceAnimator.cancelAnimation();
+ verifyZeroInteractions(mSpec);
+ assertNotAnimating(mAnimatable);
+ assertTrue(mAnimatable.mFinishedCallbackCalled);
+ verify(mTransaction).destroy(eq(mAnimatable.mLeash));
+ }
+
+ @Test
+ public void testTransferAnimation() throws Exception {
+ mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+
+ final ArgumentCaptor<OnAnimationFinishedCallback> callbackCaptor = ArgumentCaptor.forClass(
+ OnAnimationFinishedCallback.class);
+ verify(mSpec).startAnimation(any(), any(), callbackCaptor.capture());
+ final SurfaceControl leash = mAnimatable.mLeash;
+
+ mAnimatable2.mSurfaceAnimator.transferAnimation(mAnimatable.mSurfaceAnimator);
+ assertNotAnimating(mAnimatable);
+ assertAnimating(mAnimatable2);
+ assertEquals(leash, mAnimatable2.mSurfaceAnimator.mLeash);
+ verify(mTransaction, never()).destroy(eq(leash));
+ callbackCaptor.getValue().onAnimationFinished(mSpec);
+ assertNotAnimating(mAnimatable2);
+ assertTrue(mAnimatable2.mFinishedCallbackCalled);
+ verify(mTransaction).destroy(eq(leash));
+ }
+
+ @Test
+ @FlakyTest(detail = "Promote once confirmed non-flaky")
+ public void testDeferFinish() throws Exception {
+
+ // Start animation
+ mDeferFinishAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec,
+ true /* hidden */);
+ final ArgumentCaptor<OnAnimationFinishedCallback> callbackCaptor = ArgumentCaptor.forClass(
+ OnAnimationFinishedCallback.class);
+ assertAnimating(mDeferFinishAnimatable);
+ verify(mSpec).startAnimation(any(), any(), callbackCaptor.capture());
+
+ // Finish the animation but then make sure we are deferring.
+ callbackCaptor.getValue().onAnimationFinished(mSpec);
+ assertAnimating(mDeferFinishAnimatable);
+
+ // Now end defer finishing.
+ mDeferFinishAnimatable.endDeferFinishCallback.run();
+ assertNotAnimating(mAnimatable2);
+ assertTrue(mDeferFinishAnimatable.mFinishedCallbackCalled);
+ verify(mTransaction).destroy(eq(mDeferFinishAnimatable.mLeash));
+ }
+
+ private void assertAnimating(MyAnimatable animatable) {
+ assertTrue(animatable.mSurfaceAnimator.isAnimating());
+ assertNotNull(animatable.mSurfaceAnimator.getAnimation());
+ }
+
+ private void assertNotAnimating(MyAnimatable animatable) {
+ assertFalse(animatable.mSurfaceAnimator.isAnimating());
+ assertNull(animatable.mSurfaceAnimator.getAnimation());
+ }
+
+ private class MyAnimatable implements Animatable {
+
+ final SurfaceControl mParent;
+ final SurfaceControl mSurface;
+ final SurfaceAnimator mSurfaceAnimator;
+ SurfaceControl mLeash;
+ boolean mFinishedCallbackCalled;
+
+ MyAnimatable() {
+ mParent = sWm.makeSurfaceBuilder(mSession)
+ .setName("test surface parent")
+ .setSize(3000, 3000)
+ .build();
+ mSurface = sWm.makeSurfaceBuilder(mSession)
+ .setName("test surface")
+ .setSize(1, 1)
+ .build();
+ mFinishedCallbackCalled = false;
+ mLeash = null;
+ mSurfaceAnimator = new SurfaceAnimator(this, mFinishedCallback, sWm);
+ }
+
+ @Override
+ public Transaction getPendingTransaction() {
+ return mTransaction;
+ }
+
+ @Override
+ public void commitPendingTransaction() {
+ }
+
+ @Override
+ public void onAnimationLeashCreated(Transaction t, SurfaceControl leash) {
+ }
+
+ @Override
+ public void onAnimationLeashDestroyed(Transaction t) {
+ }
+
+ @Override
+ public Builder makeAnimationLeash() {
+ return new SurfaceControl.Builder(mSession) {
+
+ @Override
+ public SurfaceControl build() {
+ mLeash = super.build();
+ return mLeash;
+ }
+ }.setParent(mParent);
+ }
+
+ @Override
+ public SurfaceControl getAnimationLeashParent() {
+ return mParent;
+ }
+
+ @Override
+ public SurfaceControl getSurfaceControl() {
+ return mSurface;
+ }
+
+ @Override
+ public SurfaceControl getParentSurfaceControl() {
+ return mParent;
+ }
+
+ @Override
+ public int getSurfaceWidth() {
+ return 1;
+ }
+
+ @Override
+ public int getSurfaceHeight() {
+ return 1;
+ }
+
+ private final Runnable mFinishedCallback = () -> mFinishedCallbackCalled = true;
+ }
+
+ private class DeferFinishAnimatable extends MyAnimatable {
+
+ Runnable endDeferFinishCallback;
+
+ @Override
+ public boolean shouldDeferAnimationFinish(Runnable endDeferFinishCallback) {
+ this.endDeferFinishCallback = endDeferFinishCallback;
+ return true;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskPositionerTests.java b/services/tests/servicestests/src/com/android/server/wm/TaskPositionerTests.java
new file mode 100644
index 0000000..7bf7dd7
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskPositionerTests.java
@@ -0,0 +1,480 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.graphics.Rect;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Display;
+
+import static com.android.server.wm.TaskPositioner.MIN_ASPECT;
+import static com.android.server.wm.WindowManagerService.dipToPixel;
+import static com.android.server.wm.WindowState.MINIMUM_VISIBLE_HEIGHT_IN_DP;
+import static com.android.server.wm.WindowState.MINIMUM_VISIBLE_WIDTH_IN_DP;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for the {@link TaskPositioner} class.
+ *
+ * runtest frameworks-services -c com.android.server.wm.TaskPositionerTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TaskPositionerTests extends WindowTestsBase {
+
+ private final boolean DEBUGGING = false;
+ private final String TAG = "TaskPositionerTest";
+
+ private final static int MOUSE_DELTA_X = 5;
+ private final static int MOUSE_DELTA_Y = 5;
+
+ private int mMinVisibleWidth;
+ private int mMinVisibleHeight;
+ private TaskPositioner mPositioner;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ TaskPositioner.setFactory(null);
+
+ final Display display = mDisplayContent.getDisplay();
+ final DisplayMetrics dm = new DisplayMetrics();
+ display.getMetrics(dm);
+
+ // This should be the same calculation as the TaskPositioner uses.
+ mMinVisibleWidth = dipToPixel(MINIMUM_VISIBLE_WIDTH_IN_DP, dm);
+ mMinVisibleHeight = dipToPixel(MINIMUM_VISIBLE_HEIGHT_IN_DP, dm);
+
+ mPositioner = TaskPositioner.create(sWm);
+ mPositioner.register(mDisplayContent);
+ }
+
+ @Test
+ public void testOverrideFactory() throws Exception {
+ final boolean[] created = new boolean[1];
+ created[0] = false;
+ TaskPositioner.setFactory(new TaskPositioner.Factory() {
+ @Override
+ public TaskPositioner create(WindowManagerService service) {
+ created[0] = true;
+ return null;
+ }
+ });
+
+ assertNull(TaskPositioner.create(sWm));
+ assertTrue(created[0]);
+ }
+
+ /**
+ * This tests that free resizing will allow to change the orientation as well
+ * as does some basic tests (e.g. dragging in Y only will keep X stable).
+ */
+ @Test
+ @Ignore
+ public void testBasicFreeWindowResizing() throws Exception {
+ final Rect r = new Rect(100, 220, 700, 520);
+ final int midY = (r.top + r.bottom) / 2;
+
+ // Start a drag resize starting upper left.
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y, r);
+ assertBoundsEquals(r, mPositioner.getWindowDragBounds());
+
+ // Drag to a good landscape size.
+ mPositioner.resizeDrag(0.0f, 0.0f);
+ assertBoundsEquals(new Rect(MOUSE_DELTA_X, MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to a good portrait size.
+ mPositioner.resizeDrag(400.0f, 0.0f);
+ assertBoundsEquals(new Rect(400 + MOUSE_DELTA_X, MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to a too small size for the width.
+ mPositioner.resizeDrag(2000.0f, r.top);
+ assertBoundsEquals(
+ new Rect(r.right - mMinVisibleWidth, r.top + MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to a too small size for the height.
+ mPositioner.resizeDrag(r.left, 2000.0f);
+ assertBoundsEquals(
+ new Rect(r.left + MOUSE_DELTA_X, r.bottom - mMinVisibleHeight, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Start a drag resize left and see that only the left coord changes..
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, midY, r);
+
+ // Drag to the left.
+ mPositioner.resizeDrag(0.0f, midY);
+ assertBoundsEquals(new Rect(MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the right.
+ mPositioner.resizeDrag(200.0f, midY);
+ assertBoundsEquals(new Rect(200 + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the top
+ mPositioner.resizeDrag(r.left, 0.0f);
+ assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the bottom
+ mPositioner.resizeDrag(r.left, 1000.0f);
+ assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+ }
+
+ /**
+ * This tests that by dragging any edge, the fixed / opposite edge(s) remains anchored.
+ */
+ @Test
+ @Ignore
+ public void testFreeWindowResizingTestAllEdges() throws Exception {
+ final Rect r = new Rect(100, 220, 700, 520);
+ final int midX = (r.left + r.right) / 2;
+ final int midY = (r.top + r.bottom) / 2;
+
+ // Drag upper left.
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y, r);
+ mPositioner.resizeDrag(0.0f, 0.0f);
+ assertTrue(r.left != mPositioner.getWindowDragBounds().left);
+ assertEquals(r.right, mPositioner.getWindowDragBounds().right);
+ assertTrue(r.top != mPositioner.getWindowDragBounds().top);
+ assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom);
+
+ // Drag upper.
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/, midX, r.top - MOUSE_DELTA_Y, r);
+ mPositioner.resizeDrag(0.0f, 0.0f);
+ assertEquals(r.left, mPositioner.getWindowDragBounds().left);
+ assertEquals(r.right, mPositioner.getWindowDragBounds().right);
+ assertTrue(r.top != mPositioner.getWindowDragBounds().top);
+ assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom);
+
+ // Drag upper right.
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/, r.right + MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y, r);
+ mPositioner.resizeDrag(r.right + 100, 0.0f);
+ assertEquals(r.left, mPositioner.getWindowDragBounds().left);
+ assertTrue(r.right != mPositioner.getWindowDragBounds().right);
+ assertTrue(r.top != mPositioner.getWindowDragBounds().top);
+ assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom);
+
+ // Drag right.
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/, r.right + MOUSE_DELTA_X, midY, r);
+ mPositioner.resizeDrag(r.right + 100, 0.0f);
+ assertEquals(r.left, mPositioner.getWindowDragBounds().left);
+ assertTrue(r.right != mPositioner.getWindowDragBounds().right);
+ assertEquals(r.top, mPositioner.getWindowDragBounds().top);
+ assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom);
+
+ // Drag bottom right.
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/,
+ r.right + MOUSE_DELTA_X, r.bottom + MOUSE_DELTA_Y, r);
+ mPositioner.resizeDrag(r.right + 100, r.bottom + 100);
+ assertEquals(r.left, mPositioner.getWindowDragBounds().left);
+ assertTrue(r.right != mPositioner.getWindowDragBounds().right);
+ assertEquals(r.top, mPositioner.getWindowDragBounds().top);
+ assertTrue(r.bottom != mPositioner.getWindowDragBounds().bottom);
+
+ // Drag bottom.
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/, midX, r.bottom + MOUSE_DELTA_Y, r);
+ mPositioner.resizeDrag(r.right + 100, r.bottom + 100);
+ assertEquals(r.left, mPositioner.getWindowDragBounds().left);
+ assertEquals(r.right, mPositioner.getWindowDragBounds().right);
+ assertEquals(r.top, mPositioner.getWindowDragBounds().top);
+ assertTrue(r.bottom != mPositioner.getWindowDragBounds().bottom);
+
+ // Drag bottom left.
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.bottom + MOUSE_DELTA_Y, r);
+ mPositioner.resizeDrag(0.0f, r.bottom + 100);
+ assertTrue(r.left != mPositioner.getWindowDragBounds().left);
+ assertEquals(r.right, mPositioner.getWindowDragBounds().right);
+ assertEquals(r.top, mPositioner.getWindowDragBounds().top);
+ assertTrue(r.bottom != mPositioner.getWindowDragBounds().bottom);
+
+ // Drag left.
+ mPositioner.startDrag(true /*resizing*/,
+ false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, midX, r);
+ mPositioner.resizeDrag(0.0f, r.bottom + 100);
+ assertTrue(r.left != mPositioner.getWindowDragBounds().left);
+ assertEquals(r.right, mPositioner.getWindowDragBounds().right);
+ assertEquals(r.top, mPositioner.getWindowDragBounds().top);
+ assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom);
+ }
+
+ /**
+ * This tests that a constrained landscape window will keep the aspect and do the
+ * right things upon resizing when dragged from the top left corner.
+ */
+ @Test
+ @Ignore
+ public void testLandscapePreservedWindowResizingDragTopLeft() throws Exception {
+ final Rect r = new Rect(100, 220, 700, 520);
+
+ mPositioner.startDrag(true /*resizing*/,
+ true /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y, r);
+ assertBoundsEquals(r, mPositioner.getWindowDragBounds());
+
+ // Drag to a good landscape size.
+ mPositioner.resizeDrag(0.0f, 0.0f);
+ assertBoundsEquals(new Rect(MOUSE_DELTA_X, MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to a good portrait size.
+ mPositioner.resizeDrag(400.0f, 0.0f);
+ int width = Math.round((float) (r.bottom - MOUSE_DELTA_Y) * MIN_ASPECT);
+ assertBoundsEquals(new Rect(r.right - width, MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to a too small size for the width.
+ mPositioner.resizeDrag(2000.0f, r.top);
+ final int w = mMinVisibleWidth;
+ final int h = Math.round(w / MIN_ASPECT);
+ assertBoundsEquals(new Rect(r.right - w, r.bottom - h, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to a too small size for the height.
+ mPositioner.resizeDrag(r.left, 2000.0f);
+ assertBoundsEquals(
+ new Rect(r.left + MOUSE_DELTA_X, r.bottom - mMinVisibleHeight, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+ }
+
+ /**
+ * This tests that a constrained landscape window will keep the aspect and do the
+ * right things upon resizing when dragged from the left corner.
+ */
+ @Test
+ @Ignore
+ public void testLandscapePreservedWindowResizingDragLeft() throws Exception {
+ final Rect r = new Rect(100, 220, 700, 520);
+ final int midY = (r.top + r.bottom) / 2;
+
+ mPositioner.startDrag(true /*resizing*/,
+ true /*preserveOrientation*/, r.left - MOUSE_DELTA_X, midY, r);
+
+ // Drag to the left.
+ mPositioner.resizeDrag(0.0f, midY);
+ assertBoundsEquals(new Rect(MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the right.
+ mPositioner.resizeDrag(200.0f, midY);
+ assertBoundsEquals(new Rect(200 + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag all the way to the right and see the height also shrinking.
+ mPositioner.resizeDrag(2000.0f, midY);
+ final int w = mMinVisibleWidth;
+ final int h = Math.round((float)w / MIN_ASPECT);
+ assertBoundsEquals(new Rect(r.right - w, r.top, r.right, r.top + h),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the top.
+ mPositioner.resizeDrag(r.left, 0.0f);
+ assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the bottom.
+ mPositioner.resizeDrag(r.left, 1000.0f);
+ assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+ }
+
+ /**
+ * This tests that a constrained landscape window will keep the aspect and do the
+ * right things upon resizing when dragged from the top corner.
+ */
+ @Test
+ @Ignore
+ public void testLandscapePreservedWindowResizingDragTop() throws Exception {
+ final Rect r = new Rect(100, 220, 700, 520);
+ final int midX = (r.left + r.right) / 2;
+
+ mPositioner.startDrag(true /*resizing*/,
+ true /*preserveOrientation*/, midX, r.top - MOUSE_DELTA_Y, r);
+
+ // Drag to the left (no change).
+ mPositioner.resizeDrag(0.0f, r.top);
+ assertBoundsEquals(new Rect(r.left, r.top + MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the right (no change).
+ mPositioner.resizeDrag(2000.0f, r.top);
+ assertBoundsEquals(new Rect(r.left , r.top + MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the top.
+ mPositioner.resizeDrag(300.0f, 0.0f);
+ int h = r.bottom - MOUSE_DELTA_Y;
+ int w = Math.max(r.right - r.left, Math.round(h * MIN_ASPECT));
+ assertBoundsEquals(new Rect(r.left, MOUSE_DELTA_Y, r.left + w, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the bottom.
+ mPositioner.resizeDrag(r.left, 1000.0f);
+ h = mMinVisibleHeight;
+ assertBoundsEquals(new Rect(r.left, r.bottom - h, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+ }
+
+ /**
+ * This tests that a constrained portrait window will keep the aspect and do the
+ * right things upon resizing when dragged from the top left corner.
+ */
+ @Test
+ @Ignore
+ public void testPortraitPreservedWindowResizingDragTopLeft() throws Exception {
+ final Rect r = new Rect(330, 100, 630, 600);
+
+ mPositioner.startDrag(true /*resizing*/,
+ true /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y, r);
+ assertBoundsEquals(r, mPositioner.getWindowDragBounds());
+
+ // Drag to a good landscape size.
+ mPositioner.resizeDrag(0.0f, 0.0f);
+ int height = Math.round((float) (r.right - MOUSE_DELTA_X) * MIN_ASPECT);
+ assertBoundsEquals(new Rect(MOUSE_DELTA_X, r.bottom - height, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to a good portrait size.
+ mPositioner.resizeDrag(500.0f, 0.0f);
+ assertBoundsEquals(new Rect(500 + MOUSE_DELTA_X, MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to a too small size for the height and the the width shrinking.
+ mPositioner.resizeDrag(r.left + MOUSE_DELTA_X, 2000.0f);
+ final int w = Math.max(mMinVisibleWidth, Math.round(mMinVisibleHeight / MIN_ASPECT));
+ final int h = Math.max(mMinVisibleHeight, Math.round(w * MIN_ASPECT));
+ assertBoundsEquals(
+ new Rect(r.right - w, r.bottom - h, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+ }
+
+ /**
+ * This tests that a constrained portrait window will keep the aspect and do the
+ * right things upon resizing when dragged from the left corner.
+ */
+ @Test
+ @Ignore
+ public void testPortraitPreservedWindowResizingDragLeft() throws Exception {
+ final Rect r = new Rect(330, 100, 630, 600);
+ final int midY = (r.top + r.bottom) / 2;
+
+ mPositioner.startDrag(true /*resizing*/,
+ true /*preserveOrientation*/, r.left - MOUSE_DELTA_X, midY, r);
+
+ // Drag to the left.
+ mPositioner.resizeDrag(0.0f, midY);
+ int w = r.right - MOUSE_DELTA_X;
+ int h = Math.round(w * MIN_ASPECT);
+ assertBoundsEquals(new Rect(MOUSE_DELTA_X, r.top, r.right, r.top + h),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the right.
+ mPositioner.resizeDrag(450.0f, midY);
+ assertBoundsEquals(new Rect(450 + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag all the way to the right.
+ mPositioner.resizeDrag(2000.0f, midY);
+ w = mMinVisibleWidth;
+ h = Math.max(Math.round((float)w * MIN_ASPECT), r.height());
+ assertBoundsEquals(new Rect(r.right - w, r.top, r.right, r.top + h),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the top.
+ mPositioner.resizeDrag(r.left, 0.0f);
+ assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the bottom.
+ mPositioner.resizeDrag(r.left, 1000.0f);
+ assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+ }
+
+ /**
+ * This tests that a constrained portrait window will keep the aspect and do the
+ * right things upon resizing when dragged from the top corner.
+ */
+ @Test
+ @Ignore
+ public void testPortraitPreservedWindowResizingDragTop() throws Exception {
+ final Rect r = new Rect(330, 100, 630, 600);
+ final int midX = (r.left + r.right) / 2;
+
+ mPositioner.startDrag(true /*resizing*/,
+ true /*preserveOrientation*/, midX, r.top - MOUSE_DELTA_Y, r);
+
+ // Drag to the left (no change).
+ mPositioner.resizeDrag(0.0f, r.top);
+ assertBoundsEquals(new Rect(r.left, r.top + MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the right (no change).
+ mPositioner.resizeDrag(2000.0f, r.top);
+ assertBoundsEquals(new Rect(r.left , r.top + MOUSE_DELTA_Y, r.right, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the top.
+ mPositioner.resizeDrag(300.0f, 0.0f);
+ int h = r.bottom - MOUSE_DELTA_Y;
+ int w = Math.min(r.width(), Math.round(h / MIN_ASPECT));
+ assertBoundsEquals(new Rect(r.left, MOUSE_DELTA_Y, r.left + w, r.bottom),
+ mPositioner.getWindowDragBounds());
+
+ // Drag to the bottom.
+ mPositioner.resizeDrag(r.left, 1000.0f);
+ h = Math.max(mMinVisibleHeight, Math.round(mMinVisibleWidth * MIN_ASPECT));
+ w = Math.round(h / MIN_ASPECT);
+ assertBoundsEquals(new Rect(r.left, r.bottom - h, r.left + w, r.bottom),
+ mPositioner.getWindowDragBounds());
+ }
+
+ private void assertBoundsEquals(Rect expected, Rect actual) {
+ if (DEBUGGING) {
+ if (!expected.equals(actual)) {
+ Log.e(TAG, "rect(" + actual.toString() + ") != isRect(" + actual.toString()
+ + ") " + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ assertEquals(expected.left, actual.left);
+ assertEquals(expected.right, actual.right);
+ assertEquals(expected.top, actual.top);
+ assertEquals(expected.bottom, actual.bottom);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskPositioningControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/TaskPositioningControllerTests.java
new file mode 100644
index 0000000..6070516
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskPositioningControllerTests.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.InputChannel;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for the {@link TaskPositioningController} class.
+ *
+ * atest com.android.server.wm.TaskPositioningControllerTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@Presubmit
+public class TaskPositioningControllerTests extends WindowTestsBase {
+ private static final int TIMEOUT_MS = 1000;
+ private TaskPositioningController mTarget;
+ private WindowState mWindow;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ assertNotNull(sWm.mTaskPositioningController);
+ mTarget = sWm.mTaskPositioningController;
+
+ when(sWm.mInputManager.transferTouchFocus(
+ any(InputChannel.class),
+ any(InputChannel.class))).thenReturn(true);
+
+ mWindow = createWindow(null, TYPE_BASE_APPLICATION, "window");
+ mWindow.mInputChannel = new InputChannel();
+ synchronized (sWm.mWindowMap) {
+ sWm.mWindowMap.put(mWindow.mClient.asBinder(), mWindow);
+ }
+ }
+
+ @Test
+ public void testStartAndFinishPositioning() throws Exception {
+ synchronized (sWm.mWindowMap) {
+ assertFalse(mTarget.isPositioningLocked());
+ assertNull(mTarget.getDragWindowHandleLocked());
+ }
+
+ assertTrue(mTarget.startMovingTask(mWindow.mClient, 0, 0));
+
+ synchronized (sWm.mWindowMap) {
+ assertTrue(mTarget.isPositioningLocked());
+ assertNotNull(mTarget.getDragWindowHandleLocked());
+ }
+
+ mTarget.finishTaskPositioning();
+ // Wait until the looper processes finishTaskPositioning.
+ assertTrue(sWm.mH.runWithScissors(() -> {}, TIMEOUT_MS));
+
+ assertFalse(mTarget.isPositioningLocked());
+ assertNull(mTarget.getDragWindowHandleLocked());
+ }
+
+ @Test
+ public void testHandleTapOutsideTask() throws Exception {
+ synchronized (sWm.mWindowMap) {
+
+ assertFalse(mTarget.isPositioningLocked());
+ assertNull(mTarget.getDragWindowHandleLocked());
+ }
+
+ final DisplayContent content = mock(DisplayContent.class);
+ when(content.findTaskForResizePoint(anyInt(), anyInt())).thenReturn(mWindow.getTask());
+ assertNotNull(mWindow.getTask().getTopVisibleAppMainWindow());
+
+ mTarget.handleTapOutsideTask(content, 0, 0);
+ // Wait until the looper processes finishTaskPositioning.
+ assertTrue(sWm.mH.runWithScissors(() -> {}, TIMEOUT_MS));
+
+ synchronized (sWm.mWindowMap) {
+ assertTrue(mTarget.isPositioningLocked());
+ assertNotNull(mTarget.getDragWindowHandleLocked());
+ }
+
+ mTarget.finishTaskPositioning();
+ // Wait until the looper processes finishTaskPositioning.
+ assertTrue(sWm.mH.runWithScissors(() -> {}, TIMEOUT_MS));
+
+ assertFalse(mTarget.isPositioningLocked());
+ assertNull(mTarget.getDragWindowHandleLocked());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotCacheTest.java b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotCacheTest.java
new file mode 100644
index 0000000..649de4a
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotCacheTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test class for {@link TaskSnapshotCache}.
+ *
+ * runtest frameworks-services -c com.android.server.wm.TaskSnapshotCacheTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class TaskSnapshotCacheTest extends TaskSnapshotPersisterTestBase {
+
+ private TaskSnapshotCache mCache;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mCache = new TaskSnapshotCache(sWm, mLoader);
+ }
+
+ @Test
+ public void testAppRemoved() throws Exception {
+ final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ mCache.putSnapshot(window.getTask(), createSnapshot());
+ assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, 0 /* userId */,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+ mCache.onAppRemoved(window.mAppToken);
+ assertNull(mCache.getSnapshot(window.getTask().mTaskId, 0 /* userId */,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+ }
+
+ @Test
+ public void testAppDied() throws Exception {
+ final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ mCache.putSnapshot(window.getTask(), createSnapshot());
+ assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, 0 /* userId */,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+ mCache.onAppDied(window.mAppToken);
+ assertNull(mCache.getSnapshot(window.getTask().mTaskId, 0 /* userId */,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+ }
+
+ @Test
+ public void testTaskRemoved() throws Exception {
+ final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ mCache.putSnapshot(window.getTask(), createSnapshot());
+ assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, 0 /* userId */,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+ mCache.onTaskRemoved(window.getTask().mTaskId);
+ assertNull(mCache.getSnapshot(window.getTask().mTaskId, 0 /* userId */,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+ }
+
+ @Test
+ public void testReduced_notCached() throws Exception {
+ final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ mPersister.persistSnapshot(window.getTask().mTaskId, sWm.mCurrentUserId, createSnapshot());
+ mPersister.waitForQueueEmpty();
+ assertNull(mCache.getSnapshot(window.getTask().mTaskId, sWm.mCurrentUserId,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+
+ // Load it from disk
+ assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, sWm.mCurrentUserId,
+ true /* restoreFromDisk */, true /* reducedResolution */));
+
+ // Make sure it's not in the cache now.
+ assertNull(mCache.getSnapshot(window.getTask().mTaskId, sWm.mCurrentUserId,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+ }
+
+ @Test
+ public void testRestoreFromDisk() throws Exception {
+ final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ mPersister.persistSnapshot(window.getTask().mTaskId, sWm.mCurrentUserId, createSnapshot());
+ mPersister.waitForQueueEmpty();
+ assertNull(mCache.getSnapshot(window.getTask().mTaskId, sWm.mCurrentUserId,
+ false /* restoreFromDisk */, false /* reducedResolution */));
+
+ // Load it from disk
+ assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, sWm.mCurrentUserId,
+ true /* restoreFromDisk */, false /* reducedResolution */));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotControllerTest.java b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotControllerTest.java
new file mode 100644
index 0000000..5650050
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotControllerTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
+import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
+import static android.view.WindowManager.TRANSIT_UNSET;
+import static com.android.server.wm.TaskSnapshotController.*;
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.ArraySet;
+
+import com.google.android.collect.Sets;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test class for {@link TaskSnapshotController}.
+ *
+ * runtest frameworks-services -c com.android.server.wm.TaskSnapshotControllerTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class TaskSnapshotControllerTest extends WindowTestsBase {
+
+ @Test
+ public void testGetClosingApps_closing() throws Exception {
+ final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
+ "closingWindow");
+ closingWindow.mAppToken.setVisibility(null, false /* visible */, TRANSIT_UNSET,
+ true /* performLayout */, false /* isVoiceInteraction */);
+ final ArraySet<AppWindowToken> closingApps = new ArraySet<>();
+ closingApps.add(closingWindow.mAppToken);
+ final ArraySet<Task> closingTasks = new ArraySet<>();
+ sWm.mTaskSnapshotController.getClosingTasks(closingApps, closingTasks);
+ assertEquals(1, closingTasks.size());
+ assertEquals(closingWindow.mAppToken.getTask(), closingTasks.valueAt(0));
+ }
+
+ @Test
+ public void testGetClosingApps_notClosing() throws Exception {
+ final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
+ "closingWindow");
+ final WindowState openingWindow = createAppWindow(closingWindow.getTask(),
+ FIRST_APPLICATION_WINDOW, "openingWindow");
+ closingWindow.mAppToken.setVisibility(null, false /* visible */, TRANSIT_UNSET,
+ true /* performLayout */, false /* isVoiceInteraction */);
+ openingWindow.mAppToken.setVisibility(null, true /* visible */, TRANSIT_UNSET,
+ true /* performLayout */, false /* isVoiceInteraction */);
+ final ArraySet<AppWindowToken> closingApps = new ArraySet<>();
+ closingApps.add(closingWindow.mAppToken);
+ final ArraySet<Task> closingTasks = new ArraySet<>();
+ sWm.mTaskSnapshotController.getClosingTasks(closingApps, closingTasks);
+ assertEquals(0, closingTasks.size());
+ }
+
+ @Test
+ public void testGetClosingApps_skipClosingAppsSnapshotTasks() throws Exception {
+ final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
+ "closingWindow");
+ closingWindow.mAppToken.setVisibility(null, false /* visible */, TRANSIT_UNSET,
+ true /* performLayout */, false /* isVoiceInteraction */);
+ final ArraySet<AppWindowToken> closingApps = new ArraySet<>();
+ closingApps.add(closingWindow.mAppToken);
+ final ArraySet<Task> closingTasks = new ArraySet<>();
+ sWm.mTaskSnapshotController.addSkipClosingAppSnapshotTasks(
+ Sets.newArraySet(closingWindow.mAppToken.getTask()));
+ sWm.mTaskSnapshotController.getClosingTasks(closingApps, closingTasks);
+ assertEquals(0, closingTasks.size());
+ }
+
+ @Test
+ public void testGetSnapshotMode() throws Exception {
+ final WindowState disabledWindow = createWindow(null,
+ FIRST_APPLICATION_WINDOW, mDisplayContent, "disabledWindow");
+ disabledWindow.mAppToken.setDisablePreviewScreenshots(true);
+ assertEquals(SNAPSHOT_MODE_APP_THEME,
+ sWm.mTaskSnapshotController.getSnapshotMode(disabledWindow.getTask()));
+
+ final WindowState normalWindow = createWindow(null,
+ FIRST_APPLICATION_WINDOW, mDisplayContent, "normalWindow");
+ assertEquals(SNAPSHOT_MODE_REAL,
+ sWm.mTaskSnapshotController.getSnapshotMode(normalWindow.getTask()));
+
+ final WindowState secureWindow = createWindow(null,
+ FIRST_APPLICATION_WINDOW, mDisplayContent, "secureWindow");
+ secureWindow.mAttrs.flags |= FLAG_SECURE;
+ assertEquals(SNAPSHOT_MODE_APP_THEME,
+ sWm.mTaskSnapshotController.getSnapshotMode(secureWindow.getTask()));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java
new file mode 100644
index 0000000..325d42a
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.ActivityManager.TaskSnapshot;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.ArraySet;
+
+import android.view.View;
+import com.android.server.wm.TaskSnapshotPersister.RemoveObsoleteFilesQueueItem;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+/**
+ * Test class for {@link TaskSnapshotPersister} and {@link TaskSnapshotLoader}
+ *
+ * atest FrameworksServicesTests:TaskSnapshotPersisterLoaderTest
+ */
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class TaskSnapshotPersisterLoaderTest extends TaskSnapshotPersisterTestBase {
+
+ private static final Rect TEST_INSETS = new Rect(10, 20, 30, 40);
+
+ @Test
+ public void testPersistAndLoadSnapshot() {
+ mPersister.persistSnapshot(1 , mTestUserId, createSnapshot());
+ mPersister.waitForQueueEmpty();
+ final File[] files = new File[] { new File(sFilesDir.getPath() + "/snapshots/1.proto"),
+ new File(sFilesDir.getPath() + "/snapshots/1.jpg"),
+ new File(sFilesDir.getPath() + "/snapshots/1_reduced.jpg")};
+ assertTrueForFiles(files, File::exists, " must exist");
+ final TaskSnapshot snapshot = mLoader.loadTask(1, mTestUserId, false /* reduced */);
+ assertNotNull(snapshot);
+ assertEquals(TEST_INSETS, snapshot.getContentInsets());
+ assertNotNull(snapshot.getSnapshot());
+ assertEquals(Configuration.ORIENTATION_PORTRAIT, snapshot.getOrientation());
+ }
+
+ private void assertTrueForFiles(File[] files, Predicate<File> predicate, String message) {
+ for (File file : files) {
+ assertTrue(file.getName() + message, predicate.apply(file));
+ }
+ }
+
+ @Test
+ public void testTaskRemovedFromRecents() {
+ mPersister.persistSnapshot(1, mTestUserId, createSnapshot());
+ mPersister.onTaskRemovedFromRecents(1, mTestUserId);
+ mPersister.waitForQueueEmpty();
+ assertFalse(new File(sFilesDir.getPath() + "/snapshots/1.proto").exists());
+ assertFalse(new File(sFilesDir.getPath() + "/snapshots/1.jpg").exists());
+ assertFalse(new File(sFilesDir.getPath() + "/snapshots/1_reduced.jpg").exists());
+ }
+
+ /**
+ * Tests that persisting a couple of snapshots is being throttled.
+ */
+ @Test
+ public void testThrottling() {
+ long ms = SystemClock.elapsedRealtime();
+ mPersister.persistSnapshot(1, mTestUserId, createSnapshot());
+ mPersister.persistSnapshot(2, mTestUserId, createSnapshot());
+ mPersister.removeObsoleteFiles(new ArraySet<>(), new int[] { mTestUserId });
+ mPersister.removeObsoleteFiles(new ArraySet<>(), new int[] { mTestUserId });
+ mPersister.removeObsoleteFiles(new ArraySet<>(), new int[] { mTestUserId });
+ mPersister.removeObsoleteFiles(new ArraySet<>(), new int[] { mTestUserId });
+ mPersister.waitForQueueEmpty();
+ assertTrue(SystemClock.elapsedRealtime() - ms > 500);
+ }
+
+ /**
+ * Tests that too many store write queue items are being purged.
+ */
+ @Test
+ public void testPurging() {
+ mPersister.persistSnapshot(100, mTestUserId, createSnapshot());
+ mPersister.waitForQueueEmpty();
+ mPersister.setPaused(true);
+ mPersister.persistSnapshot(1, mTestUserId, createSnapshot());
+ mPersister.removeObsoleteFiles(new ArraySet<>(), new int[] { mTestUserId });
+ mPersister.persistSnapshot(2, mTestUserId, createSnapshot());
+ mPersister.persistSnapshot(3, mTestUserId, createSnapshot());
+ mPersister.persistSnapshot(4, mTestUserId, createSnapshot());
+ mPersister.setPaused(false);
+ mPersister.waitForQueueEmpty();
+
+ // Make sure 1,2 were purged but removeObsoleteFiles wasn't.
+ final File[] existsFiles = new File[] {
+ new File(sFilesDir.getPath() + "/snapshots/3.proto"),
+ new File(sFilesDir.getPath() + "/snapshots/4.proto")};
+ final File[] nonExistsFiles = new File[] {
+ new File(sFilesDir.getPath() + "/snapshots/100.proto"),
+ new File(sFilesDir.getPath() + "/snapshots/1.proto"),
+ new File(sFilesDir.getPath() + "/snapshots/1.proto")};
+ assertTrueForFiles(existsFiles, File::exists, " must exist");
+ assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist");
+ }
+
+ @Test
+ public void testGetTaskId() {
+ RemoveObsoleteFilesQueueItem removeObsoleteFilesQueueItem =
+ mPersister.new RemoveObsoleteFilesQueueItem(new ArraySet<>(), new int[] {});
+ assertEquals(-1, removeObsoleteFilesQueueItem.getTaskId("blablablulp"));
+ assertEquals(-1, removeObsoleteFilesQueueItem.getTaskId("nothing.err"));
+ assertEquals(-1, removeObsoleteFilesQueueItem.getTaskId("/invalid/"));
+ assertEquals(12, removeObsoleteFilesQueueItem.getTaskId("12.jpg"));
+ assertEquals(12, removeObsoleteFilesQueueItem.getTaskId("12.proto"));
+ assertEquals(1, removeObsoleteFilesQueueItem.getTaskId("1.jpg"));
+ assertEquals(1, removeObsoleteFilesQueueItem.getTaskId("1_reduced.jpg"));
+ }
+
+ @Test
+ public void testLowResolutionPersistAndLoadSnapshot() {
+ TaskSnapshot a = createSnapshot(0.5f /* reducedResolution */);
+ assertTrue(a.isReducedResolution());
+ mPersister.persistSnapshot(1 , mTestUserId, a);
+ mPersister.waitForQueueEmpty();
+ final File[] files = new File[] { new File(sFilesDir.getPath() + "/snapshots/1.proto"),
+ new File(sFilesDir.getPath() + "/snapshots/1_reduced.jpg")};
+ final File[] nonExistsFiles = new File[] {
+ new File(sFilesDir.getPath() + "/snapshots/1.jpg"),
+ };
+ assertTrueForFiles(files, File::exists, " must exist");
+ assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist");
+ final TaskSnapshot snapshot = mLoader.loadTask(1, mTestUserId, true /* reduced */);
+ assertNotNull(snapshot);
+ assertEquals(TEST_INSETS, snapshot.getContentInsets());
+ assertNotNull(snapshot.getSnapshot());
+ assertEquals(Configuration.ORIENTATION_PORTRAIT, snapshot.getOrientation());
+
+ final TaskSnapshot snapshotNotExist = mLoader.loadTask(1, mTestUserId, false /* reduced */);
+ assertNull(snapshotNotExist);
+ }
+
+ @Test
+ public void testIsRealSnapshotPersistAndLoadSnapshot() {
+ TaskSnapshot a = new TaskSnapshotBuilder()
+ .setIsRealSnapshot(true)
+ .build();
+ TaskSnapshot b = new TaskSnapshotBuilder()
+ .setIsRealSnapshot(false)
+ .build();
+ assertTrue(a.isRealSnapshot());
+ assertFalse(b.isRealSnapshot());
+ mPersister.persistSnapshot(1, mTestUserId, a);
+ mPersister.persistSnapshot(2, mTestUserId, b);
+ mPersister.waitForQueueEmpty();
+ final TaskSnapshot snapshotA = mLoader.loadTask(1, mTestUserId, false /* reduced */);
+ final TaskSnapshot snapshotB = mLoader.loadTask(2, mTestUserId, false /* reduced */);
+ assertNotNull(snapshotA);
+ assertNotNull(snapshotB);
+ assertTrue(snapshotA.isRealSnapshot());
+ assertFalse(snapshotB.isRealSnapshot());
+ }
+
+ @Test
+ public void testWindowingModePersistAndLoadSnapshot() {
+ TaskSnapshot a = new TaskSnapshotBuilder()
+ .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+ .build();
+ TaskSnapshot b = new TaskSnapshotBuilder()
+ .setWindowingMode(WINDOWING_MODE_PINNED)
+ .build();
+ assertTrue(a.getWindowingMode() == WINDOWING_MODE_FULLSCREEN);
+ assertTrue(b.getWindowingMode() == WINDOWING_MODE_PINNED);
+ mPersister.persistSnapshot(1, mTestUserId, a);
+ mPersister.persistSnapshot(2, mTestUserId, b);
+ mPersister.waitForQueueEmpty();
+ final TaskSnapshot snapshotA = mLoader.loadTask(1, mTestUserId, false /* reduced */);
+ final TaskSnapshot snapshotB = mLoader.loadTask(2, mTestUserId, false /* reduced */);
+ assertNotNull(snapshotA);
+ assertNotNull(snapshotB);
+ assertTrue(snapshotA.getWindowingMode() == WINDOWING_MODE_FULLSCREEN);
+ assertTrue(snapshotB.getWindowingMode() == WINDOWING_MODE_PINNED);
+ }
+
+ @Test
+ public void testIsTranslucentPersistAndLoadSnapshot() {
+ TaskSnapshot a = new TaskSnapshotBuilder()
+ .setIsTranslucent(true)
+ .build();
+ TaskSnapshot b = new TaskSnapshotBuilder()
+ .setIsTranslucent(false)
+ .build();
+ assertTrue(a.isTranslucent());
+ assertFalse(b.isTranslucent());
+ mPersister.persistSnapshot(1, mTestUserId, a);
+ mPersister.persistSnapshot(2, mTestUserId, b);
+ mPersister.waitForQueueEmpty();
+ final TaskSnapshot snapshotA = mLoader.loadTask(1, mTestUserId, false /* reduced */);
+ final TaskSnapshot snapshotB = mLoader.loadTask(2, mTestUserId, false /* reduced */);
+ assertNotNull(snapshotA);
+ assertNotNull(snapshotB);
+ assertTrue(snapshotA.isTranslucent());
+ assertFalse(snapshotB.isTranslucent());
+ }
+
+ @Test
+ public void testSystemUiVisibilityPersistAndLoadSnapshot() {
+ final int lightBarFlags = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
+ TaskSnapshot a = new TaskSnapshotBuilder()
+ .setSystemUiVisibility(0)
+ .build();
+ TaskSnapshot b = new TaskSnapshotBuilder()
+ .setSystemUiVisibility(lightBarFlags)
+ .build();
+ assertTrue(a.getSystemUiVisibility() == 0);
+ assertTrue(b.getSystemUiVisibility() == lightBarFlags);
+ mPersister.persistSnapshot(1, mTestUserId, a);
+ mPersister.persistSnapshot(2, mTestUserId, b);
+ mPersister.waitForQueueEmpty();
+ final TaskSnapshot snapshotA = mLoader.loadTask(1, mTestUserId, false /* reduced */);
+ final TaskSnapshot snapshotB = mLoader.loadTask(2, mTestUserId, false /* reduced */);
+ assertNotNull(snapshotA);
+ assertNotNull(snapshotB);
+ assertTrue(snapshotA.getSystemUiVisibility() == 0);
+ assertTrue(snapshotB.getSystemUiVisibility() == lightBarFlags);
+ }
+
+ @Test
+ public void testRemoveObsoleteFiles() {
+ mPersister.persistSnapshot(1, mTestUserId, createSnapshot());
+ mPersister.persistSnapshot(2, mTestUserId, createSnapshot());
+ final ArraySet<Integer> taskIds = new ArraySet<>();
+ taskIds.add(1);
+ mPersister.removeObsoleteFiles(taskIds, new int[] { mTestUserId });
+ mPersister.waitForQueueEmpty();
+ final File[] existsFiles = new File[] {
+ new File(sFilesDir.getPath() + "/snapshots/1.proto"),
+ new File(sFilesDir.getPath() + "/snapshots/1.jpg"),
+ new File(sFilesDir.getPath() + "/snapshots/1_reduced.jpg") };
+ final File[] nonExistsFiles = new File[] {
+ new File(sFilesDir.getPath() + "/snapshots/2.proto"),
+ new File(sFilesDir.getPath() + "/snapshots/2.jpg"),
+ new File(sFilesDir.getPath() + "/snapshots/2_reduced.jpg")};
+ assertTrueForFiles(existsFiles, File::exists, " must exist");
+ assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist");
+ }
+
+ @Test
+ public void testRemoveObsoleteFiles_addedOneInTheMeantime() {
+ mPersister.persistSnapshot(1, mTestUserId, createSnapshot());
+ final ArraySet<Integer> taskIds = new ArraySet<>();
+ taskIds.add(1);
+ mPersister.removeObsoleteFiles(taskIds, new int[] { mTestUserId });
+ mPersister.persistSnapshot(2, mTestUserId, createSnapshot());
+ mPersister.waitForQueueEmpty();
+ final File[] existsFiles = new File[] {
+ new File(sFilesDir.getPath() + "/snapshots/1.proto"),
+ new File(sFilesDir.getPath() + "/snapshots/1.jpg"),
+ new File(sFilesDir.getPath() + "/snapshots/1_reduced.jpg"),
+ new File(sFilesDir.getPath() + "/snapshots/2.proto"),
+ new File(sFilesDir.getPath() + "/snapshots/2.jpg"),
+ new File(sFilesDir.getPath() + "/snapshots/2_reduced.jpg")};
+ assertTrueForFiles(existsFiles, File::exists, " must exist");
+ }
+
+ /**
+ * Private predicate definition.
+ *
+ * This is needed because com.android.internal.util.Predicate is deprecated
+ * and can only be used with classes fron android.test.runner. This cannot
+ * use java.util.function.Predicate because that is not present on all API
+ * versions that this test must run on.
+ */
+ private interface Predicate<T> {
+ boolean apply(T t);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
new file mode 100644
index 0000000..8b86043
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.graphics.GraphicBuffer.USAGE_HW_TEXTURE;
+import static android.graphics.GraphicBuffer.USAGE_SW_READ_RARELY;
+
+import android.app.ActivityManager.TaskSnapshot;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.GraphicBuffer;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.os.UserManager;
+import android.support.test.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+
+import java.io.File;
+
+/**
+ * Base class for tests that use a {@link TaskSnapshotPersister}.
+ */
+class TaskSnapshotPersisterTestBase extends WindowTestsBase {
+
+ private static final Rect TEST_INSETS = new Rect(10, 20, 30, 40);
+
+ TaskSnapshotPersister mPersister;
+ TaskSnapshotLoader mLoader;
+ int mTestUserId;
+ static File sFilesDir;
+
+ @BeforeClass
+ public static void setUpUser() {
+ sFilesDir = InstrumentationRegistry.getContext().getFilesDir();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ final UserManager um = UserManager.get(InstrumentationRegistry.getContext());
+ mTestUserId = um.getUserHandle();
+ mPersister = new TaskSnapshotPersister(userId -> sFilesDir);
+ mLoader = new TaskSnapshotLoader(mPersister);
+ mPersister.start();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ cleanDirectory();
+ }
+
+ private void cleanDirectory() {
+ final File[] files = new File(sFilesDir, "snapshots").listFiles();
+ if (files == null) {
+ return;
+ }
+ for (File file : files) {
+ if (!file.isDirectory()) {
+ file.delete();
+ }
+ }
+ }
+
+ TaskSnapshot createSnapshot() {
+ return createSnapshot(1f /* scale */);
+ }
+
+ TaskSnapshot createSnapshot(float scale) {
+ return new TaskSnapshotBuilder()
+ .setScale(scale)
+ .build();
+ }
+
+ /**
+ * Builds a TaskSnapshot.
+ */
+ class TaskSnapshotBuilder {
+
+ private float mScale = 1f;
+ private boolean mIsRealSnapshot = true;
+ private boolean mIsTranslucent = false;
+ private int mWindowingMode = WINDOWING_MODE_FULLSCREEN;
+ private int mSystemUiVisibility = 0;
+
+ public TaskSnapshotBuilder setScale(float scale) {
+ mScale = scale;
+ return this;
+ }
+
+ public TaskSnapshotBuilder setIsRealSnapshot(boolean isRealSnapshot) {
+ mIsRealSnapshot = isRealSnapshot;
+ return this;
+ }
+
+ public TaskSnapshotBuilder setIsTranslucent(boolean isTranslucent) {
+ mIsTranslucent = isTranslucent;
+ return this;
+ }
+
+ public TaskSnapshotBuilder setWindowingMode(int windowingMode) {
+ mWindowingMode = windowingMode;
+ return this;
+ }
+
+ public TaskSnapshotBuilder setSystemUiVisibility(int systemUiVisibility) {
+ mSystemUiVisibility = systemUiVisibility;
+ return this;
+ }
+
+ public TaskSnapshot build() {
+ final GraphicBuffer buffer = GraphicBuffer.create(100, 100, PixelFormat.RGBA_8888,
+ USAGE_HW_TEXTURE | USAGE_SW_READ_RARELY | USAGE_SW_READ_RARELY);
+ Canvas c = buffer.lockCanvas();
+ c.drawColor(Color.RED);
+ buffer.unlockCanvasAndPost(c);
+ return new TaskSnapshot(buffer, ORIENTATION_PORTRAIT, TEST_INSETS,
+ mScale < 1f /* reducedResolution */, mScale, mIsRealSnapshot, mWindowingMode,
+ mSystemUiVisibility, mIsTranslucent);
+ }
+
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotSurfaceTest.java b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotSurfaceTest.java
new file mode 100644
index 0000000..b19373e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskSnapshotSurfaceTest.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager.TaskSnapshot;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.GraphicBuffer;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.Surface;
+
+import com.android.server.wm.TaskSnapshotSurface.Window;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test class for {@link TaskSnapshotSurface}.
+ *
+ * runtest frameworks-services -c com.android.server.wm.TaskSnapshotSurfaceTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class TaskSnapshotSurfaceTest extends WindowTestsBase {
+
+ private TaskSnapshotSurface mSurface;
+
+ private void setupSurface(int width, int height, Rect contentInsets, int sysuiVis,
+ int windowFlags, Rect taskBounds) {
+ final GraphicBuffer buffer = GraphicBuffer.create(width, height, PixelFormat.RGBA_8888,
+ GraphicBuffer.USAGE_SW_READ_NEVER | GraphicBuffer.USAGE_SW_WRITE_NEVER);
+ final TaskSnapshot snapshot = new TaskSnapshot(buffer,
+ ORIENTATION_PORTRAIT, contentInsets, false, 1.0f, true /* isRealSnapshot */,
+ WINDOWING_MODE_FULLSCREEN, 0 /* systemUiVisibility */, false /* isTranslucent */);
+ mSurface = new TaskSnapshotSurface(sWm, new Window(), new Surface(), snapshot, "Test",
+ Color.WHITE, Color.RED, Color.BLUE, sysuiVis, windowFlags, 0, taskBounds,
+ ORIENTATION_PORTRAIT);
+ }
+
+ private void setupSurface(int width, int height) {
+ setupSurface(width, height, new Rect(), 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
+ new Rect(0, 0, width, height));
+ }
+
+ @Test
+ public void fillEmptyBackground_fillHorizontally() throws Exception {
+ setupSurface(200, 100);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(200);
+ when(mockCanvas.getHeight()).thenReturn(100);
+ mSurface.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 100, 200));
+ verify(mockCanvas).drawRect(eq(100.0f), eq(0.0f), eq(200.0f), eq(100.0f), any());
+ }
+
+ @Test
+ public void fillEmptyBackground_fillVertically() throws Exception {
+ setupSurface(100, 200);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(100);
+ when(mockCanvas.getHeight()).thenReturn(200);
+ mSurface.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 200, 100));
+ verify(mockCanvas).drawRect(eq(0.0f), eq(100.0f), eq(100.0f), eq(200.0f), any());
+ }
+
+ @Test
+ public void fillEmptyBackground_fillBoth() throws Exception {
+ setupSurface(200, 200);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(200);
+ when(mockCanvas.getHeight()).thenReturn(200);
+ mSurface.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 100, 100));
+ verify(mockCanvas).drawRect(eq(100.0f), eq(0.0f), eq(200.0f), eq(100.0f), any());
+ verify(mockCanvas).drawRect(eq(0.0f), eq(100.0f), eq(200.0f), eq(200.0f), any());
+ }
+
+ @Test
+ public void fillEmptyBackground_dontFill_sameSize() throws Exception {
+ setupSurface(100, 100);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(100);
+ when(mockCanvas.getHeight()).thenReturn(100);
+ mSurface.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 100, 100));
+ verify(mockCanvas, never()).drawRect(anyInt(), anyInt(), anyInt(), anyInt(), any());
+ }
+
+ @Test
+ public void fillEmptyBackground_dontFill_bitmapLarger() throws Exception {
+ setupSurface(100, 100);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(100);
+ when(mockCanvas.getHeight()).thenReturn(100);
+ mSurface.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 200, 200));
+ verify(mockCanvas, never()).drawRect(anyInt(), anyInt(), anyInt(), anyInt(), any());
+ }
+
+ @Test
+ public void testCalculateSnapshotCrop() {
+ setupSurface(100, 100, new Rect(0, 10, 0, 10), 0, 0, new Rect(0, 0, 100, 100));
+ assertEquals(new Rect(0, 0, 100, 90), mSurface.calculateSnapshotCrop());
+ }
+
+ @Test
+ public void testCalculateSnapshotCrop_taskNotOnTop() {
+ setupSurface(100, 100, new Rect(0, 10, 0, 10), 0, 0, new Rect(0, 50, 100, 100));
+ assertEquals(new Rect(0, 10, 100, 90), mSurface.calculateSnapshotCrop());
+ }
+
+ @Test
+ public void testCalculateSnapshotCrop_navBarLeft() {
+ setupSurface(100, 100, new Rect(10, 10, 0, 0), 0, 0, new Rect(0, 0, 100, 100));
+ assertEquals(new Rect(10, 0, 100, 100), mSurface.calculateSnapshotCrop());
+ }
+
+ @Test
+ public void testCalculateSnapshotCrop_navBarRight() {
+ setupSurface(100, 100, new Rect(0, 10, 10, 0), 0, 0, new Rect(0, 0, 100, 100));
+ assertEquals(new Rect(0, 0, 90, 100), mSurface.calculateSnapshotCrop());
+ }
+
+ @Test
+ public void testCalculateSnapshotFrame() {
+ setupSurface(100, 100);
+ final Rect insets = new Rect(0, 10, 0, 10);
+ mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+ assertEquals(new Rect(0, -10, 100, 70),
+ mSurface.calculateSnapshotFrame(new Rect(0, 10, 100, 90)));
+ }
+
+ @Test
+ public void testCalculateSnapshotFrame_navBarLeft() {
+ setupSurface(100, 100);
+ final Rect insets = new Rect(10, 10, 0, 0);
+ mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+ assertEquals(new Rect(0, -10, 90, 80),
+ mSurface.calculateSnapshotFrame(new Rect(10, 10, 100, 100)));
+ }
+
+ @Test
+ public void testDrawStatusBarBackground() {
+ setupSurface(100, 100);
+ final Rect insets = new Rect(0, 10, 10, 0);
+ mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(100);
+ when(mockCanvas.getHeight()).thenReturn(100);
+ mSurface.mSystemBarBackgroundPainter.drawStatusBarBackground(
+ mockCanvas, new Rect(0, 0, 50, 100), 10);
+ verify(mockCanvas).drawRect(eq(50.0f), eq(0.0f), eq(90.0f), eq(10.0f), any());
+ }
+
+ @Test
+ public void testDrawStatusBarBackground_nullFrame() {
+ setupSurface(100, 100);
+ final Rect insets = new Rect(0, 10, 10, 0);
+ mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(100);
+ when(mockCanvas.getHeight()).thenReturn(100);
+ mSurface.mSystemBarBackgroundPainter.drawStatusBarBackground(
+ mockCanvas, null, 10);
+ verify(mockCanvas).drawRect(eq(0.0f), eq(0.0f), eq(90.0f), eq(10.0f), any());
+ }
+
+ @Test
+ public void testDrawStatusBarBackground_nope() {
+ setupSurface(100, 100);
+ final Rect insets = new Rect(0, 10, 10, 0);
+ mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(100);
+ when(mockCanvas.getHeight()).thenReturn(100);
+ mSurface.mSystemBarBackgroundPainter.drawStatusBarBackground(
+ mockCanvas, new Rect(0, 0, 100, 100), 10);
+ verify(mockCanvas, never()).drawRect(anyInt(), anyInt(), anyInt(), anyInt(), any());
+ }
+
+ @Test
+ public void testDrawNavigationBarBackground() {
+ final Rect insets = new Rect(0, 10, 0, 10);
+ setupSurface(100, 100, insets, 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
+ new Rect(0, 0, 100, 100));
+ mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(100);
+ when(mockCanvas.getHeight()).thenReturn(100);
+ mSurface.mSystemBarBackgroundPainter.drawNavigationBarBackground(mockCanvas);
+ verify(mockCanvas).drawRect(eq(new Rect(0, 90, 100, 100)), any());
+ }
+
+ @Test
+ public void testDrawNavigationBarBackground_left() {
+ final Rect insets = new Rect(10, 10, 0, 0);
+ setupSurface(100, 100, insets, 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
+ new Rect(0, 0, 100, 100));
+ mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(100);
+ when(mockCanvas.getHeight()).thenReturn(100);
+ mSurface.mSystemBarBackgroundPainter.drawNavigationBarBackground(mockCanvas);
+ verify(mockCanvas).drawRect(eq(new Rect(0, 0, 10, 100)), any());
+ }
+
+ @Test
+ public void testDrawNavigationBarBackground_right() {
+ final Rect insets = new Rect(0, 10, 10, 0);
+ setupSurface(100, 100, insets, 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
+ new Rect(0, 0, 100, 100));
+ mSurface.setFrames(new Rect(0, 0, 100, 100), insets, insets);
+ final Canvas mockCanvas = mock(Canvas.class);
+ when(mockCanvas.getWidth()).thenReturn(100);
+ when(mockCanvas.getHeight()).thenReturn(100);
+ mSurface.mSystemBarBackgroundPainter.drawNavigationBarBackground(mockCanvas);
+ verify(mockCanvas).drawRect(eq(new Rect(90, 0, 100, 100)), any());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskStackContainersTests.java b/services/tests/servicestests/src/com/android/server/wm/TaskStackContainersTests.java
new file mode 100644
index 0000000..ca1994f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskStackContainersTests.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.Before;
+import org.junit.After;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+/**
+ * Tests for the {@link DisplayContent.TaskStackContainers} container in {@link DisplayContent}.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.TaskStackContainersTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class TaskStackContainersTests extends WindowTestsBase {
+
+ private TaskStack mPinnedStack;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mPinnedStack = createStackControllerOnStackOnDisplay(
+ WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD, mDisplayContent).mContainer;
+ // Stack should contain visible app window to be considered visible.
+ final Task pinnedTask = createTaskInStack(mPinnedStack, 0 /* userId */);
+ assertFalse(mPinnedStack.isVisible());
+ final WindowTestUtils.TestAppWindowToken pinnedApp =
+ WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ pinnedTask.addChild(pinnedApp, 0 /* addPos */);
+ assertTrue(mPinnedStack.isVisible());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mPinnedStack.removeImmediately();
+ }
+
+ @Test
+ public void testStackPositionChildAt() throws Exception {
+ // Test that always-on-top stack can't be moved to position other than top.
+ final TaskStack stack1 = createTaskStackOnDisplay(mDisplayContent);
+ final TaskStack stack2 = createTaskStackOnDisplay(mDisplayContent);
+
+ final WindowContainer taskStackContainer = stack1.getParent();
+
+ final int stack1Pos = taskStackContainer.mChildren.indexOf(stack1);
+ final int stack2Pos = taskStackContainer.mChildren.indexOf(stack2);
+ final int pinnedStackPos = taskStackContainer.mChildren.indexOf(mPinnedStack);
+ assertGreaterThan(pinnedStackPos, stack2Pos);
+ assertGreaterThan(stack2Pos, stack1Pos);
+
+ taskStackContainer.positionChildAt(WindowContainer.POSITION_BOTTOM, mPinnedStack, false);
+ assertEquals(taskStackContainer.mChildren.get(stack1Pos), stack1);
+ assertEquals(taskStackContainer.mChildren.get(stack2Pos), stack2);
+ assertEquals(taskStackContainer.mChildren.get(pinnedStackPos), mPinnedStack);
+
+ taskStackContainer.positionChildAt(1, mPinnedStack, false);
+ assertEquals(taskStackContainer.mChildren.get(stack1Pos), stack1);
+ assertEquals(taskStackContainer.mChildren.get(stack2Pos), stack2);
+ assertEquals(taskStackContainer.mChildren.get(pinnedStackPos), mPinnedStack);
+ }
+
+ @Test
+ public void testStackPositionBelowPinnedStack() throws Exception {
+ // Test that no stack can be above pinned stack.
+ final TaskStack stack1 = createTaskStackOnDisplay(mDisplayContent);
+
+ final WindowContainer taskStackContainer = stack1.getParent();
+
+ final int stackPos = taskStackContainer.mChildren.indexOf(stack1);
+ final int pinnedStackPos = taskStackContainer.mChildren.indexOf(mPinnedStack);
+ assertGreaterThan(pinnedStackPos, stackPos);
+
+ taskStackContainer.positionChildAt(WindowContainer.POSITION_TOP, stack1, false);
+ assertEquals(taskStackContainer.mChildren.get(stackPos), stack1);
+ assertEquals(taskStackContainer.mChildren.get(pinnedStackPos), mPinnedStack);
+
+ taskStackContainer.positionChildAt(taskStackContainer.mChildren.size() - 1, stack1, false);
+ assertEquals(taskStackContainer.mChildren.get(stackPos), stack1);
+ assertEquals(taskStackContainer.mChildren.get(pinnedStackPos), mPinnedStack);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskStackTests.java b/services/tests/servicestests/src/com/android/server/wm/TaskStackTests.java
new file mode 100644
index 0000000..eaf71f0
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskStackTests.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for the {@link TaskStack} class.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.TaskStackTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class TaskStackTests extends WindowTestsBase {
+
+ @Test
+ public void testStackPositionChildAt() throws Exception {
+ final TaskStack stack = createTaskStackOnDisplay(mDisplayContent);
+ final Task task1 = createTaskInStack(stack, 0 /* userId */);
+ final Task task2 = createTaskInStack(stack, 1 /* userId */);
+
+ // Current user task should be moved to top.
+ stack.positionChildAt(WindowContainer.POSITION_TOP, task1, false /* includingParents */);
+ assertEquals(stack.mChildren.get(0), task2);
+ assertEquals(stack.mChildren.get(1), task1);
+
+ // Non-current user won't be moved to top.
+ stack.positionChildAt(WindowContainer.POSITION_TOP, task2, false /* includingParents */);
+ assertEquals(stack.mChildren.get(0), task2);
+ assertEquals(stack.mChildren.get(1), task1);
+ }
+
+ @Test
+ public void testClosingAppDifferentStackOrientation() throws Exception {
+ final TaskStack stack = createTaskStackOnDisplay(mDisplayContent);
+ final Task task1 = createTaskInStack(stack, 0 /* userId */);
+ WindowTestUtils.TestAppWindowToken appWindowToken1 =
+ WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ task1.addChild(appWindowToken1, 0);
+ appWindowToken1.setOrientation(SCREEN_ORIENTATION_LANDSCAPE);
+
+ final Task task2 = createTaskInStack(stack, 1 /* userId */);
+ WindowTestUtils.TestAppWindowToken appWindowToken2 =
+ WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ task2.addChild(appWindowToken2, 0);
+ appWindowToken2.setOrientation(SCREEN_ORIENTATION_PORTRAIT);
+
+ assertEquals(SCREEN_ORIENTATION_PORTRAIT, stack.getOrientation());
+ sWm.mClosingApps.add(appWindowToken2);
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, stack.getOrientation());
+ }
+
+ @Test
+ public void testMoveTaskToBackDifferentStackOrientation() throws Exception {
+ final TaskStack stack = createTaskStackOnDisplay(mDisplayContent);
+ final Task task1 = createTaskInStack(stack, 0 /* userId */);
+ WindowTestUtils.TestAppWindowToken appWindowToken1 =
+ WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ task1.addChild(appWindowToken1, 0);
+ appWindowToken1.setOrientation(SCREEN_ORIENTATION_LANDSCAPE);
+
+ final Task task2 = createTaskInStack(stack, 1 /* userId */);
+ WindowTestUtils.TestAppWindowToken appWindowToken2 =
+ WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ task2.addChild(appWindowToken2, 0);
+ appWindowToken2.setOrientation(SCREEN_ORIENTATION_PORTRAIT);
+
+ assertEquals(SCREEN_ORIENTATION_PORTRAIT, stack.getOrientation());
+ task2.setSendingToBottom(true);
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, stack.getOrientation());
+ }
+
+ @Test
+ public void testStackRemoveImmediately() throws Exception {
+ final TaskStack stack = createTaskStackOnDisplay(mDisplayContent);
+ final Task task = createTaskInStack(stack, 0 /* userId */);
+ assertEquals(stack, task.mStack);
+
+ // Remove stack and check if its child is also removed.
+ stack.removeImmediately();
+ assertNull(stack.getDisplayContent());
+ assertNull(task.mStack);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskWindowContainerControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/TaskWindowContainerControllerTests.java
new file mode 100644
index 0000000..1dd9365
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskWindowContainerControllerTests.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test class for {@link TaskWindowContainerController}.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.TaskWindowContainerControllerTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class TaskWindowContainerControllerTests extends WindowTestsBase {
+
+ @Test
+ public void testRemoveContainer() throws Exception {
+ final WindowTestUtils.TestTaskWindowContainerController taskController =
+ new WindowTestUtils.TestTaskWindowContainerController(this);
+ final WindowTestUtils.TestAppWindowContainerController appController =
+ new WindowTestUtils.TestAppWindowContainerController(taskController);
+
+ taskController.removeContainer();
+ // Assert that the container was removed.
+ assertNull(taskController.mContainer);
+ assertNull(appController.mContainer);
+ }
+
+ @Test
+ public void testRemoveContainer_deferRemoval() throws Exception {
+ final WindowTestUtils.TestTaskWindowContainerController taskController =
+ new WindowTestUtils.TestTaskWindowContainerController(this);
+ final WindowTestUtils.TestAppWindowContainerController appController =
+ new WindowTestUtils.TestAppWindowContainerController(taskController);
+
+ final WindowTestUtils.TestTask task = (WindowTestUtils.TestTask) taskController.mContainer;
+ final AppWindowToken app = appController.mContainer;
+ task.mShouldDeferRemoval = true;
+
+ taskController.removeContainer();
+ // For the case of deferred removal the task controller will no longer be connected to the
+ // container, but the app controller will still be connected to the its container until
+ // the task window container is removed.
+ assertNull(taskController.mContainer);
+ assertNull(task.getController());
+ assertNotNull(appController.mContainer);
+ assertNotNull(app.getController());
+
+ task.removeImmediately();
+ assertNull(appController.mContainer);
+ assertNull(app.getController());
+ }
+
+ @Test
+ public void testReparent() throws Exception {
+ final StackWindowController stackController1 =
+ createStackControllerOnDisplay(mDisplayContent);
+ final WindowTestUtils.TestTaskWindowContainerController taskController =
+ new WindowTestUtils.TestTaskWindowContainerController(stackController1);
+ final StackWindowController stackController2 =
+ createStackControllerOnDisplay(mDisplayContent);
+ final WindowTestUtils.TestTaskWindowContainerController taskController2 =
+ new WindowTestUtils.TestTaskWindowContainerController(stackController2);
+
+ boolean gotException = false;
+ try {
+ taskController.reparent(stackController1, 0, false/* moveParents */);
+ } catch (IllegalArgumentException e) {
+ gotException = true;
+ }
+ assertTrue("Should not be able to reparent to the same parent", gotException);
+
+ final StackWindowController stackController3 =
+ createStackControllerOnDisplay(mDisplayContent);
+ stackController3.setContainer(null);
+ gotException = false;
+ try {
+ taskController.reparent(stackController3, 0, false/* moveParents */);
+ } catch (IllegalArgumentException e) {
+ gotException = true;
+ }
+ assertTrue("Should not be able to reparent to a stack that doesn't have a container",
+ gotException);
+
+ taskController.reparent(stackController2, 0, false/* moveParents */);
+ assertEquals(stackController2.mContainer, taskController.mContainer.getParent());
+ assertEquals(0, ((WindowTestUtils.TestTask) taskController.mContainer).positionInParent());
+ assertEquals(1, ((WindowTestUtils.TestTask) taskController2.mContainer).positionInParent());
+ }
+
+ @Test
+ public void testReparent_BetweenDisplays() throws Exception {
+ // Create first stack on primary display.
+ final StackWindowController stack1Controller =
+ createStackControllerOnDisplay(mDisplayContent);
+ final TaskStack stack1 = stack1Controller.mContainer;
+ final WindowTestUtils.TestTaskWindowContainerController taskController =
+ new WindowTestUtils.TestTaskWindowContainerController(stack1Controller);
+ final WindowTestUtils.TestTask task1 = (WindowTestUtils.TestTask) taskController.mContainer;
+ task1.mOnDisplayChangedCalled = false;
+ assertEquals(mDisplayContent, stack1.getDisplayContent());
+
+ // Create second display and put second stack on it.
+ final DisplayContent dc = createNewDisplay();
+ final StackWindowController stack2Controller = createStackControllerOnDisplay(dc);
+ final TaskStack stack2 = stack2Controller.mContainer;
+ final WindowTestUtils.TestTaskWindowContainerController taskController2 =
+ new WindowTestUtils.TestTaskWindowContainerController(stack2Controller);
+ final WindowTestUtils.TestTask task2 =
+ (WindowTestUtils.TestTask) taskController2.mContainer;
+
+ // Reparent and check state
+ taskController.reparent(stack2Controller, 0, false /* moveParents */);
+ assertEquals(stack2, task1.getParent());
+ assertEquals(0, task1.positionInParent());
+ assertEquals(1, task2.positionInParent());
+ assertTrue(task1.mOnDisplayChangedCalled);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TestIWindow.java b/services/tests/servicestests/src/com/android/server/wm/TestIWindow.java
new file mode 100644
index 0000000..353aa7d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TestIWindow.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import com.android.internal.os.IResultReceiver;
+
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.MergedConfiguration;
+import android.view.DisplayCutout;
+import android.view.DragEvent;
+import android.view.IWindow;
+
+public class TestIWindow extends IWindow.Stub {
+ @Override
+ public void executeCommand(String command, String parameters,
+ ParcelFileDescriptor descriptor)
+ throws RemoteException {
+
+ }
+
+ @Override
+ public void resized(Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets,
+ Rect stableInsets, Rect outsets, boolean reportDraw, MergedConfiguration mergedConfig,
+ Rect backDropFrame, boolean forceLayout, boolean alwaysConsumeNavBar, int displayId,
+ DisplayCutout.ParcelableWrapper displayCutout)
+ throws RemoteException {
+
+ }
+
+ @Override
+ public void moved(int newX, int newY) throws RemoteException {
+
+ }
+
+ @Override
+ public void dispatchAppVisibility(boolean visible) throws RemoteException {
+
+ }
+
+ @Override
+ public void dispatchGetNewSurface() throws RemoteException {
+
+ }
+
+ @Override
+ public void windowFocusChanged(boolean hasFocus, boolean inTouchMode)
+ throws RemoteException {
+
+ }
+
+ @Override
+ public void closeSystemDialogs(String reason) throws RemoteException {
+
+ }
+
+ @Override
+ public void dispatchWallpaperOffsets(float x, float y, float xStep, float yStep,
+ boolean sync)
+ throws RemoteException {
+
+ }
+
+ @Override
+ public void dispatchWallpaperCommand(String action, int x, int y, int z, Bundle extras,
+ boolean sync) throws RemoteException {
+
+ }
+
+ @Override
+ public void dispatchDragEvent(DragEvent event) throws RemoteException {
+
+ }
+
+ @Override
+ public void updatePointerIcon(float x, float y) throws RemoteException {
+
+ }
+
+ @Override
+ public void dispatchSystemUiVisibilityChanged(int seq, int globalVisibility, int localValue,
+ int localChanges) throws RemoteException {
+
+ }
+
+ @Override
+ public void dispatchWindowShown() throws RemoteException {
+
+ }
+
+ @Override
+ public void requestAppKeyboardShortcuts(IResultReceiver receiver, int deviceId)
+ throws RemoteException {
+
+ }
+
+ @Override
+ public void dispatchPointerCaptureChanged(boolean hasCapture) {
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/servicestests/src/com/android/server/wm/TestWindowManagerPolicy.java
new file mode 100644
index 0000000..ee028ba
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TestWindowManagerPolicy.java
@@ -0,0 +1,606 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
+import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
+
+import static org.mockito.Mockito.mock;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.CompatibilityInfo;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.proto.ProtoOutputStream;
+import android.view.Display;
+import android.view.DisplayCutout;
+import android.view.IWindow;
+import android.view.IWindowManager;
+import android.view.KeyEvent;
+import android.view.WindowManager;
+import android.view.animation.Animation;
+
+import com.android.internal.policy.IKeyguardDismissCallback;
+import com.android.internal.policy.IShortcutService;
+import com.android.server.policy.WindowManagerPolicy;
+
+import java.io.PrintWriter;
+import java.util.function.Supplier;
+
+class TestWindowManagerPolicy implements WindowManagerPolicy {
+ private static final String TAG = "TestWindowManagerPolicy";
+
+ private final Supplier<WindowManagerService> mWmSupplier;
+
+ int rotationToReport = 0;
+ boolean keyguardShowingAndNotOccluded = false;
+
+ private Runnable mRunnableWhenAddingSplashScreen;
+
+ public TestWindowManagerPolicy(Supplier<WindowManagerService> wmSupplier) {
+
+ mWmSupplier = wmSupplier;
+ }
+
+ @Override
+ public void registerShortcutKey(long shortcutCode, IShortcutService shortcutKeyReceiver)
+ throws RemoteException {
+
+ }
+
+ @Override
+ public void init(Context context, IWindowManager windowManager,
+ WindowManagerFuncs windowManagerFuncs) {
+
+ }
+
+ @Override
+ public boolean isDefaultOrientationForced() {
+ return false;
+ }
+
+ @Override
+ public void setInitialDisplaySize(Display display, int width, int height, int density) {
+
+ }
+
+ @Override
+ public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {
+ return 0;
+ }
+
+ @Override
+ public boolean checkShowToOwnerOnly(WindowManager.LayoutParams attrs) {
+ return false;
+ }
+
+ @Override
+ public void adjustWindowParamsLw(WindowState win, WindowManager.LayoutParams attrs,
+ boolean hasStatusBarServicePermission) {
+ }
+
+ @Override
+ public void adjustConfigurationLw(Configuration config, int keyboardPresence,
+ int navigationPresence) {
+
+ }
+
+ @Override
+ public int getMaxWallpaperLayer() {
+ return 0;
+ }
+
+ @Override
+ public int getNonDecorDisplayWidth(int fullWidth, int fullHeight, int rotation, int uiMode,
+ int displayId, DisplayCutout displayCutout) {
+ return 0;
+ }
+
+ @Override
+ public int getNonDecorDisplayHeight(int fullWidth, int fullHeight, int rotation, int uiMode,
+ int displayId, DisplayCutout displayCutout) {
+ return 0;
+ }
+
+ @Override
+ public int getConfigDisplayWidth(int fullWidth, int fullHeight, int rotation, int uiMode,
+ int displayId, DisplayCutout displayCutout) {
+ return 0;
+ }
+
+ @Override
+ public int getConfigDisplayHeight(int fullWidth, int fullHeight, int rotation, int uiMode,
+ int displayId, DisplayCutout displayCutout) {
+ return 0;
+ }
+
+ @Override
+ public boolean isKeyguardHostWindow(WindowManager.LayoutParams attrs) {
+ return attrs.type == TYPE_STATUS_BAR;
+ }
+
+ @Override
+ public boolean canBeHiddenByKeyguardLw(WindowState win) {
+ return false;
+ }
+
+ /**
+ * Sets a runnable to run when adding a splash screen which gets executed after the window has
+ * been added but before returning the surface.
+ */
+ void setRunnableWhenAddingSplashScreen(Runnable r) {
+ mRunnableWhenAddingSplashScreen = r;
+ }
+
+ @Override
+ public StartingSurface addSplashScreen(IBinder appToken, String packageName, int theme,
+ CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes, int icon,
+ int logo, int windowFlags, Configuration overrideConfig, int displayId) {
+ final com.android.server.wm.WindowState window;
+ final AppWindowToken atoken;
+ final WindowManagerService wm = mWmSupplier.get();
+ synchronized (wm.mWindowMap) {
+ atoken = wm.mRoot.getAppWindowToken(appToken);
+ window = WindowTestsBase.createWindow(null, TYPE_APPLICATION_STARTING, atoken,
+ "Starting window", 0 /* ownerId */, false /* internalWindows */, wm,
+ mock(Session.class), mock(IWindow.class));
+ atoken.startingWindow = window;
+ }
+ if (mRunnableWhenAddingSplashScreen != null) {
+ mRunnableWhenAddingSplashScreen.run();
+ mRunnableWhenAddingSplashScreen = null;
+ }
+ return () -> {
+ synchronized (wm.mWindowMap) {
+ atoken.removeChild(window);
+ atoken.startingWindow = null;
+ }
+ };
+ }
+
+ @Override
+ public int prepareAddWindowLw(WindowState win,
+ WindowManager.LayoutParams attrs) {
+ return 0;
+ }
+
+ @Override
+ public void removeWindowLw(WindowState win) {
+
+ }
+
+ @Override
+ public int selectAnimationLw(WindowState win, int transit) {
+ return 0;
+ }
+
+ @Override
+ public void selectRotationAnimationLw(int[] anim) {
+
+ }
+
+ @Override
+ public boolean validateRotationAnimationLw(int exitAnimId, int enterAnimId,
+ boolean forceDefault) {
+ return false;
+ }
+
+ @Override
+ public Animation createHiddenByKeyguardExit(boolean onWallpaper,
+ boolean goingToNotificationShade) {
+ return null;
+ }
+
+ @Override
+ public Animation createKeyguardWallpaperExit(boolean goingToNotificationShade) {
+ return null;
+ }
+
+ @Override
+ public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
+ return 0;
+ }
+
+ @Override
+ public int interceptMotionBeforeQueueingNonInteractive(long whenNanos, int policyFlags) {
+ return 0;
+ }
+
+ @Override
+ public long interceptKeyBeforeDispatching(WindowState win, KeyEvent event,
+ int policyFlags) {
+ return 0;
+ }
+
+ @Override
+ public KeyEvent dispatchUnhandledKey(WindowState win, KeyEvent event,
+ int policyFlags) {
+ return null;
+ }
+
+ @Override
+ public int getSystemDecorLayerLw() {
+ return 0;
+ }
+
+ @Override
+ public void beginPostLayoutPolicyLw(int displayWidth, int displayHeight) {
+
+ }
+
+ @Override
+ public void applyPostLayoutPolicyLw(WindowState win,
+ WindowManager.LayoutParams attrs, WindowState attached, WindowState imeTarget) {
+ }
+
+ @Override
+ public int finishPostLayoutPolicyLw() {
+ return 0;
+ }
+
+ @Override
+ public boolean allowAppAnimationsLw() {
+ return false;
+ }
+
+ @Override
+ public int focusChangedLw(WindowState lastFocus,
+ WindowState newFocus) {
+ return 0;
+ }
+
+ @Override
+ public void startedWakingUp() {
+
+ }
+
+ @Override
+ public void finishedWakingUp() {
+
+ }
+
+ @Override
+ public void startedGoingToSleep(int why) {
+
+ }
+
+ @Override
+ public void finishedGoingToSleep(int why) {
+
+ }
+
+ @Override
+ public void screenTurningOn(ScreenOnListener screenOnListener) {
+
+ }
+
+ @Override
+ public void screenTurnedOn() {
+
+ }
+
+ @Override
+ public void screenTurningOff(ScreenOffListener screenOffListener) {
+
+ }
+
+ @Override
+ public void screenTurnedOff() {
+
+ }
+
+ @Override
+ public boolean isScreenOn() {
+ return true;
+ }
+
+ @Override
+ public boolean okToAnimate() {
+ return true;
+ }
+
+ @Override
+ public void notifyLidSwitchChanged(long whenNanos, boolean lidOpen) {
+
+ }
+
+ @Override
+ public void notifyCameraLensCoverSwitchChanged(long whenNanos, boolean lensCovered) {
+
+ }
+
+ @Override
+ public void enableKeyguard(boolean enabled) {
+
+ }
+
+ @Override
+ public void exitKeyguardSecurely(OnKeyguardExitResult callback) {
+
+ }
+
+ @Override
+ public boolean isKeyguardLocked() {
+ return keyguardShowingAndNotOccluded;
+ }
+
+ @Override
+ public boolean isKeyguardSecure(int userId) {
+ return false;
+ }
+
+ @Override
+ public boolean isKeyguardOccluded() {
+ return false;
+ }
+
+ @Override
+ public boolean isKeyguardTrustedLw() {
+ return false;
+ }
+
+ @Override
+ public boolean isKeyguardShowingAndNotOccluded() {
+ return keyguardShowingAndNotOccluded;
+ }
+
+ @Override
+ public boolean inKeyguardRestrictedKeyInputMode() {
+ return false;
+ }
+
+ @Override
+ public void dismissKeyguardLw(@Nullable IKeyguardDismissCallback callback,
+ CharSequence message) {
+ }
+
+ @Override
+ public boolean isKeyguardDrawnLw() {
+ return false;
+ }
+
+ @Override
+ public boolean isShowingDreamLw() {
+ return false;
+ }
+
+ @Override
+ public void onKeyguardOccludedChangedLw(boolean occluded) {
+ }
+
+ @Override
+ public int rotationForOrientationLw(int orientation, int lastRotation, boolean defaultDisplay) {
+ return rotationToReport;
+ }
+
+ @Override
+ public boolean rotationHasCompatibleMetricsLw(int orientation, int rotation) {
+ return true;
+ }
+
+ @Override
+ public void setRotationLw(int rotation) {
+
+ }
+
+ @Override
+ public void setSafeMode(boolean safeMode) {
+
+ }
+
+ @Override
+ public void systemReady() {
+
+ }
+
+ @Override
+ public void systemBooted() {
+
+ }
+
+ @Override
+ public void showBootMessage(CharSequence msg, boolean always) {
+
+ }
+
+ @Override
+ public void hideBootMessages() {
+
+ }
+
+ @Override
+ public void userActivity() {
+
+ }
+
+ @Override
+ public void enableScreenAfterBoot() {
+
+ }
+
+ @Override
+ public void setCurrentOrientationLw(int newOrientation) {
+
+ }
+
+ @Override
+ public boolean performHapticFeedbackLw(WindowState win, int effectId,
+ boolean always) {
+ return false;
+ }
+
+ @Override
+ public void keepScreenOnStartedLw() {
+
+ }
+
+ @Override
+ public void keepScreenOnStoppedLw() {
+
+ }
+
+ @Override
+ public int getUserRotationMode() {
+ return 0;
+ }
+
+ @Override
+ public void setUserRotationMode(int mode,
+ int rotation) {
+
+ }
+
+ @Override
+ public int adjustSystemUiVisibilityLw(int visibility) {
+ return 0;
+ }
+
+ @Override
+ public boolean hasNavigationBar() {
+ return false;
+ }
+
+ @Override
+ public void lockNow(Bundle options) {
+
+ }
+
+ @Override
+ public void setLastInputMethodWindowLw(WindowState ime,
+ WindowState target) {
+
+ }
+
+ @Override
+ public void showRecentApps() {
+
+ }
+
+ @Override
+ public void showGlobalActions() {
+
+ }
+
+ @Override
+ public void setCurrentUserLw(int newUserId) {
+
+ }
+
+ @Override
+ public void setSwitchingUser(boolean switching) {
+
+ }
+
+ @Override
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+
+ }
+
+ @Override
+ public void dump(String prefix, PrintWriter writer, String[] args) {
+
+ }
+
+ @Override
+ public boolean isTopLevelWindow(int windowType) {
+ return false;
+ }
+
+ @Override
+ public void startKeyguardExitAnimation(long startTime, long fadeoutDuration) {
+
+ }
+
+ @Override
+ public void getStableInsetsLw(int displayRotation, int displayWidth, int displayHeight,
+ DisplayCutout cutout, Rect outInsets) {
+
+ }
+
+ @Override
+ public boolean isNavBarForcedShownLw(WindowState win) {
+ return false;
+ }
+
+ @NavigationBarPosition
+ @Override
+ public int getNavBarPosition() {
+ return NAV_BAR_BOTTOM;
+ }
+
+ @Override
+ public void getNonDecorInsetsLw(int displayRotation, int displayWidth, int displayHeight,
+ DisplayCutout cutout, Rect outInsets) {
+
+ }
+
+ @Override
+ public boolean isDockSideAllowed(int dockSide, int originalDockSide, int displayWidth,
+ int displayHeight, int displayRotation) {
+ return false;
+ }
+
+ @Override
+ public void onConfigurationChanged() {
+
+ }
+
+ @Override
+ public boolean shouldRotateSeamlessly(int oldRotation, int newRotation) {
+ return false;
+ }
+
+ @Override
+ public void setPipVisibilityLw(boolean visible) {
+
+ }
+
+ @Override
+ public void setRecentsVisibilityLw(boolean visible) {
+
+ }
+
+ @Override
+ public void setNavBarVirtualKeyHapticFeedbackEnabledLw(boolean enabled) {
+ }
+
+ @Override
+ public void onSystemUiStarted() {
+ }
+
+ @Override
+ public boolean canDismissBootAnimation() {
+ return true;
+ }
+
+ @Override
+ public void requestUserActivityNotification() {
+ }
+
+ @Override
+ public void onLockTaskStateChangedLw(int lockTaskState) {
+ }
+
+ @Override
+ public boolean setAodShowing(boolean aodShowing) {
+ return false;
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java b/services/tests/servicestests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java
new file mode 100644
index 0000000..a5c47de
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test class for {@link AppTransition}.
+ *
+ * runtest frameworks-services -c com.android.server.wm.UnknownAppVisibilityControllerTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class UnknownAppVisibilityControllerTest extends WindowTestsBase {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ sWm.mUnknownAppVisibilityController.clear();
+ }
+
+ @Test
+ public void testFlow() throws Exception {
+ final AppWindowToken token = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ sWm.mUnknownAppVisibilityController.notifyLaunched(token);
+ sWm.mUnknownAppVisibilityController.notifyAppResumedFinished(token);
+ sWm.mUnknownAppVisibilityController.notifyRelayouted(token);
+
+ // Make sure our handler processed the message.
+ Thread.sleep(100);
+ assertTrue(sWm.mUnknownAppVisibilityController.allResolved());
+ }
+
+ @Test
+ public void testMultiple() throws Exception {
+ final AppWindowToken token1 = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ final AppWindowToken token2 = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ sWm.mUnknownAppVisibilityController.notifyLaunched(token1);
+ sWm.mUnknownAppVisibilityController.notifyAppResumedFinished(token1);
+ sWm.mUnknownAppVisibilityController.notifyLaunched(token2);
+ sWm.mUnknownAppVisibilityController.notifyRelayouted(token1);
+ sWm.mUnknownAppVisibilityController.notifyAppResumedFinished(token2);
+ sWm.mUnknownAppVisibilityController.notifyRelayouted(token2);
+
+ // Make sure our handler processed the message.
+ Thread.sleep(100);
+ assertTrue(sWm.mUnknownAppVisibilityController.allResolved());
+ }
+
+ @Test
+ public void testClear() throws Exception {
+ final AppWindowToken token = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ sWm.mUnknownAppVisibilityController.notifyLaunched(token);
+ sWm.mUnknownAppVisibilityController.clear();;
+ assertTrue(sWm.mUnknownAppVisibilityController.allResolved());
+ }
+
+ @Test
+ public void testAppRemoved() throws Exception {
+ final AppWindowToken token = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ sWm.mUnknownAppVisibilityController.notifyLaunched(token);
+ sWm.mUnknownAppVisibilityController.appRemovedOrHidden(token);
+ assertTrue(sWm.mUnknownAppVisibilityController.allResolved());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/WallpaperControllerTests.java
new file mode 100644
index 0000000..71ead20
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WallpaperControllerTests.java
@@ -0,0 +1,67 @@
+package com.android.server.wm;
+
+import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+
+import static junit.framework.TestCase.assertNotNull;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Bitmap;
+import android.os.IBinder;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for the {@link WallpaperController} class.
+ *
+ * Build/Install/Run:
+ * atest com.android.server.wm.WallpaperControllerTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WallpaperControllerTests extends WindowTestsBase {
+ @Test
+ public void testWallpaperScreenshot() {
+ WindowSurfaceController windowSurfaceController = mock(WindowSurfaceController.class);
+
+ synchronized (sWm.mWindowMap) {
+ // No wallpaper
+ final DisplayContent dc = createNewDisplay();
+ Bitmap wallpaperBitmap = sWm.mRoot.mWallpaperController.screenshotWallpaperLocked();
+ assertNull(wallpaperBitmap);
+
+ // No wallpaper WSA Surface
+ WindowToken wallpaperWindowToken = new WallpaperWindowToken(sWm, mock(IBinder.class),
+ true, dc, true /* ownerCanManageAppTokens */);
+ WindowState wallpaperWindow = createWindow(null /* parent */, TYPE_WALLPAPER,
+ wallpaperWindowToken, "wallpaperWindow");
+ wallpaperBitmap = mWallpaperController.screenshotWallpaperLocked();
+ assertNull(wallpaperBitmap);
+
+ // Wallpaper with not visible WSA surface.
+ wallpaperWindow.mWinAnimator.mSurfaceController = windowSurfaceController;
+ wallpaperWindow.mWinAnimator.mLastAlpha = 1;
+ wallpaperBitmap = mWallpaperController.screenshotWallpaperLocked();
+ assertNull(wallpaperBitmap);
+
+ when(windowSurfaceController.getShown()).thenReturn(true);
+
+ // Wallpaper with WSA alpha set to 0.
+ wallpaperWindow.mWinAnimator.mLastAlpha = 0;
+ wallpaperBitmap = mWallpaperController.screenshotWallpaperLocked();
+ assertNull(wallpaperBitmap);
+
+ // Wallpaper window with WSA Surface
+ wallpaperWindow.mWinAnimator.mLastAlpha = 1;
+ wallpaperBitmap = mWallpaperController.screenshotWallpaperLocked();
+ assertNotNull(wallpaperBitmap);
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowAnimationSpecTest.java b/services/tests/servicestests/src/com/android/server/wm/WindowAnimationSpecTest.java
new file mode 100644
index 0000000..ca520ed
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowAnimationSpecTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static com.android.server.wm.WindowStateAnimator.STACK_CLIP_AFTER_ANIM;
+import static com.android.server.wm.WindowStateAnimator.STACK_CLIP_BEFORE_ANIM;
+import static com.android.server.wm.WindowStateAnimator.STACK_CLIP_NONE;
+
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.view.animation.ClipRectAnimation;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for the {@link WindowAnimationSpec} class.
+ *
+ * atest FrameworksServicesTests:com.android.server.wm.WindowAnimationSpecTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WindowAnimationSpecTest {
+ private final SurfaceControl mSurfaceControl = mock(SurfaceControl.class);
+ private final SurfaceControl.Transaction mTransaction = mock(SurfaceControl.Transaction.class);
+ private final Animation mAnimation = mock(Animation.class);
+ private final Rect mStackBounds = new Rect(0, 0, 10, 10);
+
+ @Test
+ public void testApply_clipNone() {
+ Rect windowCrop = new Rect(0, 0, 20, 20);
+ Animation a = createClipRectAnimation(windowCrop, windowCrop);
+ WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(a, null,
+ mStackBounds, false /* canSkipFirstFrame */, STACK_CLIP_NONE,
+ true /* isAppAnimation */);
+ windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
+ verify(mTransaction).setWindowCrop(eq(mSurfaceControl),
+ argThat(rect -> rect.equals(windowCrop)));
+ }
+
+ @Test
+ public void testApply_clipAfter() {
+ WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(mAnimation, null,
+ mStackBounds, false /* canSkipFirstFrame */, STACK_CLIP_AFTER_ANIM,
+ true /* isAppAnimation */);
+ windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
+ verify(mTransaction).setWindowCrop(eq(mSurfaceControl), argThat(Rect::isEmpty));
+ verify(mTransaction).setFinalCrop(eq(mSurfaceControl),
+ argThat(rect -> rect.equals(mStackBounds)));
+ }
+
+ @Test
+ public void testApply_clipAfterOffsetPosition() {
+ // Stack bounds is (0, 0, 10, 10) position is (20, 40)
+ WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(mAnimation,
+ new Point(20, 40), mStackBounds, false /* canSkipFirstFrame */,
+ STACK_CLIP_AFTER_ANIM,
+ true /* isAppAnimation */);
+ windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
+ verify(mTransaction).setWindowCrop(eq(mSurfaceControl), argThat(Rect::isEmpty));
+ verify(mTransaction).setFinalCrop(eq(mSurfaceControl),
+ argThat(rect -> rect.left == 20 && rect.top == 40 && rect.right == 30
+ && rect.bottom == 50));
+ }
+
+ @Test
+ public void testApply_clipBeforeNoAnimationBounds() {
+ // Stack bounds is (0, 0, 10, 10) animation clip is (0, 0, 0, 0)
+ WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(mAnimation, null,
+ mStackBounds, false /* canSkipFirstFrame */, STACK_CLIP_BEFORE_ANIM,
+ true /* isAppAnimation */);
+ windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
+ verify(mTransaction).setWindowCrop(eq(mSurfaceControl),
+ argThat(rect -> rect.equals(mStackBounds)));
+ }
+
+ @Test
+ public void testApply_clipBeforeNoStackBounds() {
+ // Stack bounds is (0, 0, 0, 0) animation clip is (0, 0, 20, 20)
+ Rect windowCrop = new Rect(0, 0, 20, 20);
+ Animation a = createClipRectAnimation(windowCrop, windowCrop);
+ a.initialize(0, 0, 0, 0);
+ WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(a, null,
+ null, false /* canSkipFirstFrame */, STACK_CLIP_BEFORE_ANIM,
+ true /* isAppAnimation */);
+ windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
+ verify(mTransaction).setWindowCrop(eq(mSurfaceControl), argThat(Rect::isEmpty));
+ }
+
+ @Test
+ public void testApply_clipBeforeSmallerAnimationClip() {
+ // Stack bounds is (0, 0, 10, 10) animation clip is (0, 0, 5, 5)
+ Rect windowCrop = new Rect(0, 0, 5, 5);
+ Animation a = createClipRectAnimation(windowCrop, windowCrop);
+ WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(a, null,
+ mStackBounds, false /* canSkipFirstFrame */, STACK_CLIP_BEFORE_ANIM,
+ true /* isAppAnimation */);
+ windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
+ verify(mTransaction).setWindowCrop(eq(mSurfaceControl),
+ argThat(rect -> rect.equals(windowCrop)));
+ }
+
+ @Test
+ public void testApply_clipBeforeSmallerStackClip() {
+ // Stack bounds is (0, 0, 10, 10) animation clip is (0, 0, 20, 20)
+ Rect windowCrop = new Rect(0, 0, 20, 20);
+ Animation a = createClipRectAnimation(windowCrop, windowCrop);
+ WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(a, null,
+ mStackBounds, false /* canSkipFirstFrame */, STACK_CLIP_BEFORE_ANIM,
+ true /* isAppAnimation */);
+ windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
+ verify(mTransaction).setWindowCrop(eq(mSurfaceControl),
+ argThat(rect -> rect.equals(mStackBounds)));
+ }
+
+ private Animation createClipRectAnimation(Rect fromClip, Rect toClip) {
+ Animation a = new ClipRectAnimation(fromClip, toClip);
+ a.initialize(0, 0, 0, 0);
+ return a;
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowConfigurationTests.java b/services/tests/servicestests/src/com/android/server/wm/WindowConfigurationTests.java
new file mode 100644
index 0000000..513c1ec
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowConfigurationTests.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import org.junit.Test;
+
+import android.app.WindowConfiguration;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.DisplayInfo;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOW_CONFIG_APP_BOUNDS;
+import static android.app.WindowConfiguration.WINDOW_CONFIG_WINDOWING_MODE;
+import static android.content.pm.ActivityInfo.CONFIG_WINDOW_CONFIGURATION;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test class to for {@link android.app.WindowConfiguration}.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.WindowConfigurationTests
+ */
+@SmallTest
+@FlakyTest(bugId = 74078662)
+@Presubmit
+@org.junit.runner.RunWith(AndroidJUnit4.class)
+public class WindowConfigurationTests extends WindowTestsBase {
+ private Rect mParentBounds;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mParentBounds = new Rect(10 /*left*/, 30 /*top*/, 80 /*right*/, 60 /*bottom*/);
+ }
+
+ /** Tests {@link android.app.WindowConfiguration#diff(WindowConfiguration, boolean)}. */
+ @Test
+ public void testDiff() {
+ final Configuration config1 = new Configuration();
+ final WindowConfiguration winConfig1 = config1.windowConfiguration;
+ final Configuration config2 = new Configuration();
+ final WindowConfiguration winConfig2 = config2.windowConfiguration;
+ final Configuration config3 = new Configuration();
+ final WindowConfiguration winConfig3 = config3.windowConfiguration;
+
+ winConfig1.setAppBounds(0, 1, 1, 0);
+ winConfig2.setAppBounds(1, 2, 2, 1);
+ winConfig3.setAppBounds(winConfig1.getAppBounds());
+
+
+ assertEquals(CONFIG_WINDOW_CONFIGURATION, config1.diff(config2));
+ assertEquals(0, config1.diffPublicOnly(config2));
+ assertEquals(WINDOW_CONFIG_APP_BOUNDS,
+ winConfig1.diff(winConfig2, false /* compareUndefined */));
+
+ winConfig2.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ assertEquals(WINDOW_CONFIG_APP_BOUNDS | WINDOW_CONFIG_WINDOWING_MODE,
+ winConfig1.diff(winConfig2, false /* compareUndefined */));
+
+ assertEquals(0, config1.diff(config3));
+ assertEquals(0, config1.diffPublicOnly(config3));
+ assertEquals(0, winConfig1.diff(winConfig3, false /* compareUndefined */));
+ }
+
+ /** Tests {@link android.app.WindowConfiguration#compareTo(WindowConfiguration)}. */
+ @Test
+ public void testConfigurationCompareTo() throws Exception {
+ final Configuration blankConfig = new Configuration();
+ final WindowConfiguration blankWinConfig = new WindowConfiguration();
+
+ final Configuration config1 = new Configuration();
+ final WindowConfiguration winConfig1 = config1.windowConfiguration;
+ winConfig1.setAppBounds(1, 2, 3, 4);
+
+ final Configuration config2 = new Configuration(config1);
+ final WindowConfiguration winConfig2 = config2.windowConfiguration;
+
+ assertEquals(config1.compareTo(config2), 0);
+ assertEquals(winConfig1.compareTo(winConfig2), 0);
+
+ // Different windowing mode
+ winConfig2.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ assertNotEquals(config1.compareTo(config2), 0);
+ assertNotEquals(winConfig1.compareTo(winConfig2), 0);
+ winConfig2.setWindowingMode(winConfig1.getWindowingMode());
+
+ // Different bounds
+ winConfig2.setAppBounds(0, 2, 3, 4);
+ assertNotEquals(config1.compareTo(config2), 0);
+ assertNotEquals(winConfig1.compareTo(winConfig2), 0);
+
+ // No bounds
+ assertEquals(config1.compareTo(blankConfig), -1);
+ assertEquals(winConfig1.compareTo(blankWinConfig), -1);
+
+ assertEquals(blankConfig.compareTo(config1), 1);
+ assertEquals(blankWinConfig.compareTo(winConfig1), 1);
+ }
+
+ @Test
+ public void testSetActivityType() throws Exception {
+ final WindowConfiguration config = new WindowConfiguration();
+ config.setActivityType(ACTIVITY_TYPE_HOME);
+ assertEquals(ACTIVITY_TYPE_HOME, config.getActivityType());
+
+ // Allowed to change from app process.
+ config.setActivityType(ACTIVITY_TYPE_STANDARD);
+ assertEquals(ACTIVITY_TYPE_STANDARD, config.getActivityType());
+ }
+
+ /** Ensures the configuration app bounds at the root level match the app dimensions. */
+ @Test
+ public void testAppBounds_RootConfigurationBounds() throws Exception {
+ final DisplayInfo info = mDisplayContent.getDisplayInfo();
+ info.appWidth = 1024;
+ info.appHeight = 768;
+
+ final Rect appBounds = sWm.computeNewConfiguration(
+ mDisplayContent.getDisplayId()).windowConfiguration.getAppBounds();
+ // The bounds should always be positioned in the top left.
+ assertEquals(appBounds.left, 0);
+ assertEquals(appBounds.top, 0);
+
+ // The bounds should equal the defined app width and height
+ assertEquals(appBounds.width(), info.appWidth);
+ assertEquals(appBounds.height(), info.appHeight);
+ }
+
+ /** Ensures that bounds are clipped to their parent. */
+ @Test
+ public void testAppBounds_BoundsClipping() throws Exception {
+ final Rect shiftedBounds = new Rect(mParentBounds);
+ shiftedBounds.offset(10, 10);
+ final Rect expectedBounds = new Rect(mParentBounds);
+ expectedBounds.intersect(shiftedBounds);
+ testStackBoundsConfiguration(WINDOWING_MODE_FULLSCREEN, mParentBounds, shiftedBounds,
+ expectedBounds);
+ }
+
+ /** Ensures that empty bounds are not propagated to the configuration. */
+ @Test
+ public void testAppBounds_EmptyBounds() throws Exception {
+ final Rect emptyBounds = new Rect();
+ testStackBoundsConfiguration(WINDOWING_MODE_FULLSCREEN, mParentBounds, emptyBounds,
+ null /*ExpectedBounds*/);
+ }
+
+ /** Ensures that bounds on freeform stacks are not clipped. */
+ @Test
+ public void testAppBounds_FreeFormBounds() throws Exception {
+ final Rect freeFormBounds = new Rect(mParentBounds);
+ freeFormBounds.offset(10, 10);
+ testStackBoundsConfiguration(WINDOWING_MODE_FREEFORM, mParentBounds, freeFormBounds,
+ freeFormBounds);
+ }
+
+ /** Ensures that fully contained bounds are not clipped. */
+ @Test
+ public void testAppBounds_ContainedBounds() throws Exception {
+ final Rect insetBounds = new Rect(mParentBounds);
+ insetBounds.inset(5, 5, 5, 5);
+ testStackBoundsConfiguration(
+ WINDOWING_MODE_FULLSCREEN, mParentBounds, insetBounds, insetBounds);
+ }
+
+ /** Ensures that full screen free form bounds are clipped */
+ @Test
+ public void testAppBounds_FullScreenFreeFormBounds() throws Exception {
+ final Rect fullScreenBounds = new Rect(0, 0, mDisplayInfo.logicalWidth,
+ mDisplayInfo.logicalHeight);
+ testStackBoundsConfiguration(WINDOWING_MODE_FULLSCREEN, mParentBounds, fullScreenBounds,
+ mParentBounds);
+ }
+
+ private void testStackBoundsConfiguration(int windowingMode, Rect parentBounds, Rect bounds,
+ Rect expectedConfigBounds) {
+ final StackWindowController stackController = createStackControllerOnStackOnDisplay(
+ windowingMode, ACTIVITY_TYPE_STANDARD, mDisplayContent);
+
+ final Configuration parentConfig = mDisplayContent.getConfiguration();
+ parentConfig.windowConfiguration.setAppBounds(parentBounds);
+
+ final Configuration config = new Configuration();
+ final WindowConfiguration winConfig = config.windowConfiguration;
+ stackController.adjustConfigurationForBounds(bounds, null /*insetBounds*/,
+ new Rect() /*nonDecorBounds*/, new Rect() /*stableBounds*/, false /*overrideWidth*/,
+ false /*overrideHeight*/, mDisplayInfo.logicalDensityDpi, config, parentConfig,
+ windowingMode);
+ // Assert that both expected and actual are null or are equal to each other
+
+ assertEquals(expectedConfigBounds, winConfig.getAppBounds());
+ }
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowContainerControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/WindowContainerControllerTests.java
new file mode 100644
index 0000000..502cb6e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowContainerControllerTests.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import android.app.WindowConfiguration;
+import android.content.res.Configuration;
+import android.support.test.filters.FlakyTest;
+import org.junit.Test;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.content.res.Configuration.EMPTY;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test class for {@link WindowContainerController}.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.WindowContainerControllerTests
+ */
+@SmallTest
+@Presubmit
+@FlakyTest(bugId = 74078662)
+@org.junit.runner.RunWith(AndroidJUnit4.class)
+public class WindowContainerControllerTests extends WindowTestsBase {
+
+ @Test
+ public void testCreation() throws Exception {
+ final WindowContainerController controller = new WindowContainerController(null, sWm);
+ final WindowContainer container = new WindowContainer(sWm);
+
+ container.setController(controller);
+ assertEquals(controller, container.getController());
+ assertEquals(controller.mContainer, container);
+ }
+
+ @Test
+ public void testSetContainer() throws Exception {
+ final WindowContainerController controller = new WindowContainerController(null, sWm);
+ final WindowContainer container = new WindowContainer(sWm);
+
+ controller.setContainer(container);
+ assertEquals(controller.mContainer, container);
+
+ // Assert we can't change the container to another one once set
+ boolean gotException = false;
+ try {
+ controller.setContainer(new WindowContainer(sWm));
+ } catch (IllegalArgumentException e) {
+ gotException = true;
+ }
+ assertTrue(gotException);
+
+ // Assert that we can set the container to null.
+ controller.setContainer(null);
+ assertNull(controller.mContainer);
+ }
+
+ @Test
+ public void testRemoveContainer() throws Exception {
+ final WindowContainerController controller = new WindowContainerController(null, sWm);
+ final WindowContainer container = new WindowContainer(sWm);
+
+ controller.setContainer(container);
+ assertEquals(controller.mContainer, container);
+
+ controller.removeContainer();
+ assertNull(controller.mContainer);
+ }
+
+ @Test
+ public void testOnOverrideConfigurationChanged() throws Exception {
+ final WindowContainerController controller = new WindowContainerController(null, sWm);
+ final WindowContainer container = new WindowContainer(sWm);
+
+ controller.setContainer(container);
+ assertEquals(controller.mContainer, container);
+ assertEquals(EMPTY, container.getOverrideConfiguration());
+
+ final Configuration config = new Configuration();
+ config.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ config.windowConfiguration.setAppBounds(10, 10, 10, 10);
+
+ // Assert that the config change through the controller is propagated to the container.
+ controller.onOverrideConfigurationChanged(config);
+ assertEquals(config, container.getOverrideConfiguration());
+
+ // Assert the container configuration isn't changed after removal from the controller.
+ controller.removeContainer();
+ controller.onOverrideConfigurationChanged(EMPTY);
+ assertEquals(config, container.getOverrideConfiguration());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/servicestests/src/com/android/server/wm/WindowContainerTests.java
new file mode 100644
index 0000000..6c7830e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowContainerTests.java
@@ -0,0 +1,872 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import android.support.test.filters.FlakyTest;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.util.Comparator;
+
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+
+import static com.android.server.wm.WindowContainer.POSITION_BOTTOM;
+import static com.android.server.wm.WindowContainer.POSITION_TOP;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyFloat;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Test class for {@link WindowContainer}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.wm.WindowContainerTests
+ */
+@SmallTest
+@Presubmit
+@FlakyTest(bugId = 74078662)
+@RunWith(AndroidJUnit4.class)
+public class WindowContainerTests extends WindowTestsBase {
+
+ @Test
+ public void testCreation() throws Exception {
+ final TestWindowContainer w = new TestWindowContainerBuilder().setLayer(0).build();
+ assertNull("window must have no parent", w.getParentWindow());
+ assertEquals("window must have no children", 0, w.getChildrenCount());
+ }
+
+ @Test
+ public void testAdd() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer layer1 = root.addChildWindow(builder.setLayer(1));
+ final TestWindowContainer secondLayer1 = root.addChildWindow(builder.setLayer(1));
+ final TestWindowContainer layer2 = root.addChildWindow(builder.setLayer(2));
+ final TestWindowContainer layerNeg1 = root.addChildWindow(builder.setLayer(-1));
+ final TestWindowContainer layerNeg2 = root.addChildWindow(builder.setLayer(-2));
+ final TestWindowContainer secondLayerNeg1 = root.addChildWindow(builder.setLayer(-1));
+ final TestWindowContainer layer0 = root.addChildWindow(builder.setLayer(0));
+
+ assertEquals(7, root.getChildrenCount());
+
+ assertEquals(root, layer1.getParentWindow());
+ assertEquals(root, secondLayer1.getParentWindow());
+ assertEquals(root, layer2.getParentWindow());
+ assertEquals(root, layerNeg1.getParentWindow());
+ assertEquals(root, layerNeg2.getParentWindow());
+ assertEquals(root, secondLayerNeg1.getParentWindow());
+ assertEquals(root, layer0.getParentWindow());
+
+ assertEquals(layerNeg2, root.getChildAt(0));
+ assertEquals(secondLayerNeg1, root.getChildAt(1));
+ assertEquals(layerNeg1, root.getChildAt(2));
+ assertEquals(layer0, root.getChildAt(3));
+ assertEquals(layer1, root.getChildAt(4));
+ assertEquals(secondLayer1, root.getChildAt(5));
+ assertEquals(layer2, root.getChildAt(6));
+
+ assertTrue(layer1.mOnParentSetCalled);
+ assertTrue(secondLayer1.mOnParentSetCalled);
+ assertTrue(layer2.mOnParentSetCalled);
+ assertTrue(layerNeg1.mOnParentSetCalled);
+ assertTrue(layerNeg2.mOnParentSetCalled);
+ assertTrue(secondLayerNeg1.mOnParentSetCalled);
+ assertTrue(layer0.mOnParentSetCalled);
+ }
+
+ @Test
+ public void testAddChildSetsSurfacePosition() throws Exception {
+ MockSurfaceBuildingContainer top = new MockSurfaceBuildingContainer();
+
+ final SurfaceControl.Transaction transaction = mock(SurfaceControl.Transaction.class);
+ sWm.mTransactionFactory = () -> transaction;
+
+ WindowContainer child = new WindowContainer(sWm);
+ child.setBounds(1, 1, 10, 10);
+
+ verify(transaction, never()).setPosition(any(), anyFloat(), anyFloat());
+ top.addChild(child, 0);
+ verify(transaction, times(1)).setPosition(any(), eq(1.f), eq(1.f));
+ }
+
+ @Test
+ public void testAdd_AlreadyHasParent() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = root.addChildWindow();
+ final TestWindowContainer child2 = root.addChildWindow();
+
+ boolean gotException = false;
+ try {
+ child1.addChildWindow(child2);
+ } catch (IllegalArgumentException e) {
+ gotException = true;
+ }
+ assertTrue(gotException);
+
+ gotException = false;
+ try {
+ root.addChildWindow(child2);
+ } catch (IllegalArgumentException e) {
+ gotException = true;
+ }
+ assertTrue(gotException);
+ }
+
+ @Test
+ public void testHasChild() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = root.addChildWindow();
+ final TestWindowContainer child2 = root.addChildWindow();
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child12 = child1.addChildWindow();
+ final TestWindowContainer child21 = child2.addChildWindow();
+
+ assertEquals(2, root.getChildrenCount());
+ assertEquals(2, child1.getChildrenCount());
+ assertEquals(1, child2.getChildrenCount());
+
+ assertTrue(root.hasChild(child1));
+ assertTrue(root.hasChild(child2));
+ assertTrue(root.hasChild(child11));
+ assertTrue(root.hasChild(child12));
+ assertTrue(root.hasChild(child21));
+
+ assertTrue(child1.hasChild(child11));
+ assertTrue(child1.hasChild(child12));
+ assertFalse(child1.hasChild(child21));
+
+ assertTrue(child2.hasChild(child21));
+ assertFalse(child2.hasChild(child11));
+ assertFalse(child2.hasChild(child12));
+ }
+
+ @Test
+ public void testRemoveImmediately() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = root.addChildWindow();
+ final TestWindowContainer child2 = root.addChildWindow();
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child12 = child1.addChildWindow();
+ final TestWindowContainer child21 = child2.addChildWindow();
+
+ assertNotNull(child12.getParentWindow());
+ child12.removeImmediately();
+ assertNull(child12.getParentWindow());
+ assertEquals(1, child1.getChildrenCount());
+ assertFalse(child1.hasChild(child12));
+ assertFalse(root.hasChild(child12));
+
+ assertTrue(root.hasChild(child2));
+ assertNotNull(child2.getParentWindow());
+ child2.removeImmediately();
+ assertNull(child2.getParentWindow());
+ assertNull(child21.getParentWindow());
+ assertEquals(0, child2.getChildrenCount());
+ assertEquals(1, root.getChildrenCount());
+ assertFalse(root.hasChild(child2));
+ assertFalse(root.hasChild(child21));
+
+ assertTrue(root.hasChild(child1));
+ assertTrue(root.hasChild(child11));
+
+ root.removeImmediately();
+ assertEquals(0, root.getChildrenCount());
+ }
+
+ @Test
+ public void testRemoveImmediately_WithController() throws Exception {
+ final WindowContainer container = new WindowContainer(sWm);
+ final WindowContainerController controller = new WindowContainerController(null, sWm);
+
+ container.setController(controller);
+ assertEquals(controller, container.getController());
+ assertEquals(container, controller.mContainer);
+
+ container.removeImmediately();
+ assertNull(container.getController());
+ assertNull(controller.mContainer);
+ }
+
+ @Test
+ public void testSetController() throws Exception {
+ final WindowContainerController controller = new WindowContainerController(null, sWm);
+ final WindowContainer container = new WindowContainer(sWm);
+
+ container.setController(controller);
+ assertEquals(controller, container.getController());
+ assertEquals(container, controller.mContainer);
+
+ // Assert we can't change the controller to another one once set
+ boolean gotException = false;
+ try {
+ container.setController(new WindowContainerController(null, sWm));
+ } catch (IllegalArgumentException e) {
+ gotException = true;
+ }
+ assertTrue(gotException);
+
+ // Assert that we can set the controller to null.
+ container.setController(null);
+ assertNull(container.getController());
+ assertNull(controller.mContainer);
+ }
+
+ @Test
+ public void testPositionChildAt() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = root.addChildWindow();
+ final TestWindowContainer child2 = root.addChildWindow();
+ final TestWindowContainer child3 = root.addChildWindow();
+
+ // Test position at top.
+ root.positionChildAt(POSITION_TOP, child1, false /* includingParents */);
+ assertEquals(child1, root.getChildAt(root.getChildrenCount() - 1));
+
+ // Test position at bottom.
+ root.positionChildAt(POSITION_BOTTOM, child1, false /* includingParents */);
+ assertEquals(child1, root.getChildAt(0));
+
+ // Test position in the middle.
+ root.positionChildAt(1, child3, false /* includingParents */);
+ assertEquals(child1, root.getChildAt(0));
+ assertEquals(child3, root.getChildAt(1));
+ assertEquals(child2, root.getChildAt(2));
+ }
+
+ @Test
+ public void testPositionChildAtIncludeParents() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = root.addChildWindow();
+ final TestWindowContainer child2 = root.addChildWindow();
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child12 = child1.addChildWindow();
+ final TestWindowContainer child13 = child1.addChildWindow();
+ final TestWindowContainer child21 = child2.addChildWindow();
+ final TestWindowContainer child22 = child2.addChildWindow();
+ final TestWindowContainer child23 = child2.addChildWindow();
+
+ // Test moving to top.
+ child1.positionChildAt(POSITION_TOP, child11, true /* includingParents */);
+ assertEquals(child12, child1.getChildAt(0));
+ assertEquals(child13, child1.getChildAt(1));
+ assertEquals(child11, child1.getChildAt(2));
+ assertEquals(child2, root.getChildAt(0));
+ assertEquals(child1, root.getChildAt(1));
+
+ // Test moving to bottom.
+ child1.positionChildAt(POSITION_BOTTOM, child11, true /* includingParents */);
+ assertEquals(child11, child1.getChildAt(0));
+ assertEquals(child12, child1.getChildAt(1));
+ assertEquals(child13, child1.getChildAt(2));
+ assertEquals(child1, root.getChildAt(0));
+ assertEquals(child2, root.getChildAt(1));
+
+ // Test moving to middle, includeParents shouldn't do anything.
+ child2.positionChildAt(1, child21, true /* includingParents */);
+ assertEquals(child11, child1.getChildAt(0));
+ assertEquals(child12, child1.getChildAt(1));
+ assertEquals(child13, child1.getChildAt(2));
+ assertEquals(child22, child2.getChildAt(0));
+ assertEquals(child21, child2.getChildAt(1));
+ assertEquals(child23, child2.getChildAt(2));
+ assertEquals(child1, root.getChildAt(0));
+ assertEquals(child2, root.getChildAt(1));
+ }
+
+ @Test
+ public void testPositionChildAtInvalid() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = root.addChildWindow();
+ final TestWindowContainer child2 = root.addChildWindow();
+
+ boolean gotException = false;
+ try {
+ // Check response to negative position.
+ root.positionChildAt(-1, child1, false /* includingParents */);
+ } catch (IllegalArgumentException e) {
+ gotException = true;
+ }
+ assertTrue(gotException);
+
+ gotException = false;
+ try {
+ // Check response to position that's bigger than child number.
+ root.positionChildAt(3, child1, false /* includingParents */);
+ } catch (IllegalArgumentException e) {
+ gotException = true;
+ }
+ assertTrue(gotException);
+ }
+
+ @Test
+ public void testIsAnimating() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = root.addChildWindow(builder.setIsAnimating(true));
+ final TestWindowContainer child2 = root.addChildWindow();
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child12 = child1.addChildWindow(builder.setIsAnimating(true));
+ final TestWindowContainer child21 = child2.addChildWindow();
+
+ assertFalse(root.isAnimating());
+ assertTrue(child1.isAnimating());
+ assertTrue(child11.isAnimating());
+ assertTrue(child12.isAnimating());
+ assertFalse(child2.isAnimating());
+ assertFalse(child21.isAnimating());
+
+ assertTrue(root.isSelfOrChildAnimating());
+ assertTrue(child1.isSelfOrChildAnimating());
+ assertFalse(child11.isSelfOrChildAnimating());
+ assertTrue(child12.isSelfOrChildAnimating());
+ assertFalse(child2.isSelfOrChildAnimating());
+ assertFalse(child21.isSelfOrChildAnimating());
+ }
+
+ @Test
+ public void testIsVisible() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = root.addChildWindow(builder.setIsVisible(true));
+ final TestWindowContainer child2 = root.addChildWindow();
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child12 = child1.addChildWindow(builder.setIsVisible(true));
+ final TestWindowContainer child21 = child2.addChildWindow();
+
+ assertFalse(root.isVisible());
+ assertTrue(child1.isVisible());
+ assertFalse(child11.isVisible());
+ assertTrue(child12.isVisible());
+ assertFalse(child2.isVisible());
+ assertFalse(child21.isVisible());
+ }
+
+ @Test
+ public void testOverrideConfigurationAncestorNotification() {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer grandparent = builder.setLayer(0).build();
+
+ final TestWindowContainer parent = grandparent.addChildWindow();
+ final TestWindowContainer child = parent.addChildWindow();
+ child.onOverrideConfigurationChanged(new Configuration());
+
+ assertTrue(grandparent.mOnDescendantOverrideCalled);
+ }
+
+ @Test
+ public void testRemoveChild() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+ final TestWindowContainer child1 = root.addChildWindow();
+ final TestWindowContainer child2 = root.addChildWindow();
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child21 = child2.addChildWindow();
+
+ assertTrue(root.hasChild(child2));
+ assertTrue(root.hasChild(child21));
+ root.removeChild(child2);
+ assertFalse(root.hasChild(child2));
+ assertFalse(root.hasChild(child21));
+ assertNull(child2.getParentWindow());
+
+ boolean gotException = false;
+ assertTrue(root.hasChild(child11));
+ try {
+ // Can only detach our direct children.
+ root.removeChild(child11);
+ } catch (IllegalArgumentException e) {
+ gotException = true;
+ }
+ assertTrue(gotException);
+ }
+
+ @Test
+ public void testGetOrientation_childSpecified() throws Exception {
+ testGetOrientation_childSpecifiedConfig(false, SCREEN_ORIENTATION_LANDSCAPE,
+ SCREEN_ORIENTATION_LANDSCAPE);
+ testGetOrientation_childSpecifiedConfig(false, SCREEN_ORIENTATION_UNSET,
+ SCREEN_ORIENTATION_UNSPECIFIED);
+ }
+
+ private void testGetOrientation_childSpecifiedConfig(boolean childVisible, int childOrientation,
+ int expectedOrientation) {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+ root.setFillsParent(true);
+
+ builder.setIsVisible(childVisible);
+
+ if (childOrientation != SCREEN_ORIENTATION_UNSET) {
+ builder.setOrientation(childOrientation);
+ }
+
+ final TestWindowContainer child1 = root.addChildWindow(builder);
+ child1.setFillsParent(true);
+
+ assertEquals(expectedOrientation, root.getOrientation());
+ }
+
+ @Test
+ public void testGetOrientation_Unset() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).setIsVisible(true).build();
+ // Unspecified well because we didn't specify anything...
+ assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, root.getOrientation());
+ }
+
+ @Test
+ public void testGetOrientation_InvisibleParentUnsetVisibleChildren() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).setIsVisible(true).build();
+
+ builder.setIsVisible(false).setLayer(-1);
+ final TestWindowContainer invisible = root.addChildWindow(builder);
+ builder.setIsVisible(true).setLayer(-2);
+ final TestWindowContainer invisibleChild1VisibleAndSet = invisible.addChildWindow(builder);
+ invisibleChild1VisibleAndSet.setOrientation(SCREEN_ORIENTATION_LANDSCAPE);
+ // Landscape well because the container is visible and that is what we set on it above.
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, invisibleChild1VisibleAndSet.getOrientation());
+ // Landscape because even though the container isn't visible it has a child that is
+ // specifying it can influence the orientation by being visible.
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, invisible.getOrientation());
+ // Landscape because the grandchild is visible and therefore can participate.
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, root.getOrientation());
+
+ builder.setIsVisible(true).setLayer(-3);
+ final TestWindowContainer visibleUnset = root.addChildWindow(builder);
+ visibleUnset.setOrientation(SCREEN_ORIENTATION_UNSET);
+ assertEquals(SCREEN_ORIENTATION_UNSET, visibleUnset.getOrientation());
+ assertEquals(SCREEN_ORIENTATION_LANDSCAPE, root.getOrientation());
+
+ }
+
+ @Test
+ public void testGetOrientation_setBehind() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).setIsVisible(true).build();
+
+ builder.setIsVisible(true).setLayer(-1);
+ final TestWindowContainer visibleUnset = root.addChildWindow(builder);
+ visibleUnset.setOrientation(SCREEN_ORIENTATION_UNSET);
+
+ builder.setIsVisible(true).setLayer(-2);
+ final TestWindowContainer visibleUnsetChild1VisibleSetBehind =
+ visibleUnset.addChildWindow(builder);
+ visibleUnsetChild1VisibleSetBehind.setOrientation(SCREEN_ORIENTATION_BEHIND);
+ // Setting to visible behind will be used by the parents if there isn't another other
+ // container behind this one that has an orientation set.
+ assertEquals(SCREEN_ORIENTATION_BEHIND,
+ visibleUnsetChild1VisibleSetBehind.getOrientation());
+ assertEquals(SCREEN_ORIENTATION_BEHIND, visibleUnset.getOrientation());
+ assertEquals(SCREEN_ORIENTATION_BEHIND, root.getOrientation());
+ }
+
+ @Test
+ public void testGetOrientation_fillsParent() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).setIsVisible(true).build();
+
+ builder.setIsVisible(true).setLayer(-1);
+ final TestWindowContainer visibleUnset = root.addChildWindow(builder);
+ visibleUnset.setOrientation(SCREEN_ORIENTATION_BEHIND);
+
+ builder.setLayer(1).setIsVisible(true);
+ final TestWindowContainer visibleUnspecifiedRootChild = root.addChildWindow(builder);
+ visibleUnspecifiedRootChild.setFillsParent(false);
+ visibleUnspecifiedRootChild.setOrientation(SCREEN_ORIENTATION_UNSPECIFIED);
+ // Unset because the child doesn't fill the parent. May as well be invisible...
+ assertEquals(SCREEN_ORIENTATION_UNSET, visibleUnspecifiedRootChild.getOrientation());
+ // The parent uses whatever orientation is set behind this container since it doesn't fill
+ // the parent.
+ assertEquals(SCREEN_ORIENTATION_BEHIND, root.getOrientation());
+
+ // Test case of child filling its parent, but its parent isn't filling its own parent.
+ builder.setLayer(2).setIsVisible(true);
+ final TestWindowContainer visibleUnspecifiedRootChildChildFillsParent =
+ visibleUnspecifiedRootChild.addChildWindow(builder);
+ visibleUnspecifiedRootChildChildFillsParent.setOrientation(
+ SCREEN_ORIENTATION_PORTRAIT);
+ assertEquals(SCREEN_ORIENTATION_PORTRAIT,
+ visibleUnspecifiedRootChildChildFillsParent.getOrientation());
+ assertEquals(SCREEN_ORIENTATION_UNSET, visibleUnspecifiedRootChild.getOrientation());
+ assertEquals(SCREEN_ORIENTATION_BEHIND, root.getOrientation());
+
+
+ visibleUnspecifiedRootChild.setFillsParent(true);
+ assertEquals(SCREEN_ORIENTATION_PORTRAIT, visibleUnspecifiedRootChild.getOrientation());
+ assertEquals(SCREEN_ORIENTATION_PORTRAIT, root.getOrientation());
+ }
+
+ @Test
+ public void testCompareTo() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.setLayer(0).build();
+
+ final TestWindowContainer child1 = root.addChildWindow();
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child12 = child1.addChildWindow();
+
+ final TestWindowContainer child2 = root.addChildWindow();
+ final TestWindowContainer child21 = child2.addChildWindow();
+ final TestWindowContainer child22 = child2.addChildWindow();
+ final TestWindowContainer child23 = child2.addChildWindow();
+ final TestWindowContainer child221 = child22.addChildWindow();
+ final TestWindowContainer child222 = child22.addChildWindow();
+ final TestWindowContainer child223 = child22.addChildWindow();
+ final TestWindowContainer child2221 = child222.addChildWindow();
+ final TestWindowContainer child2222 = child222.addChildWindow();
+ final TestWindowContainer child2223 = child222.addChildWindow();
+
+ final TestWindowContainer root2 = builder.setLayer(0).build();
+
+ assertEquals(0, root.compareTo(root));
+ assertEquals(-1, child1.compareTo(child2));
+ assertEquals(1, child2.compareTo(child1));
+
+ boolean inTheSameTree = true;
+ try {
+ root.compareTo(root2);
+ } catch (IllegalArgumentException e) {
+ inTheSameTree = false;
+ }
+ assertFalse(inTheSameTree);
+
+ assertEquals(-1, child1.compareTo(child11));
+ assertEquals(1, child21.compareTo(root));
+ assertEquals(1, child21.compareTo(child12));
+ assertEquals(-1, child11.compareTo(child2));
+ assertEquals(1, child2221.compareTo(child11));
+ assertEquals(-1, child2222.compareTo(child223));
+ assertEquals(1, child2223.compareTo(child21));
+ }
+
+ @Test
+ public void testPrefixOrderIndex() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.build();
+
+ final TestWindowContainer child1 = root.addChildWindow();
+
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child12 = child1.addChildWindow();
+
+ final TestWindowContainer child2 = root.addChildWindow();
+
+ final TestWindowContainer child21 = child2.addChildWindow();
+ final TestWindowContainer child22 = child2.addChildWindow();
+
+ final TestWindowContainer child221 = child22.addChildWindow();
+ final TestWindowContainer child222 = child22.addChildWindow();
+ final TestWindowContainer child223 = child22.addChildWindow();
+
+ final TestWindowContainer child23 = child2.addChildWindow();
+
+ assertEquals(0, root.getPrefixOrderIndex());
+ assertEquals(1, child1.getPrefixOrderIndex());
+ assertEquals(2, child11.getPrefixOrderIndex());
+ assertEquals(3, child12.getPrefixOrderIndex());
+ assertEquals(4, child2.getPrefixOrderIndex());
+ assertEquals(5, child21.getPrefixOrderIndex());
+ assertEquals(6, child22.getPrefixOrderIndex());
+ assertEquals(7, child221.getPrefixOrderIndex());
+ assertEquals(8, child222.getPrefixOrderIndex());
+ assertEquals(9, child223.getPrefixOrderIndex());
+ assertEquals(10, child23.getPrefixOrderIndex());
+ }
+
+ @Test
+ public void testPrefixOrder_addEntireSubtree() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.build();
+ final TestWindowContainer subtree = builder.build();
+ final TestWindowContainer subtree2 = builder.build();
+
+ final TestWindowContainer child1 = subtree.addChildWindow();
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child2 = subtree2.addChildWindow();
+ final TestWindowContainer child3 = subtree2.addChildWindow();
+ subtree.addChild(subtree2, 1);
+ root.addChild(subtree, 0);
+
+ assertEquals(0, root.getPrefixOrderIndex());
+ assertEquals(1, subtree.getPrefixOrderIndex());
+ assertEquals(2, child1.getPrefixOrderIndex());
+ assertEquals(3, child11.getPrefixOrderIndex());
+ assertEquals(4, subtree2.getPrefixOrderIndex());
+ assertEquals(5, child2.getPrefixOrderIndex());
+ assertEquals(6, child3.getPrefixOrderIndex());
+ }
+
+ @Test
+ public void testPrefixOrder_remove() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.build();
+
+ final TestWindowContainer child1 = root.addChildWindow();
+
+ final TestWindowContainer child11 = child1.addChildWindow();
+ final TestWindowContainer child12 = child1.addChildWindow();
+
+ final TestWindowContainer child2 = root.addChildWindow();
+
+ assertEquals(0, root.getPrefixOrderIndex());
+ assertEquals(1, child1.getPrefixOrderIndex());
+ assertEquals(2, child11.getPrefixOrderIndex());
+ assertEquals(3, child12.getPrefixOrderIndex());
+ assertEquals(4, child2.getPrefixOrderIndex());
+
+ root.removeChild(child1);
+
+ assertEquals(1, child2.getPrefixOrderIndex());
+ }
+
+ /**
+ * Ensure children of a {@link WindowContainer} do not have
+ * {@link WindowContainer#onParentResize()} called when {@link WindowContainer#onParentResize()}
+ * is invoked with overridden bounds.
+ */
+ @Test
+ public void testOnParentResizePropagation() throws Exception {
+ final TestWindowContainerBuilder builder = new TestWindowContainerBuilder();
+ final TestWindowContainer root = builder.build();
+
+ final TestWindowContainer child = root.addChildWindow();
+ child.setBounds(new Rect(1,1,2,2));
+
+ final TestWindowContainer grandChild = mock(TestWindowContainer.class);
+
+ child.addChildWindow(grandChild);
+ root.onResize();
+
+ // Make sure the child does not propagate resize through onParentResize when bounds are set.
+ verify(grandChild, never()).onParentResize();
+
+ child.removeChild(grandChild);
+
+ child.setBounds(null);
+ child.addChildWindow(grandChild);
+ root.onResize();
+
+ // Make sure the child propagates resize through onParentResize when no bounds set.
+ verify(grandChild, times(1)).onParentResize();
+ }
+
+ /* Used so we can gain access to some protected members of the {@link WindowContainer} class */
+ private class TestWindowContainer extends WindowContainer<TestWindowContainer> {
+ private final int mLayer;
+ private boolean mIsAnimating;
+ private boolean mIsVisible;
+ private boolean mFillsParent;
+ private Integer mOrientation;
+
+ private boolean mOnParentSetCalled;
+ private boolean mOnDescendantOverrideCalled;
+
+ /**
+ * Compares 2 window layers and returns -1 if the first is lesser than the second in terms
+ * of z-order and 1 otherwise.
+ */
+ private final Comparator<TestWindowContainer> mWindowSubLayerComparator = (w1, w2) -> {
+ final int layer1 = w1.mLayer;
+ final int layer2 = w2.mLayer;
+ if (layer1 < layer2 || (layer1 == layer2 && layer2 < 0 )) {
+ // We insert the child window into the list ordered by the mLayer. For same layers,
+ // the negative one should go below others; the positive one should go above others.
+ return -1;
+ }
+ return 1;
+ };
+
+ TestWindowContainer(int layer, boolean isAnimating, boolean isVisible,
+ Integer orientation) {
+ super(sWm);
+ mLayer = layer;
+ mIsAnimating = isAnimating;
+ mIsVisible = isVisible;
+ mFillsParent = true;
+ mOrientation = orientation;
+ }
+
+ TestWindowContainer getParentWindow() {
+ return (TestWindowContainer) getParent();
+ }
+
+ int getChildrenCount() {
+ return mChildren.size();
+ }
+
+ TestWindowContainer addChildWindow(TestWindowContainer child) {
+ addChild(child, mWindowSubLayerComparator);
+ return child;
+ }
+
+ TestWindowContainer addChildWindow(TestWindowContainerBuilder childBuilder) {
+ TestWindowContainer child = childBuilder.build();
+ addChild(child, mWindowSubLayerComparator);
+ return child;
+ }
+
+ TestWindowContainer addChildWindow() {
+ return addChildWindow(new TestWindowContainerBuilder().setLayer(1));
+ }
+
+ @Override
+ void onParentSet() {
+ mOnParentSetCalled = true;
+ }
+
+ @Override
+ void onDescendantOverrideConfigurationChanged() {
+ mOnDescendantOverrideCalled = true;
+ super.onDescendantOverrideConfigurationChanged();
+ }
+
+ @Override
+ boolean isSelfAnimating() {
+ return mIsAnimating;
+ }
+
+ @Override
+ boolean isVisible() {
+ return mIsVisible;
+ }
+
+ @Override
+ int getOrientation(int candidate) {
+ return mOrientation != null ? mOrientation : super.getOrientation(candidate);
+ }
+
+ @Override
+ int getOrientation() {
+ return getOrientation(super.mOrientation);
+ }
+
+ @Override
+ boolean fillsParent() {
+ return mFillsParent;
+ }
+
+ void setFillsParent(boolean fillsParent) {
+ mFillsParent = fillsParent;
+ }
+ }
+
+ private class TestWindowContainerBuilder {
+ private int mLayer;
+ private boolean mIsAnimating;
+ private boolean mIsVisible;
+ private Integer mOrientation;
+
+ public TestWindowContainerBuilder() {
+ reset();
+ }
+
+ TestWindowContainerBuilder setLayer(int layer) {
+ mLayer = layer;
+ return this;
+ }
+
+ TestWindowContainerBuilder setIsAnimating(boolean isAnimating) {
+ mIsAnimating = isAnimating;
+ return this;
+ }
+
+ TestWindowContainerBuilder setIsVisible(boolean isVisible) {
+ mIsVisible = isVisible;
+ return this;
+ }
+
+ TestWindowContainerBuilder setOrientation(int orientation) {
+ mOrientation = orientation;
+ return this;
+ }
+
+ TestWindowContainerBuilder reset() {
+ mLayer = 0;
+ mIsAnimating = false;
+ mIsVisible = false;
+ mOrientation = null;
+ return this;
+ }
+
+ TestWindowContainer build() {
+ return new TestWindowContainer(mLayer, mIsAnimating, mIsVisible, mOrientation);
+ }
+ }
+
+ private class MockSurfaceBuildingContainer extends WindowContainer<WindowContainer> {
+ final SurfaceSession mSession = new SurfaceSession();
+
+ MockSurfaceBuildingContainer() {
+ super(sWm);
+ }
+
+ class MockSurfaceBuilder extends SurfaceControl.Builder {
+ MockSurfaceBuilder(SurfaceSession ss) {
+ super(ss);
+ }
+
+ @Override
+ public SurfaceControl build() {
+ return mock(SurfaceControl.class);
+ }
+ }
+
+ @Override
+ SurfaceControl.Builder makeChildSurface(WindowContainer child) {
+ return new MockSurfaceBuilder(mSession);
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowContainerTraversalTests.java b/services/tests/servicestests/src/com/android/server/wm/WindowContainerTraversalTests.java
new file mode 100644
index 0000000..e076399
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowContainerTraversalTests.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Matchers.eq;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.mockito.Mock;
+
+import java.util.function.Consumer;
+
+/**
+ * Tests for {@link WindowContainer#forAllWindows} and various implementations.
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WindowContainerTraversalTests extends WindowTestsBase {
+
+ @Test
+ public void testDockedDividerPosition() throws Exception {
+ final WindowState splitScreenWindow = createWindowOnStack(null,
+ WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION,
+ mDisplayContent, "splitScreenWindow");
+ final WindowState splitScreenSecondaryWindow = createWindowOnStack(null,
+ WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_STANDARD,
+ TYPE_BASE_APPLICATION, mDisplayContent, "splitScreenSecondaryWindow");
+
+ sWm.mInputMethodTarget = splitScreenWindow;
+
+ Consumer<WindowState> c = mock(Consumer.class);
+ mDisplayContent.forAllWindows(c, false);
+
+ verify(c).accept(eq(mDockedDividerWindow));
+ verify(c).accept(eq(mImeWindow));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowFrameTests.java b/services/tests/servicestests/src/com/android/server/wm/WindowFrameTests.java
new file mode 100644
index 0000000..5a56332
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowFrameTests.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.app.ActivityManager.TaskDescription;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.DisplayInfo;
+import android.view.Gravity;
+import android.view.IWindow;
+import android.view.WindowManager;
+
+import static android.view.DisplayCutout.fromBoundingRect;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.FILL_PARENT;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.server.wm.utils.WmDisplayCutout;
+
+/**
+ * Tests for the {@link WindowState#computeFrameLw} method and other window frame machinery.
+ *
+ * Build/Install/Run: bit FrameworksServicesTests:com.android.server.wm.WindowFrameTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WindowFrameTests extends WindowTestsBase {
+
+ private WindowToken mWindowToken;
+ private final IWindow mIWindow = new TestIWindow();
+
+ class WindowStateWithTask extends WindowState {
+ final Task mTask;
+ boolean mDockedResizingForTest = false;
+ WindowStateWithTask(WindowManager.LayoutParams attrs, Task t) {
+ super(sWm, null, mIWindow, mWindowToken, null, 0, 0, attrs, 0, 0,
+ false /* ownerCanAddInternalSystemWindow */);
+ mTask = t;
+ }
+
+ @Override
+ Task getTask() {
+ return mTask;
+ }
+
+ @Override
+ boolean isDockedResizing() {
+ return mDockedResizingForTest;
+ }
+ };
+
+ class TaskWithBounds extends Task {
+ final Rect mBounds;
+ final Rect mInsetBounds = new Rect();
+ boolean mFullscreenForTest = true;
+ TaskWithBounds(Rect bounds) {
+ super(0, mStubStack, 0, sWm, 0, false, new TaskDescription(), null);
+ mBounds = bounds;
+ setBounds(bounds);
+ }
+
+ @Override
+ public Rect getBounds() {
+ return mBounds;
+ }
+
+ @Override
+ public void getBounds(Rect out) {
+ out.set(mBounds);
+ }
+
+ @Override
+ public void getOverrideBounds(Rect outBounds) {
+ outBounds.set(mBounds);
+ }
+ @Override
+ void getTempInsetBounds(Rect outBounds) {
+ outBounds.set(mInsetBounds);
+ }
+ @Override
+ boolean isFullscreen() {
+ return mFullscreenForTest;
+ }
+ }
+
+ TaskStack mStubStack;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ // Just any non zero value.
+ sWm.mSystemDecorLayer = 10000;
+
+ mWindowToken = WindowTestUtils.createTestAppWindowToken(
+ sWm.getDefaultDisplayContentLocked());
+ mStubStack = new TaskStack(sWm, 0, null);
+ }
+
+ public void assertRect(Rect rect, int left, int top, int right, int bottom) {
+ assertEquals(left, rect.left);
+ assertEquals(top, rect.top);
+ assertEquals(right, rect.right);
+ assertEquals(bottom, rect.bottom);
+ }
+
+ @Test
+ public void testLayoutInFullscreenTaskInsets() throws Exception {
+ Task task = new TaskWithBounds(null); // fullscreen task doesn't use bounds for computeFrame
+ WindowState w = createWindow(task, FILL_PARENT, FILL_PARENT);
+ w.mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
+
+ final int bottomContentInset = 100;
+ final int topContentInset = 50;
+ final int bottomVisibleInset = 30;
+ final int topVisibleInset = 70;
+ final int leftStableInset = 20;
+ final int rightStableInset = 90;
+
+ // With no insets or system decor all the frames incoming from PhoneWindowManager
+ // are identical.
+ final Rect pf = new Rect(0, 0, 1000, 1000);
+ final Rect df = pf;
+ final Rect of = df;
+ final Rect cf = new Rect(pf);
+ // Produce some insets
+ cf.top += 50;
+ cf.bottom -= 100;
+ final Rect vf = new Rect(pf);
+ vf.top += topVisibleInset;
+ vf.bottom -= bottomVisibleInset;
+ final Rect sf = new Rect(pf);
+ sf.left += leftStableInset;
+ sf.right -= rightStableInset;
+
+ final Rect dcf = pf;
+ // When mFrame extends past cf, the content insets are
+ // the difference between mFrame and ContentFrame. Visible
+ // and stable frames work the same way.
+ w.computeFrameLw(pf, df, of, cf, vf, dcf, sf, null, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame,0, 0, 1000, 1000);
+ assertRect(w.mContentInsets, 0, topContentInset, 0, bottomContentInset);
+ assertRect(w.mVisibleInsets, 0, topVisibleInset, 0, bottomVisibleInset);
+ assertRect(w.mStableInsets, leftStableInset, 0, rightStableInset, 0);
+ // The frames remain as passed in shrunk to the window frame
+ assertTrue(cf.equals(w.getContentFrameLw()));
+ assertTrue(vf.equals(w.getVisibleFrameLw()));
+ assertTrue(sf.equals(w.getStableFrameLw()));
+ // On the other hand mFrame doesn't extend past cf we won't get any insets
+ w.mAttrs.x = 100;
+ w.mAttrs.y = 100;
+ w.mAttrs.width = 100; w.mAttrs.height = 100; //have to clear MATCH_PARENT
+ w.mRequestedWidth = 100;
+ w.mRequestedHeight = 100;
+ w.computeFrameLw(pf, df, of, cf, vf, dcf, sf, null, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, 100, 100, 200, 200);
+ assertRect(w.mContentInsets, 0, 0, 0, 0);
+ // In this case the frames are shrunk to the window frame.
+ assertTrue(w.mFrame.equals(w.getContentFrameLw()));
+ assertTrue(w.mFrame.equals(w.getVisibleFrameLw()));
+ assertTrue(w.mFrame.equals(w.getStableFrameLw()));
+ }
+
+ @Test
+ public void testLayoutInFullscreenTaskNoInsets() throws Exception {
+ Task task = new TaskWithBounds(null); // fullscreen task doesn't use bounds for computeFrame
+ WindowState w = createWindow(task, FILL_PARENT, FILL_PARENT);
+ w.mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
+
+ // With no insets or system decor all the frames incoming from PhoneWindowManager
+ // are identical.
+ final Rect pf = new Rect(0, 0, 1000, 1000);
+
+ // Here the window has FILL_PARENT, FILL_PARENT
+ // so we expect it to fill the entire available frame.
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, 0, 0, 1000, 1000);
+
+ // It can select various widths and heights within the bounds.
+ // Strangely the window attribute width is ignored for normal windows
+ // and we use mRequestedWidth/mRequestedHeight
+ w.mAttrs.width = 300;
+ w.mAttrs.height = 300;
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+ // Explicit width and height without requested width/height
+ // gets us nothing.
+ assertRect(w.mFrame, 0, 0, 0, 0);
+
+ w.mRequestedWidth = 300;
+ w.mRequestedHeight = 300;
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+ // With requestedWidth/Height we can freely choose our size within the
+ // parent bounds.
+ assertRect(w.mFrame, 0, 0, 300, 300);
+
+ // With FLAG_SCALED though, requestedWidth/height is used to control
+ // the unscaled surface size, and mAttrs.width/height becomes the
+ // layout controller.
+ w.mAttrs.flags = WindowManager.LayoutParams.FLAG_SCALED;
+ w.mRequestedHeight = -1;
+ w.mRequestedWidth = -1;
+ w.mAttrs.width = 100;
+ w.mAttrs.height = 100;
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, 0, 0, 100, 100);
+ w.mAttrs.flags = 0;
+
+ // But sizes too large will be clipped to the containing frame
+ w.mRequestedWidth = 1200;
+ w.mRequestedHeight = 1200;
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, 0, 0, 1000, 1000);
+
+ // Before they are clipped though windows will be shifted
+ w.mAttrs.x = 300;
+ w.mAttrs.y = 300;
+ w.mRequestedWidth = 1000;
+ w.mRequestedHeight = 1000;
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, 0, 0, 1000, 1000);
+
+ // If there is room to move around in the parent frame the window will be shifted according
+ // to gravity.
+ w.mAttrs.x = 0;
+ w.mAttrs.y = 0;
+ w.mRequestedWidth = 300;
+ w.mRequestedHeight = 300;
+ w.mAttrs.gravity = Gravity.RIGHT | Gravity.TOP;
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, 700, 0, 1000, 300);
+ w.mAttrs.gravity = Gravity.RIGHT | Gravity.BOTTOM;
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, 700, 700, 1000, 1000);
+ // Window specified x and y are interpreted as offsets in the opposite
+ // direction of gravity
+ w.mAttrs.x = 100;
+ w.mAttrs.y = 100;
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, 600, 600, 900, 900);
+ }
+
+ @Test
+ public void testLayoutNonfullscreenTask() {
+ final DisplayInfo displayInfo = sWm.getDefaultDisplayContentLocked().getDisplayInfo();
+ final int logicalWidth = displayInfo.logicalWidth;
+ final int logicalHeight = displayInfo.logicalHeight;
+
+ final int taskLeft = logicalWidth / 4;
+ final int taskTop = logicalHeight / 4;
+ final int taskRight = logicalWidth / 4 * 3;
+ final int taskBottom = logicalHeight / 4 * 3;
+ final Rect taskBounds = new Rect(taskLeft, taskTop, taskRight, taskBottom);
+ TaskWithBounds task = new TaskWithBounds(taskBounds);
+ task.mFullscreenForTest = false;
+ WindowState w = createWindow(task, FILL_PARENT, FILL_PARENT);
+ w.mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
+
+ final Rect pf = new Rect(0, 0, logicalWidth, logicalHeight);
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, null, WmDisplayCutout.NO_CUTOUT, false);
+ // For non fullscreen tasks the containing frame is based off the
+ // task bounds not the parent frame.
+ assertRect(w.mFrame, taskLeft, taskTop, taskRight, taskBottom);
+ assertRect(w.getContentFrameLw(), taskLeft, taskTop, taskRight, taskBottom);
+ assertRect(w.mContentInsets, 0, 0, 0, 0);
+
+ pf.set(0, 0, logicalWidth, logicalHeight);
+ // We still produce insets against the containing frame the same way.
+ final int cfRight = logicalWidth / 2;
+ final int cfBottom = logicalHeight / 2;
+ final Rect cf = new Rect(0, 0, cfRight, cfBottom);
+ w.computeFrameLw(pf, pf, pf, cf, cf, pf, cf, null, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, taskLeft, taskTop, taskRight, taskBottom);
+ int contentInsetRight = taskRight - cfRight;
+ int contentInsetBottom = taskBottom - cfBottom;
+ assertRect(w.mContentInsets, 0, 0, contentInsetRight, contentInsetBottom);
+ assertRect(w.getContentFrameLw(), taskLeft, taskTop, taskRight - contentInsetRight,
+ taskBottom - contentInsetBottom);
+
+ pf.set(0, 0, logicalWidth, logicalHeight);
+ // However if we set temp inset bounds, the insets will be computed
+ // as if our window was laid out there, but it will be laid out according to
+ // the task bounds.
+ final int insetLeft = logicalWidth / 5;
+ final int insetTop = logicalHeight / 5;
+ final int insetRight = insetLeft + (taskRight - taskLeft);
+ final int insetBottom = insetTop + (taskBottom - taskTop);
+ task.mInsetBounds.set(insetLeft, insetTop, insetRight, insetBottom);
+ w.computeFrameLw(pf, pf, pf, cf, cf, pf, cf, null, WmDisplayCutout.NO_CUTOUT, false);
+ assertRect(w.mFrame, taskLeft, taskTop, taskRight, taskBottom);
+ contentInsetRight = insetRight - cfRight;
+ contentInsetBottom = insetBottom - cfBottom;
+ assertRect(w.mContentInsets, 0, 0, contentInsetRight, contentInsetBottom);
+ assertRect(w.getContentFrameLw(), taskLeft, taskTop, taskRight - contentInsetRight,
+ taskBottom - contentInsetBottom);
+ }
+
+ @Test
+ public void testCalculatePolicyCrop() {
+ final WindowStateWithTask w = createWindow(
+ new TaskWithBounds(null), FILL_PARENT, FILL_PARENT);
+ w.mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
+
+ final DisplayInfo displayInfo = w.getDisplayContent().getDisplayInfo();
+ final int logicalWidth = displayInfo.logicalWidth;
+ final int logicalHeight = displayInfo.logicalHeight;
+ final Rect pf = new Rect(0, 0, logicalWidth, logicalHeight);
+ final Rect df = pf;
+ final Rect of = df;
+ final Rect cf = new Rect(pf);
+ // Produce some insets
+ cf.top += displayInfo.logicalWidth / 10;
+ cf.bottom -= displayInfo.logicalWidth / 5;
+ final Rect vf = cf;
+ final Rect sf = vf;
+ // We use a decor content frame with insets to produce cropping.
+ Rect dcf = cf;
+
+ final Rect policyCrop = new Rect();
+
+ w.computeFrameLw(pf, df, of, cf, vf, dcf, sf, null, WmDisplayCutout.NO_CUTOUT, false);
+ w.calculatePolicyCrop(policyCrop);
+ assertRect(policyCrop, 0, cf.top, logicalWidth, cf.bottom);
+
+ dcf.setEmpty();
+ // Likewise with no decor frame we would get no crop
+ w.computeFrameLw(pf, df, of, cf, vf, dcf, sf, null, WmDisplayCutout.NO_CUTOUT, false);
+ w.calculatePolicyCrop(policyCrop);
+ assertRect(policyCrop, 0, 0, logicalWidth, logicalHeight);
+
+ // Now we set up a window which doesn't fill the entire decor frame.
+ // Normally it would be cropped to it's frame but in the case of docked resizing
+ // we need to account for the fact the windows surface will be made
+ // fullscreen and thus also make the crop fullscreen.
+ w.mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
+ w.mAttrs.width = logicalWidth / 2;
+ w.mAttrs.height = logicalHeight / 2;
+ w.mRequestedWidth = logicalWidth / 2;
+ w.mRequestedHeight = logicalHeight / 2;
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, WmDisplayCutout.NO_CUTOUT, false);
+
+ w.calculatePolicyCrop(policyCrop);
+ // Normally the crop is shrunk from the decor frame
+ // to the computed window frame.
+ assertRect(policyCrop, 0, 0, logicalWidth / 2, logicalHeight / 2);
+
+ w.mDockedResizingForTest = true;
+ w.calculatePolicyCrop(policyCrop);
+ // But if we are docked resizing it won't be, however we will still be
+ // shrunk to the decor frame and the display.
+ assertRect(policyCrop, 0, 0,
+ Math.min(pf.width(), displayInfo.logicalWidth),
+ Math.min(pf.height(), displayInfo.logicalHeight));
+ }
+
+ @Test
+ public void testLayoutLetterboxedWindow() {
+ // First verify task behavior in multi-window mode.
+ final DisplayInfo displayInfo = sWm.getDefaultDisplayContentLocked().getDisplayInfo();
+ final int logicalWidth = displayInfo.logicalWidth;
+ final int logicalHeight = displayInfo.logicalHeight;
+
+ final int taskLeft = logicalWidth / 5;
+ final int taskTop = logicalHeight / 5;
+ final int taskRight = logicalWidth / 4 * 3;
+ final int taskBottom = logicalHeight / 4 * 3;
+ final Rect taskBounds = new Rect(taskLeft, taskTop, taskRight, taskBottom);
+ TaskWithBounds task = new TaskWithBounds(taskBounds);
+ task.mInsetBounds.set(taskLeft, taskTop, taskRight, taskBottom);
+ task.mFullscreenForTest = false;
+ WindowState w = createWindow(task, FILL_PARENT, FILL_PARENT);
+ w.mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
+
+ final Rect pf = new Rect(0, 0, logicalWidth, logicalHeight);
+ w.computeFrameLw(pf /* parentFrame */, pf /* displayFrame */, pf /* overscanFrame */,
+ pf /* contentFrame */, pf /* visibleFrame */, pf /* decorFrame */,
+ pf /* stableFrame */, null /* outsetFrame */, WmDisplayCutout.NO_CUTOUT, false);
+ // For non fullscreen tasks the containing frame is based off the
+ // task bounds not the parent frame.
+ assertRect(w.mFrame, taskLeft, taskTop, taskRight, taskBottom);
+ assertRect(w.getContentFrameLw(), taskLeft, taskTop, taskRight, taskBottom);
+ assertRect(w.mContentInsets, 0, 0, 0, 0);
+
+ // Now simulate switch to fullscreen for letterboxed app.
+ final int xInset = logicalWidth / 10;
+ final int yInset = logicalWidth / 10;
+ final Rect cf = new Rect(xInset, yInset, logicalWidth - xInset, logicalHeight - yInset);
+ Configuration config = new Configuration(w.mAppToken.getOverrideConfiguration());
+ config.windowConfiguration.setBounds(cf);
+ w.mAppToken.onOverrideConfigurationChanged(config);
+ pf.set(0, 0, logicalWidth, logicalHeight);
+ task.mFullscreenForTest = true;
+
+ w.computeFrameLw(pf /* parentFrame */, pf /* displayFrame */, pf /* overscanFrame */,
+ cf /* contentFrame */, cf /* visibleFrame */, pf /* decorFrame */,
+ cf /* stableFrame */, null /* outsetFrame */, WmDisplayCutout.NO_CUTOUT, false);
+ assertEquals(cf, w.mFrame);
+ assertEquals(cf, w.getContentFrameLw());
+ assertRect(w.mContentInsets, 0, 0, 0, 0);
+ }
+
+ @Test
+ public void testDisplayCutout() {
+ // Regular fullscreen task and window
+ Task task = new TaskWithBounds(null);
+ WindowState w = createWindow(task, FILL_PARENT, FILL_PARENT);
+ w.mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
+
+ final Rect pf = new Rect(0, 0, 1000, 2000);
+ // Create a display cutout of size 50x50, aligned top-center
+ final WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(500, 0, 550, 50), pf.width(), pf.height());
+
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, cutout, false);
+
+ assertEquals(w.mDisplayCutout.getDisplayCutout().getSafeInsetTop(), 50);
+ assertEquals(w.mDisplayCutout.getDisplayCutout().getSafeInsetBottom(), 0);
+ assertEquals(w.mDisplayCutout.getDisplayCutout().getSafeInsetLeft(), 0);
+ assertEquals(w.mDisplayCutout.getDisplayCutout().getSafeInsetRight(), 0);
+ }
+
+ @Test
+ public void testDisplayCutout_tempInsetBounds() {
+ // Regular fullscreen task and window
+ TaskWithBounds task = new TaskWithBounds(new Rect(0, -500, 1000, 1500));
+ task.mFullscreenForTest = false;
+ task.mInsetBounds.set(0, 0, 1000, 2000);
+ WindowState w = createWindow(task, FILL_PARENT, FILL_PARENT);
+ w.mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
+
+ final Rect pf = new Rect(0, -500, 1000, 1500);
+ // Create a display cutout of size 50x50, aligned top-center
+ final WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(500, 0, 550, 50), pf.width(), pf.height());
+
+ w.computeFrameLw(pf, pf, pf, pf, pf, pf, pf, pf, cutout, false);
+
+ assertEquals(w.mDisplayCutout.getDisplayCutout().getSafeInsetTop(), 50);
+ assertEquals(w.mDisplayCutout.getDisplayCutout().getSafeInsetBottom(), 0);
+ assertEquals(w.mDisplayCutout.getDisplayCutout().getSafeInsetLeft(), 0);
+ assertEquals(w.mDisplayCutout.getDisplayCutout().getSafeInsetRight(), 0);
+ }
+
+ private WindowStateWithTask createWindow(Task task, int width, int height) {
+ final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(TYPE_APPLICATION);
+ attrs.width = width;
+ attrs.height = height;
+
+ return new WindowStateWithTask(attrs, task);
+ }
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowManagerServiceRule.java b/services/tests/servicestests/src/com/android/server/wm/WindowManagerServiceRule.java
new file mode 100644
index 0000000..d91079e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowManagerServiceRule.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.testing.DexmakerShareClassLoaderRule.runWithDexmakerShareClassLoader;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import android.app.ActivityManagerInternal;
+import android.content.Context;
+import android.hardware.display.DisplayManagerInternal;
+import android.os.PowerManagerInternal;
+import android.os.PowerSaveState;
+import android.support.test.InstrumentationRegistry;
+import android.view.InputChannel;
+
+import com.android.server.LocalServices;
+import com.android.server.input.InputManagerService;
+import com.android.server.policy.WindowManagerPolicy;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import org.mockito.invocation.InvocationOnMock;
+
+/**
+ * A test rule that sets up a fresh WindowManagerService instance before each test and makes sure
+ * to properly tear it down after.
+ *
+ * <p>
+ * Usage:
+ * <pre>
+ * {@literal @}Rule
+ * public final WindowManagerServiceRule mWmRule = new WindowManagerServiceRule();
+ * </pre>
+ */
+public class WindowManagerServiceRule implements TestRule {
+
+ private WindowManagerService mService;
+ private TestWindowManagerPolicy mPolicy;
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ runWithDexmakerShareClassLoader(this::setUp);
+ try {
+ base.evaluate();
+ } finally {
+ tearDown();
+ }
+ }
+
+ private void setUp() {
+ final Context context = InstrumentationRegistry.getTargetContext();
+
+ removeServices();
+
+ LocalServices.addService(DisplayManagerInternal.class,
+ mock(DisplayManagerInternal.class));
+
+ LocalServices.addService(PowerManagerInternal.class,
+ mock(PowerManagerInternal.class));
+ final PowerManagerInternal pm =
+ LocalServices.getService(PowerManagerInternal.class);
+ PowerSaveState state = new PowerSaveState.Builder().build();
+ doReturn(state).when(pm).getLowPowerState(anyInt());
+
+ LocalServices.addService(ActivityManagerInternal.class,
+ mock(ActivityManagerInternal.class));
+ final ActivityManagerInternal am =
+ LocalServices.getService(ActivityManagerInternal.class);
+ doAnswer((InvocationOnMock invocationOnMock) -> {
+ final Runnable runnable = invocationOnMock.<Runnable>getArgument(0);
+ if (runnable != null) {
+ runnable.run();
+ }
+ return null;
+ }).when(am).notifyKeyguardFlagsChanged(any());
+
+ InputManagerService ims = mock(InputManagerService.class);
+ // InputChannel is final and can't be mocked.
+ InputChannel[] input = InputChannel.openInputChannelPair(TAG_WM);
+ if (input != null && input.length > 1) {
+ doReturn(input[1]).when(ims).monitorInput(anyString());
+ }
+
+ mService = WindowManagerService.main(context, ims, true, false,
+ false, mPolicy = new TestWindowManagerPolicy(
+ WindowManagerServiceRule.this::getWindowManagerService));
+
+ mService.onInitReady();
+
+ // Display creation is driven by the ActivityManagerService via ActivityStackSupervisor.
+ // We emulate those steps here.
+ mService.mRoot.createDisplayContent(
+ mService.mDisplayManager.getDisplay(DEFAULT_DISPLAY),
+ mock(DisplayWindowController.class));
+ }
+
+ private void removeServices() {
+ LocalServices.removeServiceForTest(DisplayManagerInternal.class);
+ LocalServices.removeServiceForTest(PowerManagerInternal.class);
+ LocalServices.removeServiceForTest(ActivityManagerInternal.class);
+ LocalServices.removeServiceForTest(WindowManagerInternal.class);
+ LocalServices.removeServiceForTest(WindowManagerPolicy.class);
+ }
+
+ private void tearDown() {
+ waitUntilWindowManagerHandlersIdle();
+ removeServices();
+ mService = null;
+ mPolicy = null;
+ }
+ };
+ }
+
+ public WindowManagerService getWindowManagerService() {
+ return mService;
+ }
+
+ public TestWindowManagerPolicy getWindowManagerPolicy() {
+ return mPolicy;
+ }
+
+ public void waitUntilWindowManagerHandlersIdle() {
+ final WindowManagerService wm = getWindowManagerService();
+ if (wm != null) {
+ wm.mH.runWithScissors(() -> { }, 0);
+ wm.mAnimationHandler.runWithScissors(() -> { }, 0);
+ SurfaceAnimationThread.getHandler().runWithScissors(() -> { }, 0);
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowManagerServiceRuleTest.java b/services/tests/servicestests/src/com/android/server/wm/WindowManagerServiceRuleTest.java
new file mode 100644
index 0000000..6cf6d7b
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowManagerServiceRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.junit.Assert.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@Presubmit
+@SmallTest
+public class WindowManagerServiceRuleTest {
+
+ @Rule
+ public final WindowManagerServiceRule mRule = new WindowManagerServiceRule();
+
+ @Test
+ public void testWindowManagerSetUp() {
+ assertThat(mRule.getWindowManagerService(), notNullValue());
+ }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowStateTests.java b/services/tests/servicestests/src/com/android/server/wm/WindowStateTests.java
new file mode 100644
index 0000000..85e846d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowStateTests.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import android.view.WindowManager;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.util.LinkedList;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
+import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL;
+import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
+
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for the {@link WindowState} class.
+ *
+ * atest FrameworksServicesTests:com.android.server.wm.WindowStateTests
+ */
+@SmallTest
+@FlakyTest(bugId = 74078662)
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WindowStateTests extends WindowTestsBase {
+
+ @Test
+ public void testIsParentWindowHidden() throws Exception {
+ final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow");
+ final WindowState child1 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child1");
+ final WindowState child2 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child2");
+
+ // parentWindow is initially set to hidden.
+ assertTrue(parentWindow.mHidden);
+ assertFalse(parentWindow.isParentWindowHidden());
+ assertTrue(child1.isParentWindowHidden());
+ assertTrue(child2.isParentWindowHidden());
+
+ parentWindow.mHidden = false;
+ assertFalse(parentWindow.isParentWindowHidden());
+ assertFalse(child1.isParentWindowHidden());
+ assertFalse(child2.isParentWindowHidden());
+
+ }
+
+ @Test
+ public void testIsChildWindow() throws Exception {
+ final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow");
+ final WindowState child1 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child1");
+ final WindowState child2 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child2");
+ final WindowState randomWindow = createWindow(null, TYPE_APPLICATION, "randomWindow");
+
+ assertFalse(parentWindow.isChildWindow());
+ assertTrue(child1.isChildWindow());
+ assertTrue(child2.isChildWindow());
+ assertFalse(randomWindow.isChildWindow());
+ }
+
+ @Test
+ public void testHasChild() throws Exception {
+ final WindowState win1 = createWindow(null, TYPE_APPLICATION, "win1");
+ final WindowState win11 = createWindow(win1, FIRST_SUB_WINDOW, "win11");
+ final WindowState win12 = createWindow(win1, FIRST_SUB_WINDOW, "win12");
+ final WindowState win2 = createWindow(null, TYPE_APPLICATION, "win2");
+ final WindowState win21 = createWindow(win2, FIRST_SUB_WINDOW, "win21");
+ final WindowState randomWindow = createWindow(null, TYPE_APPLICATION, "randomWindow");
+
+ assertTrue(win1.hasChild(win11));
+ assertTrue(win1.hasChild(win12));
+ assertTrue(win2.hasChild(win21));
+
+ assertFalse(win1.hasChild(win21));
+ assertFalse(win1.hasChild(randomWindow));
+
+ assertFalse(win2.hasChild(win11));
+ assertFalse(win2.hasChild(win12));
+ assertFalse(win2.hasChild(randomWindow));
+ }
+
+ @Test
+ public void testGetParentWindow() throws Exception {
+ final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow");
+ final WindowState child1 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child1");
+ final WindowState child2 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child2");
+
+ assertNull(parentWindow.getParentWindow());
+ assertEquals(parentWindow, child1.getParentWindow());
+ assertEquals(parentWindow, child2.getParentWindow());
+ }
+
+ @Test
+ public void testOverlayWindowHiddenWhenSuspended() {
+ final WindowState overlayWindow = spy(createWindow(null, TYPE_APPLICATION_OVERLAY,
+ "overlayWindow"));
+ overlayWindow.setHiddenWhileSuspended(true);
+ verify(overlayWindow).hideLw(true, true);
+ overlayWindow.setHiddenWhileSuspended(false);
+ verify(overlayWindow).showLw(true, true);
+ }
+
+ @Test
+ public void testGetTopParentWindow() throws Exception {
+ final WindowState root = createWindow(null, TYPE_APPLICATION, "root");
+ final WindowState child1 = createWindow(root, FIRST_SUB_WINDOW, "child1");
+ final WindowState child2 = createWindow(child1, FIRST_SUB_WINDOW, "child2");
+
+ assertEquals(root, root.getTopParentWindow());
+ assertEquals(root, child1.getTopParentWindow());
+ assertEquals(child1, child2.getParentWindow());
+ assertEquals(root, child2.getTopParentWindow());
+
+ // Test case were child is detached from parent.
+ root.removeChild(child1);
+ assertEquals(child1, child1.getTopParentWindow());
+ assertEquals(child1, child2.getParentWindow());
+ }
+
+ @Test
+ public void testIsOnScreen_hiddenByPolicy() {
+ final WindowState window = createWindow(null, TYPE_APPLICATION, "window");
+ window.setHasSurface(true);
+ assertTrue(window.isOnScreen());
+ window.hideLw(false /* doAnimation */);
+ assertFalse(window.isOnScreen());
+ }
+
+ @Test
+ public void testCanBeImeTarget() throws Exception {
+ final WindowState appWindow = createWindow(null, TYPE_APPLICATION, "appWindow");
+ final WindowState imeWindow = createWindow(null, TYPE_INPUT_METHOD, "imeWindow");
+
+ // Setting FLAG_NOT_FOCUSABLE without FLAG_ALT_FOCUSABLE_IM prevents the window from being
+ // an IME target.
+ appWindow.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
+ imeWindow.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
+
+ // Make windows visible
+ appWindow.setHasSurface(true);
+ imeWindow.setHasSurface(true);
+
+ // Windows without flags (FLAG_NOT_FOCUSABLE|FLAG_ALT_FOCUSABLE_IM) can't be IME targets
+ assertFalse(appWindow.canBeImeTarget());
+ assertFalse(imeWindow.canBeImeTarget());
+
+ // Add IME target flags
+ appWindow.mAttrs.flags |= (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM);
+ imeWindow.mAttrs.flags |= (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM);
+
+ // Visible app window with flags can be IME target while an IME window can never be an IME
+ // target regardless of its visibility or flags.
+ assertTrue(appWindow.canBeImeTarget());
+ assertFalse(imeWindow.canBeImeTarget());
+
+ // Make windows invisible
+ appWindow.hideLw(false /* doAnimation */);
+ imeWindow.hideLw(false /* doAnimation */);
+
+ // Invisible window can't be IME targets even if they have the right flags.
+ assertFalse(appWindow.canBeImeTarget());
+ assertFalse(imeWindow.canBeImeTarget());
+ }
+
+ @Test
+ public void testGetWindow() throws Exception {
+ final WindowState root = createWindow(null, TYPE_APPLICATION, "root");
+ final WindowState mediaChild = createWindow(root, TYPE_APPLICATION_MEDIA, "mediaChild");
+ final WindowState mediaOverlayChild = createWindow(root,
+ TYPE_APPLICATION_MEDIA_OVERLAY, "mediaOverlayChild");
+ final WindowState attachedDialogChild = createWindow(root,
+ TYPE_APPLICATION_ATTACHED_DIALOG, "attachedDialogChild");
+ final WindowState subPanelChild = createWindow(root,
+ TYPE_APPLICATION_SUB_PANEL, "subPanelChild");
+ final WindowState aboveSubPanelChild = createWindow(root,
+ TYPE_APPLICATION_ABOVE_SUB_PANEL, "aboveSubPanelChild");
+
+ final LinkedList<WindowState> windows = new LinkedList();
+
+ root.getWindow(w -> {
+ windows.addLast(w);
+ return false;
+ });
+
+ // getWindow should have returned candidate windows in z-order.
+ assertEquals(aboveSubPanelChild, windows.pollFirst());
+ assertEquals(subPanelChild, windows.pollFirst());
+ assertEquals(attachedDialogChild, windows.pollFirst());
+ assertEquals(root, windows.pollFirst());
+ assertEquals(mediaOverlayChild, windows.pollFirst());
+ assertEquals(mediaChild, windows.pollFirst());
+ assertTrue(windows.isEmpty());
+ }
+
+ @Test
+ public void testPrepareWindowToDisplayDuringRelayout() throws Exception {
+ testPrepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ testPrepareWindowToDisplayDuringRelayout(true /*wasVisible*/);
+
+ // Call prepareWindowToDisplayDuringRelayout for a window without FLAG_TURN_SCREEN_ON
+ // before calling prepareWindowToDisplayDuringRelayout for windows with flag in the same
+ // appWindowToken.
+ final AppWindowToken appWindowToken = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ final WindowState first = createWindow(null, TYPE_APPLICATION, appWindowToken, "first");
+ final WindowState second = createWindow(null, TYPE_APPLICATION, appWindowToken, "second");
+ second.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
+
+ reset(mPowerManagerWrapper);
+ first.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ verify(mPowerManagerWrapper, never()).wakeUp(anyLong(), anyString());
+ assertTrue(appWindowToken.canTurnScreenOn());
+
+ reset(mPowerManagerWrapper);
+ second.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ verify(mPowerManagerWrapper).wakeUp(anyLong(), anyString());
+ assertFalse(appWindowToken.canTurnScreenOn());
+
+ // Call prepareWindowToDisplayDuringRelayout for two window that have FLAG_TURN_SCREEN_ON
+ // from the same appWindowToken. Only one should trigger the wakeup.
+ appWindowToken.setCanTurnScreenOn(true);
+ first.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
+ second.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
+
+ reset(mPowerManagerWrapper);
+ first.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ verify(mPowerManagerWrapper).wakeUp(anyLong(), anyString());
+ assertFalse(appWindowToken.canTurnScreenOn());
+
+ reset(mPowerManagerWrapper);
+ second.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ verify(mPowerManagerWrapper, never()).wakeUp(anyLong(), anyString());
+ assertFalse(appWindowToken.canTurnScreenOn());
+
+ // Call prepareWindowToDisplayDuringRelayout for a windows that are not children of an
+ // appWindowToken. Both windows have the FLAG_TURNS_SCREEN_ON so both should call wakeup
+ final WindowToken windowToken = WindowTestUtils.createTestWindowToken(FIRST_SUB_WINDOW,
+ mDisplayContent);
+ final WindowState firstWindow = createWindow(null, TYPE_APPLICATION, windowToken,
+ "firstWindow");
+ final WindowState secondWindow = createWindow(null, TYPE_APPLICATION, windowToken,
+ "secondWindow");
+ firstWindow.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
+ secondWindow.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
+
+ reset(mPowerManagerWrapper);
+ firstWindow.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ verify(mPowerManagerWrapper).wakeUp(anyLong(), anyString());
+
+ reset(mPowerManagerWrapper);
+ secondWindow.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
+ verify(mPowerManagerWrapper).wakeUp(anyLong(), anyString());
+ }
+
+ @Test
+ public void testCanAffectSystemUiFlags() throws Exception {
+ final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ app.mToken.setHidden(false);
+ assertTrue(app.canAffectSystemUiFlags());
+ app.mToken.setHidden(true);
+ assertFalse(app.canAffectSystemUiFlags());
+ app.mToken.setHidden(false);
+ app.mAttrs.alpha = 0.0f;
+ assertFalse(app.canAffectSystemUiFlags());
+
+ }
+
+ @Test
+ public void testCanAffectSystemUiFlags_disallow() throws Exception {
+ final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ app.mToken.setHidden(false);
+ assertTrue(app.canAffectSystemUiFlags());
+ app.getTask().setCanAffectSystemUiFlags(false);
+ assertFalse(app.canAffectSystemUiFlags());
+ }
+
+ @Test
+ public void testIsSelfOrAncestorWindowAnimating() throws Exception {
+ final WindowState root = createWindow(null, TYPE_APPLICATION, "root");
+ final WindowState child1 = createWindow(root, FIRST_SUB_WINDOW, "child1");
+ final WindowState child2 = createWindow(child1, FIRST_SUB_WINDOW, "child2");
+ assertFalse(child2.isSelfOrAncestorWindowAnimatingExit());
+ child2.mAnimatingExit = true;
+ assertTrue(child2.isSelfOrAncestorWindowAnimatingExit());
+ child2.mAnimatingExit = false;
+ root.mAnimatingExit = true;
+ assertTrue(child2.isSelfOrAncestorWindowAnimatingExit());
+ }
+
+ @Test
+ public void testLayoutSeqResetOnReparent() throws Exception {
+ final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ app.mLayoutSeq = 1;
+ mDisplayContent.mLayoutSeq = 1;
+
+ app.onDisplayChanged(mDisplayContent);
+
+ assertThat(app.mLayoutSeq, not(is(mDisplayContent.mLayoutSeq)));
+ }
+
+ @Test
+ public void testDisplayIdUpdatedOnReparent() throws Exception {
+ final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ // fake a different display
+ app.mInputWindowHandle.displayId = mDisplayContent.getDisplayId() + 1;
+ app.onDisplayChanged(mDisplayContent);
+
+ assertThat(app.mInputWindowHandle.displayId, is(mDisplayContent.getDisplayId()));
+ assertThat(app.getDisplayId(), is(mDisplayContent.getDisplayId()));
+ }
+
+ private void testPrepareWindowToDisplayDuringRelayout(boolean wasVisible) {
+ reset(mPowerManagerWrapper);
+ final WindowState root = createWindow(null, TYPE_APPLICATION, "root");
+ root.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
+
+ root.prepareWindowToDisplayDuringRelayout(wasVisible /*wasVisible*/);
+ verify(mPowerManagerWrapper).wakeUp(anyLong(), anyString());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowSurfacePlacerTest.java b/services/tests/servicestests/src/com/android/server/wm/WindowSurfacePlacerTest.java
new file mode 100644
index 0000000..e173b7d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowSurfacePlacerTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.WindowManager.TRANSIT_TASK_CLOSE;
+import static android.view.WindowManager.TRANSIT_TASK_OPEN;
+import static junit.framework.Assert.assertEquals;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.WindowManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WindowSurfacePlacerTest extends WindowTestsBase {
+
+ private WindowSurfacePlacer mWindowSurfacePlacer;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mWindowSurfacePlacer = new WindowSurfacePlacer(sWm);
+ }
+
+ @Test
+ public void testTranslucentOpen() throws Exception {
+ synchronized (sWm.mWindowMap) {
+ final AppWindowToken behind = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ final AppWindowToken translucentOpening = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ translucentOpening.setFillsParent(false);
+ translucentOpening.setHidden(true);
+ sWm.mOpeningApps.add(behind);
+ sWm.mOpeningApps.add(translucentOpening);
+ assertEquals(WindowManager.TRANSIT_TRANSLUCENT_ACTIVITY_OPEN,
+ mWindowSurfacePlacer.maybeUpdateTransitToTranslucentAnim(TRANSIT_TASK_OPEN));
+ }
+ }
+
+ @Test
+ public void testTranslucentClose() throws Exception {
+ synchronized (sWm.mWindowMap) {
+ final AppWindowToken behind = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ final AppWindowToken translucentClosing = createAppWindowToken(mDisplayContent,
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ translucentClosing.setFillsParent(false);
+ sWm.mClosingApps.add(translucentClosing);
+ assertEquals(WindowManager.TRANSIT_TRANSLUCENT_ACTIVITY_CLOSE,
+ mWindowSurfacePlacer.maybeUpdateTransitToTranslucentAnim(TRANSIT_TASK_CLOSE));
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowTestUtils.java b/services/tests/servicestests/src/com/android/server/wm/WindowTestUtils.java
new file mode 100644
index 0000000..2e4740b
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowTestUtils.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.os.IBinder;
+import android.view.IApplicationToken;
+import android.view.IWindow;
+import android.view.WindowManager;
+
+import static android.app.AppOpsManager.OP_NONE;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+
+import static com.android.server.wm.WindowContainer.POSITION_TOP;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyFloat;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.mockito.invocation.InvocationOnMock;
+
+/**
+ * A collection of static functions that can be referenced by other test packages to provide access
+ * to WindowManager related test functionality.
+ */
+public class WindowTestUtils {
+ public static int sNextTaskId = 0;
+
+ /**
+ * Retrieves an instance of a mock {@link WindowManagerService}.
+ */
+ public static WindowManagerService getMockWindowManagerService() {
+ final WindowManagerService service = mock(WindowManagerService.class);
+ final WindowHashMap windowMap = new WindowHashMap();
+ when(service.getWindowManagerLock()).thenReturn(windowMap);
+ return service;
+ }
+
+ /**
+ * Creates a mock instance of {@link StackWindowController}.
+ */
+ public static StackWindowController createMockStackWindowContainerController() {
+ StackWindowController controller = mock(StackWindowController.class);
+ controller.mContainer = mock(TestTaskStack.class);
+
+ // many components rely on the {@link StackWindowController#adjustConfigurationForBounds}
+ // to properly set bounds values in the configuration. We must mimick those actions here.
+ doAnswer((InvocationOnMock invocationOnMock) -> {
+ final Configuration config = invocationOnMock.<Configuration>getArgument(7);
+ final Rect bounds = invocationOnMock.<Rect>getArgument(0);
+ config.windowConfiguration.setBounds(bounds);
+ return null;
+ }).when(controller).adjustConfigurationForBounds(any(), any(), any(), any(),
+ anyBoolean(), anyBoolean(), anyFloat(), any(), any(), anyInt());
+
+ return controller;
+ }
+
+ /** Creates a {@link Task} and adds it to the specified {@link TaskStack}. */
+ public static Task createTaskInStack(WindowManagerService service, TaskStack stack,
+ int userId) {
+ synchronized (service.mWindowMap) {
+ final Task newTask = new Task(sNextTaskId++, stack, userId, service, 0, false,
+ new ActivityManager.TaskDescription(), null);
+ stack.addTask(newTask, POSITION_TOP);
+ return newTask;
+ }
+ }
+
+ /**
+ * An extension of {@link TestTaskStack}, which overrides package scoped methods that would not
+ * normally be mocked out.
+ */
+ public static class TestTaskStack extends TaskStack {
+ TestTaskStack(WindowManagerService service, int stackId) {
+ super(service, stackId, null);
+ }
+
+ @Override
+ void addTask(Task task, int position, boolean showForAllUsers, boolean moveParents) {
+ // Do nothing.
+ }
+ }
+
+ static TestAppWindowToken createTestAppWindowToken(DisplayContent dc) {
+ synchronized (dc.mService.mWindowMap) {
+ return new TestAppWindowToken(dc);
+ }
+ }
+
+ /** Used so we can gain access to some protected members of the {@link AppWindowToken} class. */
+ public static class TestAppWindowToken extends AppWindowToken {
+ boolean mOnTop = false;
+
+ private TestAppWindowToken(DisplayContent dc) {
+ super(dc.mService, new IApplicationToken.Stub() {
+ public String getName() {return null;}
+ }, false, dc, true /* fillsParent */);
+ }
+
+ TestAppWindowToken(WindowManagerService service, IApplicationToken token,
+ boolean voiceInteraction, DisplayContent dc, long inputDispatchingTimeoutNanos,
+ boolean fullscreen, boolean showForAllUsers, int targetSdk, int orientation,
+ int rotationAnimationHint, int configChanges, boolean launchTaskBehind,
+ boolean alwaysFocusable, AppWindowContainerController controller) {
+ super(service, token, voiceInteraction, dc, inputDispatchingTimeoutNanos, fullscreen,
+ showForAllUsers, targetSdk, orientation, rotationAnimationHint, configChanges,
+ launchTaskBehind, alwaysFocusable, controller);
+ }
+
+ int getWindowsCount() {
+ return mChildren.size();
+ }
+
+ boolean hasWindow(WindowState w) {
+ return mChildren.contains(w);
+ }
+
+ WindowState getFirstChild() {
+ return mChildren.peekFirst();
+ }
+
+ WindowState getLastChild() {
+ return mChildren.peekLast();
+ }
+
+ int positionInParent() {
+ return getParent().mChildren.indexOf(this);
+ }
+
+ void setIsOnTop(boolean onTop) {
+ mOnTop = onTop;
+ }
+
+ @Override
+ boolean isOnTop() {
+ return mOnTop;
+ }
+ }
+
+ static TestWindowToken createTestWindowToken(int type, DisplayContent dc) {
+ return createTestWindowToken(type, dc, false /* persistOnEmpty */);
+ }
+
+ static TestWindowToken createTestWindowToken(int type, DisplayContent dc,
+ boolean persistOnEmpty) {
+ synchronized (dc.mService.mWindowMap) {
+ return new TestWindowToken(type, dc, persistOnEmpty);
+ }
+ }
+
+ /* Used so we can gain access to some protected members of the {@link WindowToken} class */
+ public static class TestWindowToken extends WindowToken {
+
+ private TestWindowToken(int type, DisplayContent dc, boolean persistOnEmpty) {
+ super(dc.mService, mock(IBinder.class), type, persistOnEmpty, dc,
+ false /* ownerCanManageAppTokens */);
+ }
+
+ int getWindowsCount() {
+ return mChildren.size();
+ }
+
+ boolean hasWindow(WindowState w) {
+ return mChildren.contains(w);
+ }
+ }
+
+ /* Used so we can gain access to some protected members of the {@link Task} class */
+ public static class TestTask extends Task {
+ boolean mShouldDeferRemoval = false;
+ boolean mOnDisplayChangedCalled = false;
+ private boolean mIsAnimating = false;
+
+ TestTask(int taskId, TaskStack stack, int userId, WindowManagerService service,
+ int resizeMode, boolean supportsPictureInPicture,
+ TaskWindowContainerController controller) {
+ super(taskId, stack, userId, service, resizeMode, supportsPictureInPicture,
+ new ActivityManager.TaskDescription(), controller);
+ }
+
+ boolean shouldDeferRemoval() {
+ return mShouldDeferRemoval;
+ }
+
+ int positionInParent() {
+ return getParent().mChildren.indexOf(this);
+ }
+
+ @Override
+ void onDisplayChanged(DisplayContent dc) {
+ super.onDisplayChanged(dc);
+ mOnDisplayChangedCalled = true;
+ }
+
+ @Override
+ boolean isSelfAnimating() {
+ return mIsAnimating;
+ }
+
+ void setLocalIsAnimating(boolean isAnimating) {
+ mIsAnimating = isAnimating;
+ }
+ }
+
+ /**
+ * Used so we can gain access to some protected members of {@link TaskWindowContainerController}
+ * class.
+ */
+ public static class TestTaskWindowContainerController extends TaskWindowContainerController {
+
+ TestTaskWindowContainerController(WindowTestsBase testsBase) {
+ this(testsBase.createStackControllerOnDisplay(testsBase.mDisplayContent));
+ }
+
+ TestTaskWindowContainerController(StackWindowController stackController) {
+ super(sNextTaskId++, new TaskWindowContainerListener() {
+ @Override
+ public void registerConfigurationChangeListener(
+ ConfigurationContainerListener listener) {
+
+ }
+
+ @Override
+ public void unregisterConfigurationChangeListener(
+ ConfigurationContainerListener listener) {
+
+ }
+
+ @Override
+ public void onSnapshotChanged(ActivityManager.TaskSnapshot snapshot) {
+
+ }
+
+ @Override
+ public void requestResize(Rect bounds, int resizeMode) {
+
+ }
+ }, stackController, 0 /* userId */, null /* bounds */, RESIZE_MODE_UNRESIZEABLE,
+ false /* supportsPictureInPicture */, true /* toTop*/,
+ true /* showForAllUsers */, new ActivityManager.TaskDescription(),
+ stackController.mService);
+ }
+
+ @Override
+ TestTask createTask(int taskId, TaskStack stack, int userId, int resizeMode,
+ boolean supportsPictureInPicture, ActivityManager.TaskDescription taskDescription) {
+ return new TestTask(taskId, stack, userId, mService, resizeMode,
+ supportsPictureInPicture, this);
+ }
+ }
+
+ public static class TestAppWindowContainerController extends AppWindowContainerController {
+
+ final IApplicationToken mToken;
+
+ TestAppWindowContainerController(TestTaskWindowContainerController taskController) {
+ this(taskController, new TestIApplicationToken());
+ }
+
+ TestAppWindowContainerController(TestTaskWindowContainerController taskController,
+ IApplicationToken token) {
+ super(taskController, token, null /* listener */, 0 /* index */,
+ SCREEN_ORIENTATION_UNSPECIFIED, true /* fullscreen */,
+ true /* showForAllUsers */, 0 /* configChanges */, false /* voiceInteraction */,
+ false /* launchTaskBehind */, false /* alwaysFocusable */,
+ 0 /* targetSdkVersion */, 0 /* rotationAnimationHint */,
+ 0 /* inputDispatchingTimeoutNanos */, taskController.mService);
+ mToken = token;
+ }
+
+ @Override
+ AppWindowToken createAppWindow(WindowManagerService service, IApplicationToken token,
+ boolean voiceInteraction, DisplayContent dc, long inputDispatchingTimeoutNanos,
+ boolean fullscreen, boolean showForAllUsers, int targetSdk, int orientation,
+ int rotationAnimationHint, int configChanges, boolean launchTaskBehind,
+ boolean alwaysFocusable, AppWindowContainerController controller) {
+ return new TestAppWindowToken(service, token, voiceInteraction, dc,
+ inputDispatchingTimeoutNanos, fullscreen, showForAllUsers, targetSdk,
+ orientation,
+ rotationAnimationHint, configChanges, launchTaskBehind, alwaysFocusable,
+ controller);
+ }
+
+ AppWindowToken getAppWindowToken(DisplayContent dc) {
+ return (AppWindowToken) dc.getWindowToken(mToken.asBinder());
+ }
+ }
+
+ public static class TestIApplicationToken implements IApplicationToken {
+
+ private final Binder mBinder = new Binder();
+ @Override
+ public IBinder asBinder() {
+ return mBinder;
+ }
+ @Override
+ public String getName() {
+ return null;
+ }
+ }
+
+ /** Used to track resize reports. */
+ public static class TestWindowState extends WindowState {
+ boolean resizeReported;
+
+ TestWindowState(WindowManagerService service, Session session, IWindow window,
+ WindowManager.LayoutParams attrs, WindowToken token) {
+ super(service, session, window, token, null, OP_NONE, 0, attrs, 0, 0,
+ false /* ownerCanAddInternalSystemWindow */);
+ }
+
+ @Override
+ void reportResized() {
+ super.reportResized();
+ resizeReported = true;
+ }
+
+ @Override
+ public boolean isGoneForLayoutLw() {
+ return false;
+ }
+
+ @Override
+ void updateResizingWindowIfNeeded() {
+ // Used in AppWindowTokenTests#testLandscapeSeascapeRotationRelayout to deceive
+ // the system that it can actually update the window.
+ boolean hadSurface = mHasSurface;
+ mHasSurface = true;
+
+ super.updateResizingWindowIfNeeded();
+
+ mHasSurface = hadSurface;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java
new file mode 100644
index 0000000..473a287
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.View.VISIBLE;
+
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManagerGlobal;
+import android.testing.DexmakerShareClassLoaderRule;
+import android.util.Log;
+import android.view.Display;
+import android.view.DisplayInfo;
+import org.junit.Assert;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.view.IWindow;
+import android.view.WindowManager;
+
+import static android.app.AppOpsManager.OP_NONE;
+import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
+import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
+import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
+import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG;
+import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
+import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
+import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+import static org.mockito.Mockito.mock;
+
+import com.android.server.AttributeCache;
+
+import java.util.HashSet;
+import java.util.LinkedList;
+
+/**
+ * Common base class for window manager unit test classes.
+ *
+ * Make sure any requests to WM hold the WM lock if needed b/73966377
+ */
+class WindowTestsBase {
+ private static final String TAG = WindowTestsBase.class.getSimpleName();
+ WindowManagerService sWm = null; // TODO(roosa): rename to mWm in follow-up CL
+ private final IWindow mIWindow = new TestIWindow();
+ private Session mMockSession;
+ // The default display is removed in {@link #setUp} and then we iterate over all displays to
+ // make sure we don't collide with any existing display. If we run into no other display, the
+ // added display should be treated as default. This cannot be the default display
+ private static int sNextDisplayId = DEFAULT_DISPLAY + 1;
+ static int sNextStackId = 1000;
+
+ DisplayContent mDisplayContent;
+ DisplayInfo mDisplayInfo = new DisplayInfo();
+ WindowState mWallpaperWindow;
+ WindowState mImeWindow;
+ WindowState mImeDialogWindow;
+ WindowState mStatusBarWindow;
+ WindowState mDockedDividerWindow;
+ WindowState mNavBarWindow;
+ WindowState mAppWindow;
+ WindowState mChildAppWindowAbove;
+ WindowState mChildAppWindowBelow;
+ HashSet<WindowState> mCommonWindows;
+ WallpaperController mWallpaperController;
+
+ @Rule
+ public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
+ new DexmakerShareClassLoaderRule();
+
+ @Rule
+ public final WindowManagerServiceRule mWmRule = new WindowManagerServiceRule();
+
+ static WindowState.PowerManagerWrapper mPowerManagerWrapper; // TODO(roosa): make non-static.
+
+ @Before
+ public void setUp() throws Exception {
+ // If @Before throws an exception, the error isn't logged. This will make sure any failures
+ // in the set up are clear. This can be removed when b/37850063 is fixed.
+ try {
+ mMockSession = mock(Session.class);
+ mPowerManagerWrapper = mock(WindowState.PowerManagerWrapper.class);
+
+ final Context context = InstrumentationRegistry.getTargetContext();
+ AttributeCache.init(context);
+
+ sWm = mWmRule.getWindowManagerService();
+ beforeCreateDisplay();
+
+ mWallpaperController = new WallpaperController(sWm);
+
+ context.getDisplay().getDisplayInfo(mDisplayInfo);
+ mDisplayContent = createNewDisplay();
+ sWm.mDisplayEnabled = true;
+ sWm.mDisplayReady = true;
+
+ // Set-up some common windows.
+ mCommonWindows = new HashSet();
+ synchronized (sWm.mWindowMap) {
+ mWallpaperWindow = createCommonWindow(null, TYPE_WALLPAPER, "wallpaperWindow");
+ mImeWindow = createCommonWindow(null, TYPE_INPUT_METHOD, "mImeWindow");
+ sWm.mInputMethodWindow = mImeWindow;
+ mImeDialogWindow = createCommonWindow(null, TYPE_INPUT_METHOD_DIALOG,
+ "mImeDialogWindow");
+ mStatusBarWindow = createCommonWindow(null, TYPE_STATUS_BAR, "mStatusBarWindow");
+ mNavBarWindow = createCommonWindow(null, TYPE_NAVIGATION_BAR, "mNavBarWindow");
+ mDockedDividerWindow = createCommonWindow(null, TYPE_DOCK_DIVIDER,
+ "mDockedDividerWindow");
+ mAppWindow = createCommonWindow(null, TYPE_BASE_APPLICATION, "mAppWindow");
+ mChildAppWindowAbove = createCommonWindow(mAppWindow,
+ TYPE_APPLICATION_ATTACHED_DIALOG,
+ "mChildAppWindowAbove");
+ mChildAppWindowBelow = createCommonWindow(mAppWindow,
+ TYPE_APPLICATION_MEDIA_OVERLAY,
+ "mChildAppWindowBelow");
+ }
+ // Adding a display will cause freezing the display. Make sure to wait until it's
+ // unfrozen to not run into race conditions with the tests.
+ waitUntilHandlersIdle();
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to set up test", e);
+ throw e;
+ }
+ }
+
+ void beforeCreateDisplay() {
+ // Called before display is created.
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ // If @After throws an exception, the error isn't logged. This will make sure any failures
+ // in the tear down are clear. This can be removed when b/37850063 is fixed.
+ try {
+ final LinkedList<WindowState> nonCommonWindows = new LinkedList();
+
+ synchronized (sWm.mWindowMap) {
+ sWm.mRoot.forAllWindows(w -> {
+ if (!mCommonWindows.contains(w)) {
+ nonCommonWindows.addLast(w);
+ }
+ }, true /* traverseTopToBottom */);
+
+ while (!nonCommonWindows.isEmpty()) {
+ nonCommonWindows.pollLast().removeImmediately();
+ }
+
+ mDisplayContent.removeImmediately();
+ sWm.mInputMethodTarget = null;
+ sWm.mClosingApps.clear();
+ sWm.mOpeningApps.clear();
+ }
+
+ // Wait until everything is really cleaned up.
+ waitUntilHandlersIdle();
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to tear down test", e);
+ throw e;
+ }
+ }
+
+ /**
+ * @return A SurfaceBuilderFactory to inject in to the WindowManagerService during
+ * set-up (or null).
+ */
+ SurfaceBuilderFactory getSurfaceBuilderFactory() {
+ return null;
+ }
+
+ private WindowState createCommonWindow(WindowState parent, int type, String name) {
+ synchronized (sWm.mWindowMap) {
+ final WindowState win = createWindow(parent, type, name);
+ mCommonWindows.add(win);
+ // Prevent common windows from been IMe targets
+ win.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
+ return win;
+ }
+ }
+
+ /** Asserts that the first entry is greater than the second entry. */
+ void assertGreaterThan(int first, int second) throws Exception {
+ Assert.assertTrue("Excepted " + first + " to be greater than " + second, first > second);
+ }
+
+ /** Asserts that the first entry is greater than the second entry. */
+ void assertLessThan(int first, int second) throws Exception {
+ Assert.assertTrue("Excepted " + first + " to be less than " + second, first < second);
+ }
+
+ /**
+ * Waits until the main handler for WM has processed all messages.
+ */
+ void waitUntilHandlersIdle() {
+ mWmRule.waitUntilWindowManagerHandlersIdle();
+ }
+
+ private WindowToken createWindowToken(
+ DisplayContent dc, int windowingMode, int activityType, int type) {
+ synchronized (sWm.mWindowMap) {
+ if (type < FIRST_APPLICATION_WINDOW || type > LAST_APPLICATION_WINDOW) {
+ return WindowTestUtils.createTestWindowToken(type, dc);
+ }
+
+ return createAppWindowToken(dc, windowingMode, activityType);
+ }
+ }
+
+ AppWindowToken createAppWindowToken(DisplayContent dc, int windowingMode, int activityType) {
+ final TaskStack stack = createStackControllerOnStackOnDisplay(windowingMode, activityType,
+ dc).mContainer;
+ final Task task = createTaskInStack(stack, 0 /* userId */);
+ final WindowTestUtils.TestAppWindowToken appWindowToken =
+ WindowTestUtils.createTestAppWindowToken(dc);
+ task.addChild(appWindowToken, 0);
+ return appWindowToken;
+ }
+
+ WindowState createWindow(WindowState parent, int type, String name) {
+ synchronized (sWm.mWindowMap) {
+ return (parent == null)
+ ? createWindow(parent, type, mDisplayContent, name)
+ : createWindow(parent, type, parent.mToken, name);
+ }
+ }
+
+ WindowState createWindowOnStack(WindowState parent, int windowingMode, int activityType,
+ int type, DisplayContent dc, String name) {
+ synchronized (sWm.mWindowMap) {
+ final WindowToken token = createWindowToken(dc, windowingMode, activityType, type);
+ return createWindow(parent, type, token, name);
+ }
+ }
+
+ WindowState createAppWindow(Task task, int type, String name) {
+ synchronized (sWm.mWindowMap) {
+ final AppWindowToken token = WindowTestUtils.createTestAppWindowToken(mDisplayContent);
+ task.addChild(token, 0);
+ return createWindow(null, type, token, name);
+ }
+ }
+
+ WindowState createWindow(WindowState parent, int type, DisplayContent dc, String name) {
+ synchronized (sWm.mWindowMap) {
+ final WindowToken token = createWindowToken(
+ dc, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, type);
+ return createWindow(parent, type, token, name);
+ }
+ }
+
+ WindowState createWindow(WindowState parent, int type, DisplayContent dc, String name,
+ boolean ownerCanAddInternalSystemWindow) {
+ synchronized (sWm.mWindowMap) {
+ final WindowToken token = createWindowToken(
+ dc, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, type);
+ return createWindow(parent, type, token, name, 0 /* ownerId */,
+ ownerCanAddInternalSystemWindow);
+ }
+ }
+
+ WindowState createWindow(WindowState parent, int type, WindowToken token, String name) {
+ synchronized (sWm.mWindowMap) {
+ return createWindow(parent, type, token, name, 0 /* ownerId */,
+ false /* ownerCanAddInternalSystemWindow */);
+ }
+ }
+
+ WindowState createWindow(WindowState parent, int type, WindowToken token, String name,
+ int ownerId, boolean ownerCanAddInternalSystemWindow) {
+ return createWindow(parent, type, token, name, ownerId, ownerCanAddInternalSystemWindow,
+ sWm, mMockSession, mIWindow);
+ }
+
+ static WindowState createWindow(WindowState parent, int type, WindowToken token,
+ String name, int ownerId, boolean ownerCanAddInternalSystemWindow,
+ WindowManagerService service, Session session, IWindow iWindow) {
+ synchronized (service.mWindowMap) {
+ final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(type);
+ attrs.setTitle(name);
+
+ final WindowState w = new WindowState(service, session, iWindow, token, parent,
+ OP_NONE,
+ 0, attrs, VISIBLE, ownerId, ownerCanAddInternalSystemWindow,
+ mPowerManagerWrapper);
+ // TODO: Probably better to make this call in the WindowState ctor to avoid errors with
+ // adding it to the token...
+ token.addWindow(w);
+ return w;
+ }
+ }
+
+ /** Creates a {@link TaskStack} and adds it to the specified {@link DisplayContent}. */
+ TaskStack createTaskStackOnDisplay(DisplayContent dc) {
+ synchronized (sWm.mWindowMap) {
+ return createStackControllerOnDisplay(dc).mContainer;
+ }
+ }
+
+ StackWindowController createStackControllerOnDisplay(DisplayContent dc) {
+ synchronized (sWm.mWindowMap) {
+ return createStackControllerOnStackOnDisplay(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, dc);
+ }
+ }
+
+ StackWindowController createStackControllerOnStackOnDisplay(
+ int windowingMode, int activityType, DisplayContent dc) {
+ synchronized (sWm.mWindowMap) {
+ final Configuration overrideConfig = new Configuration();
+ overrideConfig.windowConfiguration.setWindowingMode(windowingMode);
+ overrideConfig.windowConfiguration.setActivityType(activityType);
+ final int stackId = ++sNextStackId;
+ final StackWindowController controller = new StackWindowController(stackId, null,
+ dc.getDisplayId(), true /* onTop */, new Rect(), sWm);
+ controller.onOverrideConfigurationChanged(overrideConfig);
+ return controller;
+ }
+ }
+
+ /** Creates a {@link Task} and adds it to the specified {@link TaskStack}. */
+ Task createTaskInStack(TaskStack stack, int userId) {
+ return WindowTestUtils.createTaskInStack(sWm, stack, userId);
+ }
+
+ /** Creates a {@link DisplayContent} and adds it to the system. */
+ DisplayContent createNewDisplay() {
+ final int displayId = sNextDisplayId++;
+ final Display display = new Display(DisplayManagerGlobal.getInstance(), displayId,
+ mDisplayInfo, DEFAULT_DISPLAY_ADJUSTMENTS);
+ synchronized (sWm.mWindowMap) {
+ return new DisplayContent(display, sWm, mWallpaperController,
+ mock(DisplayWindowController.class));
+ }
+ }
+
+ /** Creates a {@link com.android.server.wm.WindowTestUtils.TestWindowState} */
+ WindowTestUtils.TestWindowState createWindowState(WindowManager.LayoutParams attrs,
+ WindowToken token) {
+ synchronized (sWm.mWindowMap) {
+ return new WindowTestUtils.TestWindowState(sWm, mMockSession, mIWindow, attrs, token);
+ }
+ }
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowTokenTests.java b/services/tests/servicestests/src/com/android/server/wm/WindowTokenTests.java
new file mode 100644
index 0000000..e3b7174
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowTokenTests.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for the {@link WindowToken} class.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.WindowTokenTests
+ */
+@SmallTest
+@FlakyTest(bugId = 74078662)
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WindowTokenTests extends WindowTestsBase {
+
+ @Test
+ public void testAddWindow() throws Exception {
+ final WindowTestUtils.TestWindowToken token =
+ WindowTestUtils.createTestWindowToken(0, mDisplayContent);
+
+ assertEquals(0, token.getWindowsCount());
+
+ final WindowState window1 = createWindow(null, TYPE_APPLICATION, token, "window1");
+ final WindowState window11 = createWindow(window1, FIRST_SUB_WINDOW, token, "window11");
+ final WindowState window12 = createWindow(window1, FIRST_SUB_WINDOW, token, "window12");
+ final WindowState window2 = createWindow(null, TYPE_APPLICATION, token, "window2");
+ final WindowState window3 = createWindow(null, TYPE_APPLICATION, token, "window3");
+
+ token.addWindow(window1);
+ // NOTE: Child windows will not be added to the token as window containers can only
+ // contain/reference their direct children.
+ token.addWindow(window11);
+ token.addWindow(window12);
+ token.addWindow(window2);
+ token.addWindow(window3);
+
+ // Should not contain the child windows that were added above.
+ assertEquals(3, token.getWindowsCount());
+ assertTrue(token.hasWindow(window1));
+ assertFalse(token.hasWindow(window11));
+ assertFalse(token.hasWindow(window12));
+ assertTrue(token.hasWindow(window2));
+ assertTrue(token.hasWindow(window3));
+ }
+
+ @Test
+ public void testChildRemoval() throws Exception {
+ final DisplayContent dc = mDisplayContent;
+ final WindowTestUtils.TestWindowToken token = WindowTestUtils.createTestWindowToken(0, dc);
+
+ assertEquals(token, dc.getWindowToken(token.token));
+
+ final WindowState window1 = createWindow(null, TYPE_APPLICATION, token, "window1");
+ final WindowState window2 = createWindow(null, TYPE_APPLICATION, token, "window2");
+
+ window2.removeImmediately();
+ // The token should still be mapped in the display content since it still has a child.
+ assertEquals(token, dc.getWindowToken(token.token));
+
+ window1.removeImmediately();
+ // The token should have been removed from the display content since it no longer has a
+ // child.
+ assertEquals(null, dc.getWindowToken(token.token));
+ }
+
+ /**
+ * Test that a window token isn't orphaned by the system when it is requested to be removed.
+ * Tokens should only be removed from the system when all their windows are gone.
+ */
+ @Test
+ public void testTokenRemovalProcess() throws Exception {
+ final WindowTestUtils.TestWindowToken token = WindowTestUtils.createTestWindowToken(
+ TYPE_TOAST, mDisplayContent, true /* persistOnEmpty */);
+
+ // Verify that the token is on the display
+ assertNotNull(mDisplayContent.getWindowToken(token.token));
+
+ final WindowState window1 = createWindow(null, TYPE_TOAST, token, "window1");
+ final WindowState window2 = createWindow(null, TYPE_TOAST, token, "window2");
+
+ mDisplayContent.removeWindowToken(token.token);
+ // Verify that the token is no longer mapped on the display
+ assertNull(mDisplayContent.getWindowToken(token.token));
+ // Verify that the token is still attached to its parent
+ assertNotNull(token.getParent());
+ // Verify that the token windows are still around.
+ assertEquals(2, token.getWindowsCount());
+
+ window1.removeImmediately();
+ // Verify that the token is still attached to its parent
+ assertNotNull(token.getParent());
+ // Verify that the other token window is still around.
+ assertEquals(1, token.getWindowsCount());
+
+ window2.removeImmediately();
+ // Verify that the token is no-longer attached to its parent
+ assertNull(token.getParent());
+ // Verify that the token windows are no longer attached to it.
+ assertEquals(0, token.getWindowsCount());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowTracingTest.java b/services/tests/servicestests/src/com/android/server/wm/WindowTracingTest.java
new file mode 100644
index 0000000..5085254
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowTracingTest.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.filters.FlakyTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.wm.WindowManagerTraceProto;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Test class for {@link WindowTracing}.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.WindowTracingTest
+ */
+@SmallTest
+@FlakyTest(bugId = 74078662)
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WindowTracingTest extends WindowTestsBase {
+
+ private static final byte[] MAGIC_HEADER = new byte[] {
+ 0x9, 0x57, 0x49, 0x4e, 0x54, 0x52, 0x41, 0x43, 0x45,
+ };
+
+ private Context mTestContext;
+ private WindowTracing mWindowTracing;
+ private WindowManagerService mWmMock;
+ private File mFile;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mWmMock = mock(WindowManagerService.class);
+
+ mTestContext = InstrumentationRegistry.getContext();
+
+ mFile = mTestContext.getFileStreamPath("tracing_test.dat");
+ mFile.delete();
+
+ mWindowTracing = new WindowTracing(mFile);
+ }
+
+ @Test
+ public void isEnabled_returnsFalseByDefault() throws Exception {
+ assertFalse(mWindowTracing.isEnabled());
+ }
+
+ @Test
+ public void isEnabled_returnsTrueAfterStart() throws Exception {
+ mWindowTracing.startTrace(mock(PrintWriter.class));
+ assertTrue(mWindowTracing.isEnabled());
+ }
+
+ @Test
+ public void isEnabled_returnsFalseAfterStop() throws Exception {
+ mWindowTracing.startTrace(mock(PrintWriter.class));
+ mWindowTracing.stopTrace(mock(PrintWriter.class));
+ assertFalse(mWindowTracing.isEnabled());
+ }
+
+ @Test
+ public void trace_discared_whenNotTracing() throws Exception {
+ mWindowTracing.traceStateLocked("where", mWmMock);
+ verifyZeroInteractions(mWmMock);
+ }
+
+ @Test
+ public void trace_dumpsWindowManagerState_whenTracing() throws Exception {
+ mWindowTracing.startTrace(mock(PrintWriter.class));
+ mWindowTracing.traceStateLocked("where", mWmMock);
+
+ verify(mWmMock).writeToProtoLocked(any(), eq(true));
+ }
+
+ @Test
+ public void traceFile_startsWithMagicHeader() throws Exception {
+ mWindowTracing.startTrace(mock(PrintWriter.class));
+ mWindowTracing.stopTrace(mock(PrintWriter.class));
+
+ byte[] header = new byte[MAGIC_HEADER.length];
+ try (InputStream is = new FileInputStream(mFile)) {
+ assertEquals(MAGIC_HEADER.length, is.read(header));
+ assertArrayEquals(MAGIC_HEADER, header);
+ }
+ }
+
+ @Test
+ @Ignore("Figure out why this test is crashing when setting up mWmMock.")
+ public void tracing_endsUpInFile() throws Exception {
+ mWindowTracing.startTrace(mock(PrintWriter.class));
+
+ doAnswer((inv) -> {
+ inv.<ProtoOutputStream>getArgument(0).write(
+ WindowManagerTraceProto.WHERE, "TEST_WM_PROTO");
+ return null;
+ }).when(mWmMock).writeToProtoLocked(any(), any());
+ mWindowTracing.traceStateLocked("TEST_WHERE", mWmMock);
+
+ mWindowTracing.stopTrace(mock(PrintWriter.class));
+
+ byte[] file = new byte[1000];
+ int fileLength;
+ try (InputStream is = new FileInputStream(mFile)) {
+ fileLength = is.read(file);
+ assertTrue(containsBytes(file, fileLength,
+ "TEST_WHERE".getBytes(StandardCharsets.UTF_8)));
+ assertTrue(containsBytes(file, fileLength,
+ "TEST_WM_PROTO".getBytes(StandardCharsets.UTF_8)));
+ }
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+
+ mFile.delete();
+ }
+
+ /** Return true if {@code needle} appears anywhere in {@code haystack[0..length]} */
+ boolean containsBytes(byte[] haystack, int haystackLenght, byte[] needle) {
+ Preconditions.checkArgument(haystackLenght > 0);
+ Preconditions.checkArgument(needle.length > 0);
+
+ outer: for (int i = 0; i <= haystackLenght - needle.length; i++) {
+ for (int j = 0; j < needle.length; j++) {
+ if (haystack[i+j] != needle[j]) {
+ continue outer;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Test
+ public void test_containsBytes() {
+ byte[] haystack = "hello_world".getBytes(StandardCharsets.UTF_8);
+ assertTrue(containsBytes(haystack, haystack.length,
+ "hello".getBytes(StandardCharsets.UTF_8)));
+ assertTrue(containsBytes(haystack, haystack.length,
+ "world".getBytes(StandardCharsets.UTF_8)));
+ assertFalse(containsBytes(haystack, 6,
+ "world".getBytes(StandardCharsets.UTF_8)));
+ assertFalse(containsBytes(haystack, haystack.length,
+ "world_".getBytes(StandardCharsets.UTF_8)));
+ assertFalse(containsBytes(haystack, haystack.length,
+ "absent".getBytes(StandardCharsets.UTF_8)));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/ZOrderingTests.java b/services/tests/servicestests/src/com/android/server/wm/ZOrderingTests.java
new file mode 100644
index 0000000..547be55
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/ZOrderingTests.java
@@ -0,0 +1,399 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
+import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
+import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL;
+import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL;
+import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.filters.FlakyTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+
+/**
+ * Tests for the {@link WindowLayersController} class.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.wm.ZOrderingTests
+ */
+@SmallTest
+@FlakyTest(bugId = 74078662)
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class ZOrderingTests extends WindowTestsBase {
+
+ private class LayerRecordingTransaction extends SurfaceControl.Transaction {
+ HashMap<SurfaceControl, Integer> mLayersForControl = new HashMap();
+ HashMap<SurfaceControl, SurfaceControl> mRelativeLayersForControl = new HashMap();
+
+ @Override
+ public SurfaceControl.Transaction setLayer(SurfaceControl sc, int layer) {
+ mRelativeLayersForControl.remove(sc);
+ mLayersForControl.put(sc, layer);
+ return super.setLayer(sc, layer);
+ }
+
+ @Override
+ public SurfaceControl.Transaction setRelativeLayer(SurfaceControl sc,
+ SurfaceControl relativeTo,
+ int layer) {
+ mRelativeLayersForControl.put(sc, relativeTo);
+ mLayersForControl.put(sc, layer);
+ return super.setRelativeLayer(sc, relativeTo, layer);
+ }
+
+ private int getLayer(SurfaceControl sc) {
+ return mLayersForControl.getOrDefault(sc, 0);
+ }
+
+ private SurfaceControl getRelativeLayer(SurfaceControl sc) {
+ return mRelativeLayersForControl.get(sc);
+ }
+ };
+
+ // We have WM use our Hierarchy recording subclass of SurfaceControl.Builder
+ // such that we can keep track of the parents of Surfaces as they are constructed.
+ private HashMap<SurfaceControl, SurfaceControl> mParentFor = new HashMap();
+
+ private class HierarchyRecorder extends SurfaceControl.Builder {
+ SurfaceControl mPendingParent;
+
+ HierarchyRecorder(SurfaceSession s) {
+ super(s);
+ }
+
+ public SurfaceControl.Builder setParent(SurfaceControl sc) {
+ mPendingParent = sc;
+ return super.setParent(sc);
+ }
+ public SurfaceControl build() {
+ SurfaceControl sc = super.build();
+ mParentFor.put(sc, mPendingParent);
+ mPendingParent = null;
+ return sc;
+ }
+ };
+
+ class HierarchyRecordingBuilderFactory implements SurfaceBuilderFactory {
+ public SurfaceControl.Builder make(SurfaceSession s) {
+ return new HierarchyRecorder(s);
+ }
+ };
+
+ private LayerRecordingTransaction mTransaction;
+
+ @Override
+ void beforeCreateDisplay() {
+ // We can't use @Before here because it may happen after WindowTestsBase @Before
+ // which is after construction of the DisplayContent, meaning the HierarchyRecorder
+ // would miss construction of the top-level layers.
+ mTransaction = new LayerRecordingTransaction();
+ sWm.mSurfaceBuilderFactory = new HierarchyRecordingBuilderFactory();
+ sWm.mTransactionFactory = () -> mTransaction;
+ }
+
+ @After
+ public void after() {
+ mTransaction.close();
+ mParentFor.clear();
+ }
+
+ LinkedList<SurfaceControl> getAncestors(LayerRecordingTransaction t, SurfaceControl sc) {
+ LinkedList<SurfaceControl> p = new LinkedList();
+ SurfaceControl current = sc;
+ do {
+ p.addLast(current);
+
+ SurfaceControl rs = t.getRelativeLayer(current);
+ if (rs != null) {
+ current = rs;
+ } else {
+ current = mParentFor.get(current);
+ }
+ } while (current != null);
+ return p;
+ }
+
+
+ void assertZOrderGreaterThan(LayerRecordingTransaction t, SurfaceControl left,
+ SurfaceControl right) throws Exception {
+ final LinkedList<SurfaceControl> leftParentChain = getAncestors(t, left);
+ final LinkedList<SurfaceControl> rightParentChain = getAncestors(t, right);
+
+ SurfaceControl commonAncestor = null;
+ SurfaceControl leftTop = leftParentChain.peekLast();
+ SurfaceControl rightTop = rightParentChain.peekLast();
+ while (leftTop != null && rightTop != null && leftTop == rightTop) {
+ commonAncestor = leftParentChain.removeLast();
+ rightParentChain.removeLast();
+ leftTop = leftParentChain.peekLast();
+ rightTop = rightParentChain.peekLast();
+ }
+
+ if (rightTop == null) { // right is the parent of left.
+ assertGreaterThan(t.getLayer(leftTop), 0);
+ } else if (leftTop == null) { // left is the parent of right.
+ assertGreaterThan(0, t.getLayer(rightTop));
+ } else {
+ assertGreaterThan(t.getLayer(leftTop),
+ t.getLayer(rightTop));
+ }
+ }
+
+ void assertWindowHigher(WindowState left, WindowState right) throws Exception {
+ assertZOrderGreaterThan(mTransaction, left.getSurfaceControl(), right.getSurfaceControl());
+ }
+
+ WindowState createWindow(String name) {
+ return createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, name);
+ }
+
+ @Test
+ public void testAssignWindowLayers_ForImeWithNoTarget() throws Exception {
+ sWm.mInputMethodTarget = null;
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ // The Ime has an higher base layer than app windows and lower base layer than system
+ // windows, so it should be above app windows and below system windows if there isn't an IME
+ // target.
+ assertWindowHigher(mImeWindow, mChildAppWindowAbove);
+ assertWindowHigher(mImeWindow, mAppWindow);
+ assertWindowHigher(mNavBarWindow, mImeWindow);
+ assertWindowHigher(mStatusBarWindow, mImeWindow);
+
+ // And, IME dialogs should always have an higher layer than the IME.
+ assertWindowHigher(mImeDialogWindow, mImeWindow);
+ }
+
+ @Test
+ public void testAssignWindowLayers_ForImeWithAppTarget() throws Exception {
+ final WindowState imeAppTarget = createWindow("imeAppTarget");
+ sWm.mInputMethodTarget = imeAppTarget;
+
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ // Ime should be above all app windows and below system windows if it is targeting an app
+ // window.
+ assertWindowHigher(mImeWindow, imeAppTarget);
+ assertWindowHigher(mImeWindow, mChildAppWindowAbove);
+ assertWindowHigher(mImeWindow, mAppWindow);
+ assertWindowHigher(mNavBarWindow, mImeWindow);
+ assertWindowHigher(mStatusBarWindow, mImeWindow);
+
+ // And, IME dialogs should always have an higher layer than the IME.
+ assertWindowHigher(mImeDialogWindow, mImeWindow);
+ }
+
+ @Test
+ public void testAssignWindowLayers_ForImeWithAppTargetWithChildWindows() throws Exception {
+ final WindowState imeAppTarget = createWindow("imeAppTarget");
+ final WindowState imeAppTargetChildAboveWindow = createWindow(imeAppTarget,
+ TYPE_APPLICATION_ATTACHED_DIALOG, imeAppTarget.mToken,
+ "imeAppTargetChildAboveWindow");
+ final WindowState imeAppTargetChildBelowWindow = createWindow(imeAppTarget,
+ TYPE_APPLICATION_MEDIA_OVERLAY, imeAppTarget.mToken,
+ "imeAppTargetChildBelowWindow");
+
+ sWm.mInputMethodTarget = imeAppTarget;
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ // Ime should be above all app windows except for child windows that are z-ordered above it
+ // and below system windows if it is targeting an app window.
+ assertWindowHigher(mImeWindow, imeAppTarget);
+ assertWindowHigher(imeAppTargetChildAboveWindow, mImeWindow);
+ assertWindowHigher(mImeWindow, mChildAppWindowAbove);
+ assertWindowHigher(mImeWindow, mAppWindow);
+ assertWindowHigher(mNavBarWindow, mImeWindow);
+ assertWindowHigher(mStatusBarWindow, mImeWindow);
+
+ // And, IME dialogs should always have an higher layer than the IME.
+ assertWindowHigher(mImeDialogWindow, mImeWindow);
+ }
+
+ @Test
+ public void testAssignWindowLayers_ForImeWithAppTargetAndAppAbove() throws Exception {
+ final WindowState appBelowImeTarget = createWindow("appBelowImeTarget");
+ final WindowState imeAppTarget = createWindow("imeAppTarget");
+ final WindowState appAboveImeTarget = createWindow("appAboveImeTarget");
+
+ sWm.mInputMethodTarget = imeAppTarget;
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ // Ime should be above all app windows except for non-fullscreen app window above it and
+ // below system windows if it is targeting an app window.
+ assertWindowHigher(mImeWindow, imeAppTarget);
+ assertWindowHigher(mImeWindow, appBelowImeTarget);
+ assertWindowHigher(appAboveImeTarget, mImeWindow);
+ assertWindowHigher(mImeWindow, mChildAppWindowAbove);
+ assertWindowHigher(mImeWindow, mAppWindow);
+ assertWindowHigher(mNavBarWindow, mImeWindow);
+ assertWindowHigher(mStatusBarWindow, mImeWindow);
+
+ // And, IME dialogs should always have an higher layer than the IME.
+ assertWindowHigher(mImeDialogWindow, mImeWindow);
+ }
+
+ @Test
+ public void testAssignWindowLayers_ForImeNonAppImeTarget() throws Exception {
+ final WindowState imeSystemOverlayTarget = createWindow(null, TYPE_SYSTEM_OVERLAY,
+ mDisplayContent, "imeSystemOverlayTarget",
+ true /* ownerCanAddInternalSystemWindow */);
+
+ sWm.mInputMethodTarget = imeSystemOverlayTarget;
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ // The IME target base layer is higher than all window except for the nav bar window, so the
+ // IME should be above all windows except for the nav bar.
+ assertWindowHigher(mImeWindow, imeSystemOverlayTarget);
+ assertWindowHigher(mImeWindow, mChildAppWindowAbove);
+ assertWindowHigher(mImeWindow, mAppWindow);
+ assertWindowHigher(mImeWindow, mDockedDividerWindow);
+
+ // The IME has a higher base layer than the status bar so we may expect it to go
+ // above the status bar once they are both in the Non-App layer, as past versions of this
+ // test enforced. However this seems like the wrong behavior unless the status bar is the
+ // IME target.
+ assertWindowHigher(mNavBarWindow, mImeWindow);
+ assertWindowHigher(mStatusBarWindow, mImeWindow);
+
+ // And, IME dialogs should always have an higher layer than the IME.
+ assertWindowHigher(mImeDialogWindow, mImeWindow);
+ }
+
+ @Test
+ public void testAssignWindowLayers_ForStatusBarImeTarget() throws Exception {
+ sWm.mInputMethodTarget = mStatusBarWindow;
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ assertWindowHigher(mImeWindow, mChildAppWindowAbove);
+ assertWindowHigher(mImeWindow, mAppWindow);
+ assertWindowHigher(mImeWindow, mDockedDividerWindow);
+ assertWindowHigher(mImeWindow, mStatusBarWindow);
+
+ // And, IME dialogs should always have an higher layer than the IME.
+ assertWindowHigher(mImeDialogWindow, mImeWindow);
+ }
+
+ @Test
+ public void testStackLayers() throws Exception {
+ final WindowState anyWindow1 = createWindow("anyWindow");
+ final WindowState pinnedStackWindow = createWindowOnStack(null, WINDOWING_MODE_PINNED,
+ ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION, mDisplayContent,
+ "pinnedStackWindow");
+ final WindowState dockedStackWindow = createWindowOnStack(null,
+ WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION,
+ mDisplayContent, "dockedStackWindow");
+ final WindowState assistantStackWindow = createWindowOnStack(null, WINDOWING_MODE_FULLSCREEN,
+ ACTIVITY_TYPE_ASSISTANT, TYPE_BASE_APPLICATION,
+ mDisplayContent, "assistantStackWindow");
+ final WindowState homeActivityWindow = createWindowOnStack(null, WINDOWING_MODE_FULLSCREEN,
+ ACTIVITY_TYPE_HOME, TYPE_BASE_APPLICATION,
+ mDisplayContent, "homeActivityWindow");
+ final WindowState anyWindow2 = createWindow("anyWindow2");
+
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ assertWindowHigher(dockedStackWindow, homeActivityWindow);
+ assertWindowHigher(assistantStackWindow, homeActivityWindow);
+ assertWindowHigher(pinnedStackWindow, homeActivityWindow);
+ assertWindowHigher(anyWindow1, homeActivityWindow);
+ assertWindowHigher(anyWindow2, homeActivityWindow);
+ assertWindowHigher(pinnedStackWindow, anyWindow1);
+ assertWindowHigher(pinnedStackWindow, anyWindow2);
+ assertWindowHigher(pinnedStackWindow, dockedStackWindow);
+ assertWindowHigher(pinnedStackWindow, assistantStackWindow);
+ }
+
+ @Test
+ public void testAssignWindowLayers_ForSysUiPanels() throws Exception {
+ final WindowState navBarPanel =
+ createWindow(null, TYPE_NAVIGATION_BAR_PANEL, mDisplayContent, "NavBarPanel");
+ final WindowState statusBarPanel =
+ createWindow(null, TYPE_STATUS_BAR_PANEL, mDisplayContent, "StatusBarPanel");
+ final WindowState statusBarSubPanel =
+ createWindow(null, TYPE_STATUS_BAR_SUB_PANEL, mDisplayContent, "StatusBarSubPanel");
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ // Ime should be above all app windows and below system windows if it is targeting an app
+ // window.
+ assertWindowHigher(navBarPanel, mNavBarWindow);
+ assertWindowHigher(statusBarPanel, mStatusBarWindow);
+ assertWindowHigher(statusBarSubPanel, statusBarPanel);
+ }
+
+ @Test
+ public void testAssignWindowLayers_ForNegativelyZOrderedSubtype() throws Exception {
+ // TODO(b/70040778): We should aim to eliminate the last user of TYPE_APPLICATION_MEDIA
+ // then we can drop all negative layering on the windowing side.
+
+ final WindowState anyWindow = createWindow("anyWindow");
+ final WindowState child = createWindow(anyWindow, TYPE_APPLICATION_MEDIA, mDisplayContent,
+ "TypeApplicationMediaChild");
+ final WindowState mediaOverlayChild = createWindow(anyWindow, TYPE_APPLICATION_MEDIA_OVERLAY,
+ mDisplayContent, "TypeApplicationMediaOverlayChild");
+
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ assertWindowHigher(anyWindow, mediaOverlayChild);
+ assertWindowHigher(mediaOverlayChild, child);
+ }
+
+ @Test
+ public void testDockedDividerPosition() throws Exception {
+ final WindowState pinnedStackWindow = createWindowOnStack(null, WINDOWING_MODE_PINNED,
+ ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION, mDisplayContent,
+ "pinnedStackWindow");
+ final WindowState splitScreenWindow = createWindowOnStack(null,
+ WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION,
+ mDisplayContent, "splitScreenWindow");
+ final WindowState splitScreenSecondaryWindow = createWindowOnStack(null,
+ WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_STANDARD,
+ TYPE_BASE_APPLICATION, mDisplayContent, "splitScreenSecondaryWindow");
+ final WindowState assistantStackWindow = createWindowOnStack(null, WINDOWING_MODE_FULLSCREEN,
+ ACTIVITY_TYPE_ASSISTANT, TYPE_BASE_APPLICATION,
+ mDisplayContent, "assistantStackWindow");
+
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ assertWindowHigher(mDockedDividerWindow, splitScreenWindow);
+ assertWindowHigher(mDockedDividerWindow, splitScreenSecondaryWindow);
+ assertWindowHigher(pinnedStackWindow, mDockedDividerWindow);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/utils/CoordinateTransformsTest.java b/services/tests/servicestests/src/com/android/server/wm/utils/CoordinateTransformsTest.java
new file mode 100644
index 0000000..40a10e0
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/utils/CoordinateTransformsTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.utils;
+
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+
+import static com.android.server.wm.utils.CoordinateTransforms.transformPhysicalToLogicalCoordinates;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.*;
+
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.PointF;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ErrorCollector;
+
+public class CoordinateTransformsTest {
+
+ private static final int W = 200;
+ private static final int H = 400;
+
+ private final Matrix mMatrix = new Matrix();
+
+ @Rule
+ public final ErrorCollector mErrorCollector = new ErrorCollector();
+
+ @Before
+ public void setUp() throws Exception {
+ mMatrix.setTranslate(0xdeadbeef, 0xdeadbeef);
+ }
+
+ @Test
+ public void transformPhysicalToLogicalCoordinates_rot0() throws Exception {
+ transformPhysicalToLogicalCoordinates(ROTATION_0, W, H, mMatrix);
+ assertThat(mMatrix, is(Matrix.IDENTITY_MATRIX));
+ }
+
+ @Test
+ public void transformPhysicalToLogicalCoordinates_rot90() throws Exception {
+ transformPhysicalToLogicalCoordinates(ROTATION_90, W, H, mMatrix);
+
+ checkDevicePoint(0, 0).mapsToLogicalPoint(0, W);
+ checkDevicePoint(W, H).mapsToLogicalPoint(H, 0);
+ }
+
+ @Test
+ public void transformPhysicalToLogicalCoordinates_rot180() throws Exception {
+ transformPhysicalToLogicalCoordinates(ROTATION_180, W, H, mMatrix);
+
+ checkDevicePoint(0, 0).mapsToLogicalPoint(W, H);
+ checkDevicePoint(W, H).mapsToLogicalPoint(0, 0);
+ }
+
+ @Test
+ public void transformPhysicalToLogicalCoordinates_rot270() throws Exception {
+ transformPhysicalToLogicalCoordinates(ROTATION_270, W, H, mMatrix);
+
+ checkDevicePoint(0, 0).mapsToLogicalPoint(H, 0);
+ checkDevicePoint(W, H).mapsToLogicalPoint(0, W);
+ }
+
+ private DevicePointAssertable checkDevicePoint(int x, int y) {
+ final Point devicePoint = new Point(x, y);
+ final float[] fs = new float[] {x, y};
+ mMatrix.mapPoints(fs);
+ final PointF transformedPoint = new PointF(fs[0], fs[1]);
+
+ return (expectedX, expectedY) -> {
+ mErrorCollector.checkThat("t(" + devicePoint + ")",
+ transformedPoint, is(new PointF(expectedX, expectedY)));
+ };
+ }
+
+ public interface DevicePointAssertable {
+ void mapsToLogicalPoint(int x, int y);
+ }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/wm/utils/InsetUtilsTest.java b/services/tests/servicestests/src/com/android/server/wm/utils/InsetUtilsTest.java
new file mode 100644
index 0000000..d0f0fe3
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/utils/InsetUtilsTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm.utils;
+
+import static junit.framework.Assert.assertEquals;
+
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Pair;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class InsetUtilsTest {
+
+ @Test
+ public void testAdd() throws Exception {
+ final Rect rect1 = new Rect(10, 20, 30, 40);
+ final Rect rect2 = new Rect(50, 60, 70, 80);
+ InsetUtils.addInsets(rect1, rect2);
+ assertEquals(new Rect(60, 80, 100, 120), rect1);
+ }
+}
+
diff --git a/services/tests/servicestests/src/com/android/server/wm/utils/RotationCacheTest.java b/services/tests/servicestests/src/com/android/server/wm/utils/RotationCacheTest.java
new file mode 100644
index 0000000..6bbc7eb
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/utils/RotationCacheTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.utils;
+
+import static android.util.Pair.create;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Pair;
+
+import com.android.server.wm.utils.RotationCache.RotationDependentComputation;
+
+import org.hamcrest.Matchers;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@FlakyTest(bugId = 74078662)
+@Presubmit
+public class RotationCacheTest {
+
+ private RotationCache<Object, Pair<Object, Integer>> mCache;
+ private boolean mComputationCalled;
+
+ @Before
+ public void setUp() throws Exception {
+ mComputationCalled = false;
+ mCache = new RotationCache<>((o, rot) -> {
+ mComputationCalled = true;
+ return create(o, rot);
+ });
+ }
+
+ @Test
+ public void getOrCompute_computes() throws Exception {
+ assertThat(mCache.getOrCompute("hello", 0), equalTo(create("hello", 0)));
+ assertThat(mCache.getOrCompute("hello", 1), equalTo(create("hello", 1)));
+ assertThat(mCache.getOrCompute("hello", 2), equalTo(create("hello", 2)));
+ assertThat(mCache.getOrCompute("hello", 3), equalTo(create("hello", 3)));
+ }
+
+ @Test
+ public void getOrCompute_sameParam_sameRot_hitsCache() throws Exception {
+ assertNotNull(mCache.getOrCompute("hello", 1));
+
+ mComputationCalled = false;
+ assertThat(mCache.getOrCompute("hello", 1), equalTo(create("hello", 1)));
+ assertThat(mComputationCalled, is(false));
+ }
+
+ @Test
+ public void getOrCompute_sameParam_hitsCache_forAllRots() throws Exception {
+ assertNotNull(mCache.getOrCompute("hello", 3));
+ assertNotNull(mCache.getOrCompute("hello", 2));
+ assertNotNull(mCache.getOrCompute("hello", 1));
+ assertNotNull(mCache.getOrCompute("hello", 0));
+
+ mComputationCalled = false;
+ assertThat(mCache.getOrCompute("hello", 1), equalTo(create("hello", 1)));
+ assertThat(mCache.getOrCompute("hello", 0), equalTo(create("hello", 0)));
+ assertThat(mCache.getOrCompute("hello", 2), equalTo(create("hello", 2)));
+ assertThat(mCache.getOrCompute("hello", 3), equalTo(create("hello", 3)));
+ assertThat(mComputationCalled, is(false));
+ }
+
+ @Test
+ public void getOrCompute_changingParam_recomputes() throws Exception {
+ assertNotNull(mCache.getOrCompute("hello", 1));
+
+ assertThat(mCache.getOrCompute("world", 1), equalTo(create("world", 1)));
+ }
+
+ @Test
+ public void getOrCompute_changingParam_clearsCacheForDifferentRots() throws Exception {
+ assertNotNull(mCache.getOrCompute("hello", 1));
+ assertNotNull(mCache.getOrCompute("world", 2));
+
+ mComputationCalled = false;
+ assertThat(mCache.getOrCompute("hello", 1), equalTo(create("hello", 1)));
+ assertThat(mComputationCalled, is(true));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/utils/WmDisplayCutoutTest.java b/services/tests/servicestests/src/com/android/server/wm/utils/WmDisplayCutoutTest.java
new file mode 100644
index 0000000..f7addf6
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/utils/WmDisplayCutoutTest.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.utils;
+
+
+import static android.view.DisplayCutout.NO_CUTOUT;
+import static android.view.DisplayCutout.fromBoundingRect;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Size;
+import android.view.DisplayCutout;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+/**
+ * Tests for {@link WmDisplayCutout}
+ *
+ * Run with: atest WmDisplayCutoutTest
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class WmDisplayCutoutTest {
+
+ private final DisplayCutout mCutoutTop = new DisplayCutout(
+ new Rect(0, 100, 0, 0),
+ Arrays.asList(new Rect(50, 0, 75, 100)));
+
+ @Test
+ public void calculateRelativeTo_top() {
+ WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(0, 0, 100, 20), 200, 400)
+ .calculateRelativeTo(new Rect(5, 5, 95, 195));
+
+ assertEquals(new Rect(0, 15, 0, 0), cutout.getDisplayCutout().getSafeInsets());
+ }
+
+ @Test
+ public void calculateRelativeTo_left() {
+ WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(0, 0, 20, 100), 400, 200)
+ .calculateRelativeTo(new Rect(5, 5, 195, 95));
+
+ assertEquals(new Rect(15, 0, 0, 0), cutout.getDisplayCutout().getSafeInsets());
+ }
+
+ @Test
+ public void calculateRelativeTo_bottom() {
+ WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(0, 180, 100, 200), 100, 200)
+ .calculateRelativeTo(new Rect(5, 5, 95, 195));
+
+ assertEquals(new Rect(0, 0, 0, 15), cutout.getDisplayCutout().getSafeInsets());
+ }
+
+ @Test
+ public void calculateRelativeTo_right() {
+ WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(180, 0, 200, 100), 200, 100)
+ .calculateRelativeTo(new Rect(5, 5, 195, 95));
+
+ assertEquals(new Rect(0, 0, 15, 0), cutout.getDisplayCutout().getSafeInsets());
+ }
+
+ @Test
+ public void calculateRelativeTo_bounds() {
+ WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(0, 0, 100, 20), 200, 400)
+ .calculateRelativeTo(new Rect(5, 10, 95, 180));
+
+ assertEquals(new Rect(-5, -10, 95, 10), cutout.getDisplayCutout().getBounds().getBounds());
+ }
+
+ @Test
+ public void computeSafeInsets_top() {
+ WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(0, 0, 100, 20), 200, 400);
+
+ assertEquals(new Rect(0, 20, 0, 0), cutout.getDisplayCutout().getSafeInsets());
+ }
+
+ @Test
+ public void computeSafeInsets_left() {
+ WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(0, 0, 20, 100), 400, 200);
+
+ assertEquals(new Rect(20, 0, 0, 0), cutout.getDisplayCutout().getSafeInsets());
+ }
+
+ @Test
+ public void computeSafeInsets_bottom() {
+ WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(0, 180, 100, 200), 100, 200);
+
+ assertEquals(new Rect(0, 0, 0, 20), cutout.getDisplayCutout().getSafeInsets());
+ }
+
+ @Test
+ public void computeSafeInsets_right() {
+ WmDisplayCutout cutout = WmDisplayCutout.computeSafeInsets(
+ fromBoundingRect(180, 0, 200, 100), 200, 100);
+
+ assertEquals(new Rect(0, 0, 20, 0), cutout.getDisplayCutout().getSafeInsets());
+ }
+
+ @Test
+ public void computeSafeInsets_bounds() {
+ DisplayCutout cutout = WmDisplayCutout.computeSafeInsets(mCutoutTop, 1000,
+ 2000).getDisplayCutout();
+
+ assertEquals(mCutoutTop.getBounds().getBounds(),
+ cutout.getBounds().getBounds());
+ }
+
+ @Test
+ public void test_equals() {
+ assertEquals(new WmDisplayCutout(NO_CUTOUT, null), new WmDisplayCutout(NO_CUTOUT, null));
+ assertEquals(new WmDisplayCutout(mCutoutTop, new Size(1, 2)),
+ new WmDisplayCutout(mCutoutTop, new Size(1, 2)));
+
+ assertNotEquals(new WmDisplayCutout(mCutoutTop, new Size(1, 2)),
+ new WmDisplayCutout(mCutoutTop, new Size(5, 6)));
+ assertNotEquals(new WmDisplayCutout(mCutoutTop, new Size(1, 2)),
+ new WmDisplayCutout(NO_CUTOUT, new Size(1, 2)));
+ }
+
+ @Test
+ public void test_hashCode() {
+ assertEquals(new WmDisplayCutout(NO_CUTOUT, null).hashCode(),
+ new WmDisplayCutout(NO_CUTOUT, null).hashCode());
+ assertEquals(new WmDisplayCutout(mCutoutTop, new Size(1, 2)).hashCode(),
+ new WmDisplayCutout(mCutoutTop, new Size(1, 2)).hashCode());
+ }
+}
\ No newline at end of file