Merge "Cleanup handleActivityCreated" into tm-dev am: 5284c9b855

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/18179569

Change-Id: Id88670730231e0999e67b90cee6f93ec8c7687ce
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Chris Li
2022-05-13 08:57:53 +00:00
committed by Automerger Merge Worker
5 changed files with 743 additions and 102 deletions

View File

@@ -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;
return true;
}
// 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.
// 3. Whether the new launched activity has already been in a split with a rule matched.
if (isNewActivityInSplitWithRuleMatched(launchedActivity)) {
return true;
}
// Check if the activity should form a split with the activity below in the same task
// fragment.
// 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;
}
// 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 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;
}
return activityBelow;
}
// 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;
/**
* 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;
}
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;
}
final SplitPairRule splitPairRule = getSplitRule(activityBelow, launchedActivity);
if (splitPairRule == null) {
return;
}
mPresenter.createNewSplitContainer(activityBelow, launchedActivity,
splitPairRule);
// 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();
}
/**

View File

@@ -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());
}

View File

@@ -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);
@@ -275,13 +282,25 @@ public class SplitControllerTest {
verify(mSplitPresenter).updateSplitContainer(splitContainer, tf, mTransaction);
}
@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,12 +759,18 @@ 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);
final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity);
container.setInfo(createMockTaskFragmentInfo(container, activity));
mSplitPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), info);
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(info);
mSplitPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), info);
}
/** 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)
@@ -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));
}
}
}

View File

@@ -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;
}
}

View File

@@ -899,7 +899,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