From 6831703c4aa115b1f772a2156bef1cea0ed0a6fa Mon Sep 17 00:00:00 2001 From: Evan Rosky Date: Thu, 2 Mar 2023 01:01:24 -0800 Subject: [PATCH] Use a SLEEP transition to trigger failsafe animation cancelling Some remote animations can misbehave. In particular, recents. When this happens in the form of the remote not reporting finish, the transition system can get stuck waiting. Legacy recents had the same problem and solved it by cancelling the recents and independently finishing/cleaning-up after a "failsafe" duration from turning the screen off. This CL sets-up something similar for shell-transitions. It uses SLEEP as a signal to "quickly end all animations". This signal gets sent (via merge) to all the animations. Each animation gets a small amount of time to finish after-which we forcibly finish it. Bug: 267738124 Test: Observe sleep transitions and cancelling in logs. Change-Id: I623fb5a47a5593e23fbc8f2298eff1c44ae3a0fc --- core/java/android/view/WindowManager.java | 9 +- data/etc/services.core.protolog.json | 12 +- .../shell/transition/DefaultMixedHandler.java | 12 +- .../wm/shell/transition/SleepHandler.java | 65 ++++++++++ .../wm/shell/transition/Transitions.java | 118 +++++++++++++++--- .../shared/system/RemoteTransitionCompat.java | 8 ++ .../server/wm/RootWindowContainer.java | 26 ++++ .../com/android/server/wm/Transition.java | 22 ++++ .../server/wm/WindowOrganizerController.java | 2 +- 9 files changed, 246 insertions(+), 28 deletions(-) create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/transition/SleepHandler.java diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index f8636788ee3cc..dfb11bcee5ed5 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -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) { diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 05e17720b1752..913eaf2391f6f 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -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", 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 ef405c858e3d3..82674183082c6 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 @@ -447,7 +447,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 @@ -461,8 +463,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); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/SleepHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/SleepHandler.java new file mode 100644 index 0000000000000..0386ec38a3ff4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/SleepHandler.java @@ -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 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); + } +} 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 3b154d1cda83e..8e7b9df0c9ad4 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 @@ -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 { 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 { 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 { + 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 { return; } - final ActiveTransition active = mActiveTransitions.get(activeIdx); active.mInfo = info; active.mStartT = t; active.mFinishT = finishT; @@ -797,23 +824,30 @@ public class Transitions implements RemoteCallable { } 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); @@ -839,6 +873,56 @@ public class Transitions implements RemoteCallable { 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. diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java index fca55b1c69b41..6f7d66d03cab2 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java @@ -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 openingTasks = null; ArrayList closingTasks = null; mAppearedTargets = null; diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index b3b56f273f3d7..e147219de4c6a 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -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 } 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 } 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; } diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index a30ab11d9f6db..879323e16ed39 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -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. @@ -1007,6 +1010,25 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mController.dispatchLegacyAppTransitionCancelled(); } + /** Immediately moves this to playing even if it isn't started yet. */ + void playNow() { + if (mState == STATE_PLAYING) 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; } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index b9cb59a17a2ef..495d7ce4e90bd 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -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");