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

Commit 737bd6d0 authored by Vadim Caen's avatar Vadim Caen
Browse files

Integration of back gesture animation on Sysui (BackNav 2/n)

This CL contains the BackAnimationController, on WMShell, which
controls the gesture based animation.

When the gesture is done, the animation is done. It will be handed over
to a TransisionHandler (in the next CL)

Test: atest WMShellUnitTests:BackAnimationControllerTest
Bug: 131727607
Change-Id: Ifc2ed99bfd51079f12a83468ca44e4aa19460e47
parent a4ca5286
Loading
Loading
Loading
Loading
+35 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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 android.view.MotionEvent;

/**
 * Interface for SysUI to get access to the Back animation related methods.
 */
public interface BackAnimation {

    /**
     * Called when a {@link MotionEvent} is generated by a back gesture.
     */
    void onBackMotion(MotionEvent event);

    /**
     * Sets whether the back gesture is past the trigger threshold or not.
     */
    void setTriggerBack(boolean triggerBack);
}
+286 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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 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.annotation.Nullable;
import android.app.ActivityTaskManager;
import android.app.IActivityTaskManager;
import android.app.WindowConfiguration;
import android.graphics.Point;
import android.graphics.PointF;
import android.hardware.HardwareBuffer;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.window.BackNavigationInfo;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.annotations.ShellMainThread;

/**
 * Controls the window animation run when a user initiates a back gesture.
 */
public class BackAnimationController {

    private static final String BACK_PREDICTABILITY_PROP = "persist.debug.back_predictability";
    public static final boolean IS_ENABLED = SystemProperties
            .getInt(BACK_PREDICTABILITY_PROP, 0) > 0;
    private static final String TAG = "BackAnimationController";

    /**
     * Location of the initial touch event of the back gesture.
     */
    private final PointF mInitTouchLocation = new PointF();

    /**
     * Raw delta between {@link #mInitTouchLocation} and the last touch location.
     */
    private final Point mTouchEventDelta = new Point();
    private final ShellExecutor mShellExecutor;

    /** True when a back gesture is ongoing */
    private boolean mBackGestureStarted = false;

    /** @see #setTriggerBack(boolean) */
    private boolean mTriggerBack;

    @Nullable
    private BackNavigationInfo mBackNavigationInfo;
    private final SurfaceControl.Transaction mTransaction;
    private final IActivityTaskManager mActivityTaskManager;

    public BackAnimationController(@ShellMainThread ShellExecutor shellExecutor) {
        this(shellExecutor, new SurfaceControl.Transaction(), ActivityTaskManager.getService());
    }

    @VisibleForTesting
    BackAnimationController(@NonNull ShellExecutor shellExecutor,
            @NonNull SurfaceControl.Transaction transaction,
            @NonNull IActivityTaskManager activityTaskManager) {
        mShellExecutor = shellExecutor;
        mTransaction = transaction;
        mActivityTaskManager = activityTaskManager;
    }

    public BackAnimation getBackAnimationImpl() {
        return mBackAnimation;
    }

    private final BackAnimation mBackAnimation = new BackAnimationImpl();

    private class BackAnimationImpl implements BackAnimation {

        @Override
        public void onBackMotion(MotionEvent event) {
            mShellExecutor.execute(() -> onMotionEvent(event));
        }

        @Override
        public void setTriggerBack(boolean triggerBack) {
            mShellExecutor.execute(() -> BackAnimationController.this.setTriggerBack(triggerBack));
        }
    }

    /**
     * Called when a new motion event needs to be transferred to this
     * {@link BackAnimationController}
     */
    public void onMotionEvent(MotionEvent event) {
        int action = event.getActionMasked();
        if (action == MotionEvent.ACTION_DOWN) {
            initAnimation(event);
        } else if (action == MotionEvent.ACTION_MOVE) {
            onMove(event);
        } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
            onGestureFinished();
        }
    }

    private void initAnimation(MotionEvent event) {
        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "initAnimation mMotionStarted=%b", mBackGestureStarted);
        if (mBackGestureStarted) {
            Log.e(TAG, "Animation is being initialized but is already started.");
            return;
        }

        if (mBackNavigationInfo != null) {
            finishAnimation();
        }
        mInitTouchLocation.set(event.getX(), event.getY());
        mBackGestureStarted = true;

        try {
            mBackNavigationInfo = mActivityTaskManager.startBackNavigation();
            onBackNavigationInfoReceived(mBackNavigationInfo);
        } catch (RemoteException remoteException) {
            Log.e(TAG, "Failed to initAnimation", remoteException);
            finishAnimation();
        }
    }

    private void onBackNavigationInfoReceived(@Nullable BackNavigationInfo backNavigationInfo) {
        if (backNavigationInfo == null
                || backNavigationInfo.getDepartingWindowContainer() == null) {
            Log.e(TAG, "Received BackNavigationInfo is null.");
            finishAnimation();
            return;
        }

        HardwareBuffer hardwareBuffer = backNavigationInfo.getScreenshotHardwareBuffer();
        if (hardwareBuffer != null) {
            displayTargetScreenshot(hardwareBuffer,
                    backNavigationInfo.getTaskWindowConfiguration());
        }
        mTransaction.apply();
    }

    /**
     * Display the screenshot of the activity beneath.
     *
     * @param hardwareBuffer The buffer containing the screenshot.
     */
    private void displayTargetScreenshot(@NonNull HardwareBuffer hardwareBuffer,
            WindowConfiguration taskWindowConfiguration) {
        SurfaceControl screenshotSurface =
                mBackNavigationInfo == null ? null : mBackNavigationInfo.getScreenshotSurface();
        if (screenshotSurface == null) {
            Log.e(TAG, "BackNavigationInfo doesn't contain a surface for the screenshot. ");
            return;
        }

        // Scale the buffer to fill the whole Task
        float sx = 1;
        float sy = 1;
        float w = taskWindowConfiguration.getBounds().width();
        float h = taskWindowConfiguration.getBounds().height();

        if (w != hardwareBuffer.getWidth()) {
            sx = w / hardwareBuffer.getWidth();
        }

        if (h != hardwareBuffer.getHeight()) {
            sy = h / hardwareBuffer.getHeight();
        }
        mTransaction.setScale(screenshotSurface, sx, sy);
        mTransaction.setBuffer(screenshotSurface, hardwareBuffer);
        mTransaction.setVisibility(screenshotSurface, true);
    }

    private void onMove(MotionEvent event) {
        if (!mBackGestureStarted || mBackNavigationInfo == null) {
            return;
        }
        int deltaX = Math.round(event.getX() - mInitTouchLocation.x);
        int deltaY = Math.round(event.getY() - mInitTouchLocation.y);
        ProtoLog.v(WM_SHELL_BACK_PREVIEW, "Runner move: %d %d", deltaX, deltaY);
        SurfaceControl topWindowLeash = mBackNavigationInfo.getDepartingWindowContainer();
        mTransaction.setPosition(topWindowLeash, deltaX, deltaY);
        mTouchEventDelta.set(deltaX, deltaY);
        mTransaction.apply();
    }

    private void onGestureFinished() {
        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", mTriggerBack);
        if (mBackGestureStarted) {
            if (mTriggerBack) {
                prepareTransition();
            } else {
                resetPositionAnimated();
            }
        }
        mBackGestureStarted = false;
        mTriggerBack = false;
    }

    /**
     * Animate the top window leash to its initial position.
     */
    private void resetPositionAnimated() {
        mBackGestureStarted = false;
        // TODO(208786853) Handle overlap with a new coming gesture.
        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Runner: Back not triggered, cancelling animation "
                + "mLastPos=%s mInitTouch=%s", mTouchEventDelta, mInitTouchLocation);

        // TODO(208427216) : Replace placeholder animation with an actual one.
        ValueAnimator animation = ValueAnimator.ofFloat(0f, 1f).setDuration(200);
        animation.addUpdateListener(animation1 -> {
            if (mBackNavigationInfo == null) {
                return;
            }
            float fraction = animation1.getAnimatedFraction();
            int deltaX = Math.round(mTouchEventDelta.x - (mTouchEventDelta.x * fraction));
            int deltaY = Math.round(mTouchEventDelta.y - (mTouchEventDelta.y * fraction));
            mTransaction.setPosition(mBackNavigationInfo.getDepartingWindowContainer(),
                    deltaX, deltaY);
            mTransaction.apply();
        });

        animation.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: onAnimationEnd");
                finishAnimation();
            }
        });
        animation.start();
    }

    private void prepareTransition() {
        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "prepareTransition()");
        mTriggerBack = false;
        mBackGestureStarted = false;
    }

    /**
     * Sets to true when the back gesture has passed the triggering threshold, false otherwise.
     */
    public void setTriggerBack(boolean triggerBack) {
        mTriggerBack = triggerBack;
    }

    private void finishAnimation() {
        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishAnimation()");
        mBackGestureStarted = false;
        mTouchEventDelta.set(0, 0);
        mInitTouchLocation.set(0, 0);
        BackNavigationInfo backNavigationInfo = mBackNavigationInfo;
        mBackNavigationInfo = null;
        if (backNavigationInfo == null) {
            return;
        }
        SurfaceControl topWindowLeash = backNavigationInfo.getDepartingWindowContainer();
        if (topWindowLeash != null && topWindowLeash.isValid()) {
            mTransaction.remove(topWindowLeash);
        }
        SurfaceControl screenshotSurface = backNavigationInfo.getScreenshotSurface();
        if (screenshotSurface != null && screenshotSurface.isValid()) {
            mTransaction.remove(screenshotSurface);
        }
        mTransaction.apply();
        backNavigationInfo.onBackNavigationFinished();
    }
}
+25 −0
Original line number Diff line number Diff line
@@ -40,6 +40,8 @@ import com.android.wm.shell.TaskViewTransitions;
import com.android.wm.shell.WindowManagerShellWrapper;
import com.android.wm.shell.apppairs.AppPairs;
import com.android.wm.shell.apppairs.AppPairsController;
import com.android.wm.shell.back.BackAnimation;
import com.android.wm.shell.back.BackAnimationController;
import com.android.wm.shell.bubbles.BubbleController;
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.common.DisplayController;
@@ -237,6 +239,17 @@ public abstract class WMShellBaseModule {
        return new WindowManagerShellWrapper(mainExecutor);
    }

    //
    // Back animation
    //

    @WMSingleton
    @Provides
    static Optional<BackAnimation> provideBackAnimation(
            Optional<BackAnimationController> backAnimationController) {
        return backAnimationController.map(BackAnimationController::getBackAnimationImpl);
    }

    //
    // Bubbles (optional feature)
    //
@@ -678,4 +691,16 @@ public abstract class WMShellBaseModule {
                legacySplitScreenOptional, splitScreenOptional, pipOptional, oneHandedOptional,
                hideDisplayCutout, appPairsOptional, recentTasksOptional, mainExecutor);
    }

    @WMSingleton
    @Provides
    static Optional<BackAnimationController> provideBackAnimationController(
            @ShellMainThread ShellExecutor shellExecutor
    ) {
        if (BackAnimationController.IS_ENABLED) {
            return Optional.of(
                    new BackAnimationController(shellExecutor));
        }
        return Optional.empty();
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -34,6 +34,8 @@ public enum ShellProtoLogGroup implements IProtoLogGroup {
            Consts.TAG_WM_SHELL),
    WM_SHELL_STARTING_WINDOW(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
            Consts.TAG_WM_STARTING_WINDOW),
    WM_SHELL_BACK_PREVIEW(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
            "ShellBackPreview"),
    TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest");

    private final boolean mEnabled;
+109 −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 org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import android.app.IActivityTaskManager;
import android.app.WindowConfiguration;
import android.hardware.HardwareBuffer;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.testing.AndroidTestingRunner;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.window.BackNavigationInfo;

import androidx.test.filters.SmallTest;

import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.common.ShellExecutor;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

/**
 * atest WMShellUnitTests:BackAnimationControllerTest
 */
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class BackAnimationControllerTest {

    private final ShellExecutor mShellExecutor = new TestShellExecutor();

    @Mock
    private SurfaceControl.Transaction mTransaction;

    @Mock
    private IActivityTaskManager mActivityTaskManager;

    private BackAnimationController mController;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        mController = new BackAnimationController(
                mShellExecutor, mTransaction, mActivityTaskManager);
    }

    private void createNavigationInfo(SurfaceControl topWindowLeash,
            SurfaceControl screenshotSurface,
            HardwareBuffer hardwareBuffer) {
        BackNavigationInfo navigationInfo = new BackNavigationInfo(
                BackNavigationInfo.TYPE_RETURN_TO_HOME,
                topWindowLeash,
                screenshotSurface,
                hardwareBuffer,
                new WindowConfiguration(),
                new RemoteCallback((bundle) -> {}));
        try {
            doReturn(navigationInfo).when(mActivityTaskManager).startBackNavigation();
        } catch (RemoteException ex) {
            ex.rethrowFromSystemServer();
        }
    }

    @Test
    public void screenshotAttachedAndVisible() {
        SurfaceControl topWindowLeash = new SurfaceControl();
        SurfaceControl screenshotSurface = new SurfaceControl();
        HardwareBuffer hardwareBuffer = mock(HardwareBuffer.class);
        createNavigationInfo(topWindowLeash, screenshotSurface, hardwareBuffer);
        mController.onMotionEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0));
        verify(mTransaction).setBuffer(screenshotSurface, hardwareBuffer);
        verify(mTransaction).setVisibility(screenshotSurface, true);
        verify(mTransaction).apply();
    }

    @Test
    public void surfaceMovesWithGesture() {
        SurfaceControl topWindowLeash = new SurfaceControl();
        SurfaceControl screenshotSurface = new SurfaceControl();
        HardwareBuffer hardwareBuffer = mock(HardwareBuffer.class);
        createNavigationInfo(topWindowLeash, screenshotSurface, hardwareBuffer);
        mController.onMotionEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0));
        mController.onMotionEvent(MotionEvent.obtain(10, 0, MotionEvent.ACTION_MOVE, 100, 100, 0));
        verify(mTransaction).setPosition(topWindowLeash, 100, 100);
        verify(mTransaction, atLeastOnce()).apply();
    }
}
Loading