diff --git a/core/java/android/window/TaskFragmentCreationParams.java b/core/java/android/window/TaskFragmentCreationParams.java index 81ab7836435d8..c9ddf92d37407 100644 --- a/core/java/android/window/TaskFragmentCreationParams.java +++ b/core/java/android/window/TaskFragmentCreationParams.java @@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.WindowingMode; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.TestApi; import android.graphics.Rect; import android.os.IBinder; @@ -57,14 +58,33 @@ public final class TaskFragmentCreationParams implements Parcelable { /** The initial windowing mode of the TaskFragment. Inherits from parent if not set. */ @WindowingMode - private int mWindowingMode = WINDOWING_MODE_UNDEFINED; + private final int mWindowingMode; + + /** + * The fragment token of the paired primary TaskFragment. + * When it is set, the new TaskFragment will be positioned right above the paired TaskFragment. + * Otherwise, the new TaskFragment will be positioned on the top of the Task by default. + * + * This is different from {@link WindowContainerTransaction#setAdjacentTaskFragments} as we may + * set this when the pair of TaskFragments are stacked, while adjacent is only set on the pair + * of TaskFragments that are in split. + * + * This is needed in case we need to launch a placeholder Activity to split below a transparent + * always-expand Activity. + */ + @Nullable + private final IBinder mPairedPrimaryFragmentToken; private TaskFragmentCreationParams( - @NonNull TaskFragmentOrganizerToken organizer, - @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken) { + @NonNull TaskFragmentOrganizerToken organizer, @NonNull IBinder fragmentToken, + @NonNull IBinder ownerToken, @NonNull Rect initialBounds, + @WindowingMode int windowingMode, @Nullable IBinder pairedPrimaryFragmentToken) { mOrganizer = organizer; mFragmentToken = fragmentToken; mOwnerToken = ownerToken; + mInitialBounds.set(initialBounds); + mWindowingMode = windowingMode; + mPairedPrimaryFragmentToken = pairedPrimaryFragmentToken; } @NonNull @@ -92,12 +112,22 @@ public final class TaskFragmentCreationParams implements Parcelable { return mWindowingMode; } + /** + * TODO(b/232476698): remove the hide with adding CTS for this in next release. + * @hide + */ + @Nullable + public IBinder getPairedPrimaryFragmentToken() { + return mPairedPrimaryFragmentToken; + } + private TaskFragmentCreationParams(Parcel in) { mOrganizer = TaskFragmentOrganizerToken.CREATOR.createFromParcel(in); mFragmentToken = in.readStrongBinder(); mOwnerToken = in.readStrongBinder(); mInitialBounds.readFromParcel(in); mWindowingMode = in.readInt(); + mPairedPrimaryFragmentToken = in.readStrongBinder(); } /** @hide */ @@ -108,6 +138,7 @@ public final class TaskFragmentCreationParams implements Parcelable { dest.writeStrongBinder(mOwnerToken); mInitialBounds.writeToParcel(dest, flags); dest.writeInt(mWindowingMode); + dest.writeStrongBinder(mPairedPrimaryFragmentToken); } @NonNull @@ -132,6 +163,7 @@ public final class TaskFragmentCreationParams implements Parcelable { + " ownerToken=" + mOwnerToken + " initialBounds=" + mInitialBounds + " windowingMode=" + mWindowingMode + + " pairedFragmentToken=" + mPairedPrimaryFragmentToken + "}"; } @@ -159,6 +191,9 @@ public final class TaskFragmentCreationParams implements Parcelable { @WindowingMode private int mWindowingMode = WINDOWING_MODE_UNDEFINED; + @Nullable + private IBinder mPairedPrimaryFragmentToken; + public Builder(@NonNull TaskFragmentOrganizerToken organizer, @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken) { mOrganizer = organizer; @@ -180,14 +215,29 @@ public final class TaskFragmentCreationParams implements Parcelable { return this; } + /** + * Sets the fragment token of the paired primary TaskFragment. + * When it is set, the new TaskFragment will be positioned right above the paired + * TaskFragment. Otherwise, the new TaskFragment will be positioned on the top of the Task + * by default. + * + * This is needed in case we need to launch a placeholder Activity to split below a + * transparent always-expand Activity. + * + * TODO(b/232476698): remove the hide with adding CTS for this in next release. + * @hide + */ + @NonNull + public Builder setPairedPrimaryFragmentToken(@Nullable IBinder fragmentToken) { + mPairedPrimaryFragmentToken = fragmentToken; + return this; + } + /** Constructs the options to create TaskFragment with. */ @NonNull public TaskFragmentCreationParams build() { - final TaskFragmentCreationParams result = new TaskFragmentCreationParams( - mOrganizer, mFragmentToken, mOwnerToken); - result.mInitialBounds.set(mInitialBounds); - result.mWindowingMode = mWindowingMode; - return result; + return new TaskFragmentCreationParams(mOrganizer, mFragmentToken, mOwnerToken, + mInitialBounds, mWindowingMode, mPairedPrimaryFragmentToken); } } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index d7d43aa19757b..b910287aa5358 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -133,8 +133,18 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { } // Create a TaskFragment for the secondary activity. - createTaskFragmentAndStartActivity(wct, secondaryFragmentToken, ownerToken, - secondaryFragmentBounds, windowingMode, activityIntent, + final TaskFragmentCreationParams fragmentOptions = new TaskFragmentCreationParams.Builder( + getOrganizerToken(), secondaryFragmentToken, ownerToken) + .setInitialBounds(secondaryFragmentBounds) + .setWindowingMode(windowingMode) + // Make sure to set the paired fragment token so that the new TaskFragment will be + // positioned right above the paired TaskFragment. + // This is needed in case we need to launch a placeholder Activity to split below a + // transparent always-expand Activity. + .setPairedPrimaryFragmentToken(launchingFragmentToken) + .build(); + createTaskFragment(wct, fragmentOptions); + wct.startActivityInTaskFragment(secondaryFragmentToken, ownerToken, activityIntent, activityOptions); // Set adjacent to each other so that the containers below will be invisible. @@ -173,8 +183,21 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { */ void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) { - final TaskFragmentCreationParams fragmentOptions = - createFragmentOptions(fragmentToken, ownerToken, bounds, windowingMode); + final TaskFragmentCreationParams fragmentOptions = new TaskFragmentCreationParams.Builder( + getOrganizerToken(), fragmentToken, ownerToken) + .setInitialBounds(bounds) + .setWindowingMode(windowingMode) + .build(); + createTaskFragment(wct, fragmentOptions); + } + + void createTaskFragment(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentCreationParams fragmentOptions) { + if (mFragmentInfos.containsKey(fragmentOptions.getFragmentToken())) { + throw new IllegalArgumentException( + "There is an existing TaskFragment with fragmentToken=" + + fragmentOptions.getFragmentToken()); + } wct.createTaskFragment(fragmentOptions); } @@ -189,18 +212,6 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { wct.reparentActivityToTaskFragment(fragmentToken, activity.getActivityToken()); } - /** - * @param ownerToken The token of the activity that creates this task fragment. It does not - * have to be a child of this task fragment, but must belong to the same task. - */ - private void createTaskFragmentAndStartActivity(@NonNull WindowContainerTransaction wct, - @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds, - @WindowingMode int windowingMode, @NonNull Intent activityIntent, - @Nullable Bundle activityOptions) { - createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode); - wct.startActivityInTaskFragment(fragmentToken, ownerToken, activityIntent, activityOptions); - } - void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, @NonNull IBinder primary, @Nullable IBinder secondary, @Nullable SplitRule splitRule) { WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams = null; @@ -238,22 +249,6 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { wct.setCompanionTaskFragment(secondary, finishSecondaryWithPrimary ? primary : null); } - TaskFragmentCreationParams createFragmentOptions(@NonNull IBinder fragmentToken, - @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) { - if (mFragmentInfos.containsKey(fragmentToken)) { - throw new IllegalArgumentException( - "There is an existing TaskFragment with fragmentToken=" + fragmentToken); - } - - return new TaskFragmentCreationParams.Builder( - getOrganizerToken(), - fragmentToken, - ownerToken) - .setInitialBounds(bounds) - .setWindowingMode(windowingMode) - .build(); - } - void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @Nullable Rect bounds) { if (!mFragmentInfos.containsKey(fragmentToken)) { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 6f9a4ff8188a5..d3dc05fb92d4b 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -1218,14 +1218,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity, @NonNull Activity activityInTask, int taskId) { return newContainer(pendingAppearedActivity, null /* pendingAppearedIntent */, - activityInTask, taskId); + activityInTask, taskId, null /* pairedPrimaryContainer */); } @GuardedBy("mLock") TaskFragmentContainer newContainer(@NonNull Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) { return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent, - activityInTask, taskId); + activityInTask, taskId, null /* pairedPrimaryContainer */); } /** @@ -1237,10 +1237,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * @param activityInTask activity in the same Task so that we can get the Task bounds * if needed. * @param taskId parent Task of the new TaskFragment. + * @param pairedPrimaryContainer the paired primary {@link TaskFragmentContainer}. When it is + * set, the new container will be added right above it. */ @GuardedBy("mLock") TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity, - @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) { + @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId, + @Nullable TaskFragmentContainer pairedPrimaryContainer) { if (activityInTask == null) { throw new IllegalArgumentException("activityInTask must not be null,"); } @@ -1249,7 +1252,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } final TaskContainer taskContainer = mTaskContainers.get(taskId); final TaskFragmentContainer container = new TaskFragmentContainer(pendingAppearedActivity, - pendingAppearedIntent, taskContainer, this); + pendingAppearedIntent, taskContainer, this, pairedPrimaryContainer); return container; } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 5395fb2ef5ed1..47253d388f0dd 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -36,6 +36,7 @@ import android.util.Size; import android.view.View; import android.view.WindowInsets; import android.view.WindowMetrics; +import android.window.TaskFragmentCreationParams; import android.window.WindowContainerTransaction; import androidx.annotation.GuardedBy; @@ -307,10 +308,13 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } final int taskId = primaryContainer.getTaskId(); - final TaskFragmentContainer secondaryContainer = mController.newContainer(activityIntent, - launchingActivity, taskId); - final int windowingMode = mController.getTaskContainer(taskId) - .getWindowingModeForSplitTaskFragment(primaryRectBounds); + final TaskFragmentContainer secondaryContainer = mController.newContainer( + null /* pendingAppearedActivity */, activityIntent, launchingActivity, taskId, + // Pass in the primary container to make sure it is added right above the primary. + primaryContainer); + final TaskContainer taskContainer = mController.getTaskContainer(taskId); + final int windowingMode = taskContainer.getWindowingModeForSplitTaskFragment( + primaryRectBounds); mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer, rule, splitAttributes); startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds, @@ -412,17 +416,18 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } @Override - void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, - @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) { - final TaskFragmentContainer container = mController.getContainer(fragmentToken); + void createTaskFragment(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentCreationParams fragmentOptions) { + final TaskFragmentContainer container = mController.getContainer( + fragmentOptions.getFragmentToken()); if (container == null) { throw new IllegalStateException( "Creating a task fragment that is not registered with controller."); } - container.setLastRequestedBounds(bounds); - container.setLastRequestedWindowingMode(windowingMode); - super.createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode); + container.setLastRequestedBounds(fragmentOptions.getInitialBounds()); + container.setLastRequestedWindowingMode(fragmentOptions.getWindowingMode()); + super.createTaskFragment(wct, fragmentOptions); } @Override diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index e31792ad718f1..fcf0ac78af387 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -118,10 +118,12 @@ class TaskFragmentContainer { /** * Creates a container with an existing activity that will be re-parented to it in a window * container transaction. + * @param pairedPrimaryContainer when it is set, the new container will be add right above it */ TaskFragmentContainer(@Nullable Activity pendingAppearedActivity, @Nullable Intent pendingAppearedIntent, @NonNull TaskContainer taskContainer, - @NonNull SplitController controller) { + @NonNull SplitController controller, + @Nullable TaskFragmentContainer pairedPrimaryContainer) { if ((pendingAppearedActivity == null && pendingAppearedIntent == null) || (pendingAppearedActivity != null && pendingAppearedIntent != null)) { throw new IllegalArgumentException( @@ -130,7 +132,16 @@ class TaskFragmentContainer { mController = controller; mToken = new Binder("TaskFragmentContainer"); mTaskContainer = taskContainer; - taskContainer.mContainers.add(this); + if (pairedPrimaryContainer != null) { + if (pairedPrimaryContainer.getTaskContainer() != taskContainer) { + throw new IllegalArgumentException( + "pairedPrimaryContainer must be in the same Task"); + } + final int primaryIndex = taskContainer.mContainers.indexOf(pairedPrimaryContainer); + taskContainer.mContainers.add(primaryIndex + 1, this); + } else { + taskContainer.mContainers.add(this); + } if (pendingAppearedActivity != null) { addPendingAppearedActivity(pendingAppearedActivity); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java index 31aa09c902b14..bbb454d31c388 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java @@ -102,7 +102,7 @@ public class JetpackTaskFragmentOrganizerTest { public void testExpandTaskFragment() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - new Intent(), taskContainer, mSplitController); + new Intent(), taskContainer, mSplitController, null /* pairedPrimaryContainer */); final TaskFragmentInfo info = createMockInfo(container); mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info); container.setInfo(mTransaction, info); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 221c7640ef0b5..6725dfdb94e81 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -169,7 +169,7 @@ public class SplitControllerTest { final TaskContainer taskContainer = createTestTaskContainer(); // tf1 has no running activity so is not active. final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, - new Intent(), taskContainer, mSplitController); + new Intent(), taskContainer, mSplitController, null /* pairedPrimaryContainer */); // tf2 has running activity so is active. final TaskFragmentContainer tf2 = mock(TaskFragmentContainer.class); doReturn(1).when(tf2).getRunningActivityCount(); @@ -375,7 +375,7 @@ public class SplitControllerTest { final Intent intent = new Intent(); final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - intent, taskContainer, mSplitController); + intent, taskContainer, mSplitController, null /* pairedPrimaryContainer */); final SplitController.ActivityStartMonitor monitor = mSplitController.getActivityStartMonitor(); @@ -609,7 +609,7 @@ public class SplitControllerTest { false /* isOnReparent */); assertFalse(result); - verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); + verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any()); } @Test @@ -771,7 +771,7 @@ public class SplitControllerTest { false /* isOnReparent */); assertTrue(result); - verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); + verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any()); verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any()); } @@ -813,7 +813,7 @@ public class SplitControllerTest { false /* isOnReparent */); assertTrue(result); - verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); + verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any()); verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any()); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java index 95328ce700e30..13e709271221b 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java @@ -122,7 +122,7 @@ public class TaskContainerTest { assertTrue(taskContainer.isEmpty()); final TaskFragmentContainer tf = new TaskFragmentContainer(null /* activity */, - new Intent(), taskContainer, mController); + new Intent(), taskContainer, mController, null /* pairedPrimaryContainer */); assertFalse(taskContainer.isEmpty()); @@ -138,11 +138,11 @@ public class TaskContainerTest { assertNull(taskContainer.getTopTaskFragmentContainer()); final TaskFragmentContainer tf0 = new TaskFragmentContainer(null /* activity */, - new Intent(), taskContainer, mController); + new Intent(), taskContainer, mController, null /* pairedPrimaryContainer */); assertEquals(tf0, taskContainer.getTopTaskFragmentContainer()); final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, - new Intent(), taskContainer, mController); + new Intent(), taskContainer, mController, null /* pairedPrimaryContainer */); assertEquals(tf1, taskContainer.getTopTaskFragmentContainer()); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java index 99f56b4ceb172..5c3ba72e23619 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java @@ -94,18 +94,21 @@ public class TaskFragmentContainerTest { // One of the activity and the intent must be non-null assertThrows(IllegalArgumentException.class, - () -> new TaskFragmentContainer(null, null, taskContainer, mController)); + () -> new TaskFragmentContainer(null, null, taskContainer, mController, + null /* pairedPrimaryContainer */)); // One of the activity and the intent must be null. assertThrows(IllegalArgumentException.class, - () -> new TaskFragmentContainer(mActivity, mIntent, taskContainer, mController)); + () -> new TaskFragmentContainer(mActivity, mIntent, taskContainer, mController, + null /* pairedPrimaryContainer */)); } @Test public void testFinish() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(mActivity, - null /* pendingAppearedIntent */, taskContainer, mController); + null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryContainer */); doReturn(container).when(mController).getContainerWithActivity(mActivity); // Only remove the activity, but not clear the reference until appeared. @@ -137,12 +140,14 @@ public class TaskFragmentContainerTest { public void testFinish_notFinishActivityThatIsReparenting() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity, - null /* pendingAppearedIntent */, taskContainer, mController); + null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryContainer */); final TaskFragmentInfo info = createMockTaskFragmentInfo(container0, mActivity); container0.setInfo(mTransaction, info); // Request to reparent the activity to a new TaskFragment. final TaskFragmentContainer container1 = new TaskFragmentContainer(mActivity, - null /* pendingAppearedIntent */, taskContainer, mController); + null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryContainer */); doReturn(container1).when(mController).getContainerWithActivity(mActivity); final WindowContainerTransaction wct = new WindowContainerTransaction(); @@ -159,7 +164,8 @@ public class TaskFragmentContainerTest { final TaskContainer taskContainer = createTestTaskContainer(); // Pending activity should be cleared when it has appeared on server side. final TaskFragmentContainer pendingActivityContainer = new TaskFragmentContainer(mActivity, - null /* pendingAppearedIntent */, taskContainer, mController); + null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryContainer */); assertTrue(pendingActivityContainer.mPendingAppearedActivities.contains( mActivity.getActivityToken())); @@ -172,7 +178,8 @@ public class TaskFragmentContainerTest { // Pending intent should be cleared when the container becomes non-empty. final TaskFragmentContainer pendingIntentContainer = new TaskFragmentContainer( - null /* pendingAppearedActivity */, mIntent, taskContainer, mController); + null /* pendingAppearedActivity */, mIntent, taskContainer, mController, + null /* pairedPrimaryContainer */); assertEquals(mIntent, pendingIntentContainer.getPendingAppearedIntent()); @@ -187,7 +194,7 @@ public class TaskFragmentContainerTest { public void testIsWaitingActivityAppear() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); assertTrue(container.isWaitingActivityAppear()); @@ -209,7 +216,7 @@ public class TaskFragmentContainerTest { doNothing().when(mController).onTaskFragmentAppearEmptyTimeout(any(), any()); final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); assertNull(container.mAppearEmptyTimeout); @@ -249,7 +256,7 @@ public class TaskFragmentContainerTest { public void testCollectNonFinishingActivities() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); List activities = container.collectNonFinishingActivities(); assertTrue(activities.isEmpty()); @@ -277,7 +284,7 @@ public class TaskFragmentContainerTest { public void testAddPendingActivity() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); container.addPendingAppearedActivity(mActivity); assertEquals(1, container.collectNonFinishingActivities().size()); @@ -291,9 +298,9 @@ public class TaskFragmentContainerTest { public void testIsAbove() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container0 = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); final TaskFragmentContainer container1 = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); assertTrue(container1.isAbove(container0)); assertFalse(container0.isAbove(container1)); @@ -303,7 +310,7 @@ public class TaskFragmentContainerTest { public void testGetBottomMostActivity() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); container.addPendingAppearedActivity(mActivity); assertEquals(mActivity, container.getBottomMostActivity()); @@ -320,7 +327,7 @@ public class TaskFragmentContainerTest { public void testOnActivityDestroyed() { final TaskContainer taskContainer = createTestTaskContainer(mController); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); container.addPendingAppearedActivity(mActivity); final List activities = new ArrayList<>(); activities.add(mActivity.getActivityToken()); @@ -340,7 +347,7 @@ public class TaskFragmentContainerTest { // True if no info set. final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); spyOn(taskContainer); doReturn(true).when(taskContainer).isVisible(); @@ -403,7 +410,7 @@ public class TaskFragmentContainerTest { public void testHasAppearedActivity() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); container.addPendingAppearedActivity(mActivity); assertFalse(container.hasAppearedActivity(mActivity.getActivityToken())); @@ -420,7 +427,7 @@ public class TaskFragmentContainerTest { public void testHasPendingAppearedActivity() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); container.addPendingAppearedActivity(mActivity); assertTrue(container.hasPendingAppearedActivity(mActivity.getActivityToken())); @@ -437,9 +444,9 @@ public class TaskFragmentContainerTest { public void testHasActivity() { final TaskContainer taskContainer = createTestTaskContainer(mController); final TaskFragmentContainer container1 = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); final TaskFragmentContainer container2 = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); // Activity is pending appeared on container2. container2.addPendingAppearedActivity(mActivity); @@ -472,6 +479,27 @@ public class TaskFragmentContainerTest { assertTrue(container2.hasActivity(mActivity.getActivityToken())); } + @Test + public void testNewContainerWithPairedPrimaryContainer() { + final TaskContainer taskContainer = createTestTaskContainer(); + final TaskFragmentContainer tf0 = new TaskFragmentContainer( + null /* pendingAppearedActivity */, new Intent(), taskContainer, mController, + null /* pairedPrimaryTaskFragment */); + final TaskFragmentContainer tf1 = new TaskFragmentContainer( + null /* pendingAppearedActivity */, new Intent(), taskContainer, mController, + null /* pairedPrimaryTaskFragment */); + taskContainer.mContainers.add(tf0); + taskContainer.mContainers.add(tf1); + + // When tf2 is created with using tf0 as pairedPrimaryContainer, tf2 should be inserted + // right above tf0. + final TaskFragmentContainer tf2 = new TaskFragmentContainer( + null /* pendingAppearedActivity */, new Intent(), taskContainer, mController, tf0); + assertEquals(0, taskContainer.indexOf(tf0)); + assertEquals(1, taskContainer.indexOf(tf2)); + assertEquals(2, taskContainer.indexOf(tf1)); + } + /** Creates a mock activity in the organizer process. */ private Activity createMockActivity() { final Activity activity = mock(Activity.class); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index dc1d50c435b91..cd777092d4387 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -1891,7 +1891,18 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub // actions. taskFragment.setTaskFragmentOrganizer(creationParams.getOrganizer(), ownerActivity.getUid(), ownerActivity.info.processName); - ownerTask.addChild(taskFragment, POSITION_TOP); + final int position; + if (creationParams.getPairedPrimaryFragmentToken() != null) { + // When there is a paired primary TaskFragment, we want to place the new TaskFragment + // right above the paired one to make sure there is no other window in between. + final TaskFragment pairedPrimaryTaskFragment = getTaskFragment( + creationParams.getPairedPrimaryFragmentToken()); + final int pairedPosition = ownerTask.mChildren.indexOf(pairedPrimaryTaskFragment); + position = pairedPosition != -1 ? pairedPosition + 1 : POSITION_TOP; + } else { + position = POSITION_TOP; + } + ownerTask.addChild(taskFragment, position); taskFragment.setWindowingMode(creationParams.getWindowingMode()); taskFragment.setBounds(creationParams.getInitialBounds()); mLaunchTaskFragments.put(creationParams.getFragmentToken(), taskFragment); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index 2b493145f854b..6cb3450ee4701 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -552,10 +552,9 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { @Test public void testApplyTransaction_enforceHierarchyChange_createTaskFragment() { final ActivityRecord ownerActivity = createActivityRecord(mDisplayContent); - final IBinder fragmentToken = new Binder(); // Allow organizer to create TaskFragment and start/reparent activity to TaskFragment. - createTaskFragmentFromOrganizer(mTransaction, ownerActivity, fragmentToken); + createTaskFragmentFromOrganizer(mTransaction, ownerActivity, mFragmentToken); mTransaction.startActivityInTaskFragment( mFragmentToken, null /* callerToken */, new Intent(), null /* activityOptions */); mTransaction.reparentActivityToTaskFragment(mFragmentToken, mock(IBinder.class)); @@ -564,7 +563,8 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { assertApplyTransactionAllowed(mTransaction); // Successfully created a TaskFragment. - final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment(fragmentToken); + final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment( + mFragmentToken); assertNotNull(taskFragment); assertEquals(ownerActivity.getTask(), taskFragment.getTask()); } @@ -702,6 +702,40 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { assertNotNull(mWindowOrganizerController.getTaskFragment(fragmentToken)); } + @Test + public void testApplyTransaction_createTaskFragment_withPairedPrimaryFragmentToken() { + final Task task = createTask(mDisplayContent); + mTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .setFragmentToken(mFragmentToken) + .createActivityCount(1) + .build(); + mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); + final ActivityRecord activityOnTop = createActivityRecord(task); + final int uid = Binder.getCallingUid(); + activityOnTop.info.applicationInfo.uid = uid; + activityOnTop.getTask().effectiveUid = uid; + final IBinder fragmentToken1 = new Binder(); + final TaskFragmentCreationParams params = new TaskFragmentCreationParams.Builder( + mOrganizerToken, fragmentToken1, activityOnTop.token) + .setPairedPrimaryFragmentToken(mFragmentToken) + .build(); + mTransaction.setTaskFragmentOrganizer(mIOrganizer); + mTransaction.createTaskFragment(params); + assertApplyTransactionAllowed(mTransaction); + + // Successfully created a TaskFragment. + final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment( + fragmentToken1); + assertNotNull(taskFragment); + // The new TaskFragment should be positioned right above the paired TaskFragment. + assertEquals(task.mChildren.indexOf(mTaskFragment) + 1, + task.mChildren.indexOf(taskFragment)); + // The top activity should remain on top. + assertEquals(task.mChildren.indexOf(taskFragment) + 1, + task.mChildren.indexOf(activityOnTop)); + } + @Test public void testApplyTransaction_enforceHierarchyChange_reparentChildren() { doReturn(true).when(mTaskFragment).isAttached();