Merge "Handle onActivityReparentToTask for activity in other process" into tm-dev am: 9b36cc92eb am: 3829254960

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

Change-Id: I8b3a087cf289d682bca285ebaf0e2d10b32d4541
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Chris Li
2022-05-12 12:42:09 +00:00
committed by Automerger Merge Worker
6 changed files with 543 additions and 192 deletions

View File

@@ -234,13 +234,36 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent, public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent,
@NonNull IBinder activityToken) { @NonNull IBinder activityToken) {
// If the activity belongs to the current app process, we treat it as a new activity launch. // 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) { if (activity != null) {
onActivityCreated(activity); onActivityCreated(activity);
updateCallbackIfNecessary();
return; 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. */ /** 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. // TODO(b/190433398): Break down into smaller functions.
void handleActivityCreated(@NonNull Activity launchedActivity) { void handleActivityCreated(@NonNull Activity launchedActivity) {
if (isInPictureInPicture(launchedActivity)) { if (isInPictureInPicture(launchedActivity) || launchedActivity.isFinishing()) {
// We don't embed activity when it is in PIP. // We don't embed activity when it is in PIP, or finishing.
return; return;
} }
final List<EmbeddingRule> splitRules = getSplitRules(); final TaskFragmentContainer currentContainer = getContainerWithActivity(launchedActivity);
final TaskFragmentContainer currentContainer = getContainerWithActivity(
launchedActivity.getActivityToken());
// Check if the activity is configured to always be expanded. // Check if the activity is configured to always be expanded.
if (shouldExpand(launchedActivity, null, splitRules)) { if (shouldExpand(launchedActivity, null /* intent */)) {
if (shouldContainerBeExpanded(currentContainer)) { if (shouldContainerBeExpanded(currentContainer)) {
// Make sure that the existing container is expanded // Make sure that the existing container is expanded
mPresenter.expandTaskFragment(currentContainer.getTaskFragmentToken()); mPresenter.expandTaskFragment(currentContainer.getTaskFragmentToken());
@@ -375,7 +396,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
IBinder belowToken = ActivityClient.getInstance().getActivityTokenBelow( IBinder belowToken = ActivityClient.getInstance().getActivityTokenBelow(
launchedActivity.getActivityToken()); launchedActivity.getActivityToken());
if (belowToken != null) { if (belowToken != null) {
activityBelow = ActivityThread.currentActivityThread().getActivity(belowToken); activityBelow = getActivity(belowToken);
} }
} }
if (activityBelow == null) { if (activityBelow == null) {
@@ -384,7 +405,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// Check if the split is already set. // Check if the split is already set.
final TaskFragmentContainer activityBelowContainer = getContainerWithActivity( final TaskFragmentContainer activityBelowContainer = getContainerWithActivity(
activityBelow.getActivityToken()); activityBelow);
if (currentContainer != null && activityBelowContainer != null) { if (currentContainer != null && activityBelowContainer != null) {
final SplitContainer existingSplit = getActiveSplitForContainers(currentContainer, final SplitContainer existingSplit = getActiveSplitForContainers(currentContainer,
activityBelowContainer); activityBelowContainer);
@@ -394,8 +415,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
} }
} }
final SplitPairRule splitPairRule = getSplitRule(activityBelow, launchedActivity, final SplitPairRule splitPairRule = getSplitRule(activityBelow, launchedActivity);
splitRules);
if (splitPairRule == null) { if (splitPairRule == null) {
return; return;
} }
@@ -409,8 +429,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// We don't embed activity when it is in PIP. // We don't embed activity when it is in PIP.
return; return;
} }
final TaskFragmentContainer currentContainer = getContainerWithActivity( final TaskFragmentContainer currentContainer = getContainerWithActivity(activity);
activity.getActivityToken());
if (currentContainer != null) { if (currentContainer != null) {
// Changes to activities in controllers are handled in // Changes to activities in controllers are handled in
@@ -442,12 +461,144 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
mPresenter.cleanupContainer(container, false /* shouldFinishDependent */); 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 * Returns a container that this activity is registered with. An activity can only belong to one
* container, or no container at all. * container, or no container at all.
*/ */
@Nullable @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--) { for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers; final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers;
for (TaskFragmentContainer container : containers) { 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. * Returns the top active split container that has the provided container, if available.
*/ */
@Nullable @Nullable
private SplitContainer getActiveSplitForContainer(@NonNull TaskFragmentContainer container) { private SplitContainer getActiveSplitForContainer(@Nullable TaskFragmentContainer container) {
if (container == null) {
return null;
}
final List<SplitContainer> splitContainers = container.getTaskContainer().mSplitContainers; final List<SplitContainer> splitContainers = container.getTaskContainer().mSplitContainers;
if (splitContainers.isEmpty()) { if (splitContainers.isEmpty()) {
return null; 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 * Returns the active split that has the provided containers as primary and secondary or as
* secondary and primary, if available. * secondary and primary, if available.
*/ */
@VisibleForTesting
@Nullable @Nullable
private SplitContainer getActiveSplitForContainers( SplitContainer getActiveSplitForContainers(
@NonNull TaskFragmentContainer firstContainer, @NonNull TaskFragmentContainer firstContainer,
@NonNull TaskFragmentContainer secondContainer) { @NonNull TaskFragmentContainer secondContainer) {
final List<SplitContainer> splitContainers = firstContainer.getTaskContainer() final List<SplitContainer> splitContainers = firstContainer.getTaskContainer()
@@ -718,15 +873,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
} }
boolean launchPlaceholderIfNecessary(@NonNull Activity activity) { boolean launchPlaceholderIfNecessary(@NonNull Activity activity) {
final TaskFragmentContainer container = getContainerWithActivity( final TaskFragmentContainer container = getContainerWithActivity(activity);
activity.getActivityToken());
// Don't launch placeholder if the container is occluded. // Don't launch placeholder if the container is occluded.
if (container != null && container != getTopActiveContainer(container.getTaskId())) { if (container != null && container != getTopActiveContainer(container.getTaskId())) {
return false; return false;
} }
SplitContainer splitContainer = container != null ? getActiveSplitForContainer(container) final SplitContainer splitContainer = getActiveSplitForContainer(container);
: null;
if (splitContainer != null && container.equals(splitContainer.getPrimaryContainer())) { if (splitContainer != null && container.equals(splitContainer.getPrimaryContainer())) {
// Don't launch placeholder in primary split container // Don't launch placeholder in primary split container
return false; return false;
@@ -876,9 +1029,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* if available. * if available.
*/ */
@Nullable @Nullable
private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity, private SplitPairRule getSplitRule(@NonNull Activity primaryActivity,
@NonNull Intent secondaryActivityIntent, @NonNull List<EmbeddingRule> splitRules) { @NonNull Intent secondaryActivityIntent) {
for (EmbeddingRule rule : splitRules) { for (EmbeddingRule rule : mSplitRules) {
if (!(rule instanceof SplitPairRule)) { if (!(rule instanceof SplitPairRule)) {
continue; 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. * Returns a split rule for the provided pair of primary and secondary activities if available.
*/ */
@Nullable @Nullable
private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity, private SplitPairRule getSplitRule(@NonNull Activity primaryActivity,
@NonNull Activity secondaryActivity, @NonNull List<EmbeddingRule> splitRules) { @NonNull Activity secondaryActivity) {
for (EmbeddingRule rule : splitRules) { for (EmbeddingRule rule : mSplitRules) {
if (!(rule instanceof SplitPairRule)) { if (!(rule instanceof SplitPairRule)) {
continue; continue;
} }
@@ -933,16 +1086,24 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return mHandler; 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 * 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. * 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, private boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent) {
List<EmbeddingRule> splitRules) { for (EmbeddingRule rule : mSplitRules) {
if (splitRules == null) {
return false;
}
for (EmbeddingRule rule : splitRules) {
if (!(rule instanceof ActivityRule)) { if (!(rule instanceof ActivityRule)) {
continue; continue;
} }
@@ -996,8 +1157,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
*/ */
boolean shouldRetainAssociatedActivity(@NonNull TaskFragmentContainer finishingContainer, boolean shouldRetainAssociatedActivity(@NonNull TaskFragmentContainer finishingContainer,
@NonNull Activity associatedActivity) { @NonNull Activity associatedActivity) {
TaskFragmentContainer associatedContainer = getContainerWithActivity( final TaskFragmentContainer associatedContainer = getContainerWithActivity(
associatedActivity.getActivityToken()); associatedActivity);
if (associatedContainer == null) { if (associatedContainer == null) {
return false; return false;
} }
@@ -1085,130 +1246,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return super.onStartActivity(who, intent, options); return super.onStartActivity(who, intent, options);
} }
if (shouldExpand(null, intent, getSplitRules())) { final int taskId = getTaskId(launchingActivity);
setLaunchingInExpandedContainer(launchingActivity, options); final WindowContainerTransaction wct = new WindowContainerTransaction();
} else if (!splitWithLaunchingActivity(launchingActivity, intent, options)) { final TaskFragmentContainer launchedInTaskFragment = resolveStartActivityIntent(wct,
setLaunchingInSameSideContainer(launchingActivity, intent, options); 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); 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)) { if (!isContainerReusableRule(rule1) || !isContainerReusableRule(rule2)) {
return false; return false;
} }
return rule1.getSplitRatio() == rule2.getSplitRatio() final SplitPairRule pairRule1 = (SplitPairRule) rule1;
&& rule1.getLayoutDirection() == rule2.getLayoutDirection(); 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();
} }
/** /**

View File

@@ -16,8 +16,6 @@
package androidx.window.extensions.embedding; package androidx.window.extensions.embedding;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import android.app.Activity; import android.app.Activity;
import android.app.WindowConfiguration; import android.app.WindowConfiguration;
import android.app.WindowConfiguration.WindowingMode; 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. * Creates a new split with the primary activity and an empty secondary container.
* @return The newly created 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) { @NonNull SplitPairRule rule) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
final Rect parentBounds = getParentContainerBounds(primaryActivity); final Rect parentBounds = getParentContainerBounds(primaryActivity);
final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
isLtr(primaryActivity, rule)); isLtr(primaryActivity, rule));
@@ -127,8 +125,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule);
applyTransaction(wct);
return secondaryContainer; return secondaryContainer;
} }
@@ -155,8 +151,15 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule,
isLtr(primaryActivity, 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, final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct,
secondaryActivity, secondaryRectBounds, primaryContainer); secondaryActivity, secondaryRectBounds, containerToAvoid);
// Set adjacent to each other so that the containers below will be invisible. // Set adjacent to each other so that the containers below will be invisible.
setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule); setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule);
@@ -166,21 +169,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
applyTransaction(wct); 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. * 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. * @param activity The activity to be re-parented to the container if necessary.
@@ -189,8 +177,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
private TaskFragmentContainer prepareContainerForActivity( private TaskFragmentContainer prepareContainerForActivity(
@NonNull WindowContainerTransaction wct, @NonNull Activity activity, @NonNull WindowContainerTransaction wct, @NonNull Activity activity,
@NonNull Rect bounds, @Nullable TaskFragmentContainer containerToAvoid) { @NonNull Rect bounds, @Nullable TaskFragmentContainer containerToAvoid) {
TaskFragmentContainer container = mController.getContainerWithActivity( TaskFragmentContainer container = mController.getContainerWithActivity(activity);
activity.getActivityToken());
final int taskId = container != null ? container.getTaskId() : activity.getTaskId(); final int taskId = container != null ? container.getTaskId() : activity.getTaskId();
if (container == null || container == containerToAvoid) { if (container == null || container == containerToAvoid) {
container = mController.newContainer(activity, taskId); container = mController.newContainer(activity, taskId);
@@ -230,7 +217,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
isLtr(launchingActivity, rule)); isLtr(launchingActivity, rule));
TaskFragmentContainer primaryContainer = mController.getContainerWithActivity( TaskFragmentContainer primaryContainer = mController.getContainerWithActivity(
launchingActivity.getActivityToken()); launchingActivity);
if (primaryContainer == null) { if (primaryContainer == null) {
primaryContainer = mController.newContainer(launchingActivity, primaryContainer = mController.newContainer(launchingActivity,
launchingActivity.getTaskId()); launchingActivity.getTaskId());
@@ -460,8 +447,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
@NonNull @NonNull
Rect getParentContainerBounds(@NonNull Activity activity) { Rect getParentContainerBounds(@NonNull Activity activity) {
final TaskFragmentContainer container = mController.getContainerWithActivity( final TaskFragmentContainer container = mController.getContainerWithActivity(activity);
activity.getActivityToken());
if (container != null) { if (container != null) {
return getParentContainerBounds(container); return getParentContainerBounds(container);
} }

View File

@@ -142,4 +142,23 @@ class TaskContainer {
container.removePendingAppearedActivity(pendingAppearedActivity); 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;
}
} }

View File

@@ -21,7 +21,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import android.annotation.NonNull; import android.annotation.NonNull;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.app.Activity; import android.app.Activity;
import android.app.ActivityThread;
import android.app.WindowConfiguration.WindowingMode; import android.app.WindowConfiguration.WindowingMode;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Binder; import android.os.Binder;
@@ -133,9 +132,8 @@ class TaskFragmentContainer {
if (mInfo == null) { if (mInfo == null) {
return allActivities; return allActivities;
} }
ActivityThread activityThread = ActivityThread.currentActivityThread();
for (IBinder token : mInfo.getActivities()) { for (IBinder token : mInfo.getActivities()) {
Activity activity = activityThread.getActivity(token); Activity activity = mController.getActivity(token);
if (activity != null && !activity.isFinishing() && !allActivities.contains(activity)) { if (activity != null && !activity.isFinishing() && !allActivities.contains(activity)) {
allActivities.add(activity); allActivities.add(activity);
} }

View File

@@ -16,6 +16,9 @@
package androidx.window.extensions.embedding; 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.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; 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.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import android.annotation.NonNull;
import android.app.Activity; import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Binder; import android.os.Binder;
import android.os.Handler; import android.os.Handler;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.Presubmit;
import android.util.Pair;
import android.window.TaskFragmentInfo; import android.window.TaskFragmentInfo;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction; import android.window.WindowContainerTransaction;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -53,6 +64,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
@@ -67,8 +79,8 @@ import java.util.List;
public class SplitControllerTest { public class SplitControllerTest {
private static final int TASK_ID = 10; private static final int TASK_ID = 10;
private static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200); private static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200);
private static final float SPLIT_RATIO = 0.5f;
@Mock
private Activity mActivity; private Activity mActivity;
@Mock @Mock
private Resources mActivityResources; private Resources mActivityResources;
@@ -92,9 +104,9 @@ public class SplitControllerTest {
final Configuration activityConfig = new Configuration(); final Configuration activityConfig = new Configuration();
activityConfig.windowConfiguration.setBounds(TASK_BOUNDS); activityConfig.windowConfiguration.setBounds(TASK_BOUNDS);
activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS); activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS);
doReturn(mActivityResources).when(mActivity).getResources();
doReturn(activityConfig).when(mActivityResources).getConfiguration(); doReturn(activityConfig).when(mActivityResources).getConfiguration();
doReturn(mHandler).when(mSplitController).getHandler(); doReturn(mHandler).when(mSplitController).getHandler();
mActivity = createMockActivity();
} }
@Test @Test
@@ -260,6 +272,246 @@ public class SplitControllerTest {
mSplitController.updateContainer(mTransaction, tf); 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<Activity, Intent> 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<Activity, Activity> 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));
} }
} }

View File

@@ -24,8 +24,12 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; 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.graphics.Rect;
import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.Presubmit;
@@ -147,4 +151,38 @@ public class TaskContainerTest {
assertFalse(taskContainer.isEmpty()); 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());
}
} }