Merge "Animation For Fullscreen -> Freeform Transition" into udc-dev

This commit is contained in:
Maryam Dehaini
2023-03-24 17:29:22 +00:00
committed by Android (Google) Code Review
11 changed files with 603 additions and 14 deletions

View File

@@ -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

View File

@@ -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<DesktopTasksController>, 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) }

View File

@@ -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<SurfaceControl.Transaction> 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<IBinder> mPendingTransitionTokens = new ArrayList<>();
public EnterDesktopTaskTransitionHandler(
Transitions transitions) {
this(transitions, SurfaceControl.Transaction::new);
}
public EnterDesktopTaskTransitionHandler(
Transitions transitions,
Supplier<SurfaceControl.Transaction> 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;
}
}

View File

@@ -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<ActivityManager.RunningTaskInfo> 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));
}

View File

@@ -134,6 +134,12 @@ public class Transitions implements RemoteCallable<Transitions> {
/** 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;

View File

@@ -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);

View File

@@ -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<SurfaceControl.Transaction> mTransactionFactory;
private Optional<SplitScreenController> 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> desktopTasksController,
Optional<SplitScreenController> splitScreenController,
DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory,
InputMonitorFactory inputMonitorFactory) {
InputMonitorFactory inputMonitorFactory,
Supplier<SurfaceControl.Transaction> 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 =

View File

@@ -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) {}
}

View File

@@ -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()
)

View File

@@ -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<SurfaceControl.Transaction> 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;
}
}

View File

@@ -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<SurfaceControl.Transaction> mTransactionFactory;
@Mock private SurfaceControl.Transaction mTransaction;
private final List<InputManager> 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)