From 3fef7ff6e225b742735673f60ac18048997f1cd7 Mon Sep 17 00:00:00 2001 From: Evan Rosky Date: Thu, 8 Apr 2021 14:01:26 -0700 Subject: [PATCH] Initial implementation of merge-able transitions This adds a couple things to support "merging" of transition animations in shell. 1. Moves the finishing surface operations into a transaction that gets sent to shell. This does a couple things: it complies with the contract we have where during a transition, only the player should touch relevant surfaces; and, it makes it so that the player can merge the transaction with other ones. 2. Keeps a queue of "pending" transition animations in shell and either merges them or runs them serially. 3. Any transition that becomes ready while another is playing will first be sent to the playing handler to give it a chance to "merge" the incoming transition. There are 3 expected responses to overlapping transition animations: 1. Cancel the currently playing transition and immediately start the incoming one. This can be achieved by having the currently playing transition cancel itself (ie. immediately finishing) when a merge request comes in. Then the rest of the shell transition logic will immediately start the next transition. 2. Queue up the incoming transition to play once the current one finishes animating. This is basically the default as long as the current transition simply rejects/ignores merge requests 3. Merge the incoming transition. This is achieved when the currently playing transition actually does some special logic to handle the incoming transition. It then calls the finish callback for the incoming transition (before it finishes its own animation) to indicate that it has been merged (or "consumed"). Basically, any time the finish callback for a transition is called before a pre-ceding transition, that transition is assumed to have been merged. Bug: 183994113 Test: atest ShellTransitionTests Change-Id: I3cb54e221d57642306ddf15827c21d8881b014d0 --- .../android/window/IRemoteTransition.aidl | 18 +- .../android/window/ITransitionPlayer.aidl | 5 +- libs/WindowManager/Shell/Android.bp | 2 - .../transition/OneShotRemoteHandler.java | 25 +- .../transition/RemoteTransitionHandler.java | 66 +++-- .../wm/shell/transition/Transitions.java | 232 +++++++++++++++--- .../splitscreen/SplitTransitionTests.java | 11 +- .../transition/ShellTransitionTests.java | 153 +++++++++++- .../system/RemoteAnimationAdapterCompat.java | 12 +- .../shared/system/RemoteTransitionCompat.java | 31 ++- .../shared/system/RemoteTransitionRunner.java | 14 +- .../com/android/server/wm/Transition.java | 95 ++++--- .../server/wm/TransitionController.java | 21 +- .../android/server/wm/WindowTestsBase.java | 3 +- 14 files changed, 571 insertions(+), 117 deletions(-) diff --git a/core/java/android/window/IRemoteTransition.aidl b/core/java/android/window/IRemoteTransition.aidl index e71de702334ae..2efb68a33889a 100644 --- a/core/java/android/window/IRemoteTransition.aidl +++ b/core/java/android/window/IRemoteTransition.aidl @@ -40,7 +40,23 @@ oneway interface IRemoteTransition { /** * Starts a transition animation. Once complete, the implementation should call * `finishCallback`. + * + * @param token An identifier for the transition that should be animated. */ - void startAnimation(in TransitionInfo info, in SurfaceControl.Transaction t, + void startAnimation(in IBinder token, in TransitionInfo info, in SurfaceControl.Transaction t, + in IRemoteTransitionFinishedCallback finishCallback); + + /** + * Attempts to merge a transition animation into the animation that is currently + * being played by this remote. If merge is not possible/supported, this should be a no-op. + * If it *is* merged, the implementation should call `finishCallback` immediately. + * + * @param transition An identifier for the transition that wants to be merged. + * @param mergeTarget The transition that is currently being animated by this remote. + * If it can be merged, call `finishCallback`; otherwise, do + * nothing. + */ + void mergeAnimation(in IBinder transition, in TransitionInfo info, + in SurfaceControl.Transaction t, in IBinder mergeTarget, in IRemoteTransitionFinishedCallback finishCallback); } diff --git a/core/java/android/window/ITransitionPlayer.aidl b/core/java/android/window/ITransitionPlayer.aidl index af37fbc6310f1..3eca803f3f38b 100644 --- a/core/java/android/window/ITransitionPlayer.aidl +++ b/core/java/android/window/ITransitionPlayer.aidl @@ -47,9 +47,12 @@ oneway interface ITransitionPlayer { * @param transitionToken An identifying token for the transition that is now ready to animate. * @param info A collection of all the changes encapsulated by this transition. * @param t A surface transaction containing the surface state prior to animating. + * @param finishT A surface transaction that will reset parenting/layering and generally put + * surfaces into their final (post-transition) state. Apply this after playing + * the animation but before calling finish. */ void onTransitionReady(in IBinder transitionToken, in TransitionInfo info, - in SurfaceControl.Transaction t); + in SurfaceControl.Transaction t, in SurfaceControl.Transaction finishT); /** * Called when something in WMCore requires a transition to play -- for example when an Activity diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 18f019dc949ba..9aaef3b1f6552 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -129,6 +129,4 @@ android_library { ], kotlincflags: ["-Xjvm-default=enable"], manifest: "AndroidManifest.xml", - - min_sdk_version: "26", } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java index 71fd917275eae..4da6664aa3dc5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java @@ -82,17 +82,38 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { if (mRemote.asBinder() != null) { mRemote.asBinder().linkToDeath(remoteDied, 0 /* flags */); } - mRemote.startAnimation(info, t, cb); + mRemote.startAnimation(transition, info, t, cb); } catch (RemoteException e) { + Log.e(Transitions.TAG, "Error running remote transition.", e); if (mRemote.asBinder() != null) { mRemote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */); } - Log.e(Transitions.TAG, "Error running remote transition.", e); finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); } return true; } + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Using registered One-shot remote" + + " transition %s for %s.", mRemote, transition); + + IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() { + @Override + public void onTransitionFinished(WindowContainerTransaction wct) { + mMainExecutor.execute( + () -> finishCallback.onTransitionFinished(wct, null /* wctCB */)); + } + }; + try { + mRemote.mergeAnimation(transition, info, t, mergeTarget, cb); + } catch (RemoteException e) { + Log.e(Transitions.TAG, "Error merging remote transition.", e); + } + } + @Override @Nullable public WindowContainerTransaction handleRequest(@NonNull IBinder transition, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java index 8876e5f861391..9bfb261fcb85d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java @@ -16,8 +16,6 @@ package com.android.wm.shell.transition; -import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; - import android.annotation.NonNull; import android.annotation.Nullable; import android.os.IBinder; @@ -52,7 +50,7 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { private final ShellExecutor mMainExecutor; /** Includes remotes explicitly requested by, eg, ActivityOptions */ - private final ArrayMap mPendingRemotes = new ArrayMap<>(); + private final ArrayMap mRequestedRemotes = new ArrayMap<>(); /** Ordered by specificity. Last filters will be checked first */ private final ArrayList> mFilters = @@ -63,9 +61,7 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { @Override @BinderThread public void binderDied() { - mMainExecutor.execute(() -> { - mFilters.clear(); - }); + mMainExecutor.execute(() -> mFilters.clear()); } }; @@ -96,11 +92,16 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { } } + @Override + public void onTransitionMerged(@NonNull IBinder transition) { + mRequestedRemotes.remove(transition); + } + @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull Transitions.TransitionFinishCallback finishCallback) { - IRemoteTransition pendingRemote = mPendingRemotes.remove(transition); + IRemoteTransition pendingRemote = mRequestedRemotes.get(transition); if (pendingRemote == null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s doesn't have " + "explicit remote, search filters for match for %s", transition, info); @@ -110,6 +111,8 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { mFilters.get(i)); if (mFilters.get(i).first.matches(info)) { pendingRemote = mFilters.get(i).second; + // Add to requested list so that it can be found for merge requests. + mRequestedRemotes.put(transition, pendingRemote); break; } } @@ -122,8 +125,10 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { final IRemoteTransition remote = pendingRemote; final IBinder.DeathRecipient remoteDied = () -> { Log.e(Transitions.TAG, "Remote transition died, finishing"); - mMainExecutor.execute( - () -> finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */)); + mMainExecutor.execute(() -> { + mRequestedRemotes.remove(transition); + finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); + }); }; IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() { @Override @@ -131,33 +136,66 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { if (remote.asBinder() != null) { remote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */); } - mMainExecutor.execute( - () -> finishCallback.onTransitionFinished(wct, null /* wctCB */)); + mMainExecutor.execute(() -> { + mRequestedRemotes.remove(transition); + finishCallback.onTransitionFinished(wct, null /* wctCB */); + }); } }; try { if (remote.asBinder() != null) { remote.asBinder().linkToDeath(remoteDied, 0 /* flags */); } - remote.startAnimation(info, t, cb); + remote.startAnimation(transition, info, t, cb); } catch (RemoteException e) { + Log.e(Transitions.TAG, "Error running remote transition.", e); if (remote.asBinder() != null) { remote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */); } - Log.e(Transitions.TAG, "Error running remote transition.", e); + mRequestedRemotes.remove(transition); mMainExecutor.execute( () -> finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */)); } return true; } + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + final IRemoteTransition remote = mRequestedRemotes.get(mergeTarget); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Attempt merge %s into %s", + transition, remote); + if (remote == null) return; + + IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() { + @Override + public void onTransitionFinished(WindowContainerTransaction wct) { + mMainExecutor.execute(() -> { + if (!mRequestedRemotes.containsKey(mergeTarget)) { + Log.e(TAG, "Merged transition finished after it's mergeTarget (the " + + "transition it was supposed to merge into). This usually means " + + "that the mergeTarget's RemoteTransition impl erroneously " + + "accepted/ran the merge request after finishing the mergeTarget"); + } + finishCallback.onTransitionFinished(wct, null /* wctCB */); + }); + } + }; + try { + remote.mergeAnimation(transition, info, t, mergeTarget, cb); + } catch (RemoteException e) { + Log.e(Transitions.TAG, "Error attempting to merge remote transition.", e); + } + } + @Override @Nullable public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @Nullable TransitionRequestInfo request) { IRemoteTransition remote = request.getRemoteTransition(); if (remote == null) return null; - mPendingRemotes.put(transition, remote); + mRequestedRemotes.put(transition, remote); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "RemoteTransition directly requested" + " for %s: %s", transition, remote); return new WindowContainerTransaction(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 64dc47f5549a9..3251abc3a8b8f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -35,7 +35,6 @@ import android.os.IBinder; import android.os.RemoteException; import android.os.SystemProperties; import android.provider.Settings; -import android.util.ArrayMap; import android.util.Log; import android.view.SurfaceControl; import android.view.WindowManager; @@ -91,11 +90,16 @@ public class Transitions implements RemoteCallable { private float mTransitionAnimationScaleSetting = 1.0f; private static final class ActiveTransition { - TransitionHandler mFirstHandler = null; + IBinder mToken = null; + TransitionHandler mHandler = null; + boolean mMerged = false; + TransitionInfo mInfo = null; + SurfaceControl.Transaction mStartT = null; + SurfaceControl.Transaction mFinishT = null; } - /** Keeps track of currently tracked transitions and all the animations associated with each */ - private final ArrayMap mActiveTransitions = new ArrayMap<>(); + /** Keeps track of currently playing transitions in the order of receipt. */ + private final ArrayList mActiveTransitions = new ArrayList<>(); public Transitions(@NonNull WindowOrganizer organizer, @NonNull TransactionPool pool, @NonNull Context context, @NonNull ShellExecutor mainExecutor, @@ -226,7 +230,7 @@ public class Transitions implements RemoteCallable { * type, their transit mode, and their destination z-order. */ private static void setupStartState(@NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t) { + @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) { boolean isOpening = isOpeningType(info.getType()); if (info.getRootLeash().isValid()) { t.show(info.getRootLeash()); @@ -270,6 +274,8 @@ public class Transitions implements RemoteCallable { t.setAlpha(leash, 1.f); } else { t.setAlpha(leash, 0.f); + // fix alpha in finish transaction in case the animator itself no-ops. + finishT.setAlpha(leash, 1.f); } } else { // put on bottom and leave it visible @@ -290,16 +296,24 @@ public class Transitions implements RemoteCallable { } } + private int findActiveTransition(IBinder token) { + for (int i = mActiveTransitions.size() - 1; i >= 0; --i) { + if (mActiveTransitions.get(i).mToken == token) return i; + } + return -1; + } + @VisibleForTesting void onTransitionReady(@NonNull IBinder transitionToken, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t) { + @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady %s: %s", transitionToken, info); - final ActiveTransition active = mActiveTransitions.get(transitionToken); - if (active == null) { + final int activeIdx = findActiveTransition(transitionToken); + if (activeIdx < 0) { throw new IllegalStateException("Got transitionReady for non-active transition " + transitionToken + ". expecting one of " - + Arrays.toString(mActiveTransitions.keySet().toArray())); + + Arrays.toString(mActiveTransitions.stream().map( + activeTransition -> activeTransition.mToken).toArray())); } if (!info.getRootLeash().isValid()) { // Invalid root-leash implies that the transition is empty/no-op, so just do @@ -307,30 +321,62 @@ public class Transitions implements RemoteCallable { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Invalid root leash (%s): %s", transitionToken, info); t.apply(); - onFinish(transitionToken, null /* wct */, null /* wctCB */); + onAbort(transitionToken); return; } - setupStartState(info, t); + final ActiveTransition active = mActiveTransitions.get(activeIdx); + active.mInfo = info; + active.mStartT = t; + active.mFinishT = finishT; + if (activeIdx > 0) { + // This is now playing at the same time as an existing animation, so try merging it. + attemptMergeTransition(mActiveTransitions.get(0), active); + return; + } + // The normal case, just play it. + playTransition(active); + } - final TransitionFinishCallback finishCb = (wct, cb) -> onFinish(transitionToken, wct, cb); - // If a handler chose to uniquely run this animation, try delegating to it. - if (active.mFirstHandler != null) { + /** + * Attempt to merge by delegating the transition start to the handler of the currently + * playing transition. + */ + void attemptMergeTransition(@NonNull ActiveTransition playing, + @NonNull ActiveTransition merging) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s ready while" + + " another transition %s is still animating. Notify the animating transition" + + " in case they can be merged", merging.mToken, playing.mToken); + playing.mHandler.mergeAnimation(merging.mToken, merging.mInfo, merging.mStartT, + playing.mToken, (wct, cb) -> onFinish(merging.mToken, wct, cb)); + } + + boolean startAnimation(@NonNull ActiveTransition active, TransitionHandler handler) { + return handler.startAnimation(active.mToken, active.mInfo, active.mStartT, + (wct, cb) -> onFinish(active.mToken, wct, cb)); + } + + void playTransition(@NonNull ActiveTransition active) { + setupStartState(active.mInfo, active.mStartT, active.mFinishT); + + // If a handler already chose to run this animation, try delegating to it first. + if (active.mHandler != null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " try firstHandler %s", - active.mFirstHandler); - if (active.mFirstHandler.startAnimation(transitionToken, info, t, finishCb)) { + active.mHandler); + if (startAnimation(active, active.mHandler)) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by firstHandler"); return; } } // Otherwise give every other handler a chance (in order) for (int i = mHandlers.size() - 1; i >= 0; --i) { - if (mHandlers.get(i) == active.mFirstHandler) continue; + if (mHandlers.get(i) == active.mHandler) continue; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " try handler %s", mHandlers.get(i)); - if (mHandlers.get(i).startAnimation(transitionToken, info, t, finishCb)) { + if (startAnimation(active, mHandlers.get(i))) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by %s", mHandlers.get(i)); + active.mHandler = mHandlers.get(i); return; } } @@ -338,23 +384,107 @@ public class Transitions implements RemoteCallable { "This shouldn't happen, maybe the default handler is broken."); } - private void onFinish(IBinder transition, @Nullable WindowContainerTransaction wct, + /** Special version of finish just for dealing with no-op/invalid transitions. */ + private void onAbort(IBinder transition) { + final int activeIdx = findActiveTransition(transition); + if (activeIdx < 0) return; + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "Transition animation aborted due to no-op, notifying core %s", transition); + mActiveTransitions.remove(activeIdx); + mOrganizer.finishTransition(transition, null /* wct */, null /* wctCB */); + } + + private void onFinish(IBinder transition, + @Nullable WindowContainerTransaction wct, @Nullable WindowContainerTransactionCallback wctCB) { - if (!mActiveTransitions.containsKey(transition)) { - Log.e(TAG, "Trying to finish a non-running transition. Maybe remote crashed?"); + int activeIdx = findActiveTransition(transition); + if (activeIdx < 0) { + Log.e(TAG, "Trying to finish a non-running transition. Either remote crashed or " + + " a handler didn't properly deal with a merge.", new RuntimeException()); + return; + } else if (activeIdx > 0) { + // This transition was merged. + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged: %s", + transition); + final ActiveTransition active = mActiveTransitions.get(activeIdx); + active.mMerged = true; + if (active.mHandler != null) { + active.mHandler.onTransitionMerged(active.mToken); + } return; } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - "Transition animations finished, notifying core %s", transition); - mActiveTransitions.remove(transition); + "Transition animation finished, notifying core %s", transition); + // Merge all relevant transactions together + SurfaceControl.Transaction fullFinish = mActiveTransitions.get(activeIdx).mFinishT; + for (int iA = activeIdx + 1; iA < mActiveTransitions.size(); ++iA) { + final ActiveTransition toMerge = mActiveTransitions.get(iA); + if (!toMerge.mMerged) break; + // Include start. It will be a no-op if it was already applied. Otherwise, we need it + // to maintain consistent state. + fullFinish.merge(mActiveTransitions.get(iA).mStartT); + fullFinish.merge(mActiveTransitions.get(iA).mFinishT); + } + fullFinish.apply(); + // Now perform all the finishes. + mActiveTransitions.remove(activeIdx); mOrganizer.finishTransition(transition, wct, wctCB); + while (activeIdx < mActiveTransitions.size()) { + if (!mActiveTransitions.get(activeIdx).mMerged) break; + ActiveTransition merged = mActiveTransitions.remove(activeIdx); + mOrganizer.finishTransition(merged.mToken, null /* wct */, null /* wctCB */); + } + if (mActiveTransitions.size() <= activeIdx) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition animations " + + "finished"); + return; + } + // Start animating the next active transition + final ActiveTransition next = mActiveTransitions.get(activeIdx); + if (next.mInfo == null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Pending transition after one" + + " finished, but it isn't ready yet."); + return; + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Pending transitions after one" + + " finished, so start the next one."); + playTransition(next); + // Now try to merge the rest of the transitions (re-acquire activeIdx since next may have + // finished immediately) + activeIdx = findActiveTransition(next.mToken); + if (activeIdx < 0) { + // This means 'next' finished immediately and thus re-entered this function. Since + // that is the case, just return here since all relevant logic has already run in the + // re-entered call. + return; + } + + // This logic is also convoluted because 'next' may finish immediately in response to any of + // the merge requests (eg. if it decided to "cancel" itself). + int mergeIdx = activeIdx + 1; + while (mergeIdx < mActiveTransitions.size()) { + ActiveTransition mergeCandidate = mActiveTransitions.get(mergeIdx); + if (mergeCandidate.mMerged) { + throw new IllegalStateException("Can't merge a transition after not-merging" + + " a preceding one."); + } + attemptMergeTransition(next, mergeCandidate); + mergeIdx = findActiveTransition(mergeCandidate.mToken); + if (mergeIdx < 0) { + // This means 'next' finished immediately and thus re-entered this function. Since + // that is the case, just return here since all relevant logic has already run in + // the re-entered call. + return; + } + ++mergeIdx; + } } void requestStartTransition(@NonNull IBinder transitionToken, @Nullable TransitionRequestInfo request) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition requested: %s %s", transitionToken, request); - if (mActiveTransitions.containsKey(transitionToken)) { + if (findActiveTransition(transitionToken) >= 0) { throw new RuntimeException("Transition already started " + transitionToken); } final ActiveTransition active = new ActiveTransition(); @@ -362,23 +492,23 @@ public class Transitions implements RemoteCallable { for (int i = mHandlers.size() - 1; i >= 0; --i) { wct = mHandlers.get(i).handleRequest(transitionToken, request); if (wct != null) { - active.mFirstHandler = mHandlers.get(i); + active.mHandler = mHandlers.get(i); break; } } - IBinder transition = mOrganizer.startTransition( + active.mToken = mOrganizer.startTransition( request.getType(), transitionToken, wct); - mActiveTransitions.put(transition, active); + mActiveTransitions.add(active); } /** Start a new transition directly. */ public IBinder startTransition(@WindowManager.TransitionType int type, @NonNull WindowContainerTransaction wct, @Nullable TransitionHandler handler) { final ActiveTransition active = new ActiveTransition(); - active.mFirstHandler = handler; - IBinder transition = mOrganizer.startTransition(type, null /* token */, wct); - mActiveTransitions.put(transition, active); - return transition; + active.mHandler = handler; + active.mToken = mOrganizer.startTransition(type, null /* token */, wct); + mActiveTransitions.add(active); + return active.mToken; } /** @@ -414,6 +544,32 @@ public class Transitions implements RemoteCallable { @NonNull SurfaceControl.Transaction t, @NonNull TransitionFinishCallback finishCallback); + /** + * Attempts to merge a different transition's animation into an animation that this handler + * is currently playing. If a merge is not possible/supported, this should be a no-op. + * + * This gets called if another transition becomes ready while this handler is still playing + * an animation. This is called regardless of whether this handler claims to support that + * particular transition or not. + * + * When this happens, there are 2 options: + * 1. Do nothing. This effectively rejects the merge request. This is the "safest" option. + * 2. Merge the incoming transition into this one. The implementation is up to this + * handler. To indicate that this handler has "consumed" the merge transition, it + * must call the finishCallback immediately, or at-least before the original + * transition's finishCallback is called. + * + * @param transition This is the transition that wants to be merged. + * @param info Information about what is changing in the transition. + * @param t Contains surface changes that resulted from the transition. + * @param mergeTarget This is the transition that we are attempting to merge with (ie. the + * one this handler is currently already animating). + * @param finishCallback Call this if merged. This MUST be called on main thread. + */ + default void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull TransitionFinishCallback finishCallback) { } + /** * Potentially handles a startTransition request. * @@ -426,6 +582,12 @@ public class Transitions implements RemoteCallable { WindowContainerTransaction handleRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request); + /** + * Called when a transition which was already "claimed" by this handler has been merged + * into another animation. Gives this handler a chance to clean-up any expectations. + */ + default void onTransitionMerged(@NonNull IBinder transition) { } + /** * Sets transition animation scale settings value to handler. * @@ -438,10 +600,10 @@ public class Transitions implements RemoteCallable { private class TransitionPlayerImpl extends ITransitionPlayer.Stub { @Override public void onTransitionReady(IBinder iBinder, TransitionInfo transitionInfo, - SurfaceControl.Transaction transaction) throws RemoteException { - mMainExecutor.execute(() -> { - Transitions.this.onTransitionReady(iBinder, transitionInfo, transaction); - }); + SurfaceControl.Transaction t, SurfaceControl.Transaction finishT) + throws RemoteException { + mMainExecutor.execute(() -> Transitions.this.onTransitionReady( + iBinder, transitionInfo, t, finishT)); } @Override diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index 18642fcfef6cd..08ac2a6cfa773 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -332,11 +332,18 @@ public class SplitTransitionTests extends ShellTestCase { final WindowContainerTransaction mRemoteFinishWCT = new WindowContainerTransaction(); @Override - public void startAnimation(TransitionInfo info, SurfaceControl.Transaction t, - IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { + public void startAnimation(IBinder transition, TransitionInfo info, + SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) + throws RemoteException { mCalled = true; finishCallback.onTransitionFinished(mRemoteFinishWCT); } + + @Override + public void mergeAnimation(IBinder transition, TransitionInfo info, + SurfaceControl.Transaction t, IBinder mergeTarget, + IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { + } } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index c1733de755351..2d2ab2c9f6748 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -107,7 +107,8 @@ public class ShellTransitionTests { verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any()); TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); - transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); assertEquals(1, mDefaultHandler.activeCount()); mDefaultHandler.finishAll(); mMainExecutor.flushAll(); @@ -155,7 +156,8 @@ public class ShellTransitionTests { transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), isNull()); - transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); assertEquals(1, mDefaultHandler.activeCount()); assertEquals(0, testHandler.activeCount()); mDefaultHandler.finishAll(); @@ -168,7 +170,8 @@ public class ShellTransitionTests { new TransitionRequestInfo(TRANSIT_OPEN, mwTaskInfo, null /* remote */)); verify(mOrganizer, times(1)).startTransition( eq(TRANSIT_OPEN), eq(transitToken), eq(handlerWCT)); - transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); assertEquals(1, mDefaultHandler.activeCount()); assertEquals(0, testHandler.activeCount()); mDefaultHandler.finishAll(); @@ -185,7 +188,8 @@ public class ShellTransitionTests { eq(TRANSIT_CHANGE), eq(transitToken), eq(handlerWCT)); TransitionInfo change = new TransitionInfoBuilder(TRANSIT_CHANGE) .addChange(TRANSIT_CHANGE).build(); - transitions.onTransitionReady(transitToken, change, mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken, change, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); assertEquals(0, mDefaultHandler.activeCount()); assertEquals(1, testHandler.activeCount()); assertEquals(0, topHandler.activeCount()); @@ -203,11 +207,18 @@ public class ShellTransitionTests { final WindowContainerTransaction remoteFinishWCT = new WindowContainerTransaction(); IRemoteTransition testRemote = new IRemoteTransition.Stub() { @Override - public void startAnimation(TransitionInfo info, SurfaceControl.Transaction t, + public void startAnimation(IBinder token, TransitionInfo info, + SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { remoteCalled[0] = true; finishCallback.onTransitionFinished(remoteFinishWCT); } + + @Override + public void mergeAnimation(IBinder token, TransitionInfo info, + SurfaceControl.Transaction t, IBinder mergeTarget, + IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { + } }; IBinder transitToken = new Binder(); transitions.requestStartTransition(transitToken, @@ -215,7 +226,8 @@ public class ShellTransitionTests { verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any()); TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); - transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); assertEquals(0, mDefaultHandler.activeCount()); assertTrue(remoteCalled[0]); mDefaultHandler.finishAll(); @@ -269,11 +281,18 @@ public class ShellTransitionTests { final boolean[] remoteCalled = new boolean[]{false}; IRemoteTransition testRemote = new IRemoteTransition.Stub() { @Override - public void startAnimation(TransitionInfo info, SurfaceControl.Transaction t, + public void startAnimation(IBinder token, TransitionInfo info, + SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { remoteCalled[0] = true; finishCallback.onTransitionFinished(null /* wct */); } + + @Override + public void mergeAnimation(IBinder token, TransitionInfo info, + SurfaceControl.Transaction t, IBinder mergeTarget, + IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { + } }; TransitionFilter filter = new TransitionFilter(); @@ -290,7 +309,8 @@ public class ShellTransitionTests { verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any()); TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); - transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); assertEquals(0, mDefaultHandler.activeCount()); assertTrue(remoteCalled[0]); mDefaultHandler.finishAll(); @@ -308,11 +328,18 @@ public class ShellTransitionTests { final WindowContainerTransaction remoteFinishWCT = new WindowContainerTransaction(); IRemoteTransition testRemote = new IRemoteTransition.Stub() { @Override - public void startAnimation(TransitionInfo info, SurfaceControl.Transaction t, + public void startAnimation(IBinder token, TransitionInfo info, + SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { remoteCalled[0] = true; finishCallback.onTransitionFinished(remoteFinishWCT); } + + @Override + public void mergeAnimation(IBinder token, TransitionInfo info, + SurfaceControl.Transaction t, IBinder mergeTarget, + IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { + } }; final int transitType = TRANSIT_FIRST_CUSTOM + 1; @@ -336,6 +363,86 @@ public class ShellTransitionTests { mock(SurfaceControl.Transaction.class), testFinish)); } + @Test + public void testTransitionQueueing() { + Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, + mMainExecutor, mAnimExecutor); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + IBinder transitToken1 = new Binder(); + transitions.requestStartTransition(transitToken1, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + transitions.onTransitionReady(transitToken1, info1, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); + assertEquals(1, mDefaultHandler.activeCount()); + + IBinder transitToken2 = new Binder(); + transitions.requestStartTransition(transitToken2, + new TransitionRequestInfo(TRANSIT_CLOSE, null /* trigger */, null /* remote */)); + TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + transitions.onTransitionReady(transitToken2, info2, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); + // default handler doesn't merge by default, so it shouldn't increment active count. + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(0, mDefaultHandler.mergeCount()); + verify(mOrganizer, times(0)).finishTransition(eq(transitToken1), any(), any()); + verify(mOrganizer, times(0)).finishTransition(eq(transitToken2), any(), any()); + + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + // first transition finished + verify(mOrganizer, times(1)).finishTransition(eq(transitToken1), any(), any()); + verify(mOrganizer, times(0)).finishTransition(eq(transitToken2), any(), any()); + // But now the "queued" transition is running + assertEquals(1, mDefaultHandler.activeCount()); + + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + verify(mOrganizer, times(1)).finishTransition(eq(transitToken2), any(), any()); + } + + @Test + public void testTransitionMerging() { + Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, + mMainExecutor, mAnimExecutor); + mDefaultHandler.setSimulateMerge(true); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + IBinder transitToken1 = new Binder(); + transitions.requestStartTransition(transitToken1, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + transitions.onTransitionReady(transitToken1, info1, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); + assertEquals(1, mDefaultHandler.activeCount()); + + IBinder transitToken2 = new Binder(); + transitions.requestStartTransition(transitToken2, + new TransitionRequestInfo(TRANSIT_CLOSE, null /* trigger */, null /* remote */)); + TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + transitions.onTransitionReady(transitToken2, info2, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); + // it should still only have 1 active, but then show 1 merged + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(1, mDefaultHandler.mergeCount()); + verify(mOrganizer, times(0)).finishTransition(eq(transitToken1), any(), any()); + // We don't tell organizer it is finished yet (since we still want to maintain ordering) + verify(mOrganizer, times(0)).finishTransition(eq(transitToken2), any(), any()); + + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + // transition + merged all finished. + verify(mOrganizer, times(1)).finishTransition(eq(transitToken1), any(), any()); + verify(mOrganizer, times(1)).finishTransition(eq(transitToken2), any(), any()); + // Make sure nothing was queued + assertEquals(0, mDefaultHandler.activeCount()); + } + class TransitionInfoBuilder { final TransitionInfo mInfo; @@ -364,7 +471,9 @@ public class ShellTransitionTests { } class TestTransitionHandler implements Transitions.TransitionHandler { - final ArrayList mFinishes = new ArrayList<>(); + ArrayList mFinishes = new ArrayList<>(); + final ArrayList mMerged = new ArrayList<>(); + boolean mSimulateMerge = false; @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @@ -374,6 +483,15 @@ public class ShellTransitionTests { return true; } + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (!mSimulateMerge) return; + mMerged.add(transition); + finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); + } + @Nullable @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @@ -381,16 +499,25 @@ public class ShellTransitionTests { return null; } + void setSimulateMerge(boolean sim) { + mSimulateMerge = sim; + } + void finishAll() { - for (int i = mFinishes.size() - 1; i >= 0; --i) { - mFinishes.get(i).onTransitionFinished(null /* wct */, null /* wctCB */); + final ArrayList finishes = mFinishes; + mFinishes = new ArrayList<>(); + for (int i = finishes.size() - 1; i >= 0; --i) { + finishes.get(i).onTransitionFinished(null /* wct */, null /* wctCB */); } - mFinishes.clear(); } int activeCount() { return mFinishes.size(); } + + int mergeCount() { + return mMerged.size(); + } } private static SurfaceControl createMockSurface(boolean valid) { diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java index 2f38f0ab4509e..4e0204b75d2c2 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java @@ -25,6 +25,7 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.TransitionOldType; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; +import android.os.IBinder; import android.os.RemoteException; import android.util.Log; import android.view.IRemoteAnimationFinishedCallback; @@ -153,7 +154,8 @@ public class RemoteAnimationAdapterCompat { final RemoteAnimationRunnerCompat remoteAnimationAdapter) { return new IRemoteTransition.Stub() { @Override - public void startAnimation(TransitionInfo info, SurfaceControl.Transaction t, + public void startAnimation(IBinder token, TransitionInfo info, + SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) { final RemoteAnimationTargetCompat[] appsCompat = RemoteAnimationTargetCompat.wrap(info, false /* wallpapers */); @@ -256,6 +258,14 @@ public class RemoteAnimationAdapterCompat { appsCompat, wallpapersCompat, nonAppsCompat, animationFinishedCallback); } + + @Override + public void mergeAnimation(IBinder token, TransitionInfo info, + SurfaceControl.Transaction t, IBinder mergeTarget, + IRemoteTransitionFinishedCallback finishCallback) { + // TODO: hook up merge to recents onTaskAppeared if applicable. Until then, ignore + // any incoming merges. + } }; } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java index 26ef1455be7de..88d9d68b44581 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java @@ -25,6 +25,7 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Rect; +import android.os.IBinder; import android.os.Parcelable; import android.os.RemoteException; import android.util.Log; @@ -65,7 +66,8 @@ public class RemoteTransitionCompat implements Parcelable { @NonNull Executor executor) { mTransition = new IRemoteTransition.Stub() { @Override - public void startAnimation(TransitionInfo info, SurfaceControl.Transaction t, + public void startAnimation(IBinder transition, TransitionInfo info, + SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishedCallback) { final Runnable finishAdapter = () -> { try { @@ -74,7 +76,22 @@ public class RemoteTransitionCompat implements Parcelable { Log.e(TAG, "Failed to call transition finished callback", e); } }; - executor.execute(() -> runner.startAnimation(info, t, finishAdapter)); + executor.execute(() -> runner.startAnimation(transition, info, t, finishAdapter)); + } + + @Override + public void mergeAnimation(IBinder transition, TransitionInfo info, + SurfaceControl.Transaction t, IBinder mergeTarget, + IRemoteTransitionFinishedCallback finishedCallback) { + final Runnable finishAdapter = () -> { + try { + finishedCallback.onTransitionFinished(null /* wct */); + } catch (RemoteException e) { + Log.e(TAG, "Failed to call transition finished callback", e); + } + }; + executor.execute(() -> runner.mergeAnimation(transition, info, t, mergeTarget, + finishAdapter)); } }; } @@ -84,7 +101,8 @@ public class RemoteTransitionCompat implements Parcelable { RecentsAnimationControllerCompat controller) { mTransition = new IRemoteTransition.Stub() { @Override - public void startAnimation(TransitionInfo info, SurfaceControl.Transaction t, + public void startAnimation(IBinder transition, TransitionInfo info, + SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishedCallback) { final RemoteAnimationTargetCompat[] apps = RemoteAnimationTargetCompat.wrap(info, false /* wallpapers */); @@ -113,6 +131,13 @@ public class RemoteTransitionCompat implements Parcelable { recents.onAnimationStart(wrapControl, apps, wallpapers, new Rect(0, 0, 0, 0), new Rect()); } + + @Override + public void mergeAnimation(IBinder transition, TransitionInfo info, + SurfaceControl.Transaction t, IBinder mergeTarget, + IRemoteTransitionFinishedCallback finishedCallback) { + // TODO: hook up merge to onTaskAppeared. Until then, just ignore incoming merges. + } }; } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionRunner.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionRunner.java index 6002bca81c6c5..accc456c42095 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionRunner.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionRunner.java @@ -16,6 +16,7 @@ package com.android.systemui.shared.system; +import android.os.IBinder; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -25,6 +26,17 @@ public interface RemoteTransitionRunner { * Starts a transition animation. Once complete, the implementation should call * `finishCallback`. */ - void startAnimation(TransitionInfo info, SurfaceControl.Transaction t, + void startAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, Runnable finishCallback); + + /** + * Attempts to merge a transition into the currently-running animation. If merge is not + * possible/supported, this should do nothing. Otherwise, the implementation should call + * `finishCallback` immediately to indicate that it merged the transition. + * + * @param transition The transition that wants to be merged into the running animation. + * @param mergeTarget The transition to merge into (that this runner is currently animating). + */ + default void mergeAnimation(IBinder transition, TransitionInfo info, + SurfaceControl.Transaction t, IBinder mergeTarget, Runnable finishCallback) { } } diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index e4ac1f6c5b50c..b6109b43851f2 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -106,11 +106,9 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe private final BLASTSyncEngine mSyncEngine; private IRemoteTransition mRemoteTransition = null; - /** - * This is a leash to put animating surfaces into flatly without clipping/ordering issues. It - * is a child of all the targets' shared ancestor. - */ - private SurfaceControl mRootLeash = null; + /** Only use for clean-up after binder death! */ + private SurfaceControl.Transaction mStartTransaction = null; + private SurfaceControl.Transaction mFinishTransaction = null; /** * Contains change infos for both participants and all ancestors. We have to track ancestors @@ -229,16 +227,16 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe setReady(true); } - /** The transition has finished animating and is ready to finalize WM state */ - void finishTransition() { - if (mState < STATE_PLAYING) { - throw new IllegalStateException("Can't finish a non-playing transition " + mSyncId); - } + /** + * Build a transaction that "resets" all the re-parenting and layer changes. This is + * intended to be applied at the end of the transition but before the finish callback. This + * needs to be passed/applied in shell because until finish is called, shell owns the surfaces. + * Additionally, this gives shell the ability to better deal with merged transitions. + */ + private void buildFinishTransaction(SurfaceControl.Transaction t, SurfaceControl rootLeash) { final Point tmpPos = new Point(); // usually only size 1 final ArraySet displays = new ArraySet<>(); - // Immediately apply all surface reparents, don't wait for pending/sync/etc. - SurfaceControl.Transaction t = mController.mAtm.mWindowManager.mTransactionFactory.get(); for (int i = mTargets.size() - 1; i >= 0; --i) { final WindowContainer target = mTargets.valueAt(i); if (target.getParent() != null) { @@ -247,9 +245,6 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe // Ensure surfaceControls are re-parented back into the hierarchy. t.reparent(targetLeash, origParent); t.setLayer(targetLeash, target.getLastLayer()); - // TODO(shell-transitions): Once all remotables have been moved, see if there is - // a more appropriate place to do the following. This may - // involve passing an SF transaction from shell on finish. target.getRelativePosition(tmpPos); t.setPosition(targetLeash, tmpPos.x, tmpPos.y); t.setCornerRadius(targetLeash, 0); @@ -257,25 +252,26 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe displays.add(target.getDisplayContent()); } } - // Need to update layers on ALL displays (for now) since they were all paused while - // the animation played. + // Need to update layers on involved displays since they were all paused while + // the animation played. This puts the layers back into the correct order. for (int i = displays.size() - 1; i >= 0; --i) { if (displays.valueAt(i) == null) continue; displays.valueAt(i).assignChildLayers(t); } - // Also pro-actively hide going-invisible activity surfaces in same transaction to - // prevent flickers due to reparenting and animation z-order mismatch. - for (int i = mParticipants.size() - 1; i >= 0; --i) { - final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord(); - if (ar == null || ar.mVisibleRequested || !ar.isVisible()) continue; - t.hide(ar.getSurfaceControl()); + if (rootLeash.isValid()) { + t.reparent(rootLeash, null); } - if (mRootLeash.isValid()) { - t.remove(mRootLeash); + } + + /** + * The transition has finished animating and is ready to finalize WM state. This should not + * be called directly; use {@link TransitionController#finishTransition} instead. + */ + void finishTransition() { + mStartTransaction = mFinishTransaction = null; + if (mState < STATE_PLAYING) { + throw new IllegalStateException("Can't finish a non-playing transition " + mSyncId); } - mRootLeash = null; - t.apply(); - t.close(); // Commit all going-invisible containers for (int i = 0; i < mParticipants.size(); ++i) { @@ -343,29 +339,60 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe // Resolve the animating targets from the participants mTargets = calculateTargets(mParticipants, mChanges); final TransitionInfo info = calculateTransitionInfo(mType, mFlags, mTargets, mChanges); - mRootLeash = info.getRootLeash(); handleNonAppWindowsInTransition(displayId, mType, mFlags); + // Manually show any activities that are visibleRequested. This is needed to properly + // support simultaneous animation queueing/merging. Specifically, if transition A makes + // an activity invisible, it's finishTransaction (which is applied *after* the animation) + // will hide the activity surface. If transition B then makes the activity visible again, + // the normal surfaceplacement logic won't add a show to this start transaction because + // the activity visibility hasn't been committed yet. To deal with this, we have to manually + // show here in the same way that we manually hide in finishTransaction. + for (int i = mParticipants.size() - 1; i >= 0; --i) { + final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord(); + if (ar == null || !ar.mVisibleRequested) continue; + transaction.show(ar.getSurfaceControl()); + } + + mStartTransaction = transaction; + mFinishTransaction = mController.mAtm.mWindowManager.mTransactionFactory.get(); + buildFinishTransaction(mFinishTransaction, info.getRootLeash()); if (mController.getTransitionPlayer() != null) { try { ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Calling onTransitionReady: %s", info); - mController.getTransitionPlayer().onTransitionReady(this, info, transaction); + mController.getTransitionPlayer().onTransitionReady( + this, info, transaction, mFinishTransaction); } catch (RemoteException e) { // If there's an exception when trying to send the mergedTransaction to the // client, we should finish and apply it here so the transactions aren't lost. - transaction.apply(); - finishTransition(); + cleanUpOnFailure(); } } else { // No player registered, so just finish/apply immediately - transaction.apply(); - finishTransition(); + cleanUpOnFailure(); } mSyncId = -1; } + /** + * If the remote failed for any reason, use this to do any appropriate clean-up. Do not call + * this directly, it's designed to by called by {@link TransitionController} only. + */ + void cleanUpOnFailure() { + // No need to clean-up if this isn't playing yet. + if (mState < STATE_PLAYING) return; + + if (mStartTransaction != null) { + mStartTransaction.apply(); + } + if (mFinishTransaction != null) { + mFinishTransaction.apply(); + } + finishTransition(); + } + private void handleNonAppWindowsInTransition(int displayId, @WindowManager.TransitionType int transit, int flags) { final DisplayContent dc = diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 6338f39e2e678..cc63c4922c326 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -42,17 +42,24 @@ class TransitionController { private static final String TAG = "TransitionController"; private ITransitionPlayer mTransitionPlayer; - private final IBinder.DeathRecipient mTransitionPlayerDeath = () -> mTransitionPlayer = null; final ActivityTaskManagerService mAtm; - /** Currently playing transitions. When finished, records are removed from this list. */ + /** + * Currently playing transitions (in the order they were started). When finished, records are + * removed from this list. + */ private final ArrayList mPlayingTransitions = new ArrayList<>(); - /** - * The transition currently being constructed (collecting participants). - * TODO(shell-transitions): When simultaneous transitions are supported, merge this with - * mPlayingTransitions. - */ + private final IBinder.DeathRecipient mTransitionPlayerDeath = () -> { + // clean-up/finish any playing transitions. + for (int i = 0; i < mPlayingTransitions.size(); ++i) { + mPlayingTransitions.get(i).cleanUpOnFailure(); + } + mPlayingTransitions.clear(); + mTransitionPlayer = null; + }; + + /** The transition currently being constructed (collecting participants). */ private Transition mCollectingTransition = null; TransitionController(ActivityTaskManagerService atm) { diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index 2d4e4efd74ea9..5a63fbff33d99 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -1462,7 +1462,8 @@ class WindowTestsBase extends SystemServiceTestsBase { @Override public void onTransitionReady(IBinder transitToken, TransitionInfo transitionInfo, - SurfaceControl.Transaction transaction) throws RemoteException { + SurfaceControl.Transaction transaction, SurfaceControl.Transaction finishT) + throws RemoteException { mLastTransit = Transition.fromBinder(transitToken); mLastReady = transitionInfo; }