Don't start app transition when there is empty TaskFragment

TaskFragment can be empty for either 1. wait for new launch activity, or
2. last activity just removed.

For 1, we want to wait for the activity launch.
For 2, we want to wait for organizer to remove the TaskFragment.

Bug: 196173550
Test: atest WmTests:AppTransitionControllerTest
      #testTransitionGoodToGoForTaskFragments
Change-Id: I73ac239ef2e5a75d05af6f6ae97e3f2278ebb4c3
This commit is contained in:
Chris Li
2021-09-22 12:29:52 +08:00
parent dde5115d05
commit c6ab0bb710
5 changed files with 163 additions and 74 deletions

View File

@@ -103,12 +103,6 @@
"group": "WM_DEBUG_STATES",
"at": "com\/android\/server\/wm\/TaskFragment.java"
},
"-2006946193": {
"message": "setClientVisible: %s clientVisible=%b Callers=%s",
"level": "VERBOSE",
"group": "WM_DEBUG_APP_TRANSITIONS",
"at": "com\/android\/server\/wm\/ActivityRecord.java"
},
"-2002500255": {
"message": "Defer removing snapshot surface in %dms",
"level": "VERBOSE",
@@ -553,6 +547,12 @@
"group": "WM_SHOW_TRANSACTIONS",
"at": "com\/android\/server\/wm\/RootWindowContainer.java"
},
"-1501564055": {
"message": "Organized TaskFragment is not ready= %s",
"level": "VERBOSE",
"group": "WM_DEBUG_APP_TRANSITIONS",
"at": "com\/android\/server\/wm\/AppTransitionController.java"
},
"-1499134947": {
"message": "Removing starting %s from %s",
"level": "VERBOSE",

View File

@@ -165,8 +165,8 @@ public class AppTransitionController {
void handleAppTransitionReady() {
mTempTransitionReasons.clear();
if (!transitionGoodToGo(mDisplayContent.mOpeningApps, mTempTransitionReasons)
|| !transitionGoodToGo(mDisplayContent.mChangingContainers,
mTempTransitionReasons)) {
|| !transitionGoodToGo(mDisplayContent.mChangingContainers, mTempTransitionReasons)
|| !transitionGoodToGoForTaskFragments()) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "AppTransitionReady");
@@ -599,6 +599,13 @@ public class AppTransitionController {
}
}
@Nullable
static Task findRootTaskFromContainer(WindowContainer wc) {
return wc.asTaskFragment() != null ? wc.asTaskFragment().getRootTask()
: wc.asActivityRecord().getRootTask();
}
@Nullable
static ActivityRecord getAppFromContainer(WindowContainer wc) {
return wc.asTaskFragment() != null ? wc.asTaskFragment().getTopNonFinishingActivity()
: wc.asActivityRecord();
@@ -972,72 +979,109 @@ public class AppTransitionController {
ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
"Checking %d opening apps (frozen=%b timeout=%b)...", apps.size(),
mService.mDisplayFrozen, mDisplayContent.mAppTransition.isTimeout());
if (mDisplayContent.mAppTransition.isTimeout()) {
return true;
}
final ScreenRotationAnimation screenRotationAnimation = mService.mRoot.getDisplayContent(
Display.DEFAULT_DISPLAY).getRotationAnimation();
if (!mDisplayContent.mAppTransition.isTimeout()) {
// Imagine the case where we are changing orientation due to an app transition, but a
// previous orientation change is still in progress. We won't process the orientation
// change for our transition because we need to wait for the rotation animation to
// finish.
// If we start the app transition at this point, we will interrupt it halfway with a
// new rotation animation after the old one finally finishes. It's better to defer the
// app transition.
if (screenRotationAnimation != null && screenRotationAnimation.isAnimating()
&& mDisplayContent.getDisplayRotation().needsUpdate()) {
ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
"Delaying app transition for screen rotation animation to finish");
return false;
}
for (int i = 0; i < apps.size(); i++) {
WindowContainer wc = apps.valueAt(i);
final ActivityRecord activity = getAppFromContainer(wc);
if (activity == null) {
continue;
}
ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
"Check opening app=%s: allDrawn=%b startingDisplayed=%b "
+ "startingMoved=%b isRelaunching()=%b startingWindow=%s",
activity, activity.allDrawn, activity.startingDisplayed,
activity.startingMoved, activity.isRelaunching(),
activity.mStartingWindow);
final boolean allDrawn = activity.allDrawn && !activity.isRelaunching();
if (!allDrawn && !activity.startingDisplayed && !activity.startingMoved) {
return false;
}
if (allDrawn) {
outReasons.put(activity, APP_TRANSITION_WINDOWS_DRAWN);
} else {
outReasons.put(activity,
activity.mStartingData instanceof SplashScreenStartingData
? APP_TRANSITION_SPLASH_SCREEN
: APP_TRANSITION_SNAPSHOT);
}
}
// We also need to wait for the specs to be fetched, if needed.
if (mDisplayContent.mAppTransition.isFetchingAppTransitionsSpecs()) {
ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "isFetchingAppTransitionSpecs=true");
return false;
}
if (!mDisplayContent.mUnknownAppVisibilityController.allResolved()) {
ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "unknownApps is not empty: %s",
mDisplayContent.mUnknownAppVisibilityController.getDebugMessage());
return false;
}
// If the wallpaper is visible, we need to check it's ready too.
boolean wallpaperReady = !mWallpaperControllerLocked.isWallpaperVisible() ||
mWallpaperControllerLocked.wallpaperTransitionReady();
if (wallpaperReady) {
return true;
}
// Imagine the case where we are changing orientation due to an app transition, but a
// previous orientation change is still in progress. We won't process the orientation
// change for our transition because we need to wait for the rotation animation to
// finish.
// If we start the app transition at this point, we will interrupt it halfway with a
// new rotation animation after the old one finally finishes. It's better to defer the
// app transition.
if (screenRotationAnimation != null && screenRotationAnimation.isAnimating()
&& mDisplayContent.getDisplayRotation().needsUpdate()) {
ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
"Delaying app transition for screen rotation animation to finish");
return false;
}
for (int i = 0; i < apps.size(); i++) {
WindowContainer wc = apps.valueAt(i);
final ActivityRecord activity = getAppFromContainer(wc);
if (activity == null) {
continue;
}
ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
"Check opening app=%s: allDrawn=%b startingDisplayed=%b "
+ "startingMoved=%b isRelaunching()=%b startingWindow=%s",
activity, activity.allDrawn, activity.startingDisplayed,
activity.startingMoved, activity.isRelaunching(),
activity.mStartingWindow);
final boolean allDrawn = activity.allDrawn && !activity.isRelaunching();
if (!allDrawn && !activity.startingDisplayed && !activity.startingMoved) {
return false;
}
if (allDrawn) {
outReasons.put(activity, APP_TRANSITION_WINDOWS_DRAWN);
} else {
outReasons.put(activity,
activity.mStartingData instanceof SplashScreenStartingData
? APP_TRANSITION_SPLASH_SCREEN
: APP_TRANSITION_SNAPSHOT);
}
}
// We also need to wait for the specs to be fetched, if needed.
if (mDisplayContent.mAppTransition.isFetchingAppTransitionsSpecs()) {
ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "isFetchingAppTransitionSpecs=true");
return false;
}
if (!mDisplayContent.mUnknownAppVisibilityController.allResolved()) {
ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "unknownApps is not empty: %s",
mDisplayContent.mUnknownAppVisibilityController.getDebugMessage());
return false;
}
// If the wallpaper is visible, we need to check it's ready too.
return !mWallpaperControllerLocked.isWallpaperVisible()
|| mWallpaperControllerLocked.wallpaperTransitionReady();
}
private boolean transitionGoodToGoForTaskFragments() {
if (mDisplayContent.mAppTransition.isTimeout()) {
return true;
}
// Check all Tasks in this transition. This is needed because new TaskFragment created for
// launching activity may not be in the tracking lists, but we still want to wait for the
// activity launch to start the transition.
final ArraySet<Task> rootTasks = new ArraySet<>();
for (int i = mDisplayContent.mOpeningApps.size() - 1; i >= 0; i--) {
rootTasks.add(mDisplayContent.mOpeningApps.valueAt(i).getRootTask());
}
for (int i = mDisplayContent.mClosingApps.size() - 1; i >= 0; i--) {
rootTasks.add(mDisplayContent.mClosingApps.valueAt(i).getRootTask());
}
for (int i = mDisplayContent.mChangingContainers.size() - 1; i >= 0; i--) {
rootTasks.add(
findRootTaskFromContainer(mDisplayContent.mChangingContainers.valueAt(i)));
}
// Organized TaskFragment can be empty for two situations:
// 1. New created and is waiting for Activity launch. In this case, we want to wait for
// the Activity launch to trigger the transition.
// 2. Last Activity is just removed. In this case, we want to wait for organizer to
// remove the TaskFragment because it may also want to change other TaskFragments in
// the same transition.
for (int i = rootTasks.size() - 1; i >= 0; i--) {
final Task rootTask = rootTasks.valueAt(i);
final boolean notReady = rootTask.forAllLeafTaskFragments(taskFragment -> {
if (!taskFragment.isReadyToTransit()) {
ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Organized TaskFragment is not ready= %s",
taskFragment);
return true;
}
return false;
});
if (notReady) {
return false;
}
}
return true;
}

View File

@@ -2347,11 +2347,6 @@ class Task extends TaskFragment {
return getRootTask().mTaskId;
}
@Nullable
Task getRootTask() {
return getRootTaskFragment().asTask();
}
/** @return the first organized task. */
@Nullable
Task getOrganizedTask() {

View File

@@ -459,6 +459,11 @@ class TaskFragment extends WindowContainer<WindowContainer> {
return parentTaskFragment == null ? this : parentTaskFragment.getRootTaskFragment();
}
@Nullable
Task getRootTask() {
return getRootTaskFragment().asTask();
}
@Override
TaskFragment asTaskFragment() {
return this;
@@ -2156,6 +2161,13 @@ class TaskFragment extends WindowContainer<WindowContainer> {
return mTaskFragmentOrganizer != null;
}
boolean isReadyToTransit() {
// We don't want to start the transition if the organized TaskFragment is empty, unless
// it is requested to be removed.
return !isOrganizedTaskFragment() || getTopNonFinishingActivity() != null
|| mIsRemovalRequested;
}
/** Clear {@link #mLastPausedActivity} for all {@link TaskFragment} children */
void clearLastPausedActivity() {
forAllTaskFragments(taskFragment -> taskFragment.mLastPausedActivity = null);

View File

@@ -31,13 +31,17 @@ import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.os.Binder;
@@ -791,4 +795,38 @@ public class AppTransitionControllerTest extends WindowTestsBase {
verify(mDisplayContent.mAppTransition)
.overridePendingAppTransitionRemote(adapter, false /* sync */);
}
@Test
public void testTransitionGoodToGoForTaskFragments() {
final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
final Task task = createTask(mDisplayContent);
final TaskFragment changeTaskFragment = new TaskFragmentBuilder(mAtm)
.setParentTask(task)
.createActivityCount(1)
.setOrganizer(organizer)
.build();
final TaskFragment emptyTaskFragment = new TaskFragmentBuilder(mAtm)
.setParentTask(task)
.setOrganizer(organizer)
.build();
changeTaskFragment.getTopMostActivity().allDrawn = true;
mDisplayContent.mAppTransition.prepareAppTransition(TRANSIT_CHANGE, 0);
mDisplayContent.mChangingContainers.add(changeTaskFragment);
spyOn(mDisplayContent.mAppTransition);
spyOn(emptyTaskFragment);
mDisplayContent.mAppTransitionController.handleAppTransitionReady();
// Transition not ready because there is an empty non-finishing TaskFragment.
verify(mDisplayContent.mAppTransition, never()).goodToGo(anyInt(), any());
doReturn(true).when(emptyTaskFragment).hasChild();
emptyTaskFragment.remove(false /* withTransition */, "test");
mDisplayContent.mAppTransitionController.handleAppTransitionReady();
// Transition ready because the empty (no running activity) TaskFragment is requested to be
// removed.
verify(mDisplayContent.mAppTransition).goodToGo(anyInt(), any());
}
}