Merge "Use a SLEEP transition to trigger failsafe animation cancelling" into udc-dev
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index f863678..dfb11bc 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -454,6 +454,11 @@
      */
     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
      * Core and will just be passed along as part of TransitionInfo objects. An example is
@@ -462,7 +467,7 @@
      * implementation.
      * @hide
      */
-    int TRANSIT_FIRST_CUSTOM = 12;
+    int TRANSIT_FIRST_CUSTOM = 13;
 
     /**
      * @hide
@@ -480,6 +485,7 @@
             TRANSIT_KEYGUARD_UNOCCLUDE,
             TRANSIT_PIP,
             TRANSIT_WAKE,
+            TRANSIT_SLEEP,
             TRANSIT_FIRST_CUSTOM
     })
     @Retention(RetentionPolicy.SOURCE)
@@ -1437,6 +1443,7 @@
             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 05e1772..913eaf2 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 d1565d1..75112b6 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
@@ -450,7 +450,9 @@
                 // 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 @@
                 } 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 0000000..0386ec3
--- /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<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);
+    }
+}
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 b39b953..155990a 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_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 @@
     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 @@
 
     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 @@
                     + 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 @@
             return;
         }
 
-        final ActiveTransition active = mActiveTransitions.get(activeIdx);
         active.mInfo = info;
         active.mStartT = t;
         active.mFinishT = finishT;
@@ -803,23 +830,30 @@
         }
         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;
                 }
-                mDisplayController.getChangeController().dispatchOnDisplayChange(wct,
-                        change.getDisplayId(), change.getStartRotation(), change.getEndRotation(),
-                        null /* newDisplayAreaInfo */);
+            }
+            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 */);
+                }
             }
         }
         mOrganizer.startTransition(transitionToken, wct != null && wct.isEmpty() ? null : wct);
@@ -846,6 +880,56 @@
     }
 
     /**
+     * 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 fca55b1..6f7d66d 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_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.wm.shell.util.TransitionUtil;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 
 /**
  * Helper class to build {@link RemoteTransition} objects
@@ -205,6 +207,12 @@
 
         @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;
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index b3b56f2..e147219 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_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 @@
     }
 
     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 @@
             }
             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 a30ab11..bf6983b 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 @@
 
     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 @@
         return mState == STATE_COLLECTING || mState == STATE_STARTED;
     }
 
+    boolean isAborted() {
+        return mState == STATE_ABORT;
+    }
+
     boolean isStarted() {
         return mState == STATE_STARTED;
     }
@@ -997,6 +1004,11 @@
     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 @@
         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;
     }
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 79eb634..6c951bf 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -209,6 +209,12 @@
         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 @@
                     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);
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index b9cb59a..495d7ce4 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 @@
                     }
                     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");