Merge "Add support for multiple animation tracks" into udc-dev

This commit is contained in:
Evan Rosky
2023-04-19 21:14:17 +00:00
committed by Android (Google) Code Review
5 changed files with 490 additions and 120 deletions

View File

@@ -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<Change> mChanges = new ArrayList<>();
private final ArrayList<Root> 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());

View File

@@ -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) {

View File

@@ -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<Transitions> {
static final String TAG = "ShellTransitions";
@@ -172,12 +179,15 @@ public class Transitions implements RemoteCallable<Transitions> {
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<Transitions> {
/** Ordered list of transitions which have been merged into this one. */
private ArrayList<ActiveTransition> 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<ActiveTransition> 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<ActiveTransition> mPendingTransitions = new ArrayList<>();
/** Keeps track of transitions which are ready to play but still waiting for their turn. */
private final ArrayList<ActiveTransition> 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<ActiveTransition> mReadyDuringSync = new ArrayList<>();
/** Keeps track of currently playing transitions. For now, there can only be 1 max. */
private final ArrayList<ActiveTransition> mActiveTransitions = new ArrayList<>();
private final ArrayList<Track> mTracks = new ArrayList<>();
public Transitions(@NonNull Context context,
@NonNull ShellInit shellInit,
@@ -374,14 +406,17 @@ public class Transitions implements RemoteCallable<Transitions> {
* 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<Transitions> {
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<Transitions> {
+ 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<Transitions> {
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<Transitions> {
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<Transitions> {
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<Transitions> {
}
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<Transitions> {
}
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<Transitions> {
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<Transitions> {
/** 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<Transitions> {
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<Transitions> {
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<Transitions> {
}
// 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<Transitions> {
*
* 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<Transitions> {
* @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;
}
}

View File

@@ -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();
}
}

View File

@@ -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<Transitions.TransitionFinishCallback> mFinishes = new ArrayList<>();
ArrayList<Pair<IBinder, Transitions.TransitionFinishCallback>> mFinishes =
new ArrayList<>();
final ArrayList<IBinder> mMerged = new ArrayList<>();
boolean mSimulateMerge = false;
boolean mFinishOnSync = true;
final ArraySet<IBinder> 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<Transitions.TransitionFinishCallback> finishes = mFinishes;
final ArrayList<Pair<IBinder, Transitions.TransitionFinishCallback>> 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<IBinder, Transitions.TransitionFinishCallback> fin = mFinishes.remove(0);
mMerged.clear();
fin.onTransitionFinished(null /* wct */, null /* wctCB */);
fin.second.onTransitionFinished(null /* wct */, null /* wctCB */);
}
int activeCount() {