diff options
8 files changed, 385 insertions, 114 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); diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index 867833a3271a..d24ef7529494 100644 --- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java @@ -607,6 +607,13 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr int opType, @NonNull Throwable exception) { validateAndGetState(organizer); Slog.w(TAG, "onTaskFragmentError ", exception); + final PendingTaskFragmentEvent vanishedEvent = taskFragment != null + ? getPendingTaskFragmentEvent(taskFragment, PendingTaskFragmentEvent.EVENT_VANISHED) + : null; + if (vanishedEvent != null) { + // No need to notify if the TaskFragment has been removed. + return; + } addPendingEvent(new PendingTaskFragmentEvent.Builder( PendingTaskFragmentEvent.EVENT_ERROR, organizer) .setErrorCallbackToken(errorCallbackToken) @@ -878,23 +885,6 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr return null; } - private boolean shouldSendEventWhenTaskInvisible(@NonNull PendingTaskFragmentEvent event) { - if (event.mEventType == PendingTaskFragmentEvent.EVENT_ERROR - // Always send parent info changed to update task visibility - || event.mEventType == PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED) { - return true; - } - - final TaskFragmentOrganizerState state = - mTaskFragmentOrganizerState.get(event.mTaskFragmentOrg.asBinder()); - final TaskFragmentInfo lastInfo = state.mLastSentTaskFragmentInfos.get(event.mTaskFragment); - final TaskFragmentInfo info = event.mTaskFragment.getTaskFragmentInfo(); - // Send an info changed callback if this event is for the last activities to finish in a - // TaskFragment so that the {@link TaskFragmentOrganizer} can delete this TaskFragment. - return event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED - && lastInfo != null && lastInfo.hasRunningActivity() && info.isEmpty(); - } - void dispatchPendingEvents() { if (mAtmService.mWindowManager.mWindowPlacerLocked.isLayoutDeferred() || mPendingTaskFragmentEvents.isEmpty()) { @@ -908,37 +898,19 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } } - void dispatchPendingEvents(@NonNull TaskFragmentOrganizerState state, + private void dispatchPendingEvents(@NonNull TaskFragmentOrganizerState state, @NonNull List<PendingTaskFragmentEvent> pendingEvents) { if (pendingEvents.isEmpty()) { return; } - - final ArrayList<Task> visibleTasks = new ArrayList<>(); - final ArrayList<Task> invisibleTasks = new ArrayList<>(); - final ArrayList<PendingTaskFragmentEvent> candidateEvents = new ArrayList<>(); - for (int i = 0, n = pendingEvents.size(); i < n; i++) { - final PendingTaskFragmentEvent event = pendingEvents.get(i); - final Task task = event.mTaskFragment != null ? event.mTaskFragment.getTask() : null; - // TODO(b/251132298): move visibility check to the client side. - if (task != null && (task.lastActiveTime <= event.mDeferTime - || !(isTaskVisible(task, visibleTasks, invisibleTasks) - || shouldSendEventWhenTaskInvisible(event)))) { - // Defer sending events to the TaskFragment until the host task is active again. - event.mDeferTime = task.lastActiveTime; - continue; - } - candidateEvents.add(event); - } - final int numEvents = candidateEvents.size(); - if (numEvents == 0) { + if (shouldDeferPendingEvents(state, pendingEvents)) { return; } - mTmpTaskSet.clear(); + final int numEvents = pendingEvents.size(); final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); for (int i = 0; i < numEvents; i++) { - final PendingTaskFragmentEvent event = candidateEvents.get(i); + final PendingTaskFragmentEvent event = pendingEvents.get(i); if (event.mEventType == PendingTaskFragmentEvent.EVENT_APPEARED || event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED) { final Task task = event.mTaskFragment.getTask(); @@ -954,7 +926,47 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } mTmpTaskSet.clear(); state.dispatchTransaction(transaction); - pendingEvents.removeAll(candidateEvents); + pendingEvents.clear(); + } + + /** + * Whether or not to defer sending the events to the organizer to avoid waking the app process + * when it is in background. We want to either send all events or none to avoid inconsistency. + */ + private boolean shouldDeferPendingEvents(@NonNull TaskFragmentOrganizerState state, + @NonNull List<PendingTaskFragmentEvent> pendingEvents) { + final ArrayList<Task> visibleTasks = new ArrayList<>(); + final ArrayList<Task> invisibleTasks = new ArrayList<>(); + for (int i = 0, n = pendingEvents.size(); i < n; i++) { + final PendingTaskFragmentEvent event = pendingEvents.get(i); + if (event.mEventType != PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED + && event.mEventType != PendingTaskFragmentEvent.EVENT_INFO_CHANGED + && event.mEventType != PendingTaskFragmentEvent.EVENT_APPEARED) { + // Send events for any other types. + return false; + } + + // Check if we should send the event given the Task visibility and events. + final Task task; + if (event.mEventType == PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED) { + task = event.mTask; + } else { + task = event.mTaskFragment.getTask(); + } + if (task.lastActiveTime > event.mDeferTime + && isTaskVisible(task, visibleTasks, invisibleTasks)) { + // Send events when the app has at least one visible Task. + return false; + } else if (shouldSendEventWhenTaskInvisible(task, state, event)) { + // Sent events even if the Task is invisible. + return false; + } + + // Defer sending events to the organizer until the host task is active (visible) again. + event.mDeferTime = task.lastActiveTime; + } + // Defer for invisible Task. + return true; } private static boolean isTaskVisible(@NonNull Task task, @@ -975,6 +987,28 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } } + private boolean shouldSendEventWhenTaskInvisible(@NonNull Task task, + @NonNull TaskFragmentOrganizerState state, + @NonNull PendingTaskFragmentEvent event) { + final TaskFragmentParentInfo lastParentInfo = state.mLastSentTaskFragmentParentInfos + .get(task.mTaskId); + if (lastParentInfo == null || lastParentInfo.isVisible()) { + // When the Task was visible, or when there was no Task info changed sent (in which case + // the organizer will consider it as visible by default), always send the event to + // update the Task visibility. + return true; + } + if (event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED) { + // Send info changed if the TaskFragment is becoming empty/non-empty so the + // organizer can choose whether or not to remove the TaskFragment. + final TaskFragmentInfo lastInfo = state.mLastSentTaskFragmentInfos + .get(event.mTaskFragment); + final boolean isEmpty = event.mTaskFragment.getNonFinishingActivityCount() == 0; + return lastInfo == null || lastInfo.isEmpty() != isEmpty; + } + return false; + } + void dispatchPendingInfoChangedEvent(@NonNull TaskFragment taskFragment) { final PendingTaskFragmentEvent event = getPendingTaskFragmentEvent(taskFragment, PendingTaskFragmentEvent.EVENT_INFO_CHANGED); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index 0b23359627fb..ad0b9b2c2a83 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -874,29 +874,87 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { @Test public void testDeferPendingTaskFragmentEventsOfInvisibleTask() { - // Task - TaskFragment - Activity. final Task task = createTask(mDisplayContent); final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) .setParentTask(task) .setOrganizer(mOrganizer) .setFragmentToken(mFragmentToken) .build(); - - // Mock the task to invisible doReturn(false).when(task).shouldBeVisible(any()); - // Sending events - taskFragment.mTaskFragmentAppearedSent = true; - mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); + // Dispatch the initial event in the Task to update the Task visibility to the organizer. + mController.onTaskFragmentAppeared(mIOrganizer, taskFragment); mController.dispatchPendingEvents(); + verify(mOrganizer).onTransactionReady(any()); - // Verifies that event was not sent + // Verify that events were not sent when the Task is in background. + clearInvocations(mOrganizer); + final Rect bounds = new Rect(0, 0, 500, 1000); + task.setBoundsUnchecked(bounds); + mController.onTaskFragmentParentInfoChanged(mIOrganizer, task); + mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); + mController.dispatchPendingEvents(); verify(mOrganizer, never()).onTransactionReady(any()); + + // Verify that the events were sent when the Task becomes visible. + doReturn(true).when(task).shouldBeVisible(any()); + task.lastActiveTime++; + mController.dispatchPendingEvents(); + verify(mOrganizer).onTransactionReady(any()); + } + + @Test + public void testSendAllPendingTaskFragmentEventsWhenAnyTaskIsVisible() { + // Invisible Task. + final Task invisibleTask = createTask(mDisplayContent); + final TaskFragment invisibleTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(invisibleTask) + .setOrganizer(mOrganizer) + .setFragmentToken(mFragmentToken) + .build(); + doReturn(false).when(invisibleTask).shouldBeVisible(any()); + + // Visible Task. + final IBinder fragmentToken = new Binder(); + final Task visibleTask = createTask(mDisplayContent); + final TaskFragment visibleTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(visibleTask) + .setOrganizer(mOrganizer) + .setFragmentToken(fragmentToken) + .build(); + doReturn(true).when(invisibleTask).shouldBeVisible(any()); + + // Sending events + invisibleTaskFragment.mTaskFragmentAppearedSent = true; + visibleTaskFragment.mTaskFragmentAppearedSent = true; + mController.onTaskFragmentInfoChanged(mIOrganizer, invisibleTaskFragment); + mController.onTaskFragmentInfoChanged(mIOrganizer, visibleTaskFragment); + mController.dispatchPendingEvents(); + + // Verify that both events are sent. + verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture()); + final TaskFragmentTransaction transaction = mTransactionCaptor.getValue(); + final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); + + // There should be two Task info changed with two TaskFragment info changed. + assertEquals(4, changes.size()); + // Invisible Task info changed + assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, changes.get(0).getType()); + assertEquals(invisibleTask.mTaskId, changes.get(0).getTaskId()); + // Invisible TaskFragment info changed + assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, changes.get(1).getType()); + assertEquals(invisibleTaskFragment.getFragmentToken(), + changes.get(1).getTaskFragmentToken()); + // Visible Task info changed + assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, changes.get(2).getType()); + assertEquals(visibleTask.mTaskId, changes.get(2).getTaskId()); + // Visible TaskFragment info changed + assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, changes.get(3).getType()); + assertEquals(visibleTaskFragment.getFragmentToken(), changes.get(3).getTaskFragmentToken()); } @Test public void testCanSendPendingTaskFragmentEventsAfterActivityResumed() { - // Task - TaskFragment - Activity. final Task task = createTask(mDisplayContent); final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) .setParentTask(task) @@ -905,24 +963,26 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { .createActivityCount(1) .build(); final ActivityRecord activity = taskFragment.getTopMostActivity(); - - // Mock the task to invisible doReturn(false).when(task).shouldBeVisible(any()); taskFragment.setResumedActivity(null, "test"); - // Sending events - taskFragment.mTaskFragmentAppearedSent = true; - mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); + // Dispatch the initial event in the Task to update the Task visibility to the organizer. + mController.onTaskFragmentAppeared(mIOrganizer, taskFragment); mController.dispatchPendingEvents(); + verify(mOrganizer).onTransactionReady(any()); - // Verifies that event was not sent + // Verify the info changed event is not sent because the Task is invisible + clearInvocations(mOrganizer); + final Rect bounds = new Rect(0, 0, 500, 1000); + task.setBoundsUnchecked(bounds); + mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); + mController.dispatchPendingEvents(); verify(mOrganizer, never()).onTransactionReady(any()); - // Mock the task becomes visible, and activity resumed + // Mock the task becomes visible, and activity resumed. Verify the info changed event is + // sent. doReturn(true).when(task).shouldBeVisible(any()); taskFragment.setResumedActivity(activity, "test"); - - // Verifies that event is sent. mController.dispatchPendingEvents(); verify(mOrganizer).onTransactionReady(any()); } @@ -977,25 +1037,24 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final ActivityRecord embeddedActivity = taskFragment.getTopNonFinishingActivity(); // Add another activity in the Task so that it always contains a non-finishing activity. createActivityRecord(task); - assertTrue(task.shouldBeVisible(null)); + doReturn(false).when(task).shouldBeVisible(any()); - // Dispatch pending info changed event from creating the activity - taskFragment.mTaskFragmentAppearedSent = true; - mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); + // Dispatch the initial event in the Task to update the Task visibility to the organizer. + mController.onTaskFragmentAppeared(mIOrganizer, taskFragment); mController.dispatchPendingEvents(); verify(mOrganizer).onTransactionReady(any()); - // Verify the info changed callback is not called when the task is invisible + // Verify the info changed event is not sent because the Task is invisible clearInvocations(mOrganizer); - doReturn(false).when(task).shouldBeVisible(any()); + final Rect bounds = new Rect(0, 0, 500, 1000); + task.setBoundsUnchecked(bounds); mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); mController.dispatchPendingEvents(); verify(mOrganizer, never()).onTransactionReady(any()); - // Finish the embedded activity, and verify the info changed callback is called because the + // Finish the embedded activity, and verify the info changed event is sent because the // TaskFragment is becoming empty. embeddedActivity.finishing = true; - mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); mController.dispatchPendingEvents(); verify(mOrganizer).onTransactionReady(any()); } |