diff options
Diffstat (limited to 'libs')
6 files changed, 227 insertions, 49 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java index 00be5a6e3416..77284c4166bd 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java @@ -109,6 +109,12 @@ class SplitContainer { return (mSplitRule instanceof SplitPlaceholderRule); } + @NonNull + SplitInfo toSplitInfo() { + return new SplitInfo(mPrimaryContainer.toActivityStack(), + mSecondaryContainer.toActivityStack(), mSplitAttributes); + } + static boolean shouldFinishPrimaryWithSecondary(@NonNull SplitRule splitRule) { final boolean isPlaceholderContainer = splitRule instanceof SplitPlaceholderRule; final boolean shouldFinishPrimaryWithSecondary = (splitRule instanceof SplitPairRule) 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 bf7326a5b30e..1d513e444050 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -1422,6 +1422,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") void updateContainer(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { + if (!container.getTaskContainer().isVisible()) { + // Wait until the Task is visible to avoid unnecessary update when the Task is still in + // background. + return; + } if (launchPlaceholderIfNecessary(wct, container)) { // Placeholder was launched, the positions will be updated when the activity is added // to the secondary container. @@ -1643,16 +1648,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Notifies listeners about changes to split states if necessary. */ + @VisibleForTesting @GuardedBy("mLock") - private void updateCallbackIfNecessary() { - if (mEmbeddingCallback == null) { + void updateCallbackIfNecessary() { + if (mEmbeddingCallback == null || !readyToReportToClient()) { return; } - if (!allActivitiesCreated()) { - return; - } - List<SplitInfo> currentSplitStates = getActiveSplitStates(); - if (currentSplitStates == null || mLastReportedSplitStates.equals(currentSplitStates)) { + final List<SplitInfo> currentSplitStates = getActiveSplitStates(); + if (mLastReportedSplitStates.equals(currentSplitStates)) { return; } mLastReportedSplitStates.clear(); @@ -1661,48 +1664,27 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** - * @return a list of descriptors for currently active split states. If the value returned is - * null, that indicates that the active split states are in an intermediate state and should - * not be reported. + * Returns a list of descriptors for currently active split states. */ @GuardedBy("mLock") - @Nullable + @NonNull private List<SplitInfo> getActiveSplitStates() { - List<SplitInfo> splitStates = new ArrayList<>(); + final List<SplitInfo> splitStates = new ArrayList<>(); for (int i = mTaskContainers.size() - 1; i >= 0; i--) { - final List<SplitContainer> splitContainers = mTaskContainers.valueAt(i) - .mSplitContainers; - for (SplitContainer container : splitContainers) { - if (container.getPrimaryContainer().isEmpty() - || container.getSecondaryContainer().isEmpty()) { - // We are in an intermediate state because either the split container is about - // to be removed or the primary or secondary container are about to receive an - // activity. - return null; - } - final ActivityStack primaryContainer = container.getPrimaryContainer() - .toActivityStack(); - final ActivityStack secondaryContainer = container.getSecondaryContainer() - .toActivityStack(); - final SplitInfo splitState = new SplitInfo(primaryContainer, secondaryContainer, - container.getSplitAttributes()); - splitStates.add(splitState); - } + mTaskContainers.valueAt(i).getSplitStates(splitStates); } return splitStates; } /** - * Checks if all activities that are registered with the containers have already appeared in - * the client. + * Whether we can now report the split states to the client. */ - private boolean allActivitiesCreated() { + @GuardedBy("mLock") + private boolean readyToReportToClient() { for (int i = mTaskContainers.size() - 1; i >= 0; i--) { - final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers; - for (TaskFragmentContainer container : containers) { - if (!container.taskInfoActivityCountMatchesCreated()) { - return false; - } + if (mTaskContainers.valueAt(i).isInIntermediateState()) { + // If any Task is in an intermediate state, wait for the server update. + return false; } } return true; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index 00943f2d53e1..231da0542e95 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -221,6 +221,24 @@ class TaskContainer { return mContainers.indexOf(child); } + /** Whether the Task is in an intermediate state waiting for the server update.*/ + boolean isInIntermediateState() { + for (TaskFragmentContainer container : mContainers) { + if (container.isInIntermediateState()) { + // We are in an intermediate state to wait for server update on this TaskFragment. + return true; + } + } + return false; + } + + /** Adds the descriptors of split states in this Task to {@code outSplitStates}. */ + void getSplitStates(@NonNull List<SplitInfo> outSplitStates) { + for (SplitContainer container : mSplitContainers) { + outSplitStates.add(container.toSplitInfo()); + } + } + /** * A wrapper class which contains the display ID and {@link Configuration} of a * {@link TaskContainer} 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 18712aed1be6..71b884018bdb 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -166,16 +166,34 @@ class TaskFragmentContainer { return allActivities; } - /** - * Checks if the count of activities from the same process in task fragment info corresponds to - * the ones created and available on the client side. - */ - boolean taskInfoActivityCountMatchesCreated() { + /** Whether the TaskFragment is in an intermediate state waiting for the server update.*/ + boolean isInIntermediateState() { if (mInfo == null) { - return false; + // Haven't received onTaskFragmentAppeared event. + return true; + } + if (mInfo.isEmpty()) { + // Empty TaskFragment will be removed or will have activity launched into it soon. + return true; + } + if (!mPendingAppearedActivities.isEmpty()) { + // Reparented activity hasn't appeared. + return true; } - return mPendingAppearedActivities.isEmpty() - && mInfo.getActivities().size() == collectNonFinishingActivities().size(); + // Check if there is any reported activity that is no longer alive. + for (IBinder token : mInfo.getActivities()) { + final Activity activity = mController.getActivity(token); + if (activity == null && !mTaskContainer.isVisible()) { + // Activity can be null if the activity is not attached to process yet. That can + // happen when the activity is started in background. + continue; + } + if (activity == null || activity.isFinishing()) { + // One of the reported activity is no longer alive, wait for the server update. + return true; + } + } + return false; } @NonNull 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 a40303150079..87d027899eb4 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 @@ -102,6 +102,7 @@ import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Consumer; /** * Test class for {@link SplitController}. @@ -132,6 +133,8 @@ public class SplitControllerTest { private SplitController mSplitController; private SplitPresenter mSplitPresenter; + private Consumer<List<SplitInfo>> mEmbeddingCallback; + private List<SplitInfo> mSplitInfos; private TransactionManager mTransactionManager; @Before @@ -141,9 +144,16 @@ public class SplitControllerTest { .getCurrentWindowLayoutInfo(anyInt(), any()); mSplitController = new SplitController(mWindowLayoutComponent); mSplitPresenter = mSplitController.mPresenter; + mSplitInfos = new ArrayList<>(); + mEmbeddingCallback = splitInfos -> { + mSplitInfos.clear(); + mSplitInfos.addAll(splitInfos); + }; + mSplitController.setSplitInfoCallback(mEmbeddingCallback); mTransactionManager = mSplitController.mTransactionManager; spyOn(mSplitController); spyOn(mSplitPresenter); + spyOn(mEmbeddingCallback); spyOn(mTransactionManager); doNothing().when(mSplitPresenter).applyTransaction(any(), anyInt(), anyBoolean()); final Configuration activityConfig = new Configuration(); @@ -329,6 +339,30 @@ public class SplitControllerTest { } @Test + public void testUpdateContainer_skipIfTaskIsInvisible() { + final Activity r0 = createMockActivity(); + final Activity r1 = createMockActivity(); + addSplitTaskFragments(r0, r1); + final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID); + final TaskFragmentContainer taskFragmentContainer = taskContainer.mContainers.get(0); + spyOn(taskContainer); + + // No update when the Task is invisible. + clearInvocations(mSplitPresenter); + doReturn(false).when(taskContainer).isVisible(); + mSplitController.updateContainer(mTransaction, taskFragmentContainer); + + verify(mSplitPresenter, never()).updateSplitContainer(any(), any(), any()); + + // Update the split when the Task is visible. + doReturn(true).when(taskContainer).isVisible(); + mSplitController.updateContainer(mTransaction, taskFragmentContainer); + + verify(mSplitPresenter).updateSplitContainer(taskContainer.mSplitContainers.get(0), + taskFragmentContainer, mTransaction); + } + + @Test public void testOnStartActivityResultError() { final Intent intent = new Intent(); final TaskContainer taskContainer = createTestTaskContainer(); @@ -1162,14 +1196,69 @@ public class SplitControllerTest { new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED))); } + @Test + public void testSplitInfoCallback_reportSplit() { + final Activity r0 = createMockActivity(); + final Activity r1 = createMockActivity(); + addSplitTaskFragments(r0, r1); + + mSplitController.updateCallbackIfNecessary(); + assertEquals(1, mSplitInfos.size()); + final SplitInfo splitInfo = mSplitInfos.get(0); + assertEquals(1, splitInfo.getPrimaryActivityStack().getActivities().size()); + assertEquals(1, splitInfo.getSecondaryActivityStack().getActivities().size()); + assertEquals(r0, splitInfo.getPrimaryActivityStack().getActivities().get(0)); + assertEquals(r1, splitInfo.getSecondaryActivityStack().getActivities().get(0)); + } + + @Test + public void testSplitInfoCallback_reportSplitInMultipleTasks() { + final int taskId0 = 1; + final int taskId1 = 2; + final Activity r0 = createMockActivity(taskId0); + final Activity r1 = createMockActivity(taskId0); + final Activity r2 = createMockActivity(taskId1); + final Activity r3 = createMockActivity(taskId1); + addSplitTaskFragments(r0, r1); + addSplitTaskFragments(r2, r3); + + mSplitController.updateCallbackIfNecessary(); + assertEquals(2, mSplitInfos.size()); + } + + @Test + public void testSplitInfoCallback_doNotReportIfInIntermediateState() { + final Activity r0 = createMockActivity(); + final Activity r1 = createMockActivity(); + addSplitTaskFragments(r0, r1); + final TaskFragmentContainer tf0 = mSplitController.getContainerWithActivity(r0); + final TaskFragmentContainer tf1 = mSplitController.getContainerWithActivity(r1); + spyOn(tf0); + spyOn(tf1); + + // Do not report if activity has not appeared in the TaskFragmentContainer in split. + doReturn(true).when(tf0).isInIntermediateState(); + mSplitController.updateCallbackIfNecessary(); + verify(mEmbeddingCallback, never()).accept(any()); + + doReturn(false).when(tf0).isInIntermediateState(); + mSplitController.updateCallbackIfNecessary(); + verify(mEmbeddingCallback).accept(any()); + } + /** Creates a mock activity in the organizer process. */ private Activity createMockActivity() { + return createMockActivity(TASK_ID); + } + + /** Creates a mock activity in the organizer process. */ + private Activity createMockActivity(int taskId) { 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); - doReturn(TASK_ID).when(activity).getTaskId(); + doReturn(taskId).when(activity).getTaskId(); doReturn(new ActivityInfo()).when(activity).getActivityInfo(); doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId(); return activity; @@ -1177,7 +1266,8 @@ public class SplitControllerTest { /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) { - final TaskFragmentContainer container = mSplitController.newContainer(activity, TASK_ID); + final TaskFragmentContainer container = mSplitController.newContainer(activity, + activity.getTaskId()); setupTaskFragmentInfo(container, activity); return container; } @@ -1268,7 +1358,7 @@ public class SplitControllerTest { // 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) + final int windowingMode = mSplitController.getTaskContainer(primaryContainer.getTaskId()) .getWindowingModeForSplitTaskFragment(TASK_BOUNDS); primaryContainer.setLastRequestedWindowingMode(windowingMode); secondaryContainer.setLastRequestedWindowingMode(windowingMode); 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 35415d816d8b..d43c471fb8ae 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 @@ -334,6 +334,70 @@ public class TaskFragmentContainerTest { assertFalse(container.hasActivity(mActivity.getActivityToken())); } + @Test + public void testIsInIntermediateState() { + // True if no info set. + final TaskContainer taskContainer = createTestTaskContainer(); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController); + spyOn(taskContainer); + doReturn(true).when(taskContainer).isVisible(); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // True if empty info set. + final List<IBinder> activities = new ArrayList<>(); + doReturn(activities).when(mInfo).getActivities(); + doReturn(true).when(mInfo).isEmpty(); + container.setInfo(mTransaction, mInfo); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // False if info is not empty. + doReturn(false).when(mInfo).isEmpty(); + container.setInfo(mTransaction, mInfo); + + assertFalse(container.isInIntermediateState()); + assertFalse(taskContainer.isInIntermediateState()); + + // True if there is pending appeared activity. + container.addPendingAppearedActivity(mActivity); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // True if the activity is finishing. + activities.add(mActivity.getActivityToken()); + doReturn(true).when(mActivity).isFinishing(); + container.setInfo(mTransaction, mInfo); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // False if the activity is not finishing. + doReturn(false).when(mActivity).isFinishing(); + container.setInfo(mTransaction, mInfo); + + assertFalse(container.isInIntermediateState()); + assertFalse(taskContainer.isInIntermediateState()); + + // True if there is a token that can't find associated activity. + activities.clear(); + activities.add(new Binder()); + container.setInfo(mTransaction, mInfo); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // False if there is a token that can't find associated activity when the Task is invisible. + doReturn(false).when(taskContainer).isVisible(); + + assertFalse(container.isInIntermediateState()); + assertFalse(taskContainer.isInIntermediateState()); + } + /** Creates a mock activity in the organizer process. */ private Activity createMockActivity() { final Activity activity = mock(Activity.class); |