diff options
5 files changed, 742 insertions, 101 deletions
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 1cd422087ccb..015205c7a063 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -32,6 +32,7 @@ import android.app.ActivityClient; import android.app.ActivityOptions; import android.app.ActivityThread; import android.app.Instrumentation; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; @@ -236,7 +237,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // If the activity belongs to the current app process, we treat it as a new activity launch. final Activity activity = getActivity(activityToken); if (activity != null) { - onActivityCreated(activity); + // We don't allow split as primary for new launch because we currently only support + // launching to top. We allow split as primary for activity reparent because the + // activity may be split as primary before it is reparented out. In that case, we want + // to show it as primary again when it is reparented back. + if (!resolveActivityToContainer(activity, true /* canSplitAsPrimary */)) { + // When there is no embedding rule matched, try to place it in the top container + // like a normal launch. + placeActivityInTopContainer(activity); + } + updateCallbackIfNecessary(); return; } @@ -253,7 +263,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen 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 + // When there is no embedding rule matched, try to place it in the top container like a // normal launch. targetContainer = taskContainer.getTopTaskFragmentContainer(); } @@ -339,89 +349,244 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return false; } + @VisibleForTesting void onActivityCreated(@NonNull Activity launchedActivity) { - handleActivityCreated(launchedActivity); + // TODO(b/229680885): we don't support launching into primary yet because we want to always + // launch the new activity on top. + resolveActivityToContainer(launchedActivity, false /* canSplitAsPrimary */); updateCallbackIfNecessary(); } /** - * Checks if the activity start should be routed to a particular container. It can create a new - * container for the activity and a new split container if necessary. + * Checks if the new added activity should be routed to a particular container. It can create a + * new container for the activity and a new split container if necessary. + * @param launchedActivity the new launched activity. + * @param canSplitAsPrimary whether we can put the new launched activity into primary split. + * @return {@code true} if the activity was placed in TaskFragment container. */ - // TODO(b/190433398): Break down into smaller functions. - void handleActivityCreated(@NonNull Activity launchedActivity) { + @VisibleForTesting + boolean resolveActivityToContainer(@NonNull Activity launchedActivity, + boolean canSplitAsPrimary) { if (isInPictureInPicture(launchedActivity) || launchedActivity.isFinishing()) { - // We don't embed activity when it is in PIP, or finishing. - return; + // We don't embed activity when it is in PIP, or finishing. Return true since we don't + // want any extra handling. + return true; } - final TaskFragmentContainer currentContainer = getContainerWithActivity(launchedActivity); - // Check if the activity is configured to always be expanded. + /* + * We will check the following to see if there is any embedding rule matched: + * 1. Whether the new launched activity should always expand. + * 2. Whether the new launched activity should launch a placeholder. + * 3. Whether the new launched activity has already been in a split with a rule matched + * (likely done in #onStartActivity). + * 4. Whether the activity below (if any) should be split with the new launched activity. + * 5. Whether the activity split with the activity below (if any) should be split with the + * new launched activity. + */ + + // 1. Whether the new launched activity should always expand. if (shouldExpand(launchedActivity, null /* intent */)) { - if (shouldContainerBeExpanded(currentContainer)) { - // Make sure that the existing container is expanded - mPresenter.expandTaskFragment(currentContainer.getTaskFragmentToken()); - } else { - // Put activity into a new expanded container - final TaskFragmentContainer newContainer = newContainer(launchedActivity, - launchedActivity.getTaskId()); - mPresenter.expandActivity(newContainer.getTaskFragmentToken(), - launchedActivity); - } - return; + expandActivity(launchedActivity); + return true; } - // Check if activity requires a placeholder + // 2. Whether the new launched activity should launch a placeholder. if (launchPlaceholderIfNecessary(launchedActivity)) { + return true; + } + + // 3. Whether the new launched activity has already been in a split with a rule matched. + if (isNewActivityInSplitWithRuleMatched(launchedActivity)) { + return true; + } + + // 4. Whether the activity below (if any) should be split with the new launched activity. + final Activity activityBelow = findActivityBelow(launchedActivity); + if (activityBelow == null) { + // Can't find any activity below. + return false; + } + if (putActivitiesIntoSplitIfNecessary(activityBelow, launchedActivity)) { + // Have split rule of [ activityBelow | launchedActivity ]. + return true; + } + if (canSplitAsPrimary + && putActivitiesIntoSplitIfNecessary(launchedActivity, activityBelow)) { + // Have split rule of [ launchedActivity | activityBelow]. + return true; + } + + // 5. Whether the activity split with the activity below (if any) should be split with the + // new launched activity. + final TaskFragmentContainer activityBelowContainer = getContainerWithActivity( + activityBelow); + final SplitContainer topSplit = getActiveSplitForContainer(activityBelowContainer); + if (topSplit == null || !isTopMostSplit(topSplit)) { + // Skip if it is not the topmost split. + return false; + } + final TaskFragmentContainer otherTopContainer = + topSplit.getPrimaryContainer() == activityBelowContainer + ? topSplit.getSecondaryContainer() + : topSplit.getPrimaryContainer(); + final Activity otherTopActivity = otherTopContainer.getTopNonFinishingActivity(); + if (otherTopActivity == null || otherTopActivity == launchedActivity) { + // Can't find the top activity on the other split TaskFragment. + return false; + } + if (putActivitiesIntoSplitIfNecessary(otherTopActivity, launchedActivity)) { + // Have split rule of [ otherTopActivity | launchedActivity ]. + return true; + } + // Have split rule of [ launchedActivity | otherTopActivity]. + return canSplitAsPrimary + && putActivitiesIntoSplitIfNecessary(launchedActivity, otherTopActivity); + } + + /** + * Places the given activity to the top most TaskFragment in the task if there is any. + */ + @VisibleForTesting + void placeActivityInTopContainer(@NonNull Activity activity) { + if (getContainerWithActivity(activity) != null) { + // The activity has already been put in a TaskFragment. This is likely to be done by + // the server when the activity is started. + return; + } + final int taskId = getTaskId(activity); + final TaskContainer taskContainer = getTaskContainer(taskId); + if (taskContainer == null) { return; } + final TaskFragmentContainer targetContainer = taskContainer.getTopTaskFragmentContainer(); + if (targetContainer == null) { + return; + } + targetContainer.addPendingAppearedActivity(activity); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(), + activity.getActivityToken()); + mPresenter.applyTransaction(wct); + } + + /** + * Expands the given activity by either expanding the TaskFragment it is currently in or putting + * it into a new expanded TaskFragment. + */ + private void expandActivity(@NonNull Activity activity) { + final TaskFragmentContainer container = getContainerWithActivity(activity); + if (shouldContainerBeExpanded(container)) { + // Make sure that the existing container is expanded. + mPresenter.expandTaskFragment(container.getTaskFragmentToken()); + } else { + // Put activity into a new expanded container. + final TaskFragmentContainer newContainer = newContainer(activity, getTaskId(activity)); + mPresenter.expandActivity(newContainer.getTaskFragmentToken(), activity); + } + } + + /** Whether the given new launched activity is in a split with a rule matched. */ + private boolean isNewActivityInSplitWithRuleMatched(@NonNull Activity launchedActivity) { + final TaskFragmentContainer container = getContainerWithActivity(launchedActivity); + final SplitContainer splitContainer = getActiveSplitForContainer(container); + if (splitContainer == null) { + return false; + } + + if (container == splitContainer.getPrimaryContainer()) { + // The new launched can be in the primary container when it is starting a new activity + // onCreate, thus the secondary may still be empty. + final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); + final Activity secondaryActivity = secondaryContainer.getTopNonFinishingActivity(); + return secondaryActivity == null + || getSplitRule(launchedActivity, secondaryActivity) != null; + } - // TODO(b/190433398): Check if it is a placeholder and there is already another split - // created by the primary activity. This is necessary for the case when the primary activity - // launched another secondary in the split, but the placeholder was still launched by the - // logic above. We didn't prevent the placeholder launcher because we didn't know that - // another secondary activity is coming up. + // Check if the new launched activity is a placeholder. + if (splitContainer.getSplitRule() instanceof SplitPlaceholderRule) { + final SplitPlaceholderRule placeholderRule = + (SplitPlaceholderRule) splitContainer.getSplitRule(); + final ComponentName placeholderName = placeholderRule.getPlaceholderIntent() + .getComponent(); + // TODO(b/232330767): Do we have a better way to check this? + return placeholderName == null + || placeholderName.equals(launchedActivity.getComponentName()) + || placeholderRule.getPlaceholderIntent().equals(launchedActivity.getIntent()); + } - // Check if the activity should form a split with the activity below in the same task - // fragment. + // Check if the new launched activity should be split with the primary top activity. + final Activity primaryActivity = splitContainer.getPrimaryContainer() + .getTopNonFinishingActivity(); + if (primaryActivity == null) { + return false; + } + /* TODO(b/231845476) we should always respect clearTop. + final SplitPairRule curSplitRule = (SplitPairRule) splitContainer.getSplitRule(); + final SplitPairRule splitRule = getSplitRule(primaryActivity, launchedActivity); + return splitRule != null && haveSamePresentation(splitRule, curSplitRule) + // If the new launched split rule should clear top and it is not the bottom most, + // it means we should create a new split pair and clear the existing secondary. + && (!splitRule.shouldClearTop() + || container.getBottomMostActivity() == launchedActivity); + */ + return getSplitRule(primaryActivity, launchedActivity) != null; + } + + /** Finds the activity below the given activity. */ + @Nullable + private Activity findActivityBelow(@NonNull Activity activity) { Activity activityBelow = null; - if (currentContainer != null) { - final List<Activity> containerActivities = currentContainer.collectActivities(); - final int index = containerActivities.indexOf(launchedActivity); + final TaskFragmentContainer container = getContainerWithActivity(activity); + if (container != null) { + final List<Activity> containerActivities = container.collectActivities(); + final int index = containerActivities.indexOf(activity); if (index > 0) { activityBelow = containerActivities.get(index - 1); } } if (activityBelow == null) { - IBinder belowToken = ActivityClient.getInstance().getActivityTokenBelow( - launchedActivity.getActivityToken()); + final IBinder belowToken = ActivityClient.getInstance().getActivityTokenBelow( + activity.getActivityToken()); if (belowToken != null) { activityBelow = getActivity(belowToken); } } - if (activityBelow == null) { - return; - } - - // Check if the split is already set. - final TaskFragmentContainer activityBelowContainer = getContainerWithActivity( - activityBelow); - if (currentContainer != null && activityBelowContainer != null) { - final SplitContainer existingSplit = getActiveSplitForContainers(currentContainer, - activityBelowContainer); - if (existingSplit != null) { - // There is already an active split with the activity below. - return; - } - } + return activityBelow; + } - final SplitPairRule splitPairRule = getSplitRule(activityBelow, launchedActivity); - if (splitPairRule == null) { - return; + /** + * Checks if there is a rule to split the two activities. If there is one, puts them into split + * and returns {@code true}. Otherwise, returns {@code false}. + */ + private boolean putActivitiesIntoSplitIfNecessary(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + final SplitPairRule splitRule = getSplitRule(primaryActivity, secondaryActivity); + if (splitRule == null) { + return false; } - - mPresenter.createNewSplitContainer(activityBelow, launchedActivity, - splitPairRule); + final TaskFragmentContainer primaryContainer = getContainerWithActivity( + primaryActivity); + final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer); + if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer() + && canReuseContainer(splitRule, splitContainer.getSplitRule())) { + // Can launch in the existing secondary container if the rules share the same + // presentation. + final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); + if (secondaryContainer == getContainerWithActivity(secondaryActivity)) { + // The activity is already in the target TaskFragment. + return true; + } + secondaryContainer.addPendingAppearedActivity(secondaryActivity); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reparentActivityToTaskFragment( + secondaryContainer.getTaskFragmentToken(), + secondaryActivity.getActivityToken()); + mPresenter.applyTransaction(wct); + return true; + } + // Create new split pair. + mPresenter.createNewSplitContainer(primaryActivity, secondaryActivity, splitRule); + return true; } private void onActivityConfigurationChanged(@NonNull Activity activity) { @@ -601,7 +766,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final IBinder activityToken = activity.getActivityToken(); for (int i = mTaskContainers.size() - 1; i >= 0; i--) { final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers; - for (TaskFragmentContainer container : containers) { + // Traverse from top to bottom in case an activity is added to top pending, and hasn't + // received update from server yet. + for (int j = containers.size() - 1; j >= 0; j--) { + final TaskFragmentContainer container = containers.get(j); if (container.hasActivity(activityToken)) { return container; } @@ -798,8 +966,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (splitContainer == null) { return; } - final List<SplitContainer> splitContainers = container.getTaskContainer().mSplitContainers; - if (splitContainer != splitContainers.get(splitContainers.size() - 1)) { + if (!isTopMostSplit(splitContainer)) { // Skip position update - it isn't the topmost split. return; } @@ -815,6 +982,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen mPresenter.updateSplitContainer(splitContainer, container, wct); } + /** Whether the given split is the topmost split in the Task. */ + private boolean isTopMostSplit(@NonNull SplitContainer splitContainer) { + final List<SplitContainer> splitContainers = splitContainer.getPrimaryContainer() + .getTaskContainer().mSplitContainers; + return splitContainer == splitContainers.get(splitContainers.size() - 1); + } + /** * Returns the top active split container that has the provided container, if available. */ @@ -1014,14 +1188,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (container == null) { return false; } - final List<SplitContainer> splitContainers = container.getTaskContainer().mSplitContainers; - for (SplitContainer splitContainer : splitContainers) { - if (container.equals(splitContainer.getPrimaryContainer()) - || container.equals(splitContainer.getSecondaryContainer())) { - return false; - } - } - return true; + return getActiveSplitForContainer(container) == null; } /** @@ -1279,15 +1446,18 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (!isContainerReusableRule(rule1) || !isContainerReusableRule(rule2)) { return false; } - final SplitPairRule pairRule1 = (SplitPairRule) rule1; - final SplitPairRule pairRule2 = (SplitPairRule) rule2; + return haveSamePresentation((SplitPairRule) rule1, (SplitPairRule) rule2); + } + + /** Whether the two rules have the same presentation. */ + private static boolean haveSamePresentation(SplitPairRule rule1, 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(); + return rule1.getSplitRatio() == rule2.getSplitRatio() + && rule1.getLayoutDirection() == rule2.getLayoutDirection() + && rule1.getFinishPrimaryWithSecondary() + == rule2.getFinishPrimaryWithSecondary() + && rule1.getFinishSecondaryWithPrimary() + == rule2.getFinishSecondaryWithPrimary(); } /** 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 3bbeda9f0033..26ddae4a0818 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -121,20 +121,24 @@ class TaskFragmentContainer { /** List of activities that belong to this container and live in this process. */ @NonNull List<Activity> collectActivities() { + final List<Activity> allActivities = new ArrayList<>(); + if (mInfo != null) { + // Add activities reported from the server. + for (IBinder token : mInfo.getActivities()) { + final Activity activity = mController.getActivity(token); + if (activity != null && !activity.isFinishing()) { + allActivities.add(activity); + } + } + } + // Add the re-parenting activity, in case the server has not yet reported the task // fragment info update with it placed in this container. We still want to apply rules // in this intermediate state. - List<Activity> allActivities = new ArrayList<>(); - if (!mPendingAppearedActivities.isEmpty()) { - allActivities.addAll(mPendingAppearedActivities); - } - // Add activities reported from the server. - if (mInfo == null) { - return allActivities; - } - for (IBinder token : mInfo.getActivities()) { - Activity activity = mController.getActivity(token); - if (activity != null && !activity.isFinishing() && !allActivities.contains(activity)) { + // Place those on top of the list since they will be on the top after reported from the + // server. + for (Activity activity : mPendingAppearedActivities) { + if (!activity.isFinishing()) { allActivities.add(activity); } } @@ -241,6 +245,12 @@ class TaskFragmentContainer { return i >= 0 ? activities.get(i) : null; } + @Nullable + Activity getBottomMostActivity() { + final List<Activity> activities = collectActivities(); + return activities.isEmpty() ? null : activities.get(0); + } + boolean isEmpty() { return mPendingAppearedActivities.isEmpty() && (mInfo == null || mInfo.isEmpty()); } 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 e8d9960c4bb3..353c7df2cbc5 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 @@ -31,8 +31,11 @@ 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.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -40,6 +43,7 @@ import static org.mockito.Mockito.never; import android.annotation.NonNull; import android.app.Activity; +import android.content.ComponentName; import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; @@ -80,6 +84,8 @@ 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; + private static final Intent PLACEHOLDER_INTENT = new Intent().setComponent( + new ComponentName("test", "placeholder")); private Activity mActivity; @Mock @@ -101,6 +107,7 @@ public class SplitControllerTest { mSplitPresenter = mSplitController.mPresenter; spyOn(mSplitController); spyOn(mSplitPresenter); + doNothing().when(mSplitPresenter).applyTransaction(any()); final Configuration activityConfig = new Configuration(); activityConfig.windowConfiguration.setBounds(TASK_BOUNDS); activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS); @@ -276,12 +283,24 @@ public class SplitControllerTest { } @Test + public void testOnActivityCreated() { + mSplitController.onActivityCreated(mActivity); + + // Disallow to split as primary because we want the new launch to be always on top. + verify(mSplitController).resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + } + + @Test public void testOnActivityReparentToTask_sameProcess() { mSplitController.onActivityReparentToTask(TASK_ID, new Intent(), mActivity.getActivityToken()); - // Treated as on activity created. - verify(mSplitController).onActivityCreated(mActivity); + // Treated as on activity created, but allow to split as primary. + verify(mSplitController).resolveActivityToContainer(mActivity, + true /* canSplitAsPrimary */); + // Try to place the activity to the top TaskFragment when there is no matched rule. + verify(mSplitController).placeActivityInTopContainer(mActivity); } @Test @@ -294,7 +313,7 @@ public class SplitControllerTest { mSplitController.onActivityReparentToTask(TASK_ID, intent, activityToken); // Treated as starting new intent - verify(mSplitController, never()).onActivityCreated(mActivity); + verify(mSplitController, never()).resolveActivityToContainer(any(), anyBoolean()); verify(mSplitController).resolveStartActivityIntent(any(), eq(TASK_ID), eq(intent), isNull()); } @@ -391,6 +410,327 @@ public class SplitControllerTest { assertSplitPair(primaryContainer, container); } + @Test + public void testPlaceActivityInTopContainer() { + mSplitController.placeActivityInTopContainer(mActivity); + + verify(mSplitPresenter, never()).applyTransaction(any()); + + mSplitController.newContainer(null /* activity */, mActivity, TASK_ID); + mSplitController.placeActivityInTopContainer(mActivity); + + verify(mSplitPresenter).applyTransaction(any()); + + // Not reparent if activity is in a TaskFragment. + clearInvocations(mSplitPresenter); + mSplitController.newContainer(mActivity, TASK_ID); + mSplitController.placeActivityInTopContainer(mActivity); + + verify(mSplitPresenter, never()).applyTransaction(any()); + } + + @Test + public void testResolveActivityToContainer_noRuleMatched() { + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertFalse(result); + verify(mSplitController, never()).newContainer(any(), any(), anyInt()); + } + + @Test + public void testResolveActivityToContainer_expandRule_notInTaskFragment() { + setupExpandRule(mActivity); + + // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it. + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + final TaskFragmentContainer container = mSplitController.getContainerWithActivity( + mActivity); + + assertTrue(result); + assertNotNull(container); + verify(mSplitController).newContainer(mActivity, TASK_ID); + verify(mSplitPresenter).expandActivity(container.getTaskFragmentToken(), mActivity); + } + + @Test + public void testResolveActivityToContainer_expandRule_inSingleTaskFragment() { + setupExpandRule(mActivity); + + // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it. + final TaskFragmentContainer container = mSplitController.newContainer(mActivity, TASK_ID); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertTrue(result); + verify(mSplitPresenter).expandTaskFragment(container.getTaskFragmentToken()); + } + + @Test + public void testResolveActivityToContainer_expandRule_inSplitTaskFragment() { + setupExpandRule(mActivity); + + // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it. + final Activity activity = createMockActivity(); + addSplitTaskFragments(activity, mActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + final TaskFragmentContainer container = mSplitController.getContainerWithActivity( + mActivity); + + assertTrue(result); + assertNotNull(container); + verify(mSplitPresenter).expandActivity(container.getTaskFragmentToken(), mActivity); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_notInTaskFragment() { + setupPlaceholderRule(mActivity); + final SplitPlaceholderRule placeholderRule = + (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); + + // Launch placeholder if the activity is not in any TaskFragment. + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertTrue(result); + verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT, + null /* activityOptions */, placeholderRule, true /* isPlaceholder */); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_inOccludedTaskFragment() { + setupPlaceholderRule(mActivity); + + // Don't launch placeholder if the activity is not in the topmost active TaskFragment. + final Activity activity = createMockActivity(); + mSplitController.newContainer(mActivity, TASK_ID); + mSplitController.newContainer(activity, TASK_ID); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertFalse(result); + verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), + anyBoolean()); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_inTopMostTaskFragment() { + setupPlaceholderRule(mActivity); + final SplitPlaceholderRule placeholderRule = + (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); + + // Launch placeholder if the activity is in the topmost expanded TaskFragment. + mSplitController.newContainer(mActivity, TASK_ID); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertTrue(result); + verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT, + null /* activityOptions */, placeholderRule, true /* isPlaceholder */); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_inPrimarySplit() { + setupPlaceholderRule(mActivity); + + // Don't launch placeholder if the activity is in primary split. + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(mActivity, secondaryActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertFalse(result); + verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), + anyBoolean()); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_inSecondarySplit() { + setupPlaceholderRule(mActivity); + final SplitPlaceholderRule placeholderRule = + (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); + + // Launch placeholder if the activity is in secondary split. + final Activity primaryActivity = createMockActivity(); + addSplitTaskFragments(primaryActivity, mActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertTrue(result); + verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT, + null /* activityOptions */, placeholderRule, true /* isPlaceholder */); + } + + @Test + public void testResolveActivityToContainer_splitRule_inPrimarySplitWithRuleMatched() { + final Intent secondaryIntent = new Intent(); + setupSplitRule(mActivity, secondaryIntent); + final SplitPairRule splitRule = (SplitPairRule) mSplitController.getSplitRules().get(0); + + // Activity is already in primary split, no need to create new split. + final TaskFragmentContainer primaryContainer = mSplitController.newContainer(mActivity, + TASK_ID); + final TaskFragmentContainer secondaryContainer = mSplitController.newContainer( + null /* activity */, mActivity, TASK_ID); + mSplitController.registerSplit( + mTransaction, + primaryContainer, + mActivity, + secondaryContainer, + splitRule); + clearInvocations(mSplitController); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertTrue(result); + verify(mSplitController, never()).newContainer(any(), any(), anyInt()); + verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any()); + } + + @Test + public void testResolveActivityToContainer_splitRule_inSecondarySplitWithRuleMatched() { + final Activity primaryActivity = createMockActivity(); + setupSplitRule(primaryActivity, mActivity); + + // Activity is already in secondary split, no need to create new split. + addSplitTaskFragments(primaryActivity, mActivity); + clearInvocations(mSplitController); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertTrue(result); + verify(mSplitController, never()).newContainer(any(), any(), anyInt()); + verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any()); + } + + @Test + public void testResolveActivityToContainer_splitRule_inSecondarySplitWithNoRuleMatched() { + final Activity primaryActivity = createMockActivity(); + final Activity secondaryActivity = createMockActivity(); + setupSplitRule(primaryActivity, secondaryActivity); + + // Activity is in secondary split, but there is no rule to split it with primary. + addSplitTaskFragments(primaryActivity, secondaryActivity); + mSplitController.getContainerWithActivity(secondaryActivity) + .addPendingAppearedActivity(mActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertFalse(result); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_isPlaceholderWithRuleMatched() { + final Activity primaryActivity = createMockActivity(); + setupPlaceholderRule(primaryActivity); + final SplitPlaceholderRule placeholderRule = + (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); + doReturn(PLACEHOLDER_INTENT).when(mActivity).getIntent(); + + // Activity is a placeholder. + final TaskFragmentContainer primaryContainer = mSplitController.newContainer( + primaryActivity, TASK_ID); + final TaskFragmentContainer secondaryContainer = mSplitController.newContainer(mActivity, + TASK_ID); + mSplitController.registerSplit( + mTransaction, + primaryContainer, + mActivity, + secondaryContainer, + placeholderRule); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertTrue(result); + } + + @Test + public void testResolveActivityToContainer_splitRule_splitWithActivityBelowAsSecondary() { + final Activity activityBelow = createMockActivity(); + setupSplitRule(activityBelow, mActivity); + + final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, + TASK_ID); + container.addPendingAppearedActivity(mActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertTrue(result); + assertSplitPair(activityBelow, mActivity); + } + + @Test + public void testResolveActivityToContainer_splitRule_splitWithActivityBelowAsPrimary() { + final Activity activityBelow = createMockActivity(); + setupSplitRule(mActivity, activityBelow); + + // Disallow to split as primary. + final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, + TASK_ID); + container.addPendingAppearedActivity(mActivity); + boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertFalse(result); + assertEquals(container, mSplitController.getContainerWithActivity(mActivity)); + + // Allow to split as primary. + result = mSplitController.resolveActivityToContainer(mActivity, + true /* canSplitAsPrimary */); + + assertTrue(result); + assertSplitPair(mActivity, activityBelow); + } + + @Test + public void testResolveActivityToContainer_splitRule_splitWithCurrentPrimaryAsSecondary() { + final Activity primaryActivity = createMockActivity(); + setupSplitRule(primaryActivity, mActivity); + + final Activity activityBelow = createMockActivity(); + addSplitTaskFragments(primaryActivity, activityBelow); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + primaryActivity); + final TaskFragmentContainer secondaryContainer = mSplitController.getContainerWithActivity( + activityBelow); + secondaryContainer.addPendingAppearedActivity(mActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + final TaskFragmentContainer container = mSplitController.getContainerWithActivity( + mActivity); + + assertTrue(result); + // TODO(b/231845476) we should always respect clearTop. + // assertNotEquals(secondaryContainer, container); + assertSplitPair(primaryContainer, container); + } + + @Test + public void testResolveActivityToContainer_splitRule_splitWithCurrentPrimaryAsPrimary() { + final Activity primaryActivity = createMockActivity(); + setupSplitRule(mActivity, primaryActivity); + + final Activity activityBelow = createMockActivity(); + addSplitTaskFragments(primaryActivity, activityBelow); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + primaryActivity); + primaryContainer.addPendingAppearedActivity(mActivity); + boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertFalse(result); + assertEquals(primaryContainer, mSplitController.getContainerWithActivity(mActivity)); + + + result = mSplitController.resolveActivityToContainer(mActivity, + true /* canSplitAsPrimary */); + + assertTrue(result); + assertSplitPair(mActivity, primaryActivity); + } + /** Creates a mock activity in the organizer process. */ private Activity createMockActivity() { final Activity activity = mock(Activity.class); @@ -398,6 +738,7 @@ public class SplitControllerTest { final IBinder activityToken = new Binder(); doReturn(activityToken).when(activity).getActivityToken(); doReturn(activity).when(mSplitController).getActivity(activityToken); + doReturn(TASK_ID).when(activity).getTaskId(); return activity; } @@ -418,10 +759,16 @@ public class SplitControllerTest { /** 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); + setupTaskFragmentInfo(container, activity); + return container; + } + + /** Setups the given TaskFragment as it has appeared in the server. */ + private void setupTaskFragmentInfo(@NonNull TaskFragmentContainer container, + @NonNull Activity activity) { final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity); - container.setInfo(createMockTaskFragmentInfo(container, activity)); + container.setInfo(info); mSplitPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), info); - return container; } /** Setups a rule to always expand the given intent. */ @@ -432,6 +779,23 @@ public class SplitControllerTest { mSplitController.setEmbeddingRules(Collections.singleton(expandRule)); } + /** Setups a rule to always expand the given activity. */ + private void setupExpandRule(@NonNull Activity expandActivity) { + final ActivityRule expandRule = new ActivityRule.Builder(expandActivity::equals, i -> false) + .setShouldAlwaysExpand(true) + .build(); + mSplitController.setEmbeddingRules(Collections.singleton(expandRule)); + } + + /** Setups a rule to launch placeholder for the given activity. */ + private void setupPlaceholderRule(@NonNull Activity primaryActivity) { + final SplitRule placeholderRule = new SplitPlaceholderRule.Builder(PLACEHOLDER_INTENT, + primaryActivity::equals, i -> false, w -> true) + .setSplitRatio(SPLIT_RATIO) + .build(); + mSplitController.setEmbeddingRules(Collections.singleton(placeholderRule)); + } + /** Setups a rule to always split the given activities. */ private void setupSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryIntent) { @@ -439,6 +803,13 @@ public class SplitControllerTest { mSplitController.setEmbeddingRules(Collections.singleton(splitRule)); } + /** Setups a rule to always split the given activities. */ + private void setupSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity); + 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) { @@ -499,19 +870,31 @@ public class SplitControllerTest { TASK_BOUNDS.bottom); } + /** Asserts that the two given activities are in split. */ + private void assertSplitPair(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + assertSplitPair(mSplitController.getContainerWithActivity(primaryActivity), + mSplitController.getContainerWithActivity(secondaryActivity)); + } + /** 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)); + if (primaryContainer.mInfo != null) { + assertTrue(primaryContainer.areLastRequestedBoundsEqual( + getSplitBounds(true /* isPrimary */))); + assertTrue(primaryContainer.isLastRequestedWindowingModeEqual( + WINDOWING_MODE_MULTI_WINDOW)); + } + if (secondaryContainer.mInfo != null) { + assertTrue(secondaryContainer.areLastRequestedBoundsEqual( + getSplitBounds(false /* isPrimary */))); + assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual( + WINDOWING_MODE_MULTI_WINDOW)); + } } } 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 ce80cbf323b2..587878f3bf01 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 @@ -18,6 +18,7 @@ package androidx.window.extensions.embedding; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -29,7 +30,9 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import android.app.Activity; +import android.os.Binder; import android.os.Handler; +import android.os.IBinder; import android.platform.test.annotations.Presubmit; import android.window.TaskFragmentInfo; import android.window.WindowContainerTransaction; @@ -37,6 +40,8 @@ import android.window.WindowContainerTransaction; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.google.android.collect.Lists; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -44,6 +49,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; +import java.util.List; /** * Test class for {@link TaskFragmentContainer}. @@ -62,16 +68,16 @@ public class TaskFragmentContainerTest { @Mock private SplitController mController; @Mock - private Activity mActivity; - @Mock private TaskFragmentInfo mInfo; @Mock private Handler mHandler; + private Activity mActivity; @Before public void setup() { MockitoAnnotations.initMocks(this); doReturn(mHandler).when(mController).getHandler(); + mActivity = createMockActivity(); } @Test @@ -165,4 +171,72 @@ public class TaskFragmentContainerTest { assertNull(container.mAppearEmptyTimeout); verify(mController).onTaskFragmentAppearEmptyTimeout(container); } + + @Test + public void testCollectActivities() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + taskContainer, mController); + List<Activity> activities = container.collectActivities(); + + assertTrue(activities.isEmpty()); + + container.addPendingAppearedActivity(mActivity); + activities = container.collectActivities(); + + assertEquals(1, activities.size()); + + final Activity activity0 = createMockActivity(); + final Activity activity1 = createMockActivity(); + final List<IBinder> runningActivities = Lists.newArrayList(activity0.getActivityToken(), + activity1.getActivityToken()); + doReturn(runningActivities).when(mInfo).getActivities(); + container.setInfo(mInfo); + activities = container.collectActivities(); + + assertEquals(3, activities.size()); + assertEquals(activity0, activities.get(0)); + assertEquals(activity1, activities.get(1)); + assertEquals(mActivity, activities.get(2)); + } + + @Test + public void testAddPendingActivity() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + taskContainer, mController); + container.addPendingAppearedActivity(mActivity); + + assertEquals(1, container.collectActivities().size()); + + container.addPendingAppearedActivity(mActivity); + + assertEquals(1, container.collectActivities().size()); + } + + @Test + public void testGetBottomMostActivity() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + taskContainer, mController); + container.addPendingAppearedActivity(mActivity); + + assertEquals(mActivity, container.getBottomMostActivity()); + + final Activity activity = createMockActivity(); + final List<IBinder> runningActivities = Lists.newArrayList(activity.getActivityToken()); + doReturn(runningActivities).when(mInfo).getActivities(); + container.setInfo(mInfo); + + assertEquals(activity, container.getBottomMostActivity()); + } + + /** Creates a mock activity in the organizer process. */ + private Activity createMockActivity() { + final Activity activity = mock(Activity.class); + final IBinder activityToken = new Binder(); + doReturn(activityToken).when(activity).getActivityToken(); + doReturn(activity).when(mController).getActivity(activityToken); + return activity; + } } diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java index 9c2ced087c0e..ad3bb2dcc322 100644 --- a/services/core/java/com/android/server/wm/AppTransitionController.java +++ b/services/core/java/com/android/server/wm/AppTransitionController.java @@ -909,7 +909,11 @@ public class AppTransitionController { // We cannot promote the animation on Task's parent when the task is in // clearing task in case the animating get stuck when performing the opening // task that behind it. - || (current.asTask() != null && current.asTask().mInRemoveTask)) { + || (current.asTask() != null && current.asTask().mInRemoveTask) + // We cannot promote the animation to changing window. This may happen when an + // activity is open in a TaskFragment that is resizing, while the existing + // activity in the TaskFragment is reparented to another TaskFragment. + || parent.isChangingAppTransition()) { canPromote = false; } else { // In case a descendant of the parent belongs to the other group, we cannot promote |