blob: 9480ae81aede2193df1b9f37504ae3b4afda102f [file] [log] [blame]
/*
* 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.ActivityTaskManager.INVALID_TASK_ID;
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.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME;
import static android.content.pm.ActivityInfo.FLAG_RELINQUISH_TASK_IDENTITY;
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 android.view.IWindowManager.FIXED_TO_USER_ROTATION_ENABLED;
import static android.view.Surface.ROTATION_0;
import static android.view.Surface.ROTATION_90;
import static android.window.DisplayAreaOrganizer.FEATURE_VENDOR_FIRST;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.server.policy.WindowManagerPolicy.USER_ROTATION_FREE;
import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG;
import static com.android.server.wm.TaskFragment.TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT;
import static com.google.common.truth.Truth.assertThat;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.sameInstance;
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.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.TaskInfo;
import android.app.WindowConfiguration;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.res.Configuration;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
import android.util.DisplayMetrics;
import android.util.TypedXmlPullParser;
import android.util.TypedXmlSerializer;
import android.util.Xml;
import android.view.Display;
import android.view.DisplayInfo;
import androidx.test.filters.MediumTest;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
/**
* Test class for {@link Task}.
*
* Build/Install/Run:
* atest WmTests:TaskTests
*/
@MediumTest
@Presubmit
@RunWith(WindowTestRunner.class)
public class TaskTests extends WindowTestsBase {
private static final String TASK_TAG = "task";
private Rect mParentBounds;
@Before
public void setUp() throws Exception {
mParentBounds = new Rect(10 /*left*/, 30 /*top*/, 80 /*right*/, 60 /*bottom*/);
removeGlobalMinSizeRestriction();
}
@Test
public void testRemoveContainer() {
final Task rootTask = createTask(mDisplayContent);
final Task task = createTaskInRootTask(rootTask, 0 /* userId */);
final ActivityRecord activity = createActivityRecord(mDisplayContent, task);
task.remove(false /* withTransition */, "testRemoveContainer");
// There is still an activity to be destroyed, so the task is not removed immediately.
assertNotNull(task.getParent());
assertTrue(rootTask.hasChild());
assertTrue(task.hasChild());
assertTrue(activity.finishing);
activity.destroyed("testRemoveContainer");
// Assert that the container was removed after the activity is destroyed.
assertNull(task.getParent());
assertEquals(0, task.getChildCount());
assertNull(activity.getParent());
verify(mAtm.getLockTaskController(), atLeast(1)).clearLockedTask(task);
verify(mAtm.getLockTaskController(), atLeast(1)).clearLockedTask(rootTask);
}
@Test
public void testRemoveContainer_multipleNestedTasks() {
final Task rootTask = createTask(mDisplayContent);
rootTask.mCreatedByOrganizer = true;
final Task task1 = new TaskBuilder(mSupervisor).setParentTaskFragment(rootTask).build();
final Task task2 = new TaskBuilder(mSupervisor).setParentTaskFragment(rootTask).build();
final ActivityRecord activity1 = createActivityRecord(task1);
final ActivityRecord activity2 = createActivityRecord(task2);
activity1.setVisible(false);
// All activities under the root task should be finishing.
rootTask.remove(true /* withTransition */, "test");
assertTrue(activity1.finishing);
assertTrue(activity2.finishing);
// After all activities activities are destroyed, the root task should also be removed.
activity1.removeImmediately();
activity2.removeImmediately();
assertFalse(rootTask.isAttached());
}
@Test
public void testRemoveContainer_deferRemoval() {
final Task rootTask = createTask(mDisplayContent);
final Task task = createTaskInRootTask(rootTask, 0 /* userId */);
final ActivityRecord activity = createActivityRecord(mDisplayContent, task);
doReturn(true).when(task).shouldDeferRemoval();
task.removeIfPossible();
// For the case of deferred removal the task will still be connected to the its app token
// until the task window container is removed.
assertNotNull(task.getParent());
assertNotEquals(0, task.getChildCount());
assertNotNull(activity.getParent());
task.removeImmediately();
assertNull(task.getParent());
assertEquals(0, task.getChildCount());
assertNull(activity.getParent());
}
@Test
public void testReparent() {
final Task taskController1 = createTask(mDisplayContent);
final Task task = createTaskInRootTask(taskController1, 0 /* userId */);
final Task taskController2 = createTask(mDisplayContent);
final Task task2 = createTaskInRootTask(taskController2, 0 /* userId */);
boolean gotException = false;
try {
task.reparent(taskController1, 0, false/* moveParents */, "testReparent");
} catch (IllegalArgumentException e) {
gotException = true;
}
assertTrue("Should not be able to reparent to the same parent", gotException);
gotException = false;
try {
task.reparent(null, 0, false/* moveParents */, "testReparent");
} catch (Exception e) {
gotException = true;
}
assertTrue("Should not be able to reparent to a task that doesn't exist", gotException);
task.reparent(taskController2, 0, false/* moveParents */, "testReparent");
assertEquals(taskController2, task.getParent());
assertEquals(0, task.getParent().mChildren.indexOf(task));
assertEquals(1, task2.getParent().mChildren.indexOf(task2));
}
@Test
public void testReparent_BetweenDisplays() {
// Create first task on primary display.
final Task rootTask1 = createTask(mDisplayContent);
final Task task = createTaskInRootTask(rootTask1, 0 /* userId */);
assertEquals(mDisplayContent, rootTask1.getDisplayContent());
// Create second display and put second task on it.
final DisplayContent dc = createNewDisplay();
final Task rootTask2 = createTask(dc);
final Task task2 = createTaskInRootTask(rootTask2, 0 /* userId */);
// Reparent and check state
clearInvocations(task); // reset the number of onDisplayChanged for task.
task.reparent(rootTask2, 0, false /* moveParents */, "testReparent_BetweenDisplays");
assertEquals(rootTask2, task.getParent());
assertEquals(0, task.getParent().mChildren.indexOf(task));
assertEquals(1, task2.getParent().mChildren.indexOf(task2));
verify(task, times(1)).onDisplayChanged(any());
}
@Test
public void testBounds() {
final Task rootTask1 = createTask(mDisplayContent);
final Task task = createTaskInRootTask(rootTask1, 0 /* userId */);
// Check that setting bounds also updates surface position
task.setWindowingMode(WINDOWING_MODE_FREEFORM);
Rect bounds = new Rect(10, 10, 100, 200);
task.setBounds(bounds);
assertEquals(new Point(bounds.left, bounds.top), task.getLastSurfacePosition());
}
@Test
public void testIsInTask() {
final Task task1 = createTask(mDisplayContent);
final Task task2 = createTask(mDisplayContent);
final ActivityRecord activity1 = createActivityRecord(mDisplayContent, task1);
final ActivityRecord activity2 = createActivityRecord(mDisplayContent, task2);
assertEquals(activity1, task1.isInTask(activity1));
assertNull(task1.isInTask(activity2));
}
@Test
public void testPerformClearTop() {
final Task task = createTask(mDisplayContent);
final ActivityRecord activity1 = new ActivityBuilder(mAtm).setTask(task).build();
final ActivityRecord activity2 = new ActivityBuilder(mAtm).setTask(task).build();
// Detach from process so the activities can be removed from hierarchy when finishing.
activity1.detachFromProcess();
activity2.detachFromProcess();
int[] finishCount = new int[1];
assertTrue(task.performClearTop(activity1, 0 /* launchFlags */, finishCount).finishing);
assertFalse(task.hasChild());
// In real case, the task should be preserved for adding new activity.
assertTrue(task.isAttached());
final ActivityRecord activityA = new ActivityBuilder(mAtm).setTask(task).build();
final ActivityRecord activityB = new ActivityBuilder(mAtm).setTask(task).build();
final ActivityRecord activityC = new ActivityBuilder(mAtm).setTask(task).build();
activityA.setState(ActivityRecord.State.STOPPED, "test");
activityB.setState(ActivityRecord.State.PAUSED, "test");
activityC.setState(ActivityRecord.State.RESUMED, "test");
doReturn(true).when(activityB).shouldBeVisibleUnchecked();
doReturn(true).when(activityC).shouldBeVisibleUnchecked();
activityA.getConfiguration().densityDpi += 100;
assertTrue(task.performClearTop(activityA, 0 /* launchFlags */, finishCount).finishing);
// The bottom activity should destroy directly without relaunch for config change.
assertEquals(ActivityRecord.State.DESTROYING, activityA.getState());
verify(activityA, never()).startRelaunching();
}
@Test
public void testRemoveChildForOverlayTask() {
final Task task = createTask(mDisplayContent);
final int taskId = task.mTaskId;
final ActivityRecord activity1 = createActivityRecord(mDisplayContent, task);
final ActivityRecord activity2 = createActivityRecord(mDisplayContent, task);
final ActivityRecord activity3 = createActivityRecord(mDisplayContent, task);
activity1.setTaskOverlay(true);
activity2.setTaskOverlay(true);
activity3.setTaskOverlay(true);
assertEquals(3, task.getChildCount());
assertTrue(task.onlyHasTaskOverlayActivities(true));
task.removeChild(activity1);
verify(task.mTaskSupervisor).removeTask(any(), anyBoolean(), anyBoolean(), anyString());
assertEquals(2, task.getChildCount());
task.forAllActivities((r) -> {
assertTrue(r.finishing);
});
}
@Test
public void testSwitchUser() {
final Task rootTask = createTask(mDisplayContent);
final Task childTask = createTaskInRootTask(rootTask, 0 /* userId */);
final Task leafTask1 = createTaskInRootTask(childTask, 10 /* userId */);
final Task leafTask2 = createTaskInRootTask(childTask, 0 /* userId */);
assertEquals(1, rootTask.getChildCount());
assertEquals(leafTask2, childTask.getTopChild());
doReturn(true).when(leafTask1).showToCurrentUser();
rootTask.switchUser(10);
assertEquals(1, rootTask.getChildCount());
assertEquals(leafTask1, childTask.getTopChild());
}
@Test
public void testEnsureActivitiesVisible() {
final Task rootTask = createTask(mDisplayContent);
final Task leafTask1 = createTaskInRootTask(rootTask, 0 /* userId */);
final Task leafTask2 = createTaskInRootTask(rootTask, 0 /* userId */);
final ActivityRecord activity1 = createActivityRecord(mDisplayContent, leafTask1);
final ActivityRecord activity2 = createActivityRecord(mDisplayContent, leafTask2);
// Check visibility of occluded tasks
doReturn(false).when(leafTask1).shouldBeVisible(any());
doReturn(true).when(leafTask2).shouldBeVisible(any());
rootTask.ensureActivitiesVisible(
null /* starting */ , 0 /* configChanges */, false /* preserveWindows */);
assertFalse(activity1.isVisible());
assertTrue(activity2.isVisible());
// Check visibility of not occluded tasks
doReturn(true).when(leafTask1).shouldBeVisible(any());
doReturn(true).when(leafTask2).shouldBeVisible(any());
rootTask.ensureActivitiesVisible(
null /* starting */ , 0 /* configChanges */, false /* preserveWindows */);
assertTrue(activity1.isVisible());
assertTrue(activity2.isVisible());
}
@Test
public void testResolveNonResizableTaskWindowingMode() {
// Test with no support non-resizable in multi window regardless the screen size.
mAtm.mSupportsNonResizableMultiWindow = -1;
final Task task = createTask(mDisplayContent);
Configuration parentConfig = task.getParent().getConfiguration();
parentConfig.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
doReturn(false).when(task).isResizeable();
WindowConfiguration requestedOverride =
task.getRequestedOverrideConfiguration().windowConfiguration;
WindowConfiguration resolvedOverride =
task.getResolvedOverrideConfiguration().windowConfiguration;
// The resolved override windowing mode of a non-resizeable task should be resolved as
// fullscreen even as a child of a freeform display.
requestedOverride.setWindowingMode(WINDOWING_MODE_UNDEFINED);
task.resolveOverrideConfiguration(parentConfig);
assertThat(resolvedOverride.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN);
// The resolved override windowing mode of a non-resizeable task should be resolved as
// fullscreen, even when requested as freeform windowing mode
requestedOverride.setWindowingMode(WINDOWING_MODE_FREEFORM);
task.resolveOverrideConfiguration(parentConfig);
assertThat(resolvedOverride.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN);
// The resolved override windowing mode of a non-resizeable task can be undefined as long
// as its parents is not in multi-window mode.
parentConfig.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
requestedOverride.setWindowingMode(WINDOWING_MODE_UNDEFINED);
task.resolveOverrideConfiguration(parentConfig);
assertThat(resolvedOverride.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED);
}
@Test
public void testHandlesOrientationChangeFromDescendant() {
final Task rootTask = createTask(mDisplayContent,
WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD);
final Task leafTask1 = createTaskInRootTask(rootTask, 0 /* userId */);
final Task leafTask2 = createTaskInRootTask(rootTask, 0 /* userId */);
leafTask1.getWindowConfiguration().setActivityType(ACTIVITY_TYPE_HOME);
leafTask2.getWindowConfiguration().setActivityType(ACTIVITY_TYPE_STANDARD);
assertEquals(leafTask2, rootTask.getTopChild());
assertTrue(rootTask.handlesOrientationChangeFromDescendant());
// Treat orientation request from home as handled.
assertTrue(leafTask1.handlesOrientationChangeFromDescendant());
// Orientation request from standard activity in multi window will not be handled.
assertFalse(leafTask2.handlesOrientationChangeFromDescendant());
}
@Test
public void testAlwaysOnTop() {
final Task task = createTask(mDisplayContent);
task.setAlwaysOnTop(true);
task.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
assertTrue(task.isAlwaysOnTop());
task.setForceHidden(FLAG_FORCE_HIDDEN_FOR_TASK_ORG, true /* set */);
assertFalse(task.isAlwaysOnTop());
}
@Test
public void testRestoreWindowedTask() throws Exception {
final Task expected = createTask(64);
expected.mLastNonFullscreenBounds = new Rect(50, 50, 100, 100);
final byte[] serializedBytes = serializeToBytes(expected);
final Task actual = restoreFromBytes(serializedBytes);
assertEquals(expected.mTaskId, actual.mTaskId);
assertEquals(expected.mLastNonFullscreenBounds, actual.mLastNonFullscreenBounds);
}
/** Ensure we have no chance to modify the original intent. */
@Test
public void testCopyBaseIntentForTaskInfo() {
final Task task = createTask(1);
task.setTaskDescription(new ActivityManager.TaskDescription());
final TaskInfo info = task.getTaskInfo();
// The intent of info should be a copy so assert that they are different instances.
Assert.assertThat(info.baseIntent, not(sameInstance(task.getBaseIntent())));
}
@Test
public void testReturnsToHomeRootTask() throws Exception {
final Task task = createTask(1);
spyOn(task);
doReturn(true).when(task).hasChild();
assertFalse(task.returnsToHomeRootTask());
task.intent = null;
assertFalse(task.returnsToHomeRootTask());
task.intent = new Intent();
assertFalse(task.returnsToHomeRootTask());
task.intent.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_TASK_ON_HOME);
assertTrue(task.returnsToHomeRootTask());
}
/** Ensures that empty bounds cause appBounds to inherit from parent. */
@Test
public void testAppBounds_EmptyBounds() {
final Rect emptyBounds = new Rect();
testRootTaskBoundsConfiguration(WINDOWING_MODE_FULLSCREEN, mParentBounds, emptyBounds,
mParentBounds);
}
/** Ensures that bounds on freeform root tasks are not clipped. */
@Test
public void testAppBounds_FreeFormBounds() {
final Rect freeFormBounds = new Rect(mParentBounds);
freeFormBounds.offset(10, 10);
testRootTaskBoundsConfiguration(WINDOWING_MODE_FREEFORM, mParentBounds, freeFormBounds,
freeFormBounds);
}
/** Ensures that fully contained bounds are not clipped. */
@Test
public void testAppBounds_ContainedBounds() {
final Rect insetBounds = new Rect(mParentBounds);
insetBounds.inset(5, 5, 5, 5);
testRootTaskBoundsConfiguration(
WINDOWING_MODE_FREEFORM, mParentBounds, insetBounds, insetBounds);
}
@Test
public void testFitWithinBounds() {
final Rect parentBounds = new Rect(10, 10, 200, 200);
TaskDisplayArea taskDisplayArea = mAtm.mRootWindowContainer.getDefaultTaskDisplayArea();
Task rootTask = taskDisplayArea.createRootTask(WINDOWING_MODE_FREEFORM,
ACTIVITY_TYPE_STANDARD, true /* onTop */);
Task task = new TaskBuilder(mSupervisor).setParentTaskFragment(rootTask).build();
final Configuration parentConfig = rootTask.getConfiguration();
parentConfig.windowConfiguration.setBounds(parentBounds);
parentConfig.densityDpi = DisplayMetrics.DENSITY_DEFAULT;
// check top and left
Rect reqBounds = new Rect(-190, -190, 0, 0);
task.setBounds(reqBounds);
// Make sure part of it is exposed
assertTrue(task.getBounds().right > parentBounds.left);
assertTrue(task.getBounds().bottom > parentBounds.top);
// Should still be more-or-less in that corner
assertTrue(task.getBounds().left <= parentBounds.left);
assertTrue(task.getBounds().top <= parentBounds.top);
assertEquals(reqBounds.width(), task.getBounds().width());
assertEquals(reqBounds.height(), task.getBounds().height());
// check bottom and right
reqBounds = new Rect(210, 210, 400, 400);
task.setBounds(reqBounds);
// Make sure part of it is exposed
assertTrue(task.getBounds().left < parentBounds.right);
assertTrue(task.getBounds().top < parentBounds.bottom);
// Should still be more-or-less in that corner
assertTrue(task.getBounds().right >= parentBounds.right);
assertTrue(task.getBounds().bottom >= parentBounds.bottom);
assertEquals(reqBounds.width(), task.getBounds().width());
assertEquals(reqBounds.height(), task.getBounds().height());
}
/**
* Tests that a task with forced orientation has orientation-consistent bounds within the
* parent.
*/
@Test
public void testFullscreenBoundsForcedOrientation() {
final Rect fullScreenBounds = new Rect(0, 0, 1920, 1080);
final Rect fullScreenBoundsPort = new Rect(0, 0, 1080, 1920);
final DisplayContent display = new TestDisplayContent.Builder(mAtm,
fullScreenBounds.width(), fullScreenBounds.height()).setCanRotate(false).build();
assertNotNull(mRootWindowContainer.getDisplayContent(display.mDisplayId));
// Fix the display orientation to landscape which is the natural rotation (0) for the test
// display.
final DisplayRotation dr = display.mDisplayContent.getDisplayRotation();
dr.setFixedToUserRotation(FIXED_TO_USER_ROTATION_ENABLED);
dr.setUserRotation(USER_ROTATION_FREE, ROTATION_0);
final Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
.setWindowingMode(WINDOWING_MODE_FULLSCREEN).setDisplay(display).build();
final Task task = rootTask.getBottomMostTask();
final ActivityRecord root = task.getTopNonFinishingActivity();
assertEquals(fullScreenBounds, task.getBounds());
// Setting app to fixed portrait fits within parent
root.setRequestedOrientation(SCREEN_ORIENTATION_PORTRAIT);
assertEquals(root, task.getRootActivity());
assertEquals(SCREEN_ORIENTATION_PORTRAIT, task.getRootActivity().getOrientation());
// Portrait orientation is enforced on activity level. Task should fill fullscreen bounds.
assertThat(task.getBounds().height()).isLessThan(task.getBounds().width());
assertEquals(fullScreenBounds, task.getBounds());
// Top activity gets used
final ActivityRecord top = new ActivityBuilder(mAtm).setTask(task).setParentTask(rootTask)
.build();
assertEquals(top, task.getTopNonFinishingActivity());
top.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE);
assertThat(task.getBounds().width()).isGreaterThan(task.getBounds().height());
assertEquals(task.getBounds().width(), fullScreenBounds.width());
// Setting app to unspecified restores
top.setRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED);
assertEquals(fullScreenBounds, task.getBounds());
// Setting app to fixed landscape and changing display
top.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE);
// Fix the display orientation to portrait which is 90 degrees for the test display.
dr.setUserRotation(USER_ROTATION_FREE, ROTATION_90);
// Fixed orientation request should be resolved on activity level. Task fills display
// bounds.
assertThat(task.getBounds().height()).isGreaterThan(task.getBounds().width());
assertThat(top.getBounds().width()).isGreaterThan(top.getBounds().height());
assertEquals(fullScreenBoundsPort, task.getBounds());
// in FREEFORM, no constraint
final Rect freeformBounds = new Rect(display.getBounds());
freeformBounds.inset((int) (freeformBounds.width() * 0.2),
(int) (freeformBounds.height() * 0.2));
rootTask.setWindowingMode(WINDOWING_MODE_FREEFORM);
task.setBounds(freeformBounds);
assertEquals(freeformBounds, task.getBounds());
// FULLSCREEN letterboxes bounds on activity level, no constraint on task level.
rootTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
rootTask.setBounds(null);
assertThat(task.getBounds().height()).isGreaterThan(task.getBounds().width());
assertThat(top.getBounds().width()).isGreaterThan(top.getBounds().height());
assertEquals(fullScreenBoundsPort, task.getBounds());
// FREEFORM restores bounds as before
rootTask.setWindowingMode(WINDOWING_MODE_FREEFORM);
assertEquals(freeformBounds, task.getBounds());
}
@Test
public void testReportsOrientationRequestInLetterboxForOrientation() {
final Rect fullScreenBounds = new Rect(0, 0, 1920, 1080);
final Rect fullScreenBoundsPort = new Rect(0, 0, 1080, 1920);
final DisplayContent display = new TestDisplayContent.Builder(mAtm,
fullScreenBounds.width(), fullScreenBounds.height()).setCanRotate(false).build();
assertNotNull(mRootWindowContainer.getDisplayContent(display.mDisplayId));
// Fix the display orientation to landscape which is the natural rotation (0) for the test
// display.
final DisplayRotation dr = display.mDisplayContent.getDisplayRotation();
dr.setFixedToUserRotation(FIXED_TO_USER_ROTATION_ENABLED);
dr.setUserRotation(USER_ROTATION_FREE, ROTATION_0);
final Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
.setWindowingMode(WINDOWING_MODE_FULLSCREEN).setDisplay(display).build();
final Task task = rootTask.getBottomMostTask();
ActivityRecord root = task.getTopNonFinishingActivity();
assertEquals(fullScreenBounds, task.getBounds());
// Setting app to fixed portrait fits within parent on activity level. Task fills parent.
root.setRequestedOrientation(SCREEN_ORIENTATION_PORTRAIT);
assertThat(root.getBounds().width()).isLessThan(root.getBounds().height());
assertEquals(task.getBounds(), fullScreenBounds);
assertEquals(SCREEN_ORIENTATION_PORTRAIT, task.getOrientation());
}
@Test
public void testIgnoresForcedOrientationWhenParentHandles() {
final Rect fullScreenBounds = new Rect(0, 0, 1920, 1080);
DisplayContent display = new TestDisplayContent.Builder(
mAtm, fullScreenBounds.width(), fullScreenBounds.height()).build();
display.getRequestedOverrideConfiguration().orientation =
Configuration.ORIENTATION_LANDSCAPE;
display.onRequestedOverrideConfigurationChanged(
display.getRequestedOverrideConfiguration());
Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
.setWindowingMode(WINDOWING_MODE_FULLSCREEN).setDisplay(display).build();
Task task = rootTask.getBottomMostTask();
ActivityRecord root = task.getTopNonFinishingActivity();
final WindowContainer parentWindowContainer =
new WindowContainer(mSystemServicesTestRule.getWindowManagerService());
spyOn(parentWindowContainer);
parentWindowContainer.setBounds(fullScreenBounds);
doReturn(parentWindowContainer).when(task).getParent();
doReturn(display.getDefaultTaskDisplayArea()).when(task).getDisplayArea();
doReturn(rootTask).when(task).getRootTask();
doReturn(true).when(parentWindowContainer).handlesOrientationChangeFromDescendant();
// Setting app to fixed portrait fits within parent, but Task shouldn't adjust the
// bounds because its parent says it will handle it at a later time.
root.setRequestedOrientation(SCREEN_ORIENTATION_PORTRAIT);
assertEquals(root, task.getRootActivity());
assertEquals(SCREEN_ORIENTATION_PORTRAIT, task.getRootActivity().getOrientation());
assertEquals(fullScreenBounds, task.getBounds());
}
@Test
public void testComputeConfigResourceOverrides() {
final Rect fullScreenBounds = new Rect(0, 0, 1080, 1920);
TestDisplayContent display = new TestDisplayContent.Builder(
mAtm, fullScreenBounds.width(), fullScreenBounds.height()).build();
final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build();
final Configuration inOutConfig = new Configuration();
final Configuration parentConfig = new Configuration();
final Rect parentBounds = new Rect(0, 0, 250, 500);
final Rect parentAppBounds = new Rect(0, 0, 250, 480);
parentConfig.windowConfiguration.setBounds(parentBounds);
parentConfig.windowConfiguration.setAppBounds(parentAppBounds);
parentConfig.densityDpi = 400;
parentConfig.screenHeightDp = (parentBounds.bottom * 160) / parentConfig.densityDpi; // 200
parentConfig.screenWidthDp = (parentBounds.right * 160) / parentConfig.densityDpi; // 100
parentConfig.windowConfiguration.setRotation(ROTATION_0);
// By default, the input bounds will fill parent.
task.computeConfigResourceOverrides(inOutConfig, parentConfig);
assertEquals(parentConfig.screenHeightDp, inOutConfig.screenHeightDp);
assertEquals(parentConfig.screenWidthDp, inOutConfig.screenWidthDp);
assertEquals(parentAppBounds, inOutConfig.windowConfiguration.getAppBounds());
assertEquals(Configuration.ORIENTATION_PORTRAIT, inOutConfig.orientation);
// If bounds are overridden, config properties should be made to match. Surface hierarchy
// will crop for policy.
inOutConfig.setToDefaults();
final int longSide = 960;
final int shortSide = 540;
parentConfig.densityDpi = 192;
final Rect largerPortraitBounds = new Rect(0, 0, shortSide, longSide);
inOutConfig.windowConfiguration.setBounds(largerPortraitBounds);
task.computeConfigResourceOverrides(inOutConfig, parentConfig);
// The override bounds are beyond the parent, the out appBounds should not be intersected
// by parent appBounds.
assertEquals(largerPortraitBounds, inOutConfig.windowConfiguration.getAppBounds());
assertEquals(800, inOutConfig.screenHeightDp); // 960/(192/160) = 800
assertEquals(450, inOutConfig.screenWidthDp); // 540/(192/160) = 450
inOutConfig.setToDefaults();
// Landscape bounds.
final Rect largerLandscapeBounds = new Rect(0, 0, longSide, shortSide);
inOutConfig.windowConfiguration.setBounds(largerLandscapeBounds);
// Setup the display with a top stable inset. The later assertion will ensure the inset is
// excluded from screenHeightDp.
final int statusBarHeight = 100;
final DisplayInfo di = display.getDisplayInfo();
display.getDisplayPolicy().getDecorInsetsInfo(di.rotation,
di.logicalWidth, di.logicalHeight).mConfigInsets.top = statusBarHeight;
// Without limiting to be inside the parent bounds, the out screen size should keep relative
// to the input bounds.
final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task).build();
final ActivityRecord.CompatDisplayInsets compatInsets =
new ActivityRecord.CompatDisplayInsets(
display, activity, /* fixedOrientationBounds= */ null);
task.computeConfigResourceOverrides(inOutConfig, parentConfig, compatInsets);
assertEquals(largerLandscapeBounds, inOutConfig.windowConfiguration.getAppBounds());
final float density = parentConfig.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
final int expectedHeightDp = (int) ((shortSide - statusBarHeight) / density + 0.5f);
assertEquals(expectedHeightDp, inOutConfig.screenHeightDp);
final int expectedWidthDp = (int) (longSide / density + 0.5f);
assertEquals(expectedWidthDp, inOutConfig.screenWidthDp);
assertEquals(Configuration.ORIENTATION_LANDSCAPE, inOutConfig.orientation);
}
@Test
public void testComputeConfigResourceLayoutOverrides() {
final Rect fullScreenBounds = new Rect(0, 0, 1000, 2500);
TestDisplayContent display = new TestDisplayContent.Builder(
mAtm, fullScreenBounds.width(), fullScreenBounds.height()).build();
final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build();
final Configuration inOutConfig = new Configuration();
final Configuration parentConfig = new Configuration();
final Rect nonLongBounds = new Rect(0, 0, 1000, 1250);
parentConfig.windowConfiguration.setBounds(fullScreenBounds);
parentConfig.windowConfiguration.setAppBounds(fullScreenBounds);
parentConfig.densityDpi = 400;
parentConfig.screenHeightDp = (fullScreenBounds.bottom * 160) / parentConfig.densityDpi;
parentConfig.screenWidthDp = (fullScreenBounds.right * 160) / parentConfig.densityDpi;
parentConfig.windowConfiguration.setRotation(ROTATION_0);
// Set BOTH screenW/H to an override value
inOutConfig.screenWidthDp = nonLongBounds.width() * 160 / parentConfig.densityDpi;
inOutConfig.screenHeightDp = nonLongBounds.height() * 160 / parentConfig.densityDpi;
task.computeConfigResourceOverrides(inOutConfig, parentConfig);
// screenLayout should honor override when both screenW/H are set.
assertTrue((inOutConfig.screenLayout & Configuration.SCREENLAYOUT_LONG_NO) != 0);
}
@Test
public void testComputeNestedConfigResourceOverrides() {
final Task task = new TaskBuilder(mSupervisor).build();
assertTrue(task.getResolvedOverrideBounds().isEmpty());
int origScreenH = task.getConfiguration().screenHeightDp;
Configuration rootTaskConfig = new Configuration();
rootTaskConfig.setTo(task.getRootTask().getRequestedOverrideConfiguration());
rootTaskConfig.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
// Set bounds on root task (not task) and verify that the task resource configuration
// changes despite it's override bounds being empty.
Rect bounds = new Rect(task.getRootTask().getBounds());
bounds.bottom = (int) (bounds.bottom * 0.6f);
rootTaskConfig.windowConfiguration.setBounds(bounds);
task.getRootTask().onRequestedOverrideConfigurationChanged(rootTaskConfig);
assertNotEquals(origScreenH, task.getConfiguration().screenHeightDp);
}
@Test
public void testFullScreenTaskNotAdjustedByMinimalSize() {
final Task fullscreenTask = new TaskBuilder(mSupervisor).build();
final Rect originalTaskBounds = new Rect(fullscreenTask.getBounds());
final ActivityInfo aInfo = new ActivityInfo();
aInfo.windowLayout = new ActivityInfo.WindowLayout(0 /* width */, 0 /* widthFraction */,
0 /* height */, 0 /* heightFraction */, 0 /* gravity */,
originalTaskBounds.width() * 2 /* minWidth */,
originalTaskBounds.height() * 2 /* minHeight */);
fullscreenTask.setMinDimensions(aInfo);
fullscreenTask.onConfigurationChanged(fullscreenTask.getParent().getConfiguration());
assertEquals(originalTaskBounds, fullscreenTask.getBounds());
}
@Test
public void testInsetDisregardedWhenFreeformOverlapsNavBar() {
TaskDisplayArea taskDisplayArea = mAtm.mRootWindowContainer.getDefaultTaskDisplayArea();
Task rootTask = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN,
ACTIVITY_TYPE_STANDARD, true /* onTop */);
DisplayInfo displayInfo = new DisplayInfo();
mAtm.mContext.getDisplay().getDisplayInfo(displayInfo);
final int displayHeight = displayInfo.logicalHeight;
final Task task = new TaskBuilder(mSupervisor).setParentTaskFragment(rootTask).build();
final Configuration inOutConfig = new Configuration();
final Configuration parentConfig = new Configuration();
final int longSide = 1200;
final int shortSide = 600;
parentConfig.densityDpi = 400;
parentConfig.screenHeightDp = 200; // 200 * 400 / 160 = 500px
parentConfig.screenWidthDp = 100; // 100 * 400 / 160 = 250px
parentConfig.windowConfiguration.setRotation(ROTATION_0);
final int longSideDp = 480; // longSide / density = 1200 / 400 * 160
final int shortSideDp = 240; // shortSide / density = 600 / 400 * 160
final int screenLayout = parentConfig.screenLayout
& (Configuration.SCREENLAYOUT_LONG_MASK | Configuration.SCREENLAYOUT_SIZE_MASK);
final int reducedScreenLayout =
Configuration.reduceScreenLayout(screenLayout, longSideDp, shortSideDp);
// Portrait bounds overlapping with navigation bar, without insets.
final Rect freeformBounds = new Rect(0,
displayHeight - 10 - longSide,
shortSide,
displayHeight - 10);
inOutConfig.windowConfiguration.setBounds(freeformBounds);
// Set to freeform mode to verify bug fix.
inOutConfig.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
task.computeConfigResourceOverrides(inOutConfig, parentConfig);
// screenW/H should not be effected by parent since overridden and freeform
assertEquals(freeformBounds.width() * 160 / parentConfig.densityDpi,
inOutConfig.screenWidthDp);
assertEquals(freeformBounds.height() * 160 / parentConfig.densityDpi,
inOutConfig.screenHeightDp);
assertEquals(reducedScreenLayout, inOutConfig.screenLayout);
inOutConfig.setToDefaults();
// Landscape bounds overlapping with navigtion bar, without insets.
freeformBounds.set(0,
displayHeight - 10 - shortSide,
longSide,
displayHeight - 10);
inOutConfig.windowConfiguration.setBounds(freeformBounds);
inOutConfig.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
task.computeConfigResourceOverrides(inOutConfig, parentConfig);
assertEquals(freeformBounds.width() * 160 / parentConfig.densityDpi,
inOutConfig.screenWidthDp);
assertEquals(freeformBounds.height() * 160 / parentConfig.densityDpi,
inOutConfig.screenHeightDp);
assertEquals(reducedScreenLayout, inOutConfig.screenLayout);
}
/** Ensures that the alias intent won't have target component resolved. */
@Test
public void testTaskIntentActivityAlias() {
final String aliasClassName = DEFAULT_COMPONENT_PACKAGE_NAME + ".aliasActivity";
final String targetClassName = DEFAULT_COMPONENT_PACKAGE_NAME + ".targetActivity";
final ComponentName aliasComponent =
new ComponentName(DEFAULT_COMPONENT_PACKAGE_NAME, aliasClassName);
final ComponentName targetComponent =
new ComponentName(DEFAULT_COMPONENT_PACKAGE_NAME, targetClassName);
final Intent intent = new Intent();
intent.setComponent(aliasComponent);
final ActivityInfo info = new ActivityInfo();
info.applicationInfo = new ApplicationInfo();
info.packageName = DEFAULT_COMPONENT_PACKAGE_NAME;
info.targetActivity = targetClassName;
final Task task = new Task.Builder(mAtm)
.setTaskId(1)
.setActivityInfo(info)
.setIntent(intent)
.build();
assertEquals("The alias activity component should be saved in task intent.", aliasClassName,
task.intent.getComponent().getClassName());
ActivityRecord aliasActivity = new ActivityBuilder(mAtm).setComponent(
aliasComponent).setTargetActivity(targetClassName).build();
assertEquals("Should be the same intent filter.", true,
task.isSameIntentFilter(aliasActivity));
ActivityRecord targetActivity = new ActivityBuilder(mAtm).setComponent(
targetComponent).build();
assertEquals("Should be the same intent filter.", true,
task.isSameIntentFilter(targetActivity));
ActivityRecord defaultActivity = new ActivityBuilder(mAtm).build();
assertEquals("Should not be the same intent filter.", false,
task.isSameIntentFilter(defaultActivity));
}
/** Test that root activity index is reported correctly for several activities in the task. */
@Test
public void testFindRootIndex() {
final Task task = getTestTask();
// Add an extra activity on top of the root one
new ActivityBuilder(mAtm).setTask(task).build();
assertEquals("The root activity in the task must be reported.", task.getChildAt(0),
task.getRootActivity(
true /*ignoreRelinquishIdentity*/, true /*setToBottomIfNone*/));
}
/**
* Test that root activity index is reported correctly for several activities in the task when
* the activities on the bottom are finishing.
*/
@Test
public void testFindRootIndex_finishing() {
final Task task = getTestTask();
// Add extra two activities and mark the two on the bottom as finishing.
final ActivityRecord activity0 = task.getBottomMostActivity();
activity0.finishing = true;
final ActivityRecord activity1 = new ActivityBuilder(mAtm).setTask(task).build();
activity1.finishing = true;
new ActivityBuilder(mAtm).setTask(task).build();
assertEquals("The first non-finishing activity in the task must be reported.",
task.getChildAt(2), task.getRootActivity(
true /*ignoreRelinquishIdentity*/, true /*setToBottomIfNone*/));
}
/**
* Test that root activity index is reported correctly for several activities in the task when
* looking for the 'effective root'.
*/
@Test
public void testFindRootIndex_effectiveRoot() {
final Task task = getTestTask();
// Add an extra activity on top of the root one
new ActivityBuilder(mAtm).setTask(task).build();
assertEquals("The root activity in the task must be reported.",
task.getChildAt(0), task.getRootActivity(
false /*ignoreRelinquishIdentity*/, true /*setToBottomIfNone*/));
}
/**
* Test that root activity index is reported correctly when looking for the 'effective root' in
* case when bottom activities are relinquishing task identity or finishing.
*/
@Test
public void testFindRootIndex_effectiveRoot_finishingAndRelinquishing() {
final ActivityRecord activity0 = new ActivityBuilder(mAtm).setCreateTask(true).build();
final Task task = activity0.getTask();
// Add extra two activities. Mark the one on the bottom with "relinquishTaskIdentity" and
// one above as finishing.
activity0.info.flags |= FLAG_RELINQUISH_TASK_IDENTITY;
final ActivityRecord activity1 = new ActivityBuilder(mAtm).setTask(task).build();
activity1.finishing = true;
new ActivityBuilder(mAtm).setTask(task).build();
assertEquals("The first non-finishing activity and non-relinquishing task identity "
+ "must be reported.", task.getChildAt(2), task.getRootActivity(
false /*ignoreRelinquishIdentity*/, true /*setToBottomIfNone*/));
}
/**
* Test that root activity index is reported correctly when looking for the 'effective root'
* for the case when there is only a single activity that also has relinquishTaskIdentity set.
*/
@Test
public void testFindRootIndex_effectiveRoot_relinquishingAndSingleActivity() {
final Task task = getTestTask();
// Set relinquishTaskIdentity for the only activity in the task
task.getBottomMostActivity().info.flags |= FLAG_RELINQUISH_TASK_IDENTITY;
assertEquals("The root activity in the task must be reported.",
task.getChildAt(0), task.getRootActivity(
false /*ignoreRelinquishIdentity*/, true /*setToBottomIfNone*/));
}
/**
* Test that the topmost activity index is reported correctly when looking for the
* 'effective root' for the case when all activities have relinquishTaskIdentity set.
*/
@Test
public void testFindRootIndex_effectiveRoot_relinquishingMultipleActivities() {
final ActivityRecord activity0 = new ActivityBuilder(mAtm).setCreateTask(true).build();
final Task task = activity0.getTask();
// Set relinquishTaskIdentity for all activities in the task
activity0.info.flags |= FLAG_RELINQUISH_TASK_IDENTITY;
final ActivityRecord activity1 = new ActivityBuilder(mAtm).setTask(task).build();
activity1.info.flags |= FLAG_RELINQUISH_TASK_IDENTITY;
assertEquals("The topmost activity in the task must be reported.",
task.getChildAt(task.getChildCount() - 1), task.getRootActivity(
false /*ignoreRelinquishIdentity*/, true /*setToBottomIfNone*/));
}
/** Test that bottom-most activity is reported in {@link Task#getRootActivity()}. */
@Test
public void testGetRootActivity() {
final Task task = getTestTask();
// Add an extra activity on top of the root one
new ActivityBuilder(mAtm).setTask(task).build();
assertEquals("The root activity in the task must be reported.",
task.getBottomMostActivity(), task.getRootActivity());
}
/**
* Test that first non-finishing activity is reported in {@link Task#getRootActivity()}.
*/
@Test
public void testGetRootActivity_finishing() {
final Task task = getTestTask();
// Add an extra activity on top of the root one
new ActivityBuilder(mAtm).setTask(task).build();
// Mark the root as finishing
task.getBottomMostActivity().finishing = true;
assertEquals("The first non-finishing activity in the task must be reported.",
task.getChildAt(1), task.getRootActivity());
}
/**
* Test that relinquishTaskIdentity flag is ignored in {@link Task#getRootActivity()}.
*/
@Test
public void testGetRootActivity_relinquishTaskIdentity() {
final Task task = getTestTask();
// Mark the bottom-most activity with FLAG_RELINQUISH_TASK_IDENTITY.
final ActivityRecord activity0 = task.getBottomMostActivity();
activity0.info.flags |= FLAG_RELINQUISH_TASK_IDENTITY;
// Add an extra activity on top of the root one.
new ActivityBuilder(mAtm).setTask(task).build();
assertEquals("The root activity in the task must be reported.",
task.getBottomMostActivity(), task.getRootActivity());
}
/**
* Test that no activity is reported in {@link Task#getRootActivity()} when all activities
* in the task are finishing.
*/
@Test
public void testGetRootActivity_allFinishing() {
final Task task = getTestTask();
// Mark the bottom-most activity as finishing.
final ActivityRecord activity0 = task.getBottomMostActivity();
activity0.finishing = true;
// Add an extra activity on top of the root one and mark it as finishing
final ActivityRecord activity1 = new ActivityBuilder(mAtm).setTask(task).build();
activity1.finishing = true;
assertNull("No activity must be reported if all are finishing", task.getRootActivity());
}
/**
* Test that first non-finishing activity is the root of task.
*/
@Test
public void testIsRootActivity() {
final Task task = getTestTask();
// Mark the bottom-most activity as finishing.
final ActivityRecord activity0 = task.getBottomMostActivity();
activity0.finishing = true;
// Add an extra activity on top of the root one.
final ActivityRecord activity1 = new ActivityBuilder(mAtm).setTask(task).build();
assertFalse("Finishing activity must not be the root of task", activity0.isRootOfTask());
assertTrue("Non-finishing activity must be the root of task", activity1.isRootOfTask());
}
/**
* Test that if all activities in the task are finishing, then the one on the bottom is the
* root of task.
*/
@Test
public void testIsRootActivity_allFinishing() {
final Task task = getTestTask();
// Mark the bottom-most activity as finishing.
final ActivityRecord activity0 = task.getBottomMostActivity();
activity0.finishing = true;
// Add an extra activity on top of the root one and mark it as finishing
final ActivityRecord activity1 = new ActivityBuilder(mAtm).setTask(task).build();
activity1.finishing = true;
assertTrue("Bottom activity must be the root of task", activity0.isRootOfTask());
assertFalse("Finishing activity on top must not be the root of task",
activity1.isRootOfTask());
}
/**
* Test {@link ActivityRecord#getTaskForActivityLocked(IBinder, boolean)}.
*/
@Test
public void testGetTaskForActivity() {
final Task task0 = getTestTask();
final ActivityRecord activity0 = task0.getBottomMostActivity();
final Task task1 = getTestTask();
final ActivityRecord activity1 = task1.getBottomMostActivity();
assertEquals(task0.mTaskId,
ActivityRecord.getTaskForActivityLocked(activity0.token, false /* onlyRoot */));
assertEquals(task1.mTaskId,
ActivityRecord.getTaskForActivityLocked(activity1.token, false /* onlyRoot */));
}
/**
* Test {@link ActivityRecord#getTaskForActivityLocked(IBinder, boolean)} with finishing
* activity.
*/
@Test
public void testGetTaskForActivity_onlyRoot_finishing() {
final Task task = getTestTask();
// Make the current root activity finishing
final ActivityRecord activity0 = task.getBottomMostActivity();
activity0.finishing = true;
// Add an extra activity on top - this will be the new root
final ActivityRecord activity1 = new ActivityBuilder(mAtm).setTask(task).build();
// Add one more on top
final ActivityRecord activity2 = new ActivityBuilder(mAtm).setTask(task).build();
assertEquals(task.mTaskId,
ActivityRecord.getTaskForActivityLocked(activity0.token, true /* onlyRoot */));
assertEquals(task.mTaskId,
ActivityRecord.getTaskForActivityLocked(activity1.token, true /* onlyRoot */));
assertEquals("No task must be reported for activity that is above root", INVALID_TASK_ID,
ActivityRecord.getTaskForActivityLocked(activity2.token, true /* onlyRoot */));
}
/**
* Test {@link ActivityRecord#getTaskForActivityLocked(IBinder, boolean)} with activity that
* relinquishes task identity.
*/
@Test
public void testGetTaskForActivity_onlyRoot_relinquishTaskIdentity() {
final ActivityRecord activity0 = new ActivityBuilder(mAtm).setCreateTask(true).build();
final Task task = activity0.getTask();
// Make the current root activity relinquish task identity
activity0.info.flags |= FLAG_RELINQUISH_TASK_IDENTITY;
// Add an extra activity on top - this will be the new root
final ActivityRecord activity1 = new ActivityBuilder(mAtm).setTask(task).build();
// Add one more on top
final ActivityRecord activity2 = new ActivityBuilder(mAtm).setTask(task).build();
assertEquals(task.mTaskId,
ActivityRecord.getTaskForActivityLocked(activity0.token, true /* onlyRoot */));
assertEquals(task.mTaskId,
ActivityRecord.getTaskForActivityLocked(activity1.token, true /* onlyRoot */));
assertEquals("No task must be reported for activity that is above root", INVALID_TASK_ID,
ActivityRecord.getTaskForActivityLocked(activity2.token, true /* onlyRoot */));
}
/**
* Test {@link ActivityRecord#getTaskForActivityLocked(IBinder, boolean)} allowing non-root
* entries.
*/
@Test
public void testGetTaskForActivity_notOnlyRoot() {
final Task task = getTestTask();
// Mark the bottom-most activity as finishing.
final ActivityRecord activity0 = task.getBottomMostActivity();
activity0.finishing = true;
// Add an extra activity on top of the root one and make it relinquish task identity
final ActivityRecord activity1 = new ActivityBuilder(mAtm).setTask(task).build();
activity1.info.flags |= FLAG_RELINQUISH_TASK_IDENTITY;
// Add one more activity on top
final ActivityRecord activity2 = new ActivityBuilder(mAtm).setTask(task).build();
assertEquals(task.mTaskId,
ActivityRecord.getTaskForActivityLocked(activity0.token, false /* onlyRoot */));
assertEquals(task.mTaskId,
ActivityRecord.getTaskForActivityLocked(activity1.token, false /* onlyRoot */));
assertEquals(task.mTaskId,
ActivityRecord.getTaskForActivityLocked(activity2.token, false /* onlyRoot */));
}
/**
* Test {@link Task#updateEffectiveIntent()}.
*/
@Test
public void testUpdateEffectiveIntent() {
// Test simple case with a single activity.
final Task task = getTestTask();
final ActivityRecord activity0 = task.getBottomMostActivity();
spyOn(task);
task.updateEffectiveIntent();
verify(task).setIntent(eq(activity0));
}
/**
* Test {@link Task#updateEffectiveIntent()} with root activity marked as finishing. This
* should make the task use the second activity when updating the intent.
*/
@Test
public void testUpdateEffectiveIntent_rootFinishing() {
// Test simple case with a single activity.
final Task task = getTestTask();
final ActivityRecord activity0 = task.getBottomMostActivity();
// Mark the bottom-most activity as finishing.
activity0.finishing = true;
// Add an extra activity on top of the root one
final ActivityRecord activity1 = new ActivityBuilder(mAtm).setTask(task).build();
spyOn(task);
task.updateEffectiveIntent();
verify(task).setIntent(eq(activity1));
}
/**
* Test {@link Task#updateEffectiveIntent()} when all activities are finishing or
* relinquishing task identity. In this case the root activity should still be used when
* updating the intent (legacy behavior).
*/
@Test
public void testUpdateEffectiveIntent_allFinishing() {
// Test simple case with a single activity.
final Task task = getTestTask();
final ActivityRecord activity0 = task.getBottomMostActivity();
// Mark the bottom-most activity as finishing.
activity0.finishing = true;
// Add an extra activity on top of the root one and make it relinquish task identity
final ActivityRecord activity1 = new ActivityBuilder(mAtm).setTask(task).build();
activity1.finishing = true;
// Task must still update the intent using the root activity (preserving legacy behavior).
spyOn(task);
task.updateEffectiveIntent();
verify(task).setIntent(eq(activity0));
}
/**
* Test {@link Task#updateEffectiveIntent()} when activity with relinquishTaskIdentity but
* another with different uid. This should make the task use the root activity when updating the
* intent.
*/
@Test
public void testUpdateEffectiveIntent_relinquishingWithDifferentUid() {
final ActivityRecord activity0 = new ActivityBuilder(mAtm)
.setActivityFlags(FLAG_RELINQUISH_TASK_IDENTITY).setCreateTask(true).build();
final Task task = activity0.getTask();
// Add an extra activity on top
new ActivityBuilder(mAtm).setUid(11).setTask(task).build();
spyOn(task);
task.updateEffectiveIntent();
verify(task).setIntent(eq(activity0));
}
/**
* Test {@link Task#updateEffectiveIntent()} with activities set as relinquishTaskIdentity.
* This should make the task use the topmost activity when updating the intent.
*/
@Test
public void testUpdateEffectiveIntent_relinquishingMultipleActivities() {
final ActivityRecord activity0 = new ActivityBuilder(mAtm)
.setActivityFlags(FLAG_RELINQUISH_TASK_IDENTITY).setCreateTask(true).build();
final Task task = activity0.getTask();
// Add an extra activity on top
final ActivityRecord activity1 = new ActivityBuilder(mAtm).setTask(task).build();
activity1.info.flags |= FLAG_RELINQUISH_TASK_IDENTITY;
// Add an extra activity on top
final ActivityRecord activity2 = new ActivityBuilder(mAtm).setTask(task).build();
spyOn(task);
task.updateEffectiveIntent();
verify(task).setIntent(eq(activity2));
}
@Test
public void testSaveLaunchingStateWhenConfigurationChanged() {
LaunchParamsPersister persister = mAtm.mTaskSupervisor.mLaunchParamsPersister;
spyOn(persister);
final Task task = getTestTask();
task.setHasBeenVisible(false);
task.getDisplayContent().setDisplayWindowingMode(WINDOWING_MODE_FREEFORM);
task.getRootTask().setWindowingMode(WINDOWING_MODE_FULLSCREEN);
task.setHasBeenVisible(true);
task.onConfigurationChanged(task.getParent().getConfiguration());
verify(persister).saveTask(task, task.getDisplayContent());
}
@Test
public void testSaveLaunchingStateWhenClearingParent() {
LaunchParamsPersister persister = mAtm.mTaskSupervisor.mLaunchParamsPersister;
spyOn(persister);
final Task task = getTestTask();
task.setHasBeenVisible(false);
task.getDisplayContent().setWindowingMode(WindowConfiguration.WINDOWING_MODE_FREEFORM);
task.getRootTask().setWindowingMode(WINDOWING_MODE_FULLSCREEN);
final DisplayContent oldDisplay = task.getDisplayContent();
LaunchParamsController.LaunchParams params = new LaunchParamsController.LaunchParams();
params.mWindowingMode = WINDOWING_MODE_UNDEFINED;
persister.getLaunchParams(task, null, params);
assertEquals(WINDOWING_MODE_UNDEFINED, params.mWindowingMode);
task.setHasBeenVisible(true);
task.removeImmediately();
verify(persister).saveTask(task, oldDisplay);
persister.getLaunchParams(task, null, params);
assertEquals(WINDOWING_MODE_FULLSCREEN, params.mWindowingMode);
}
@Test
public void testNotSaveLaunchingStateNonFreeformDisplay() {
LaunchParamsPersister persister = mAtm.mTaskSupervisor.mLaunchParamsPersister;
spyOn(persister);
final Task task = getTestTask();
task.setHasBeenVisible(false);
task.getRootTask().setWindowingMode(WINDOWING_MODE_FULLSCREEN);
task.setHasBeenVisible(true);
task.onConfigurationChanged(task.getParent().getConfiguration());
Mockito.verify(persister, never()).saveTask(same(task), any());
}
@Test
public void testNotSaveLaunchingStateWhenNotFullscreenOrFreeformWindow() {
LaunchParamsPersister persister = mAtm.mTaskSupervisor.mLaunchParamsPersister;
spyOn(persister);
final Task task = getTestTask();
task.setHasBeenVisible(false);
task.getDisplayContent().setDisplayWindowingMode(WINDOWING_MODE_FREEFORM);
task.getRootTask().setWindowingMode(WINDOWING_MODE_PINNED);
task.setHasBeenVisible(true);
task.onConfigurationChanged(task.getParent().getConfiguration());
Mockito.verify(persister, never()).saveTask(same(task), any());
}
@Test
public void testNotSaveLaunchingStateForNonLeafTask() {
LaunchParamsPersister persister = mAtm.mTaskSupervisor.mLaunchParamsPersister;
spyOn(persister);
final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true)
.setCreateParentTask(true).build().getRootTask();
task.setHasBeenVisible(false);
task.getDisplayContent().setDisplayWindowingMode(WINDOWING_MODE_FREEFORM);
task.getRootTask().setWindowingMode(WINDOWING_MODE_FULLSCREEN);
final Task leafTask = createTaskInRootTask(task, 0 /* userId */);
leafTask.setHasBeenVisible(true);
task.setHasBeenVisible(true);
task.onConfigurationChanged(task.getParent().getConfiguration());
Mockito.verify(persister, never()).saveTask(same(task), any());
verify(persister).saveTask(same(leafTask), any());
}
@Test
public void testNotSpecifyOrientationByFloatingTask() {
final Task task = new TaskBuilder(mSupervisor)
.setCreateActivity(true).setCreateParentTask(true).build();
final ActivityRecord activity = task.getTopMostActivity();
final WindowContainer<?> parentContainer = task.getParent();
final TaskDisplayArea taskDisplayArea = task.getDisplayArea();
activity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE);
assertEquals(SCREEN_ORIENTATION_LANDSCAPE, parentContainer.getOrientation());
assertEquals(SCREEN_ORIENTATION_LANDSCAPE, taskDisplayArea.getOrientation());
task.setWindowingMode(WINDOWING_MODE_PINNED);
// TDA returns the last orientation when child returns UNSET
assertEquals(SCREEN_ORIENTATION_UNSET, parentContainer.getOrientation());
assertEquals(SCREEN_ORIENTATION_LANDSCAPE, taskDisplayArea.getOrientation());
}
@Test
public void testNotSpecifyOrientation_taskDisplayAreaNotFocused() {
final TaskDisplayArea firstTaskDisplayArea = mDisplayContent.getDefaultTaskDisplayArea();
final TaskDisplayArea secondTaskDisplayArea = createTaskDisplayArea(
mDisplayContent, mRootWindowContainer.mWmService, "TestTaskDisplayArea",
FEATURE_VENDOR_FIRST);
final Task firstRootTask = firstTaskDisplayArea.createRootTask(
WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, false /* onTop */);
final Task secondRootTask = secondTaskDisplayArea.createRootTask(
WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, false /* onTop */);
final ActivityRecord firstActivity = new ActivityBuilder(mAtm)
.setTask(firstRootTask).build();
final ActivityRecord secondActivity = new ActivityBuilder(mAtm)
.setTask(secondRootTask).build();
firstActivity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE);
secondActivity.setRequestedOrientation(SCREEN_ORIENTATION_PORTRAIT);
// Activity on TDA1 is focused
mDisplayContent.setFocusedApp(firstActivity);
assertEquals(SCREEN_ORIENTATION_LANDSCAPE, firstTaskDisplayArea.getOrientation());
assertEquals(SCREEN_ORIENTATION_UNSET, secondTaskDisplayArea.getOrientation());
// No focused app, TDA1 is still recorded as last focused.
mDisplayContent.setFocusedApp(null);
assertEquals(SCREEN_ORIENTATION_LANDSCAPE, firstTaskDisplayArea.getOrientation());
assertEquals(SCREEN_ORIENTATION_UNSET, secondTaskDisplayArea.getOrientation());
// Activity on TDA2 is focused
mDisplayContent.setFocusedApp(secondActivity);
assertEquals(SCREEN_ORIENTATION_UNSET, firstTaskDisplayArea.getOrientation());
assertEquals(SCREEN_ORIENTATION_PORTRAIT, secondTaskDisplayArea.getOrientation());
}
@Test
public void testTaskOrientationOnDisplayWindowingModeChange() {
// Skip unnecessary operations to speed up the test.
mAtm.deferWindowLayout();
final Task task = getTestTask();
final ActivityRecord activity = task.getTopMostActivity();
final DisplayContent display = task.getDisplayContent();
mWm.setWindowingMode(display.mDisplayId, WINDOWING_MODE_FREEFORM);
activity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE);
assertEquals(SCREEN_ORIENTATION_UNSET, task.getOrientation());
assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, display.getLastOrientation());
mWm.setWindowingMode(display.mDisplayId, WINDOWING_MODE_FULLSCREEN);
assertEquals(SCREEN_ORIENTATION_LANDSCAPE, task.getOrientation());
assertEquals(SCREEN_ORIENTATION_LANDSCAPE, display.getLastOrientation());
assertEquals(Configuration.ORIENTATION_LANDSCAPE, display.getConfiguration().orientation);
}
@Test
public void testGetNonNullDimmerOnUntrustedDisplays() {
final DisplayInfo untrustedDisplayInfo = new DisplayInfo(mDisplayInfo);
untrustedDisplayInfo.flags &= ~Display.FLAG_TRUSTED;
final DisplayContent untrustedDisplay = createNewDisplay(untrustedDisplayInfo);
final ActivityRecord activity = createActivityRecord(untrustedDisplay);
activity.setOccludesParent(false);
assertNotNull(activity.getTask().getDimmer());
}
@Test
public void testResumeTask_doNotResumeTaskFragmentBehindTranslucent() {
final Task task = createTask(mDisplayContent);
final TaskFragment tfBehind = createTaskFragmentWithParentTask(
task, false /* createEmbeddedTask */);
final TaskFragment tfFront = createTaskFragmentWithParentTask(
task, false /* createEmbeddedTask */);
spyOn(tfFront);
doReturn(true).when(tfFront).isTranslucent(any());
// TaskFragment behind another translucent TaskFragment should not be resumed.
assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
tfBehind.getVisibility(null /* starting */));
assertTrue(tfBehind.isFocusable());
assertFalse(tfBehind.canBeResumed(null /* starting */));
spyOn(tfBehind);
task.resumeTopActivityUncheckedLocked(null /* prev */, ActivityOptions.makeBasic(),
false /* deferPause */);
verify(tfBehind, never()).resumeTopActivity(any(), any(), anyBoolean());
}
private Task getTestTask() {
final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
return task.getBottomMostTask();
}
private void testRootTaskBoundsConfiguration(int windowingMode, Rect parentBounds, Rect bounds,
Rect expectedConfigBounds) {
TaskDisplayArea taskDisplayArea = mAtm.mRootWindowContainer.getDefaultTaskDisplayArea();
Task rootTask = taskDisplayArea.createRootTask(windowingMode, ACTIVITY_TYPE_STANDARD,
true /* onTop */);
Task task = new TaskBuilder(mSupervisor).setParentTaskFragment(rootTask).build();
final Configuration parentConfig = rootTask.getConfiguration();
parentConfig.windowConfiguration.setAppBounds(parentBounds);
task.setBounds(bounds);
task.resolveOverrideConfiguration(parentConfig);
// Assert that both expected and actual are null or are equal to each other
assertEquals(expectedConfigBounds,
task.getResolvedOverrideConfiguration().windowConfiguration.getAppBounds());
}
private byte[] serializeToBytes(Task r) throws Exception {
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
final TypedXmlSerializer serializer = Xml.newFastSerializer();
serializer.setOutput(os, "UTF-8");
serializer.startDocument(null, true);
serializer.startTag(null, TASK_TAG);
r.saveToXml(serializer);
serializer.endTag(null, TASK_TAG);
serializer.endDocument();
os.flush();
return os.toByteArray();
}
}
private Task restoreFromBytes(byte[] in) throws IOException, XmlPullParserException {
try (Reader reader = new InputStreamReader(new ByteArrayInputStream(in))) {
final TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(reader);
assertEquals(XmlPullParser.START_TAG, parser.next());
assertEquals(TASK_TAG, parser.getName());
return Task.restoreFromXml(parser, mAtm.mTaskSupervisor);
}
}
private Task createTask(int taskId) {
return new Task.Builder(mAtm)
.setTaskId(taskId)
.setIntent(new Intent())
.setRealActivity(ActivityBuilder.getDefaultComponent())
.setEffectiveUid(10050)
.buildInner();
}
}