From a4421ba49f3d130b6b0e9f445579a24a64c727fd Mon Sep 17 00:00:00 2001 From: Matt Sziklay Date: Wed, 22 Mar 2023 22:26:50 +0000 Subject: [PATCH] Animation for Freeform -> Fullscreen Transition When dragged into the status bar, the task will animate to fullscreen via shell transition. Video: https://recall.googleplex.com/projects/099cef64-5403-42f3-bc17-ea11eb878bba/sessions/2f77cbf6-5c2b-4784-a51d-30a913eab75d Bug: 266973675 Test: Manual; drag caption into status bar from various starting sizes. Change-Id: Ie542676530c630bd1f31d769b050b09603291074 --- .../wm/shell/dagger/WMShellModule.java | 16 +- .../desktopmode/DesktopTasksController.kt | 22 ++- .../ExitDesktopTaskTransitionHandler.java | 162 ++++++++++++++++++ .../wm/shell/transition/Transitions.java | 3 + .../desktopmode/DesktopTasksControllerTest.kt | 6 +- .../ExitDesktopTaskTransitionHandlerTest.java | 151 ++++++++++++++++ 6 files changed, 352 insertions(+), 8 deletions(-) create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java create mode 100644 libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index e2cd7a0d1d776..d8e2f5c4a817e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -54,6 +54,7 @@ import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler; +import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.freeform.FreeformComponents; import com.android.wm.shell.freeform.FreeformTaskListener; @@ -677,13 +678,15 @@ public abstract class WMShellModule { SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, Transitions transitions, - EnterDesktopTaskTransitionHandler transitionHandler, + EnterDesktopTaskTransitionHandler enterDesktopTransitionHandler, + ExitDesktopTaskTransitionHandler exitDesktopTransitionHandler, @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, @ShellMainThread ShellExecutor mainExecutor ) { return new DesktopTasksController(context, shellInit, shellController, displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, transitions, - transitionHandler, desktopModeTaskRepository, mainExecutor); + enterDesktopTransitionHandler, exitDesktopTransitionHandler, + desktopModeTaskRepository, mainExecutor); } @WMSingleton @@ -693,6 +696,15 @@ public abstract class WMShellModule { return new EnterDesktopTaskTransitionHandler(transitions); } + @WMSingleton + @Provides + static ExitDesktopTaskTransitionHandler provideExitDesktopTaskTransitionHandler( + Transitions transitions, + Context context + ) { + return new ExitDesktopTaskTransitionHandler(transitions, context); + } + @WMSingleton @Provides @DynamicOverride diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index cb04a43e72052..c35cd5a8be028 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -68,7 +68,8 @@ class DesktopTasksController( private val syncQueue: SyncTransactionQueue, private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, private val transitions: Transitions, - private val animationTransitionHandler: EnterDesktopTaskTransitionHandler, + private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler, + private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler, private val desktopModeTaskRepository: DesktopModeTaskRepository, @ShellMainThread private val mainExecutor: ShellExecutor ) : RemoteCallable, Transitions.TransitionHandler { @@ -149,7 +150,7 @@ class DesktopTasksController( wct.setBounds(taskInfo.token, startBounds) if (Transitions.ENABLE_SHELL_TRANSITIONS) { - animationTransitionHandler.startTransition( + enterDesktopTaskTransitionHandler.startTransition( Transitions.TRANSIT_ENTER_FREEFORM, wct) } else { shellTaskOrganizer.applyTransaction(wct) @@ -167,7 +168,8 @@ class DesktopTasksController( wct.setBounds(taskInfo.token, freeformBounds) if (Transitions.ENABLE_SHELL_TRANSITIONS) { - animationTransitionHandler.startTransition(Transitions.TRANSIT_ENTER_DESKTOP_MODE, wct) + enterDesktopTaskTransitionHandler.startTransition( + Transitions.TRANSIT_ENTER_DESKTOP_MODE, wct) } else { shellTaskOrganizer.applyTransaction(wct) } @@ -191,6 +193,18 @@ class DesktopTasksController( } } + fun moveToFullscreenWithAnimation(task: ActivityManager.RunningTaskInfo) { + val wct = WindowContainerTransaction() + addMoveToFullscreenChanges(wct, task.token) + + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + exitDesktopTaskTransitionHandler.startTransition( + Transitions.TRANSIT_EXIT_DESKTOP_MODE, wct) + } else { + shellTaskOrganizer.applyTransaction(wct) + } + } + /** Move a task to the front **/ fun moveTaskToFront(taskInfo: ActivityManager.RunningTaskInfo) { val wct = WindowContainerTransaction() @@ -396,9 +410,9 @@ class DesktopTasksController( val statusBarHeight = displayController .getDisplayLayout(taskInfo.displayId)?.stableInsets()?.top ?: 0 if (y <= statusBarHeight && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) { - moveToFullscreen(taskInfo.taskId) visualIndicator?.releaseFullscreenIndicator() visualIndicator = null + moveToFullscreenWithAnimation(taskInfo) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java new file mode 100644 index 0000000000000..d18e98af0988f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java @@ -0,0 +1,162 @@ +/* + * 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.desktopmode; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + +import android.animation.ValueAnimator; +import android.app.ActivityManager; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.IBinder; +import android.util.DisplayMetrics; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.transition.Transitions; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + + +/** + * The {@link Transitions.TransitionHandler} that handles transitions for desktop mode tasks + * entering and exiting freeform. + */ +public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionHandler { + private static final int FULLSCREEN_ANIMATION_DURATION = 336; + private final Context mContext; + private final Transitions mTransitions; + private final List mPendingTransitionTokens = new ArrayList<>(); + + private Supplier mTransactionSupplier; + + public ExitDesktopTaskTransitionHandler( + Transitions transitions, + Context context) { + this(transitions, SurfaceControl.Transaction::new, context); + } + + private ExitDesktopTaskTransitionHandler( + Transitions transitions, + Supplier supplier, + Context context) { + mTransitions = transitions; + mTransactionSupplier = supplier; + mContext = context; + } + + /** + * Starts Transition of a given type + * @param type Transition type + * @param wct WindowContainerTransaction for transition + */ + public void startTransition(@WindowManager.TransitionType int type, + @NonNull WindowContainerTransaction wct) { + final IBinder token = mTransitions.startTransition(type, wct, this); + mPendingTransitionTokens.add(token); + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + boolean transitionHandled = false; + for (TransitionInfo.Change change : info.getChanges()) { + if ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0) { + continue; + } + + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo == null || taskInfo.taskId == -1) { + continue; + } + + if (change.getMode() == WindowManager.TRANSIT_CHANGE) { + transitionHandled |= startChangeTransition( + transition, info.getType(), change, startT, finishCallback); + } + } + + mPendingTransitionTokens.remove(transition); + + return transitionHandled; + } + + @VisibleForTesting + boolean startChangeTransition( + @NonNull IBinder transition, + @WindowManager.TransitionType int type, + @NonNull TransitionInfo.Change change, + @NonNull SurfaceControl.Transaction startT, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (!mPendingTransitionTokens.contains(transition)) { + return false; + } + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (type == Transitions.TRANSIT_EXIT_DESKTOP_MODE + && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { + // This Transition animates a task to fullscreen after being dragged to status bar + final Resources resources = mContext.getResources(); + final DisplayMetrics metrics = resources.getDisplayMetrics(); + final int screenWidth = metrics.widthPixels; + final int screenHeight = metrics.heightPixels; + final SurfaceControl sc = change.getLeash(); + startT.setCrop(sc, null); + startT.apply(); + final ValueAnimator animator = new ValueAnimator(); + animator.setFloatValues(0f, 1f); + animator.setDuration(FULLSCREEN_ANIMATION_DURATION); + final Rect startBounds = change.getStartAbsBounds(); + final float scaleX = (float) startBounds.width() / screenWidth; + final float scaleY = (float) startBounds.height() / screenHeight; + final SurfaceControl.Transaction t = mTransactionSupplier.get(); + Point startPos = new Point(startBounds.left, + startBounds.top); + animator.addUpdateListener(animation -> { + float fraction = animation.getAnimatedFraction(); + float currentScaleX = scaleX + ((1 - scaleX) * fraction); + float currentScaleY = scaleY + ((1 - scaleY) * fraction); + t.setPosition(sc, startPos.x * (1 - fraction), startPos.y * (1 - fraction)); + t.setScale(sc, currentScaleX, currentScaleY); + t.apply(); + }); + animator.start(); + return true; + } + + return false; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + return null; + } +} 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 beabf1842f8d3..1b1566300f654 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 @@ -140,6 +140,9 @@ public class Transitions implements RemoteCallable { /** Transition type to freeform in desktop mode. */ public static final int TRANSIT_ENTER_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 11; + /** Transition type to fullscreen from desktop mode. */ + public static final int TRANSIT_EXIT_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 12; + private final WindowOrganizer mOrganizer; private final Context mContext; private final ShellExecutor mMainExecutor; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 4ccc4678ac5e5..c9bd695ffb33c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -81,7 +81,8 @@ class DesktopTasksControllerTest : ShellTestCase() { @Mock lateinit var syncQueue: SyncTransactionQueue @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock lateinit var transitions: Transitions - @Mock lateinit var transitionHandler: EnterDesktopTaskTransitionHandler + @Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler + @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler lateinit var mockitoSession: StaticMockitoSession lateinit var controller: DesktopTasksController @@ -117,7 +118,8 @@ class DesktopTasksControllerTest : ShellTestCase() { syncQueue, rootTaskDisplayAreaOrganizer, transitions, - transitionHandler, + enterDesktopTransitionHandler, + exitDesktopTransitionHandler, desktopModeTaskRepository, TestShellExecutor() ) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java new file mode 100644 index 0000000000000..2c5a5cd72c539 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java @@ -0,0 +1,151 @@ +/* + * 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.desktopmode; + + +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + +import static androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.annotation.NonNull; +import android.app.ActivityManager; +import android.app.WindowConfiguration; +import android.content.Context; +import android.content.res.Resources; +import android.os.IBinder; +import android.util.DisplayMetrics; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.IWindowContainerToken; +import android.window.TransitionInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.transition.Transitions; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.function.Supplier; + +/** Tests of {@link com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler} */ +@SmallTest +public class ExitDesktopTaskTransitionHandlerTest extends ShellTestCase { + + @Mock + private Transitions mTransitions; + @Mock + IBinder mToken; + @Mock + Supplier mTransactionFactory; + @Mock + Context mContext; + @Mock + DisplayMetrics mDisplayMetrics; + @Mock + Resources mResources; + @Mock + SurfaceControl.Transaction mStartT; + @Mock + SurfaceControl.Transaction mFinishT; + @Mock + SurfaceControl.Transaction mAnimationT; + @Mock + Transitions.TransitionFinishCallback mTransitionFinishCallback; + @Mock + ShellExecutor mExecutor; + + private ExitDesktopTaskTransitionHandler mExitDesktopTaskTransitionHandler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + doReturn(mExecutor).when(mTransitions).getMainExecutor(); + doReturn(mAnimationT).when(mTransactionFactory).get(); + doReturn(mResources).when(mContext).getResources(); + doReturn(mDisplayMetrics).when(mResources).getDisplayMetrics(); + when(mResources.getDisplayMetrics()) + .thenReturn(getContext().getResources().getDisplayMetrics()); + + mExitDesktopTaskTransitionHandler = new ExitDesktopTaskTransitionHandler(mTransitions, + mContext); + } + + @Test + public void testTransitExitDesktopModeAnimation() throws Throwable { + final int transitionType = Transitions.TRANSIT_EXIT_DESKTOP_MODE; + final int taskId = 1; + WindowContainerTransaction wct = new WindowContainerTransaction(); + doReturn(mToken).when(mTransitions) + .startTransition(transitionType, wct, mExitDesktopTaskTransitionHandler); + + mExitDesktopTaskTransitionHandler.startTransition(transitionType, wct); + + TransitionInfo.Change change = + createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FULLSCREEN); + TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_EXIT_DESKTOP_MODE, change); + ArrayList exceptions = new ArrayList<>(); + runOnUiThread(() -> { + try { + assertTrue(mExitDesktopTaskTransitionHandler + .startAnimation(mToken, info, mStartT, mFinishT, + mTransitionFinishCallback)); + } catch (Exception e) { + exceptions.add(e); + } + }); + if (!exceptions.isEmpty()) { + throw exceptions.get(0); + } + } + + private TransitionInfo.Change createChange(@WindowManager.TransitionType int type, int taskId, + @WindowConfiguration.WindowingMode int windowingMode) { + final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.taskId = taskId; + taskInfo.token = new WindowContainerToken(mock(IWindowContainerToken.class)); + taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); + SurfaceControl.Builder b = new SurfaceControl.Builder() + .setName("test task"); + final TransitionInfo.Change change = new TransitionInfo.Change( + taskInfo.token, b.build()); + change.setMode(type); + change.setTaskInfo(taskInfo); + return change; + } + + private static TransitionInfo createTransitionInfo( + @WindowManager.TransitionType int type, @NonNull TransitionInfo.Change change) { + TransitionInfo info = new TransitionInfo(type, 0); + info.addChange(change); + return info; + } + +}