diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index 0f3eef7a32898..628fc3140ee82 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -152,8 +152,14 @@ public final class TransitionInfo implements Parcelable { /** The task became the top-most task even if it didn't change visibility. */ public static final int FLAG_MOVED_TO_TOP = 1 << 20; + /** + * This transition must be the only transition when it starts (ie. it must wait for all other + * transition animations to finish). + */ + public static final int FLAG_SYNC = 1 << 21; + /** The first unused bit. This can be used by remotes to attach custom flags to this change. */ - public static final int FLAG_FIRST_CUSTOM = 1 << 21; + public static final int FLAG_FIRST_CUSTOM = 1 << 22; /** The change belongs to a window that won't contain activities. */ public static final int FLAGS_IS_NON_APP_WINDOW = @@ -183,12 +189,14 @@ public final class TransitionInfo implements Parcelable { FLAG_NO_ANIMATION, FLAG_TASK_LAUNCHING_BEHIND, FLAG_MOVED_TO_TOP, + FLAG_SYNC, FLAG_FIRST_CUSTOM }) public @interface ChangeFlags {} private final @TransitionType int mType; - private final @TransitionFlags int mFlags; + private @TransitionFlags int mFlags; + private int mTrack = 0; private final ArrayList mChanges = new ArrayList<>(); private final ArrayList mRoots = new ArrayList<>(); @@ -210,6 +218,7 @@ public final class TransitionInfo implements Parcelable { in.readTypedList(mRoots, Root.CREATOR); mOptions = in.readTypedObject(AnimationOptions.CREATOR); mDebugId = in.readInt(); + mTrack = in.readInt(); } @Override @@ -221,6 +230,7 @@ public final class TransitionInfo implements Parcelable { dest.writeTypedList(mRoots, flags); dest.writeTypedObject(mOptions, flags); dest.writeInt(mDebugId); + dest.writeInt(mTrack); } @NonNull @@ -262,6 +272,10 @@ public final class TransitionInfo implements Parcelable { return mType; } + public void setFlags(int flags) { + mFlags = flags; + } + public int getFlags() { return mFlags; } @@ -356,6 +370,16 @@ public final class TransitionInfo implements Parcelable { return (mFlags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0; } + /** Gets which animation track this transition should run on. */ + public int getTrack() { + return mTrack; + } + + /** Sets which animation track this transition should run on. */ + public void setTrack(int track) { + mTrack = track; + } + /** * Set an arbitrary "debug" id for this info. This id will not be used for any "real work", * it is just for debugging and logging. @@ -373,7 +397,8 @@ public final class TransitionInfo implements Parcelable { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("{id=").append(mDebugId).append(" t=").append(transitTypeToString(mType)) - .append(" f=0x").append(Integer.toHexString(mFlags)).append(" r=["); + .append(" f=0x").append(Integer.toHexString(mFlags)).append(" trk=").append(mTrack) + .append(" r=["); for (int i = 0; i < mRoots.size(); ++i) { if (i > 0) { sb.append(','); @@ -461,6 +486,9 @@ public final class TransitionInfo implements Parcelable { if ((flags & FLAG_TASK_LAUNCHING_BEHIND) != 0) { sb.append((sb.length() == 0 ? "" : "|") + "TASK_LAUNCHING_BEHIND"); } + if ((flags & FLAG_SYNC) != 0) { + sb.append((sb.length() == 0 ? "" : "|") + "SYNC"); + } if ((flags & FLAG_FIRST_CUSTOM) != 0) { sb.append(sb.length() == 0 ? "" : "|").append("FIRST_CUSTOM"); } @@ -532,6 +560,7 @@ public final class TransitionInfo implements Parcelable { */ public TransitionInfo localRemoteCopy() { final TransitionInfo out = new TransitionInfo(mType, mFlags); + out.mTrack = mTrack; out.mDebugId = mDebugId; for (int i = 0; i < mChanges.size(); ++i) { out.mChanges.add(mChanges.get(i).localRemoteCopy()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index 5a92f78301944..7c729a46b6799 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -235,6 +235,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, private TransitionInfo subCopy(@NonNull TransitionInfo info, @WindowManager.TransitionType int newType, boolean withChanges) { final TransitionInfo out = new TransitionInfo(newType, withChanges ? info.getFlags() : 0); + out.setTrack(info.getTrack()); out.setDebugId(info.getDebugId()); if (withChanges) { for (int i = 0; i < info.getChanges().size(); ++i) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 08b0bf74f4131..5c8791effe18e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -90,14 +90,21 @@ import java.util.Arrays; * Basically: --start--> PENDING --onTransitionReady--> READY --play--> ACTIVE --finish--> | * --merge--> MERGED --^ * - * At the moment, only one transition can be animating at a time. While a transition is animating, - * transitions will be queued in the "ready" state for their turn. At the same time, whenever a - * transition makes it to the head of the "ready" queue, it will attempt to merge to with the - * "active" transition. If the merge succeeds, it will be moved to the "active" transition's - * "merged" and then the next "ready" transition can attempt to merge. + * The READY and beyond lifecycle is managed per "track". Within a track, all the animations are + * serialized as described; however, multiple tracks can play simultaneously. This implies that, + * within a track, only one transition can be animating ("active") at a time. * - * Once the "active" transition animation is finished, it will be removed from the "active" list - * and then the next "ready" transition can play. + * While a transition is animating in a track, transitions dispatched to the track will be queued + * in the "ready" state for their turn. At the same time, whenever a transition makes it to the + * head of the "ready" queue, it will attempt to merge to with the "active" transition. If the + * merge succeeds, it will be moved to the "active" transition's "merged" list and then the next + * "ready" transition can attempt to merge. Once the "active" transition animation is finished, + * the next "ready" transition can play. + * + * Track assignments are expected to be provided by WMCore and this generally tries to maintain + * the same assignments. If, however, WMCore decides that a transition conflicts with >1 active + * track, it will be marked as SYNC. This means that all currently active tracks must be flushed + * before the SYNC transition can play. */ public class Transitions implements RemoteCallable { static final String TAG = "ShellTransitions"; @@ -172,12 +179,15 @@ public class Transitions implements RemoteCallable { private float mTransitionAnimationScaleSetting = 1.0f; /** - * How much time we allow for an animation to finish itself on sleep. If it takes longer, we + * How much time we allow for an animation to finish itself on sync. If it takes longer, we * will force-finish it (on this end) which may leave it in a bad state but won't hang the * device. This needs to be pretty small because it is an allowance for each queued animation, * however it can't be too small since there is some potential IPC involved. */ - private static final int SLEEP_ALLOWANCE_MS = 120; + private static final int SYNC_ALLOWANCE_MS = 120; + + /** For testing only. Disables the force-finish timeout on sync. */ + private boolean mDisableForceSync = false; private static final class ActiveTransition { IBinder mToken; @@ -190,23 +200,45 @@ public class Transitions implements RemoteCallable { /** Ordered list of transitions which have been merged into this one. */ private ArrayList mMerged; + boolean isSync() { + return (mInfo.getFlags() & TransitionInfo.FLAG_SYNC) != 0; + } + + int getTrack() { + return mInfo != null ? mInfo.getTrack() : -1; + } + @Override public String toString() { if (mInfo != null && mInfo.getDebugId() >= 0) { - return "(#" + mInfo.getDebugId() + ")" + mToken; + return "(#" + mInfo.getDebugId() + ")" + mToken + "@" + getTrack(); } - return mToken.toString(); + return mToken.toString() + "@" + getTrack(); + } + } + + private static class Track { + /** Keeps track of transitions which are ready to play but still waiting for their turn. */ + final ArrayList mReadyTransitions = new ArrayList<>(); + + /** The currently playing transition in this track. */ + ActiveTransition mActiveTransition = null; + + boolean isIdle() { + return mActiveTransition == null && mReadyTransitions.isEmpty(); } } /** Keeps track of transitions which have been started, but aren't ready yet. */ private final ArrayList mPendingTransitions = new ArrayList<>(); - /** Keeps track of transitions which are ready to play but still waiting for their turn. */ - private final ArrayList mReadyTransitions = new ArrayList<>(); + /** + * Transitions which are ready to play, but haven't been sent to a track yet because a sync + * is ongoing. + */ + private final ArrayList mReadyDuringSync = new ArrayList<>(); - /** Keeps track of currently playing transitions. For now, there can only be 1 max. */ - private final ArrayList mActiveTransitions = new ArrayList<>(); + private final ArrayList mTracks = new ArrayList<>(); public Transitions(@NonNull Context context, @NonNull ShellInit shellInit, @@ -374,14 +406,17 @@ public class Transitions implements RemoteCallable { * will be executed when the last active transition is finished. */ public void runOnIdle(Runnable runnable) { - if (mActiveTransitions.isEmpty() && mPendingTransitions.isEmpty() - && mReadyTransitions.isEmpty()) { + if (isIdle()) { runnable.run(); } else { mRunWhenIdleQueue.add(runnable); } } + void setDisableForceSyncForTest(boolean disable) { + mDisableForceSync = disable; + } + /** * Sets up visibility/alpha/transforms to resemble the starting state of an animation. */ @@ -542,6 +577,13 @@ public class Transitions implements RemoteCallable { return true; } + private Track getOrCreateTrack(int trackId) { + while (trackId >= mTracks.size()) { + mTracks.add(new Track()); + } + return mTracks.get(trackId); + } + @VisibleForTesting void onTransitionReady(@NonNull IBinder transitionToken, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) { @@ -554,29 +596,58 @@ public class Transitions implements RemoteCallable { + Arrays.toString(mPendingTransitions.stream().map( activeTransition -> activeTransition.mToken).toArray())); } - if (activeIdx > 0) { - Log.e(TAG, "Transition became ready out-of-order " + mPendingTransitions.get(activeIdx) - + ". Expected order: " + Arrays.toString(mPendingTransitions.stream().map( - activeTransition -> activeTransition.mToken).toArray())); - } // Move from pending to ready final ActiveTransition active = mPendingTransitions.remove(activeIdx); - mReadyTransitions.add(active); active.mInfo = info; active.mStartT = t; active.mFinishT = finishT; + if (activeIdx > 0) { + Log.i(TAG, "Transition might be ready out-of-order " + activeIdx + " for " + active + + ". This is ok if it's on a different track."); + } + if (!mReadyDuringSync.isEmpty()) { + mReadyDuringSync.add(active); + } else { + dispatchReady(active); + } + } - for (int i = 0; i < mObservers.size(); ++i) { - mObservers.get(i).onTransitionReady(transitionToken, info, t, finishT); + /** + * Returns true if dispatching succeeded, otherwise false. Dispatching can fail if it is + * blocked by a sync or sleep. + */ + boolean dispatchReady(ActiveTransition active) { + final TransitionInfo info = active.mInfo; + + if (info.getType() == TRANSIT_SLEEP || active.isSync()) { + // Adding to *front*! If we are here, it means that it was pulled off the front + // so we are just putting it back; or, it is the first one so it doesn't matter. + mReadyDuringSync.add(0, active); + boolean hadPreceding = false; + // Now flush all the tracks. + for (int i = 0; i < mTracks.size(); ++i) { + final Track tr = mTracks.get(i); + if (tr.isIdle()) continue; + hadPreceding = true; + // Sleep starts a process of forcing all prior transitions to finish immediately + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "Start finish-for-sync track %d", i); + finishForSync(i, null /* forceFinish */); + } + if (hadPreceding) { + return false; + } + // Actually able to process the sleep now, so re-remove it from the queue and continue + // the normal flow. + mReadyDuringSync.remove(active); } - if (info.getType() == TRANSIT_SLEEP) { - if (activeIdx > 0 || !mActiveTransitions.isEmpty() || mReadyTransitions.size() > 1) { - // Sleep starts a process of forcing all prior transitions to finish immediately - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Start finish-for-sleep"); - finishForSleep(null /* forceFinish */); - return; - } + final Track track = getOrCreateTrack(info.getTrack()); + track.mReadyTransitions.add(active); + + for (int i = 0; i < mObservers.size(); ++i) { + mObservers.get(i).onTransitionReady( + active.mToken, info, active.mStartT, active.mFinishT); } if (info.getRootCount() == 0 && !alwaysReportToKeyguard(info)) { @@ -585,7 +656,7 @@ public class Transitions implements RemoteCallable { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "No transition roots in %s so" + " abort", active); onAbort(active); - return; + return true; } final int changeSize = info.getChanges().size(); @@ -615,16 +686,17 @@ public class Transitions implements RemoteCallable { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Non-visible anim so abort: %s", active); onAbort(active); - return; + return true; } setupStartState(active.mInfo, active.mStartT, active.mFinishT); - if (mReadyTransitions.size() > 1) { + if (track.mReadyTransitions.size() > 1) { // There are already transitions waiting in the queue, so just return. - return; + return true; } - processReadyQueue(); + processReadyQueue(track); + return true; } /** @@ -644,25 +716,53 @@ public class Transitions implements RemoteCallable { return false; } - void processReadyQueue() { - if (mReadyTransitions.isEmpty()) { - // Check if idle. - if (mActiveTransitions.isEmpty() && mPendingTransitions.isEmpty()) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition " - + "animations finished"); - // Run all runnables from the run-when-idle queue. - for (int i = 0; i < mRunWhenIdleQueue.size(); i++) { - mRunWhenIdleQueue.get(i).run(); + private boolean areTracksIdle() { + for (int i = 0; i < mTracks.size(); ++i) { + if (!mTracks.get(i).isIdle()) return false; + } + return true; + } + + private boolean isAnimating() { + return !mReadyDuringSync.isEmpty() || !areTracksIdle(); + } + + private boolean isIdle() { + return mPendingTransitions.isEmpty() && !isAnimating(); + } + + void processReadyQueue(Track track) { + if (track.mReadyTransitions.isEmpty()) { + if (track.mActiveTransition == null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Track %d became idle", + mTracks.indexOf(track)); + if (areTracksIdle()) { + if (!mReadyDuringSync.isEmpty()) { + // Dispatch everything unless we hit another sync + while (!mReadyDuringSync.isEmpty()) { + ActiveTransition next = mReadyDuringSync.remove(0); + boolean success = dispatchReady(next); + // Hit a sync or sleep, so stop dispatching. + if (!success) break; + } + } else if (mPendingTransitions.isEmpty()) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition " + + "animations finished"); + // Run all runnables from the run-when-idle queue. + for (int i = 0; i < mRunWhenIdleQueue.size(); i++) { + mRunWhenIdleQueue.get(i).run(); + } + mRunWhenIdleQueue.clear(); + } } - mRunWhenIdleQueue.clear(); } return; } - final ActiveTransition ready = mReadyTransitions.get(0); - if (mActiveTransitions.isEmpty()) { - // The normal case, just play it (currently we only support 1 active transition). - mReadyTransitions.remove(0); - mActiveTransitions.add(ready); + final ActiveTransition ready = track.mReadyTransitions.get(0); + if (track.mActiveTransition == null) { + // The normal case, just play it. + track.mReadyTransitions.remove(0); + track.mActiveTransition = ready; if (ready.mAborted) { // finish now since there's nothing to animate. Calls back into processReadyQueue onFinish(ready, null, null); @@ -670,11 +770,11 @@ public class Transitions implements RemoteCallable { } playTransition(ready); // Attempt to merge any more queued-up transitions. - processReadyQueue(); + processReadyQueue(track); return; } // An existing animation is playing, so see if we can merge. - final ActiveTransition playing = mActiveTransitions.get(0); + final ActiveTransition playing = track.mActiveTransition; if (ready.mAborted) { // record as merged since it is no-op. Calls back into processReadyQueue onMerged(playing, ready); @@ -688,18 +788,23 @@ public class Transitions implements RemoteCallable { } private void onMerged(@NonNull ActiveTransition playing, @NonNull ActiveTransition merged) { + if (playing.getTrack() != merged.getTrack()) { + throw new IllegalStateException("Can't merge across tracks: " + merged + " into " + + playing); + } + final Track track = mTracks.get(playing.getTrack()); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged: %s into %s", merged, playing); int readyIdx = 0; - if (mReadyTransitions.isEmpty() || mReadyTransitions.get(0) != merged) { + if (track.mReadyTransitions.isEmpty() || track.mReadyTransitions.get(0) != merged) { Log.e(TAG, "Merged transition out-of-order? " + merged); - readyIdx = mReadyTransitions.indexOf(merged); + readyIdx = track.mReadyTransitions.indexOf(merged); if (readyIdx < 0) { Log.e(TAG, "Merged a transition that is no-longer queued? " + merged); return; } } - mReadyTransitions.remove(readyIdx); + track.mReadyTransitions.remove(readyIdx); if (playing.mMerged == null) { playing.mMerged = new ArrayList<>(); } @@ -712,7 +817,7 @@ public class Transitions implements RemoteCallable { mObservers.get(i).onTransitionMerged(merged.mToken, playing.mToken); } // See if we should merge another transition. - processReadyQueue(); + processReadyQueue(track); } private void playTransition(@NonNull ActiveTransition active) { @@ -781,6 +886,7 @@ public class Transitions implements RemoteCallable { /** Aborts a transition. This will still queue it up to maintain order. */ private void onAbort(ActiveTransition transition) { + final Track track = mTracks.get(transition.getTrack()); // apply immediately since they may be "parallel" operations: We currently we use abort for // thing which are independent to other transitions (like starting-window transfer). transition.mStartT.apply(); @@ -796,11 +902,11 @@ public class Transitions implements RemoteCallable { releaseSurfaces(transition.mInfo); // This still went into the queue (to maintain the correct finish ordering). - if (mReadyTransitions.size() > 1) { + if (track.mReadyTransitions.size() > 1) { // There are already transitions waiting in the queue, so just return. return; } - processReadyQueue(); + processReadyQueue(track); } /** @@ -815,17 +921,14 @@ public class Transitions implements RemoteCallable { private void onFinish(ActiveTransition active, @Nullable WindowContainerTransaction wct, @Nullable WindowContainerTransactionCallback wctCB) { - int activeIdx = mActiveTransitions.indexOf(active); - if (activeIdx < 0) { + final Track track = mTracks.get(active.getTrack()); + if (track.mActiveTransition != active) { Log.e(TAG, "Trying to finish a non-running transition. Either remote crashed or " + " a handler didn't properly deal with a merge. " + active, new RuntimeException()); return; - } else if (activeIdx != 0) { - // Relevant right now since we only allow 1 active transition at a time. - Log.e(TAG, "Finishing a transition out of order. " + active); } - mActiveTransitions.remove(activeIdx); + track.mActiveTransition = null; for (int i = 0; i < mObservers.size(); ++i) { mObservers.get(i).onTransitionFinished(active.mToken, active.mAborted); @@ -877,18 +980,20 @@ public class Transitions implements RemoteCallable { } // Now that this is done, check the ready queue for more work. - processReadyQueue(); + processReadyQueue(track); } private boolean isTransitionKnown(IBinder token) { for (int i = 0; i < mPendingTransitions.size(); ++i) { if (mPendingTransitions.get(i).mToken == token) return true; } - for (int i = 0; i < mReadyTransitions.size(); ++i) { - if (mReadyTransitions.get(i).mToken == token) return true; - } - for (int i = 0; i < mActiveTransitions.size(); ++i) { - final ActiveTransition active = mActiveTransitions.get(i); + for (int t = 0; t < mTracks.size(); ++t) { + final Track tr = mTracks.get(t); + for (int i = 0; i < tr.mReadyTransitions.size(); ++i) { + if (tr.mReadyTransitions.get(i).mToken == token) return true; + } + final ActiveTransition active = tr.mActiveTransition; + if (active == null) continue; if (active.mToken == token) return true; if (active.mMerged == null) continue; for (int m = 0; m < active.mMerged.size(); ++m) { @@ -963,7 +1068,7 @@ public class Transitions implements RemoteCallable { * * This works by "merging" the sleep transition into the currently-playing transition (even if * its out-of-order) -- turning SLEEP into a signal. If the playing transition doesn't finish - * within `SLEEP_ALLOWANCE_MS` from this merge attempt, this will then finish it directly (and + * within `SYNC_ALLOWANCE_MS` from this merge attempt, this will then finish it directly (and * send an abort/consumed message). * * This is then repeated until there are no more pending sleep transitions. @@ -971,48 +1076,53 @@ public class Transitions implements RemoteCallable { * @param forceFinish When non-null, this is the transition that we last sent the SLEEP merge * signal to -- so it will be force-finished if it's still running. */ - private void finishForSleep(@Nullable ActiveTransition forceFinish) { - if ((mActiveTransitions.isEmpty() && mReadyTransitions.isEmpty()) - || mSleepHandler.mSleepTransitions.isEmpty()) { - // Done finishing things. - // Prevent any weird leaks... shouldn't happen though. - mSleepHandler.mSleepTransitions.clear(); - return; - } - if (forceFinish != null && mActiveTransitions.contains(forceFinish)) { - Log.e(TAG, "Forcing transition to finish due to sleep timeout: " + forceFinish); - forceFinish.mAborted = true; - // Last notify of it being consumed. Note: mHandler should never be null, - // but check just to be safe. - if (forceFinish.mHandler != null) { - forceFinish.mHandler.onTransitionConsumed( - forceFinish.mToken, true /* aborted */, null /* finishTransaction */); + private void finishForSync(int trackIdx, @Nullable ActiveTransition forceFinish) { + final Track track = mTracks.get(trackIdx); + if (forceFinish != null) { + final Track trk = mTracks.get(forceFinish.getTrack()); + if (trk != track) { + Log.e(TAG, "finishForSleep: mismatched Tracks between forceFinish and logic " + + forceFinish.getTrack() + " vs " + trackIdx); } - onFinish(forceFinish, null, null); + if (trk.mActiveTransition == forceFinish) { + Log.e(TAG, "Forcing transition to finish due to sync timeout: " + forceFinish); + forceFinish.mAborted = true; + // Last notify of it being consumed. Note: mHandler should never be null, + // but check just to be safe. + if (forceFinish.mHandler != null) { + forceFinish.mHandler.onTransitionConsumed( + forceFinish.mToken, true /* aborted */, null /* finishTransaction */); + } + onFinish(forceFinish, null, null); + } + } + if (track.isIdle() || mReadyDuringSync.isEmpty()) { + // Done finishing things. + return; } final SurfaceControl.Transaction dummyT = new SurfaceControl.Transaction(); final TransitionInfo dummyInfo = new TransitionInfo(TRANSIT_SLEEP, 0 /* flags */); - while (!mActiveTransitions.isEmpty() && !mSleepHandler.mSleepTransitions.isEmpty()) { - final ActiveTransition playing = mActiveTransitions.get(0); - int sleepIdx = findByToken(mReadyTransitions, mSleepHandler.mSleepTransitions.get(0)); - if (sleepIdx >= 0) { - // Try to signal that we are sleeping by attempting to merge the sleep transition - // into the playing one. - final ActiveTransition nextSleep = mReadyTransitions.get(sleepIdx); - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Attempt to merge SLEEP %s" - + " into %s", nextSleep, playing); - playing.mHandler.mergeAnimation(nextSleep.mToken, dummyInfo, dummyT, - playing.mToken, (wct, cb) -> {}); - } else { - Log.e(TAG, "Couldn't find sleep transition in ready list: " - + mSleepHandler.mSleepTransitions.get(0)); + while (track.mActiveTransition != null && !mReadyDuringSync.isEmpty()) { + final ActiveTransition playing = track.mActiveTransition; + final ActiveTransition nextSync = mReadyDuringSync.get(0); + if (!nextSync.isSync()) { + Log.e(TAG, "Somehow blocked on a non-sync transition? " + nextSync); } + // Attempt to merge a SLEEP info to signal that the playing transition needs to + // fast-forward. + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Attempt to merge sync %s" + + " into %s via a SLEEP proxy", nextSync, playing); + playing.mHandler.mergeAnimation(nextSync.mToken, dummyInfo, dummyT, + playing.mToken, (wct, cb) -> {}); // it's possible to complete immediately. If that happens, just repeat the signal // loop until we either finish everything or start playing an animation that isn't // finishing immediately. - if (!mActiveTransitions.isEmpty() && mActiveTransitions.get(0) == playing) { - // Give it a (very) short amount of time to process it before forcing. - mMainExecutor.executeDelayed(() -> finishForSleep(playing), SLEEP_ALLOWANCE_MS); + if (track.mActiveTransition == playing) { + if (!mDisableForceSync) { + // Give it a short amount of time to process it before forcing. + mMainExecutor.executeDelayed(() -> finishForSync(trackIdx, playing), + SYNC_ALLOWANCE_MS); + } break; } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java index da95c77d2b89d..499870220190e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java @@ -48,9 +48,8 @@ public class TestShellExecutor implements ShellExecutor { } public void flushAll() { - for (Runnable r : mRunnables) { - r.run(); + while (!mRunnables.isEmpty()) { + mRunnables.remove(0).run(); } - mRunnables.clear(); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index 5cd548bfe5ab2..8eb5c6a08d881 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -30,10 +30,12 @@ import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_SLEEP; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; +import static android.window.TransitionInfo.FLAG_SYNC; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -63,6 +65,7 @@ import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.util.ArraySet; +import android.util.Pair; import android.view.Surface; import android.view.SurfaceControl; import android.view.WindowManager; @@ -587,7 +590,8 @@ public class ShellTransitionTests extends ShellTestCase { requestStartTransition(transitions, transitTokenNotReady); mDefaultHandler.setSimulateMerge(true); - mDefaultHandler.mFinishes.get(0).onTransitionFinished(null /* wct */, null /* wctCB */); + mDefaultHandler.mFinishes.get(0).second.onTransitionFinished( + null /* wct */, null /* wctCB */); // Make sure that the non-ready transition is not merged. assertEquals(0, mDefaultHandler.mergeCount()); @@ -1059,6 +1063,223 @@ public class ShellTransitionTests extends ShellTestCase { assertEquals(1, mDefaultHandler.activeCount()); } + @Test + public void testMultipleTracks() { + Transitions transitions = createTestTransitions(); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + TestTransitionHandler alwaysMergeHandler = new TestTransitionHandler(); + alwaysMergeHandler.setSimulateMerge(true); + + final boolean[] becameIdle = new boolean[]{false}; + + final WindowContainerTransaction emptyWCT = new WindowContainerTransaction(); + final SurfaceControl.Transaction mockSCT = mock(SurfaceControl.Transaction.class); + + // Make this always merge so we can ensure that it does NOT get a merge-attempt for a + // different track. + IBinder transitA = transitions.startTransition(TRANSIT_OPEN, emptyWCT, alwaysMergeHandler); + // start tracking idle + transitions.runOnIdle(() -> becameIdle[0] = true); + + IBinder transitB = transitions.startTransition(TRANSIT_OPEN, emptyWCT, mDefaultHandler); + IBinder transitC = transitions.startTransition(TRANSIT_CLOSE, emptyWCT, mDefaultHandler); + + TransitionInfo infoA = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).build(); + infoA.setTrack(0); + TransitionInfo infoB = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).build(); + infoB.setTrack(1); + TransitionInfo infoC = new TransitionInfoBuilder(TRANSIT_CLOSE) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + infoC.setTrack(1); + + transitions.onTransitionReady(transitA, infoA, mockSCT, mockSCT); + assertEquals(1, alwaysMergeHandler.activeCount()); + transitions.onTransitionReady(transitB, infoB, mockSCT, mockSCT); + // should now be running in parallel + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(1, alwaysMergeHandler.activeCount()); + // make sure we didn't try to merge into a different track. + assertEquals(0, alwaysMergeHandler.mergeCount()); + + // This should be queued-up since it is on track 1 (same as B) + transitions.onTransitionReady(transitC, infoC, mockSCT, mockSCT); + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(1, alwaysMergeHandler.activeCount()); + + // Now finish B and make sure C starts + mDefaultHandler.finishOne(); + mMainExecutor.flushAll(); + + // Now C and A running in parallel + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(1, alwaysMergeHandler.activeCount()); + assertEquals(0, alwaysMergeHandler.mergeCount()); + + // Finish A + alwaysMergeHandler.finishOne(); + mMainExecutor.flushAll(); + + // C still running + assertEquals(0, alwaysMergeHandler.activeCount()); + assertEquals(1, mDefaultHandler.activeCount()); + assertFalse(becameIdle[0]); + + mDefaultHandler.finishOne(); + mMainExecutor.flushAll(); + + assertEquals(0, mDefaultHandler.activeCount()); + assertTrue(becameIdle[0]); + } + + @Test + public void testSyncMultipleTracks() { + Transitions transitions = createTestTransitions(); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + TestTransitionHandler secondHandler = new TestTransitionHandler(); + + // Disable the forced early-sync-finish so that we can test the ordering mechanics. + transitions.setDisableForceSyncForTest(true); + mDefaultHandler.mFinishOnSync = false; + secondHandler.mFinishOnSync = false; + + final WindowContainerTransaction emptyWCT = new WindowContainerTransaction(); + final SurfaceControl.Transaction mockSCT = mock(SurfaceControl.Transaction.class); + + // Make this always merge so we can ensure that it does NOT get a merge-attempt for a + // different track. + IBinder transitA = transitions.startTransition(TRANSIT_OPEN, emptyWCT, mDefaultHandler); + IBinder transitB = transitions.startTransition(TRANSIT_OPEN, emptyWCT, secondHandler); + IBinder transitC = transitions.startTransition(TRANSIT_CLOSE, emptyWCT, secondHandler); + IBinder transitSync = transitions.startTransition(TRANSIT_CLOSE, emptyWCT, mDefaultHandler); + IBinder transitD = transitions.startTransition(TRANSIT_OPEN, emptyWCT, secondHandler); + IBinder transitE = transitions.startTransition(TRANSIT_OPEN, emptyWCT, mDefaultHandler); + + TransitionInfo infoA = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).build(); + infoA.setTrack(0); + TransitionInfo infoB = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).build(); + infoB.setTrack(1); + TransitionInfo infoC = new TransitionInfoBuilder(TRANSIT_CLOSE) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + infoC.setTrack(1); + TransitionInfo infoSync = new TransitionInfoBuilder(TRANSIT_CLOSE) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + infoSync.setTrack(0); + infoSync.setFlags(FLAG_SYNC); + TransitionInfo infoD = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).build(); + infoD.setTrack(1); + TransitionInfo infoE = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).build(); + infoE.setTrack(0); + + // Start A B and C where A is track 0, B and C are track 1 (C should be queued) + transitions.onTransitionReady(transitA, infoA, mockSCT, mockSCT); + transitions.onTransitionReady(transitB, infoB, mockSCT, mockSCT); + transitions.onTransitionReady(transitC, infoC, mockSCT, mockSCT); + // should now be running in parallel (with one queued) + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(1, secondHandler.activeCount()); + + // Make the sync ready and the following (D, E) ready. + transitions.onTransitionReady(transitSync, infoSync, mockSCT, mockSCT); + transitions.onTransitionReady(transitD, infoD, mockSCT, mockSCT); + transitions.onTransitionReady(transitE, infoE, mockSCT, mockSCT); + + // nothing should have happened yet since the sync is queued and blocking everything. + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(1, secondHandler.activeCount()); + + // Finish A (which is track 0 like the sync). + mDefaultHandler.finishOne(); + mMainExecutor.flushAll(); + + // Even though the sync is on track 0 and track 0 became idle, it should NOT be started yet + // because it must wait for everything. Additionally, D/E shouldn't start yet either. + assertEquals(0, mDefaultHandler.activeCount()); + assertEquals(1, secondHandler.activeCount()); + + // Now finish B and C -- this should then allow the sync to start and D to run (in parallel) + secondHandler.finishOne(); + secondHandler.finishOne(); + mMainExecutor.flushAll(); + + // Now the sync and D (on track 1) should be running + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(1, secondHandler.activeCount()); + + // finish the sync. track 0 still has E + mDefaultHandler.finishOne(); + mMainExecutor.flushAll(); + assertEquals(1, mDefaultHandler.activeCount()); + + mDefaultHandler.finishOne(); + secondHandler.finishOne(); + mMainExecutor.flushAll(); + + assertEquals(0, mDefaultHandler.activeCount()); + assertEquals(0, secondHandler.activeCount()); + } + + @Test + public void testForceSyncTracks() { + Transitions transitions = createTestTransitions(); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + TestTransitionHandler secondHandler = new TestTransitionHandler(); + + final WindowContainerTransaction emptyWCT = new WindowContainerTransaction(); + final SurfaceControl.Transaction mockSCT = mock(SurfaceControl.Transaction.class); + + // Make this always merge so we can ensure that it does NOT get a merge-attempt for a + // different track. + IBinder transitA = transitions.startTransition(TRANSIT_OPEN, emptyWCT, mDefaultHandler); + IBinder transitB = transitions.startTransition(TRANSIT_OPEN, emptyWCT, mDefaultHandler); + IBinder transitC = transitions.startTransition(TRANSIT_CLOSE, emptyWCT, secondHandler); + IBinder transitD = transitions.startTransition(TRANSIT_OPEN, emptyWCT, secondHandler); + IBinder transitSync = transitions.startTransition(TRANSIT_CLOSE, emptyWCT, mDefaultHandler); + + TransitionInfo infoA = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).build(); + infoA.setTrack(0); + TransitionInfo infoB = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).build(); + infoB.setTrack(0); + TransitionInfo infoC = new TransitionInfoBuilder(TRANSIT_CLOSE) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + infoC.setTrack(1); + TransitionInfo infoD = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).build(); + infoD.setTrack(1); + TransitionInfo infoSync = new TransitionInfoBuilder(TRANSIT_CLOSE) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + infoSync.setTrack(0); + infoSync.setFlags(FLAG_SYNC); + + transitions.onTransitionReady(transitA, infoA, mockSCT, mockSCT); + transitions.onTransitionReady(transitB, infoB, mockSCT, mockSCT); + transitions.onTransitionReady(transitC, infoC, mockSCT, mockSCT); + transitions.onTransitionReady(transitD, infoD, mockSCT, mockSCT); + // should now be running in parallel (with one queued in each) + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(1, secondHandler.activeCount()); + + // Make the sync ready. + transitions.onTransitionReady(transitSync, infoSync, mockSCT, mockSCT); + mMainExecutor.flushAll(); + + // Everything should be forced-finish now except the sync + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(0, secondHandler.activeCount()); + + mDefaultHandler.finishOne(); + mMainExecutor.flushAll(); + + assertEquals(0, mDefaultHandler.activeCount()); + } + class ChangeBuilder { final TransitionInfo.Change mChange; @@ -1097,9 +1318,11 @@ public class ShellTransitionTests extends ShellTestCase { } class TestTransitionHandler implements Transitions.TransitionHandler { - ArrayList mFinishes = new ArrayList<>(); + ArrayList> mFinishes = + new ArrayList<>(); final ArrayList mMerged = new ArrayList<>(); boolean mSimulateMerge = false; + boolean mFinishOnSync = true; final ArraySet mShouldMerge = new ArraySet<>(); @Override @@ -1107,7 +1330,7 @@ public class ShellTransitionTests extends ShellTestCase { @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { - mFinishes.add(finishCallback); + mFinishes.add(new Pair<>(transition, finishCallback)); return true; } @@ -1115,6 +1338,13 @@ public class ShellTransitionTests extends ShellTestCase { public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (mFinishOnSync && info.getType() == TRANSIT_SLEEP) { + for (int i = 0; i < mFinishes.size(); ++i) { + if (mFinishes.get(i).first != mergeTarget) continue; + mFinishes.remove(i).second.onTransitionFinished(null, null); + return; + } + } if (!(mSimulateMerge || mShouldMerge.contains(transition))) return; mMerged.add(transition); finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); @@ -1136,18 +1366,19 @@ public class ShellTransitionTests extends ShellTestCase { } void finishAll() { - final ArrayList finishes = mFinishes; + final ArrayList> finishes = + mFinishes; mFinishes = new ArrayList<>(); for (int i = finishes.size() - 1; i >= 0; --i) { - finishes.get(i).onTransitionFinished(null /* wct */, null /* wctCB */); + finishes.get(i).second.onTransitionFinished(null /* wct */, null /* wctCB */); } mShouldMerge.clear(); } void finishOne() { - Transitions.TransitionFinishCallback fin = mFinishes.remove(0); + Pair fin = mFinishes.remove(0); mMerged.clear(); - fin.onTransitionFinished(null /* wct */, null /* wctCB */); + fin.second.onTransitionFinished(null /* wct */, null /* wctCB */); } int activeCount() {