Merge "Use a SLEEP transition to trigger failsafe animation cancelling" into udc-dev

This commit is contained in:
Evan Rosky
2023-03-07 02:13:47 +00:00
committed by Android (Google) Code Review
10 changed files with 271 additions and 28 deletions

View File

@@ -453,6 +453,11 @@ public interface WindowManager extends ViewManager {
* @hide
*/
int TRANSIT_WAKE = 11;
/**
* The screen is turning off. This is used as a message to stop all animations.
* @hide
*/
int TRANSIT_SLEEP = 12;
/**
* The first slot for custom transition types. Callers (like Shell) can make use of custom
* transition types for dealing with special cases. These types are effectively ignored by
@@ -462,7 +467,7 @@ public interface WindowManager extends ViewManager {
* implementation.
* @hide
*/
int TRANSIT_FIRST_CUSTOM = 12;
int TRANSIT_FIRST_CUSTOM = 13;
/**
* @hide
@@ -480,6 +485,7 @@ public interface WindowManager extends ViewManager {
TRANSIT_KEYGUARD_UNOCCLUDE,
TRANSIT_PIP,
TRANSIT_WAKE,
TRANSIT_SLEEP,
TRANSIT_FIRST_CUSTOM
})
@Retention(RetentionPolicy.SOURCE)
@@ -1437,6 +1443,7 @@ public interface WindowManager extends ViewManager {
case TRANSIT_KEYGUARD_UNOCCLUDE: return "KEYGUARD_UNOCCLUDE";
case TRANSIT_PIP: return "PIP";
case TRANSIT_WAKE: return "WAKE";
case TRANSIT_SLEEP: return "SLEEP";
case TRANSIT_FIRST_CUSTOM: return "FIRST_CUSTOM";
default:
if (type > TRANSIT_FIRST_CUSTOM) {

View File

@@ -1297,6 +1297,12 @@
"group": "WM_ERROR",
"at": "com\/android\/server\/wm\/WindowManagerService.java"
},
"-894942237": {
"message": "Force Playing Transition: %d",
"level": "VERBOSE",
"group": "WM_DEBUG_WINDOW_TRANSITIONS",
"at": "com\/android\/server\/wm\/Transition.java"
},
"-883738232": {
"message": "Adding more than one toast window for UID at a time.",
"level": "WARN",
@@ -4243,12 +4249,6 @@
"group": "WM_DEBUG_ORIENTATION",
"at": "com\/android\/server\/wm\/DisplayContent.java"
},
"1878927091": {
"message": "prepareSurface: No changes in animation for %s",
"level": "VERBOSE",
"group": "WM_DEBUG_ANIM",
"at": "com\/android\/server\/wm\/WindowStateAnimator.java"
},
"1891501279": {
"message": "cancelAnimation(): reason=%s",
"level": "DEBUG",

View File

@@ -450,7 +450,9 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
// Already done, so no need to end it.
return;
}
if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) {
// queue since no actual animation.
} else if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
if (mixed.mAnimType == MixedTransition.ANIM_TYPE_GOING_HOME) {
boolean ended = mSplitHandler.end();
// If split couldn't end (because it is remote), then don't end everything else
@@ -464,8 +466,12 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
} else {
mPipHandler.end();
}
} else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) {
// queue
} else if (mixed.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) {
mPipHandler.end();
if (mixed.mLeftoversHandler != null) {
mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
finishCallback);
}
} else {
throw new IllegalStateException("Playing a mixed transition with unknown type? "
+ mixed.mType);

View File

@@ -0,0 +1,65 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.wm.shell.transition;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.IBinder;
import android.util.Log;
import android.view.SurfaceControl;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
import android.window.WindowContainerTransaction;
import java.util.ArrayList;
/**
* A Simple handler that tracks SLEEP transitions. We track them specially since we (ab)use these
* as sentinels for fast-forwarding through animations when the screen is off.
*
* There should only be one SleepHandler and it is used explicitly by {@link Transitions} so we
* don't register it like a normal handler.
*/
class SleepHandler implements Transitions.TransitionHandler {
final ArrayList<IBinder> mSleepTransitions = new ArrayList<>();
@Override
public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
startTransaction.apply();
finishCallback.onTransitionFinished(null, null);
mSleepTransitions.remove(transition);
return true;
}
@Override
@Nullable
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
@NonNull TransitionRequestInfo request) {
mSleepTransitions.add(transition);
return new WindowContainerTransaction();
}
@Override
public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
@Nullable SurfaceControl.Transaction finishTransaction) {
Log.e(Transitions.TAG, "Sleep transition was consumed. This doesn't make sense");
mSleepTransitions.remove(transition);
}
}

View File

@@ -21,6 +21,7 @@ import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM;
import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
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.view.WindowManager.fixScale;
@@ -124,6 +125,7 @@ public class Transitions implements RemoteCallable<Transitions> {
private final DisplayController mDisplayController;
private final ShellController mShellController;
private final ShellTransitionImpl mImpl = new ShellTransitionImpl();
private final SleepHandler mSleepHandler = new SleepHandler();
private boolean mIsRegistered = false;
@@ -137,6 +139,14 @@ 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
* 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 class ActiveTransition {
IBinder mToken;
TransitionHandler mHandler;
@@ -478,11 +488,29 @@ public class Transitions implements RemoteCallable<Transitions> {
+ Arrays.toString(mActiveTransitions.stream().map(
activeTransition -> activeTransition.mToken).toArray()));
}
final ActiveTransition active = mActiveTransitions.get(activeIdx);
for (int i = 0; i < mObservers.size(); ++i) {
mObservers.get(i).onTransitionReady(transitionToken, info, t, finishT);
}
if (info.getType() == TRANSIT_SLEEP) {
if (activeIdx > 0) {
active.mInfo = info;
active.mStartT = t;
active.mFinishT = finishT;
if (!info.getRootLeash().isValid()) {
// Shell has some debug settings which makes calling binders with invalid
// surfaces crash, so replace it with a "real" one.
info.setRootLeash(new SurfaceControl.Builder().setName("Invalid")
.setContainerLayer().build(), 0, 0);
}
// Sleep starts a process of forcing all prior transitions to finish immediately
finishForSleep(null /* forceFinish */);
return;
}
}
// Allow to notify keyguard un-occluding state to KeyguardService, which can happen while
// screen-off, so there might no visibility change involved.
if (!info.getRootLeash().isValid() && info.getType() != TRANSIT_KEYGUARD_UNOCCLUDE) {
@@ -527,7 +555,6 @@ public class Transitions implements RemoteCallable<Transitions> {
return;
}
final ActiveTransition active = mActiveTransitions.get(activeIdx);
active.mInfo = info;
active.mStartT = t;
active.mFinishT = finishT;
@@ -803,23 +830,30 @@ public class Transitions implements RemoteCallable<Transitions> {
}
final ActiveTransition active = new ActiveTransition();
WindowContainerTransaction wct = null;
for (int i = mHandlers.size() - 1; i >= 0; --i) {
wct = mHandlers.get(i).handleRequest(transitionToken, request);
if (wct != null) {
active.mHandler = mHandlers.get(i);
break;
}
}
if (request.getDisplayChange() != null) {
TransitionRequestInfo.DisplayChange change = request.getDisplayChange();
if (change.getEndRotation() != change.getStartRotation()) {
// Is a rotation, so dispatch to all displayChange listeners
if (wct == null) {
wct = new WindowContainerTransaction();
// If we have sleep, we use a special handler and we try to finish everything ASAP.
if (request.getType() == TRANSIT_SLEEP) {
mSleepHandler.handleRequest(transitionToken, request);
active.mHandler = mSleepHandler;
} else {
for (int i = mHandlers.size() - 1; i >= 0; --i) {
wct = mHandlers.get(i).handleRequest(transitionToken, request);
if (wct != null) {
active.mHandler = mHandlers.get(i);
break;
}
}
if (request.getDisplayChange() != null) {
TransitionRequestInfo.DisplayChange change = request.getDisplayChange();
if (change.getEndRotation() != change.getStartRotation()) {
// Is a rotation, so dispatch to all displayChange listeners
if (wct == null) {
wct = new WindowContainerTransaction();
}
mDisplayController.getChangeController().dispatchOnDisplayChange(wct,
change.getDisplayId(), change.getStartRotation(),
change.getEndRotation(), null /* newDisplayAreaInfo */);
}
mDisplayController.getChangeController().dispatchOnDisplayChange(wct,
change.getDisplayId(), change.getStartRotation(), change.getEndRotation(),
null /* newDisplayAreaInfo */);
}
}
mOrganizer.startTransition(transitionToken, wct != null && wct.isEmpty() ? null : wct);
@@ -845,6 +879,56 @@ public class Transitions implements RemoteCallable<Transitions> {
return active.mToken;
}
/**
* Finish running animations (almost) immediately when a SLEEP transition comes in. We use this
* as both a way to reduce unnecessary work (animations not visible while screen off) and as a
* failsafe to unblock "stuck" animations (in particular remote animations).
*
* 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
* send an abort/consumed message).
*
* This is then repeated until there are no more pending sleep 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 IBinder forceFinish) {
if (mActiveTransitions.isEmpty() || mSleepHandler.mSleepTransitions.isEmpty()) {
return;
}
if (forceFinish != null && mActiveTransitions.get(0).mToken == forceFinish) {
Log.e(TAG, "Forcing transition to finish due to sleep timeout: "
+ mActiveTransitions.get(0).mToken);
onFinish(mActiveTransitions.get(0).mToken, null, null, true);
}
final SurfaceControl.Transaction dummyT = new SurfaceControl.Transaction();
while (!mActiveTransitions.isEmpty() && !mSleepHandler.mSleepTransitions.isEmpty()) {
final ActiveTransition playing = mActiveTransitions.get(0);
int sleepIdx = findActiveTransition(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 = mActiveTransitions.get(sleepIdx);
playing.mHandler.mergeAnimation(nextSleep.mToken, nextSleep.mInfo, dummyT,
playing.mToken, (wct, cb) -> {});
} else {
Log.e(TAG, "Couldn't find sleep transition in active list: "
+ mSleepHandler.mSleepTransitions.get(0));
}
// 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.mToken), SLEEP_ALLOWANCE_MS);
break;
}
}
}
/**
* Interface for a callback that must be called after a TransitionHandler finishes playing an
* animation.

View File

@@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED;
import static android.view.WindowManager.TRANSIT_SLEEP;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
@@ -46,6 +47,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.wm.shell.util.TransitionUtil;
import java.util.ArrayList;
import java.util.HashMap;
/**
* Helper class to build {@link RemoteTransition} objects
@@ -205,6 +207,12 @@ public class RemoteTransitionCompat {
@SuppressLint("NewApi")
boolean merge(TransitionInfo info, SurfaceControl.Transaction t) {
if (info.getType() == TRANSIT_SLEEP) {
// A sleep event means we need to stop animations immediately, so cancel here.
mListener.onAnimationCanceled(new HashMap<>());
finish(mWillFinishToHome, false /* userLeaveHint */);
return false;
}
ArrayList<TransitionInfo.Change> openingTasks = null;
ArrayList<TransitionInfo.Change> closingTasks = null;
mAppearedTargets = null;

View File

@@ -33,6 +33,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
import static android.view.WindowManager.TRANSIT_NONE;
import static android.view.WindowManager.TRANSIT_PIP;
import static android.view.WindowManager.TRANSIT_SLEEP;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_WAKE;
@@ -2329,6 +2330,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent>
}
void applySleepTokens(boolean applyToRootTasks) {
boolean builtSleepTransition = false;
for (int displayNdx = getChildCount() - 1; displayNdx >= 0; --displayNdx) {
// Set the sleeping state of the display.
final DisplayContent display = getChildAt(displayNdx);
@@ -2338,6 +2340,30 @@ class RootWindowContainer extends WindowContainer<DisplayContent>
}
display.setIsSleeping(displayShouldSleep);
if (display.mTransitionController.isShellTransitionsEnabled() && !builtSleepTransition
// Only care if there are actual sleep tokens.
&& displayShouldSleep && !display.mAllSleepTokens.isEmpty()) {
builtSleepTransition = true;
// We don't actually care about collecting anything here. We really just want
// this as a signal to the transition-player.
final Transition transition = new Transition(TRANSIT_SLEEP, 0 /* flags */,
display.mTransitionController, mWmService.mSyncEngine);
final Runnable sendSleepTransition = () -> {
display.mTransitionController.requestStartTransition(transition,
null /* trigger */, null /* remote */, null /* display */);
// Force playing immediately so that unrelated ops can't be collected.
transition.playNow();
};
if (display.mTransitionController.isCollecting()) {
mWmService.mSyncEngine.queueSyncSet(
() -> display.mTransitionController.moveToCollecting(transition),
sendSleepTransition);
} else {
display.mTransitionController.moveToCollecting(transition);
sendSleepTransition.run();
}
}
if (!applyToRootTasks) {
continue;
}

View File

@@ -218,6 +218,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
final TransitionController.Logger mLogger = new TransitionController.Logger();
/** Whether this transition was forced to play early (eg for a SLEEP signal). */
private boolean mForcePlaying = false;
/**
* {@code false} if this transition runs purely in WMCore (meaning Shell is completely unaware
* of it). Currently, this happens before the display is ready since nothing can be seen yet.
@@ -389,6 +392,10 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
return mState == STATE_COLLECTING || mState == STATE_STARTED;
}
boolean isAborted() {
return mState == STATE_ABORT;
}
boolean isStarted() {
return mState == STATE_STARTED;
}
@@ -997,6 +1004,11 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
void abort() {
// This calls back into itself via controller.abort, so just early return here.
if (mState == STATE_ABORT) return;
if (mState == STATE_PENDING) {
// hasn't started collecting, so can jump directly to aborted state.
mState = STATE_ABORT;
return;
}
if (mState != STATE_COLLECTING && mState != STATE_STARTED) {
throw new IllegalStateException("Too late to abort. state=" + mState);
}
@@ -1007,6 +1019,27 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
mController.dispatchLegacyAppTransitionCancelled();
}
/** Immediately moves this to playing even if it isn't started yet. */
void playNow() {
if (!(mState == STATE_COLLECTING || mState == STATE_STARTED)) {
return;
}
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Force Playing Transition: %d",
mSyncId);
mForcePlaying = true;
setAllReady();
if (mState == STATE_COLLECTING) {
start();
}
// Don't wait for actual surface-placement. We don't want anything else collected in this
// transition.
mSyncEngine.onSurfacePlacement();
}
boolean isForcePlaying() {
return mForcePlaying;
}
void setRemoteTransition(RemoteTransition remoteTransition) {
mRemoteTransition = remoteTransition;
}

View File

@@ -209,6 +209,12 @@ class TransitionController {
if (mCollectingTransition != null) {
throw new IllegalStateException("Simultaneous transition collection not supported.");
}
if (mTransitionPlayer == null) {
// If sysui has been killed (by a test) or crashed, we can temporarily have no player
// In this case, abort the transition.
transition.abort();
return;
}
mCollectingTransition = transition;
// Distinguish change type because the response time is usually expected to be not too long.
final long timeoutMs =
@@ -511,6 +517,14 @@ class TransitionController {
transition.getToken(), null));
return transition;
}
if (mTransitionPlayer == null || transition.isAborted()) {
// Apparently, some tests will kill(and restart) systemui, so there is a chance that
// the player might be transiently null.
if (transition.isCollecting()) {
transition.abort();
}
return transition;
}
try {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
"Requesting StartTransition: %s", transition);

View File

@@ -317,7 +317,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
}
transition = mTransitionController.createTransition(type);
}
if (!transition.isCollecting()) {
if (!transition.isCollecting() && !transition.isForcePlaying()) {
Slog.e(TAG, "Trying to start a transition that isn't collecting. This probably"
+ " means Shell took too long to respond to a request. WM State may be"
+ " incorrect now, please file a bug");