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:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user