Fix ActivityEmbedding issues with REORDER_TO_FRONT
1. When the primary TaskFragment is finished with shouldFinishDependent = false, it should still finish the placeholder TaskFragment. Otherwise it may leave the placeholder there forever. 2. In case the app starts two activities one after another, if the first Intent is not handled, but the second is, when the first activity onCreate reach the organizer, it may be reparented to a new TaskFragment that is on top of the second activity. We want to make sure that the TaskFragment is created at the same position as the reparent Activity. Bug: 255628567 Test: manually verify with the test app in the bug. Test: atest WMJetpackUnitTests:TaskFragmentContainerTest Test: atest WmTests:TaskFragmentOrganizerControllerTest Change-Id: Ie48d7e46786cabcf3a1ededa9275f0223e2f477f
This commit is contained in:
@@ -71,20 +71,42 @@ public final class TaskFragmentCreationParams implements Parcelable {
|
||||
*
|
||||
* This is needed in case we need to launch a placeholder Activity to split below a transparent
|
||||
* always-expand Activity.
|
||||
*
|
||||
* This should not be used with {@link #mPairedActivityToken}.
|
||||
*/
|
||||
@Nullable
|
||||
private final IBinder mPairedPrimaryFragmentToken;
|
||||
|
||||
/**
|
||||
* The Activity token to place the new TaskFragment on top of.
|
||||
* When it is set, the new TaskFragment will be positioned right above the target Activity.
|
||||
* Otherwise, the new TaskFragment will be positioned on the top of the Task by default.
|
||||
*
|
||||
* This is needed in case we need to place an Activity into TaskFragment to launch placeholder
|
||||
* below a transparent always-expand Activity, or when there is another Intent being started in
|
||||
* a TaskFragment above.
|
||||
*
|
||||
* This should not be used with {@link #mPairedPrimaryFragmentToken}.
|
||||
*/
|
||||
@Nullable
|
||||
private final IBinder mPairedActivityToken;
|
||||
|
||||
private TaskFragmentCreationParams(
|
||||
@NonNull TaskFragmentOrganizerToken organizer, @NonNull IBinder fragmentToken,
|
||||
@NonNull IBinder ownerToken, @NonNull Rect initialBounds,
|
||||
@WindowingMode int windowingMode, @Nullable IBinder pairedPrimaryFragmentToken) {
|
||||
@WindowingMode int windowingMode, @Nullable IBinder pairedPrimaryFragmentToken,
|
||||
@Nullable IBinder pairedActivityToken) {
|
||||
if (pairedPrimaryFragmentToken != null && pairedActivityToken != null) {
|
||||
throw new IllegalArgumentException("pairedPrimaryFragmentToken and"
|
||||
+ " pairedActivityToken should not be set at the same time.");
|
||||
}
|
||||
mOrganizer = organizer;
|
||||
mFragmentToken = fragmentToken;
|
||||
mOwnerToken = ownerToken;
|
||||
mInitialBounds.set(initialBounds);
|
||||
mWindowingMode = windowingMode;
|
||||
mPairedPrimaryFragmentToken = pairedPrimaryFragmentToken;
|
||||
mPairedActivityToken = pairedActivityToken;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -121,6 +143,15 @@ public final class TaskFragmentCreationParams implements Parcelable {
|
||||
return mPairedPrimaryFragmentToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO(b/232476698): remove the hide with adding CTS for this in next release.
|
||||
* @hide
|
||||
*/
|
||||
@Nullable
|
||||
public IBinder getPairedActivityToken() {
|
||||
return mPairedActivityToken;
|
||||
}
|
||||
|
||||
private TaskFragmentCreationParams(Parcel in) {
|
||||
mOrganizer = TaskFragmentOrganizerToken.CREATOR.createFromParcel(in);
|
||||
mFragmentToken = in.readStrongBinder();
|
||||
@@ -128,6 +159,7 @@ public final class TaskFragmentCreationParams implements Parcelable {
|
||||
mInitialBounds.readFromParcel(in);
|
||||
mWindowingMode = in.readInt();
|
||||
mPairedPrimaryFragmentToken = in.readStrongBinder();
|
||||
mPairedActivityToken = in.readStrongBinder();
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@@ -139,6 +171,7 @@ public final class TaskFragmentCreationParams implements Parcelable {
|
||||
mInitialBounds.writeToParcel(dest, flags);
|
||||
dest.writeInt(mWindowingMode);
|
||||
dest.writeStrongBinder(mPairedPrimaryFragmentToken);
|
||||
dest.writeStrongBinder(mPairedActivityToken);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -164,6 +197,7 @@ public final class TaskFragmentCreationParams implements Parcelable {
|
||||
+ " initialBounds=" + mInitialBounds
|
||||
+ " windowingMode=" + mWindowingMode
|
||||
+ " pairedFragmentToken=" + mPairedPrimaryFragmentToken
|
||||
+ " pairedActivityToken=" + mPairedActivityToken
|
||||
+ "}";
|
||||
}
|
||||
|
||||
@@ -194,6 +228,9 @@ public final class TaskFragmentCreationParams implements Parcelable {
|
||||
@Nullable
|
||||
private IBinder mPairedPrimaryFragmentToken;
|
||||
|
||||
@Nullable
|
||||
private IBinder mPairedActivityToken;
|
||||
|
||||
public Builder(@NonNull TaskFragmentOrganizerToken organizer,
|
||||
@NonNull IBinder fragmentToken, @NonNull IBinder ownerToken) {
|
||||
mOrganizer = organizer;
|
||||
@@ -224,6 +261,8 @@ public final class TaskFragmentCreationParams implements Parcelable {
|
||||
* This is needed in case we need to launch a placeholder Activity to split below a
|
||||
* transparent always-expand Activity.
|
||||
*
|
||||
* This should not be used with {@link #setPairedActivityToken}.
|
||||
*
|
||||
* TODO(b/232476698): remove the hide with adding CTS for this in next release.
|
||||
* @hide
|
||||
*/
|
||||
@@ -233,11 +272,32 @@ public final class TaskFragmentCreationParams implements Parcelable {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Activity token to place the new TaskFragment on top of.
|
||||
* When it is set, the new TaskFragment will be positioned right above the target Activity.
|
||||
* Otherwise, the new TaskFragment will be positioned on the top of the Task by default.
|
||||
*
|
||||
* This is needed in case we need to place an Activity into TaskFragment to launch
|
||||
* placeholder below a transparent always-expand Activity, or when there is another Intent
|
||||
* being started in a TaskFragment above.
|
||||
*
|
||||
* This should not be used with {@link #setPairedPrimaryFragmentToken}.
|
||||
*
|
||||
* TODO(b/232476698): remove the hide with adding CTS for this in next release.
|
||||
* @hide
|
||||
*/
|
||||
@NonNull
|
||||
public Builder setPairedActivityToken(@Nullable IBinder activityToken) {
|
||||
mPairedActivityToken = activityToken;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Constructs the options to create TaskFragment with. */
|
||||
@NonNull
|
||||
public TaskFragmentCreationParams build() {
|
||||
return new TaskFragmentCreationParams(mOrganizer, mFragmentToken, mOwnerToken,
|
||||
mInitialBounds, mWindowingMode, mPairedPrimaryFragmentToken);
|
||||
mInitialBounds, mWindowingMode, mPairedPrimaryFragmentToken,
|
||||
mPairedActivityToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,10 +191,25 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
|
||||
*/
|
||||
void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
|
||||
@NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) {
|
||||
createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode,
|
||||
null /* pairedActivityToken */);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ownerToken The token of the activity that creates this task fragment. It does not
|
||||
* have to be a child of this task fragment, but must belong to the same task.
|
||||
* @param pairedActivityToken The token of the activity that will be reparented to this task
|
||||
* fragment. When it is not {@code null}, the task fragment will be
|
||||
* positioned right above it.
|
||||
*/
|
||||
void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
|
||||
@NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode,
|
||||
@Nullable IBinder pairedActivityToken) {
|
||||
final TaskFragmentCreationParams fragmentOptions = new TaskFragmentCreationParams.Builder(
|
||||
getOrganizerToken(), fragmentToken, ownerToken)
|
||||
.setInitialBounds(bounds)
|
||||
.setWindowingMode(windowingMode)
|
||||
.setPairedActivityToken(pairedActivityToken)
|
||||
.build();
|
||||
createTaskFragment(wct, fragmentOptions);
|
||||
}
|
||||
@@ -216,8 +231,10 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
|
||||
private void createTaskFragmentAndReparentActivity(@NonNull WindowContainerTransaction wct,
|
||||
@NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds,
|
||||
@WindowingMode int windowingMode, @NonNull Activity activity) {
|
||||
createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode);
|
||||
wct.reparentActivityToTaskFragment(fragmentToken, activity.getActivityToken());
|
||||
final IBinder reparentActivityToken = activity.getActivityToken();
|
||||
createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode,
|
||||
reparentActivityToken);
|
||||
wct.reparentActivityToTaskFragment(fragmentToken, reparentActivityToken);
|
||||
}
|
||||
|
||||
void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
|
||||
|
||||
@@ -1481,7 +1481,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
|
||||
* Returns the active split that has the provided containers as primary and secondary or as
|
||||
* secondary and primary, if available.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
@GuardedBy("mLock")
|
||||
@Nullable
|
||||
SplitContainer getActiveSplitForContainers(
|
||||
@NonNull TaskFragmentContainer firstContainer,
|
||||
|
||||
@@ -268,10 +268,11 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
|
||||
container = mController.newContainer(activity, taskId);
|
||||
final int windowingMode = mController.getTaskContainer(taskId)
|
||||
.getWindowingModeForSplitTaskFragment(bounds);
|
||||
createTaskFragment(wct, container.getTaskFragmentToken(), activity.getActivityToken(),
|
||||
bounds, windowingMode);
|
||||
final IBinder reparentActivityToken = activity.getActivityToken();
|
||||
createTaskFragment(wct, container.getTaskFragmentToken(), reparentActivityToken,
|
||||
bounds, windowingMode, reparentActivityToken);
|
||||
wct.reparentActivityToTaskFragment(container.getTaskFragmentToken(),
|
||||
activity.getActivityToken());
|
||||
reparentActivityToken);
|
||||
} else {
|
||||
resizeTaskFragmentIfRegistered(wct, container, bounds);
|
||||
final int windowingMode = mController.getTaskContainer(taskId)
|
||||
|
||||
@@ -141,12 +141,26 @@ class TaskFragmentContainer {
|
||||
mToken = new Binder("TaskFragmentContainer");
|
||||
mTaskContainer = taskContainer;
|
||||
if (pairedPrimaryContainer != null) {
|
||||
// The TaskFragment will be positioned right above the paired container.
|
||||
if (pairedPrimaryContainer.getTaskContainer() != taskContainer) {
|
||||
throw new IllegalArgumentException(
|
||||
"pairedPrimaryContainer must be in the same Task");
|
||||
}
|
||||
final int primaryIndex = taskContainer.mContainers.indexOf(pairedPrimaryContainer);
|
||||
taskContainer.mContainers.add(primaryIndex + 1, this);
|
||||
} else if (pendingAppearedActivity != null) {
|
||||
// The TaskFragment will be positioned right above the pending appeared Activity. If any
|
||||
// existing TaskFragment is empty with pending Intent, it is likely that the Activity of
|
||||
// the pending Intent hasn't been created yet, so the new Activity should be below the
|
||||
// empty TaskFragment.
|
||||
int i = taskContainer.mContainers.size() - 1;
|
||||
for (; i >= 0; i--) {
|
||||
final TaskFragmentContainer container = taskContainer.mContainers.get(i);
|
||||
if (!container.isEmpty() || container.getPendingAppearedIntent() == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
taskContainer.mContainers.add(i + 1, this);
|
||||
} else {
|
||||
taskContainer.mContainers.add(this);
|
||||
}
|
||||
@@ -500,6 +514,8 @@ class TaskFragmentContainer {
|
||||
}
|
||||
|
||||
if (!shouldFinishDependent) {
|
||||
// Always finish the placeholder when the primary is finished.
|
||||
finishPlaceholderIfAny(wct, presenter);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -526,6 +542,28 @@ class TaskFragmentContainer {
|
||||
mActivitiesToFinishOnExit.clear();
|
||||
}
|
||||
|
||||
@GuardedBy("mController.mLock")
|
||||
private void finishPlaceholderIfAny(@NonNull WindowContainerTransaction wct,
|
||||
@NonNull SplitPresenter presenter) {
|
||||
final List<TaskFragmentContainer> containersToRemove = new ArrayList<>();
|
||||
for (TaskFragmentContainer container : mContainersToFinishOnExit) {
|
||||
if (container.mIsFinished) {
|
||||
continue;
|
||||
}
|
||||
final SplitContainer splitContainer = mController.getActiveSplitForContainers(
|
||||
this, container);
|
||||
if (splitContainer != null && splitContainer.isPlaceholderContainer()
|
||||
&& splitContainer.getSecondaryContainer() == container) {
|
||||
// Remove the placeholder secondary TaskFragment.
|
||||
containersToRemove.add(container);
|
||||
}
|
||||
}
|
||||
mContainersToFinishOnExit.removeAll(containersToRemove);
|
||||
for (TaskFragmentContainer container : containersToRemove) {
|
||||
container.finish(false /* shouldFinishDependent */, presenter, wct, mController);
|
||||
}
|
||||
}
|
||||
|
||||
boolean isFinished() {
|
||||
return mIsFinished;
|
||||
}
|
||||
|
||||
@@ -154,16 +154,51 @@ public class TaskFragmentContainerTest {
|
||||
null /* pendingAppearedIntent */, taskContainer, mController,
|
||||
null /* pairedPrimaryContainer */);
|
||||
doReturn(container1).when(mController).getContainerWithActivity(mActivity);
|
||||
final WindowContainerTransaction wct = new WindowContainerTransaction();
|
||||
|
||||
// The activity is requested to be reparented, so don't finish it.
|
||||
container0.finish(true /* shouldFinishDependent */, mPresenter, wct, mController);
|
||||
container0.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController);
|
||||
|
||||
verify(mTransaction, never()).finishActivity(any());
|
||||
verify(mPresenter).deleteTaskFragment(wct, container0.getTaskFragmentToken());
|
||||
verify(mPresenter).deleteTaskFragment(mTransaction, container0.getTaskFragmentToken());
|
||||
verify(mController).removeContainer(container0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFinish_alwaysFinishPlaceholder() {
|
||||
// Register container1 as a placeholder
|
||||
final TaskContainer taskContainer = createTestTaskContainer();
|
||||
final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity,
|
||||
null /* pendingAppearedIntent */, taskContainer, mController,
|
||||
null /* pairedPrimaryContainer */);
|
||||
final TaskFragmentInfo info0 = createMockTaskFragmentInfo(container0, mActivity);
|
||||
container0.setInfo(mTransaction, info0);
|
||||
final Activity placeholderActivity = createMockActivity();
|
||||
final TaskFragmentContainer container1 = new TaskFragmentContainer(placeholderActivity,
|
||||
null /* pendingAppearedIntent */, taskContainer, mController,
|
||||
null /* pairedPrimaryContainer */);
|
||||
final TaskFragmentInfo info1 = createMockTaskFragmentInfo(container1, placeholderActivity);
|
||||
container1.setInfo(mTransaction, info1);
|
||||
final SplitAttributes splitAttributes = new SplitAttributes.Builder().build();
|
||||
final SplitPlaceholderRule rule = new SplitPlaceholderRule.Builder(new Intent(),
|
||||
mActivity::equals, (java.util.function.Predicate) i -> false,
|
||||
(java.util.function.Predicate) w -> true)
|
||||
.setDefaultSplitAttributes(splitAttributes)
|
||||
.build();
|
||||
mController.registerSplit(mTransaction, container0, mActivity, container1, rule,
|
||||
splitAttributes);
|
||||
|
||||
// The placeholder TaskFragment should be finished even if the primary is finished with
|
||||
// shouldFinishDependent = false.
|
||||
container0.finish(false /* shouldFinishDependent */, mPresenter, mTransaction, mController);
|
||||
|
||||
assertTrue(container0.isFinished());
|
||||
assertTrue(container1.isFinished());
|
||||
verify(mPresenter).deleteTaskFragment(mTransaction, container0.getTaskFragmentToken());
|
||||
verify(mPresenter).deleteTaskFragment(mTransaction, container1.getTaskFragmentToken());
|
||||
verify(mController).removeContainer(container0);
|
||||
verify(mController).removeContainer(container1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetInfo() {
|
||||
final TaskContainer taskContainer = createTestTaskContainer();
|
||||
@@ -493,8 +528,6 @@ public class TaskFragmentContainerTest {
|
||||
final TaskFragmentContainer tf1 = new TaskFragmentContainer(
|
||||
null /* pendingAppearedActivity */, new Intent(), taskContainer, mController,
|
||||
null /* pairedPrimaryTaskFragment */);
|
||||
taskContainer.mContainers.add(tf0);
|
||||
taskContainer.mContainers.add(tf1);
|
||||
|
||||
// When tf2 is created with using tf0 as pairedPrimaryContainer, tf2 should be inserted
|
||||
// right above tf0.
|
||||
@@ -505,6 +538,26 @@ public class TaskFragmentContainerTest {
|
||||
assertEquals(2, taskContainer.indexOf(tf1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNewContainerWithPairedPendingAppearedActivity() {
|
||||
final TaskContainer taskContainer = createTestTaskContainer();
|
||||
final TaskFragmentContainer tf0 = new TaskFragmentContainer(
|
||||
createMockActivity(), null /* pendingAppearedIntent */, taskContainer, mController,
|
||||
null /* pairedPrimaryTaskFragment */);
|
||||
final TaskFragmentContainer tf1 = new TaskFragmentContainer(
|
||||
null /* pendingAppearedActivity */, new Intent(), taskContainer, mController,
|
||||
null /* pairedPrimaryTaskFragment */);
|
||||
|
||||
// When tf2 is created with pendingAppearedActivity, tf2 should be inserted below any
|
||||
// TaskFragment without any Activity.
|
||||
final TaskFragmentContainer tf2 = new TaskFragmentContainer(
|
||||
createMockActivity(), null /* pendingAppearedIntent */, taskContainer, mController,
|
||||
null /* pairedPrimaryTaskFragment */);
|
||||
assertEquals(0, taskContainer.indexOf(tf0));
|
||||
assertEquals(1, taskContainer.indexOf(tf2));
|
||||
assertEquals(2, taskContainer.indexOf(tf1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsVisible() {
|
||||
final TaskContainer taskContainer = createTestTaskContainer();
|
||||
|
||||
@@ -1949,6 +1949,13 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
|
||||
creationParams.getPairedPrimaryFragmentToken());
|
||||
final int pairedPosition = ownerTask.mChildren.indexOf(pairedPrimaryTaskFragment);
|
||||
position = pairedPosition != -1 ? pairedPosition + 1 : POSITION_TOP;
|
||||
} else if (creationParams.getPairedActivityToken() != null) {
|
||||
// When there is a paired Activity, we want to place the new TaskFragment right above
|
||||
// the paired Activity to make sure the Activity position is not changed after reparent.
|
||||
final ActivityRecord pairedActivity = ActivityRecord.forTokenLocked(
|
||||
creationParams.getPairedActivityToken());
|
||||
final int pairedPosition = ownerTask.mChildren.indexOf(pairedActivity);
|
||||
position = pairedPosition != -1 ? pairedPosition + 1 : POSITION_TOP;
|
||||
} else {
|
||||
position = POSITION_TOP;
|
||||
}
|
||||
|
||||
@@ -795,6 +795,40 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
|
||||
task.mChildren.indexOf(activityOnTop));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testApplyTransaction_createTaskFragment_withPairedActivityToken() {
|
||||
final Task task = createTask(mDisplayContent);
|
||||
final ActivityRecord activityAtBottom = createActivityRecord(task);
|
||||
final int uid = Binder.getCallingUid();
|
||||
activityAtBottom.info.applicationInfo.uid = uid;
|
||||
activityAtBottom.getTask().effectiveUid = uid;
|
||||
mTaskFragment = new TaskFragmentBuilder(mAtm)
|
||||
.setParentTask(task)
|
||||
.setFragmentToken(mFragmentToken)
|
||||
.createActivityCount(1)
|
||||
.build();
|
||||
mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment);
|
||||
final IBinder fragmentToken1 = new Binder();
|
||||
final TaskFragmentCreationParams params = new TaskFragmentCreationParams.Builder(
|
||||
mOrganizerToken, fragmentToken1, activityAtBottom.token)
|
||||
.setPairedActivityToken(activityAtBottom.token)
|
||||
.build();
|
||||
mTransaction.setTaskFragmentOrganizer(mIOrganizer);
|
||||
mTransaction.createTaskFragment(params);
|
||||
assertApplyTransactionAllowed(mTransaction);
|
||||
|
||||
// Successfully created a TaskFragment.
|
||||
final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment(
|
||||
fragmentToken1);
|
||||
assertNotNull(taskFragment);
|
||||
// The new TaskFragment should be positioned right above the paired activity.
|
||||
assertEquals(task.mChildren.indexOf(activityAtBottom) + 1,
|
||||
task.mChildren.indexOf(taskFragment));
|
||||
// The top TaskFragment should remain on top.
|
||||
assertEquals(task.mChildren.indexOf(taskFragment) + 1,
|
||||
task.mChildren.indexOf(mTaskFragment));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testApplyTransaction_enforceHierarchyChange_reparentChildren() {
|
||||
doReturn(true).when(mTaskFragment).isAttached();
|
||||
|
||||
Reference in New Issue
Block a user