Loading services/core/java/com/android/server/wm/AppTransitionController.java +3 −7 Original line number Diff line number Diff line Loading @@ -562,9 +562,6 @@ public class AppTransitionController { leafTask = null; break; } // The activity may be a child of embedded Task, but we want to find the owner Task. // As a result, find the organized TaskFragment first. final TaskFragment organizedTaskFragment = r.getOrganizedTaskFragment(); // There are also cases where the Task contains non-embedded activity, such as launching // split TaskFragments from a non-embedded activity. // The hierarchy may looks like this: Loading @@ -575,10 +572,9 @@ public class AppTransitionController { // - TaskFragment // - Activity // We also want to have the organizer handle the transition for such case. final Task task = organizedTaskFragment != null ? organizedTaskFragment.getTask() : r.getTask(); if (task == null) { final Task task = r.getTask(); // We don't support embedding in PiP, leave the animation to the PipTaskOrganizer. if (task == null || task.inPinnedWindowingMode()) { leafTask = null; break; } Loading services/core/java/com/android/server/wm/RootWindowContainer.java +10 −6 Original line number Diff line number Diff line Loading @@ -2097,11 +2097,14 @@ class RootWindowContainer extends WindowContainer<DisplayContent> r.setWindowingMode(intermediateWindowingMode); r.mWaitForEnteringPinnedMode = true; rootTask.forAllTaskFragments(tf -> { // When the Task is entering picture-in-picture, we should clear all override from // the client organizer, so the PIP activity can get the correct config from the // Task, and prevent conflict with the PipTaskOrganizer. if (tf.isOrganizedTaskFragment()) { if (!tf.isOrganizedTaskFragment()) { return; } tf.resetAdjacentTaskFragment(); if (tf.getTopNonFinishingActivity() != null) { // When the Task is entering picture-in-picture, we should clear all override // from the client organizer, so the PIP activity can get the correct config // from the Task, and prevent conflict with the PipTaskOrganizer. tf.updateRequestedOverrideConfiguration(EMPTY); } }); Loading @@ -2116,7 +2119,8 @@ class RootWindowContainer extends WindowContainer<DisplayContent> // to the root pinned task r.supportsEnterPipOnTaskSwitch = false; if (organizedTf != null && organizedTf.mClearedTaskFragmentForPip) { if (organizedTf != null && organizedTf.mClearedTaskFragmentForPip && organizedTf.isTaskVisibleRequested()) { // Dispatch the pending info to TaskFragmentOrganizer before PIP animation. // Otherwise, it will keep waiting for the empty TaskFragment to be non-empty. mService.mTaskFragmentOrganizerController.dispatchPendingInfoChangedEvent( Loading services/core/java/com/android/server/wm/TaskFragment.java +34 −2 Original line number Diff line number Diff line Loading @@ -2302,11 +2302,32 @@ class TaskFragment extends WindowContainer<WindowContainer> { return mTaskFragmentOrganizer != null; } /** Whether the Task should be visible. */ boolean isTaskVisibleRequested() { final Task task = getTask(); return task != null && task.isVisibleRequested(); } boolean isReadyToTransit() { // We only wait when this is organized to give the organizer a chance to update. if (!isOrganizedTaskFragment()) { return true; } // We don't want to start the transition if the organized TaskFragment is empty, unless // it is requested to be removed. return !isOrganizedTaskFragment() || getTopNonFinishingActivity() != null || mIsRemovalRequested; if (getTopNonFinishingActivity() != null || mIsRemovalRequested) { return true; } // Organizer shouldn't change embedded TaskFragment in PiP. if (isEmbeddedTaskFragmentInPip()) { return true; } // The TaskFragment becomes empty because the last running activity enters PiP when the Task // is minimized. if (mClearedTaskFragmentForPip && !isTaskVisibleRequested()) { return true; } return false; } /** Clear {@link #mLastPausedActivity} for all {@link TaskFragment} children */ Loading Loading @@ -2424,8 +2445,19 @@ class TaskFragment extends WindowContainer<WindowContainer> { mIsRemovalRequested = false; resetAdjacentTaskFragment(); cleanUp(); final boolean shouldExecuteAppTransition = mClearedTaskFragmentForPip && isTaskVisibleRequested(); super.removeImmediately(); sendTaskFragmentVanished(); if (shouldExecuteAppTransition && mDisplayContent != null) { // When the Task is still visible, and the TaskFragment is removed because the last // running activity is reparenting to PiP, it is possible that no activity is getting // paused or resumed (having an embedded activity in split), thus we need to relayout // and execute it explicitly. mAtmService.addWindowLayoutReasons( ActivityTaskManagerService.LAYOUT_REASON_VISIBILITY_CHANGED); mDisplayContent.executeAppTransition(); } } /** Called on remove to cleanup. */ Loading services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +6 −1 Original line number Diff line number Diff line Loading @@ -518,8 +518,13 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr // longer has activities. As a result, the organizer will never get this info changed event // and will not delete the TaskFragment because the organizer thinks the TaskFragment still // has running activities. // Another case is when an organized TaskFragment became empty because the last running // activity is reparented to a new Task due to enter PiP. We also want to notify the // organizer, so it can remove the empty TaskFragment and update the paired TaskFragment // without causing the extra delay. return event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED && task.topRunningActivity() == null && lastInfo != null && (task.topRunningActivity() == null || info.isTaskFragmentClearedForPip()) && lastInfo != null && lastInfo.getRunningActivityCount() > 0 && info.getRunningActivityCount() == 0; } Loading services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +42 −0 Original line number Diff line number Diff line Loading @@ -19,8 +19,10 @@ 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 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; Loading @@ -30,6 +32,7 @@ import static com.android.server.wm.ActivityRecord.State.RESUMED; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.clearInvocations; import android.content.res.Configuration; Loading Loading @@ -257,6 +260,9 @@ public class TaskFragmentTest extends WindowTestsBase { .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. Loading @@ -269,10 +275,46 @@ public class TaskFragmentTest extends WindowTestsBase { 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); } @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, Loading Loading
services/core/java/com/android/server/wm/AppTransitionController.java +3 −7 Original line number Diff line number Diff line Loading @@ -562,9 +562,6 @@ public class AppTransitionController { leafTask = null; break; } // The activity may be a child of embedded Task, but we want to find the owner Task. // As a result, find the organized TaskFragment first. final TaskFragment organizedTaskFragment = r.getOrganizedTaskFragment(); // There are also cases where the Task contains non-embedded activity, such as launching // split TaskFragments from a non-embedded activity. // The hierarchy may looks like this: Loading @@ -575,10 +572,9 @@ public class AppTransitionController { // - TaskFragment // - Activity // We also want to have the organizer handle the transition for such case. final Task task = organizedTaskFragment != null ? organizedTaskFragment.getTask() : r.getTask(); if (task == null) { final Task task = r.getTask(); // We don't support embedding in PiP, leave the animation to the PipTaskOrganizer. if (task == null || task.inPinnedWindowingMode()) { leafTask = null; break; } Loading
services/core/java/com/android/server/wm/RootWindowContainer.java +10 −6 Original line number Diff line number Diff line Loading @@ -2097,11 +2097,14 @@ class RootWindowContainer extends WindowContainer<DisplayContent> r.setWindowingMode(intermediateWindowingMode); r.mWaitForEnteringPinnedMode = true; rootTask.forAllTaskFragments(tf -> { // When the Task is entering picture-in-picture, we should clear all override from // the client organizer, so the PIP activity can get the correct config from the // Task, and prevent conflict with the PipTaskOrganizer. if (tf.isOrganizedTaskFragment()) { if (!tf.isOrganizedTaskFragment()) { return; } tf.resetAdjacentTaskFragment(); if (tf.getTopNonFinishingActivity() != null) { // When the Task is entering picture-in-picture, we should clear all override // from the client organizer, so the PIP activity can get the correct config // from the Task, and prevent conflict with the PipTaskOrganizer. tf.updateRequestedOverrideConfiguration(EMPTY); } }); Loading @@ -2116,7 +2119,8 @@ class RootWindowContainer extends WindowContainer<DisplayContent> // to the root pinned task r.supportsEnterPipOnTaskSwitch = false; if (organizedTf != null && organizedTf.mClearedTaskFragmentForPip) { if (organizedTf != null && organizedTf.mClearedTaskFragmentForPip && organizedTf.isTaskVisibleRequested()) { // Dispatch the pending info to TaskFragmentOrganizer before PIP animation. // Otherwise, it will keep waiting for the empty TaskFragment to be non-empty. mService.mTaskFragmentOrganizerController.dispatchPendingInfoChangedEvent( Loading
services/core/java/com/android/server/wm/TaskFragment.java +34 −2 Original line number Diff line number Diff line Loading @@ -2302,11 +2302,32 @@ class TaskFragment extends WindowContainer<WindowContainer> { return mTaskFragmentOrganizer != null; } /** Whether the Task should be visible. */ boolean isTaskVisibleRequested() { final Task task = getTask(); return task != null && task.isVisibleRequested(); } boolean isReadyToTransit() { // We only wait when this is organized to give the organizer a chance to update. if (!isOrganizedTaskFragment()) { return true; } // We don't want to start the transition if the organized TaskFragment is empty, unless // it is requested to be removed. return !isOrganizedTaskFragment() || getTopNonFinishingActivity() != null || mIsRemovalRequested; if (getTopNonFinishingActivity() != null || mIsRemovalRequested) { return true; } // Organizer shouldn't change embedded TaskFragment in PiP. if (isEmbeddedTaskFragmentInPip()) { return true; } // The TaskFragment becomes empty because the last running activity enters PiP when the Task // is minimized. if (mClearedTaskFragmentForPip && !isTaskVisibleRequested()) { return true; } return false; } /** Clear {@link #mLastPausedActivity} for all {@link TaskFragment} children */ Loading Loading @@ -2424,8 +2445,19 @@ class TaskFragment extends WindowContainer<WindowContainer> { mIsRemovalRequested = false; resetAdjacentTaskFragment(); cleanUp(); final boolean shouldExecuteAppTransition = mClearedTaskFragmentForPip && isTaskVisibleRequested(); super.removeImmediately(); sendTaskFragmentVanished(); if (shouldExecuteAppTransition && mDisplayContent != null) { // When the Task is still visible, and the TaskFragment is removed because the last // running activity is reparenting to PiP, it is possible that no activity is getting // paused or resumed (having an embedded activity in split), thus we need to relayout // and execute it explicitly. mAtmService.addWindowLayoutReasons( ActivityTaskManagerService.LAYOUT_REASON_VISIBILITY_CHANGED); mDisplayContent.executeAppTransition(); } } /** Called on remove to cleanup. */ Loading
services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +6 −1 Original line number Diff line number Diff line Loading @@ -518,8 +518,13 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr // longer has activities. As a result, the organizer will never get this info changed event // and will not delete the TaskFragment because the organizer thinks the TaskFragment still // has running activities. // Another case is when an organized TaskFragment became empty because the last running // activity is reparented to a new Task due to enter PiP. We also want to notify the // organizer, so it can remove the empty TaskFragment and update the paired TaskFragment // without causing the extra delay. return event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED && task.topRunningActivity() == null && lastInfo != null && (task.topRunningActivity() == null || info.isTaskFragmentClearedForPip()) && lastInfo != null && lastInfo.getRunningActivityCount() > 0 && info.getRunningActivityCount() == 0; } Loading
services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +42 −0 Original line number Diff line number Diff line Loading @@ -19,8 +19,10 @@ 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 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; Loading @@ -30,6 +32,7 @@ import static com.android.server.wm.ActivityRecord.State.RESUMED; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.clearInvocations; import android.content.res.Configuration; Loading Loading @@ -257,6 +260,9 @@ public class TaskFragmentTest extends WindowTestsBase { .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. Loading @@ -269,10 +275,46 @@ public class TaskFragmentTest extends WindowTestsBase { 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); } @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, Loading