blob: 88eadfceedb633b90304fdc28b28f5fe8536bdf8 [file] [log] [blame]
/*
* Copyright (C) 2021 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.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
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_UNSET;
import static android.os.Process.FIRST_APPLICATION_UID;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.server.wm.ActivityRecord.State.RESUMED;
import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED;
import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_MIN_DIMENSION_VIOLATION;
import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_NEW_TASK_FRAGMENT;
import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_UNTRUSTED_HOST;
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.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.clearInvocations;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Binder;
import android.platform.test.annotations.Presubmit;
import android.view.SurfaceControl;
import android.window.ITaskFragmentOrganizer;
import android.window.TaskFragmentInfo;
import android.window.TaskFragmentOrganizer;
import androidx.test.filters.MediumTest;
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 TaskFragment}.
*
* Build/Install/Run:
* atest WmTests:TaskFragmentTest
*/
@MediumTest
@Presubmit
@RunWith(WindowTestRunner.class)
public class TaskFragmentTest extends WindowTestsBase {
private TaskFragmentOrganizer mOrganizer;
private ITaskFragmentOrganizer mIOrganizer;
private TaskFragment mTaskFragment;
private SurfaceControl mLeash;
@Mock
private SurfaceControl.Transaction mTransaction;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mOrganizer = new TaskFragmentOrganizer(Runnable::run);
mIOrganizer = ITaskFragmentOrganizer.Stub.asInterface(mOrganizer.getOrganizerToken()
.asBinder());
mAtm.mWindowOrganizerController.mTaskFragmentOrganizerController
.registerOrganizer(mIOrganizer);
mTaskFragment = new TaskFragmentBuilder(mAtm)
.setCreateParentTask()
.setOrganizer(mOrganizer)
.setFragmentToken(new Binder())
.build();
mLeash = mTaskFragment.getSurfaceControl();
spyOn(mTaskFragment);
doReturn(mTransaction).when(mTaskFragment).getSyncTransaction();
doReturn(mTransaction).when(mTaskFragment).getPendingTransaction();
}
@Test
public void testOnConfigurationChanged_updateSurface() {
final Rect bounds = new Rect(100, 100, 1100, 1100);
mTaskFragment.setBounds(bounds);
verify(mTransaction).setPosition(mLeash, 100, 100);
verify(mTransaction).setWindowCrop(mLeash, 1000, 1000);
}
@Test
public void testStartChangeTransition_resetSurface() {
mockSurfaceFreezerSnapshot(mTaskFragment.mSurfaceFreezer);
final Rect startBounds = new Rect(0, 0, 1000, 1000);
final Rect endBounds = new Rect(500, 500, 1000, 1000);
mTaskFragment.setBounds(startBounds);
doReturn(true).when(mTaskFragment).isVisible();
doReturn(true).when(mTaskFragment).isVisibleRequested();
clearInvocations(mTransaction);
mTaskFragment.setBounds(endBounds);
// Surface reset when prepare transition.
verify(mTaskFragment).initializeChangeTransition(startBounds);
verify(mTransaction).setPosition(mLeash, 0, 0);
verify(mTransaction).setWindowCrop(mLeash, 0, 0);
clearInvocations(mTransaction);
mTaskFragment.mSurfaceFreezer.unfreeze(mTransaction);
// Update surface after animation.
verify(mTransaction).setPosition(mLeash, 500, 500);
verify(mTransaction).setWindowCrop(mLeash, 500, 500);
}
@Test
public void testNotOkToAnimate_doNotStartChangeTransition() {
mockSurfaceFreezerSnapshot(mTaskFragment.mSurfaceFreezer);
final Rect startBounds = new Rect(0, 0, 1000, 1000);
final Rect endBounds = new Rect(500, 500, 1000, 1000);
mTaskFragment.setBounds(startBounds);
doReturn(true).when(mTaskFragment).isVisible();
doReturn(true).when(mTaskFragment).isVisibleRequested();
final DisplayPolicy displayPolicy = mDisplayContent.getDisplayPolicy();
displayPolicy.screenTurnedOff();
assertFalse(mTaskFragment.okToAnimate());
mTaskFragment.setBounds(endBounds);
verify(mTaskFragment, never()).initializeChangeTransition(any());
}
/**
* Tests that when a {@link TaskFragmentInfo} is generated from a {@link TaskFragment}, an
* activity that has not yet been attached to a process because it is being initialized but
* belongs to the TaskFragmentOrganizer process is still reported in the TaskFragmentInfo.
*/
@Test
public void testActivityStillReported_NotYetAssignedToProcess() {
mTaskFragment.addChild(new ActivityBuilder(mAtm).setUid(DEFAULT_TASK_FRAGMENT_ORGANIZER_UID)
.setProcessName(DEFAULT_TASK_FRAGMENT_ORGANIZER_PROCESS_NAME).build());
final ActivityRecord activity = mTaskFragment.getTopMostActivity();
// Remove the process to simulate an activity that has not yet been attached to a process
activity.app = null;
final TaskFragmentInfo info = activity.getTaskFragment().getTaskFragmentInfo();
assertEquals(1, info.getRunningActivityCount());
assertEquals(1, info.getActivities().size());
assertEquals(false, info.isEmpty());
assertEquals(activity.token, info.getActivities().get(0));
}
@Test
public void testActivityVisibilityBehindTranslucentTaskFragment() {
// Having an activity covered by a translucent TaskFragment:
// Task
// - TaskFragment
// - Activity (Translucent)
// - Activity
ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
.setUid(DEFAULT_TASK_FRAGMENT_ORGANIZER_UID).build();
mTaskFragment.addChild(translucentActivity);
doReturn(true).when(mTaskFragment).isTranslucent(any());
ActivityRecord activityBelow = new ActivityBuilder(mAtm).build();
mTaskFragment.getTask().addChild(activityBelow, 0);
// Ensure the activity below is visible
mTaskFragment.getTask().ensureActivitiesVisible(null /* starting */, 0 /* configChanges */,
false /* preserveWindows */);
assertEquals(true, activityBelow.isVisibleRequested());
}
@Test
public void testMoveTaskToFront_supportsEnterPipOnTaskSwitchForAdjacentTaskFragment() {
final Task bottomTask = createTask(mDisplayContent);
final ActivityRecord bottomActivity = createActivityRecord(bottomTask);
final Task topTask = createTask(mDisplayContent);
// First create primary TF, and then secondary TF, so that the secondary will be on the top.
final TaskFragment primaryTf = createTaskFragmentWithParentTask(
topTask, false /* createEmbeddedTask */);
final TaskFragment secondaryTf = createTaskFragmentWithParentTask(
topTask, false /* createEmbeddedTask */);
final ActivityRecord primaryActivity = primaryTf.getTopMostActivity();
final ActivityRecord secondaryActivity = secondaryTf.getTopMostActivity();
doReturn(true).when(primaryActivity).supportsPictureInPicture();
doReturn(false).when(secondaryActivity).supportsPictureInPicture();
primaryTf.setAdjacentTaskFragment(secondaryTf);
primaryActivity.setState(RESUMED, "test");
secondaryActivity.setState(RESUMED, "test");
assertEquals(topTask, bottomTask.getDisplayArea().getTopRootTask());
// When moving Task to front, the resumed activity that supports PIP should support enter
// PIP on Task switch even if it is not the topmost in the Task.
bottomTask.moveTaskToFront(bottomTask, false /* noAnimation */, null /* options */,
null /* timeTracker */, "test");
assertTrue(primaryActivity.supportsEnterPipOnTaskSwitch);
assertFalse(secondaryActivity.supportsEnterPipOnTaskSwitch);
}
@Test
public void testEmbeddedTaskFragmentEnterPip_singleActivity_resetOrganizerOverrideConfig() {
final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
.setOrganizer(mOrganizer)
.setFragmentToken(new Binder())
.setCreateParentTask()
.createActivityCount(1)
.build();
final Task task = taskFragment.getTask();
final ActivityRecord activity = taskFragment.getTopMostActivity();
final Rect taskFragmentBounds = new Rect(0, 0, 300, 1000);
task.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
taskFragment.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
taskFragment.setBounds(taskFragmentBounds);
assertEquals(taskFragmentBounds, activity.getBounds());
assertEquals(WINDOWING_MODE_MULTI_WINDOW, activity.getWindowingMode());
// Move activity to pinned root task.
mRootWindowContainer.moveActivityToPinnedRootTask(activity,
null /* launchIntoPipHostActivity */, "test");
// Ensure taskFragment requested config is reset.
assertEquals(taskFragment, activity.getOrganizedTaskFragment());
assertEquals(task, activity.getTask());
assertTrue(task.inPinnedWindowingMode());
assertTrue(taskFragment.inPinnedWindowingMode());
final Rect taskBounds = task.getBounds();
assertEquals(taskBounds, taskFragment.getBounds());
assertEquals(taskBounds, activity.getBounds());
assertEquals(Configuration.EMPTY, taskFragment.getRequestedOverrideConfiguration());
// Because the whole Task is entering PiP, no need to record for future reparent.
assertNull(activity.mLastTaskFragmentOrganizerBeforePip);
}
@Test
public void testEmbeddedTaskFragmentEnterPip_multiActivities_notifyOrganizer() {
final Task task = createTask(mDisplayContent);
final TaskFragment taskFragment0 = new TaskFragmentBuilder(mAtm)
.setParentTask(task)
.setOrganizer(mOrganizer)
.setFragmentToken(new Binder())
.createActivityCount(1)
.build();
final TaskFragment taskFragment1 = new TaskFragmentBuilder(mAtm)
.setParentTask(task)
.setOrganizer(mOrganizer)
.setFragmentToken(new Binder())
.createActivityCount(1)
.build();
final ActivityRecord activity0 = taskFragment0.getTopMostActivity();
final ActivityRecord activity1 = taskFragment1.getTopMostActivity();
activity0.setVisibility(true /* visible */, false /* deferHidingClient */);
activity1.setVisibility(true /* visible */, false /* deferHidingClient */);
spyOn(mAtm.mTaskFragmentOrganizerController);
// Move activity to pinned.
mRootWindowContainer.moveActivityToPinnedRootTask(activity0,
null /* launchIntoPipHostActivity */, "test");
// Ensure taskFragment requested config is reset.
assertTrue(taskFragment0.mClearedTaskFragmentForPip);
assertFalse(taskFragment1.mClearedTaskFragmentForPip);
final TaskFragmentInfo info = taskFragment0.getTaskFragmentInfo();
assertTrue(info.isTaskFragmentClearedForPip());
assertTrue(info.isEmpty());
// Notify organizer because the Task is still visible.
assertTrue(task.isVisibleRequested());
verify(mAtm.mTaskFragmentOrganizerController)
.dispatchPendingInfoChangedEvent(taskFragment0);
// Make sure the organizer is recorded so that it can be reused when the activity is
// reparented back on exiting PiP.
assertEquals(mIOrganizer, activity0.mLastTaskFragmentOrganizerBeforePip);
}
@Test
public void testEmbeddedActivityExitPip_notifyOrganizer() {
final Task task = createTask(mDisplayContent);
final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
.setParentTask(task)
.setOrganizer(mOrganizer)
.setFragmentToken(new Binder())
.createActivityCount(1)
.build();
new TaskFragmentBuilder(mAtm)
.setParentTask(task)
.setOrganizer(mOrganizer)
.setFragmentToken(new Binder())
.createActivityCount(1)
.build();
final ActivityRecord activity = taskFragment.getTopMostActivity();
mRootWindowContainer.moveActivityToPinnedRootTask(activity,
null /* launchIntoPipHostActivity */, "test");
spyOn(mAtm.mTaskFragmentOrganizerController);
assertEquals(mIOrganizer, activity.mLastTaskFragmentOrganizerBeforePip);
// Move the activity back to its original Task.
activity.reparent(task, POSITION_TOP);
// Notify the organizer about the reparent.
verify(mAtm.mTaskFragmentOrganizerController).onActivityReparentToTask(activity);
assertNull(activity.mLastTaskFragmentOrganizerBeforePip);
}
@Test
public void testIsReadyToTransit() {
final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
.setCreateParentTask()
.setOrganizer(mOrganizer)
.setFragmentToken(new Binder())
.build();
final Task task = taskFragment.getTask();
// Not ready when it is empty.
assertFalse(taskFragment.isReadyToTransit());
// Ready when it is not empty.
final ActivityRecord activity = createActivityRecord(mDisplayContent);
doNothing().when(activity).setDropInputMode(anyInt());
activity.reparent(taskFragment, WindowContainer.POSITION_TOP);
assertTrue(taskFragment.isReadyToTransit());
// Ready when the Task is in PiP.
taskFragment.removeChild(activity);
task.setWindowingMode(WINDOWING_MODE_PINNED);
assertTrue(taskFragment.isReadyToTransit());
// Ready when the TaskFragment is empty because of PiP, and the Task is invisible.
task.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
taskFragment.mClearedTaskFragmentForPip = true;
assertTrue(taskFragment.isReadyToTransit());
// Not ready if the task is still visible when the TaskFragment becomes empty.
doReturn(true).when(task).isVisibleRequested();
assertFalse(taskFragment.isReadyToTransit());
}
@Test
public void testActivityHasOverlayOverUntrustedModeEmbedded() {
final Task rootTask = createTask(mDisplayContent, WINDOWING_MODE_MULTI_WINDOW,
ACTIVITY_TYPE_STANDARD);
final Task leafTask0 = new TaskBuilder(mSupervisor)
.setParentTaskFragment(rootTask)
.build();
final TaskFragment organizedTf = new TaskFragmentBuilder(mAtm)
.createActivityCount(2)
.setParentTask(leafTask0)
.setFragmentToken(new Binder())
.setOrganizer(mOrganizer)
.build();
final ActivityRecord activity0 = organizedTf.getBottomMostActivity();
final ActivityRecord activity1 = organizedTf.getTopMostActivity();
// Bottom activity is untrusted embedding. Top activity is trusted embedded.
// Activity0 has overlay over untrusted mode embedded.
activity0.info.applicationInfo.uid = DEFAULT_TASK_FRAGMENT_ORGANIZER_UID + 1;
activity1.info.applicationInfo.uid = DEFAULT_TASK_FRAGMENT_ORGANIZER_UID;
doReturn(true).when(organizedTf).isAllowedToEmbedActivityInUntrustedMode(activity0);
assertTrue(activity0.hasOverlayOverUntrustedModeEmbedded());
assertFalse(activity1.hasOverlayOverUntrustedModeEmbedded());
// Both activities are trusted embedded.
// None of the two has overlay over untrusted mode embedded.
activity0.info.applicationInfo.uid = DEFAULT_TASK_FRAGMENT_ORGANIZER_UID;
assertFalse(activity0.hasOverlayOverUntrustedModeEmbedded());
assertFalse(activity1.hasOverlayOverUntrustedModeEmbedded());
// Bottom activity is trusted embedding. Top activity is untrusted embedded.
// None of the two has overlay over untrusted mode embedded.
activity1.info.applicationInfo.uid = DEFAULT_TASK_FRAGMENT_ORGANIZER_UID + 1;
assertFalse(activity0.hasOverlayOverUntrustedModeEmbedded());
assertFalse(activity1.hasOverlayOverUntrustedModeEmbedded());
// There is an activity in a different leaf task on top of activity0 and activity1.
// None of the two has overlay over untrusted mode embedded because it is not the same Task.
final Task leafTask1 = new TaskBuilder(mSupervisor)
.setParentTaskFragment(rootTask)
.setOnTop(true)
.setCreateActivity(true)
.build();
final ActivityRecord activity2 = leafTask1.getTopMostActivity();
activity2.info.applicationInfo.uid = DEFAULT_TASK_FRAGMENT_ORGANIZER_UID + 2;
assertFalse(activity0.hasOverlayOverUntrustedModeEmbedded());
assertFalse(activity1.hasOverlayOverUntrustedModeEmbedded());
}
@Test
public void testIsAllowedToBeEmbeddedInTrustedMode() {
final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
.setCreateParentTask()
.createActivityCount(2)
.build();
final ActivityRecord activity0 = taskFragment.getBottomMostActivity();
final ActivityRecord activity1 = taskFragment.getTopMostActivity();
// Allowed if all children activities are allowed.
doReturn(true).when(taskFragment).isAllowedToEmbedActivityInTrustedMode(activity0);
doReturn(true).when(taskFragment).isAllowedToEmbedActivityInTrustedMode(activity1);
assertTrue(taskFragment.isAllowedToBeEmbeddedInTrustedMode());
// Disallowed if any child activity is not allowed.
doReturn(false).when(taskFragment).isAllowedToEmbedActivityInTrustedMode(activity0);
assertFalse(taskFragment.isAllowedToBeEmbeddedInTrustedMode());
doReturn(false).when(taskFragment).isAllowedToEmbedActivityInTrustedMode(activity1);
assertFalse(taskFragment.isAllowedToBeEmbeddedInTrustedMode());
}
@Test
public void testIsAllowedToEmbedActivity() {
final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
.setCreateParentTask()
.createActivityCount(1)
.build();
final ActivityRecord activity = taskFragment.getTopMostActivity();
// Not allow embedding activity if not a trusted host.
doReturn(false).when(taskFragment).isAllowedToEmbedActivityInUntrustedMode(any());
doReturn(false).when(taskFragment).isAllowedToEmbedActivityInTrustedMode(any(), anyInt());
assertEquals(EMBEDDING_DISALLOWED_UNTRUSTED_HOST,
taskFragment.isAllowedToEmbedActivity(activity));
// Not allow embedding activity if the TaskFragment is smaller than activity min dimension.
doReturn(true).when(taskFragment).isAllowedToEmbedActivityInTrustedMode(any(), anyInt());
doReturn(true).when(taskFragment).smallerThanMinDimension(any());
assertEquals(EMBEDDING_DISALLOWED_MIN_DIMENSION_VIOLATION,
taskFragment.isAllowedToEmbedActivity(activity));
// Not allow to start activity across TaskFragments for result.
final TaskFragment newTaskFragment = new TaskFragmentBuilder(mAtm)
.setParentTask(taskFragment.getTask())
.build();
final ActivityRecord newActivity = new ActivityBuilder(mAtm)
.setUid(FIRST_APPLICATION_UID)
.build();
doReturn(true).when(newTaskFragment).isAllowedToEmbedActivityInTrustedMode(any(), anyInt());
doReturn(false).when(newTaskFragment).smallerThanMinDimension(any());
newActivity.resultTo = activity;
assertEquals(EMBEDDING_DISALLOWED_NEW_TASK_FRAGMENT,
newTaskFragment.isAllowedToEmbedActivity(newActivity));
// Allow embedding if the resultTo activity is finishing.
activity.finishing = true;
assertEquals(EMBEDDING_ALLOWED, newTaskFragment.isAllowedToEmbedActivity(newActivity));
}
@Test
public void testIgnoreRequestedOrientationForActivityEmbeddingSplit() {
// Setup two activities in ActivityEmbedding split.
final Task task = createTask(mDisplayContent);
final TaskFragment tf0 = new TaskFragmentBuilder(mAtm)
.setParentTask(task)
.createActivityCount(1)
.setOrganizer(mOrganizer)
.setFragmentToken(new Binder())
.build();
final TaskFragment tf1 = new TaskFragmentBuilder(mAtm)
.setParentTask(task)
.createActivityCount(1)
.setOrganizer(mOrganizer)
.setFragmentToken(new Binder())
.build();
tf0.setAdjacentTaskFragment(tf1);
tf0.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
tf1.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
task.setBounds(0, 0, 1200, 1000);
tf0.setBounds(0, 0, 600, 1000);
tf1.setBounds(600, 0, 1200, 1000);
final ActivityRecord activity0 = tf0.getTopMostActivity();
final ActivityRecord activity1 = tf1.getTopMostActivity();
// Assert fixed orientation request is ignored for activity in ActivityEmbedding split.
activity0.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE);
assertFalse(activity0.isLetterboxedForFixedOrientationAndAspectRatio());
assertEquals(SCREEN_ORIENTATION_UNSET, task.getOrientation());
activity1.setRequestedOrientation(SCREEN_ORIENTATION_PORTRAIT);
assertFalse(activity1.isLetterboxedForFixedOrientationAndAspectRatio());
assertEquals(SCREEN_ORIENTATION_UNSET, task.getOrientation());
// Also verify the behavior on device that ignore orientation request.
mDisplayContent.setIgnoreOrientationRequest(true);
task.onConfigurationChanged(task.getParent().getConfiguration());
assertFalse(activity0.isLetterboxedForFixedOrientationAndAspectRatio());
assertFalse(activity1.isLetterboxedForFixedOrientationAndAspectRatio());
assertEquals(SCREEN_ORIENTATION_UNSET, task.getOrientation());
tf0.setResumedActivity(activity0, "test");
tf1.setResumedActivity(activity1, "test");
mDisplayContent.mFocusedApp = activity1;
// Making the activity0 be the focused activity and ensure the focused app is updated.
activity0.moveFocusableActivityToTop("test");
assertEquals(activity0, mDisplayContent.mFocusedApp);
}
}