Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6831703c authored by Evan Rosky's avatar Evan Rosky
Browse files

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
parent b5528bd2
Loading
Loading
Loading
Loading
+8 −1
Original line number Diff line number Diff line
@@ -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) {
+6 −6
Original line number Diff line number Diff line
@@ -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",
+9 −3
Original line number Diff line number Diff line
@@ -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);
+65 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+100 −16
Original line number Diff line number Diff line
@@ -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;
@@ -797,6 +824,12 @@ public class Transitions implements RemoteCallable<Transitions> {
        }
        final ActiveTransition active = new ActiveTransition();
        WindowContainerTransaction wct = null;

        // 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) {
@@ -812,8 +845,9 @@ public class Transitions implements RemoteCallable<Transitions> {
                        wct = new WindowContainerTransaction();
                    }
                    mDisplayController.getChangeController().dispatchOnDisplayChange(wct,
                        change.getDisplayId(), change.getStartRotation(), change.getEndRotation(),
                        null /* newDisplayAreaInfo */);
                            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<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.
Loading