Don't launch placeholder for bottom TaskFragment when waiting for top

1. When the top TaskFragment just appears, we should treat it as active
   even if it is empty, because we are expecting an activity to be
   launched into it soon. Before that happens, we don't want to launch
   placeholder for the activity behind.
2. When a TaskFragment is appeared as empty, set a timeout to remove it
   instead of waiting forever in case the activtiy start failed.

Fix: 230540531
Bug: 219683160
Test: atest WMJetpackUnitTests:SplitControllerTest
Test: atest WMJetpackUnitTests:TaskFragmentContainerTest
Change-Id: Ie05e379bf898b67912a849ad84f9fe7708e289c6
This commit is contained in:
Chris Li
2022-04-27 23:09:09 +08:00
parent ec77a90abc
commit c45e1c1ed5
6 changed files with 197 additions and 16 deletions

View File

@@ -81,9 +81,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@NonNull
private Consumer<List<SplitInfo>> mEmbeddingCallback;
private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>();
private final Handler mHandler;
public SplitController() {
mPresenter = new SplitPresenter(new MainThreadExecutor(), this);
final MainThreadExecutor executor = new MainThreadExecutor();
mHandler = executor.mHandler;
mPresenter = new SplitPresenter(executor, this);
ActivityThread activityThread = ActivityThread.currentActivityThread();
// Register a callback to be notified about activities being created.
activityThread.getApplication().registerActivityLifecycleCallbacks(
@@ -167,11 +170,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// to fullscreen.
cleanupForEnterPip(wct, container);
mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct);
} else {
} else if (taskFragmentInfo.isTaskClearedForReuse()) {
// Do not finish the dependents if this TaskFragment was cleared due to launching
// activity in the Task.
final boolean shouldFinishDependent = !taskFragmentInfo.isTaskClearedForReuse();
mPresenter.cleanupContainer(container, shouldFinishDependent, wct);
mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct);
} else if (!container.isWaitingActivityAppear()) {
// Do not finish the container before the expected activity appear until timeout.
mPresenter.cleanupContainer(container, true /* shouldFinishDependent */, wct);
}
} else if (wasInPip && isInPip) {
// No update until exit PIP.
@@ -417,6 +422,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
launchPlaceholderIfNecessary(activity);
}
/**
* Called when we have been waiting too long for the TaskFragment to become non-empty after
* creation.
*/
void onTaskFragmentAppearEmptyTimeout(@NonNull TaskFragmentContainer container) {
mPresenter.cleanupContainer(container, false /* shouldFinishDependent */);
}
/**
* Returns a container that this activity is registered with. An activity can only belong to one
* container, or no container at all.
@@ -452,7 +465,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
if (activityInTask == null) {
throw new IllegalArgumentException("activityInTask must not be null,");
}
final TaskFragmentContainer container = new TaskFragmentContainer(activity, taskId);
final TaskFragmentContainer container = new TaskFragmentContainer(activity, taskId, this);
if (!mTaskContainers.contains(taskId)) {
mTaskContainers.put(taskId, new TaskContainer(taskId));
}
@@ -590,7 +603,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
for (int i = taskContainer.mContainers.size() - 1; i >= 0; i--) {
final TaskFragmentContainer container = taskContainer.mContainers.get(i);
if (!container.isFinished() && container.getRunningActivityCount() > 0) {
if (!container.isFinished() && (container.getRunningActivityCount() > 0
// We may be waiting for the top TaskFragment to become non-empty after
// creation. In that case, we don't want to treat the TaskFragment below it as
// top active, otherwise it may incorrectly launch placeholder on top of the
// pending TaskFragment.
|| container.isWaitingActivityAppear())) {
return container;
}
}
@@ -920,6 +938,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return mTaskContainers.get(taskId);
}
Handler getHandler() {
return mHandler;
}
/**
* Returns {@code true} if an Activity with the provided component name should always be
* expanded to occupy full task bounds. Such activity must not be put in a split.

View File

@@ -30,6 +30,8 @@ import android.os.IBinder;
import android.window.TaskFragmentInfo;
import android.window.WindowContainerTransaction;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@@ -39,6 +41,11 @@ import java.util.List;
* on the server side.
*/
class TaskFragmentContainer {
private static final int APPEAR_EMPTY_TIMEOUT_MS = 3000;
@NonNull
private final SplitController mController;
/**
* Client-created token that uniquely identifies the task fragment container instance.
*/
@@ -51,7 +58,8 @@ class TaskFragmentContainer {
/**
* Server-provided task fragment information.
*/
private TaskFragmentInfo mInfo;
@VisibleForTesting
TaskFragmentInfo mInfo;
/**
* Activities that are being reparented or being started to this container, but haven't been
@@ -80,11 +88,21 @@ class TaskFragmentContainer {
@WindowingMode
private int mLastRequestedWindowingMode = WINDOWING_MODE_UNDEFINED;
/**
* When the TaskFragment has appeared in server, but is empty, we should remove the TaskFragment
* if it is still empty after the timeout.
*/
@VisibleForTesting
@Nullable
Runnable mAppearEmptyTimeout;
/**
* Creates a container with an existing activity that will be re-parented to it in a window
* container transaction.
*/
TaskFragmentContainer(@Nullable Activity activity, int taskId) {
TaskFragmentContainer(@Nullable Activity activity, int taskId,
@NonNull SplitController controller) {
mController = controller;
mToken = new Binder("TaskFragmentContainer");
if (taskId == INVALID_TASK_ID) {
throw new IllegalArgumentException("Invalid Task id");
@@ -155,12 +173,30 @@ class TaskFragmentContainer {
return count;
}
/** Whether we are waiting for the TaskFragment to appear and become non-empty. */
boolean isWaitingActivityAppear() {
return !mIsFinished && (mInfo == null || mAppearEmptyTimeout != null);
}
@Nullable
TaskFragmentInfo getInfo() {
return mInfo;
}
void setInfo(@NonNull TaskFragmentInfo info) {
if (!mIsFinished && mInfo == null && info.isEmpty()) {
// onTaskFragmentAppeared with empty info. We will remove the TaskFragment if it is
// still empty after timeout.
mAppearEmptyTimeout = () -> {
mAppearEmptyTimeout = null;
mController.onTaskFragmentAppearEmptyTimeout(this);
};
mController.getHandler().postDelayed(mAppearEmptyTimeout, APPEAR_EMPTY_TIMEOUT_MS);
} else if (mAppearEmptyTimeout != null && !info.isEmpty()) {
mController.getHandler().removeCallbacks(mAppearEmptyTimeout);
mAppearEmptyTimeout = null;
}
mInfo = info;
if (mInfo == null || mPendingAppearedActivities.isEmpty()) {
return;
@@ -234,6 +270,10 @@ class TaskFragmentContainer {
@NonNull WindowContainerTransaction wct, @NonNull SplitController controller) {
if (!mIsFinished) {
mIsFinished = true;
if (mAppearEmptyTimeout != null) {
mController.getHandler().removeCallbacks(mAppearEmptyTimeout);
mAppearEmptyTimeout = null;
}
finishActivities(shouldFinishDependent, presenter, wct, controller);
}

View File

@@ -18,6 +18,7 @@ package androidx.window.extensions.embedding;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
@@ -29,6 +30,7 @@ import static org.mockito.Mockito.never;
import android.content.res.Configuration;
import android.graphics.Point;
import android.os.Handler;
import android.platform.test.annotations.Presubmit;
import android.window.TaskFragmentInfo;
import android.window.WindowContainerToken;
@@ -61,6 +63,10 @@ public class JetpackTaskFragmentOrganizerTest {
private WindowContainerTransaction mTransaction;
@Mock
private JetpackTaskFragmentOrganizer.TaskFragmentCallback mCallback;
@Mock
private SplitController mSplitController;
@Mock
private Handler mHandler;
private JetpackTaskFragmentOrganizer mOrganizer;
@Before
@@ -69,6 +75,7 @@ public class JetpackTaskFragmentOrganizerTest {
mOrganizer = new JetpackTaskFragmentOrganizer(Runnable::run, mCallback);
mOrganizer.registerOrganizer();
spyOn(mOrganizer);
doReturn(mHandler).when(mSplitController).getHandler();
}
@Test
@@ -106,7 +113,8 @@ public class JetpackTaskFragmentOrganizerTest {
@Test
public void testExpandTaskFragment() {
final TaskFragmentContainer container = new TaskFragmentContainer(null, TASK_ID);
final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
TASK_ID, mSplitController);
final TaskFragmentInfo info = createMockInfo(container);
mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info);
container.setInfo(info);

View File

@@ -35,6 +35,7 @@ import android.app.Activity;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
import android.os.Handler;
import android.platform.test.annotations.Presubmit;
import android.window.TaskFragmentInfo;
import android.window.WindowContainerTransaction;
@@ -48,6 +49,7 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.List;
/**
@@ -71,6 +73,9 @@ public class SplitControllerTest {
private TaskFragmentInfo mInfo;
@Mock
private WindowContainerTransaction mTransaction;
@Mock
private Handler mHandler;
private SplitController mSplitController;
private SplitPresenter mSplitPresenter;
@@ -86,6 +91,7 @@ public class SplitControllerTest {
activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS);
doReturn(mActivityResources).when(mActivity).getResources();
doReturn(activityConfig).when(mActivityResources).getConfiguration();
doReturn(mHandler).when(mSplitController).getHandler();
}
@Test
@@ -94,28 +100,45 @@ public class SplitControllerTest {
// tf3 is finished so is not active.
TaskFragmentContainer tf3 = mock(TaskFragmentContainer.class);
doReturn(true).when(tf3).isFinished();
doReturn(false).when(tf3).isWaitingActivityAppear();
// tf2 has running activity so is active.
TaskFragmentContainer tf2 = mock(TaskFragmentContainer.class);
doReturn(1).when(tf2).getRunningActivityCount();
// tf1 has no running activity so is not active.
TaskFragmentContainer tf1 = new TaskFragmentContainer(null, TASK_ID);
TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, TASK_ID,
mSplitController);
taskContainer.mContainers.add(tf3);
taskContainer.mContainers.add(tf2);
taskContainer.mContainers.add(tf1);
taskContainer.mContainers.add(tf2);
taskContainer.mContainers.add(tf3);
mSplitController.mTaskContainers.put(TASK_ID, taskContainer);
assertWithMessage("Must return tf2 because tf3 is not active.")
.that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf2);
taskContainer.mContainers.remove(tf1);
taskContainer.mContainers.remove(tf3);
assertWithMessage("Must return tf2 because tf2 has running activity.")
.that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf2);
taskContainer.mContainers.remove(tf2);
assertWithMessage("Must return null because tf1 has no running activity.")
assertWithMessage("Must return tf because we are waiting for tf1 to appear.")
.that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf1);
final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
doReturn(new ArrayList<>()).when(info).getActivities();
doReturn(true).when(info).isEmpty();
tf1.setInfo(info);
assertWithMessage("Must return tf because we are waiting for tf1 to become non-empty after"
+ " creation.")
.that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf1);
doReturn(false).when(info).isEmpty();
tf1.setInfo(info);
assertWithMessage("Must return null because tf1 becomes empty.")
.that(mSplitController.getTopActiveContainer(TASK_ID)).isNull();
}
@@ -132,6 +155,14 @@ public class SplitControllerTest {
verify(mActivity, never()).finish();
}
@Test
public void testOnTaskFragmentAppearEmptyTimeout() {
final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID);
mSplitController.onTaskFragmentAppearEmptyTimeout(tf);
verify(mSplitPresenter).cleanupContainer(tf, false /* shouldFinishDependent */);
}
@Test
public void testNewContainer() {
// Must pass in a valid activity.

View File

@@ -32,8 +32,11 @@ import android.platform.test.annotations.Presubmit;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/**
* Test class for {@link TaskContainer}.
@@ -48,6 +51,14 @@ public class TaskContainerTest {
private static final int TASK_ID = 10;
private static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200);
@Mock
private SplitController mController;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testIsTaskBoundsInitialized() {
final TaskContainer taskContainer = new TaskContainer(TASK_ID);
@@ -126,7 +137,8 @@ public class TaskContainerTest {
assertTrue(taskContainer.isEmpty());
final TaskFragmentContainer tf = new TaskFragmentContainer(null, TASK_ID);
final TaskFragmentContainer tf = new TaskFragmentContainer(null /* activity */, TASK_ID,
mController);
taskContainer.mContainers.add(tf);
assertFalse(taskContainer.isEmpty());

View File

@@ -18,12 +18,18 @@ package androidx.window.extensions.embedding;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import android.app.Activity;
import android.os.Handler;
import android.platform.test.annotations.Presubmit;
import android.window.TaskFragmentInfo;
import android.window.WindowContainerTransaction;
@@ -59,15 +65,19 @@ public class TaskFragmentContainerTest {
private Activity mActivity;
@Mock
private TaskFragmentInfo mInfo;
@Mock
private Handler mHandler;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
doReturn(mHandler).when(mController).getHandler();
}
@Test
public void testFinish() {
final TaskFragmentContainer container = new TaskFragmentContainer(mActivity, TASK_ID);
final TaskFragmentContainer container = new TaskFragmentContainer(mActivity, TASK_ID,
mController);
final WindowContainerTransaction wct = new WindowContainerTransaction();
// Only remove the activity, but not clear the reference until appeared.
@@ -94,4 +104,62 @@ public class TaskFragmentContainerTest {
verify(mPresenter).deleteTaskFragment(wct, container.getTaskFragmentToken());
verify(mController).removeContainer(container);
}
@Test
public void testIsWaitingActivityAppear() {
final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
TASK_ID, mController);
assertTrue(container.isWaitingActivityAppear());
final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
doReturn(new ArrayList<>()).when(info).getActivities();
doReturn(true).when(info).isEmpty();
container.setInfo(info);
assertTrue(container.isWaitingActivityAppear());
doReturn(false).when(info).isEmpty();
container.setInfo(info);
assertFalse(container.isWaitingActivityAppear());
}
@Test
public void testAppearEmptyTimeout() {
final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
TASK_ID, mController);
assertNull(container.mAppearEmptyTimeout);
// Not set if it is not appeared empty.
final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
doReturn(new ArrayList<>()).when(info).getActivities();
doReturn(false).when(info).isEmpty();
container.setInfo(info);
assertNull(container.mAppearEmptyTimeout);
// Set timeout if the first info set is empty.
container.mInfo = null;
doReturn(true).when(info).isEmpty();
container.setInfo(info);
assertNotNull(container.mAppearEmptyTimeout);
// Remove timeout after the container becomes non-empty.
doReturn(false).when(info).isEmpty();
container.setInfo(info);
assertNull(container.mAppearEmptyTimeout);
// Running the timeout will call into SplitController.onTaskFragmentAppearEmptyTimeout.
container.mInfo = null;
doReturn(true).when(info).isEmpty();
container.setInfo(info);
container.mAppearEmptyTimeout.run();
assertNull(container.mAppearEmptyTimeout);
verify(mController).onTaskFragmentAppearEmptyTimeout(container);
}
}