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

Commit 80419fb9 authored by Arthur Hung's avatar Arthur Hung
Browse files

Intruduce Cross Task Animator

Add a new animator to support `TYPE_CROSS_TASK` when starting a back
navigation and it will return to another task.

Bug: 236759828
Test: atest BackAnimationControllerTest BackNavigationControllerTests
Change-Id: If7db2645f40f16fc8b81f1d0f84cd606a393c0e0
parent 1328cadf
Loading
Loading
Loading
Loading
+26 −0
Original line number Diff line number Diff line
@@ -38,6 +38,7 @@ import android.os.UserHandle;
import android.provider.Settings.Global;
import android.util.Log;
import android.util.SparseArray;
import android.view.IRemoteAnimationFinishedCallback;
import android.view.IRemoteAnimationRunner;
import android.view.IWindowFocusObserver;
import android.view.InputDevice;
@@ -187,6 +188,31 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
        }
        mShellController.addExternalInterface(KEY_EXTRA_SHELL_BACK_ANIMATION,
                this::createExternalInterface, this);

        initBackAnimationRunners();
    }

    private void initBackAnimationRunners() {
        final IOnBackInvokedCallback dummyCallback = new IOnBackInvokedCallback.Default();
        final IRemoteAnimationRunner dummyRunner = new IRemoteAnimationRunner.Default() {
            @Override
            public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
                    RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
                    IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException {
                // Animation missing. Simply finish animation.
                finishedCallback.onAnimationFinished();
            }
        };

        final BackAnimationRunner dummyBackRunner =
                new BackAnimationRunner(dummyCallback, dummyRunner);
        final CrossTaskBackAnimation crossTaskAnimation = new CrossTaskBackAnimation(mContext);
        mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_TASK,
                new BackAnimationRunner(crossTaskAnimation.mCallback, crossTaskAnimation.mRunner));
        // TODO (238474994): register cross activity animation when it's completed.
        mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_ACTIVITY, dummyBackRunner);
        // TODO (236760237): register dialog close animation when it's completed.
        mAnimationDefinition.set(BackNavigationInfo.TYPE_DIALOG_CLOSE, dummyBackRunner);
    }

    private void setupAnimationDeveloperSettingsObserver(
+365 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.back;

import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static android.window.BackEvent.EDGE_RIGHT;

import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.RemoteException;
import android.view.IRemoteAnimationFinishedCallback;
import android.view.IRemoteAnimationRunner;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import android.window.BackEvent;
import android.window.BackProgressAnimator;
import android.window.IOnBackInvokedCallback;

import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.annotations.ShellMainThread;

/**
 * Controls the animation of swiping back and returning to another task.
 *
 * This is a two part animation. The first part is an animation that tracks gesture location to
 * scale and move the closing and entering app windows.
 * Once the gesture is committed, the second part remains the closing window in place.
 * The entering window plays the rest of app opening transition to enter full screen.
 *
 * This animation is used only for apps that enable back dispatching via
 * {@link android.window.OnBackInvokedDispatcher}. The controller registers
 * an {@link IOnBackInvokedCallback} with WM Shell and receives back dispatches when a back
 * navigation to launcher starts.
 */
@ShellMainThread
class CrossTaskBackAnimation {
    private static final float[] BACKGROUNDCOLOR = {0.263f, 0.263f, 0.227f};

    /**
     * Minimum scale of the entering window.
     */
    private static final float ENTERING_MIN_WINDOW_SCALE = 0.85f;

    /**
     * Minimum scale of the closing window.
     */
    private static final float CLOSING_MIN_WINDOW_SCALE = 0.75f;

    /**
     * Minimum color scale of the closing window.
     */
    private static final float CLOSING_MIN_WINDOW_COLOR_SCALE = 0.1f;

    /**
     * The margin between the entering window and the closing window
     */
    private static final int WINDOW_MARGIN = 35;

    /** Max window translation in the Y axis. */
    private static final int WINDOW_MAX_DELTA_Y = 160;

    private final Rect mStartTaskRect = new Rect();
    private final float mCornerRadius;

    // The closing window properties.
    private final RectF mClosingCurrentRect = new RectF();

    // The entering window properties.
    private final Rect mEnteringStartRect = new Rect();
    private final RectF mEnteringCurrentRect = new RectF();

    private final PointF mInitialTouchPos = new PointF();
    private final Interpolator mInterpolator = new AccelerateDecelerateInterpolator();

    private final Matrix mTransformMatrix = new Matrix();

    private final float[] mTmpFloat9 = new float[9];
    private final float[] mTmpTranslate = {0, 0, 0};

    private RemoteAnimationTarget mEnteringTarget;
    private RemoteAnimationTarget mClosingTarget;
    private SurfaceControl mBackgroundSurface;
    private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();

    private boolean mBackInProgress = false;

    private boolean mIsRightEdge;
    private float mProgress = 0;
    private PointF mTouchPos = new PointF();
    private IRemoteAnimationFinishedCallback mFinishCallback;

    private BackProgressAnimator mProgressAnimator = new BackProgressAnimator();

    final IOnBackInvokedCallback mCallback = new IOnBackInvokedCallback.Default() {
        @Override
        public void onBackStarted(BackEvent backEvent) {
            mProgressAnimator.onBackStarted(backEvent,
                    CrossTaskBackAnimation.this::onGestureProgress);
        }

        @Override
        public void onBackProgressed(@NonNull BackEvent backEvent) {
            mProgressAnimator.onBackProgressed(backEvent);
        }

        @Override
        public void onBackCancelled() {
            mProgressAnimator.reset();
            finishAnimation();
        }

        @Override
        public void onBackInvoked() {
            mProgressAnimator.reset();
            onGestureCommitted();
        }
    };

    final IRemoteAnimationRunner mRunner = new IRemoteAnimationRunner.Default() {
        @Override
        public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
                RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
                IRemoteAnimationFinishedCallback finishedCallback) {
            ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to task animation.");
            for (RemoteAnimationTarget a : apps) {
                if (a.mode == MODE_CLOSING) {
                    mClosingTarget = a;
                }
                if (a.mode == MODE_OPENING) {
                    mEnteringTarget = a;
                }
            }

            startBackAnimation();
            mFinishCallback = finishedCallback;
        }
    };

    CrossTaskBackAnimation(Context context) {
        mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
    }

    private float getInterpolatedProgress(float backProgress) {
        return 1 - (1 - backProgress) * (1 - backProgress) * (1 - backProgress);
    }

    private void startBackAnimation() {
        if (mEnteringTarget == null || mClosingTarget == null) {
            ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null.");
            return;
        }

        // Offset start rectangle to align task bounds.
        mStartTaskRect.set(mClosingTarget.windowConfiguration.getBounds());
        mStartTaskRect.offsetTo(0, 0);

        // Draw background.
        mBackgroundSurface = new SurfaceControl.Builder()
                .setName("Background of Back Navigation")
                .setColorLayer()
                .setHidden(false)
                .build();
        mTransaction.setColor(mBackgroundSurface, BACKGROUNDCOLOR)
                .setLayer(mBackgroundSurface, -1);
        mTransaction.apply();
    }

    private void updateGestureBackProgress(float progress, BackEvent event) {
        if (mEnteringTarget == null || mClosingTarget == null) {
            return;
        }

        float touchX = event.getTouchX();
        float touchY = event.getTouchY();
        float dX = Math.abs(touchX - mInitialTouchPos.x);

        // The 'follow width' is the width of the window if it completely matches
        // the gesture displacement.
        final int width = mStartTaskRect.width();
        final int height = mStartTaskRect.height();

        // The 'progress width' is the width of the window if it strictly linearly interpolates
        // to minimum scale base on progress.
        float enteringScale = mapRange(progress, 1, ENTERING_MIN_WINDOW_SCALE);
        float closingScale = mapRange(progress, 1, CLOSING_MIN_WINDOW_SCALE);
        float closingColorScale = mapRange(progress, 1, CLOSING_MIN_WINDOW_COLOR_SCALE);

        // The final width is derived from interpolating between the follow with and progress width
        // using gesture progress.
        float enteringWidth = enteringScale * width;
        float closingWidth = closingScale * width;
        float enteringHeight = (float) height / width * enteringWidth;
        float closingHeight = (float) height / width * closingWidth;

        float deltaYRatio = (touchY - mInitialTouchPos.y) / height;
        // Base the window movement in the Y axis on the touch movement in the Y axis.
        float deltaY = (float) Math.sin(deltaYRatio * Math.PI * 0.5f) * WINDOW_MAX_DELTA_Y;
        // Move the window along the Y axis.
        float closingTop = (height - closingHeight) * 0.5f + deltaY;
        float enteringTop = (height - enteringHeight) * 0.5f + deltaY;
        // Move the window along the X axis.
        float right = width - (progress * WINDOW_MARGIN);
        float left = right - closingWidth;

        mClosingCurrentRect.set(left, closingTop, right, closingTop + closingHeight);
        mEnteringCurrentRect.set(left - enteringWidth - WINDOW_MARGIN, enteringTop,
                left - WINDOW_MARGIN, enteringTop + enteringHeight);

        applyTransform(mClosingTarget.leash, mClosingCurrentRect, mCornerRadius);
        applyColorTransform(mClosingTarget.leash, closingColorScale);
        applyTransform(mEnteringTarget.leash, mEnteringCurrentRect, mCornerRadius);
        mTransaction.apply();
    }

    private void updatePostCommitClosingAnimation(float progress) {
        mTransaction.setLayer(mClosingTarget.leash, 0);
        float alpha = mapRange(progress, 1, 0);
        mTransaction.setAlpha(mClosingTarget.leash, alpha);
    }

    private void updatePostCommitEnteringAnimation(float progress) {
        float left = mapRange(progress, mEnteringStartRect.left, mStartTaskRect.left);
        float top = mapRange(progress, mEnteringStartRect.top, mStartTaskRect.top);
        float width = mapRange(progress, mEnteringStartRect.width(), mStartTaskRect.width());
        float height = mapRange(progress, mEnteringStartRect.height(), mStartTaskRect.height());

        mEnteringCurrentRect.set(left, top, left + width, top + height);
        applyTransform(mEnteringTarget.leash, mEnteringCurrentRect, mCornerRadius);
    }

    /** Transform the target window to match the target rect. */
    private void applyTransform(SurfaceControl leash, RectF targetRect, float cornerRadius) {
        if (leash == null) {
            return;
        }

        final float scale = targetRect.width() / mStartTaskRect.width();
        mTransformMatrix.reset();
        mTransformMatrix.setScale(scale, scale);
        mTransformMatrix.postTranslate(targetRect.left, targetRect.top);
        mTransaction.setMatrix(leash, mTransformMatrix, mTmpFloat9)
                .setWindowCrop(leash, mStartTaskRect)
                .setCornerRadius(leash, cornerRadius);
    }

    private void applyColorTransform(SurfaceControl leash, float colorScale) {
        if (leash == null) {
            return;
        }
        computeScaleTransformMatrix(colorScale, mTmpFloat9);
        mTransaction.setColorTransform(leash, mTmpFloat9, mTmpTranslate);
    }

    static void computeScaleTransformMatrix(float scale, float[] matrix) {
        matrix[0] = scale;
        matrix[1] = 0;
        matrix[2] = 0;
        matrix[3] = 0;
        matrix[4] = scale;
        matrix[5] = 0;
        matrix[6] = 0;
        matrix[7] = 0;
        matrix[8] = scale;
    }

    private void finishAnimation() {
        if (mEnteringTarget != null) {
            mEnteringTarget.leash.release();
            mEnteringTarget = null;
        }
        if (mClosingTarget != null) {
            mClosingTarget.leash.release();
            mClosingTarget = null;
        }

        if (mBackgroundSurface != null) {
            mBackgroundSurface.release();
            mBackgroundSurface = null;
        }

        mBackInProgress = false;
        mTransformMatrix.reset();
        mClosingCurrentRect.setEmpty();
        mInitialTouchPos.set(0, 0);

        if (mFinishCallback != null) {
            try {
                mFinishCallback.onAnimationFinished();
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            mFinishCallback = null;
        }
    }

    private void onGestureProgress(@NonNull BackEvent backEvent) {
        if (!mBackInProgress) {
            mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
            mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT;
            mBackInProgress = true;
        }
        mProgress = backEvent.getProgress();
        mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
        updateGestureBackProgress(getInterpolatedProgress(mProgress), backEvent);
    }

    private void onGestureCommitted() {
        if (mEnteringTarget == null || mClosingTarget == null) {
            finishAnimation();
            return;
        }

        // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current
        // coordinate of the gesture driven phase.
        mEnteringCurrentRect.round(mEnteringStartRect);

        ValueAnimator valueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(300);
        valueAnimator.setInterpolator(mInterpolator);
        valueAnimator.addUpdateListener(animation -> {
            float progress = animation.getAnimatedFraction();
            updatePostCommitEnteringAnimation(progress);
            updatePostCommitClosingAnimation(progress);
            mTransaction.apply();
        });

        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                finishAnimation();
            }
        });
        valueAnimator.start();
    }

    private static float mapRange(float value, float min, float max) {
        return min + (value * (max - min));
    }
}
+16 −8
Original line number Diff line number Diff line
@@ -243,16 +243,22 @@ class BackNavigationController {
            } else if (currentActivity.isRootOfTask()) {
                // TODO(208789724): Create single source of truth for this, maybe in
                //  RootWindowContainer
                // TODO: Also check Task.shouldUpRecreateTaskLocked() for prevActivity logic
                prevTask = currentTask.mRootWindowContainer.getTaskBelow(currentTask);
                removedWindowContainer = currentTask;
                // If it reaches the top activity, we will check the below task from parent.
                // If it's null or multi-window, fallback the type to TYPE_CALLBACK.
                // or set the type to proper value when it's return to home or another task.
                if (prevTask == null || prevTask.inMultiWindowMode()) {
                    backType = BackNavigationInfo.TYPE_CALLBACK;
                } else {
                    prevActivity = prevTask.getTopNonFinishingActivity();
                    if (prevTask.isActivityTypeHome()) {
                        backType = BackNavigationInfo.TYPE_RETURN_TO_HOME;
                        mShowWallpaper = true;
                    } else {
                        backType = BackNavigationInfo.TYPE_CROSS_TASK;
                    }
                mShowWallpaper = true;
                }
            }
            infoBuilder.setType(backType);

@@ -263,8 +269,10 @@ class BackNavigationController {
                    removedWindowContainer,
                    BackNavigationInfo.typeToString(backType));

            // For now, we only animate when going home.
            boolean prepareAnimation = backType == BackNavigationInfo.TYPE_RETURN_TO_HOME
            // For now, we only animate when going home and cross task.
            boolean prepareAnimation =
                    (backType == BackNavigationInfo.TYPE_RETURN_TO_HOME
                            || backType == BackNavigationInfo.TYPE_CROSS_TASK)
                    && adapter != null;

            // Only prepare animation if no leash has been created (no animation is running).
+8 −1
Original line number Diff line number Diff line
@@ -98,12 +98,19 @@ public class BackNavigationControllerTests extends WindowTestsBase {
    @Test
    public void backTypeCrossTaskWhenBackToPreviousTask() {
        Task taskA = createTask(mDefaultDisplay);
        createActivityRecord(taskA);
        ActivityRecord recordA = createActivityRecord(taskA);
        Mockito.doNothing().when(recordA).reparentSurfaceControl(any(), any());

        withSystemCallback(createTopTaskWithActivity());
        BackNavigationInfo backNavigationInfo = startBackNavigation();
        assertWithMessage("BackNavigationInfo").that(backNavigationInfo).isNotNull();
        assertThat(typeToString(backNavigationInfo.getType()))
                .isEqualTo(typeToString(BackNavigationInfo.TYPE_CROSS_TASK));

        // verify if back animation would start.
        verify(mBackNavigationController).scheduleAnimationLocked(
                eq(BackNavigationInfo.TYPE_CROSS_TASK), any(), eq(mBackAnimationAdapter),
                any());
    }

    @Test