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 eb7c32fe8227c..f8743ed23aaac 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.DesktopModeController; 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.draganddrop.DragAndDropController; import com.android.wm.shell.freeform.FreeformComponents; import com.android.wm.shell.freeform.FreeformTaskListener; @@ -676,12 +677,20 @@ public abstract class WMShellModule { SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, Transitions transitions, + EnterDesktopTaskTransitionHandler transitionHandler, @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, @ShellMainThread ShellExecutor mainExecutor ) { return new DesktopTasksController(context, shellInit, shellController, displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, transitions, - desktopModeTaskRepository, mainExecutor); + transitionHandler, desktopModeTaskRepository, mainExecutor); + } + + @WMSingleton + @Provides + static EnterDesktopTaskTransitionHandler provideEnterDesktopModeTaskTransitionHandler( + Transitions transitions) { + return new EnterDesktopTaskTransitionHandler(transitions); } @WMSingleton 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 5696dfc5069e7..3d70d52ea1d11 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 @@ -25,6 +25,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.app.WindowConfiguration.WindowingMode import android.content.Context +import android.graphics.Rect import android.os.IBinder import android.os.SystemProperties import android.view.SurfaceControl @@ -67,6 +68,7 @@ class DesktopTasksController( private val syncQueue: SyncTransactionQueue, private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, private val transitions: Transitions, + private val animationTransitionHandler: EnterDesktopTaskTransitionHandler, private val desktopModeTaskRepository: DesktopModeTaskRepository, @ShellMainThread private val mainExecutor: ShellExecutor ) : RemoteCallable, Transitions.TransitionHandler { @@ -133,6 +135,44 @@ class DesktopTasksController( } } + /** + * Moves a single task to freeform and sets the taskBounds to the passed in bounds, + * startBounds + */ + fun moveToFreeform( + taskInfo: RunningTaskInfo, + startBounds: Rect + ) { + val wct = WindowContainerTransaction() + moveHomeTaskToFront(wct) + addMoveToDesktopChanges(wct, taskInfo.getToken()) + wct.setBounds(taskInfo.token, startBounds) + + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + animationTransitionHandler.startTransition( + Transitions.TRANSIT_ENTER_FREEFORM, wct) + } else { + shellTaskOrganizer.applyTransaction(wct) + } + } + + /** Brings apps to front and sets freeform task bounds */ + fun moveToDesktopWithAnimation( + taskInfo: RunningTaskInfo, + freeformBounds: Rect + ) { + val wct = WindowContainerTransaction() + bringDesktopAppsToFront(wct) + addMoveToDesktopChanges(wct, taskInfo.getToken()) + wct.setBounds(taskInfo.token, freeformBounds) + + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + animationTransitionHandler.startTransition(Transitions.TRANSIT_ENTER_DESKTOP_MODE, wct) + } else { + shellTaskOrganizer.applyTransaction(wct) + } + } + /** Move a task with given `taskId` to fullscreen */ fun moveToFullscreen(taskId: Int) { shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveToFullscreen(task) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java new file mode 100644 index 0000000000000..3df2340d45240 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java @@ -0,0 +1,185 @@ +/* + * 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_FREEFORM; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.app.ActivityManager; +import android.graphics.Rect; +import android.os.IBinder; +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.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 EnterDesktopTaskTransitionHandler implements Transitions.TransitionHandler { + + private final Transitions mTransitions; + private final Supplier mTransactionSupplier; + + // The size of the screen during drag relative to the fullscreen size + public static final float DRAG_FREEFORM_SCALE = 0.4f; + // The size of the screen after drag relative to the fullscreen size + public static final float FINAL_FREEFORM_SCALE = 0.6f; + public static final int FREEFORM_ANIMATION_DURATION = 336; + + private final List mPendingTransitionTokens = new ArrayList<>(); + + public EnterDesktopTaskTransitionHandler( + Transitions transitions) { + this(transitions, SurfaceControl.Transaction::new); + } + + public EnterDesktopTaskTransitionHandler( + Transitions transitions, + Supplier supplier) { + mTransitions = transitions; + mTransactionSupplier = supplier; + } + + /** + * 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; + } + + private 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_ENTER_FREEFORM + && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { + // Transitioning to freeform but keeping fullscreen bounds, so the crop is set + // to null and we don't require an animation + final SurfaceControl sc = change.getLeash(); + startT.setWindowCrop(sc, null); + startT.apply(); + mTransitions.getMainExecutor().execute( + () -> finishCallback.onTransitionFinished(null, null)); + return true; + } + + Rect endBounds = change.getEndAbsBounds(); + if (type == Transitions.TRANSIT_ENTER_DESKTOP_MODE + && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM + && !endBounds.isEmpty()) { + // This Transition animates a task to freeform bounds after being dragged into freeform + // mode and brings the remaining freeform tasks to front + final SurfaceControl sc = change.getLeash(); + startT.setWindowCrop(sc, endBounds.width(), + endBounds.height()); + startT.apply(); + + // We want to find the scale of the current bounds relative to the end bounds. The + // task is currently scaled to DRAG_FREEFORM_SCALE and the final bounds will be + // scaled to FINAL_FREEFORM_SCALE. So, it is scaled to + // DRAG_FREEFORM_SCALE / FINAL_FREEFORM_SCALE relative to the freeform bounds + final ValueAnimator animator = + ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE / FINAL_FREEFORM_SCALE, 1f); + animator.setDuration(FREEFORM_ANIMATION_DURATION); + final SurfaceControl.Transaction t = mTransactionSupplier.get(); + animator.addUpdateListener(animation -> { + final float animationValue = (float) animation.getAnimatedValue(); + t.setScale(sc, animationValue, animationValue); + + final float animationWidth = endBounds.width() * animationValue; + final float animationHeight = endBounds.height() * animationValue; + final int animationX = endBounds.centerX() - (int) (animationWidth / 2); + final int animationY = endBounds.centerY() - (int) (animationHeight / 2); + + t.setPosition(sc, animationX, animationY); + t.apply(); + }); + + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mTransitions.getMainExecutor().execute( + () -> finishCallback.onTransitionFinished(null, null)); + } + }); + + 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/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java index 60e5ff27cab9a..e1a56a1a5a7ae 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java @@ -112,6 +112,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs onChangeTransitionReady(change, startT, finishT); break; } + mWindowDecorViewModel.onTransitionReady(transition, info, change); } mTransitionToTaskInfo.put(transition, taskInfoList); } @@ -168,6 +169,8 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs } else { mTransitionToTaskInfo.put(playing, infoOfMerged); } + + mWindowDecorViewModel.onTransitionMerged(merged, playing); } @Override @@ -175,7 +178,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs final List taskInfo = mTransitionToTaskInfo.getOrDefault(transition, Collections.emptyList()); mTransitionToTaskInfo.remove(transition); - + mWindowDecorViewModel.onTransitionFinished(transition); for (int i = 0; i < taskInfo.size(); ++i) { mWindowDecorViewModel.destroyWindowDecoration(taskInfo.get(i)); } 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 b15802a0f5d48..beabf1842f8d3 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 @@ -134,6 +134,12 @@ public class Transitions implements RemoteCallable { /** Transition type for maximize to freeform transition. */ public static final int TRANSIT_RESTORE_FROM_MAXIMIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 9; + /** Transition type to freeform in desktop mode. */ + public static final int TRANSIT_ENTER_FREEFORM = WindowManager.TRANSIT_FIRST_CUSTOM + 10; + + /** Transition type to freeform in desktop mode. */ + public static final int TRANSIT_ENTER_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 11; + private final WindowOrganizer mOrganizer; private final Context mContext; private final ShellExecutor mMainExecutor; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 6b7ca421f5ed4..8e8facadfd5d4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -23,11 +23,13 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.os.Handler; +import android.os.IBinder; import android.util.SparseArray; import android.view.Choreographer; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; +import android.window.TransitionInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -71,6 +73,16 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } } + @Override + public void onTransitionReady(IBinder transition, TransitionInfo info, + TransitionInfo.Change change) {} + + @Override + public void onTransitionMerged(IBinder merged, IBinder playing) {} + + @Override + public void onTransitionFinished(IBinder transition) {} + @Override public void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter) { mTaskOperations = new TaskOperations(transitionStarter, mContext, mSyncQueue); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 317b9a322fbc0..7cacf5caa3a68 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -22,7 +22,11 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.DRAG_FREEFORM_SCALE; +import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FINAL_FREEFORM_SCALE; +import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FREEFORM_ANIMATION_DURATION; +import android.animation.ValueAnimator; import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityTaskManager; @@ -30,6 +34,7 @@ import android.content.Context; import android.graphics.Rect; import android.hardware.input.InputManager; import android.os.Handler; +import android.os.IBinder; import android.os.Looper; import android.util.SparseArray; import android.view.Choreographer; @@ -39,9 +44,13 @@ import android.view.InputEventReceiver; import android.view.InputMonitor; import android.view.MotionEvent; import android.view.SurfaceControl; +import android.view.SurfaceControl.Transaction; import android.view.View; +import android.view.WindowManager; +import android.window.TransitionInfo; import android.window.WindowContainerToken; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; @@ -54,8 +63,10 @@ import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.transition.Transitions; import java.util.Optional; +import java.util.function.Supplier; /** * View model for the window decoration with a caption and shadows. Works with @@ -83,9 +94,20 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl(); private final InputMonitorFactory mInputMonitorFactory; private TaskOperations mTaskOperations; + private final Supplier mTransactionFactory; private Optional mSplitScreenController; + private ValueAnimator mDragToDesktopValueAnimator; + private final Rect mDragToDesktopAnimationStartBounds = new Rect(); + private boolean mDragToDesktopAnimationStarted; + private float mCaptionDragStartX; + + // These values keep track of any transitions to freeform to stop relayout from running on + // changing task so that shellTransitions has a chance to animate the transition + private int mPauseRelayoutForTask = -1; + private IBinder mTransitionPausingRelayout; + public DesktopModeWindowDecorViewModel( Context context, Handler mainHandler, @@ -107,7 +129,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { desktopTasksController, splitScreenController, new DesktopModeWindowDecoration.Factory(), - new InputMonitorFactory()); + new InputMonitorFactory(), + SurfaceControl.Transaction::new); } @VisibleForTesting @@ -122,7 +145,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { Optional desktopTasksController, Optional splitScreenController, DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory, - InputMonitorFactory inputMonitorFactory) { + InputMonitorFactory inputMonitorFactory, + Supplier transactionFactory) { mContext = context; mMainHandler = mainHandler; mMainChoreographer = mainChoreographer; @@ -136,6 +160,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDesktopModeWindowDecorFactory = desktopModeWindowDecorFactory; mInputMonitorFactory = inputMonitorFactory; + mTransactionFactory = transactionFactory; } @Override @@ -154,6 +179,31 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { return true; } + @Override + public void onTransitionReady( + @NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change) { + if (change.getMode() == WindowManager.TRANSIT_CHANGE + && info.getType() == Transitions.TRANSIT_ENTER_DESKTOP_MODE) { + mTransitionPausingRelayout = transition; + } + } + + @Override + public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) { + if (mTransitionPausingRelayout.equals(merged)) { + mTransitionPausingRelayout = playing; + } + } + + @Override + public void onTransitionFinished(@NonNull IBinder transition) { + if (transition.equals(mTransitionPausingRelayout)) { + mPauseRelayoutForTask = -1; + } + } + @Override public void onTaskInfoChanged(RunningTaskInfo taskInfo) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); @@ -165,7 +215,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { incrementEventReceiverTasks(taskInfo.displayId); } - decoration.relayout(taskInfo); + // TaskListener callbacks and shell transitions aren't synchronized, so starting a shell + // transition can trigger an onTaskInfoChanged call that updates the task's SurfaceControl + // and interferes with the transition animation that is playing at the same time. + if (taskInfo.taskId != mPauseRelayoutForTask) { + decoration.relayout(taskInfo); + } } @Override @@ -295,7 +350,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { case MotionEvent.ACTION_DOWN: { mDragPointerId = e.getPointerId(0); mDragPositioningCallback.onDragPositioningStart( - 0 /* ctrlType */, e.getRawX(0), e.getRawY(0)); + 0 /* ctrlType */, e.getRawX(0), + e.getRawY(0)); mIsDragging = false; return false; } @@ -403,7 +459,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { final DesktopModeWindowDecoration relevantDecor = getRelevantWindowDecor(ev); if (DesktopModeStatus.isProto2Enabled()) { if (relevantDecor == null - || relevantDecor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM) { + || relevantDecor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM + || mTransitionDragActive) { handleCaptionThroughStatusBar(ev, relevantDecor); } } @@ -444,8 +501,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { DesktopModeWindowDecoration relevantDecor) { switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: { + mCaptionDragStartX = ev.getX(); // Begin drag through status bar if applicable. if (relevantDecor != null) { + mDragToDesktopAnimationStartBounds.set( + relevantDecor.mTaskInfo.configuration.windowConfiguration.getBounds()); boolean dragFromStatusBarAllowed = false; if (DesktopModeStatus.isProto2Enabled()) { // In proto2 any full screen task can be dragged to freeform @@ -461,33 +521,105 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } case MotionEvent.ACTION_UP: { if (relevantDecor == null) { + mDragToDesktopAnimationStarted = false; mTransitionDragActive = false; return; } if (mTransitionDragActive) { mTransitionDragActive = false; - final int statusBarHeight = mDisplayController - .getDisplayLayout(relevantDecor.mTaskInfo.displayId).stableInsets().top; + final int statusBarHeight = getStatusBarHeight( + relevantDecor.mTaskInfo.displayId); if (ev.getY() > statusBarHeight) { if (DesktopModeStatus.isProto2Enabled()) { + mPauseRelayoutForTask = relevantDecor.mTaskInfo.taskId; mDesktopTasksController.ifPresent( - c -> c.moveToDesktop(relevantDecor.mTaskInfo)); + c -> c.moveToDesktopWithAnimation(relevantDecor.mTaskInfo, + getFreeformBounds(ev))); } else if (DesktopModeStatus.isProto1Enabled()) { mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(true)); } - + mDragToDesktopAnimationStarted = false; + return; + } else if (mDragToDesktopAnimationStarted) { + mDesktopTasksController.ifPresent(c -> + c.moveToFullscreen(relevantDecor.mTaskInfo)); + mDragToDesktopAnimationStarted = false; return; } } relevantDecor.checkClickEvent(ev); break; } + + case MotionEvent.ACTION_MOVE: { + if (relevantDecor == null) { + return; + } + if (mTransitionDragActive) { + final int statusBarHeight = mDisplayController + .getDisplayLayout( + relevantDecor.mTaskInfo.displayId).stableInsets().top; + if (ev.getY() > statusBarHeight) { + if (!mDragToDesktopAnimationStarted) { + mDragToDesktopAnimationStarted = true; + mDesktopTasksController.ifPresent( + c -> c.moveToFreeform(relevantDecor.mTaskInfo, + mDragToDesktopAnimationStartBounds)); + startAnimation(relevantDecor); + } + } + if (mDragToDesktopAnimationStarted) { + Transaction t = mTransactionFactory.get(); + float width = (float) mDragToDesktopValueAnimator.getAnimatedValue() + * mDragToDesktopAnimationStartBounds.width(); + float x = ev.getX() - (width / 2); + t.setPosition(relevantDecor.mTaskSurface, x, ev.getY()); + t.apply(); + } + } + break; + } + case MotionEvent.ACTION_CANCEL: { mTransitionDragActive = false; + mDragToDesktopAnimationStarted = false; } } } + private Rect getFreeformBounds(@NonNull MotionEvent ev) { + final Rect endBounds = new Rect(); + final int finalWidth = (int) (FINAL_FREEFORM_SCALE + * mDragToDesktopAnimationStartBounds.width()); + final int finalHeight = (int) (FINAL_FREEFORM_SCALE + * mDragToDesktopAnimationStartBounds.height()); + + endBounds.left = mDragToDesktopAnimationStartBounds.centerX() - finalWidth / 2 + + (int) (ev.getX() - mCaptionDragStartX); + endBounds.right = endBounds.left + (int) (FINAL_FREEFORM_SCALE + * mDragToDesktopAnimationStartBounds.width()); + endBounds.top = (int) (ev.getY() + - ((FINAL_FREEFORM_SCALE - DRAG_FREEFORM_SCALE) + * mDragToDesktopAnimationStartBounds.height() / 2)); + endBounds.bottom = endBounds.top + finalHeight; + + return endBounds; + } + + private void startAnimation(@NonNull DesktopModeWindowDecoration focusedDecor) { + mDragToDesktopValueAnimator = ValueAnimator.ofFloat(1f, DRAG_FREEFORM_SCALE); + mDragToDesktopValueAnimator.setDuration(FREEFORM_ANIMATION_DURATION); + final Transaction t = mTransactionFactory.get(); + mDragToDesktopValueAnimator.addUpdateListener(animation -> { + final float animatorValue = (float) animation.getAnimatedValue(); + SurfaceControl sc = focusedDecor.mTaskSurface; + t.setScale(sc, animatorValue, animatorValue); + t.apply(); + }); + + mDragToDesktopValueAnimator.start(); + } + @Nullable private DesktopModeWindowDecoration getRelevantWindowDecor(MotionEvent ev) { if (mSplitScreenController.isPresent() @@ -534,6 +666,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { return focusedDecor; } + private int getStatusBarHeight(int displayId) { + return mDisplayController.getDisplayLayout(displayId).stableInsets().top; + } + private void createInputChannel(int displayId) { final InputManager inputManager = mContext.getSystemService(InputManager.class); final InputMonitor inputMonitor = diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java index 3734487e8f4b7..9f03d9aec1669 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java @@ -17,7 +17,9 @@ package com.android.wm.shell.windowdecor; import android.app.ActivityManager; +import android.os.IBinder; import android.view.SurfaceControl; +import android.window.TransitionInfo; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; @@ -95,4 +97,34 @@ public interface WindowDecorViewModel { * @param taskInfo the info of the task */ void destroyWindowDecoration(ActivityManager.RunningTaskInfo taskInfo); + + /** + * Notifies that a shell transition is about to start. If the transition is of type + * TRANSIT_ENTER_DESKTOP, it will save that transition to unpause relayout for the transitioning + * task after the transition has ended. + * + * @param transition the ready transaction + * @param info of Transition to check if relayout needs to be paused for a task + * @param change a change in the given transition + */ + default void onTransitionReady(IBinder transition, TransitionInfo info, + TransitionInfo.Change change) {} + + /** + * Notifies that a shell transition is about to merge with another to give the window + * decoration a chance to prepare for this merge. + * + * @param merged the transaction being merged + * @param playing the transaction being merged into + */ + default void onTransitionMerged(IBinder merged, IBinder playing) {} + + /** + * Notifies that a shell transition is about to finish to give the window decoration a chance + * to clean up. + * + * @param transaction + */ + default void onTransitionFinished(IBinder transaction) {} + } \ No newline at end of file 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 5cad50da7e423..4ccc4678ac5e5 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,6 +81,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Mock lateinit var syncQueue: SyncTransactionQueue @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock lateinit var transitions: Transitions + @Mock lateinit var transitionHandler: EnterDesktopTaskTransitionHandler lateinit var mockitoSession: StaticMockitoSession lateinit var controller: DesktopTasksController @@ -116,6 +117,7 @@ class DesktopTasksControllerTest : ShellTestCase() { syncQueue, rootTaskDisplayAreaOrganizer, transitions, + transitionHandler, desktopModeTaskRepository, TestShellExecutor() ) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java new file mode 100644 index 0000000000000..6199e0b050597 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java @@ -0,0 +1,159 @@ +/* + * 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_FREEFORM; + +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.verify; + +import android.annotation.NonNull; +import android.app.ActivityManager; +import android.app.WindowConfiguration; +import android.graphics.Rect; +import android.os.IBinder; +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.common.ShellExecutor; +import com.android.wm.shell.transition.Transitions; + +import junit.framework.AssertionFailedError; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.function.Supplier; + +/** Tests of {@link com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler} */ +@SmallTest +public class EnterDesktopTaskTransitionHandlerTest { + + @Mock + private Transitions mTransitions; + @Mock + IBinder mToken; + @Mock + Supplier mTransactionFactory; + @Mock + SurfaceControl.Transaction mStartT; + @Mock + SurfaceControl.Transaction mFinishT; + @Mock + SurfaceControl.Transaction mAnimationT; + @Mock + Transitions.TransitionFinishCallback mTransitionFinishCallback; + @Mock + ShellExecutor mExecutor; + @Mock + SurfaceControl mSurfaceControl; + + private EnterDesktopTaskTransitionHandler mEnterDesktopTaskTransitionHandler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + doReturn(mExecutor).when(mTransitions).getMainExecutor(); + doReturn(mAnimationT).when(mTransactionFactory).get(); + + mEnterDesktopTaskTransitionHandler = new EnterDesktopTaskTransitionHandler(mTransitions, + mTransactionFactory); + } + + @Test + public void testEnterFreeformAnimation() { + final int transitionType = Transitions.TRANSIT_ENTER_FREEFORM; + final int taskId = 1; + WindowContainerTransaction wct = new WindowContainerTransaction(); + doReturn(mToken).when(mTransitions) + .startTransition(transitionType, wct, mEnterDesktopTaskTransitionHandler); + mEnterDesktopTaskTransitionHandler.startTransition(transitionType, wct); + + TransitionInfo.Change change = + createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FREEFORM); + TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_ENTER_FREEFORM, change); + + + assertTrue(mEnterDesktopTaskTransitionHandler + .startAnimation(mToken, info, mStartT, mFinishT, mTransitionFinishCallback)); + + verify(mStartT).setWindowCrop(mSurfaceControl, null); + verify(mStartT).apply(); + } + + @Test + public void testTransitEnterDesktopModeAnimation() throws Throwable { + final int transitionType = Transitions.TRANSIT_ENTER_DESKTOP_MODE; + final int taskId = 1; + WindowContainerTransaction wct = new WindowContainerTransaction(); + doReturn(mToken).when(mTransitions) + .startTransition(transitionType, wct, mEnterDesktopTaskTransitionHandler); + mEnterDesktopTaskTransitionHandler.startTransition(transitionType, wct); + + TransitionInfo.Change change = + createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FREEFORM); + change.setEndAbsBounds(new Rect(0, 0, 1, 1)); + TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_ENTER_DESKTOP_MODE, change); + + runOnUiThread(() -> { + try { + assertTrue(mEnterDesktopTaskTransitionHandler + .startAnimation(mToken, info, mStartT, mFinishT, + mTransitionFinishCallback)); + } catch (Exception e) { + throw new AssertionFailedError(e.getMessage()); + } + }); + + verify(mStartT).setWindowCrop(mSurfaceControl, change.getEndAbsBounds().width(), + change.getEndAbsBounds().height()); + verify(mStartT).apply(); + } + + 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.configuration.windowConfiguration.setWindowingMode(windowingMode); + final TransitionInfo.Change change = new TransitionInfo.Change( + new WindowContainerToken(mock(IWindowContainerToken.class)), mSurfaceControl); + 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; + } + +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java index 1d1aa795173ce..9a90996b786c5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.app.WindowConfiguration; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.hardware.input.InputManager; @@ -60,6 +61,7 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; /** Tests of {@link DesktopModeWindowDecorViewModel} */ @SmallTest @@ -80,8 +82,9 @@ public class DesktopModeWindowDecorViewModelTests extends ShellTestCase { @Mock private DesktopTasksController mDesktopTasksController; @Mock private InputMonitor mInputMonitor; @Mock private InputManager mInputManager; - @Mock private DesktopModeWindowDecorViewModel.InputMonitorFactory mMockInputMonitorFactory; + @Mock private Supplier mTransactionFactory; + @Mock private SurfaceControl.Transaction mTransaction; private final List mMockInputManagers = new ArrayList<>(); private DesktopModeWindowDecorViewModel mDesktopModeWindowDecorViewModel; @@ -102,12 +105,14 @@ public class DesktopModeWindowDecorViewModelTests extends ShellTestCase { Optional.of(mDesktopTasksController), Optional.of(mSplitScreenController), mDesktopModeWindowDecorFactory, - mMockInputMonitorFactory + mMockInputMonitorFactory, + mTransactionFactory ); doReturn(mDesktopModeWindowDecoration) .when(mDesktopModeWindowDecorFactory) .create(any(), any(), any(), any(), any(), any(), any(), any()); + doReturn(mTransaction).when(mTransactionFactory).get(); when(mMockInputMonitorFactory.create(any(), any())).thenReturn(mInputMonitor); // InputChannel cannot be mocked because it passes to InputEventReceiver. @@ -250,7 +255,7 @@ public class DesktopModeWindowDecorViewModelTests extends ShellTestCase { } private static ActivityManager.RunningTaskInfo createTaskInfo(int taskId, - int displayId, int windowingMode) { + int displayId, @WindowConfiguration.WindowingMode int windowingMode) { ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() .setDisplayId(displayId)