Merge "Move the ActivityEmbedding Task visibility filter to the client" into tm-qpr-dev

This commit is contained in:
Chris Li
2022-10-25 06:05:23 +00:00
committed by Android (Google) Code Review
8 changed files with 385 additions and 114 deletions

View File

@@ -109,6 +109,12 @@ class SplitContainer {
return (mSplitRule instanceof SplitPlaceholderRule); return (mSplitRule instanceof SplitPlaceholderRule);
} }
@NonNull
SplitInfo toSplitInfo() {
return new SplitInfo(mPrimaryContainer.toActivityStack(),
mSecondaryContainer.toActivityStack(), mSplitAttributes);
}
static boolean shouldFinishPrimaryWithSecondary(@NonNull SplitRule splitRule) { static boolean shouldFinishPrimaryWithSecondary(@NonNull SplitRule splitRule) {
final boolean isPlaceholderContainer = splitRule instanceof SplitPlaceholderRule; final boolean isPlaceholderContainer = splitRule instanceof SplitPlaceholderRule;
final boolean shouldFinishPrimaryWithSecondary = (splitRule instanceof SplitPairRule) final boolean shouldFinishPrimaryWithSecondary = (splitRule instanceof SplitPairRule)

View File

@@ -1422,6 +1422,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@GuardedBy("mLock") @GuardedBy("mLock")
void updateContainer(@NonNull WindowContainerTransaction wct, void updateContainer(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer container) { @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)) { if (launchPlaceholderIfNecessary(wct, container)) {
// Placeholder was launched, the positions will be updated when the activity is added // Placeholder was launched, the positions will be updated when the activity is added
// to the secondary container. // to the secondary container.
@@ -1643,16 +1648,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
/** /**
* Notifies listeners about changes to split states if necessary. * Notifies listeners about changes to split states if necessary.
*/ */
@VisibleForTesting
@GuardedBy("mLock") @GuardedBy("mLock")
private void updateCallbackIfNecessary() { void updateCallbackIfNecessary() {
if (mEmbeddingCallback == null) { if (mEmbeddingCallback == null || !readyToReportToClient()) {
return; return;
} }
if (!allActivitiesCreated()) { final List<SplitInfo> currentSplitStates = getActiveSplitStates();
return; if (mLastReportedSplitStates.equals(currentSplitStates)) {
}
List<SplitInfo> currentSplitStates = getActiveSplitStates();
if (currentSplitStates == null || mLastReportedSplitStates.equals(currentSplitStates)) {
return; return;
} }
mLastReportedSplitStates.clear(); 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 * Returns a list of descriptors for currently active split states.
* null, that indicates that the active split states are in an intermediate state and should
* not be reported.
*/ */
@GuardedBy("mLock") @GuardedBy("mLock")
@Nullable @NonNull
private List<SplitInfo> getActiveSplitStates() { private List<SplitInfo> getActiveSplitStates() {
List<SplitInfo> splitStates = new ArrayList<>(); final List<SplitInfo> splitStates = new ArrayList<>();
for (int i = mTaskContainers.size() - 1; i >= 0; i--) { for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
final List<SplitContainer> splitContainers = mTaskContainers.valueAt(i) mTaskContainers.valueAt(i).getSplitStates(splitStates);
.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);
}
} }
return splitStates; return splitStates;
} }
/** /**
* Checks if all activities that are registered with the containers have already appeared in * Whether we can now report the split states to the client.
* the client.
*/ */
private boolean allActivitiesCreated() { @GuardedBy("mLock")
private boolean readyToReportToClient() {
for (int i = mTaskContainers.size() - 1; i >= 0; i--) { for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers; if (mTaskContainers.valueAt(i).isInIntermediateState()) {
for (TaskFragmentContainer container : containers) { // If any Task is in an intermediate state, wait for the server update.
if (!container.taskInfoActivityCountMatchesCreated()) { return false;
return false;
}
} }
} }
return true; return true;

View File

@@ -221,6 +221,24 @@ class TaskContainer {
return mContainers.indexOf(child); 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 * A wrapper class which contains the display ID and {@link Configuration} of a
* {@link TaskContainer} * {@link TaskContainer}

View File

@@ -166,16 +166,34 @@ class TaskFragmentContainer {
return allActivities; return allActivities;
} }
/** /** Whether the TaskFragment is in an intermediate state waiting for the server update.*/
* Checks if the count of activities from the same process in task fragment info corresponds to boolean isInIntermediateState() {
* the ones created and available on the client side.
*/
boolean taskInfoActivityCountMatchesCreated() {
if (mInfo == null) { if (mInfo == null) {
return false; // Haven't received onTaskFragmentAppeared event.
return true;
} }
return mPendingAppearedActivities.isEmpty() if (mInfo.isEmpty()) {
&& mInfo.getActivities().size() == collectNonFinishingActivities().size(); // 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;
}
// 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 @NonNull

View File

@@ -102,6 +102,7 @@ import org.mockito.MockitoAnnotations;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.function.Consumer;
/** /**
* Test class for {@link SplitController}. * Test class for {@link SplitController}.
@@ -132,6 +133,8 @@ public class SplitControllerTest {
private SplitController mSplitController; private SplitController mSplitController;
private SplitPresenter mSplitPresenter; private SplitPresenter mSplitPresenter;
private Consumer<List<SplitInfo>> mEmbeddingCallback;
private List<SplitInfo> mSplitInfos;
private TransactionManager mTransactionManager; private TransactionManager mTransactionManager;
@Before @Before
@@ -141,9 +144,16 @@ public class SplitControllerTest {
.getCurrentWindowLayoutInfo(anyInt(), any()); .getCurrentWindowLayoutInfo(anyInt(), any());
mSplitController = new SplitController(mWindowLayoutComponent); mSplitController = new SplitController(mWindowLayoutComponent);
mSplitPresenter = mSplitController.mPresenter; mSplitPresenter = mSplitController.mPresenter;
mSplitInfos = new ArrayList<>();
mEmbeddingCallback = splitInfos -> {
mSplitInfos.clear();
mSplitInfos.addAll(splitInfos);
};
mSplitController.setSplitInfoCallback(mEmbeddingCallback);
mTransactionManager = mSplitController.mTransactionManager; mTransactionManager = mSplitController.mTransactionManager;
spyOn(mSplitController); spyOn(mSplitController);
spyOn(mSplitPresenter); spyOn(mSplitPresenter);
spyOn(mEmbeddingCallback);
spyOn(mTransactionManager); spyOn(mTransactionManager);
doNothing().when(mSplitPresenter).applyTransaction(any(), anyInt(), anyBoolean()); doNothing().when(mSplitPresenter).applyTransaction(any(), anyInt(), anyBoolean());
final Configuration activityConfig = new Configuration(); final Configuration activityConfig = new Configuration();
@@ -328,6 +338,30 @@ public class SplitControllerTest {
verify(mSplitPresenter).updateSplitContainer(splitContainer, tf, mTransaction); verify(mSplitPresenter).updateSplitContainer(splitContainer, tf, mTransaction);
} }
@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 @Test
public void testOnStartActivityResultError() { public void testOnStartActivityResultError() {
final Intent intent = new Intent(); final Intent intent = new Intent();
@@ -1162,14 +1196,69 @@ public class SplitControllerTest {
new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED))); 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. */ /** Creates a mock activity in the organizer process. */
private Activity createMockActivity() { 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); final Activity activity = mock(Activity.class);
doReturn(mActivityResources).when(activity).getResources(); doReturn(mActivityResources).when(activity).getResources();
final IBinder activityToken = new Binder(); final IBinder activityToken = new Binder();
doReturn(activityToken).when(activity).getActivityToken(); doReturn(activityToken).when(activity).getActivityToken();
doReturn(activity).when(mSplitController).getActivity(activityToken); doReturn(activity).when(mSplitController).getActivity(activityToken);
doReturn(TASK_ID).when(activity).getTaskId(); doReturn(taskId).when(activity).getTaskId();
doReturn(new ActivityInfo()).when(activity).getActivityInfo(); doReturn(new ActivityInfo()).when(activity).getActivityInfo();
doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId(); doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId();
return activity; return activity;
@@ -1177,7 +1266,8 @@ public class SplitControllerTest {
/** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */
private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) { 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); setupTaskFragmentInfo(container, activity);
return container; return container;
} }
@@ -1268,7 +1358,7 @@ public class SplitControllerTest {
// We need to set those in case we are not respecting clear top. // We need to set those in case we are not respecting clear top.
// TODO(b/231845476) we should always respect clearTop. // 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); .getWindowingModeForSplitTaskFragment(TASK_BOUNDS);
primaryContainer.setLastRequestedWindowingMode(windowingMode); primaryContainer.setLastRequestedWindowingMode(windowingMode);
secondaryContainer.setLastRequestedWindowingMode(windowingMode); secondaryContainer.setLastRequestedWindowingMode(windowingMode);

View File

@@ -334,6 +334,70 @@ public class TaskFragmentContainerTest {
assertFalse(container.hasActivity(mActivity.getActivityToken())); 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. */ /** Creates a mock activity in the organizer process. */
private Activity createMockActivity() { private Activity createMockActivity() {
final Activity activity = mock(Activity.class); final Activity activity = mock(Activity.class);

View File

@@ -607,6 +607,13 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr
int opType, @NonNull Throwable exception) { int opType, @NonNull Throwable exception) {
validateAndGetState(organizer); validateAndGetState(organizer);
Slog.w(TAG, "onTaskFragmentError ", exception); 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( addPendingEvent(new PendingTaskFragmentEvent.Builder(
PendingTaskFragmentEvent.EVENT_ERROR, organizer) PendingTaskFragmentEvent.EVENT_ERROR, organizer)
.setErrorCallbackToken(errorCallbackToken) .setErrorCallbackToken(errorCallbackToken)
@@ -878,23 +885,6 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr
return null; 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() { void dispatchPendingEvents() {
if (mAtmService.mWindowManager.mWindowPlacerLocked.isLayoutDeferred() if (mAtmService.mWindowManager.mWindowPlacerLocked.isLayoutDeferred()
|| mPendingTaskFragmentEvents.isEmpty()) { || 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) { @NonNull List<PendingTaskFragmentEvent> pendingEvents) {
if (pendingEvents.isEmpty()) { if (pendingEvents.isEmpty()) {
return; return;
} }
if (shouldDeferPendingEvents(state, pendingEvents)) {
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) {
return; return;
} }
mTmpTaskSet.clear(); mTmpTaskSet.clear();
final int numEvents = pendingEvents.size();
final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
for (int i = 0; i < numEvents; i++) { 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 if (event.mEventType == PendingTaskFragmentEvent.EVENT_APPEARED
|| event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED) { || event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED) {
final Task task = event.mTaskFragment.getTask(); final Task task = event.mTaskFragment.getTask();
@@ -954,7 +926,47 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr
} }
mTmpTaskSet.clear(); mTmpTaskSet.clear();
state.dispatchTransaction(transaction); 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, 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) { void dispatchPendingInfoChangedEvent(@NonNull TaskFragment taskFragment) {
final PendingTaskFragmentEvent event = getPendingTaskFragmentEvent(taskFragment, final PendingTaskFragmentEvent event = getPendingTaskFragmentEvent(taskFragment,
PendingTaskFragmentEvent.EVENT_INFO_CHANGED); PendingTaskFragmentEvent.EVENT_INFO_CHANGED);

View File

@@ -874,29 +874,87 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
@Test @Test
public void testDeferPendingTaskFragmentEventsOfInvisibleTask() { public void testDeferPendingTaskFragmentEventsOfInvisibleTask() {
// Task - TaskFragment - Activity.
final Task task = createTask(mDisplayContent); final Task task = createTask(mDisplayContent);
final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
.setParentTask(task) .setParentTask(task)
.setOrganizer(mOrganizer) .setOrganizer(mOrganizer)
.setFragmentToken(mFragmentToken) .setFragmentToken(mFragmentToken)
.build(); .build();
// Mock the task to invisible
doReturn(false).when(task).shouldBeVisible(any()); doReturn(false).when(task).shouldBeVisible(any());
// Sending events // Dispatch the initial event in the Task to update the Task visibility to the organizer.
taskFragment.mTaskFragmentAppearedSent = true; mController.onTaskFragmentAppeared(mIOrganizer, taskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer).onTransactionReady(any());
// 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.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
mController.dispatchPendingEvents(); mController.dispatchPendingEvents();
// Verifies that event was not sent
verify(mOrganizer, never()).onTransactionReady(any()); 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 @Test
public void testCanSendPendingTaskFragmentEventsAfterActivityResumed() { public void testCanSendPendingTaskFragmentEventsAfterActivityResumed() {
// Task - TaskFragment - Activity.
final Task task = createTask(mDisplayContent); final Task task = createTask(mDisplayContent);
final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
.setParentTask(task) .setParentTask(task)
@@ -905,24 +963,26 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
.createActivityCount(1) .createActivityCount(1)
.build(); .build();
final ActivityRecord activity = taskFragment.getTopMostActivity(); final ActivityRecord activity = taskFragment.getTopMostActivity();
// Mock the task to invisible
doReturn(false).when(task).shouldBeVisible(any()); doReturn(false).when(task).shouldBeVisible(any());
taskFragment.setResumedActivity(null, "test"); taskFragment.setResumedActivity(null, "test");
// Sending events // Dispatch the initial event in the Task to update the Task visibility to the organizer.
taskFragment.mTaskFragmentAppearedSent = true; mController.onTaskFragmentAppeared(mIOrganizer, taskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer).onTransactionReady(any());
// 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.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
mController.dispatchPendingEvents(); mController.dispatchPendingEvents();
// Verifies that event was not sent
verify(mOrganizer, never()).onTransactionReady(any()); 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()); doReturn(true).when(task).shouldBeVisible(any());
taskFragment.setResumedActivity(activity, "test"); taskFragment.setResumedActivity(activity, "test");
// Verifies that event is sent.
mController.dispatchPendingEvents(); mController.dispatchPendingEvents();
verify(mOrganizer).onTransactionReady(any()); verify(mOrganizer).onTransactionReady(any());
} }
@@ -977,25 +1037,24 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
final ActivityRecord embeddedActivity = taskFragment.getTopNonFinishingActivity(); final ActivityRecord embeddedActivity = taskFragment.getTopNonFinishingActivity();
// Add another activity in the Task so that it always contains a non-finishing activity. // Add another activity in the Task so that it always contains a non-finishing activity.
createActivityRecord(task); createActivityRecord(task);
assertTrue(task.shouldBeVisible(null)); doReturn(false).when(task).shouldBeVisible(any());
// Dispatch pending info changed event from creating the activity // Dispatch the initial event in the Task to update the Task visibility to the organizer.
taskFragment.mTaskFragmentAppearedSent = true; mController.onTaskFragmentAppeared(mIOrganizer, taskFragment);
mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
mController.dispatchPendingEvents(); mController.dispatchPendingEvents();
verify(mOrganizer).onTransactionReady(any()); 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); 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.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
mController.dispatchPendingEvents(); mController.dispatchPendingEvents();
verify(mOrganizer, never()).onTransactionReady(any()); 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. // TaskFragment is becoming empty.
embeddedActivity.finishing = true; embeddedActivity.finishing = true;
mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
mController.dispatchPendingEvents(); mController.dispatchPendingEvents();
verify(mOrganizer).onTransactionReady(any()); verify(mOrganizer).onTransactionReady(any());
} }