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 9713c2735a9cb..1cd422087ccbd 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -234,13 +234,36 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent, @NonNull IBinder activityToken) { // If the activity belongs to the current app process, we treat it as a new activity launch. - final Activity activity = ActivityThread.currentActivityThread().getActivity(activityToken); + final Activity activity = getActivity(activityToken); if (activity != null) { onActivityCreated(activity); - updateCallbackIfNecessary(); return; } - // TODO: handle for activity in other process. + + final TaskContainer taskContainer = getTaskContainer(taskId); + if (taskContainer == null || taskContainer.isInPictureInPicture()) { + // We don't embed activity when it is in PIP. + return; + } + + // If the activity belongs to a different app process, we treat it as starting new intent, + // since both actions might result in a new activity that should appear in an organized + // TaskFragment. + final WindowContainerTransaction wct = new WindowContainerTransaction(); + TaskFragmentContainer targetContainer = resolveStartActivityIntent(wct, taskId, + activityIntent, null /* launchingActivity */); + if (targetContainer == null) { + // When there is no split rule matched, try to place it in the top container like a + // normal launch. + targetContainer = taskContainer.getTopTaskFragmentContainer(); + } + if (targetContainer == null) { + return; + } + wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(), activityToken); + mPresenter.applyTransaction(wct); + // Because the activity does not belong to the organizer process, we wait until + // onTaskFragmentAppeared to trigger updateCallbackIfNecessary(). } /** Called on receiving {@link #onTaskFragmentVanished(TaskFragmentInfo)} for cleanup. */ @@ -327,16 +350,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ // TODO(b/190433398): Break down into smaller functions. void handleActivityCreated(@NonNull Activity launchedActivity) { - if (isInPictureInPicture(launchedActivity)) { - // We don't embed activity when it is in PIP. + if (isInPictureInPicture(launchedActivity) || launchedActivity.isFinishing()) { + // We don't embed activity when it is in PIP, or finishing. return; } - final List splitRules = getSplitRules(); - final TaskFragmentContainer currentContainer = getContainerWithActivity( - launchedActivity.getActivityToken()); + final TaskFragmentContainer currentContainer = getContainerWithActivity(launchedActivity); // Check if the activity is configured to always be expanded. - if (shouldExpand(launchedActivity, null, splitRules)) { + if (shouldExpand(launchedActivity, null /* intent */)) { if (shouldContainerBeExpanded(currentContainer)) { // Make sure that the existing container is expanded mPresenter.expandTaskFragment(currentContainer.getTaskFragmentToken()); @@ -375,7 +396,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen IBinder belowToken = ActivityClient.getInstance().getActivityTokenBelow( launchedActivity.getActivityToken()); if (belowToken != null) { - activityBelow = ActivityThread.currentActivityThread().getActivity(belowToken); + activityBelow = getActivity(belowToken); } } if (activityBelow == null) { @@ -384,7 +405,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Check if the split is already set. final TaskFragmentContainer activityBelowContainer = getContainerWithActivity( - activityBelow.getActivityToken()); + activityBelow); if (currentContainer != null && activityBelowContainer != null) { final SplitContainer existingSplit = getActiveSplitForContainers(currentContainer, activityBelowContainer); @@ -394,8 +415,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } } - final SplitPairRule splitPairRule = getSplitRule(activityBelow, launchedActivity, - splitRules); + final SplitPairRule splitPairRule = getSplitRule(activityBelow, launchedActivity); if (splitPairRule == null) { return; } @@ -409,8 +429,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // We don't embed activity when it is in PIP. return; } - final TaskFragmentContainer currentContainer = getContainerWithActivity( - activity.getActivityToken()); + final TaskFragmentContainer currentContainer = getContainerWithActivity(activity); if (currentContainer != null) { // Changes to activities in controllers are handled in @@ -442,12 +461,144 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen mPresenter.cleanupContainer(container, false /* shouldFinishDependent */); } + /** + * When we are trying to handle a new activity Intent, returns the {@link TaskFragmentContainer} + * that we should reparent the new activity to if there is any embedding rule matched. + * + * @param wct {@link WindowContainerTransaction} including all the window change + * requests. The caller is responsible to call + * {@link android.window.TaskFragmentOrganizer#applyTransaction}. + * @param taskId The Task to start the activity in. + * @param intent The {@link Intent} for starting the new launched activity. + * @param launchingActivity The {@link Activity} that starts the new activity. We will + * prioritize to split the new activity with it if it is not + * {@code null}. + * @return the {@link TaskFragmentContainer} to start the new activity in. {@code null} if there + * is no embedding rule matched. + */ + @VisibleForTesting + @Nullable + TaskFragmentContainer resolveStartActivityIntent(@NonNull WindowContainerTransaction wct, + int taskId, @NonNull Intent intent, @Nullable Activity launchingActivity) { + /* + * We will check the following to see if there is any embedding rule matched: + * 1. Whether the new activity intent should always expand. + * 2. Whether the launching activity (if set) should be split with the new activity intent. + * 3. Whether the top activity (if any) should be split with the new activity intent. + * 4. Whether the top activity (if any) in other split should be split with the new + * activity intent. + */ + + // 1. Whether the new activity intent should always expand. + if (shouldExpand(null /* activity */, intent)) { + return createEmptyExpandedContainer(wct, taskId, launchingActivity); + } + + // 2. Whether the launching activity (if set) should be split with the new activity intent. + if (launchingActivity != null) { + final TaskFragmentContainer container = getSecondaryContainerForSplitIfAny(wct, + launchingActivity, intent, true /* respectClearTop */); + if (container != null) { + return container; + } + } + + // 3. Whether the top activity (if any) should be split with the new activity intent. + final TaskContainer taskContainer = getTaskContainer(taskId); + if (taskContainer == null || taskContainer.getTopTaskFragmentContainer() == null) { + // There is no other activity in the Task to check split with. + return null; + } + final TaskFragmentContainer topContainer = taskContainer.getTopTaskFragmentContainer(); + final Activity topActivity = topContainer.getTopNonFinishingActivity(); + if (topActivity != null && topActivity != launchingActivity) { + final TaskFragmentContainer container = getSecondaryContainerForSplitIfAny(wct, + topActivity, intent, false /* respectClearTop */); + if (container != null) { + return container; + } + } + + // 4. Whether the top activity (if any) in other split should be split with the new + // activity intent. + final SplitContainer topSplit = getActiveSplitForContainer(topContainer); + if (topSplit == null) { + return null; + } + final TaskFragmentContainer otherTopContainer = + topSplit.getPrimaryContainer() == topContainer + ? topSplit.getSecondaryContainer() + : topSplit.getPrimaryContainer(); + final Activity otherTopActivity = otherTopContainer.getTopNonFinishingActivity(); + if (otherTopActivity != null && otherTopActivity != launchingActivity) { + return getSecondaryContainerForSplitIfAny(wct, otherTopActivity, intent, + false /* respectClearTop */); + } + return null; + } + + /** + * Returns an empty expanded {@link TaskFragmentContainer} that we can launch an activity into. + */ + @Nullable + private TaskFragmentContainer createEmptyExpandedContainer( + @NonNull WindowContainerTransaction wct, int taskId, + @Nullable Activity launchingActivity) { + // We need an activity in the organizer process in the same Task to use as the owner + // activity, as well as to get the Task window info. + final Activity activityInTask; + if (launchingActivity != null) { + activityInTask = launchingActivity; + } else { + final TaskContainer taskContainer = getTaskContainer(taskId); + activityInTask = taskContainer != null + ? taskContainer.getTopNonFinishingActivity() + : null; + } + if (activityInTask == null) { + // Can't find any activity in the Task that we can use as the owner activity. + return null; + } + final TaskFragmentContainer expandedContainer = newContainer(null /* activity */, + activityInTask, taskId); + mPresenter.createTaskFragment(wct, expandedContainer.getTaskFragmentToken(), + activityInTask.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED); + return expandedContainer; + } + + /** + * Returns a container for the new activity intent to launch into as splitting with the primary + * activity. + */ + @Nullable + private TaskFragmentContainer getSecondaryContainerForSplitIfAny( + @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, + @NonNull Intent intent, boolean respectClearTop) { + final SplitPairRule splitRule = getSplitRule(primaryActivity, intent); + if (splitRule == null) { + return null; + } + final TaskFragmentContainer existingContainer = getContainerWithActivity(primaryActivity); + final SplitContainer splitContainer = getActiveSplitForContainer(existingContainer); + if (splitContainer != null && existingContainer == splitContainer.getPrimaryContainer() + && (canReuseContainer(splitRule, splitContainer.getSplitRule()) + // TODO(b/231845476) we should always respect clearTop. + || !respectClearTop)) { + // Can launch in the existing secondary container if the rules share the same + // presentation. + return splitContainer.getSecondaryContainer(); + } + // Create a new TaskFragment to split with the primary activity for the new activity. + return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, splitRule); + } + /** * Returns a container that this activity is registered with. An activity can only belong to one * container, or no container at all. */ @Nullable - TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) { + TaskFragmentContainer getContainerWithActivity(@NonNull Activity activity) { + final IBinder activityToken = activity.getActivityToken(); for (int i = mTaskContainers.size() - 1; i >= 0; i--) { final List containers = mTaskContainers.valueAt(i).mContainers; for (TaskFragmentContainer container : containers) { @@ -668,7 +819,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns the top active split container that has the provided container, if available. */ @Nullable - private SplitContainer getActiveSplitForContainer(@NonNull TaskFragmentContainer container) { + private SplitContainer getActiveSplitForContainer(@Nullable TaskFragmentContainer container) { + if (container == null) { + return null; + } final List splitContainers = container.getTaskContainer().mSplitContainers; if (splitContainers.isEmpty()) { return null; @@ -687,8 +841,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns the active split that has the provided containers as primary and secondary or as * secondary and primary, if available. */ + @VisibleForTesting @Nullable - private SplitContainer getActiveSplitForContainers( + SplitContainer getActiveSplitForContainers( @NonNull TaskFragmentContainer firstContainer, @NonNull TaskFragmentContainer secondContainer) { final List splitContainers = firstContainer.getTaskContainer() @@ -718,15 +873,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } boolean launchPlaceholderIfNecessary(@NonNull Activity activity) { - final TaskFragmentContainer container = getContainerWithActivity( - activity.getActivityToken()); + final TaskFragmentContainer container = getContainerWithActivity(activity); // Don't launch placeholder if the container is occluded. if (container != null && container != getTopActiveContainer(container.getTaskId())) { return false; } - SplitContainer splitContainer = container != null ? getActiveSplitForContainer(container) - : null; + final SplitContainer splitContainer = getActiveSplitForContainer(container); if (splitContainer != null && container.equals(splitContainer.getPrimaryContainer())) { // Don't launch placeholder in primary split container return false; @@ -876,9 +1029,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * if available. */ @Nullable - private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity, - @NonNull Intent secondaryActivityIntent, @NonNull List splitRules) { - for (EmbeddingRule rule : splitRules) { + private SplitPairRule getSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryActivityIntent) { + for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof SplitPairRule)) { continue; } @@ -894,9 +1047,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns a split rule for the provided pair of primary and secondary activities if available. */ @Nullable - private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity, - @NonNull Activity secondaryActivity, @NonNull List splitRules) { - for (EmbeddingRule rule : splitRules) { + private SplitPairRule getSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof SplitPairRule)) { continue; } @@ -933,16 +1086,24 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return mHandler; } + int getTaskId(@NonNull Activity activity) { + // Prefer to get the taskId from TaskFragmentContainer because Activity.getTaskId() is an + // IPC call. + final TaskFragmentContainer container = getContainerWithActivity(activity); + return container != null ? container.getTaskId() : activity.getTaskId(); + } + + @Nullable + Activity getActivity(@NonNull IBinder activityToken) { + return ActivityThread.currentActivityThread().getActivity(activityToken); + } + /** * Returns {@code true} if an Activity with the provided component name should always be * expanded to occupy full task bounds. Such activity must not be put in a split. */ - private static boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent, - List splitRules) { - if (splitRules == null) { - return false; - } - for (EmbeddingRule rule : splitRules) { + private boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent) { + for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof ActivityRule)) { continue; } @@ -996,8 +1157,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ boolean shouldRetainAssociatedActivity(@NonNull TaskFragmentContainer finishingContainer, @NonNull Activity associatedActivity) { - TaskFragmentContainer associatedContainer = getContainerWithActivity( - associatedActivity.getActivityToken()); + final TaskFragmentContainer associatedContainer = getContainerWithActivity( + associatedActivity); if (associatedContainer == null) { return false; } @@ -1085,130 +1246,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return super.onStartActivity(who, intent, options); } - if (shouldExpand(null, intent, getSplitRules())) { - setLaunchingInExpandedContainer(launchingActivity, options); - } else if (!splitWithLaunchingActivity(launchingActivity, intent, options)) { - setLaunchingInSameSideContainer(launchingActivity, intent, options); + final int taskId = getTaskId(launchingActivity); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + final TaskFragmentContainer launchedInTaskFragment = resolveStartActivityIntent(wct, + taskId, intent, launchingActivity); + if (launchedInTaskFragment != null) { + mPresenter.applyTransaction(wct); + // Amend the request to let the WM know that the activity should be placed in the + // dedicated container. + options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, + launchedInTaskFragment.getTaskFragmentToken()); } return super.onStartActivity(who, intent, options); } - - private void setLaunchingInExpandedContainer(Activity launchingActivity, Bundle options) { - TaskFragmentContainer newContainer = mPresenter.createNewExpandedContainer( - launchingActivity); - - // Amend the request to let the WM know that the activity should be placed in the - // dedicated container. - options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, - newContainer.getTaskFragmentToken()); - } - - /** - * Returns {@code true} if the activity that is going to be started via the - * {@code intent} should be paired with the {@code launchingActivity} and is set to be - * launched in the side container. - */ - private boolean splitWithLaunchingActivity(Activity launchingActivity, Intent intent, - Bundle options) { - final SplitPairRule splitPairRule = getSplitRule(launchingActivity, intent, - getSplitRules()); - if (splitPairRule == null) { - return false; - } - - // Check if there is any existing side container to launch into. - TaskFragmentContainer secondaryContainer = findSideContainerForNewLaunch( - launchingActivity, splitPairRule); - if (secondaryContainer == null) { - // Create a new split with an empty side container. - secondaryContainer = mPresenter - .createNewSplitWithEmptySideContainer(launchingActivity, splitPairRule); - } - - // Amend the request to let the WM know that the activity should be placed in the - // dedicated container. - options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, - secondaryContainer.getTaskFragmentToken()); - return true; - } - - /** - * Finds if there is an existing split side {@link TaskFragmentContainer} that can be used - * for the new rule. - */ - @Nullable - private TaskFragmentContainer findSideContainerForNewLaunch(Activity launchingActivity, - SplitPairRule splitPairRule) { - final TaskFragmentContainer launchingContainer = getContainerWithActivity( - launchingActivity.getActivityToken()); - if (launchingContainer == null) { - return null; - } - - // We only check if the launching activity is the primary of the split. We will check - // if the launching activity is the secondary in #setLaunchingInSameSideContainer. - final SplitContainer splitContainer = getActiveSplitForContainer(launchingContainer); - if (splitContainer == null - || splitContainer.getPrimaryContainer() != launchingContainer) { - return null; - } - - if (canReuseContainer(splitPairRule, splitContainer.getSplitRule())) { - return splitContainer.getSecondaryContainer(); - } - return null; - } - - /** - * Checks if the activity that is going to be started via the {@code intent} should be - * paired with the existing top activity which is currently paired with the - * {@code launchingActivity}. If so, set the activity to be launched in the same side - * container of the {@code launchingActivity}. - */ - private void setLaunchingInSameSideContainer(Activity launchingActivity, Intent intent, - Bundle options) { - final TaskFragmentContainer launchingContainer = getContainerWithActivity( - launchingActivity.getActivityToken()); - if (launchingContainer == null) { - return; - } - - final SplitContainer splitContainer = getActiveSplitForContainer(launchingContainer); - if (splitContainer == null) { - return; - } - - if (splitContainer.getSecondaryContainer() != launchingContainer) { - return; - } - - // The launching activity is on the secondary container. Retrieve the primary - // activity from the other container. - Activity primaryActivity = - splitContainer.getPrimaryContainer().getTopNonFinishingActivity(); - if (primaryActivity == null) { - return; - } - - final SplitPairRule splitPairRule = getSplitRule(primaryActivity, intent, - getSplitRules()); - if (splitPairRule == null) { - return; - } - - // Can only launch in the same container if the rules share the same presentation. - if (!canReuseContainer(splitPairRule, splitContainer.getSplitRule())) { - return; - } - - // Amend the request to let the WM know that the activity should be placed in the - // dedicated container. This is necessary for the case that the activity is started - // into a new Task, or new Task will be escaped from the current host Task and be - // displayed in fullscreen. - options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, - launchingContainer.getTaskFragmentToken()); - } } /** @@ -1228,8 +1279,15 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (!isContainerReusableRule(rule1) || !isContainerReusableRule(rule2)) { return false; } - return rule1.getSplitRatio() == rule2.getSplitRatio() - && rule1.getLayoutDirection() == rule2.getLayoutDirection(); + final SplitPairRule pairRule1 = (SplitPairRule) rule1; + final SplitPairRule pairRule2 = (SplitPairRule) rule2; + // TODO(b/231655482): add util method to do the comparison in SplitPairRule. + return pairRule1.getSplitRatio() == pairRule2.getSplitRatio() + && pairRule1.getLayoutDirection() == pairRule2.getLayoutDirection() + && pairRule1.getFinishPrimaryWithSecondary() + == pairRule2.getFinishPrimaryWithSecondary() + && pairRule1.getFinishSecondaryWithPrimary() + == pairRule2.getFinishSecondaryWithPrimary(); } /** 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 b32f4fa679060..43d0402c1525b 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -16,8 +16,6 @@ package androidx.window.extensions.embedding; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; - import android.app.Activity; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; @@ -100,10 +98,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * Creates a new split with the primary activity and an empty secondary container. * @return The newly created secondary container. */ - TaskFragmentContainer createNewSplitWithEmptySideContainer(@NonNull Activity primaryActivity, + @NonNull + TaskFragmentContainer createNewSplitWithEmptySideContainer( + @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, @NonNull SplitPairRule rule) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - final Rect parentBounds = getParentContainerBounds(primaryActivity); final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, isLtr(primaryActivity, rule)); @@ -127,8 +125,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); - applyTransaction(wct); - return secondaryContainer; } @@ -155,8 +151,15 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, isLtr(primaryActivity, rule)); + final TaskFragmentContainer curSecondaryContainer = mController.getContainerWithActivity( + secondaryActivity); + TaskFragmentContainer containerToAvoid = primaryContainer; + if (rule.shouldClearTop() && curSecondaryContainer != null) { + // Do not reuse the current TaskFragment if the rule is to clear top. + containerToAvoid = curSecondaryContainer; + } final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct, - secondaryActivity, secondaryRectBounds, primaryContainer); + secondaryActivity, secondaryRectBounds, containerToAvoid); // Set adjacent to each other so that the containers below will be invisible. setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule); @@ -166,21 +169,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { applyTransaction(wct); } - /** - * Creates a new expanded container. - */ - TaskFragmentContainer createNewExpandedContainer(@NonNull Activity launchingActivity) { - final TaskFragmentContainer newContainer = mController.newContainer(null /* activity */, - launchingActivity, launchingActivity.getTaskId()); - - final WindowContainerTransaction wct = new WindowContainerTransaction(); - createTaskFragment(wct, newContainer.getTaskFragmentToken(), - launchingActivity.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED); - - applyTransaction(wct); - return newContainer; - } - /** * Creates a new container or resizes an existing container for activity to the provided bounds. * @param activity The activity to be re-parented to the container if necessary. @@ -189,8 +177,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { private TaskFragmentContainer prepareContainerForActivity( @NonNull WindowContainerTransaction wct, @NonNull Activity activity, @NonNull Rect bounds, @Nullable TaskFragmentContainer containerToAvoid) { - TaskFragmentContainer container = mController.getContainerWithActivity( - activity.getActivityToken()); + TaskFragmentContainer container = mController.getContainerWithActivity(activity); final int taskId = container != null ? container.getTaskId() : activity.getTaskId(); if (container == null || container == containerToAvoid) { container = mController.newContainer(activity, taskId); @@ -230,7 +217,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { isLtr(launchingActivity, rule)); TaskFragmentContainer primaryContainer = mController.getContainerWithActivity( - launchingActivity.getActivityToken()); + launchingActivity); if (primaryContainer == null) { primaryContainer = mController.newContainer(launchingActivity, launchingActivity.getTaskId()); @@ -460,8 +447,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @NonNull Rect getParentContainerBounds(@NonNull Activity activity) { - final TaskFragmentContainer container = mController.getContainerWithActivity( - activity.getActivityToken()); + final TaskFragmentContainer container = mController.getContainerWithActivity(activity); if (container != null) { return getParentContainerBounds(container); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index dba71ef219469..0ea5603b1f3d9 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -142,4 +142,23 @@ class TaskContainer { container.removePendingAppearedActivity(pendingAppearedActivity); } } + + @Nullable + TaskFragmentContainer getTopTaskFragmentContainer() { + if (mContainers.isEmpty()) { + return null; + } + return mContainers.get(mContainers.size() - 1); + } + + @Nullable + Activity getTopNonFinishingActivity() { + for (int i = mContainers.size() - 1; i >= 0; i--) { + final Activity activity = mContainers.get(i).getTopNonFinishingActivity(); + if (activity != null) { + return activity; + } + } + return null; + } } 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 6693755ee1022..3bbeda9f0033c 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -21,7 +21,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; -import android.app.ActivityThread; import android.app.WindowConfiguration.WindowingMode; import android.graphics.Rect; import android.os.Binder; @@ -133,9 +132,8 @@ class TaskFragmentContainer { if (mInfo == null) { return allActivities; } - ActivityThread activityThread = ActivityThread.currentActivityThread(); for (IBinder token : mInfo.getActivities()) { - Activity activity = activityThread.getActivity(token); + Activity activity = mController.getActivity(token); if (activity != null && !activity.isFinishing() && !allActivities.contains(activity)) { allActivities.add(activity); } 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 34cde9bca7630..e8d9960c4bb33 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 @@ -16,6 +16,9 @@ package androidx.window.extensions.embedding; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -24,23 +27,31 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import android.annotation.NonNull; import android.app.Activity; +import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Point; import android.graphics.Rect; import android.os.Binder; import android.os.Handler; +import android.os.IBinder; import android.platform.test.annotations.Presubmit; +import android.util.Pair; import android.window.TaskFragmentInfo; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -53,6 +64,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -67,8 +79,8 @@ import java.util.List; public class SplitControllerTest { private static final int TASK_ID = 10; private static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200); + private static final float SPLIT_RATIO = 0.5f; - @Mock private Activity mActivity; @Mock private Resources mActivityResources; @@ -92,9 +104,9 @@ public class SplitControllerTest { final Configuration activityConfig = new Configuration(); activityConfig.windowConfiguration.setBounds(TASK_BOUNDS); activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS); - doReturn(mActivityResources).when(mActivity).getResources(); doReturn(activityConfig).when(mActivityResources).getConfiguration(); doReturn(mHandler).when(mSplitController).getHandler(); + mActivity = createMockActivity(); } @Test @@ -260,6 +272,246 @@ public class SplitControllerTest { mSplitController.updateContainer(mTransaction, tf); - verify(mSplitPresenter).updateSplitContainer(eq(splitContainer), eq(tf), eq(mTransaction)); + verify(mSplitPresenter).updateSplitContainer(splitContainer, tf, mTransaction); + } + + @Test + public void testOnActivityReparentToTask_sameProcess() { + mSplitController.onActivityReparentToTask(TASK_ID, new Intent(), + mActivity.getActivityToken()); + + // Treated as on activity created. + verify(mSplitController).onActivityCreated(mActivity); + } + + @Test + public void testOnActivityReparentToTask_diffProcess() { + // Create an empty TaskFragment to initialize for the Task. + mSplitController.newContainer(null, mActivity, TASK_ID); + final IBinder activityToken = new Binder(); + final Intent intent = new Intent(); + + mSplitController.onActivityReparentToTask(TASK_ID, intent, activityToken); + + // Treated as starting new intent + verify(mSplitController, never()).onActivityCreated(mActivity); + verify(mSplitController).resolveStartActivityIntent(any(), eq(TASK_ID), eq(intent), + isNull()); + } + + @Test + public void testResolveStartActivityIntent_withoutLaunchingActivity() { + final Intent intent = new Intent(); + final ActivityRule expandRule = new ActivityRule.Builder(r -> false, i -> i == intent) + .setShouldAlwaysExpand(true) + .build(); + mSplitController.setEmbeddingRules(Collections.singleton(expandRule)); + + // No other activity available in the Task. + TaskFragmentContainer container = mSplitController.resolveStartActivityIntent(mTransaction, + TASK_ID, intent, null /* launchingActivity */); + assertNull(container); + + // Task contains another activity that can be used as owner activity. + createMockTaskFragmentContainer(mActivity); + container = mSplitController.resolveStartActivityIntent(mTransaction, + TASK_ID, intent, null /* launchingActivity */); + assertNotNull(container); + } + + @Test + public void testResolveStartActivityIntent_shouldExpand() { + final Intent intent = new Intent(); + setupExpandRule(intent); + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, mActivity); + + assertNotNull(container); + assertTrue(container.areLastRequestedBoundsEqual(null)); + assertTrue(container.isLastRequestedWindowingModeEqual(WINDOWING_MODE_UNDEFINED)); + assertFalse(container.hasActivity(mActivity.getActivityToken())); + verify(mSplitPresenter).createTaskFragment(mTransaction, container.getTaskFragmentToken(), + mActivity.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED); + } + + @Test + public void testResolveStartActivityIntent_shouldSplitWithLaunchingActivity() { + final Intent intent = new Intent(); + setupSplitRule(mActivity, intent); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, mActivity); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertSplitPair(primaryContainer, container); + } + + @Test + public void testResolveStartActivityIntent_shouldSplitWithTopExpandActivity() { + final Intent intent = new Intent(); + setupSplitRule(mActivity, intent); + createMockTaskFragmentContainer(mActivity); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, null /* launchingActivity */); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertSplitPair(primaryContainer, container); + } + + @Test + public void testResolveStartActivityIntent_shouldSplitWithTopSecondaryActivity() { + final Intent intent = new Intent(); + setupSplitRule(mActivity, intent); + final Activity primaryActivity = createMockActivity(); + addSplitTaskFragments(primaryActivity, mActivity); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, null /* launchingActivity */); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertSplitPair(primaryContainer, container); + } + + @Test + public void testResolveStartActivityIntent_shouldSplitWithTopPrimaryActivity() { + final Intent intent = new Intent(); + setupSplitRule(mActivity, intent); + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(mActivity, secondaryActivity); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, null /* launchingActivity */); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertSplitPair(primaryContainer, container); + } + + /** Creates a mock activity in the organizer process. */ + private Activity createMockActivity() { + final Activity activity = mock(Activity.class); + doReturn(mActivityResources).when(activity).getResources(); + final IBinder activityToken = new Binder(); + doReturn(activityToken).when(activity).getActivityToken(); + doReturn(activity).when(mSplitController).getActivity(activityToken); + return activity; + } + + /** Creates a mock TaskFragmentInfo for the given TaskFragment. */ + private TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container, + @NonNull Activity activity) { + return new TaskFragmentInfo(container.getTaskFragmentToken(), + mock(WindowContainerToken.class), + new Configuration(), + 1, + true /* isVisible */, + Collections.singletonList(activity.getActivityToken()), + new Point(), + false /* isTaskClearedForReuse */, + false /* isTaskFragmentClearedForPip */); + } + + /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ + private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) { + final TaskFragmentContainer container = mSplitController.newContainer(activity, TASK_ID); + final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity); + container.setInfo(createMockTaskFragmentInfo(container, activity)); + mSplitPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), info); + return container; + } + + /** Setups a rule to always expand the given intent. */ + private void setupExpandRule(@NonNull Intent expandIntent) { + final ActivityRule expandRule = new ActivityRule.Builder(r -> false, expandIntent::equals) + .setShouldAlwaysExpand(true) + .build(); + mSplitController.setEmbeddingRules(Collections.singleton(expandRule)); + } + + /** Setups a rule to always split the given activities. */ + private void setupSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent) { + final SplitRule splitRule = createSplitRule(primaryActivity, secondaryIntent); + mSplitController.setEmbeddingRules(Collections.singleton(splitRule)); + } + + /** Creates a rule to always split the given activity and the given intent. */ + private SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent) { + final Pair targetPair = new Pair<>(primaryActivity, secondaryIntent); + return new SplitPairRule.Builder( + activityPair -> false, + targetPair::equals, + w -> true) + .setSplitRatio(SPLIT_RATIO) + .setShouldClearTop(true) + .build(); + } + + /** Creates a rule to always split the given activities. */ + private SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + final Pair targetPair = new Pair<>(primaryActivity, secondaryActivity); + return new SplitPairRule.Builder( + targetPair::equals, + activityIntentPair -> false, + w -> true) + .setSplitRatio(SPLIT_RATIO) + .setShouldClearTop(true) + .build(); + } + + /** Adds a pair of TaskFragments as split for the given activities. */ + private void addSplitTaskFragments(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + final TaskFragmentContainer primaryContainer = createMockTaskFragmentContainer( + primaryActivity); + final TaskFragmentContainer secondaryContainer = createMockTaskFragmentContainer( + secondaryActivity); + mSplitController.registerSplit( + mock(WindowContainerTransaction.class), + primaryContainer, + primaryActivity, + secondaryContainer, + createSplitRule(primaryActivity, secondaryActivity)); + + // We need to set those in case we are not respecting clear top. + // TODO(b/231845476) we should always respect clearTop. + final int windowingMode = mSplitController.getTaskContainer(TASK_ID) + .getWindowingModeForSplitTaskFragment(TASK_BOUNDS); + primaryContainer.setLastRequestedWindowingMode(windowingMode); + secondaryContainer.setLastRequestedWindowingMode(windowingMode); + primaryContainer.setLastRequestedBounds(getSplitBounds(true /* isPrimary */)); + secondaryContainer.setLastRequestedBounds(getSplitBounds(false /* isPrimary */)); + } + + /** Gets the bounds of a TaskFragment that is in split. */ + private Rect getSplitBounds(boolean isPrimary) { + final int width = (int) (TASK_BOUNDS.width() * SPLIT_RATIO); + return isPrimary + ? new Rect(TASK_BOUNDS.left, TASK_BOUNDS.top, TASK_BOUNDS.left + width, + TASK_BOUNDS.bottom) + : new Rect(TASK_BOUNDS.left + width, TASK_BOUNDS.top, TASK_BOUNDS.right, + TASK_BOUNDS.bottom); + } + + /** Asserts that the two given TaskFragments are in split. */ + private void assertSplitPair(@NonNull TaskFragmentContainer primaryContainer, + @NonNull TaskFragmentContainer secondaryContainer) { + assertNotNull(primaryContainer); + assertNotNull(secondaryContainer); + assertTrue(primaryContainer.areLastRequestedBoundsEqual( + getSplitBounds(true /* isPrimary */))); + assertTrue(secondaryContainer.areLastRequestedBoundsEqual( + getSplitBounds(false /* isPrimary */))); + assertTrue(primaryContainer.isLastRequestedWindowingModeEqual(WINDOWING_MODE_MULTI_WINDOW)); + assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual( + WINDOWING_MODE_MULTI_WINDOW)); + assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, + secondaryContainer)); } } 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 0de94b0dc26f2..f1042ab6ce7de 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 @@ -24,8 +24,12 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import android.app.Activity; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; @@ -147,4 +151,38 @@ public class TaskContainerTest { assertFalse(taskContainer.isEmpty()); } + + @Test + public void testGetTopTaskFragmentContainer() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + assertNull(taskContainer.getTopTaskFragmentContainer()); + + final TaskFragmentContainer tf0 = new TaskFragmentContainer(null /* activity */, + taskContainer, mController); + assertEquals(tf0, taskContainer.getTopTaskFragmentContainer()); + + final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, + taskContainer, mController); + assertEquals(tf1, taskContainer.getTopTaskFragmentContainer()); + } + + @Test + public void testGetTopNonFinishingActivity() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + assertNull(taskContainer.getTopNonFinishingActivity()); + + final TaskFragmentContainer tf0 = mock(TaskFragmentContainer.class); + taskContainer.mContainers.add(tf0); + final Activity activity0 = mock(Activity.class); + doReturn(activity0).when(tf0).getTopNonFinishingActivity(); + assertEquals(activity0, taskContainer.getTopNonFinishingActivity()); + + final TaskFragmentContainer tf1 = mock(TaskFragmentContainer.class); + taskContainer.mContainers.add(tf1); + assertEquals(activity0, taskContainer.getTopNonFinishingActivity()); + + final Activity activity1 = mock(Activity.class); + doReturn(activity1).when(tf1).getTopNonFinishingActivity(); + assertEquals(activity1, taskContainer.getTopNonFinishingActivity()); + } }