diff options
4 files changed, 604 insertions, 38 deletions
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index b314ed17244c..54dfdd93b26c 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -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"; @@ -192,6 +197,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. */ @@ -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; @@ -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; @@ -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); + } } /** @@ -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 @@ -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; @@ -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); @@ -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(); @@ -1383,24 +1442,70 @@ 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); - } - for (int i = 0; i < onTopTasksEnd.size(); ++i) { - final Task task = onTopTasksEnd.get(i); + boolean includesOrderChange = false; + for (int i = 0; i < mOnTopTasksAtReady.size(); ++i) { + final Task task = mOnTopTasksAtReady.get(i); if (mOnTopTasksStart.contains(task)) continue; - mParticipants.add(task); - int changeIdx = mChanges.indexOfKey(task); - if (changeIdx < 0) { - mChanges.put(task, new ChangeInfo(task)); - changeIdx = mChanges.indexOfKey(task); + includesOrderChange = true; + break; + } + 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) { + mChanges.put(task, new ChangeInfo(task)); + changeIdx = mChanges.indexOfKey(task); + } + mChanges.valueAt(changeIdx).mFlags |= ChangeInfo.FLAG_CHANGE_MOVED_TO_TOP; } - 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(); } } diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index e8e4792690be..8af037be8d06 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -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; @@ -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"; @@ -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; @@ -143,10 +172,26 @@ 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 * calculate the layers for the finishTransaction. So, when assigning layers into the finish @@ -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(); } @@ -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); @@ -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; } /** @@ -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; } /** @@ -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; } @@ -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; } @@ -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(); } } @@ -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); @@ -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()); + } } - mCollectingTransition = null; 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()); @@ -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."); + } + 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(); + } } - transition.abort(); - mCollectingTransition = null; 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. } /** @@ -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); diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 0dac346bb717..b59f027fec83 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -23,6 +23,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS; +import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; @@ -36,6 +37,7 @@ import static android.window.TransitionInfo.FLAG_FILLS_TASK; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; +import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static android.window.TransitionInfo.isIndependent; @@ -74,6 +76,7 @@ import android.platform.test.annotations.Presubmit; import android.util.ArrayMap; import android.util.ArraySet; import android.view.SurfaceControl; +import android.view.WindowManager; import android.window.IDisplayAreaOrganizer; import android.window.IRemoteTransition; import android.window.ITaskFragmentOrganizer; @@ -96,6 +99,7 @@ import java.util.ArrayList; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import java.util.function.Function; /** @@ -1903,7 +1907,7 @@ public class TransitionTests extends WindowTestsBase { assertTrue(targets.isEmpty()); // After collecting order changes, it should recognize that a task moved to top. - transition.collectOrderChanges(); + transition.collectOrderChanges(true); targets = Transition.calculateTargets(participants, changes); assertEquals(1, targets.size()); @@ -1914,6 +1918,192 @@ public class TransitionTests extends WindowTestsBase { assertEquals(TRANSIT_CHANGE, info.getChanges().get(0).getMode()); } + private class OrderChangeTestSetup { + final TransitionController mController; + final TestTransitionPlayer mPlayer; + final Transition mTransitA; + final Transition mTransitB; + + OrderChangeTestSetup() { + mController = mAtm.getTransitionController(); + mPlayer = registerTestTransitionPlayer(); + mController.setSyncEngine(mWm.mSyncEngine); + + mTransitA = createTestTransition(TRANSIT_OPEN, mController); + mTransitA.mParallelCollectType = Transition.PARALLEL_TYPE_MUTUAL; + mTransitB = createTestTransition(TRANSIT_OPEN, mController); + mTransitB.mParallelCollectType = Transition.PARALLEL_TYPE_MUTUAL; + } + + void startParallelCollect(boolean activityLevelFirst) { + // Start with taskB on top and taskA on bottom but both visible. + final Task taskA = createTask(mDisplayContent); + taskA.setVisibleRequested(true); + final ActivityRecord actA = createActivityRecord(taskA); + final TestWindowState winA = createWindowState( + new WindowManager.LayoutParams(TYPE_BASE_APPLICATION), actA); + actA.addWindow(winA); + final ActivityRecord actB = createActivityRecord(taskA); + final TestWindowState winB = createWindowState( + new WindowManager.LayoutParams(TYPE_BASE_APPLICATION), actB); + actB.addWindow(winB); + + final Task taskB = createTask(mDisplayContent); + actA.setVisibleRequested(true); + actB.setVisibleRequested(false); + taskB.setVisibleRequested(true); + assertTrue(actA.isAttached()); + + final Consumer<Boolean> startAndCollectA = (doReady) -> { + mController.startCollectOrQueue(mTransitA, (deferred) -> { + }); + + // Collect activity-level change into A + mTransitA.collect(actA); + actA.setVisibleRequested(false); + winA.onSyncFinishedDrawing(); + mTransitA.collect(actB); + actB.setVisibleRequested(true); + winB.onSyncFinishedDrawing(); + mTransitA.start(); + if (doReady) { + mTransitA.setReady(mDisplayContent, true); + } + }; + final Consumer<Boolean> startAndCollectB = (doReady) -> { + mController.startCollectOrQueue(mTransitB, (deferred) -> { + }); + mTransitB.collect(taskA); + taskA.moveToFront("test"); + mTransitB.start(); + if (doReady) { + mTransitB.setReady(mDisplayContent, true); + } + }; + + if (activityLevelFirst) { + startAndCollectA.accept(true); + startAndCollectB.accept(false); + } else { + startAndCollectB.accept(true); + startAndCollectA.accept(false); + } + } + } + + @Test + public void testMoveToTopStartAfterReadyAfterParallel() { + // Start collect activity-only transit A + // Start collect task transit B in parallel + // finish A first -> should not include order change from B. + final OrderChangeTestSetup setup = new OrderChangeTestSetup(); + setup.startParallelCollect(true /* activity first */); + + mWm.mSyncEngine.tryFinishForTest(setup.mTransitA.getSyncId()); + waitUntilHandlersIdle(); + for (int i = 0; i < setup.mPlayer.mLastReady.getChanges().size(); ++i) { + assertNull(setup.mPlayer.mLastReady.getChanges().get(i).getTaskInfo()); + } + + setup.mTransitB.setAllReady(); + mWm.mSyncEngine.tryFinishForTest(setup.mTransitB.getSyncId()); + waitUntilHandlersIdle(); + boolean hasOrderChange = false; + for (int i = 0; i < setup.mPlayer.mLastReady.getChanges().size(); ++i) { + final TransitionInfo.Change chg = setup.mPlayer.mLastReady.getChanges().get(i); + if (chg.getTaskInfo() == null) continue; + hasOrderChange = hasOrderChange || (chg.getFlags() & FLAG_MOVED_TO_TOP) != 0; + } + assertTrue(hasOrderChange); + } + + @Test + public void testMoveToTopStartAfterReadyBeforeParallel() { + // Start collect activity-only transit A + // Start collect task transit B in parallel + // finish B first -> should include order change + // then finish A -> should NOT include order change. + final OrderChangeTestSetup setup = new OrderChangeTestSetup(); + setup.startParallelCollect(true /* activity first */); + // Make it unready now so that it doesn't get dequeued automatically. + setup.mTransitA.setReady(mDisplayContent, false); + + // Make task change ready first + setup.mTransitB.setAllReady(); + mWm.mSyncEngine.tryFinishForTest(setup.mTransitB.getSyncId()); + waitUntilHandlersIdle(); + boolean hasOrderChange = false; + for (int i = 0; i < setup.mPlayer.mLastReady.getChanges().size(); ++i) { + final TransitionInfo.Change chg = setup.mPlayer.mLastReady.getChanges().get(i); + if (chg.getTaskInfo() == null) continue; + hasOrderChange = hasOrderChange || (chg.getFlags() & FLAG_MOVED_TO_TOP) != 0; + } + assertTrue(hasOrderChange); + + setup.mTransitA.setAllReady(); + mWm.mSyncEngine.tryFinishForTest(setup.mTransitA.getSyncId()); + waitUntilHandlersIdle(); + for (int i = 0; i < setup.mPlayer.mLastReady.getChanges().size(); ++i) { + assertNull(setup.mPlayer.mLastReady.getChanges().get(i).getTaskInfo()); + } + } + + @Test + public void testMoveToTopStartBeforeReadyAfterParallel() { + // Start collect task transit B + // Start collect activity-only transit A in parallel + // finish A first -> should not include order change from B. + final OrderChangeTestSetup setup = new OrderChangeTestSetup(); + setup.startParallelCollect(false /* activity first */); + // Make B unready now so that it doesn't get dequeued automatically. + setup.mTransitB.setReady(mDisplayContent, false); + + setup.mTransitA.setAllReady(); + mWm.mSyncEngine.tryFinishForTest(setup.mTransitA.getSyncId()); + waitUntilHandlersIdle(); + for (int i = 0; i < setup.mPlayer.mLastReady.getChanges().size(); ++i) { + assertNull(setup.mPlayer.mLastReady.getChanges().get(i).getTaskInfo()); + } + + setup.mTransitB.setAllReady(); + mWm.mSyncEngine.tryFinishForTest(setup.mTransitB.getSyncId()); + waitUntilHandlersIdle(); + boolean hasOrderChange = false; + for (int i = 0; i < setup.mPlayer.mLastReady.getChanges().size(); ++i) { + final TransitionInfo.Change chg = setup.mPlayer.mLastReady.getChanges().get(i); + if (chg.getTaskInfo() == null) continue; + hasOrderChange = hasOrderChange || (chg.getFlags() & FLAG_MOVED_TO_TOP) != 0; + } + assertTrue(hasOrderChange); + } + + @Test + public void testMoveToTopStartBeforeReadyBeforeParallel() { + // Start collect task transit B + // Start collect activity-only transit A in parallel + // finish B first -> should include order change + // then finish A -> should NOT include order change. + final OrderChangeTestSetup setup = new OrderChangeTestSetup(); + setup.startParallelCollect(false /* activity first */); + + mWm.mSyncEngine.tryFinishForTest(setup.mTransitB.getSyncId()); + waitUntilHandlersIdle(); + boolean hasOrderChange = false; + for (int i = 0; i < setup.mPlayer.mLastReady.getChanges().size(); ++i) { + final TransitionInfo.Change chg = setup.mPlayer.mLastReady.getChanges().get(i); + if (chg.getTaskInfo() == null) continue; + hasOrderChange = hasOrderChange || (chg.getFlags() & FLAG_MOVED_TO_TOP) != 0; + } + assertTrue(hasOrderChange); + + setup.mTransitA.setAllReady(); + mWm.mSyncEngine.tryFinishForTest(setup.mTransitA.getSyncId()); + waitUntilHandlersIdle(); + for (int i = 0; i < setup.mPlayer.mLastReady.getChanges().size(); ++i) { + assertNull(setup.mPlayer.mLastReady.getChanges().get(i).getTaskInfo()); + } + } + @Test public void testQueueStartCollect() { final TransitionController controller = mAtm.getTransitionController(); @@ -2018,6 +2208,94 @@ public class TransitionTests extends WindowTestsBase { assertTrue(transitB.isCollecting()); } + @Test + public void testQueueParallel() { + final TransitionController controller = mAtm.getTransitionController(); + final TestTransitionPlayer player = registerTestTransitionPlayer(); + + mSyncEngine = createTestBLASTSyncEngine(); + controller.setSyncEngine(mSyncEngine); + + final Transition transitA = createTestTransition(TRANSIT_OPEN, controller); + transitA.mParallelCollectType = Transition.PARALLEL_TYPE_MUTUAL; + final Transition transitB = createTestTransition(TRANSIT_OPEN, controller); + transitB.mParallelCollectType = Transition.PARALLEL_TYPE_MUTUAL; + final Transition transitC = createTestTransition(TRANSIT_OPEN, controller); + transitC.mParallelCollectType = Transition.PARALLEL_TYPE_MUTUAL; + final Transition transitSync = createTestTransition(TRANSIT_OPEN, controller); + final Transition transitD = createTestTransition(TRANSIT_OPEN, controller); + + controller.startCollectOrQueue(transitA, (deferred) -> {}); + controller.startCollectOrQueue(transitB, (deferred) -> {}); + controller.startCollectOrQueue(transitC, (deferred) -> {}); + controller.startCollectOrQueue(transitSync, (deferred) -> {}); + controller.startCollectOrQueue(transitD, (deferred) -> {}); + + assertTrue(transitA.isCollecting() && !transitA.isStarted()); + // We still serialize on readiness + assertTrue(transitB.isPending()); + assertTrue(transitC.isPending()); + + transitA.start(); + transitA.setAllReady(); + transitB.start(); + transitB.setAllReady(); + + // A, B, and C should be collecting in parallel now. + assertTrue(transitA.isStarted()); + assertTrue(transitB.isStarted()); + assertTrue(transitC.isCollecting() && !transitC.isStarted()); + + transitC.start(); + transitC.setAllReady(); + + assertTrue(transitA.isStarted()); + assertTrue(transitB.isStarted()); + assertTrue(transitC.isStarted()); + // Not parallel so should remain pending + assertTrue(transitSync.isPending()); + // After Sync, so should also remain pending. + assertTrue(transitD.isPending()); + // There should always be a collector, since Sync can't collect yet, C should remain. + assertEquals(transitC, controller.getCollectingTransition()); + + mSyncEngine.tryFinishForTest(transitB.getSyncId()); + + // The other transitions should remain waiting. + assertTrue(transitA.isStarted()); + assertTrue(transitB.isPlaying()); + assertTrue(transitC.isStarted()); + assertEquals(transitC, controller.getCollectingTransition()); + + mSyncEngine.tryFinishForTest(transitC.getSyncId()); + assertTrue(transitA.isStarted()); + assertTrue(transitC.isPlaying()); + // The "collecting" one became ready, so the first "waiting" should move back to collecting. + assertEquals(transitA, controller.getCollectingTransition()); + + assertTrue(transitSync.isPending()); + assertTrue(transitD.isPending()); + mSyncEngine.tryFinishForTest(transitA.getSyncId()); + + // Now all collectors are done, so sync can be pulled-off the queue. + assertTrue(transitSync.isCollecting() && !transitSync.isStarted()); + transitSync.start(); + transitSync.setAllReady(); + // Since D can run in parallel, it should be pulled-off the queue. + assertTrue(transitSync.isStarted()); + assertTrue(transitD.isPending()); + + mSyncEngine.tryFinishForTest(transitSync.getSyncId()); + assertTrue(transitD.isCollecting()); + + transitD.start(); + transitD.setAllReady(); + mSyncEngine.tryFinishForTest(transitD.getSyncId()); + + // Now nothing should be collecting + assertFalse(controller.isCollecting()); + } + private static void makeTaskOrganized(Task... tasks) { final ITaskOrganizer organizer = mock(ITaskOrganizer.class); for (Task t : tasks) { diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java index 453096306694..01ddcca99300 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java @@ -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. |