summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/core/java/com/android/server/wm/Transition.java145
-rw-r--r--services/core/java/com/android/server/wm/TransitionController.java215
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TransitionTests.java280
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java2
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.