summaryrefslogtreecommitdiff
path: root/services/core/java/com/android/server/wm/AsyncRotationController.java
blob: 01158779c24f94576417c307f2d07d9dd7aa7e80 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
/*
 * Copyright (C) 2020 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.server.wm;

import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;

import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_TOKEN_TRANSFORM;

import android.annotation.IntDef;
import android.os.HandlerExecutor;
import android.util.ArrayMap;
import android.util.Slog;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;

import com.android.internal.R;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.function.Consumer;

/**
 * Controller to handle the appearance of non-activity windows which can update asynchronously when
 * the display rotation is changing. This is an optimization to reduce the latency to start screen
 * rotation or app transition animation.
 * <pre>The appearance:
 * - Open app with rotation change: the target windows are faded out with open transition, and then
 *   faded in after the transition when the windows are drawn with new rotation.
 * - Normal rotation: the target windows are hidden by a parent leash with zero alpha after the
 *   screenshot layer is shown, and will be faded in when they are drawn with new rotation.
 * - Seamless rotation: Only shell transition uses this controller in this case. The target windows
 *   will be requested to use sync transaction individually. Their window token will rotate to old
 *   rotation. After the start transaction of transition is applied and the window is drawn in new
 *   rotation, the old rotation transformation will be removed with applying the sync transaction.
 * </pre>
 * For the windows which are forced to be seamless (e.g. screen decor overlay), the case is the
 * same as above mentioned seamless rotation (only shell). Just the appearance may be mixed, e.g.
 * 2 windows FADE and 2 windows SEAMLESS in normal rotation or app transition. And 4 (all) windows
 * SEAMLESS in seamless rotation.
 */
class AsyncRotationController extends FadeAnimationController implements Consumer<WindowState> {
    private static final String TAG = "AsyncRotation";
    private static final boolean DEBUG = false;

    private final WindowManagerService mService;
    /** The map of async windows to the operations of rotation appearance. */
    private final ArrayMap<WindowToken, Operation> mTargetWindowTokens = new ArrayMap<>();
    /** If non-null, it usually indicates that there will be a screen rotation animation. */
    private Runnable mTimeoutRunnable;
    /** Non-null to indicate that the navigation bar is always handled by legacy seamless. */
    private WindowToken mNavBarToken;

    /** A runnable which gets called when the {@link #completeAll()} is called. */
    private Runnable mOnShowRunnable;

    /** Whether to use constant zero alpha animation. */
    private boolean mHideImmediately;

    /** The case of legacy transition. */
    private static final int OP_LEGACY = 0;
    /** It is usually OPEN/CLOSE/TO_FRONT/TO_BACK. */
    private static final int OP_APP_SWITCH = 1;
    /** The normal display change transition which should have a screen rotation animation. */
    private static final int OP_CHANGE = 2;
    /** The app requests seamless and the display supports. But the decision is still in shell. */
    private static final int OP_CHANGE_MAY_SEAMLESS = 3;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef(value = { OP_LEGACY, OP_APP_SWITCH, OP_CHANGE, OP_CHANGE_MAY_SEAMLESS })
    @interface TransitionOp {}

    /** Non-zero if this controller is triggered by shell transition. */
    private final @TransitionOp int mTransitionOp;

    /** Whether the start transaction of the transition is committed (by shell). */
    private boolean mIsStartTransactionCommitted;

    /** Whether all windows should wait for the start transaction. */
    private boolean mAlwaysWaitForStartTransaction;

    /** Whether the target windows have been requested to sync their draw transactions. */
    private boolean mIsSyncDrawRequested;

    private SeamlessRotator mRotator;

    private final int mOriginalRotation;
    private final boolean mHasScreenRotationAnimation;

    AsyncRotationController(DisplayContent displayContent) {
        super(displayContent);
        mService = displayContent.mWmService;
        mOriginalRotation = displayContent.getWindowConfiguration().getRotation();
        final int transitionType =
                displayContent.mTransitionController.getCollectingTransitionType();
        if (transitionType == WindowManager.TRANSIT_CHANGE) {
            final DisplayRotation dr = displayContent.getDisplayRotation();
            final WindowState w = displayContent.getDisplayPolicy().getTopFullscreenOpaqueWindow();
            // A rough condition to check whether it may be seamless style. Though the final
            // decision in shell may be different, it is fine because the jump cut can be covered
            // by a screenshot if shell falls back to use normal rotation animation.
            if (w != null && w.mAttrs.rotationAnimation == ROTATION_ANIMATION_SEAMLESS
                    && w.getTask() != null
                    && dr.canRotateSeamlessly(mOriginalRotation, dr.getRotation())) {
                mTransitionOp = OP_CHANGE_MAY_SEAMLESS;
            } else {
                mTransitionOp = OP_CHANGE;
            }
        } else if (displayContent.mTransitionController.isShellTransitionsEnabled()) {
            mTransitionOp = OP_APP_SWITCH;
        } else {
            mTransitionOp = OP_LEGACY;
        }

        // Although OP_CHANGE_MAY_SEAMLESS may still play screen rotation animation because shell
        // decides not to perform seamless rotation, it only affects whether to use fade animation
        // when the windows are drawn. If the windows are not too slow (after rotation animation is
        // done) to be drawn, the visual result can still look smooth.
        mHasScreenRotationAnimation =
                displayContent.getRotationAnimation() != null || mTransitionOp == OP_CHANGE;
        if (mHasScreenRotationAnimation) {
            // Hide the windows immediately because screen should have been covered by screenshot.
            mHideImmediately = true;
        }

        // Collect the windows which can rotate asynchronously without blocking the display.
        displayContent.forAllWindows(this, true /* traverseTopToBottom */);

        // Legacy animation doesn't need to wait for the start transaction.
        if (mTransitionOp == OP_LEGACY) {
            mIsStartTransactionCommitted = true;
        } else if (displayContent.mTransitionController.isCollecting(displayContent)) {
            final Transition transition =
                    mDisplayContent.mTransitionController.getCollectingTransition();
            if (transition != null) {
                final BLASTSyncEngine.SyncGroup syncGroup =
                        mDisplayContent.mWmService.mSyncEngine.getSyncSet(transition.getSyncId());
                if (syncGroup != null && syncGroup.mSyncMethod == BLASTSyncEngine.METHOD_BLAST) {
                    mAlwaysWaitForStartTransaction = true;
                }
            }
            keepAppearanceInPreviousRotation();
        }
    }

    /** Assigns the operation for the window tokens which can update rotation asynchronously. */
    @Override
    public void accept(WindowState w) {
        if (!w.mHasSurface || !canBeAsync(w.mToken)) {
            return;
        }
        if (mTransitionOp == OP_LEGACY && w.mForceSeamlesslyRotate) {
            // Legacy transition already handles seamlessly windows.
            return;
        }
        if (w.mAttrs.type == TYPE_NAVIGATION_BAR) {
            int action = Operation.ACTION_FADE;
            final boolean navigationBarCanMove =
                    mDisplayContent.getDisplayPolicy().navigationBarCanMove();
            if (mTransitionOp == OP_LEGACY) {
                mNavBarToken = w.mToken;
                // Do not animate movable navigation bar (e.g. 3-buttons mode).
                if (navigationBarCanMove) return;
                // Or when the navigation bar is currently controlled by recents animation.
                final RecentsAnimationController recents = mService.getRecentsAnimationController();
                if (recents != null && recents.isNavigationBarAttachedToApp()) {
                    return;
                }
            } else if (navigationBarCanMove || mTransitionOp == OP_CHANGE_MAY_SEAMLESS) {
                action = Operation.ACTION_SEAMLESS;
            } else if (mDisplayContent.mTransitionController.mNavigationBarAttachedToApp) {
                return;
            }
            mTargetWindowTokens.put(w.mToken, new Operation(action));
            return;
        }

        final int action = mTransitionOp == OP_CHANGE_MAY_SEAMLESS || w.mForceSeamlesslyRotate
                ? Operation.ACTION_SEAMLESS : Operation.ACTION_FADE;
        mTargetWindowTokens.put(w.mToken, new Operation(action));
    }

    /** Returns {@code true} if the window token can update rotation independently. */
    static boolean canBeAsync(WindowToken token) {
        final int type = token.windowType;
        return type > WindowManager.LayoutParams.LAST_APPLICATION_WINDOW
                && type != WindowManager.LayoutParams.TYPE_INPUT_METHOD
                && type != WindowManager.LayoutParams.TYPE_WALLPAPER
                && type != WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
    }

    /**
     * Enables {@link #handleFinishDrawing(WindowState, SurfaceControl.Transaction)} to capture the
     * draw transactions of the target windows if needed.
     */
    void keepAppearanceInPreviousRotation() {
        if (mIsSyncDrawRequested) return;
        // The transition sync group may be finished earlier because it doesn't wait for these
        // target windows. But the windows still need to use sync transaction to keep the appearance
        // in previous rotation, so request a no-op sync to keep the state.
        for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) {
            if (canDrawBeforeStartTransaction(mTargetWindowTokens.valueAt(i))) {
                // Expect a screenshot layer will cover the non seamless windows.
                continue;
            }
            final WindowToken token = mTargetWindowTokens.keyAt(i);
            for (int j = token.getChildCount() - 1; j >= 0; j--) {
                // TODO(b/234585256): The consumer should be handleFinishDrawing().
                token.getChildAt(j).applyWithNextDraw(t -> {});
                if (DEBUG) Slog.d(TAG, "Sync draw for " + token.getChildAt(j));
            }
        }
        mIsSyncDrawRequested = true;
        if (DEBUG) Slog.d(TAG, "Requested to sync draw transaction");
    }

    /**
     * If an async window is not requested to redraw or its surface is removed, then complete its
     * operation directly to avoid waiting until timeout.
     */
    void updateTargetWindows() {
        if (mTransitionOp == OP_LEGACY || !mIsStartTransactionCommitted) return;
        for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) {
            final Operation op = mTargetWindowTokens.valueAt(i);
            if (op.mIsCompletionPending || op.mAction == Operation.ACTION_SEAMLESS) {
                // Skip completed target. And seamless windows use the signal from blast sync.
                continue;
            }
            final WindowToken token = mTargetWindowTokens.keyAt(i);
            int readyCount = 0;
            final int childCount = token.getChildCount();
            for (int j = childCount - 1; j >= 0; j--) {
                final WindowState w = token.getChildAt(j);
                // If the token no longer contains pending drawn windows, then it is ready.
                if (w.isDrawn() || !w.mWinAnimator.getShown()) {
                    readyCount++;
                }
            }
            if (readyCount == childCount) {
                mDisplayContent.finishAsyncRotation(token);
            }
        }
    }

    /** Lets the window fit in new rotation naturally. */
    private void finishOp(WindowToken windowToken) {
        final Operation op = mTargetWindowTokens.remove(windowToken);
        if (op == null) return;
        if (op.mDrawTransaction != null) {
            // Unblock the window to show its latest content.
            windowToken.getSyncTransaction().merge(op.mDrawTransaction);
            op.mDrawTransaction = null;
            if (DEBUG) Slog.d(TAG, "finishOp merge transaction " + windowToken.getTopChild());
        }
        if (op.mAction == Operation.ACTION_TOGGLE_IME) {
            if (DEBUG) Slog.d(TAG, "finishOp fade-in IME " + windowToken.getTopChild());
            fadeWindowToken(true /* show */, windowToken, ANIMATION_TYPE_TOKEN_TRANSFORM,
                    (type, anim) -> mDisplayContent.getInsetsStateController()
                            .getImeSourceProvider().reportImeDrawnForOrganizer());
        } else if (op.mAction == Operation.ACTION_FADE) {
            if (DEBUG) Slog.d(TAG, "finishOp fade-in " + windowToken.getTopChild());
            // The previous animation leash will be dropped when preparing fade-in animation, so
            // simply apply new animation without restoring the transformation.
            fadeWindowToken(true /* show */, windowToken, ANIMATION_TYPE_TOKEN_TRANSFORM);
        } else if (op.mAction == Operation.ACTION_SEAMLESS && mRotator != null
                && op.mLeash != null && op.mLeash.isValid()) {
            if (DEBUG) Slog.d(TAG, "finishOp undo seamless " + windowToken.getTopChild());
            mRotator.setIdentityMatrix(windowToken.getSyncTransaction(), op.mLeash);
        }
    }

    /**
     * Completes all operations such as applying fade-in animation on the previously hidden window
     * tokens. This is called if all windows are ready in new rotation or timed out.
     */
    void completeAll() {
        for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) {
            finishOp(mTargetWindowTokens.keyAt(i));
        }
        mTargetWindowTokens.clear();
        if (mTimeoutRunnable != null) {
            mService.mH.removeCallbacks(mTimeoutRunnable);
        }
        if (mOnShowRunnable != null) {
            mOnShowRunnable.run();
            mOnShowRunnable = null;
        }
    }

    /**
     * Notifies that the window is ready in new rotation. Returns {@code true} if all target
     * windows have completed their rotation operations.
     */
    boolean completeRotation(WindowToken token) {
        if (!mIsStartTransactionCommitted) {
            final Operation op = mTargetWindowTokens.get(token);
            // The animation or draw transaction should only start after the start transaction is
            // applied by shell (e.g. show screenshot layer). Otherwise the window will be blinking
            // before the rotation animation starts. So store to a pending list and animate them
            // until the transaction is committed.
            if (op != null) {
                if (DEBUG) Slog.d(TAG, "Complete set pending " + token.getTopChild());
                op.mIsCompletionPending = true;
            }
            return false;
        }
        if (mTransitionOp == OP_APP_SWITCH && token.mTransitionController.inTransition()) {
            final Operation op = mTargetWindowTokens.get(token);
            if (op != null && op.mAction == Operation.ACTION_FADE) {
                // Defer showing to onTransitionFinished().
                if (DEBUG) Slog.d(TAG, "Defer completion " + token.getTopChild());
                return false;
            }
        }
        if (!isTargetToken(token)) return false;
        if (mHasScreenRotationAnimation || mTransitionOp != OP_LEGACY) {
            if (DEBUG) Slog.d(TAG, "Complete directly " + token.getTopChild());
            finishOp(token);
            if (mTargetWindowTokens.isEmpty()) {
                if (mTimeoutRunnable != null) mService.mH.removeCallbacks(mTimeoutRunnable);
                return true;
            }
        }
        // The case (legacy fixed rotation) will be handled by completeAll() when all seamless
        // windows are done.
        return false;
    }

    /**
     * Prepares the corresponding operations (e.g. hide animation) for the window tokens which may
     * be seamlessly rotated later.
     */
    void start() {
        for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) {
            final WindowToken windowToken = mTargetWindowTokens.keyAt(i);
            final Operation op = mTargetWindowTokens.valueAt(i);
            if (op.mAction == Operation.ACTION_FADE || op.mAction == Operation.ACTION_TOGGLE_IME) {
                fadeWindowToken(false /* show */, windowToken, ANIMATION_TYPE_TOKEN_TRANSFORM);
                op.mLeash = windowToken.getAnimationLeash();
                if (DEBUG) Slog.d(TAG, "Start fade-out " + windowToken.getTopChild());
            } else if (op.mAction == Operation.ACTION_SEAMLESS) {
                op.mLeash = windowToken.mSurfaceControl;
                if (DEBUG) Slog.d(TAG, "Start seamless " + windowToken.getTopChild());
            }
        }
        if (mHasScreenRotationAnimation) {
            scheduleTimeout();
        }
    }

    private void scheduleTimeout() {
        if (mTimeoutRunnable == null) {
            mTimeoutRunnable = () -> {
                synchronized (mService.mGlobalLock) {
                    Slog.i(TAG, "Async rotation timeout: " + (!mIsStartTransactionCommitted
                            ? " start transaction is not committed" : mTargetWindowTokens));
                    mIsStartTransactionCommitted = true;
                    mDisplayContent.finishAsyncRotationIfPossible();
                    mService.mWindowPlacerLocked.performSurfacePlacement();
                }
            };
        }
        mService.mH.postDelayed(mTimeoutRunnable,
                WindowManagerService.WINDOW_FREEZE_TIMEOUT_DURATION);
    }

    /** Hides the IME window immediately until it is drawn in new rotation. */
    void hideImeImmediately() {
        if (mDisplayContent.mInputMethodWindow == null) return;
        final WindowToken imeWindowToken = mDisplayContent.mInputMethodWindow.mToken;
        if (isTargetToken(imeWindowToken)) return;
        final boolean original = mHideImmediately;
        mHideImmediately = true;
        final Operation op = new Operation(Operation.ACTION_TOGGLE_IME);
        mTargetWindowTokens.put(imeWindowToken, op);
        fadeWindowToken(false /* show */, imeWindowToken, ANIMATION_TYPE_TOKEN_TRANSFORM);
        op.mLeash = imeWindowToken.getAnimationLeash();
        mHideImmediately = original;
        if (DEBUG) Slog.d(TAG, "hideImeImmediately " + imeWindowToken.getTopChild());
    }

    /** Returns {@code true} if the window will rotate independently. */
    boolean isAsync(WindowState w) {
        return w.mToken == mNavBarToken
                || (w.mForceSeamlesslyRotate && mTransitionOp == OP_LEGACY)
                || isTargetToken(w.mToken);
    }

    /** Returns {@code true} if the controller will run fade animations on the window. */
    boolean isTargetToken(WindowToken token) {
        return mTargetWindowTokens.containsKey(token);
    }

    /**
     * Whether the insets animation leash should use previous position when running fade animation
     * or seamless transformation in a rotated display.
     */
    boolean shouldFreezeInsetsPosition(WindowState w) {
        if (TransitionController.SYNC_METHOD != BLASTSyncEngine.METHOD_BLAST) {
            // Expect a screenshot layer has covered the screen, so it is fine to let client side
            // insets animation runner update the position directly.
            return false;
        }
        return mTransitionOp != OP_LEGACY && !mIsStartTransactionCommitted
                && isTargetToken(w.mToken);
    }

    /**
     * Returns the transaction which will be applied after the window redraws in new rotation.
     * This is used to update the position of insets animation leash synchronously.
     */
    SurfaceControl.Transaction getDrawTransaction(WindowToken token) {
        if (mTransitionOp == OP_LEGACY) {
            // Legacy transition uses startSeamlessRotation and finishSeamlessRotation of
            // InsetsSourceProvider.
            return null;
        }
        final Operation op = mTargetWindowTokens.get(token);
        if (op != null) {
            if (op.mDrawTransaction == null) {
                op.mDrawTransaction = new SurfaceControl.Transaction();
            }
            return op.mDrawTransaction;
        }
        return null;
    }

    void setOnShowRunnable(Runnable onShowRunnable) {
        mOnShowRunnable = onShowRunnable;
    }

    /**
     * Puts initial operation of leash to the transaction which will be executed when the
     * transition starts. And associate transaction callback to consume pending animations.
     */
    void setupStartTransaction(SurfaceControl.Transaction t) {
        if (mIsStartTransactionCommitted) return;
        for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) {
            final Operation op = mTargetWindowTokens.valueAt(i);
            final SurfaceControl leash = op.mLeash;
            if (leash == null || !leash.isValid()) continue;
            if (mHasScreenRotationAnimation && op.mAction == Operation.ACTION_FADE) {
                // Hide the windows immediately because a screenshot layer should cover the screen.
                t.setAlpha(leash, 0f);
                if (DEBUG) {
                    Slog.d(TAG, "Setup alpha0 " + mTargetWindowTokens.keyAt(i).getTopChild());
                }
            } else {
                // Take OPEN/CLOSE transition type as the example, the non-activity windows need to
                // fade out in previous rotation while display has rotated to the new rotation, so
                // their leashes are transformed with the start transaction.
                if (mRotator == null) {
                    mRotator = new SeamlessRotator(mOriginalRotation,
                            mDisplayContent.getWindowConfiguration().getRotation(),
                            mDisplayContent.getDisplayInfo(),
                            false /* applyFixedTransformationHint */);
                }
                mRotator.applyTransform(t, leash);
                if (DEBUG) {
                    Slog.d(TAG, "Setup unrotate " + mTargetWindowTokens.keyAt(i).getTopChild());
                }
            }
        }

        // If there are windows have redrawn in new rotation but the start transaction has not
        // been applied yet, the fade-in animation will be deferred. So once the transaction is
        // committed, the fade-in animation can run with screen rotation animation.
        t.addTransactionCommittedListener(new HandlerExecutor(mService.mH), () -> {
            synchronized (mService.mGlobalLock) {
                if (DEBUG) Slog.d(TAG, "Start transaction is committed");
                mIsStartTransactionCommitted = true;
                for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) {
                    if (mTargetWindowTokens.valueAt(i).mIsCompletionPending) {
                        if (DEBUG) {
                            Slog.d(TAG, "Continue pending completion "
                                    + mTargetWindowTokens.keyAt(i).getTopChild());
                        }
                        mDisplayContent.finishAsyncRotation(mTargetWindowTokens.keyAt(i));
                    }
                }
            }
        });
    }

    /** Called when the transition by shell is done. */
    void onTransitionFinished() {
        if (mTransitionOp == OP_CHANGE) {
            // With screen rotation animation, the windows are always faded in when they are drawn.
            // Because if they are drawn fast enough, the fade animation should not be observable.
            return;
        }
        if (DEBUG) Slog.d(TAG, "onTransitionFinished " + mTargetWindowTokens);
        // For other transition types, the fade-in animation runs after the transition to make the
        // transition animation (e.g. launch activity) look cleaner.
        for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) {
            final WindowToken token = mTargetWindowTokens.keyAt(i);
            if (!token.isVisible()) {
                mDisplayContent.finishAsyncRotation(token);
                continue;
            }
            for (int j = token.getChildCount() - 1; j >= 0; j--) {
                // Only fade in the drawn windows. If the remaining windows are drawn later,
                // show(WindowToken) will be called to fade in them.
                if (token.getChildAt(j).isDrawFinishedLw()) {
                    mDisplayContent.finishAsyncRotation(token);
                    break;
                }
            }
        }
        if (!mTargetWindowTokens.isEmpty()) {
            scheduleTimeout();
        }
    }

    /**
     * Captures the post draw transaction if the window should keep its appearance in previous
     * rotation when running transition. Returns {@code true} if the draw transaction is handled
     * by this controller.
     */
    boolean handleFinishDrawing(WindowState w, SurfaceControl.Transaction postDrawTransaction) {
        if (mTransitionOp == OP_LEGACY) {
            return false;
        }
        final Operation op = mTargetWindowTokens.get(w.mToken);
        if (op == null) return false;
        if (DEBUG) Slog.d(TAG, "handleFinishDrawing " + w);
        if (postDrawTransaction == null || !mIsSyncDrawRequested
                || canDrawBeforeStartTransaction(op)) {
            mDisplayContent.finishAsyncRotation(w.mToken);
            return false;
        }
        if (op.mDrawTransaction == null) {
            if (w.isClientLocal()) {
                // Use a new transaction to merge the draw transaction of local window because the
                // same instance will be cleared (Transaction#clear()) after reporting draw.
                op.mDrawTransaction = mService.mTransactionFactory.get();
                op.mDrawTransaction.merge(postDrawTransaction);
            } else {
                // The transaction read from parcel (the client is in a different process) is
                // already a copy, so just reference it directly.
                op.mDrawTransaction = postDrawTransaction;
            }
        } else {
            op.mDrawTransaction.merge(postDrawTransaction);
        }
        mDisplayContent.finishAsyncRotation(w.mToken);
        return true;
    }

    @Override
    public Animation getFadeInAnimation() {
        if (mHasScreenRotationAnimation) {
            // Use a shorter animation so it is easier to align with screen rotation animation.
            return AnimationUtils.loadAnimation(mContext, R.anim.screen_rotate_0_enter);
        }
        return super.getFadeInAnimation();
    }

    @Override
    public Animation getFadeOutAnimation() {
        if (mHideImmediately) {
            // For change transition, the hide transaction needs to be applied with sync transaction
            // (setupStartTransaction). So keep alpha 1 just to get the animation leash.
            final float alpha = mTransitionOp == OP_CHANGE ? 1 : 0;
            return new AlphaAnimation(alpha /* fromAlpha */, alpha /* toAlpha */);
        }
        return super.getFadeOutAnimation();
    }

    /**
     * Returns {@code true} if the corresponding window can draw its latest content before the
     * start transaction of rotation transition is applied.
     */
    private boolean canDrawBeforeStartTransaction(Operation op) {
        return !mAlwaysWaitForStartTransaction && op.mAction != Operation.ACTION_SEAMLESS;
    }

    /** The operation to control the rotation appearance associated with window token. */
    private static class Operation {
        @Retention(RetentionPolicy.SOURCE)
        @IntDef(value = { ACTION_SEAMLESS, ACTION_FADE, ACTION_TOGGLE_IME })
        @interface Action {}

        static final int ACTION_SEAMLESS = 1;
        static final int ACTION_FADE = 2;
        /** The action to toggle the IME window appearance */
        static final int ACTION_TOGGLE_IME = 3;
        final @Action int mAction;
        /** The leash of window token. It can be animation leash or the token itself. */
        SurfaceControl mLeash;
        /** Whether the window is drawn before the transition starts. */
        boolean mIsCompletionPending;

        /**
         * The sync transaction of the target window. It is used when the display has rotated but
         * the window needs to show in previous rotation. The transaction will be applied after the
         * the start transaction of transition, so there won't be a flickering such as the window
         * has redrawn during fading out.
         */
        SurfaceControl.Transaction mDrawTransaction;

        Operation(@Action int action) {
            mAction = action;
        }
    }
}