Add split screen unfold animation

Creates controllers in the Shell that manipulate
SurfaceControl leashes of the current active
split screen surfaces based on the current
unfold transition progress.

Bug: 193795566
Test: manual
Change-Id: Ib80807c632444b2939e9b075fb24fd40cc14648a
This commit is contained in:
Nick Chameyev
2021-10-05 19:42:20 +01:00
parent 49327062c2
commit f96ed6b6a1
15 changed files with 573 additions and 91 deletions

View File

@@ -16,9 +16,6 @@
package com.android.wm.shell.fullscreen;
import static android.graphics.Color.blue;
import static android.graphics.Color.green;
import static android.graphics.Color.red;
import static android.util.MathUtils.lerp;
import static android.view.Display.DEFAULT_DISPLAY;
@@ -36,12 +33,11 @@ import android.view.InsetsState;
import android.view.SurfaceControl;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.wm.shell.R;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
import com.android.wm.shell.unfold.UnfoldBackgroundController;
import java.util.concurrent.Executor;
@@ -59,21 +55,17 @@ public final class FullscreenUnfoldController implements UnfoldListener,
private static final float VERTICAL_START_MARGIN = 0.03f;
private static final float END_SCALE = 1f;
private static final float START_SCALE = END_SCALE - VERTICAL_START_MARGIN * 2;
private static final int BACKGROUND_LAYER_Z_INDEX = -1;
private final Context mContext;
private final Executor mExecutor;
private final ShellUnfoldProgressProvider mProgressProvider;
private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
private final DisplayInsetsController mDisplayInsetsController;
private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>();
private final UnfoldBackgroundController mBackgroundController;
private SurfaceControl mBackgroundLayer;
private InsetsSource mTaskbarInsetsSource;
private final float mWindowCornerRadiusPx;
private final float[] mBackgroundColor;
private final float mExpandedTaskBarHeight;
private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
@@ -81,19 +73,17 @@ public final class FullscreenUnfoldController implements UnfoldListener,
public FullscreenUnfoldController(
@NonNull Context context,
@NonNull Executor executor,
@NonNull UnfoldBackgroundController backgroundController,
@NonNull ShellUnfoldProgressProvider progressProvider,
@NonNull RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
@NonNull DisplayInsetsController displayInsetsController
) {
mContext = context;
mExecutor = executor;
mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
mProgressProvider = progressProvider;
mDisplayInsetsController = displayInsetsController;
mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context);
mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.taskbar_frame_height);
mBackgroundColor = getBackgroundColor();
mBackgroundController = backgroundController;
}
/**
@@ -108,7 +98,7 @@ public final class FullscreenUnfoldController implements UnfoldListener,
public void onStateChangeProgress(float progress) {
if (mAnimationContextByTaskId.size() == 0) return;
ensureBackground();
mBackgroundController.ensureBackground(mTransaction);
for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
@@ -135,7 +125,7 @@ public final class FullscreenUnfoldController implements UnfoldListener,
resetSurface(context);
}
removeBackground();
mBackgroundController.removeBackground(mTransaction);
mTransaction.apply();
}
@@ -178,7 +168,7 @@ public final class FullscreenUnfoldController implements UnfoldListener,
}
if (mAnimationContextByTaskId.size() == 0) {
removeBackground();
mBackgroundController.removeBackground(mTransaction);
}
mTransaction.apply();
@@ -194,39 +184,6 @@ public final class FullscreenUnfoldController implements UnfoldListener,
(float) context.mTaskInfo.positionInParent.y);
}
private void ensureBackground() {
if (mBackgroundLayer != null) return;
SurfaceControl.Builder colorLayerBuilder = new SurfaceControl.Builder()
.setName("app-unfold-background")
.setCallsite("AppUnfoldTransitionController")
.setColorLayer();
mRootTaskDisplayAreaOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, colorLayerBuilder);
mBackgroundLayer = colorLayerBuilder.build();
mTransaction
.setColor(mBackgroundLayer, mBackgroundColor)
.show(mBackgroundLayer)
.setLayer(mBackgroundLayer, BACKGROUND_LAYER_Z_INDEX);
}
private void removeBackground() {
if (mBackgroundLayer == null) return;
if (mBackgroundLayer.isValid()) {
mTransaction.remove(mBackgroundLayer);
}
mBackgroundLayer = null;
}
private float[] getBackgroundColor() {
int colorInt = mContext.getResources().getColor(R.color.unfold_transition_background);
return new float[]{
(float) red(colorInt) / 255.0F,
(float) green(colorInt) / 255.0F,
(float) blue(colorInt) / 255.0F
};
}
private class AnimationContext {
final SurfaceControl mLeash;
final Rect mStartCropRect = new Rect();

View File

@@ -18,6 +18,7 @@ package com.android.wm.shell.splitscreen;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import android.annotation.Nullable;
import android.graphics.Rect;
import android.view.SurfaceSession;
import android.window.WindowContainerToken;
@@ -38,8 +39,10 @@ class MainStage extends StageTaskListener {
MainStage(ShellTaskOrganizer taskOrganizer, int displayId,
StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
SurfaceSession surfaceSession) {
super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession);
SurfaceSession surfaceSession,
@Nullable StageTaskUnfoldController stageTaskUnfoldController) {
super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
stageTaskUnfoldController);
}
boolean isActive() {

View File

@@ -46,8 +46,10 @@ class SideStage extends StageTaskListener implements
SideStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
SurfaceSession surfaceSession) {
super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession);
SurfaceSession surfaceSession,
@Nullable StageTaskUnfoldController stageTaskUnfoldController) {
super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
stageTaskUnfoldController);
mContext = context;
}

View File

@@ -68,8 +68,11 @@ import com.android.wm.shell.transition.Transitions;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.Executor;
import javax.inject.Provider;
/**
* Class manages split-screen multitasking mode and implements the main interface
* {@link SplitScreen}.
@@ -91,6 +94,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
private final Transitions mTransitions;
private final TransactionPool mTransactionPool;
private final SplitscreenEventLogger mLogger;
private final Provider<Optional<StageTaskUnfoldController>> mUnfoldControllerProvider;
private StageCoordinator mStageCoordinator;
@@ -99,7 +103,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
RootTaskDisplayAreaOrganizer rootTDAOrganizer,
ShellExecutor mainExecutor, DisplayImeController displayImeController,
DisplayInsetsController displayInsetsController,
Transitions transitions, TransactionPool transactionPool) {
Transitions transitions, TransactionPool transactionPool,
Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
mTaskOrganizer = shellTaskOrganizer;
mSyncQueue = syncQueue;
mContext = context;
@@ -109,6 +114,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
mDisplayInsetsController = displayInsetsController;
mTransitions = transitions;
mTransactionPool = transactionPool;
mUnfoldControllerProvider = unfoldControllerProvider;
mLogger = new SplitscreenEventLogger();
}
@@ -131,7 +137,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
// TODO: Multi-display
mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController,
mDisplayInsetsController, mTransitions, mTransactionPool, mLogger);
mDisplayInsetsController, mTransitions, mTransactionPool, mLogger,
mUnfoldControllerProvider);
}
}

View File

@@ -95,6 +95,9 @@ import com.android.wm.shell.transition.Transitions;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.inject.Provider;
/**
* Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and
@@ -121,8 +124,10 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private final MainStage mMainStage;
private final StageListenerImpl mMainStageListener = new StageListenerImpl();
private final StageTaskUnfoldController mMainUnfoldController;
private final SideStage mSideStage;
private final StageListenerImpl mSideStageListener = new StageListenerImpl();
private final StageTaskUnfoldController mSideUnfoldController;
@SplitPosition
private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT;
@@ -179,26 +184,32 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
DisplayImeController displayImeController,
DisplayInsetsController displayInsetsController, Transitions transitions,
TransactionPool transactionPool, SplitscreenEventLogger logger) {
TransactionPool transactionPool, SplitscreenEventLogger logger,
Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
mContext = context;
mDisplayId = displayId;
mSyncQueue = syncQueue;
mRootTDAOrganizer = rootTDAOrganizer;
mTaskOrganizer = taskOrganizer;
mLogger = logger;
mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
mMainStage = new MainStage(
mTaskOrganizer,
mDisplayId,
mMainStageListener,
mSyncQueue,
mSurfaceSession);
mSurfaceSession,
mMainUnfoldController);
mSideStage = new SideStage(
mContext,
mTaskOrganizer,
mDisplayId,
mSideStageListener,
mSyncQueue,
mSurfaceSession);
mSurfaceSession,
mSideUnfoldController);
mDisplayImeController = displayImeController;
mDisplayInsetsController = displayInsetsController;
mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSideStage);
@@ -218,7 +229,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
MainStage mainStage, SideStage sideStage, DisplayImeController displayImeController,
DisplayInsetsController displayInsetsController, SplitLayout splitLayout,
Transitions transitions, TransactionPool transactionPool,
SplitscreenEventLogger logger) {
SplitscreenEventLogger logger,
Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
mContext = context;
mDisplayId = displayId;
mSyncQueue = syncQueue;
@@ -232,6 +244,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSplitLayout = splitLayout;
mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions,
mOnTransitionAnimationComplete);
mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
mLogger = logger;
transitions.addHandler(this);
}
@@ -460,6 +474,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
onLayoutChanged(mSplitLayout);
} else {
updateWindowBounds(mSplitLayout, wct);
updateUnfoldBounds();
}
}
}
@@ -603,6 +618,11 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
final SplitScreen.SplitScreenListener l = mListeners.get(i);
l.onSplitVisibilityChanged(mDividerVisible);
}
if (mMainUnfoldController != null && mSideUnfoldController != null) {
mMainUnfoldController.onSplitVisibilityChanged(mDividerVisible);
mSideUnfoldController.onSplitVisibilityChanged(mDividerVisible);
}
}
private void onStageRootTaskAppeared(StageListenerImpl stageListener) {
@@ -641,6 +661,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mDividerVisible = visible;
if (visible) {
mSplitLayout.init();
updateUnfoldBounds();
} else {
mSplitLayout.release();
}
@@ -784,12 +805,20 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
public void onLayoutChanged(SplitLayout layout) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
updateWindowBounds(layout, wct);
updateUnfoldBounds();
mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t));
mSideStage.setOutlineVisibility(true);
mLogger.logResize(mSplitLayout.getDividerPositionAsFraction());
}
private void updateUnfoldBounds() {
if (mMainUnfoldController != null && mSideUnfoldController != null) {
mMainUnfoldController.onLayoutChanged(getMainStageBounds());
mSideUnfoldController.onLayoutChanged(getSideStageBounds());
}
}
/**
* Populates `wct` with operations that match the split windows to the current layout.
* To match relevant surfaces, make sure to call updateSurfaceBounds after `wct` is applied
@@ -846,6 +875,11 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mDisplayAreaInfo.configuration, this, mParentContainerCallbacks,
mDisplayImeController, mTaskOrganizer);
mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout);
if (mMainUnfoldController != null && mSideUnfoldController != null) {
mMainUnfoldController.init();
mSideUnfoldController.init();
}
}
}

View File

@@ -24,6 +24,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
import android.annotation.CallSuper;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.graphics.Point;
import android.graphics.Rect;
@@ -80,12 +81,16 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
protected SparseArray<ActivityManager.RunningTaskInfo> mChildrenTaskInfo = new SparseArray<>();
private final SparseArray<SurfaceControl> mChildrenLeashes = new SparseArray<>();
private final StageTaskUnfoldController mStageTaskUnfoldController;
StageTaskListener(ShellTaskOrganizer taskOrganizer, int displayId,
StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
SurfaceSession surfaceSession) {
SurfaceSession surfaceSession,
@Nullable StageTaskUnfoldController stageTaskUnfoldController) {
mCallbacks = callbacks;
mSyncQueue = syncQueue;
mSurfaceSession = surfaceSession;
mStageTaskUnfoldController = stageTaskUnfoldController;
taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this);
}
@@ -158,6 +163,10 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
+ "\n mRootTaskInfo: " + mRootTaskInfo);
}
if (mStageTaskUnfoldController != null) {
mStageTaskUnfoldController.onTaskAppeared(taskInfo, leash);
}
}
@Override
@@ -210,6 +219,10 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
+ "\n mRootTaskInfo: " + mRootTaskInfo);
}
if (mStageTaskUnfoldController != null) {
mStageTaskUnfoldController.onTaskVanished(taskInfo);
}
}
@Override

View File

@@ -0,0 +1,224 @@
/*
* 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.splitscreen;
import static android.view.Display.DEFAULT_DISPLAY;
import android.animation.RectEvaluator;
import android.animation.TypeEvaluator;
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Rect;
import android.util.SparseArray;
import android.view.InsetsSource;
import android.view.InsetsState;
import android.view.SurfaceControl;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
import com.android.wm.shell.unfold.UnfoldBackgroundController;
import java.util.concurrent.Executor;
/**
* Controls transformations of the split screen task surfaces in response
* to the unfolding/folding action on foldable devices
*/
public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChangedListener {
private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect());
private static final float CROPPING_START_MARGIN_FRACTION = 0.05f;
private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>();
private final ShellUnfoldProgressProvider mUnfoldProgressProvider;
private final DisplayInsetsController mDisplayInsetsController;
private final UnfoldBackgroundController mBackgroundController;
private final Executor mExecutor;
private final int mExpandedTaskBarHeight;
private final float mWindowCornerRadiusPx;
private final Rect mStageBounds = new Rect();
private final TransactionPool mTransactionPool;
private InsetsSource mTaskbarInsetsSource;
private boolean mBothStagesVisible;
public StageTaskUnfoldController(@NonNull Context context,
@NonNull TransactionPool transactionPool,
@NonNull ShellUnfoldProgressProvider unfoldProgressProvider,
@NonNull DisplayInsetsController displayInsetsController,
@NonNull UnfoldBackgroundController backgroundController,
@NonNull Executor executor) {
mUnfoldProgressProvider = unfoldProgressProvider;
mTransactionPool = transactionPool;
mExecutor = executor;
mBackgroundController = backgroundController;
mDisplayInsetsController = displayInsetsController;
mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context);
mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.taskbar_frame_height);
}
/**
* Initializes the controller, starts listening for the external events
*/
public void init() {
mUnfoldProgressProvider.addListener(mExecutor, this);
mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this);
}
@Override
public void insetsChanged(InsetsState insetsState) {
mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
AnimationContext context = mAnimationContextByTaskId.valueAt(i);
context.update();
}
}
/**
* Called when split screen task appeared
* @param taskInfo info for the appeared task
* @param leash surface leash for the appeared task
*/
public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
AnimationContext context = new AnimationContext(leash);
mAnimationContextByTaskId.put(taskInfo.taskId, context);
}
/**
* Called when a split screen task vanished
* @param taskInfo info for the vanished task
*/
public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
AnimationContext context = mAnimationContextByTaskId.get(taskInfo.taskId);
if (context != null) {
final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
resetSurface(transaction, context);
transaction.apply();
mTransactionPool.release(transaction);
}
mAnimationContextByTaskId.remove(taskInfo.taskId);
}
@Override
public void onStateChangeProgress(float progress) {
if (mAnimationContextByTaskId.size() == 0 || !mBothStagesVisible) return;
final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
mBackgroundController.ensureBackground(transaction);
for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
AnimationContext context = mAnimationContextByTaskId.valueAt(i);
context.mCurrentCropRect.set(RECT_EVALUATOR
.evaluate(progress, context.mStartCropRect, context.mEndCropRect));
transaction.setWindowCrop(context.mLeash, context.mCurrentCropRect)
.setCornerRadius(context.mLeash, mWindowCornerRadiusPx);
}
transaction.apply();
mTransactionPool.release(transaction);
}
@Override
public void onStateChangeFinished() {
resetTransformations();
}
/**
* Called when split screen visibility changes
* @param bothStagesVisible true if both stages of the split screen are visible
*/
public void onSplitVisibilityChanged(boolean bothStagesVisible) {
mBothStagesVisible = bothStagesVisible;
if (!bothStagesVisible) {
resetTransformations();
}
}
/**
* Called when split screen stage bounds changed
* @param bounds new bounds for this stage
*/
public void onLayoutChanged(Rect bounds) {
mStageBounds.set(bounds);
for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
context.update();
}
}
private void resetTransformations() {
final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
resetSurface(transaction, context);
}
mBackgroundController.removeBackground(transaction);
transaction.apply();
mTransactionPool.release(transaction);
}
private void resetSurface(SurfaceControl.Transaction transaction, AnimationContext context) {
transaction
.setWindowCrop(context.mLeash, null)
.setCornerRadius(context.mLeash, 0.0F);
}
private class AnimationContext {
final SurfaceControl mLeash;
final Rect mStartCropRect = new Rect();
final Rect mEndCropRect = new Rect();
final Rect mCurrentCropRect = new Rect();
private AnimationContext(SurfaceControl leash) {
this.mLeash = leash;
update();
}
private void update() {
mStartCropRect.set(mStageBounds);
if (mTaskbarInsetsSource != null) {
// Only insets the cropping window with taskbar when taskbar is expanded
if (mTaskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
mStartCropRect.inset(mTaskbarInsetsSource
.calculateVisibleInsets(mStartCropRect));
}
}
// Offset to surface coordinates as layout bounds are in screen coordinates
mStartCropRect.offsetTo(0, 0);
mEndCropRect.set(mStartCropRect);
int maxSize = Math.max(mEndCropRect.width(), mEndCropRect.height());
int margin = (int) (maxSize * CROPPING_START_MARGIN_FRACTION);
mStartCropRect.inset(margin, margin, margin, margin);
}
}
}

View File

@@ -0,0 +1,89 @@
/*
* 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.unfold;
import static android.graphics.Color.blue;
import static android.graphics.Color.green;
import static android.graphics.Color.red;
import static android.view.Display.DEFAULT_DISPLAY;
import android.annotation.NonNull;
import android.content.Context;
import android.view.SurfaceControl;
import com.android.wm.shell.R;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
/**
* Controls background color layer for the unfold animations
*/
public class UnfoldBackgroundController {
private static final int BACKGROUND_LAYER_Z_INDEX = -1;
private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
private final float[] mBackgroundColor;
private SurfaceControl mBackgroundLayer;
public UnfoldBackgroundController(
@NonNull Context context,
@NonNull RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
mBackgroundColor = getBackgroundColor(context);
}
/**
* Ensures that unfold animation background color layer is present,
* @param transaction where we should add the background if it is not added
*/
public void ensureBackground(@NonNull SurfaceControl.Transaction transaction) {
if (mBackgroundLayer != null) return;
SurfaceControl.Builder colorLayerBuilder = new SurfaceControl.Builder()
.setName("app-unfold-background")
.setCallsite("AppUnfoldTransitionController")
.setColorLayer();
mRootTaskDisplayAreaOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, colorLayerBuilder);
mBackgroundLayer = colorLayerBuilder.build();
transaction
.setColor(mBackgroundLayer, mBackgroundColor)
.show(mBackgroundLayer)
.setLayer(mBackgroundLayer, BACKGROUND_LAYER_Z_INDEX);
}
/**
* Ensures that the background is not visible
* @param transaction as part of which the removal will happen if needed
*/
public void removeBackground(@NonNull SurfaceControl.Transaction transaction) {
if (mBackgroundLayer == null) return;
if (mBackgroundLayer.isValid()) {
transaction.remove(mBackgroundLayer);
}
mBackgroundLayer = null;
}
private float[] getBackgroundColor(Context context) {
int colorInt = context.getResources().getColor(R.color.unfold_transition_background);
return new float[]{
(float) red(colorInt) / 255.0F,
(float) green(colorInt) / 255.0F,
(float) blue(colorInt) / 255.0F
};
}
}

View File

@@ -56,7 +56,7 @@ public class MainStageTests {
MockitoAnnotations.initMocks(this);
mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
mMainStage = new MainStage(mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, mSyncQueue,
mSurfaceSession);
mSurfaceSession, null);
mMainStage.onTaskAppeared(mRootTaskInfo, mRootLeash);
}

View File

@@ -64,7 +64,7 @@ public class SideStageTests extends ShellTestCase {
MockitoAnnotations.initMocks(this);
mRootTask = new TestRunningTaskInfoBuilder().build();
mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks,
mSyncQueue, mSurfaceSession);
mSyncQueue, mSurfaceSession, null);
mSideStage.onTaskAppeared(mRootTask, mRootLeash);
}

View File

@@ -18,7 +18,6 @@ package com.android.wm.shell.splitscreen;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -39,6 +38,10 @@ import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.split.SplitLayout;
import com.android.wm.shell.transition.Transitions;
import java.util.Optional;
import javax.inject.Provider;
public class SplitTestUtils {
static SplitLayout createMockSplitLayout() {
@@ -68,10 +71,11 @@ public class SplitTestUtils {
MainStage mainStage, SideStage sideStage, DisplayImeController imeController,
DisplayInsetsController insetsController, SplitLayout splitLayout,
Transitions transitions, TransactionPool transactionPool,
SplitscreenEventLogger logger) {
SplitscreenEventLogger logger,
Provider<Optional<StageTaskUnfoldController>> unfoldController) {
super(context, displayId, syncQueue, rootTDAOrganizer, taskOrganizer, mainStage,
sideStage, imeController, insetsController, splitLayout, transitions,
transactionPool, logger);
transactionPool, logger, unfoldController);
// Prepare default TaskDisplayArea for testing.
mDisplayAreaInfo = new DisplayAreaInfo(

View File

@@ -74,6 +74,8 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;
import java.util.Optional;
/** Tests for {@link StageCoordinator} */
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -106,16 +108,16 @@ public class SplitTransitionTests extends ShellTestCase {
doReturn(mock(SurfaceControl.Transaction.class)).when(mTransactionPool).acquire();
mSplitLayout = SplitTestUtils.createMockSplitLayout();
mMainStage = new MainStage(mTaskOrganizer, DEFAULT_DISPLAY, mock(
StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession);
StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession, null);
mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession);
StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession, null);
mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage,
mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions,
mTransactionPool,
mLogger);
mLogger, Optional::empty);
mSplitScreenTransitions = mStageCoordinator.getSplitTransitions();
doAnswer((Answer<IBinder>) invocation -> mock(IBinder.class))
.when(mTransitions).startTransition(anyInt(), any(), any());

View File

@@ -18,18 +18,20 @@ package com.android.wm.shell.splitscreen;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME;
import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
import android.graphics.Rect;
import android.window.DisplayAreaInfo;
import android.window.WindowContainerTransaction;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -43,6 +45,7 @@ import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.split.SplitLayout;
import com.android.wm.shell.transition.Transitions;
import org.junit.Before;
@@ -51,29 +54,55 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/** Tests for {@link StageCoordinator} */
import java.util.Optional;
import javax.inject.Provider;
/**
* Tests for {@link StageCoordinator}
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
public class StageCoordinatorTests extends ShellTestCase {
@Mock private ShellTaskOrganizer mTaskOrganizer;
@Mock private SyncTransactionQueue mSyncQueue;
@Mock private RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
@Mock private MainStage mMainStage;
@Mock private SideStage mSideStage;
@Mock private DisplayImeController mDisplayImeController;
@Mock private DisplayInsetsController mDisplayInsetsController;
@Mock private Transitions mTransitions;
@Mock private TransactionPool mTransactionPool;
@Mock private SplitscreenEventLogger mLogger;
@Mock
private ShellTaskOrganizer mTaskOrganizer;
@Mock
private SyncTransactionQueue mSyncQueue;
@Mock
private RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
@Mock
private MainStage mMainStage;
@Mock
private SideStage mSideStage;
@Mock
private StageTaskUnfoldController mMainUnfoldController;
@Mock
private StageTaskUnfoldController mSideUnfoldController;
@Mock
private SplitLayout mSplitLayout;
@Mock
private DisplayImeController mDisplayImeController;
@Mock
private DisplayInsetsController mDisplayInsetsController;
@Mock
private Transitions mTransitions;
@Mock
private TransactionPool mTransactionPool;
@Mock
private SplitscreenEventLogger mLogger;
private final Rect mBounds1 = new Rect(10, 20, 30, 40);
private final Rect mBounds2 = new Rect(5, 10, 15, 20);
private StageCoordinator mStageCoordinator;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage,
mDisplayImeController, mDisplayInsetsController, null /* splitLayout */,
mTransitions, mTransactionPool, mLogger);
mStageCoordinator = createStageCoordinator(/* splitLayout */ null);
when(mSplitLayout.getBounds1()).thenReturn(mBounds1);
when(mSplitLayout.getBounds2()).thenReturn(mBounds2);
}
@Test
@@ -87,6 +116,38 @@ public class StageCoordinatorTests extends ShellTestCase {
any(WindowContainerTransaction.class));
}
@Test
public void testDisplayAreaAppeared_initializesUnfoldControllers() {
mStageCoordinator.onDisplayAreaAppeared(mock(DisplayAreaInfo.class));
verify(mMainUnfoldController).init();
verify(mSideUnfoldController).init();
}
@Test
public void testLayoutChanged_topLeftSplitPosition_updatesUnfoldStageBounds() {
mStageCoordinator = createStageCoordinator(mSplitLayout);
mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null);
clearInvocations(mMainUnfoldController, mSideUnfoldController);
mStageCoordinator.onLayoutChanged(mSplitLayout);
verify(mMainUnfoldController).onLayoutChanged(mBounds2);
verify(mSideUnfoldController).onLayoutChanged(mBounds1);
}
@Test
public void testLayoutChanged_bottomRightSplitPosition_updatesUnfoldStageBounds() {
mStageCoordinator = createStageCoordinator(mSplitLayout);
mStageCoordinator.setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, null);
clearInvocations(mMainUnfoldController, mSideUnfoldController);
mStageCoordinator.onLayoutChanged(mSplitLayout);
verify(mMainUnfoldController).onLayoutChanged(mBounds1);
verify(mSideUnfoldController).onLayoutChanged(mBounds2);
}
@Test
public void testRemoveFromSideStage() {
final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build();
@@ -131,4 +192,27 @@ public class StageCoordinatorTests extends ShellTestCase {
verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(true));
verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(false));
}
private StageCoordinator createStageCoordinator(SplitLayout splitLayout) {
return new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage,
mDisplayImeController, mDisplayInsetsController, splitLayout,
mTransitions, mTransactionPool, mLogger, new UnfoldControllerProvider());
}
private class UnfoldControllerProvider implements
Provider<Optional<StageTaskUnfoldController>> {
private boolean isMain = true;
@Override
public Optional<StageTaskUnfoldController> get() {
if (isMain) {
isMain = false;
return Optional.of(mMainUnfoldController);
} else {
return Optional.of(mSideUnfoldController);
}
}
}
}

View File

@@ -23,6 +23,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assume.assumeFalse;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -60,6 +61,7 @@ public final class StageTaskListenerTests {
@Mock private ShellTaskOrganizer mTaskOrganizer;
@Mock private StageTaskListener.StageListenerCallbacks mCallbacks;
@Mock private SyncTransactionQueue mSyncQueue;
@Mock private StageTaskUnfoldController mStageTaskUnfoldController;
@Captor private ArgumentCaptor<SyncTransactionQueue.TransactionRunnable> mRunnableCaptor;
private SurfaceSession mSurfaceSession = new SurfaceSession();
private SurfaceControl mSurfaceControl;
@@ -74,7 +76,8 @@ public final class StageTaskListenerTests {
DEFAULT_DISPLAY,
mCallbacks,
mSyncQueue,
mSurfaceSession);
mSurfaceSession,
mStageTaskUnfoldController);
mRootTask = new TestRunningTaskInfoBuilder().build();
mRootTask.parentTaskId = INVALID_TASK_ID;
mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession).setName("test").build();
@@ -111,6 +114,28 @@ public final class StageTaskListenerTests {
verify(mCallbacks).onStatusChanged(eq(mRootTask.isVisible), eq(true));
}
@Test
public void testTaskAppeared_notifiesUnfoldListener() {
final ActivityManager.RunningTaskInfo task =
new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build();
mStageTaskListener.onTaskAppeared(task, mSurfaceControl);
verify(mStageTaskUnfoldController).onTaskAppeared(eq(task), eq(mSurfaceControl));
}
@Test
public void testTaskVanished_notifiesUnfoldListener() {
final ActivityManager.RunningTaskInfo task =
new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build();
mStageTaskListener.onTaskAppeared(task, mSurfaceControl);
clearInvocations(mStageTaskUnfoldController);
mStageTaskListener.onTaskVanished(task);
verify(mStageTaskUnfoldController).onTaskVanished(eq(task));
}
@Test(expected = IllegalArgumentException.class)
public void testUnknownTaskVanished() {
final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build();

View File

@@ -73,6 +73,7 @@ import com.android.wm.shell.pip.phone.PipTouchHandler;
import com.android.wm.shell.sizecompatui.SizeCompatUIController;
import com.android.wm.shell.splitscreen.SplitScreen;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.splitscreen.StageTaskUnfoldController;
import com.android.wm.shell.startingsurface.StartingSurface;
import com.android.wm.shell.startingsurface.StartingWindowController;
import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm;
@@ -81,10 +82,14 @@ import com.android.wm.shell.tasksurfacehelper.TaskSurfaceHelperController;
import com.android.wm.shell.transition.ShellTransitions;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
import com.android.wm.shell.unfold.UnfoldBackgroundController;
import java.util.Optional;
import javax.inject.Provider;
import dagger.BindsOptionalOf;
import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
@@ -234,16 +239,48 @@ public abstract class WMShellBaseModule {
static Optional<FullscreenUnfoldController> provideFullscreenUnfoldController(
Context context,
Optional<ShellUnfoldProgressProvider> progressProvider,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
Lazy<UnfoldBackgroundController> unfoldBackgroundController,
DisplayInsetsController displayInsetsController,
@ShellMainThread ShellExecutor mainExecutor
) {
return progressProvider.map(shellUnfoldTransitionProgressProvider ->
new FullscreenUnfoldController(context, mainExecutor,
shellUnfoldTransitionProgressProvider, rootTaskDisplayAreaOrganizer,
unfoldBackgroundController.get(), shellUnfoldTransitionProgressProvider,
displayInsetsController));
}
@Provides
static Optional<StageTaskUnfoldController> provideStageTaskUnfoldController(
Optional<ShellUnfoldProgressProvider> progressProvider,
Context context,
TransactionPool transactionPool,
Lazy<UnfoldBackgroundController> unfoldBackgroundController,
DisplayInsetsController displayInsetsController,
@ShellMainThread ShellExecutor mainExecutor
) {
return progressProvider.map(shellUnfoldTransitionProgressProvider ->
new StageTaskUnfoldController(
context,
transactionPool,
shellUnfoldTransitionProgressProvider,
displayInsetsController,
unfoldBackgroundController.get(),
mainExecutor
));
}
@WMSingleton
@Provides
static UnfoldBackgroundController provideUnfoldBackgroundController(
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
Context context
) {
return new UnfoldBackgroundController(
context,
rootTaskDisplayAreaOrganizer
);
}
//
// Freeform (optional feature)
//
@@ -401,12 +438,13 @@ public abstract class WMShellBaseModule {
@ShellMainThread ShellExecutor mainExecutor,
DisplayImeController displayImeController,
DisplayInsetsController displayInsetsController, Transitions transitions,
TransactionPool transactionPool) {
TransactionPool transactionPool,
Provider<Optional<StageTaskUnfoldController>> stageTaskUnfoldControllerProvider) {
if (ActivityTaskManager.supportsSplitScreenMultiWindow(context)) {
return Optional.of(new SplitScreenController(shellTaskOrganizer, syncQueue, context,
rootTaskDisplayAreaOrganizer, mainExecutor, displayImeController,
displayInsetsController, transitions,
transactionPool));
transactionPool, stageTaskUnfoldControllerProvider));
} else {
return Optional.empty();
}