Loading services/core/java/com/android/server/wm/Transition.java +125 −20 Original line number Diff line number Diff line Loading @@ -107,6 +107,11 @@ import java.util.function.Predicate; * Represents a logical transition. This keeps track of all the changes associated with a logical * WM state -> state transition. * @see TransitionController * * In addition to tracking individual container changes, this also tracks ordering-changes (just * on-top for now). However, since order is a "global" property, the mechanics of order-change * detection/reporting is non-trivial when transitions are collecting in parallel. See * {@link #collectOrderChanges} for more details. */ class Transition implements BLASTSyncEngine.TransactionReadyListener { private static final String TAG = "Transition"; Loading Loading @@ -191,6 +196,12 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { */ private final ArrayList<Task> mOnTopTasksStart = new ArrayList<>(); /** * The (non alwaysOnTop) tasks which were on-top of their display when this transition became * ready (via setReady, not animation-ready). */ private final ArrayList<Task> mOnTopTasksAtReady = new ArrayList<>(); /** * Set of participating windowtokens (activity/wallpaper) which are visible at the end of * the transition animation. Loading Loading @@ -244,6 +255,36 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { */ boolean mIsPlayerEnabled = true; /** This transition doesn't run in parallel. */ static final int PARALLEL_TYPE_NONE = 0; /** Any 2 transitions of this type can run in parallel with each other. Used for testing. */ static final int PARALLEL_TYPE_MUTUAL = 1; @IntDef(prefix = { "PARALLEL_TYPE_" }, value = { PARALLEL_TYPE_NONE, PARALLEL_TYPE_MUTUAL }) @Retention(RetentionPolicy.SOURCE) @interface ParallelType {} /** * What category of parallel-collect support this transition has. The value of this is used * by {@link TransitionController} to determine which transitions can collect in parallel. If * a transition can collect in parallel, it means that it will start collecting as soon as the * prior collecting transition is {@link #isPopulated}. This is a shortcut for supporting * a couple specific situations before we have full-fledged support for parallel transitions. */ @ParallelType int mParallelCollectType = PARALLEL_TYPE_NONE; /** * A "Track" is a set of animations which must cooperate with each other to play smoothly. If * animations can play independently of each other, then they can be in different tracks. If * a transition must cooperate with transitions in >1 other track, then it must be marked * FLAG_SYNC and it will end-up flushing all animations before it starts. */ int mAnimationTrack = 0; Transition(@TransitionType int type, @TransitionFlags int flags, TransitionController controller, BLASTSyncEngine syncEngine) { mType = type; Loading Loading @@ -447,7 +488,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { throw new IllegalStateException("Attempting to re-use a transition"); } mState = STATE_COLLECTING; mSyncId = mSyncEngine.startSyncSet(this, timeoutMs, TAG, false /* parallel */); mSyncId = mSyncEngine.startSyncSet(this, timeoutMs, TAG, mParallelCollectType != PARALLEL_TYPE_NONE); mSyncEngine.setSyncMethod(mSyncId, TransitionController.SYNC_METHOD); mLogger.mSyncId = mSyncId; Loading Loading @@ -718,8 +760,15 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { final boolean ready = mReadyTracker.allReady(); ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Set transition ready=%b %d", ready, mSyncId); mSyncEngine.setReady(mSyncId, ready); if (ready) mLogger.mReadyTimeNs = SystemClock.elapsedRealtimeNanos(); boolean changed = mSyncEngine.setReady(mSyncId, ready); if (changed && ready) { mLogger.mReadyTimeNs = SystemClock.elapsedRealtimeNanos(); mOnTopTasksAtReady.clear(); for (int i = 0; i < mTargetDisplays.size(); ++i) { addOnTopTasks(mTargetDisplays.get(i), mOnTopTasksAtReady); } mController.onTransitionPopulated(this); } } /** Loading @@ -737,6 +786,11 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return mReadyTracker.allReady(); } /** This transition has all of its expected participants. */ boolean isPopulated() { return mState >= STATE_STARTED && mReadyTracker.allReady(); } /** * 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 Loading Loading @@ -1211,7 +1265,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { : mController.mAtm.mRootWindowContainer.getDefaultDisplay(); if (mState == STATE_ABORT) { mController.abort(this); mController.onAbort(this); primaryDisplay.getPendingTransaction().merge(transaction); mSyncId = -1; mOverrideOptions = null; Loading @@ -1222,13 +1276,15 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mState = STATE_PLAYING; mStartTransaction = transaction; mFinishTransaction = mController.mAtm.mWindowManager.mTransactionFactory.get(); mController.moveToPlaying(this); // Flags must be assigned before calculateTransitionInfo. Otherwise it won't take effect. if (primaryDisplay.isKeyguardLocked()) { mFlags |= TRANSIT_FLAG_KEYGUARD_LOCKED; } collectOrderChanges(); // This is the only (or last) transition that is collecting, so we need to report any // leftover order changes. collectOrderChanges(mController.mWaitingTransitions.isEmpty()); // Resolve the animating targets from the participants. mTargets = calculateTargets(mParticipants, mChanges); Loading @@ -1236,6 +1292,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mController.mAtm.mBackNavigationController.onTransactionReady(this, mTargets); final TransitionInfo info = calculateTransitionInfo(mType, mFlags, mTargets, transaction); info.setDebugId(mSyncId); mController.assignTrack(this, info); mController.moveToPlaying(this); // Repopulate the displays based on the resolved targets. mTargetDisplays.clear(); Loading Loading @@ -1383,17 +1442,58 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { info.releaseAnimSurfaces(); } /** Collect tasks which moved-to-top but didn't change otherwise. */ /** * Collect tasks which moved-to-top as part of this transition. This also updates the * controller's latest-reported when relevant. * * This is a non-trivial operation because transition can collect in parallel; however, it can * be made tenable by acknowledging that the "setup" part of collection (phase 1) is still * globally serial; so, we can build some reasonable rules around it. * * First, we record the "start" on-top state (to compare against). Then, when this becomes * ready (via allReady, NOT onTransactionReady), we also record the "onReady" on-top state * -- the idea here is that upon "allReady", all the actual WM changes should be done and we * are now just waiting for window content to become ready (finish drawing). * * Then, in this function (during onTransactionReady), we compare the two orders and include * any changes to the order in the reported transition-info. Unfortunately, because of parallel * collection, the order can change in unexpected ways by now. To resolve this, we ALSO keep a * global "latest reported order" in TransitionController and use that to make decisions. */ @VisibleForTesting void collectOrderChanges() { void collectOrderChanges(boolean reportCurrent) { if (mOnTopTasksStart.isEmpty()) return; final ArrayList<Task> onTopTasksEnd = new ArrayList<>(); for (int i = 0; i < mTargetDisplays.size(); ++i) { addOnTopTasks(mTargetDisplays.get(i), onTopTasksEnd); boolean includesOrderChange = false; for (int i = 0; i < mOnTopTasksAtReady.size(); ++i) { final Task task = mOnTopTasksAtReady.get(i); if (mOnTopTasksStart.contains(task)) continue; includesOrderChange = true; break; } for (int i = 0; i < onTopTasksEnd.size(); ++i) { if (!includesOrderChange && !reportCurrent) { // This transition doesn't include an order change, so if it isn't required to report // the current focus (eg. it's the last of a cluster of transitions), then don't // report. return; } // The transition included an order change, but it may not be up-to-date, so grab the // latest state and compare with the last reported state (or our start state if no // reported state exists). ArrayList<Task> onTopTasksEnd = new ArrayList<>(); for (int d = 0; d < mTargetDisplays.size(); ++d) { addOnTopTasks(mTargetDisplays.get(d), onTopTasksEnd); final int displayId = mTargetDisplays.get(d).mDisplayId; ArrayList<Task> reportedOnTop = mController.mLatestOnTopTasksReported.get(displayId); for (int i = onTopTasksEnd.size() - 1; i >= 0; --i) { final Task task = onTopTasksEnd.get(i); if (task.getDisplayId() != displayId) continue; // If it didn't change since last report, don't report if (reportedOnTop == null) { if (mOnTopTasksStart.contains(task)) continue; } else if (reportedOnTop.contains(task)) { continue; } // Need to report it. mParticipants.add(task); int changeIdx = mChanges.indexOfKey(task); if (changeIdx < 0) { Loading @@ -1402,6 +1502,11 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } mChanges.valueAt(changeIdx).mFlags |= ChangeInfo.FLAG_CHANGE_MOVED_TO_TOP; } // Swap in the latest on-top tasks. mController.mLatestOnTopTasksReported.put(displayId, onTopTasksEnd); onTopTasksEnd = reportedOnTop != null ? reportedOnTop : new ArrayList<>(); onTopTasksEnd.clear(); } } private void postCleanupOnFailure() { Loading services/core/java/com/android/server/wm/TransitionController.java +199 −16 Original line number Diff line number Diff line Loading @@ -39,6 +39,7 @@ import android.os.SystemClock; import android.os.SystemProperties; import android.util.ArrayMap; import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; import android.view.SurfaceControl; Loading @@ -60,7 +61,34 @@ import java.util.ArrayList; import java.util.function.LongConsumer; /** * Handles all the aspects of recording and synchronizing transitions. * Handles all the aspects of recording (collecting) and synchronizing transitions. This is only * concerned with the WM changes. The actual animations are handled by the Player. * * Currently, only 1 transition can be the primary "collector" at a time. This is because WM changes * are still performed in a "global" manner. However, collecting can actually be broken into * two phases: * 1. Actually making WM changes and recording the participating containers. * 2. Waiting for the participating containers to become ready (eg. redrawing content). * Because (2) takes most of the time AND doesn't change WM, we can actually have multiple * transitions in phase (2) concurrently with one in phase (1). We refer to this arrangement as * "parallel" collection even though there is still only ever 1 transition actually able to gain * participants. * * Parallel collection happens when the "primary collector" has finished "setup" (phase 1) and is * just waiting. At this point, another transition can start collecting. When this happens, the * first transition is moved to a "waiting" list and the new transition becomes the "primary * collector". If at any time, the "primary collector" moves to playing before one of the waiting * transitions, then the first waiting transition will move back to being the "primary collector". * This maintains the "global"-like abstraction that the rest of WM currently expects. * * When a transition move-to-playing, we check it against all other playing transitions. If it * doesn't overlap with them, it can also animate in parallel. In this case it will be assigned a * new "track". "tracks" are a way to communicate to the player about which transitions need to be * played serially with each-other. So, if we find that a transition overlaps with other transitions * in one track, the transition will be assigned to that track. If, however, the transition overlaps * with transition in >1 track, we will actually just mark it as SYNC meaning it can't actually * play until all prior transition animations finish. This is heavy-handed because it is a fallback * situation and supporting something fancier would be unnecessarily complicated. */ class TransitionController { private static final String TAG = "TransitionController"; Loading Loading @@ -109,6 +137,7 @@ class TransitionController { * removed from this list. */ private final ArrayList<Transition> mPlayingTransitions = new ArrayList<>(); int mTrackCount = 0; /** The currently finishing transition. */ Transition mFinishingTransition; Loading Loading @@ -143,9 +172,25 @@ class TransitionController { private final ArrayList<QueuedTransition> mQueuedTransitions = new ArrayList<>(); /** The transition currently being constructed (collecting participants). */ /** * The transition currently being constructed (collecting participants). Unless interrupted, * all WM changes will go into this. */ private Transition mCollectingTransition = null; /** * The transitions that are complete but still waiting for participants to become ready */ final ArrayList<Transition> mWaitingTransitions = new ArrayList<>(); /** * The (non alwaysOnTop) tasks which were reported as on-top of their display most recently * within a cluster of simultaneous transitions. If tasks are nested, all the tasks that are * parents of the on-top task are also included. This is used to decide which transitions * report which on-top changes. */ final SparseArray<ArrayList<Task>> mLatestOnTopTasksReported = new SparseArray<>(); /** * `true` when building surface layer order for the finish transaction. We want to prevent * wm from touching z-order of surfaces during transitions, but we still need to be able to Loading Loading @@ -199,6 +244,11 @@ class TransitionController { mPlayingTransitions.get(i).cleanUpOnFailure(); } mPlayingTransitions.clear(); // Clean up waiting transitions first since they technically started first. for (int i = 0; i < mWaitingTransitions.size(); ++i) { mWaitingTransitions.get(i).abort(); } mWaitingTransitions.clear(); if (mCollectingTransition != null) { mCollectingTransition.abort(); } Loading @@ -223,8 +273,9 @@ class TransitionController { throw new IllegalStateException("Shell Transitions not enabled"); } if (mCollectingTransition != null) { throw new IllegalStateException("Simultaneous transition collection not supported" + " yet. Use {@link #createPendingTransition} for explicit queueing."); throw new IllegalStateException("Trying to directly start transition collection while " + " collection is already ongoing. Use {@link #startCollectOrQueue} if" + " possible."); } Transition transit = new Transition(type, flags, this, mSyncEngine); ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Creating Transition: %s", transit); Loading Loading @@ -318,7 +369,12 @@ class TransitionController { * This is {@code false} once a transition is playing. */ boolean isCollecting(@NonNull WindowContainer wc) { return mCollectingTransition != null && mCollectingTransition.mParticipants.contains(wc); if (mCollectingTransition == null) return false; if (mCollectingTransition.mParticipants.contains(wc)) return true; for (int i = 0; i < mWaitingTransitions.size(); ++i) { if (mWaitingTransitions.get(i).mParticipants.contains(wc)) return true; } return false; } /** Loading @@ -327,7 +383,11 @@ class TransitionController { */ boolean inCollectingTransition(@NonNull WindowContainer wc) { if (!isCollecting()) return false; return mCollectingTransition.isInTransition(wc); if (mCollectingTransition.isInTransition(wc)) return true; for (int i = 0; i < mWaitingTransitions.size(); ++i) { if (mWaitingTransitions.get(i).isInTransition(wc)) return true; } return false; } /** Loading Loading @@ -369,6 +429,9 @@ class TransitionController { if (mCollectingTransition != null && mCollectingTransition.isOnDisplay(dc)) { return true; } for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) { if (mWaitingTransitions.get(i).isOnDisplay(dc)) return true; } for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { if (mPlayingTransitions.get(i).isOnDisplay(dc)) return true; } Loading @@ -379,6 +442,9 @@ class TransitionController { if (mCollectingTransition != null && mCollectingTransition.isInTransientHide(task)) { return true; } for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) { if (mWaitingTransitions.get(i).isInTransientHide(task)) return true; } for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { if (mPlayingTransitions.get(i).isInTransientHide(task)) return true; } Loading Loading @@ -754,6 +820,8 @@ class TransitionController { mRunningLock.doNotifyLocked(); // Run state-validation checks when no transitions are active anymore. if (!inTransition()) { // Can reset track-count now that everything is idle. mTrackCount = 0; validateStates(); } } Loading @@ -771,12 +839,39 @@ class TransitionController { mStateValidators.clear(); } /** * Called when the transition has a complete set of participants for its operation. In other * words, it is when the transition is "ready" but is still waiting for participants to draw. */ void onTransitionPopulated(Transition transition) { tryStartCollectFromQueue(); } private boolean canStartCollectingNow(Transition queued) { if (mCollectingTransition == null) return true; // Population (collect until ready) is still serialized, so always wait for that. if (!mCollectingTransition.isPopulated()) return false; // Check if queued *can* be independent with all collecting/waiting transitions. if (!getCanBeIndependent(mCollectingTransition, queued)) return false; for (int i = 0; i < mWaitingTransitions.size(); ++i) { if (!getCanBeIndependent(mWaitingTransitions.get(i), queued)) return false; } return true; } void tryStartCollectFromQueue() { if (mQueuedTransitions.isEmpty()) return; // Only need to try the next one since, even when transition can collect in parallel, // they still need to serialize on readiness. final QueuedTransition queued = mQueuedTransitions.get(0); if (mCollectingTransition != null || mSyncEngine.hasActiveSync()) { if (mCollectingTransition != null) { // If it's a legacy sync, then it needs to wait until there is no collecting transition. if (queued.mTransition == null) return; if (!canStartCollectingNow(queued.mTransition)) return; mWaitingTransitions.add(mCollectingTransition); mCollectingTransition = null; } else if (mSyncEngine.hasActiveSync()) { // A legacy transition is on-going, so we must wait. return; } mQueuedTransitions.remove(0); Loading @@ -797,16 +892,81 @@ class TransitionController { } void moveToPlaying(Transition transition) { if (transition != mCollectingTransition) { throw new IllegalStateException("Trying to move non-collecting transition to playing"); } if (transition == mCollectingTransition) { mCollectingTransition = null; if (!mWaitingTransitions.isEmpty()) { mCollectingTransition = mWaitingTransitions.remove(0); } if (mCollectingTransition == null) { // nothing collecting anymore, so clear order records. mLatestOnTopTasksReported.clear(); } } else { if (!mWaitingTransitions.remove(transition)) { throw new IllegalStateException("Trying to move non-collecting transition to" + "playing " + transition.getSyncId()); } } mPlayingTransitions.add(transition); updateRunningRemoteAnimation(transition, true /* isPlaying */); mTransitionTracer.logState(transition); // Sync engine should become idle after this, so the idle listener will check the queue. } /** * Checks if the `queued` transition has the potential to run independently of the * `collecting` transition. It may still ultimately block in sync-engine or become dependent * in {@link #getIsIndependent} later. */ boolean getCanBeIndependent(Transition collecting, Transition queued) { if (queued.mParallelCollectType == Transition.PARALLEL_TYPE_MUTUAL && collecting.mParallelCollectType == Transition.PARALLEL_TYPE_MUTUAL) { return true; } return false; } /** * Checks if `incoming` transition can run independently of `running` transition assuming that * `running` is playing based on its current state. */ static boolean getIsIndependent(Transition running, Transition incoming) { if (running.mParallelCollectType == Transition.PARALLEL_TYPE_MUTUAL && incoming.mParallelCollectType == Transition.PARALLEL_TYPE_MUTUAL) { return true; } return false; } void assignTrack(Transition transition, TransitionInfo info) { int track = -1; boolean sync = false; for (int i = 0; i < mPlayingTransitions.size(); ++i) { // ignore ourself obviously if (mPlayingTransitions.get(i) == transition) continue; if (getIsIndependent(mPlayingTransitions.get(i), transition)) continue; if (track >= 0) { // At this point, transition overlaps with multiple tracks, so just wait for // everything sync = true; break; } track = mPlayingTransitions.get(i).mAnimationTrack; } if (sync) { track = 0; } if (track < 0) { // Didn't overlap with anything, so give it its own track track = mTrackCount; } if (sync) { info.setFlags(info.getFlags() | TransitionInfo.FLAG_SYNC); } info.setTrack(track); mTrackCount = Math.max(mTrackCount, track + 1); } void updateAnimatingState(SurfaceControl.Transaction t) { final boolean animatingState = !mPlayingTransitions.isEmpty() || (mCollectingTransition != null && mCollectingTransition.isStarted()); Loading Loading @@ -842,14 +1002,27 @@ class TransitionController { mRemotePlayer.update(delegate, isPlaying, true /* predict */); } void abort(Transition transition) { /** Called when a transition is aborted. This should only be called by {@link Transition} */ void onAbort(Transition transition) { if (transition != mCollectingTransition) { throw new IllegalStateException("Too late to abort."); int waitingIdx = mWaitingTransitions.indexOf(transition); if (waitingIdx < 0) { throw new IllegalStateException("Too late for abort."); } transition.abort(); mWaitingTransitions.remove(waitingIdx); } else { mCollectingTransition = null; if (!mWaitingTransitions.isEmpty()) { mCollectingTransition = mWaitingTransitions.remove(0); } if (mCollectingTransition == null) { // nothing collecting anymore, so clear order records. mLatestOnTopTasksReported.clear(); } } mTransitionTracer.logState(transition); // abort will call through the normal finish paths and thus check the queue. // This is called during Transition.abort whose codepath will eventually check the queue // via sync-engine idle. } /** Loading Loading @@ -956,7 +1129,17 @@ class TransitionController { return false; } if (mSyncEngine.hasActiveSync()) { if (!isCollecting()) { if (isCollecting()) { // Check if we can run in parallel here. if (canStartCollectingNow(transit)) { // start running in parallel. mWaitingTransitions.add(mCollectingTransition); mCollectingTransition = null; moveToCollecting(transit); onStartCollect.onCollectStarted(false /* deferred */); return true; } } else { Slog.w(TAG, "Ongoing Sync outside of transition."); } queueTransition(transit, onStartCollect); Loading services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +279 −1 File changed.Preview size limit exceeded, changes collapsed. Show changes services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java +1 −1 Original line number Diff line number Diff line Loading @@ -382,7 +382,7 @@ public class WallpaperControllerTests extends WindowTestsBase { assertTrue(wallpaperWindow.isVisible()); assertTrue(token.isVisibleRequested()); assertTrue(token.isVisible()); mWm.mAtmService.getTransitionController().abort(transit); transit.abort(); // In a transition, setting invisible should ONLY set requestedVisible false; otherwise // wallpaper should remain "visible" until transition is over. Loading Loading
services/core/java/com/android/server/wm/Transition.java +125 −20 Original line number Diff line number Diff line Loading @@ -107,6 +107,11 @@ import java.util.function.Predicate; * Represents a logical transition. This keeps track of all the changes associated with a logical * WM state -> state transition. * @see TransitionController * * In addition to tracking individual container changes, this also tracks ordering-changes (just * on-top for now). However, since order is a "global" property, the mechanics of order-change * detection/reporting is non-trivial when transitions are collecting in parallel. See * {@link #collectOrderChanges} for more details. */ class Transition implements BLASTSyncEngine.TransactionReadyListener { private static final String TAG = "Transition"; Loading Loading @@ -191,6 +196,12 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { */ private final ArrayList<Task> mOnTopTasksStart = new ArrayList<>(); /** * The (non alwaysOnTop) tasks which were on-top of their display when this transition became * ready (via setReady, not animation-ready). */ private final ArrayList<Task> mOnTopTasksAtReady = new ArrayList<>(); /** * Set of participating windowtokens (activity/wallpaper) which are visible at the end of * the transition animation. Loading Loading @@ -244,6 +255,36 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { */ boolean mIsPlayerEnabled = true; /** This transition doesn't run in parallel. */ static final int PARALLEL_TYPE_NONE = 0; /** Any 2 transitions of this type can run in parallel with each other. Used for testing. */ static final int PARALLEL_TYPE_MUTUAL = 1; @IntDef(prefix = { "PARALLEL_TYPE_" }, value = { PARALLEL_TYPE_NONE, PARALLEL_TYPE_MUTUAL }) @Retention(RetentionPolicy.SOURCE) @interface ParallelType {} /** * What category of parallel-collect support this transition has. The value of this is used * by {@link TransitionController} to determine which transitions can collect in parallel. If * a transition can collect in parallel, it means that it will start collecting as soon as the * prior collecting transition is {@link #isPopulated}. This is a shortcut for supporting * a couple specific situations before we have full-fledged support for parallel transitions. */ @ParallelType int mParallelCollectType = PARALLEL_TYPE_NONE; /** * A "Track" is a set of animations which must cooperate with each other to play smoothly. If * animations can play independently of each other, then they can be in different tracks. If * a transition must cooperate with transitions in >1 other track, then it must be marked * FLAG_SYNC and it will end-up flushing all animations before it starts. */ int mAnimationTrack = 0; Transition(@TransitionType int type, @TransitionFlags int flags, TransitionController controller, BLASTSyncEngine syncEngine) { mType = type; Loading Loading @@ -447,7 +488,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { throw new IllegalStateException("Attempting to re-use a transition"); } mState = STATE_COLLECTING; mSyncId = mSyncEngine.startSyncSet(this, timeoutMs, TAG, false /* parallel */); mSyncId = mSyncEngine.startSyncSet(this, timeoutMs, TAG, mParallelCollectType != PARALLEL_TYPE_NONE); mSyncEngine.setSyncMethod(mSyncId, TransitionController.SYNC_METHOD); mLogger.mSyncId = mSyncId; Loading Loading @@ -718,8 +760,15 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { final boolean ready = mReadyTracker.allReady(); ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Set transition ready=%b %d", ready, mSyncId); mSyncEngine.setReady(mSyncId, ready); if (ready) mLogger.mReadyTimeNs = SystemClock.elapsedRealtimeNanos(); boolean changed = mSyncEngine.setReady(mSyncId, ready); if (changed && ready) { mLogger.mReadyTimeNs = SystemClock.elapsedRealtimeNanos(); mOnTopTasksAtReady.clear(); for (int i = 0; i < mTargetDisplays.size(); ++i) { addOnTopTasks(mTargetDisplays.get(i), mOnTopTasksAtReady); } mController.onTransitionPopulated(this); } } /** Loading @@ -737,6 +786,11 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return mReadyTracker.allReady(); } /** This transition has all of its expected participants. */ boolean isPopulated() { return mState >= STATE_STARTED && mReadyTracker.allReady(); } /** * 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 Loading Loading @@ -1211,7 +1265,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { : mController.mAtm.mRootWindowContainer.getDefaultDisplay(); if (mState == STATE_ABORT) { mController.abort(this); mController.onAbort(this); primaryDisplay.getPendingTransaction().merge(transaction); mSyncId = -1; mOverrideOptions = null; Loading @@ -1222,13 +1276,15 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mState = STATE_PLAYING; mStartTransaction = transaction; mFinishTransaction = mController.mAtm.mWindowManager.mTransactionFactory.get(); mController.moveToPlaying(this); // Flags must be assigned before calculateTransitionInfo. Otherwise it won't take effect. if (primaryDisplay.isKeyguardLocked()) { mFlags |= TRANSIT_FLAG_KEYGUARD_LOCKED; } collectOrderChanges(); // This is the only (or last) transition that is collecting, so we need to report any // leftover order changes. collectOrderChanges(mController.mWaitingTransitions.isEmpty()); // Resolve the animating targets from the participants. mTargets = calculateTargets(mParticipants, mChanges); Loading @@ -1236,6 +1292,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mController.mAtm.mBackNavigationController.onTransactionReady(this, mTargets); final TransitionInfo info = calculateTransitionInfo(mType, mFlags, mTargets, transaction); info.setDebugId(mSyncId); mController.assignTrack(this, info); mController.moveToPlaying(this); // Repopulate the displays based on the resolved targets. mTargetDisplays.clear(); Loading Loading @@ -1383,17 +1442,58 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { info.releaseAnimSurfaces(); } /** Collect tasks which moved-to-top but didn't change otherwise. */ /** * Collect tasks which moved-to-top as part of this transition. This also updates the * controller's latest-reported when relevant. * * This is a non-trivial operation because transition can collect in parallel; however, it can * be made tenable by acknowledging that the "setup" part of collection (phase 1) is still * globally serial; so, we can build some reasonable rules around it. * * First, we record the "start" on-top state (to compare against). Then, when this becomes * ready (via allReady, NOT onTransactionReady), we also record the "onReady" on-top state * -- the idea here is that upon "allReady", all the actual WM changes should be done and we * are now just waiting for window content to become ready (finish drawing). * * Then, in this function (during onTransactionReady), we compare the two orders and include * any changes to the order in the reported transition-info. Unfortunately, because of parallel * collection, the order can change in unexpected ways by now. To resolve this, we ALSO keep a * global "latest reported order" in TransitionController and use that to make decisions. */ @VisibleForTesting void collectOrderChanges() { void collectOrderChanges(boolean reportCurrent) { if (mOnTopTasksStart.isEmpty()) return; final ArrayList<Task> onTopTasksEnd = new ArrayList<>(); for (int i = 0; i < mTargetDisplays.size(); ++i) { addOnTopTasks(mTargetDisplays.get(i), onTopTasksEnd); boolean includesOrderChange = false; for (int i = 0; i < mOnTopTasksAtReady.size(); ++i) { final Task task = mOnTopTasksAtReady.get(i); if (mOnTopTasksStart.contains(task)) continue; includesOrderChange = true; break; } for (int i = 0; i < onTopTasksEnd.size(); ++i) { if (!includesOrderChange && !reportCurrent) { // This transition doesn't include an order change, so if it isn't required to report // the current focus (eg. it's the last of a cluster of transitions), then don't // report. return; } // The transition included an order change, but it may not be up-to-date, so grab the // latest state and compare with the last reported state (or our start state if no // reported state exists). ArrayList<Task> onTopTasksEnd = new ArrayList<>(); for (int d = 0; d < mTargetDisplays.size(); ++d) { addOnTopTasks(mTargetDisplays.get(d), onTopTasksEnd); final int displayId = mTargetDisplays.get(d).mDisplayId; ArrayList<Task> reportedOnTop = mController.mLatestOnTopTasksReported.get(displayId); for (int i = onTopTasksEnd.size() - 1; i >= 0; --i) { final Task task = onTopTasksEnd.get(i); if (task.getDisplayId() != displayId) continue; // If it didn't change since last report, don't report if (reportedOnTop == null) { if (mOnTopTasksStart.contains(task)) continue; } else if (reportedOnTop.contains(task)) { continue; } // Need to report it. mParticipants.add(task); int changeIdx = mChanges.indexOfKey(task); if (changeIdx < 0) { Loading @@ -1402,6 +1502,11 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } mChanges.valueAt(changeIdx).mFlags |= ChangeInfo.FLAG_CHANGE_MOVED_TO_TOP; } // Swap in the latest on-top tasks. mController.mLatestOnTopTasksReported.put(displayId, onTopTasksEnd); onTopTasksEnd = reportedOnTop != null ? reportedOnTop : new ArrayList<>(); onTopTasksEnd.clear(); } } private void postCleanupOnFailure() { Loading
services/core/java/com/android/server/wm/TransitionController.java +199 −16 Original line number Diff line number Diff line Loading @@ -39,6 +39,7 @@ import android.os.SystemClock; import android.os.SystemProperties; import android.util.ArrayMap; import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; import android.view.SurfaceControl; Loading @@ -60,7 +61,34 @@ import java.util.ArrayList; import java.util.function.LongConsumer; /** * Handles all the aspects of recording and synchronizing transitions. * Handles all the aspects of recording (collecting) and synchronizing transitions. This is only * concerned with the WM changes. The actual animations are handled by the Player. * * Currently, only 1 transition can be the primary "collector" at a time. This is because WM changes * are still performed in a "global" manner. However, collecting can actually be broken into * two phases: * 1. Actually making WM changes and recording the participating containers. * 2. Waiting for the participating containers to become ready (eg. redrawing content). * Because (2) takes most of the time AND doesn't change WM, we can actually have multiple * transitions in phase (2) concurrently with one in phase (1). We refer to this arrangement as * "parallel" collection even though there is still only ever 1 transition actually able to gain * participants. * * Parallel collection happens when the "primary collector" has finished "setup" (phase 1) and is * just waiting. At this point, another transition can start collecting. When this happens, the * first transition is moved to a "waiting" list and the new transition becomes the "primary * collector". If at any time, the "primary collector" moves to playing before one of the waiting * transitions, then the first waiting transition will move back to being the "primary collector". * This maintains the "global"-like abstraction that the rest of WM currently expects. * * When a transition move-to-playing, we check it against all other playing transitions. If it * doesn't overlap with them, it can also animate in parallel. In this case it will be assigned a * new "track". "tracks" are a way to communicate to the player about which transitions need to be * played serially with each-other. So, if we find that a transition overlaps with other transitions * in one track, the transition will be assigned to that track. If, however, the transition overlaps * with transition in >1 track, we will actually just mark it as SYNC meaning it can't actually * play until all prior transition animations finish. This is heavy-handed because it is a fallback * situation and supporting something fancier would be unnecessarily complicated. */ class TransitionController { private static final String TAG = "TransitionController"; Loading Loading @@ -109,6 +137,7 @@ class TransitionController { * removed from this list. */ private final ArrayList<Transition> mPlayingTransitions = new ArrayList<>(); int mTrackCount = 0; /** The currently finishing transition. */ Transition mFinishingTransition; Loading Loading @@ -143,9 +172,25 @@ class TransitionController { private final ArrayList<QueuedTransition> mQueuedTransitions = new ArrayList<>(); /** The transition currently being constructed (collecting participants). */ /** * The transition currently being constructed (collecting participants). Unless interrupted, * all WM changes will go into this. */ private Transition mCollectingTransition = null; /** * The transitions that are complete but still waiting for participants to become ready */ final ArrayList<Transition> mWaitingTransitions = new ArrayList<>(); /** * The (non alwaysOnTop) tasks which were reported as on-top of their display most recently * within a cluster of simultaneous transitions. If tasks are nested, all the tasks that are * parents of the on-top task are also included. This is used to decide which transitions * report which on-top changes. */ final SparseArray<ArrayList<Task>> mLatestOnTopTasksReported = new SparseArray<>(); /** * `true` when building surface layer order for the finish transaction. We want to prevent * wm from touching z-order of surfaces during transitions, but we still need to be able to Loading Loading @@ -199,6 +244,11 @@ class TransitionController { mPlayingTransitions.get(i).cleanUpOnFailure(); } mPlayingTransitions.clear(); // Clean up waiting transitions first since they technically started first. for (int i = 0; i < mWaitingTransitions.size(); ++i) { mWaitingTransitions.get(i).abort(); } mWaitingTransitions.clear(); if (mCollectingTransition != null) { mCollectingTransition.abort(); } Loading @@ -223,8 +273,9 @@ class TransitionController { throw new IllegalStateException("Shell Transitions not enabled"); } if (mCollectingTransition != null) { throw new IllegalStateException("Simultaneous transition collection not supported" + " yet. Use {@link #createPendingTransition} for explicit queueing."); throw new IllegalStateException("Trying to directly start transition collection while " + " collection is already ongoing. Use {@link #startCollectOrQueue} if" + " possible."); } Transition transit = new Transition(type, flags, this, mSyncEngine); ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Creating Transition: %s", transit); Loading Loading @@ -318,7 +369,12 @@ class TransitionController { * This is {@code false} once a transition is playing. */ boolean isCollecting(@NonNull WindowContainer wc) { return mCollectingTransition != null && mCollectingTransition.mParticipants.contains(wc); if (mCollectingTransition == null) return false; if (mCollectingTransition.mParticipants.contains(wc)) return true; for (int i = 0; i < mWaitingTransitions.size(); ++i) { if (mWaitingTransitions.get(i).mParticipants.contains(wc)) return true; } return false; } /** Loading @@ -327,7 +383,11 @@ class TransitionController { */ boolean inCollectingTransition(@NonNull WindowContainer wc) { if (!isCollecting()) return false; return mCollectingTransition.isInTransition(wc); if (mCollectingTransition.isInTransition(wc)) return true; for (int i = 0; i < mWaitingTransitions.size(); ++i) { if (mWaitingTransitions.get(i).isInTransition(wc)) return true; } return false; } /** Loading Loading @@ -369,6 +429,9 @@ class TransitionController { if (mCollectingTransition != null && mCollectingTransition.isOnDisplay(dc)) { return true; } for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) { if (mWaitingTransitions.get(i).isOnDisplay(dc)) return true; } for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { if (mPlayingTransitions.get(i).isOnDisplay(dc)) return true; } Loading @@ -379,6 +442,9 @@ class TransitionController { if (mCollectingTransition != null && mCollectingTransition.isInTransientHide(task)) { return true; } for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) { if (mWaitingTransitions.get(i).isInTransientHide(task)) return true; } for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { if (mPlayingTransitions.get(i).isInTransientHide(task)) return true; } Loading Loading @@ -754,6 +820,8 @@ class TransitionController { mRunningLock.doNotifyLocked(); // Run state-validation checks when no transitions are active anymore. if (!inTransition()) { // Can reset track-count now that everything is idle. mTrackCount = 0; validateStates(); } } Loading @@ -771,12 +839,39 @@ class TransitionController { mStateValidators.clear(); } /** * Called when the transition has a complete set of participants for its operation. In other * words, it is when the transition is "ready" but is still waiting for participants to draw. */ void onTransitionPopulated(Transition transition) { tryStartCollectFromQueue(); } private boolean canStartCollectingNow(Transition queued) { if (mCollectingTransition == null) return true; // Population (collect until ready) is still serialized, so always wait for that. if (!mCollectingTransition.isPopulated()) return false; // Check if queued *can* be independent with all collecting/waiting transitions. if (!getCanBeIndependent(mCollectingTransition, queued)) return false; for (int i = 0; i < mWaitingTransitions.size(); ++i) { if (!getCanBeIndependent(mWaitingTransitions.get(i), queued)) return false; } return true; } void tryStartCollectFromQueue() { if (mQueuedTransitions.isEmpty()) return; // Only need to try the next one since, even when transition can collect in parallel, // they still need to serialize on readiness. final QueuedTransition queued = mQueuedTransitions.get(0); if (mCollectingTransition != null || mSyncEngine.hasActiveSync()) { if (mCollectingTransition != null) { // If it's a legacy sync, then it needs to wait until there is no collecting transition. if (queued.mTransition == null) return; if (!canStartCollectingNow(queued.mTransition)) return; mWaitingTransitions.add(mCollectingTransition); mCollectingTransition = null; } else if (mSyncEngine.hasActiveSync()) { // A legacy transition is on-going, so we must wait. return; } mQueuedTransitions.remove(0); Loading @@ -797,16 +892,81 @@ class TransitionController { } void moveToPlaying(Transition transition) { if (transition != mCollectingTransition) { throw new IllegalStateException("Trying to move non-collecting transition to playing"); } if (transition == mCollectingTransition) { mCollectingTransition = null; if (!mWaitingTransitions.isEmpty()) { mCollectingTransition = mWaitingTransitions.remove(0); } if (mCollectingTransition == null) { // nothing collecting anymore, so clear order records. mLatestOnTopTasksReported.clear(); } } else { if (!mWaitingTransitions.remove(transition)) { throw new IllegalStateException("Trying to move non-collecting transition to" + "playing " + transition.getSyncId()); } } mPlayingTransitions.add(transition); updateRunningRemoteAnimation(transition, true /* isPlaying */); mTransitionTracer.logState(transition); // Sync engine should become idle after this, so the idle listener will check the queue. } /** * Checks if the `queued` transition has the potential to run independently of the * `collecting` transition. It may still ultimately block in sync-engine or become dependent * in {@link #getIsIndependent} later. */ boolean getCanBeIndependent(Transition collecting, Transition queued) { if (queued.mParallelCollectType == Transition.PARALLEL_TYPE_MUTUAL && collecting.mParallelCollectType == Transition.PARALLEL_TYPE_MUTUAL) { return true; } return false; } /** * Checks if `incoming` transition can run independently of `running` transition assuming that * `running` is playing based on its current state. */ static boolean getIsIndependent(Transition running, Transition incoming) { if (running.mParallelCollectType == Transition.PARALLEL_TYPE_MUTUAL && incoming.mParallelCollectType == Transition.PARALLEL_TYPE_MUTUAL) { return true; } return false; } void assignTrack(Transition transition, TransitionInfo info) { int track = -1; boolean sync = false; for (int i = 0; i < mPlayingTransitions.size(); ++i) { // ignore ourself obviously if (mPlayingTransitions.get(i) == transition) continue; if (getIsIndependent(mPlayingTransitions.get(i), transition)) continue; if (track >= 0) { // At this point, transition overlaps with multiple tracks, so just wait for // everything sync = true; break; } track = mPlayingTransitions.get(i).mAnimationTrack; } if (sync) { track = 0; } if (track < 0) { // Didn't overlap with anything, so give it its own track track = mTrackCount; } if (sync) { info.setFlags(info.getFlags() | TransitionInfo.FLAG_SYNC); } info.setTrack(track); mTrackCount = Math.max(mTrackCount, track + 1); } void updateAnimatingState(SurfaceControl.Transaction t) { final boolean animatingState = !mPlayingTransitions.isEmpty() || (mCollectingTransition != null && mCollectingTransition.isStarted()); Loading Loading @@ -842,14 +1002,27 @@ class TransitionController { mRemotePlayer.update(delegate, isPlaying, true /* predict */); } void abort(Transition transition) { /** Called when a transition is aborted. This should only be called by {@link Transition} */ void onAbort(Transition transition) { if (transition != mCollectingTransition) { throw new IllegalStateException("Too late to abort."); int waitingIdx = mWaitingTransitions.indexOf(transition); if (waitingIdx < 0) { throw new IllegalStateException("Too late for abort."); } transition.abort(); mWaitingTransitions.remove(waitingIdx); } else { mCollectingTransition = null; if (!mWaitingTransitions.isEmpty()) { mCollectingTransition = mWaitingTransitions.remove(0); } if (mCollectingTransition == null) { // nothing collecting anymore, so clear order records. mLatestOnTopTasksReported.clear(); } } mTransitionTracer.logState(transition); // abort will call through the normal finish paths and thus check the queue. // This is called during Transition.abort whose codepath will eventually check the queue // via sync-engine idle. } /** Loading Loading @@ -956,7 +1129,17 @@ class TransitionController { return false; } if (mSyncEngine.hasActiveSync()) { if (!isCollecting()) { if (isCollecting()) { // Check if we can run in parallel here. if (canStartCollectingNow(transit)) { // start running in parallel. mWaitingTransitions.add(mCollectingTransition); mCollectingTransition = null; moveToCollecting(transit); onStartCollect.onCollectStarted(false /* deferred */); return true; } } else { Slog.w(TAG, "Ongoing Sync outside of transition."); } queueTransition(transit, onStartCollect); Loading
services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +279 −1 File changed.Preview size limit exceeded, changes collapsed. Show changes
services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java +1 −1 Original line number Diff line number Diff line Loading @@ -382,7 +382,7 @@ public class WallpaperControllerTests extends WindowTestsBase { assertTrue(wallpaperWindow.isVisible()); assertTrue(token.isVisibleRequested()); assertTrue(token.isVisible()); mWm.mAtmService.getTransitionController().abort(transit); transit.abort(); // In a transition, setting invisible should ONLY set requestedVisible false; otherwise // wallpaper should remain "visible" until transition is over. Loading