diff options
3 files changed, 106 insertions, 9 deletions
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 786c635706e6..656a30561fe4 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -139,6 +139,12 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe /** The final animation targets derived from participants after promotion. */ private ArraySet<WindowContainer> mTargets = null; + /** + * Set of participating windowtokens (activity/wallpaper) which are visible at the end of + * the transition animation. + */ + private final ArraySet<WindowToken> mVisibleAtTransitionEndTokens = new ArraySet<>(); + /** Custom activity-level animation options and callbacks. */ private TransitionInfo.AnimationOptions mOverrideOptions; private IRemoteCallback mClientAnimationStartCallback = null; @@ -345,7 +351,14 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe for (int i = 0; i < mParticipants.size(); ++i) { final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord(); if (ar != null) { - if (!ar.isVisibleRequested()) { + boolean visibleAtTransitionEnd = mVisibleAtTransitionEndTokens.contains(ar); + // We need both the expected visibility AND current requested-visibility to be + // false. If it is expected-visible but not currently visible, it means that + // another animation is queued-up to animate this to invisibility, so we can't + // remove the surfaces yet. If it is currently visible, but not expected-visible, + // then doing commitVisibility here would actually be out-of-order and leave the + // activity in a bad state. + if (!visibleAtTransitionEnd && !ar.isVisibleRequested()) { boolean commitVisibility = true; if (ar.getDeferHidingClient() && ar.getTask() != null) { if (ar.pictureInPictureArgs != null @@ -368,17 +381,20 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe activitiesWentInvisible = true; } } - if (mChanges.get(ar).mVisible != ar.isVisibleRequested()) { + if (mChanges.get(ar).mVisible != visibleAtTransitionEnd) { // Legacy dispatch relies on this (for now). - ar.mEnteringAnimation = ar.isVisibleRequested(); + ar.mEnteringAnimation = visibleAtTransitionEnd; } mController.dispatchLegacyAppTransitionFinished(ar); } final WallpaperWindowToken wt = mParticipants.valueAt(i).asWallpaperToken(); - if (wt != null && !wt.isVisibleRequested()) { - ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, - " Commit wallpaper becoming invisible: %s", wt); - wt.commitVisibility(false /* visible */); + if (wt != null) { + final boolean visibleAtTransitionEnd = mVisibleAtTransitionEndTokens.contains(wt); + if (!visibleAtTransitionEnd && !wt.isVisibleRequested()) { + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + " Commit wallpaper becoming invisible: %s", wt); + wt.commitVisibility(false /* visible */); + } } } if (activitiesWentInvisible) { @@ -488,6 +504,15 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe } } + // Record windowtokens (activity/wallpaper) that are expected to be visible after the + // transition animation. This will be used in finishTransition to prevent prematurely + // committing visibility. + for (int i = mParticipants.size() - 1; i >= 0; --i) { + final WindowContainer wc = mParticipants.valueAt(i); + if (wc.asWindowToken() == null || !wc.isVisibleRequested()) continue; + mVisibleAtTransitionEndTokens.add(wc.asWindowToken()); + } + mStartTransaction = transaction; mFinishTransaction = mController.mAtm.mWindowManager.mTransactionFactory.get(); buildFinishTransaction(mFinishTransaction, info.getRootLeash()); diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 96309f247cde..419b4c3a706d 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -117,11 +117,16 @@ class TransitionController { void registerTransitionPlayer(@Nullable ITransitionPlayer player) { try { + // Note: asBinder() can be null if player is same process (likely in a test). if (mTransitionPlayer != null) { - mTransitionPlayer.asBinder().unlinkToDeath(mTransitionPlayerDeath, 0); + if (mTransitionPlayer.asBinder() != null) { + mTransitionPlayer.asBinder().unlinkToDeath(mTransitionPlayerDeath, 0); + } mTransitionPlayer = null; } - player.asBinder().linkToDeath(mTransitionPlayerDeath, 0); + if (player.asBinder() != null) { + player.asBinder().linkToDeath(mTransitionPlayerDeath, 0); + } mTransitionPlayer = player; } catch (RemoteException e) { throw new RuntimeException("Unable to set transition player"); diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 45e5f8e55f8a..6d60bcf1fce6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; +import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; @@ -41,6 +42,7 @@ import android.platform.test.annotations.Presubmit; import android.util.ArrayMap; import android.util.ArraySet; import android.window.ITaskOrganizer; +import android.window.ITransitionPlayer; import android.window.TransitionInfo; import androidx.test.filters.SmallTest; @@ -444,6 +446,71 @@ public class TransitionTests extends WindowTestsBase { info.getChange(openInChangeTask.mRemoteToken.toWindowContainerToken()), info)); } + @Test + public void testIntermediateVisibility() { + final TransitionController controller = new TransitionController(mAtm); + final ITransitionPlayer player = new ITransitionPlayer.Default(); + controller.registerTransitionPlayer(player); + ITaskOrganizer mockOrg = mock(ITaskOrganizer.class); + final Transition openTransition = controller.createTransition(TRANSIT_OPEN); + + // Start out with task2 visible and set up a transition that closes task2 and opens task1 + final Task task1 = createTask(mDisplayContent); + task1.mTaskOrganizer = mockOrg; + final ActivityRecord activity1 = createActivityRecord(task1); + activity1.mVisibleRequested = false; + activity1.setVisible(false); + final Task task2 = createTask(mDisplayContent); + task2.mTaskOrganizer = mockOrg; + final ActivityRecord activity2 = createActivityRecord(task1); + activity2.mVisibleRequested = true; + activity2.setVisible(true); + + openTransition.collectExistenceChange(task1); + openTransition.collectExistenceChange(activity1); + openTransition.collectExistenceChange(task2); + openTransition.collectExistenceChange(activity2); + + activity1.mVisibleRequested = true; + activity1.setVisible(true); + activity2.mVisibleRequested = false; + + // Using abort to force-finish the sync (since we can't wait for drawing in unit test). + // We didn't call abort on the transition itself, so it will still run onTransactionReady + // normally. + mWm.mSyncEngine.abort(openTransition.getSyncId()); + + // Before finishing openTransition, we are now going to simulate closing task1 to return + // back to (open) task2. + final Transition closeTransition = controller.createTransition(TRANSIT_CLOSE); + + closeTransition.collectExistenceChange(task1); + closeTransition.collectExistenceChange(activity1); + closeTransition.collectExistenceChange(task2); + closeTransition.collectExistenceChange(activity2); + + activity1.mVisibleRequested = false; + activity2.mVisibleRequested = true; + + openTransition.finishTransition(); + + // We finished the openTransition. Even though activity1 is visibleRequested=false, since + // the closeTransition animation hasn't played yet, make sure that we didn't commit + // visible=false on activity1 since it needs to remain visible for the animation. + assertTrue(activity1.isVisible()); + assertTrue(activity2.isVisible()); + + // Using abort to force-finish the sync (since we obviously can't wait for drawing). + // We didn't call abort on the actual transition, so it will still run onTransactionReady + // normally. + mWm.mSyncEngine.abort(closeTransition.getSyncId()); + + closeTransition.finishTransition(); + + assertFalse(activity1.isVisible()); + assertTrue(activity2.isVisible()); + } + /** Fill the change map with all the parents of top. Change maps are usually fully populated */ private static void fillChangeMap(ArrayMap<WindowContainer, Transition.ChangeInfo> changes, WindowContainer top) { |