From c6ab0bb710557af78d7de009b12afd76e860e9cd Mon Sep 17 00:00:00 2001 From: Chris Li Date: Wed, 22 Sep 2021 12:29:52 +0800 Subject: [PATCH] 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 --- data/etc/services.core.protolog.json | 12 +- .../server/wm/AppTransitionController.java | 170 +++++++++++------- .../core/java/com/android/server/wm/Task.java | 5 - .../com/android/server/wm/TaskFragment.java | 12 ++ .../wm/AppTransitionControllerTest.java | 38 ++++ 5 files changed, 163 insertions(+), 74 deletions(-) diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 119c939135a67..0bfc4a851948d 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -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", diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java index c0b69794966b0..82377b34d22a5 100644 --- a/services/core/java/com/android/server/wm/AppTransitionController.java +++ b/services/core/java/com/android/server/wm/AppTransitionController.java @@ -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 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; } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index a3626d215cf4b..3f6708d1843c0 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -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() { diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index a902ca913773a..77e7e535504d9 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -459,6 +459,11 @@ class TaskFragment extends 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 { 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); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java index 0d0cec7f5dce1..d6d7f07b2ef30 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java @@ -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()); + } } \ No newline at end of file