From f7f106b4372b9b1a2254ae878aea91aaf55c7073 Mon Sep 17 00:00:00 2001 From: Chris Li Date: Tue, 24 May 2022 23:43:45 +0800 Subject: Add pendingAppearedIntent to TaskFragmentContainer 1. When an activity is launched without specifying launch TaskFragment, it may be placed to the primary TaskFragment of a split if all activities in secondary split are finishing. In this case, we still want to check if activity should be splitted with the activity below. In order to do that, we need to know if it should be split with the secondary TaskFragment, which may still be empty, so store the pending appeared Intent until the TaskFragment becomes non-empty. i. The secondary TaskFragment can be empty when the primary trys to start the secondary during #onCreated. ii. If the Intent never actually started, it will trigger #onTaskFragmentAppearEmptyTimeout to update the existing top container. 2. When we are finishing a TaskFragment, we used to finish all activities in it as well. We should not do that for activity that is requested to be reparented (as pending appeared). Fix: 233684054 Test: atest WMJetpackUnitTests:TaskFragmentContainerTest Test: atest WMJetpackUnitTests:SplitControllerTest Change-Id: I47329b23709da7e069a6a0c84124a3d7d08aae12 --- .../extensions/embedding/SplitController.java | 59 +++++++---- .../extensions/embedding/SplitPresenter.java | 6 +- .../embedding/TaskFragmentContainer.java | 66 ++++++++---- .../JetpackTaskFragmentOrganizerTest.java | 3 +- .../extensions/embedding/SplitControllerTest.java | 42 ++++++-- .../extensions/embedding/TaskContainerTest.java | 7 +- .../embedding/TaskFragmentContainerTest.java | 112 ++++++++++++++++++--- 7 files changed, 227 insertions(+), 68 deletions(-) diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 3380a23f3c09..575c3f002791 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -530,11 +530,18 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen 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. + // onCreate. final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); + final Intent secondaryIntent = secondaryContainer.getPendingAppearedIntent(); + if (secondaryIntent != null) { + // Check with the pending Intent before it is started on the server side. + // This can happen if the launched Activity start a new Intent to secondary during + // #onCreated(). + return getSplitRule(launchedActivity, secondaryIntent) != null; + } final Activity secondaryActivity = secondaryContainer.getTopNonFinishingActivity(); - return secondaryActivity == null - || getSplitRule(launchedActivity, secondaryActivity) != null; + return secondaryActivity != null + && getSplitRule(launchedActivity, secondaryActivity) != null; } // Check if the new launched activity is a placeholder. @@ -573,7 +580,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen Activity activityBelow = null; final TaskFragmentContainer container = getContainerWithActivity(activity); if (container != null) { - final List containerActivities = container.collectActivities(); + final List containerActivities = container.collectNonFinishingActivities(); final int index = containerActivities.indexOf(activity); if (index > 0) { activityBelow = containerActivities.get(index - 1); @@ -691,7 +698,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // 1. Whether the new activity intent should always expand. if (shouldExpand(null /* activity */, intent)) { - return createEmptyExpandedContainer(wct, taskId, launchingActivity); + return createEmptyExpandedContainer(wct, intent, taskId, launchingActivity); } // 2. Whether the launching activity (if set) should be split with the new activity intent. @@ -742,7 +749,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ @Nullable private TaskFragmentContainer createEmptyExpandedContainer( - @NonNull WindowContainerTransaction wct, int taskId, + @NonNull WindowContainerTransaction wct, @NonNull Intent intent, 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. @@ -759,8 +766,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // 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); + final TaskFragmentContainer expandedContainer = newContainer(intent, activityInTask, + taskId); mPresenter.createTaskFragment(wct, expandedContainer.getTaskFragmentToken(), activityInTask.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED); return expandedContainer; @@ -789,7 +796,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return splitContainer.getSecondaryContainer(); } // Create a new TaskFragment to split with the primary activity for the new activity. - return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, splitRule); + return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, intent, + splitRule); } /** @@ -813,21 +821,34 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return null; } - TaskFragmentContainer newContainer(@NonNull Activity activity, int taskId) { - return newContainer(activity, activity, taskId); + TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity, int taskId) { + return newContainer(pendingAppearedActivity, pendingAppearedActivity, taskId); + } + + TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity, + @NonNull Activity activityInTask, int taskId) { + return newContainer(pendingAppearedActivity, null /* pendingAppearedIntent */, + activityInTask, taskId); + } + + TaskFragmentContainer newContainer(@NonNull Intent pendingAppearedIntent, + @NonNull Activity activityInTask, int taskId) { + return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent, + activityInTask, taskId); } /** * Creates and registers a new organized container with an optional activity that will be * re-parented to it in a WCT. * - * @param activity the activity that will be reparented to the TaskFragment. - * @param activityInTask activity in the same Task so that we can get the Task bounds if - * needed. - * @param taskId parent Task of the new TaskFragment. + * @param pendingAppearedActivity the activity that will be reparented to the TaskFragment. + * @param pendingAppearedIntent the Intent that will be started in the TaskFragment. + * @param activityInTask activity in the same Task so that we can get the Task bounds + * if needed. + * @param taskId parent Task of the new TaskFragment. */ - TaskFragmentContainer newContainer(@Nullable Activity activity, - @NonNull Activity activityInTask, int taskId) { + TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity, + @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) { if (activityInTask == null) { throw new IllegalArgumentException("activityInTask must not be null,"); } @@ -835,8 +856,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen mTaskContainers.put(taskId, new TaskContainer(taskId)); } final TaskContainer taskContainer = mTaskContainers.get(taskId); - final TaskFragmentContainer container = new TaskFragmentContainer(activity, taskContainer, - this); + final TaskFragmentContainer container = new TaskFragmentContainer(pendingAppearedActivity, + pendingAppearedIntent, taskContainer, this); if (!taskContainer.isTaskBoundsInitialized()) { // Get the initial bounds before the TaskFragment has appeared. final Rect taskBounds = SplitPresenter.getTaskBoundsFromActivity(activityInTask); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 43d0402c1525..ac3b05a0e825 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -101,7 +101,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @NonNull TaskFragmentContainer createNewSplitWithEmptySideContainer( @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, - @NonNull SplitPairRule rule) { + @NonNull Intent secondaryIntent, @NonNull SplitPairRule rule) { final Rect parentBounds = getParentContainerBounds(primaryActivity); final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, isLtr(primaryActivity, rule)); @@ -111,7 +111,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // Create new empty task fragment final int taskId = primaryContainer.getTaskId(); final TaskFragmentContainer secondaryContainer = mController.newContainer( - null /* activity */, primaryActivity, taskId); + secondaryIntent, primaryActivity, taskId); final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, isLtr(primaryActivity, rule)); final int windowingMode = mController.getTaskContainer(taskId) @@ -224,7 +224,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } final int taskId = primaryContainer.getTaskId(); - TaskFragmentContainer secondaryContainer = mController.newContainer(null /* activity */, + final TaskFragmentContainer secondaryContainer = mController.newContainer(activityIntent, launchingActivity, taskId); final int windowingMode = mController.getTaskContainer(taskId) .getWindowingModeForSplitTaskFragment(primaryRectBounds); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index 26ddae4a0818..497c2478806e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -22,6 +22,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; import android.app.WindowConfiguration.WindowingMode; +import android.content.Intent; import android.graphics.Rect; import android.os.Binder; import android.os.IBinder; @@ -64,7 +65,16 @@ class TaskFragmentContainer { * Activities that are being reparented or being started to this container, but haven't been * added to {@link #mInfo} yet. */ - private final ArrayList mPendingAppearedActivities = new ArrayList<>(); + @VisibleForTesting + final ArrayList mPendingAppearedActivities = new ArrayList<>(); + + /** + * When this container is created for an {@link Intent} to start within, we store that Intent + * until the container becomes non-empty on the server side, so that we can use it to check + * rules associated with this container. + */ + @Nullable + private Intent mPendingAppearedIntent; /** Containers that are dependent on this one and should be completely destroyed on exit. */ private final List mContainersToFinishOnExit = @@ -99,15 +109,22 @@ class TaskFragmentContainer { * Creates a container with an existing activity that will be re-parented to it in a window * container transaction. */ - TaskFragmentContainer(@Nullable Activity activity, @NonNull TaskContainer taskContainer, + TaskFragmentContainer(@Nullable Activity pendingAppearedActivity, + @Nullable Intent pendingAppearedIntent, @NonNull TaskContainer taskContainer, @NonNull SplitController controller) { + if ((pendingAppearedActivity == null && pendingAppearedIntent == null) + || (pendingAppearedActivity != null && pendingAppearedIntent != null)) { + throw new IllegalArgumentException( + "One and only one of pending activity and intent must be non-null"); + } mController = controller; mToken = new Binder("TaskFragmentContainer"); mTaskContainer = taskContainer; taskContainer.mContainers.add(this); - if (activity != null) { - addPendingAppearedActivity(activity); + if (pendingAppearedActivity != null) { + addPendingAppearedActivity(pendingAppearedActivity); } + mPendingAppearedIntent = pendingAppearedIntent; } /** @@ -118,9 +135,9 @@ class TaskFragmentContainer { return mToken; } - /** List of activities that belong to this container and live in this process. */ + /** List of non-finishing activities that belong to this container and live in this process. */ @NonNull - List collectActivities() { + List collectNonFinishingActivities() { final List allActivities = new ArrayList<>(); if (mInfo != null) { // Add activities reported from the server. @@ -154,13 +171,14 @@ class TaskFragmentContainer { return false; } return mPendingAppearedActivities.isEmpty() - && mInfo.getActivities().size() == collectActivities().size(); + && mInfo.getActivities().size() == collectNonFinishingActivities().size(); } ActivityStack toActivityStack() { - return new ActivityStack(collectActivities(), isEmpty()); + return new ActivityStack(collectNonFinishingActivities(), isEmpty()); } + /** Adds the activity that will be reparented to this container. */ void addPendingAppearedActivity(@NonNull Activity pendingAppearedActivity) { if (hasActivity(pendingAppearedActivity.getActivityToken())) { return; @@ -174,6 +192,11 @@ class TaskFragmentContainer { mPendingAppearedActivities.remove(pendingAppearedActivity); } + @Nullable + Intent getPendingAppearedIntent() { + return mPendingAppearedIntent; + } + boolean hasActivity(@NonNull IBinder token) { if (mInfo != null && mInfo.getActivities().contains(token)) { return true; @@ -219,7 +242,12 @@ class TaskFragmentContainer { } mInfo = info; - if (mInfo == null || mPendingAppearedActivities.isEmpty()) { + if (mInfo == null || mInfo.isEmpty()) { + return; + } + // Only track the pending Intent when the container is empty. + mPendingAppearedIntent = null; + if (mPendingAppearedActivities.isEmpty()) { return; } // Cleanup activities that were being re-parented @@ -234,20 +262,13 @@ class TaskFragmentContainer { @Nullable Activity getTopNonFinishingActivity() { - List activities = collectActivities(); - if (activities.isEmpty()) { - return null; - } - int i = activities.size() - 1; - while (i >= 0 && activities.get(i).isFinishing()) { - i--; - } - return i >= 0 ? activities.get(i) : null; + final List activities = collectNonFinishingActivities(); + return activities.isEmpty() ? null : activities.get(activities.size() - 1); } @Nullable Activity getBottomMostActivity() { - final List activities = collectActivities(); + final List activities = collectNonFinishingActivities(); return activities.isEmpty() ? null : activities.get(0); } @@ -320,8 +341,11 @@ class TaskFragmentContainer { private void finishActivities(boolean shouldFinishDependent, @NonNull SplitPresenter presenter, @NonNull WindowContainerTransaction wct, @NonNull SplitController controller) { // Finish own activities - for (Activity activity : collectActivities()) { - if (!activity.isFinishing()) { + for (Activity activity : collectNonFinishingActivities()) { + if (!activity.isFinishing() + // In case we have requested to reparent the activity to another container (as + // pendingAppeared), we don't want to finish it with this container. + && mController.getContainerWithActivity(activity) == this) { activity.finish(); } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java index 792a53168a0d..a191e685f651 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java @@ -28,6 +28,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import android.content.Intent; import android.content.res.Configuration; import android.graphics.Point; import android.os.Handler; @@ -115,7 +116,7 @@ public class JetpackTaskFragmentOrganizerTest { public void testExpandTaskFragment() { final TaskContainer taskContainer = new TaskContainer(TASK_ID); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - taskContainer, mSplitController); + new Intent(), taskContainer, mSplitController); final TaskFragmentInfo info = createMockInfo(container); mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info); container.setInfo(info); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 2fd491312f63..937d670287ad 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -123,7 +123,7 @@ public class SplitControllerTest { final TaskContainer taskContainer = new TaskContainer(TASK_ID); // tf1 has no running activity so is not active. final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, - taskContainer, mSplitController); + new Intent(), taskContainer, mSplitController); // tf2 has running activity so is active. final TaskFragmentContainer tf2 = mock(TaskFragmentContainer.class); doReturn(1).when(tf2).getRunningActivityCount(); @@ -205,7 +205,8 @@ public class SplitControllerTest { assertThrows(IllegalArgumentException.class, () -> mSplitController.newContainer(mActivity, null /* launchingActivity */, TASK_ID)); - final TaskFragmentContainer tf = mSplitController.newContainer(null, mActivity, TASK_ID); + final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, mActivity, + TASK_ID); final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID); assertNotNull(tf); @@ -307,7 +308,7 @@ public class SplitControllerTest { @Test public void testOnActivityReparentToTask_diffProcess() { // Create an empty TaskFragment to initialize for the Task. - mSplitController.newContainer(null, mActivity, TASK_ID); + mSplitController.newContainer(new Intent(), mActivity, TASK_ID); final IBinder activityToken = new Binder(); final Intent intent = new Intent(); @@ -417,7 +418,7 @@ public class SplitControllerTest { verify(mSplitPresenter, never()).applyTransaction(any()); - mSplitController.newContainer(null /* activity */, mActivity, TASK_ID); + mSplitController.newContainer(new Intent(), mActivity, TASK_ID); mSplitController.placeActivityInTopContainer(mActivity); verify(mSplitPresenter).applyTransaction(any()); @@ -436,7 +437,7 @@ public class SplitControllerTest { false /* isOnReparent */); assertFalse(result); - verify(mSplitController, never()).newContainer(any(), any(), anyInt()); + verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); } @Test @@ -577,7 +578,7 @@ public class SplitControllerTest { final TaskFragmentContainer primaryContainer = mSplitController.newContainer(mActivity, TASK_ID); final TaskFragmentContainer secondaryContainer = mSplitController.newContainer( - null /* activity */, mActivity, TASK_ID); + secondaryIntent, mActivity, TASK_ID); mSplitController.registerSplit( mTransaction, primaryContainer, @@ -589,10 +590,35 @@ public class SplitControllerTest { false /* isOnReparent */); assertTrue(result); - verify(mSplitController, never()).newContainer(any(), any(), anyInt()); + verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any()); } + @Test + public void testResolveActivityToContainer_splitRule_inPrimarySplitWithNoRuleMatched() { + final Intent secondaryIntent = new Intent(); + setupSplitRule(mActivity, secondaryIntent); + final SplitPairRule splitRule = (SplitPairRule) mSplitController.getSplitRules().get(0); + + // The new launched activity is in primary split, but there is no rule for it to split with + // the secondary, so return false. + final TaskFragmentContainer primaryContainer = mSplitController.newContainer(mActivity, + TASK_ID); + final TaskFragmentContainer secondaryContainer = mSplitController.newContainer( + secondaryIntent, mActivity, TASK_ID); + mSplitController.registerSplit( + mTransaction, + primaryContainer, + mActivity, + secondaryContainer, + splitRule); + final Activity launchedActivity = createMockActivity(); + primaryContainer.addPendingAppearedActivity(launchedActivity); + + assertFalse(mSplitController.resolveActivityToContainer(launchedActivity, + false /* isOnReparent */)); + } + @Test public void testResolveActivityToContainer_splitRule_inSecondarySplitWithRuleMatched() { final Activity primaryActivity = createMockActivity(); @@ -605,7 +631,7 @@ public class SplitControllerTest { false /* isOnReparent */); assertTrue(result); - verify(mSplitController, never()).newContainer(any(), any(), anyInt()); + verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any()); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java index f1042ab6ce7d..ebe202db4e54 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java @@ -30,6 +30,7 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import android.app.Activity; +import android.content.Intent; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; @@ -142,7 +143,7 @@ public class TaskContainerTest { assertTrue(taskContainer.isEmpty()); final TaskFragmentContainer tf = new TaskFragmentContainer(null /* activity */, - taskContainer, mController); + new Intent(), taskContainer, mController); assertFalse(taskContainer.isEmpty()); @@ -158,11 +159,11 @@ public class TaskContainerTest { assertNull(taskContainer.getTopTaskFragmentContainer()); final TaskFragmentContainer tf0 = new TaskFragmentContainer(null /* activity */, - taskContainer, mController); + new Intent(), taskContainer, mController); assertEquals(tf0, taskContainer.getTopTaskFragmentContainer()); final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, - taskContainer, mController); + new Intent(), taskContainer, mController); assertEquals(tf1, taskContainer.getTopTaskFragmentContainer()); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java index 587878f3bf01..fcbd8a3ac020 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.clearInvocations; @@ -29,12 +30,17 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import android.annotation.NonNull; import android.app.Activity; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Point; 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.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -49,6 +55,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -72,19 +79,35 @@ public class TaskFragmentContainerTest { @Mock private Handler mHandler; private Activity mActivity; + private Intent mIntent; @Before public void setup() { MockitoAnnotations.initMocks(this); doReturn(mHandler).when(mController).getHandler(); mActivity = createMockActivity(); + mIntent = new Intent(); + } + + @Test + public void testNewContainer() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + + // One of the activity and the intent must be non-null + assertThrows(IllegalArgumentException.class, + () -> new TaskFragmentContainer(null, null, taskContainer, mController)); + + // One of the activity and the intent must be null. + assertThrows(IllegalArgumentException.class, + () -> new TaskFragmentContainer(mActivity, mIntent, taskContainer, mController)); } @Test public void testFinish() { final TaskContainer taskContainer = new TaskContainer(TASK_ID); - final TaskFragmentContainer container = new TaskFragmentContainer(mActivity, taskContainer, - mController); + final TaskFragmentContainer container = new TaskFragmentContainer(mActivity, + null /* pendingAppearedIntent */, taskContainer, mController); + doReturn(container).when(mController).getContainerWithActivity(mActivity); final WindowContainerTransaction wct = new WindowContainerTransaction(); // Only remove the activity, but not clear the reference until appeared. @@ -112,11 +135,60 @@ public class TaskFragmentContainerTest { verify(mController).removeContainer(container); } + @Test + public void testFinish_notFinishActivityThatIsReparenting() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity, + null /* pendingAppearedIntent */, taskContainer, mController); + final TaskFragmentInfo info = createMockTaskFragmentInfo(container0, mActivity); + container0.setInfo(info); + // Request to reparent the activity to a new TaskFragment. + final TaskFragmentContainer container1 = new TaskFragmentContainer(mActivity, + null /* pendingAppearedIntent */, taskContainer, mController); + 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); + + verify(mActivity, never()).finish(); + verify(mPresenter).deleteTaskFragment(wct, container0.getTaskFragmentToken()); + verify(mController).removeContainer(container0); + } + + @Test + public void testSetInfo() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + // Pending activity should be cleared when it has appeared on server side. + final TaskFragmentContainer pendingActivityContainer = new TaskFragmentContainer(mActivity, + null /* pendingAppearedIntent */, taskContainer, mController); + + assertTrue(pendingActivityContainer.mPendingAppearedActivities.contains(mActivity)); + + final TaskFragmentInfo info0 = createMockTaskFragmentInfo(pendingActivityContainer, + mActivity); + pendingActivityContainer.setInfo(info0); + + assertTrue(pendingActivityContainer.mPendingAppearedActivities.isEmpty()); + + // Pending intent should be cleared when the container becomes non-empty. + final TaskFragmentContainer pendingIntentContainer = new TaskFragmentContainer( + null /* pendingAppearedActivity */, mIntent, taskContainer, mController); + + assertEquals(mIntent, pendingIntentContainer.getPendingAppearedIntent()); + + final TaskFragmentInfo info1 = createMockTaskFragmentInfo(pendingIntentContainer, + mActivity); + pendingIntentContainer.setInfo(info1); + + assertNull(pendingIntentContainer.getPendingAppearedIntent()); + } + @Test public void testIsWaitingActivityAppear() { final TaskContainer taskContainer = new TaskContainer(TASK_ID); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - taskContainer, mController); + mIntent, taskContainer, mController); assertTrue(container.isWaitingActivityAppear()); @@ -137,7 +209,7 @@ public class TaskFragmentContainerTest { public void testAppearEmptyTimeout() { final TaskContainer taskContainer = new TaskContainer(TASK_ID); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - taskContainer, mController); + mIntent, taskContainer, mController); assertNull(container.mAppearEmptyTimeout); @@ -173,16 +245,16 @@ public class TaskFragmentContainerTest { } @Test - public void testCollectActivities() { + public void testCollectNonFinishingActivities() { final TaskContainer taskContainer = new TaskContainer(TASK_ID); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - taskContainer, mController); - List activities = container.collectActivities(); + mIntent, taskContainer, mController); + List activities = container.collectNonFinishingActivities(); assertTrue(activities.isEmpty()); container.addPendingAppearedActivity(mActivity); - activities = container.collectActivities(); + activities = container.collectNonFinishingActivities(); assertEquals(1, activities.size()); @@ -192,7 +264,7 @@ public class TaskFragmentContainerTest { activity1.getActivityToken()); doReturn(runningActivities).when(mInfo).getActivities(); container.setInfo(mInfo); - activities = container.collectActivities(); + activities = container.collectNonFinishingActivities(); assertEquals(3, activities.size()); assertEquals(activity0, activities.get(0)); @@ -204,21 +276,21 @@ public class TaskFragmentContainerTest { public void testAddPendingActivity() { final TaskContainer taskContainer = new TaskContainer(TASK_ID); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - taskContainer, mController); + mIntent, taskContainer, mController); container.addPendingAppearedActivity(mActivity); - assertEquals(1, container.collectActivities().size()); + assertEquals(1, container.collectNonFinishingActivities().size()); container.addPendingAppearedActivity(mActivity); - assertEquals(1, container.collectActivities().size()); + assertEquals(1, container.collectNonFinishingActivities().size()); } @Test public void testGetBottomMostActivity() { final TaskContainer taskContainer = new TaskContainer(TASK_ID); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - taskContainer, mController); + mIntent, taskContainer, mController); container.addPendingAppearedActivity(mActivity); assertEquals(mActivity, container.getBottomMostActivity()); @@ -239,4 +311,18 @@ public class TaskFragmentContainerTest { doReturn(activity).when(mController).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 */); + } } -- cgit v1.2.3-59-g8ed1b