summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Chris Li <lihongyu@google.com> 2022-10-25 06:05:23 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2022-10-25 06:05:23 +0000
commita8345a1f05a8c4168998c4510cd45d2dc55a95cc (patch)
treecf8e8eecfe8f6a4b82272ae8d6f22db005a1b95c
parent37840b8a919ce683c7f892cfecb16671dbf14f87 (diff)
parent5dafb54f14fa66857b9453bc6fb8e8e1386ef8b1 (diff)
Merge "Move the ActivityEmbedding Task visibility filter to the client" into tm-qpr-dev
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java6
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java58
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java18
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java34
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java96
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java64
-rw-r--r--services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java114
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java109
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());
}