Copy current split codes to stagesplit package

Backup current split screen implementation

Fix: 202740656
Test: build pass
Test: pass existing tests
Change-Id: I0dcd3ba5b48a43dab902f10c60e840e0ecccddbe
This commit is contained in:
Tony Huang
2021-10-12 12:26:29 +08:00
parent 152da31edf
commit 7d3fcc6fe2
13 changed files with 3800 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
/*
* 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.stagesplit;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.os.UserHandle;
import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationTarget;
import android.window.RemoteTransition;
import com.android.wm.shell.stagesplit.ISplitScreenListener;
/**
* Interface that is exposed to remote callers to manipulate the splitscreen feature.
*/
interface ISplitScreen {
/**
* Registers a split screen listener.
*/
oneway void registerSplitScreenListener(in ISplitScreenListener listener) = 1;
/**
* Unregisters a split screen listener.
*/
oneway void unregisterSplitScreenListener(in ISplitScreenListener listener) = 2;
/**
* Hides the side-stage if it is currently visible.
*/
oneway void setSideStageVisibility(boolean visible) = 3;
/**
* Removes a task from the side stage.
*/
oneway void removeFromSideStage(int taskId) = 4;
/**
* Removes the split-screen stages and leaving indicated task to top. Passing INVALID_TASK_ID
* to indicate leaving no top task after leaving split-screen.
*/
oneway void exitSplitScreen(int toTopTaskId) = 5;
/**
* @param exitSplitScreenOnHide if to exit split-screen if both stages are not visible.
*/
oneway void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) = 6;
/**
* Starts a task in a stage.
*/
oneway void startTask(int taskId, int stage, int position, in Bundle options) = 7;
/**
* Starts a shortcut in a stage.
*/
oneway void startShortcut(String packageName, String shortcutId, int stage, int position,
in Bundle options, in UserHandle user) = 8;
/**
* Starts an activity in a stage.
*/
oneway void startIntent(in PendingIntent intent, in Intent fillInIntent, int stage,
int position, in Bundle options) = 9;
/**
* Starts tasks simultaneously in one transition.
*/
oneway void startTasks(int mainTaskId, in Bundle mainOptions, int sideTaskId,
in Bundle sideOptions, int sidePosition, in RemoteTransition remoteTransition) = 10;
/**
* Version of startTasks using legacy transition system.
*/
oneway void startTasksWithLegacyTransition(int mainTaskId, in Bundle mainOptions,
int sideTaskId, in Bundle sideOptions, int sidePosition,
in RemoteAnimationAdapter adapter) = 11;
/**
* Blocking call that notifies and gets additional split-screen targets when entering
* recents (for example: the dividerBar).
* @param cancel is true if leaving recents back to split (eg. the gesture was cancelled).
* @param appTargets apps that will be re-parented to display area
*/
RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel,
in RemoteAnimationTarget[] appTargets) = 12;
}

View File

@@ -0,0 +1,33 @@
/*
* 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.stagesplit;
/**
* Listener interface that Launcher attaches to SystemUI to get split-screen callbacks.
*/
oneway interface ISplitScreenListener {
/**
* Called when the stage position changes.
*/
void onStagePositionChanged(int stage, int position);
/**
* Called when a task changes stages.
*/
void onTaskStageChanged(int taskId, int stage, boolean visible);
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright (C) 2020 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.stagesplit;
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;
import android.window.WindowContainerTransaction;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.SyncTransactionQueue;
/**
* Main stage for split-screen mode. When split-screen is active all standard activity types launch
* on the main stage, except for task that are explicitly pinned to the {@link SideStage}.
* @see StageCoordinator
*/
class MainStage extends StageTaskListener {
private static final String TAG = MainStage.class.getSimpleName();
private boolean mIsActive = false;
MainStage(ShellTaskOrganizer taskOrganizer, int displayId,
StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
SurfaceSession surfaceSession,
@Nullable StageTaskUnfoldController stageTaskUnfoldController) {
super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
stageTaskUnfoldController);
}
boolean isActive() {
return mIsActive;
}
void activate(Rect rootBounds, WindowContainerTransaction wct) {
if (mIsActive) return;
final WindowContainerToken rootToken = mRootTaskInfo.token;
wct.setBounds(rootToken, rootBounds)
.setWindowingMode(rootToken, WINDOWING_MODE_MULTI_WINDOW)
.setLaunchRoot(
rootToken,
CONTROLLED_WINDOWING_MODES,
CONTROLLED_ACTIVITY_TYPES)
.reparentTasks(
null /* currentParent */,
rootToken,
CONTROLLED_WINDOWING_MODES,
CONTROLLED_ACTIVITY_TYPES,
true /* onTop */)
// Moving the root task to top after the child tasks were re-parented , or the root
// task cannot be visible and focused.
.reorder(rootToken, true /* onTop */);
mIsActive = true;
}
void deactivate(WindowContainerTransaction wct) {
deactivate(wct, false /* toTop */);
}
void deactivate(WindowContainerTransaction wct, boolean toTop) {
if (!mIsActive) return;
mIsActive = false;
if (mRootTaskInfo == null) return;
final WindowContainerToken rootToken = mRootTaskInfo.token;
wct.setLaunchRoot(
rootToken,
null,
null)
.reparentTasks(
rootToken,
null /* newParent */,
CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE,
CONTROLLED_ACTIVITY_TYPES,
toTop)
// We want this re-order to the bottom regardless since we are re-parenting
// all its tasks.
.reorder(rootToken, false /* onTop */);
}
void updateConfiguration(int windowingMode, Rect bounds, WindowContainerTransaction wct) {
wct.setBounds(mRootTaskInfo.token, bounds)
.setWindowingMode(mRootTaskInfo.token, windowingMode);
}
}

View File

@@ -0,0 +1,181 @@
/*
* 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.stagesplit;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.Binder;
import android.view.IWindow;
import android.view.InsetsSource;
import android.view.InsetsState;
import android.view.LayoutInflater;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
import android.widget.FrameLayout;
import com.android.wm.shell.R;
/**
* Handles drawing outline of the bounds of provided root surface. The outline will be drown with
* the consideration of display insets like status bar, navigation bar and display cutout.
*/
class OutlineManager extends WindowlessWindowManager {
private static final String WINDOW_NAME = "SplitOutlineLayer";
private final Context mContext;
private final Rect mRootBounds = new Rect();
private final Rect mTempRect = new Rect();
private final Rect mLastOutlineBounds = new Rect();
private final InsetsState mInsetsState = new InsetsState();
private final int mExpandedTaskBarHeight;
private OutlineView mOutlineView;
private SurfaceControlViewHost mViewHost;
private SurfaceControl mHostLeash;
private SurfaceControl mLeash;
OutlineManager(Context context, Configuration configuration) {
super(configuration, null /* rootSurface */, null /* hostInputToken */);
mContext = context.createWindowContext(context.getDisplay(), TYPE_APPLICATION_OVERLAY,
null /* options */);
mExpandedTaskBarHeight = mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.taskbar_frame_height);
}
@Override
protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) {
b.setParent(mHostLeash);
}
void inflate(SurfaceControl rootLeash, Rect rootBounds) {
if (mLeash != null || mViewHost != null) return;
mHostLeash = rootLeash;
mRootBounds.set(rootBounds);
mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this);
final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(mContext)
.inflate(R.layout.split_outline, null);
mOutlineView = rootLayout.findViewById(R.id.split_outline);
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY,
FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT);
lp.width = mRootBounds.width();
lp.height = mRootBounds.height();
lp.token = new Binder();
lp.setTitle(WINDOW_NAME);
lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
// TODO(b/189839391): Set INPUT_FEATURE_NO_INPUT_CHANNEL after WM supports
// TRUSTED_OVERLAY for windowless window without input channel.
mViewHost.setView(rootLayout, lp);
mLeash = getSurfaceControl(mViewHost.getWindowToken());
drawOutline();
}
void release() {
if (mViewHost != null) {
mViewHost.release();
mViewHost = null;
}
mRootBounds.setEmpty();
mLastOutlineBounds.setEmpty();
mOutlineView = null;
mHostLeash = null;
mLeash = null;
}
@Nullable
SurfaceControl getOutlineLeash() {
return mLeash;
}
void setVisibility(boolean visible) {
if (mOutlineView != null) {
mOutlineView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
}
}
void setRootBounds(Rect rootBounds) {
if (mViewHost == null || mViewHost.getView() == null) {
return;
}
if (!mRootBounds.equals(rootBounds)) {
WindowManager.LayoutParams lp =
(WindowManager.LayoutParams) mViewHost.getView().getLayoutParams();
lp.width = rootBounds.width();
lp.height = rootBounds.height();
mViewHost.relayout(lp);
mRootBounds.set(rootBounds);
drawOutline();
}
}
void onInsetsChanged(InsetsState insetsState) {
if (!mInsetsState.equals(insetsState)) {
mInsetsState.set(insetsState);
drawOutline();
}
}
private void computeOutlineBounds(Rect rootBounds, InsetsState insetsState, Rect outBounds) {
outBounds.set(rootBounds);
final InsetsSource taskBarInsetsSource =
insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
// Only insets the divider bar with task bar when it's expanded so that the rounded corners
// will be drawn against task bar.
if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
outBounds.inset(taskBarInsetsSource.calculateVisibleInsets(outBounds));
}
// Offset the coordinate from screen based to surface based.
outBounds.offset(-rootBounds.left, -rootBounds.top);
}
void drawOutline() {
if (mOutlineView == null) {
return;
}
computeOutlineBounds(mRootBounds, mInsetsState, mTempRect);
if (mTempRect.equals(mLastOutlineBounds)) {
return;
}
ViewGroup.MarginLayoutParams lp =
(ViewGroup.MarginLayoutParams) mOutlineView.getLayoutParams();
lp.leftMargin = mTempRect.left;
lp.topMargin = mTempRect.top;
lp.width = mTempRect.width();
lp.height = mTempRect.height();
mOutlineView.setLayoutParams(lp);
mLastOutlineBounds.set(mTempRect);
}
}

View File

@@ -0,0 +1,82 @@
/*
* 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.stagesplit;
import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT;
import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT;
import static android.view.RoundedCorner.POSITION_TOP_LEFT;
import static android.view.RoundedCorner.POSITION_TOP_RIGHT;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.RoundedCorner;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.R;
/** View for drawing split outline. */
public class OutlineView extends View {
private final Paint mPaint = new Paint();
private final Path mPath = new Path();
private final float[] mRadii = new float[8];
public OutlineView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(
getResources().getDimension(R.dimen.accessibility_focus_highlight_stroke_width));
mPaint.setColor(getResources().getColor(R.color.system_accent1_100, null));
}
@Override
protected void onAttachedToWindow() {
// TODO(b/200850654): match the screen corners with the actual display decor.
mRadii[0] = mRadii[1] = getCornerRadius(POSITION_TOP_LEFT);
mRadii[2] = mRadii[3] = getCornerRadius(POSITION_TOP_RIGHT);
mRadii[4] = mRadii[5] = getCornerRadius(POSITION_BOTTOM_RIGHT);
mRadii[6] = mRadii[7] = getCornerRadius(POSITION_BOTTOM_LEFT);
}
private int getCornerRadius(@RoundedCorner.Position int position) {
final RoundedCorner roundedCorner = getDisplay().getRoundedCorner(position);
return roundedCorner == null ? 0 : roundedCorner.getRadius();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (changed) {
mPath.reset();
mPath.addRoundRect(0, 0, getWidth(), getHeight(), mRadii, Path.Direction.CW);
}
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawPath(mPath, mPaint);
}
@Override
public boolean hasOverlappingRendering() {
return false;
}
}

View File

@@ -0,0 +1,144 @@
/*
* Copyright (C) 2020 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.stagesplit;
import android.annotation.CallSuper;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Rect;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.SyncTransactionQueue;
/**
* Side stage for split-screen mode. Only tasks that are explicitly pinned to this stage show up
* here. All other task are launch in the {@link MainStage}.
*
* @see StageCoordinator
*/
class SideStage extends StageTaskListener implements
DisplayInsetsController.OnInsetsChangedListener {
private static final String TAG = SideStage.class.getSimpleName();
private final Context mContext;
private OutlineManager mOutlineManager;
SideStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
SurfaceSession surfaceSession,
@Nullable StageTaskUnfoldController stageTaskUnfoldController) {
super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
stageTaskUnfoldController);
mContext = context;
}
void addTask(ActivityManager.RunningTaskInfo task, Rect rootBounds,
WindowContainerTransaction wct) {
final WindowContainerToken rootToken = mRootTaskInfo.token;
wct.setBounds(rootToken, rootBounds)
.reparent(task.token, rootToken, true /* onTop*/)
// Moving the root task to top after the child tasks were reparented , or the root
// task cannot be visible and focused.
.reorder(rootToken, true /* onTop */);
}
boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) {
// No matter if the root task is empty or not, moving the root to bottom because it no
// longer preserves visible child task.
wct.reorder(mRootTaskInfo.token, false /* onTop */);
if (mChildrenTaskInfo.size() == 0) return false;
wct.reparentTasks(
mRootTaskInfo.token,
null /* newParent */,
CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE,
CONTROLLED_ACTIVITY_TYPES,
toTop);
return true;
}
boolean removeTask(int taskId, WindowContainerToken newParent, WindowContainerTransaction wct) {
final ActivityManager.RunningTaskInfo task = mChildrenTaskInfo.get(taskId);
if (task == null) return false;
wct.reparent(task.token, newParent, false /* onTop */);
return true;
}
@Nullable
public SurfaceControl getOutlineLeash() {
return mOutlineManager.getOutlineLeash();
}
@Override
@CallSuper
public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
super.onTaskAppeared(taskInfo, leash);
if (isRootTask(taskInfo)) {
mOutlineManager = new OutlineManager(mContext, taskInfo.configuration);
enableOutline(true);
}
}
@Override
@CallSuper
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
super.onTaskInfoChanged(taskInfo);
if (isRootTask(taskInfo)) {
mOutlineManager.setRootBounds(taskInfo.configuration.windowConfiguration.getBounds());
}
}
private boolean isRootTask(ActivityManager.RunningTaskInfo taskInfo) {
return mRootTaskInfo != null && mRootTaskInfo.taskId == taskInfo.taskId;
}
void enableOutline(boolean enable) {
if (mOutlineManager == null) {
return;
}
if (enable) {
if (mRootTaskInfo != null) {
mOutlineManager.inflate(mRootLeash,
mRootTaskInfo.configuration.windowConfiguration.getBounds());
}
} else {
mOutlineManager.release();
}
}
void setOutlineVisibility(boolean visible) {
mOutlineManager.setVisibility(visible);
}
@Override
public void insetsChanged(InsetsState insetsState) {
mOutlineManager.onInsetsChanged(insetsState);
}
@Override
public void insetsControlChanged(InsetsState insetsState,
InsetsSourceControl[] activeControls) {
insetsChanged(insetsState);
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright (C) 2020 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.stagesplit;
import android.annotation.IntDef;
import android.annotation.NonNull;
import com.android.wm.shell.common.annotations.ExternalThread;
import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
import java.util.concurrent.Executor;
/**
* Interface to engage split-screen feature.
* TODO: Figure out which of these are actually needed outside of the Shell
*/
@ExternalThread
public interface SplitScreen {
/**
* Stage type isn't specified normally meaning to use what ever the default is.
* E.g. exit split-screen and launch the app in fullscreen.
*/
int STAGE_TYPE_UNDEFINED = -1;
/**
* The main stage type.
* @see MainStage
*/
int STAGE_TYPE_MAIN = 0;
/**
* The side stage type.
* @see SideStage
*/
int STAGE_TYPE_SIDE = 1;
@IntDef(prefix = { "STAGE_TYPE_" }, value = {
STAGE_TYPE_UNDEFINED,
STAGE_TYPE_MAIN,
STAGE_TYPE_SIDE
})
@interface StageType {}
/** Callback interface for listening to changes in a split-screen stage. */
interface SplitScreenListener {
default void onStagePositionChanged(@StageType int stage, @SplitPosition int position) {}
default void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {}
default void onSplitVisibilityChanged(boolean visible) {}
}
/** Registers listener that gets split screen callback. */
void registerSplitScreenListener(@NonNull SplitScreenListener listener,
@NonNull Executor executor);
/** Unregisters listener that gets split screen callback. */
void unregisterSplitScreenListener(@NonNull SplitScreenListener listener);
/**
* Returns a binder that can be passed to an external process to manipulate SplitScreen.
*/
default ISplitScreen createExternalInterface() {
return null;
}
/**
* Called when the keyguard occluded state changes.
* @param occluded Indicates if the keyguard is now occluded.
*/
void onKeyguardOccludedChanged(boolean occluded);
/**
* Called when the visibility of the keyguard changes.
* @param showing Indicates if the keyguard is now visible.
*/
void onKeyguardVisibilityChanged(boolean showing);
/** Get a string representation of a stage type */
static String stageTypeToString(@StageType int stage) {
switch (stage) {
case STAGE_TYPE_UNDEFINED: return "UNDEFINED";
case STAGE_TYPE_MAIN: return "MAIN";
case STAGE_TYPE_SIDE: return "SIDE";
default: return "UNKNOWN(" + stage + ")";
}
}
}

View File

@@ -0,0 +1,595 @@
/*
* Copyright (C) 2020 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.stagesplit;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
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 android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.LauncherApps;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Slog;
import android.view.IRemoteAnimationFinishedCallback;
import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.view.WindowManager;
import android.window.RemoteTransition;
import android.window.WindowContainerTransaction;
import androidx.annotation.BinderThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.logging.InstanceId;
import com.android.internal.util.FrameworkStatsLog;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.annotations.ExternalThread;
import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
import com.android.wm.shell.draganddrop.DragAndDropPolicy;
import com.android.wm.shell.transition.LegacyTransitions;
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}.
* @see StageCoordinator
*/
// TODO(b/198577848): Implement split screen flicker test to consolidate CUJ of split screen.
public class SplitScreenController implements DragAndDropPolicy.Starter,
RemoteCallable<SplitScreenController> {
private static final String TAG = SplitScreenController.class.getSimpleName();
private final ShellTaskOrganizer mTaskOrganizer;
private final SyncTransactionQueue mSyncQueue;
private final Context mContext;
private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
private final ShellExecutor mMainExecutor;
private final SplitScreenImpl mImpl = new SplitScreenImpl();
private final DisplayImeController mDisplayImeController;
private final DisplayInsetsController mDisplayInsetsController;
private final Transitions mTransitions;
private final TransactionPool mTransactionPool;
private final SplitscreenEventLogger mLogger;
private final Provider<Optional<StageTaskUnfoldController>> mUnfoldControllerProvider;
private StageCoordinator mStageCoordinator;
public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer,
SyncTransactionQueue syncQueue, Context context,
RootTaskDisplayAreaOrganizer rootTDAOrganizer,
ShellExecutor mainExecutor, DisplayImeController displayImeController,
DisplayInsetsController displayInsetsController,
Transitions transitions, TransactionPool transactionPool,
Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
mTaskOrganizer = shellTaskOrganizer;
mSyncQueue = syncQueue;
mContext = context;
mRootTDAOrganizer = rootTDAOrganizer;
mMainExecutor = mainExecutor;
mDisplayImeController = displayImeController;
mDisplayInsetsController = displayInsetsController;
mTransitions = transitions;
mTransactionPool = transactionPool;
mUnfoldControllerProvider = unfoldControllerProvider;
mLogger = new SplitscreenEventLogger();
}
public SplitScreen asSplitScreen() {
return mImpl;
}
@Override
public Context getContext() {
return mContext;
}
@Override
public ShellExecutor getRemoteCallExecutor() {
return mMainExecutor;
}
public void onOrganizerRegistered() {
if (mStageCoordinator == null) {
// TODO: Multi-display
mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController,
mDisplayInsetsController, mTransitions, mTransactionPool, mLogger,
mUnfoldControllerProvider);
}
}
public boolean isSplitScreenVisible() {
return mStageCoordinator.isSplitScreenVisible();
}
public boolean moveToSideStage(int taskId, @SplitPosition int sideStagePosition) {
final ActivityManager.RunningTaskInfo task = mTaskOrganizer.getRunningTaskInfo(taskId);
if (task == null) {
throw new IllegalArgumentException("Unknown taskId" + taskId);
}
return moveToSideStage(task, sideStagePosition);
}
public boolean moveToSideStage(ActivityManager.RunningTaskInfo task,
@SplitPosition int sideStagePosition) {
return mStageCoordinator.moveToSideStage(task, sideStagePosition);
}
public boolean removeFromSideStage(int taskId) {
return mStageCoordinator.removeFromSideStage(taskId);
}
public void setSideStageOutline(boolean enable) {
mStageCoordinator.setSideStageOutline(enable);
}
public void setSideStagePosition(@SplitPosition int sideStagePosition) {
mStageCoordinator.setSideStagePosition(sideStagePosition, null /* wct */);
}
public void setSideStageVisibility(boolean visible) {
mStageCoordinator.setSideStageVisibility(visible);
}
public void enterSplitScreen(int taskId, boolean leftOrTop) {
moveToSideStage(taskId,
leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT);
}
public void exitSplitScreen(int toTopTaskId, int exitReason) {
mStageCoordinator.exitSplitScreen(toTopTaskId, exitReason);
}
public void onKeyguardOccludedChanged(boolean occluded) {
mStageCoordinator.onKeyguardOccludedChanged(occluded);
}
public void onKeyguardVisibilityChanged(boolean showing) {
mStageCoordinator.onKeyguardVisibilityChanged(showing);
}
public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
mStageCoordinator.exitSplitScreenOnHide(exitSplitScreenOnHide);
}
public void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) {
mStageCoordinator.getStageBounds(outTopOrLeftBounds, outBottomOrRightBounds);
}
public void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) {
mStageCoordinator.registerSplitScreenListener(listener);
}
public void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) {
mStageCoordinator.unregisterSplitScreenListener(listener);
}
public void startTask(int taskId, @SplitScreen.StageType int stage,
@SplitPosition int position, @Nullable Bundle options) {
options = mStageCoordinator.resolveStartStage(stage, position, options, null /* wct */);
try {
ActivityTaskManager.getService().startActivityFromRecents(taskId, options);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to launch task", e);
}
}
public void startShortcut(String packageName, String shortcutId,
@SplitScreen.StageType int stage, @SplitPosition int position,
@Nullable Bundle options, UserHandle user) {
options = mStageCoordinator.resolveStartStage(stage, position, options, null /* wct */);
try {
LauncherApps launcherApps =
mContext.getSystemService(LauncherApps.class);
launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */,
options, user);
} catch (ActivityNotFoundException e) {
Slog.e(TAG, "Failed to launch shortcut", e);
}
}
public void startIntent(PendingIntent intent, Intent fillInIntent,
@SplitScreen.StageType int stage, @SplitPosition int position,
@Nullable Bundle options) {
if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
startIntentLegacy(intent, fillInIntent, stage, position, options);
return;
}
mStageCoordinator.startIntent(intent, fillInIntent, stage, position, options,
null /* remote */);
}
private void startIntentLegacy(PendingIntent intent, Intent fillInIntent,
@SplitScreen.StageType int stage, @SplitPosition int position,
@Nullable Bundle options) {
LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() {
@Override
public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
IRemoteAnimationFinishedCallback finishedCallback,
SurfaceControl.Transaction t) {
mStageCoordinator.updateSurfaceBounds(null /* layout */, t);
if (apps != null) {
for (int i = 0; i < apps.length; ++i) {
if (apps[i].mode == MODE_OPENING) {
t.show(apps[i].leash);
}
}
}
t.apply();
if (finishedCallback != null) {
try {
finishedCallback.onAnimationFinished();
} catch (RemoteException e) {
Slog.e(TAG, "Error finishing legacy transition: ", e);
}
}
}
};
WindowContainerTransaction wct = new WindowContainerTransaction();
options = mStageCoordinator.resolveStartStage(stage, position, options, wct);
wct.sendPendingIntent(intent, fillInIntent, options);
mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct);
}
RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, RemoteAnimationTarget[] apps) {
if (!isSplitScreenVisible()) return null;
final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
.setContainerLayer()
.setName("RecentsAnimationSplitTasks")
.setHidden(false)
.setCallsite("SplitScreenController#onGoingtoRecentsLegacy");
mRootTDAOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, builder);
SurfaceControl sc = builder.build();
SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
// Ensure that we order these in the parent in the right z-order as their previous order
Arrays.sort(apps, (a1, a2) -> a1.prefixOrderIndex - a2.prefixOrderIndex);
int layer = 1;
for (RemoteAnimationTarget appTarget : apps) {
transaction.reparent(appTarget.leash, sc);
transaction.setPosition(appTarget.leash, appTarget.screenSpaceBounds.left,
appTarget.screenSpaceBounds.top);
transaction.setLayer(appTarget.leash, layer++);
}
transaction.apply();
transaction.close();
return new RemoteAnimationTarget[]{
mStageCoordinator.getDividerBarLegacyTarget(),
mStageCoordinator.getOutlineLegacyTarget()};
}
/**
* Sets drag info to be logged when splitscreen is entered.
*/
public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) {
mStageCoordinator.logOnDroppedToSplit(position, dragSessionId);
}
public void dump(@NonNull PrintWriter pw, String prefix) {
pw.println(prefix + TAG);
if (mStageCoordinator != null) {
mStageCoordinator.dump(pw, prefix);
}
}
/**
* The interface for calls from outside the Shell, within the host process.
*/
@ExternalThread
private class SplitScreenImpl implements SplitScreen {
private ISplitScreenImpl mISplitScreen;
private final ArrayMap<SplitScreenListener, Executor> mExecutors = new ArrayMap<>();
private final SplitScreenListener mListener = new SplitScreenListener() {
@Override
public void onStagePositionChanged(int stage, int position) {
for (int i = 0; i < mExecutors.size(); i++) {
final int index = i;
mExecutors.valueAt(index).execute(() -> {
mExecutors.keyAt(index).onStagePositionChanged(stage, position);
});
}
}
@Override
public void onTaskStageChanged(int taskId, int stage, boolean visible) {
for (int i = 0; i < mExecutors.size(); i++) {
final int index = i;
mExecutors.valueAt(index).execute(() -> {
mExecutors.keyAt(index).onTaskStageChanged(taskId, stage, visible);
});
}
}
@Override
public void onSplitVisibilityChanged(boolean visible) {
for (int i = 0; i < mExecutors.size(); i++) {
final int index = i;
mExecutors.valueAt(index).execute(() -> {
mExecutors.keyAt(index).onSplitVisibilityChanged(visible);
});
}
}
};
@Override
public ISplitScreen createExternalInterface() {
if (mISplitScreen != null) {
mISplitScreen.invalidate();
}
mISplitScreen = new ISplitScreenImpl(SplitScreenController.this);
return mISplitScreen;
}
@Override
public void onKeyguardOccludedChanged(boolean occluded) {
mMainExecutor.execute(() -> {
SplitScreenController.this.onKeyguardOccludedChanged(occluded);
});
}
@Override
public void registerSplitScreenListener(SplitScreenListener listener, Executor executor) {
if (mExecutors.containsKey(listener)) return;
mMainExecutor.execute(() -> {
if (mExecutors.size() == 0) {
SplitScreenController.this.registerSplitScreenListener(mListener);
}
mExecutors.put(listener, executor);
});
executor.execute(() -> {
mStageCoordinator.sendStatusToListener(listener);
});
}
@Override
public void unregisterSplitScreenListener(SplitScreenListener listener) {
mMainExecutor.execute(() -> {
mExecutors.remove(listener);
if (mExecutors.size() == 0) {
SplitScreenController.this.unregisterSplitScreenListener(mListener);
}
});
}
@Override
public void onKeyguardVisibilityChanged(boolean showing) {
mMainExecutor.execute(() -> {
SplitScreenController.this.onKeyguardVisibilityChanged(showing);
});
}
}
/**
* The interface for calls from outside the host process.
*/
@BinderThread
private static class ISplitScreenImpl extends ISplitScreen.Stub {
private SplitScreenController mController;
private ISplitScreenListener mListener;
private final SplitScreen.SplitScreenListener mSplitScreenListener =
new SplitScreen.SplitScreenListener() {
@Override
public void onStagePositionChanged(int stage, int position) {
try {
if (mListener != null) {
mListener.onStagePositionChanged(stage, position);
}
} catch (RemoteException e) {
Slog.e(TAG, "onStagePositionChanged", e);
}
}
@Override
public void onTaskStageChanged(int taskId, int stage, boolean visible) {
try {
if (mListener != null) {
mListener.onTaskStageChanged(taskId, stage, visible);
}
} catch (RemoteException e) {
Slog.e(TAG, "onTaskStageChanged", e);
}
}
};
private final IBinder.DeathRecipient mListenerDeathRecipient =
new IBinder.DeathRecipient() {
@Override
@BinderThread
public void binderDied() {
final SplitScreenController controller = mController;
controller.getRemoteCallExecutor().execute(() -> {
mListener = null;
controller.unregisterSplitScreenListener(mSplitScreenListener);
});
}
};
public ISplitScreenImpl(SplitScreenController controller) {
mController = controller;
}
/**
* Invalidates this instance, preventing future calls from updating the controller.
*/
void invalidate() {
mController = null;
}
@Override
public void registerSplitScreenListener(ISplitScreenListener listener) {
executeRemoteCallWithTaskPermission(mController, "registerSplitScreenListener",
(controller) -> {
if (mListener != null) {
mListener.asBinder().unlinkToDeath(mListenerDeathRecipient,
0 /* flags */);
}
if (listener != null) {
try {
listener.asBinder().linkToDeath(mListenerDeathRecipient,
0 /* flags */);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to link to death");
return;
}
}
mListener = listener;
controller.registerSplitScreenListener(mSplitScreenListener);
});
}
@Override
public void unregisterSplitScreenListener(ISplitScreenListener listener) {
executeRemoteCallWithTaskPermission(mController, "unregisterSplitScreenListener",
(controller) -> {
if (mListener != null) {
mListener.asBinder().unlinkToDeath(mListenerDeathRecipient,
0 /* flags */);
}
mListener = null;
controller.unregisterSplitScreenListener(mSplitScreenListener);
});
}
@Override
public void exitSplitScreen(int toTopTaskId) {
executeRemoteCallWithTaskPermission(mController, "exitSplitScreen",
(controller) -> {
controller.exitSplitScreen(toTopTaskId,
FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT);
});
}
@Override
public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
executeRemoteCallWithTaskPermission(mController, "exitSplitScreenOnHide",
(controller) -> {
controller.exitSplitScreenOnHide(exitSplitScreenOnHide);
});
}
@Override
public void setSideStageVisibility(boolean visible) {
executeRemoteCallWithTaskPermission(mController, "setSideStageVisibility",
(controller) -> {
controller.setSideStageVisibility(visible);
});
}
@Override
public void removeFromSideStage(int taskId) {
executeRemoteCallWithTaskPermission(mController, "removeFromSideStage",
(controller) -> {
controller.removeFromSideStage(taskId);
});
}
@Override
public void startTask(int taskId, int stage, int position, @Nullable Bundle options) {
executeRemoteCallWithTaskPermission(mController, "startTask",
(controller) -> {
controller.startTask(taskId, stage, position, options);
});
}
@Override
public void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions,
int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition,
RemoteAnimationAdapter adapter) {
executeRemoteCallWithTaskPermission(mController, "startTasks",
(controller) -> controller.mStageCoordinator.startTasksWithLegacyTransition(
mainTaskId, mainOptions, sideTaskId, sideOptions, sidePosition,
adapter));
}
@Override
public void startTasks(int mainTaskId, @Nullable Bundle mainOptions,
int sideTaskId, @Nullable Bundle sideOptions,
@SplitPosition int sidePosition,
@Nullable RemoteTransition remoteTransition) {
executeRemoteCallWithTaskPermission(mController, "startTasks",
(controller) -> controller.mStageCoordinator.startTasks(mainTaskId, mainOptions,
sideTaskId, sideOptions, sidePosition, remoteTransition));
}
@Override
public void startShortcut(String packageName, String shortcutId, int stage, int position,
@Nullable Bundle options, UserHandle user) {
executeRemoteCallWithTaskPermission(mController, "startShortcut",
(controller) -> {
controller.startShortcut(packageName, shortcutId, stage, position,
options, user);
});
}
@Override
public void startIntent(PendingIntent intent, Intent fillInIntent, int stage, int position,
@Nullable Bundle options) {
executeRemoteCallWithTaskPermission(mController, "startIntent",
(controller) -> {
controller.startIntent(intent, fillInIntent, stage, position, options);
});
}
@Override
public RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel,
RemoteAnimationTarget[] apps) {
final RemoteAnimationTarget[][] out = new RemoteAnimationTarget[][]{null};
executeRemoteCallWithTaskPermission(mController, "onGoingToRecentsLegacy",
(controller) -> out[0] = controller.onGoingToRecentsLegacy(cancel, apps),
true /* blocking */);
return out[0];
}
}
}

View File

@@ -0,0 +1,298 @@
/*
* 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.stagesplit;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM;
import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP;
import static com.android.wm.shell.transition.Transitions.isOpeningType;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Rect;
import android.os.IBinder;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.RemoteTransition;
import android.window.TransitionInfo;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.transition.OneShotRemoteHandler;
import com.android.wm.shell.transition.Transitions;
import java.util.ArrayList;
/** Manages transition animations for split-screen. */
class SplitScreenTransitions {
private static final String TAG = "SplitScreenTransitions";
/** Flag applied to a transition change to identify it as a divider bar for animation. */
public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
private final TransactionPool mTransactionPool;
private final Transitions mTransitions;
private final Runnable mOnFinish;
IBinder mPendingDismiss = null;
IBinder mPendingEnter = null;
private IBinder mAnimatingTransition = null;
private OneShotRemoteHandler mRemoteHandler = null;
private Transitions.TransitionFinishCallback mRemoteFinishCB = (wct, wctCB) -> {
if (wct != null || wctCB != null) {
throw new UnsupportedOperationException("finish transactions not supported yet.");
}
onFinish();
};
/** Keeps track of currently running animations */
private final ArrayList<Animator> mAnimations = new ArrayList<>();
private Transitions.TransitionFinishCallback mFinishCallback = null;
private SurfaceControl.Transaction mFinishTransaction;
SplitScreenTransitions(@NonNull TransactionPool pool, @NonNull Transitions transitions,
@NonNull Runnable onFinishCallback) {
mTransactionPool = pool;
mTransitions = transitions;
mOnFinish = onFinishCallback;
}
void playAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback,
@NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot) {
mFinishCallback = finishCallback;
mAnimatingTransition = transition;
if (mRemoteHandler != null) {
mRemoteHandler.startAnimation(transition, info, startTransaction, finishTransaction,
mRemoteFinishCB);
mRemoteHandler = null;
return;
}
playInternalAnimation(transition, info, startTransaction, mainRoot, sideRoot);
}
private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull WindowContainerToken mainRoot,
@NonNull WindowContainerToken sideRoot) {
mFinishTransaction = mTransactionPool.acquire();
// Play some place-holder fade animations
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
final SurfaceControl leash = change.getLeash();
final int mode = info.getChanges().get(i).getMode();
if (mode == TRANSIT_CHANGE) {
if (change.getParent() != null) {
// This is probably reparented, so we want the parent to be immediately visible
final TransitionInfo.Change parentChange = info.getChange(change.getParent());
t.show(parentChange.getLeash());
t.setAlpha(parentChange.getLeash(), 1.f);
// and then animate this layer outside the parent (since, for example, this is
// the home task animating from fullscreen to part-screen).
t.reparent(leash, info.getRootLeash());
t.setLayer(leash, info.getChanges().size() - i);
// build the finish reparent/reposition
mFinishTransaction.reparent(leash, parentChange.getLeash());
mFinishTransaction.setPosition(leash,
change.getEndRelOffset().x, change.getEndRelOffset().y);
}
// TODO(shell-transitions): screenshot here
final Rect startBounds = new Rect(change.getStartAbsBounds());
if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) {
// Dismissing split via snap which means the still-visible task has been
// dragged to its end position at animation start so reflect that here.
startBounds.offsetTo(change.getEndAbsBounds().left,
change.getEndAbsBounds().top);
}
final Rect endBounds = new Rect(change.getEndAbsBounds());
startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
startExampleResizeAnimation(leash, startBounds, endBounds);
}
if (change.getParent() != null) {
continue;
}
if (transition == mPendingEnter && (mainRoot.equals(change.getContainer())
|| sideRoot.equals(change.getContainer()))) {
t.setWindowCrop(leash, change.getStartAbsBounds().width(),
change.getStartAbsBounds().height());
}
boolean isOpening = isOpeningType(info.getType());
if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) {
// fade in
startExampleAnimation(leash, true /* show */);
} else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) {
// fade out
if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) {
// Dismissing via snap-to-top/bottom means that the dismissed task is already
// not-visible (usually cropped to oblivion) so immediately set its alpha to 0
// and don't animate it so it doesn't pop-in when reparented.
t.setAlpha(leash, 0.f);
} else {
startExampleAnimation(leash, false /* show */);
}
}
}
t.apply();
onFinish();
}
/** Starts a transition to enter split with a remote transition animator. */
IBinder startEnterTransition(@WindowManager.TransitionType int transitType,
@NonNull WindowContainerTransaction wct, @Nullable RemoteTransition remoteTransition,
@NonNull Transitions.TransitionHandler handler) {
if (remoteTransition != null) {
// Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff)
mRemoteHandler = new OneShotRemoteHandler(
mTransitions.getMainExecutor(), remoteTransition);
}
final IBinder transition = mTransitions.startTransition(transitType, wct, handler);
mPendingEnter = transition;
if (mRemoteHandler != null) {
mRemoteHandler.setTransition(transition);
}
return transition;
}
/** Starts a transition for dismissing split after dragging the divider to a screen edge */
IBinder startSnapToDismiss(@NonNull WindowContainerTransaction wct,
@NonNull Transitions.TransitionHandler handler) {
final IBinder transition = mTransitions.startTransition(
TRANSIT_SPLIT_DISMISS_SNAP, wct, handler);
mPendingDismiss = transition;
return transition;
}
void onFinish() {
if (!mAnimations.isEmpty()) return;
mOnFinish.run();
if (mFinishTransaction != null) {
mFinishTransaction.apply();
mTransactionPool.release(mFinishTransaction);
mFinishTransaction = null;
}
mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
mFinishCallback = null;
if (mAnimatingTransition == mPendingEnter) {
mPendingEnter = null;
}
if (mAnimatingTransition == mPendingDismiss) {
mPendingDismiss = null;
}
mAnimatingTransition = null;
}
// TODO(shell-transitions): real animations
private void startExampleAnimation(@NonNull SurfaceControl leash, boolean show) {
final float end = show ? 1.f : 0.f;
final float start = 1.f - end;
final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
final ValueAnimator va = ValueAnimator.ofFloat(start, end);
va.setDuration(500);
va.addUpdateListener(animation -> {
float fraction = animation.getAnimatedFraction();
transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction);
transaction.apply();
});
final Runnable finisher = () -> {
transaction.setAlpha(leash, end);
transaction.apply();
mTransactionPool.release(transaction);
mTransitions.getMainExecutor().execute(() -> {
mAnimations.remove(va);
onFinish();
});
};
va.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) { }
@Override
public void onAnimationEnd(Animator animation) {
finisher.run();
}
@Override
public void onAnimationCancel(Animator animation) {
finisher.run();
}
@Override
public void onAnimationRepeat(Animator animation) { }
});
mAnimations.add(va);
mTransitions.getAnimExecutor().execute(va::start);
}
// TODO(shell-transitions): real animations
private void startExampleResizeAnimation(@NonNull SurfaceControl leash,
@NonNull Rect startBounds, @NonNull Rect endBounds) {
final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
final ValueAnimator va = ValueAnimator.ofFloat(0.f, 1.f);
va.setDuration(500);
va.addUpdateListener(animation -> {
float fraction = animation.getAnimatedFraction();
transaction.setWindowCrop(leash,
(int) (startBounds.width() * (1.f - fraction) + endBounds.width() * fraction),
(int) (startBounds.height() * (1.f - fraction)
+ endBounds.height() * fraction));
transaction.setPosition(leash,
startBounds.left * (1.f - fraction) + endBounds.left * fraction,
startBounds.top * (1.f - fraction) + endBounds.top * fraction);
transaction.apply();
});
final Runnable finisher = () -> {
transaction.setWindowCrop(leash, 0, 0);
transaction.setPosition(leash, endBounds.left, endBounds.top);
transaction.apply();
mTransactionPool.release(transaction);
mTransitions.getMainExecutor().execute(() -> {
mAnimations.remove(va);
onFinish();
});
};
va.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
finisher.run();
}
@Override
public void onAnimationCancel(Animator animation) {
finisher.run();
}
});
mAnimations.add(va);
mTransitions.getAnimExecutor().execute(va::start);
}
}

View File

@@ -0,0 +1,324 @@
/*
* 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.stagesplit;
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW;
import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED;
import com.android.internal.logging.InstanceId;
import com.android.internal.logging.InstanceIdSequence;
import com.android.internal.util.FrameworkStatsLog;
import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
/**
* Helper class that to log Drag & Drop UIEvents for a single session, see also go/uievent
*/
public class SplitscreenEventLogger {
// Used to generate instance ids for this drag if one is not provided
private final InstanceIdSequence mIdSequence;
// The instance id for the current splitscreen session (from start to end)
private InstanceId mLoggerSessionId;
// Drag info
private @SplitPosition int mDragEnterPosition;
private InstanceId mDragEnterSessionId;
// For deduping async events
private int mLastMainStagePosition = -1;
private int mLastMainStageUid = -1;
private int mLastSideStagePosition = -1;
private int mLastSideStageUid = -1;
private float mLastSplitRatio = -1f;
public SplitscreenEventLogger() {
mIdSequence = new InstanceIdSequence(Integer.MAX_VALUE);
}
/**
* Return whether a splitscreen session has started.
*/
public boolean hasStartedSession() {
return mLoggerSessionId != null;
}
/**
* May be called before logEnter() to indicate that the session was started from a drag.
*/
public void enterRequestedByDrag(@SplitPosition int position, InstanceId dragSessionId) {
mDragEnterPosition = position;
mDragEnterSessionId = dragSessionId;
}
/**
* Logs when the user enters splitscreen.
*/
public void logEnter(float splitRatio,
@SplitPosition int mainStagePosition, int mainStageUid,
@SplitPosition int sideStagePosition, int sideStageUid,
boolean isLandscape) {
mLoggerSessionId = mIdSequence.newInstanceId();
int enterReason = mDragEnterPosition != SPLIT_POSITION_UNDEFINED
? getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape)
: SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW;
updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
mainStageUid);
updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape),
sideStageUid);
updateSplitRatioState(splitRatio);
FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__ENTER,
enterReason,
0 /* exitReason */,
splitRatio,
mLastMainStagePosition,
mLastMainStageUid,
mLastSideStagePosition,
mLastSideStageUid,
mDragEnterSessionId != null ? mDragEnterSessionId.getId() : 0,
mLoggerSessionId.getId());
}
/**
* Logs when the user exits splitscreen. Only one of the main or side stages should be
* specified to indicate which position was focused as a part of exiting (both can be unset).
*/
public void logExit(int exitReason, @SplitPosition int mainStagePosition, int mainStageUid,
@SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) {
if (mLoggerSessionId == null) {
// Ignore changes until we've started logging the session
return;
}
if ((mainStagePosition != SPLIT_POSITION_UNDEFINED
&& sideStagePosition != SPLIT_POSITION_UNDEFINED)
|| (mainStageUid != 0 && sideStageUid != 0)) {
throw new IllegalArgumentException("Only main or side stage should be set");
}
FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__EXIT,
0 /* enterReason */,
exitReason,
0f /* splitRatio */,
getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
mainStageUid,
getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape),
sideStageUid,
0 /* dragInstanceId */,
mLoggerSessionId.getId());
// Reset states
mLoggerSessionId = null;
mDragEnterPosition = SPLIT_POSITION_UNDEFINED;
mDragEnterSessionId = null;
mLastMainStagePosition = -1;
mLastMainStageUid = -1;
mLastSideStagePosition = -1;
mLastSideStageUid = -1;
}
/**
* Logs when an app in the main stage changes.
*/
public void logMainStageAppChange(@SplitPosition int mainStagePosition, int mainStageUid,
boolean isLandscape) {
if (mLoggerSessionId == null) {
// Ignore changes until we've started logging the session
return;
}
if (!updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition,
isLandscape), mainStageUid)) {
// Ignore if there are no user perceived changes
return;
}
FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE,
0 /* enterReason */,
0 /* exitReason */,
0f /* splitRatio */,
mLastMainStagePosition,
mLastMainStageUid,
0 /* sideStagePosition */,
0 /* sideStageUid */,
0 /* dragInstanceId */,
mLoggerSessionId.getId());
}
/**
* Logs when an app in the side stage changes.
*/
public void logSideStageAppChange(@SplitPosition int sideStagePosition, int sideStageUid,
boolean isLandscape) {
if (mLoggerSessionId == null) {
// Ignore changes until we've started logging the session
return;
}
if (!updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition,
isLandscape), sideStageUid)) {
// Ignore if there are no user perceived changes
return;
}
FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE,
0 /* enterReason */,
0 /* exitReason */,
0f /* splitRatio */,
0 /* mainStagePosition */,
0 /* mainStageUid */,
mLastSideStagePosition,
mLastSideStageUid,
0 /* dragInstanceId */,
mLoggerSessionId.getId());
}
/**
* Logs when the splitscreen ratio changes.
*/
public void logResize(float splitRatio) {
if (mLoggerSessionId == null) {
// Ignore changes until we've started logging the session
return;
}
if (splitRatio <= 0f || splitRatio >= 1f) {
// Don't bother reporting resizes that end up dismissing the split, that will be logged
// via the exit event
return;
}
if (!updateSplitRatioState(splitRatio)) {
// Ignore if there are no user perceived changes
return;
}
FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__RESIZE,
0 /* enterReason */,
0 /* exitReason */,
mLastSplitRatio,
0 /* mainStagePosition */, 0 /* mainStageUid */,
0 /* sideStagePosition */, 0 /* sideStageUid */,
0 /* dragInstanceId */,
mLoggerSessionId.getId());
}
/**
* Logs when the apps in splitscreen are swapped.
*/
public void logSwap(@SplitPosition int mainStagePosition, int mainStageUid,
@SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) {
if (mLoggerSessionId == null) {
// Ignore changes until we've started logging the session
return;
}
updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
mainStageUid);
updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape),
sideStageUid);
FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__SWAP,
0 /* enterReason */,
0 /* exitReason */,
0f /* splitRatio */,
mLastMainStagePosition,
mLastMainStageUid,
mLastSideStagePosition,
mLastSideStageUid,
0 /* dragInstanceId */,
mLoggerSessionId.getId());
}
private boolean updateMainStageState(int mainStagePosition, int mainStageUid) {
boolean changed = (mLastMainStagePosition != mainStagePosition)
|| (mLastMainStageUid != mainStageUid);
if (!changed) {
return false;
}
mLastMainStagePosition = mainStagePosition;
mLastMainStageUid = mainStageUid;
return true;
}
private boolean updateSideStageState(int sideStagePosition, int sideStageUid) {
boolean changed = (mLastSideStagePosition != sideStagePosition)
|| (mLastSideStageUid != sideStageUid);
if (!changed) {
return false;
}
mLastSideStagePosition = sideStagePosition;
mLastSideStageUid = sideStageUid;
return true;
}
private boolean updateSplitRatioState(float splitRatio) {
boolean changed = Float.compare(mLastSplitRatio, splitRatio) != 0;
if (!changed) {
return false;
}
mLastSplitRatio = splitRatio;
return true;
}
public int getDragEnterReasonFromSplitPosition(@SplitPosition int position,
boolean isLandscape) {
if (isLandscape) {
return position == SPLIT_POSITION_TOP_OR_LEFT
? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_LEFT
: FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_RIGHT;
} else {
return position == SPLIT_POSITION_TOP_OR_LEFT
? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_TOP
: FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_BOTTOM;
}
}
private int getMainStagePositionFromSplitPosition(@SplitPosition int position,
boolean isLandscape) {
if (position == SPLIT_POSITION_UNDEFINED) {
return 0;
}
if (isLandscape) {
return position == SPLIT_POSITION_TOP_OR_LEFT
? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__LEFT
: FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__RIGHT;
} else {
return position == SPLIT_POSITION_TOP_OR_LEFT
? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__TOP
: FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__BOTTOM;
}
}
private int getSideStagePositionFromSplitPosition(@SplitPosition int position,
boolean isLandscape) {
if (position == SPLIT_POSITION_UNDEFINED) {
return 0;
}
if (isLandscape) {
return position == SPLIT_POSITION_TOP_OR_LEFT
? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__LEFT
: FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__RIGHT;
} else {
return position == SPLIT_POSITION_TOP_OR_LEFT
? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__TOP
: FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__BOTTOM;
}
}
}

View File

@@ -0,0 +1,288 @@
/*
* Copyright (C) 2020 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.stagesplit;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
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;
import android.util.SparseArray;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.SurfaceUtils;
import com.android.wm.shell.common.SyncTransactionQueue;
import java.io.PrintWriter;
/**
* Base class that handle common task org. related for split-screen stages.
* Note that this class and its sub-class do not directly perform hierarchy operations.
* They only serve to hold a collection of tasks and provide APIs like
* {@link #setBounds(Rect, WindowContainerTransaction)} for the centralized {@link StageCoordinator}
* to perform operations in-sync with other containers.
*
* @see StageCoordinator
*/
class StageTaskListener implements ShellTaskOrganizer.TaskListener {
private static final String TAG = StageTaskListener.class.getSimpleName();
protected static final int[] CONTROLLED_ACTIVITY_TYPES = {ACTIVITY_TYPE_STANDARD};
protected static final int[] CONTROLLED_WINDOWING_MODES =
{WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED};
protected static final int[] CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE =
{WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW};
/** Callback interface for listening to changes in a split-screen stage. */
public interface StageListenerCallbacks {
void onRootTaskAppeared();
void onStatusChanged(boolean visible, boolean hasChildren);
void onChildTaskStatusChanged(int taskId, boolean present, boolean visible);
void onRootTaskVanished();
void onNoLongerSupportMultiWindow();
}
private final StageListenerCallbacks mCallbacks;
private final SurfaceSession mSurfaceSession;
protected final SyncTransactionQueue mSyncQueue;
protected ActivityManager.RunningTaskInfo mRootTaskInfo;
protected SurfaceControl mRootLeash;
protected SurfaceControl mDimLayer;
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,
@Nullable StageTaskUnfoldController stageTaskUnfoldController) {
mCallbacks = callbacks;
mSyncQueue = syncQueue;
mSurfaceSession = surfaceSession;
mStageTaskUnfoldController = stageTaskUnfoldController;
taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this);
}
int getChildCount() {
return mChildrenTaskInfo.size();
}
boolean containsTask(int taskId) {
return mChildrenTaskInfo.contains(taskId);
}
/**
* Returns the top activity uid for the top child task.
*/
int getTopChildTaskUid() {
for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
final ActivityManager.RunningTaskInfo info = mChildrenTaskInfo.valueAt(i);
if (info.topActivityInfo == null) {
continue;
}
return info.topActivityInfo.applicationInfo.uid;
}
return 0;
}
/** @return {@code true} if this listener contains the currently focused task. */
boolean isFocused() {
if (mRootTaskInfo == null) {
return false;
}
if (mRootTaskInfo.isFocused) {
return true;
}
for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
if (mChildrenTaskInfo.valueAt(i).isFocused) {
return true;
}
}
return false;
}
@Override
@CallSuper
public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
if (mRootTaskInfo == null && !taskInfo.hasParentTask()) {
mRootLeash = leash;
mRootTaskInfo = taskInfo;
mCallbacks.onRootTaskAppeared();
sendStatusChanged();
mSyncQueue.runInSync(t -> {
t.hide(mRootLeash);
mDimLayer =
SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer", mSurfaceSession);
});
} else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
final int taskId = taskInfo.taskId;
mChildrenLeashes.put(taskId, leash);
mChildrenTaskInfo.put(taskId, taskInfo);
updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */);
mCallbacks.onChildTaskStatusChanged(taskId, true /* present */, taskInfo.isVisible);
if (ENABLE_SHELL_TRANSITIONS) {
// Status is managed/synchronized by the transition lifecycle.
return;
}
sendStatusChanged();
} else {
throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
+ "\n mRootTaskInfo: " + mRootTaskInfo);
}
if (mStageTaskUnfoldController != null) {
mStageTaskUnfoldController.onTaskAppeared(taskInfo, leash);
}
}
@Override
@CallSuper
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
if (!taskInfo.supportsMultiWindow) {
// Leave split screen if the task no longer supports multi window.
mCallbacks.onNoLongerSupportMultiWindow();
return;
}
if (mRootTaskInfo.taskId == taskInfo.taskId) {
mRootTaskInfo = taskInfo;
} else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
mChildrenTaskInfo.put(taskInfo.taskId, taskInfo);
mCallbacks.onChildTaskStatusChanged(taskInfo.taskId, true /* present */,
taskInfo.isVisible);
if (!ENABLE_SHELL_TRANSITIONS) {
updateChildTaskSurface(
taskInfo, mChildrenLeashes.get(taskInfo.taskId), false /* firstAppeared */);
}
} else {
throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
+ "\n mRootTaskInfo: " + mRootTaskInfo);
}
if (ENABLE_SHELL_TRANSITIONS) {
// Status is managed/synchronized by the transition lifecycle.
return;
}
sendStatusChanged();
}
@Override
@CallSuper
public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
final int taskId = taskInfo.taskId;
if (mRootTaskInfo.taskId == taskId) {
mCallbacks.onRootTaskVanished();
mSyncQueue.runInSync(t -> t.remove(mDimLayer));
mRootTaskInfo = null;
} else if (mChildrenTaskInfo.contains(taskId)) {
mChildrenTaskInfo.remove(taskId);
mChildrenLeashes.remove(taskId);
mCallbacks.onChildTaskStatusChanged(taskId, false /* present */, taskInfo.isVisible);
if (ENABLE_SHELL_TRANSITIONS) {
// Status is managed/synchronized by the transition lifecycle.
return;
}
sendStatusChanged();
} else {
throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
+ "\n mRootTaskInfo: " + mRootTaskInfo);
}
if (mStageTaskUnfoldController != null) {
mStageTaskUnfoldController.onTaskVanished(taskInfo);
}
}
@Override
public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) {
if (mRootTaskInfo.taskId == taskId) {
b.setParent(mRootLeash);
} else if (mChildrenLeashes.contains(taskId)) {
b.setParent(mChildrenLeashes.get(taskId));
} else {
throw new IllegalArgumentException("There is no surface for taskId=" + taskId);
}
}
void setBounds(Rect bounds, WindowContainerTransaction wct) {
wct.setBounds(mRootTaskInfo.token, bounds);
}
void reorderChild(int taskId, boolean onTop, WindowContainerTransaction wct) {
if (!containsTask(taskId)) {
return;
}
wct.reorder(mChildrenTaskInfo.get(taskId).token, onTop /* onTop */);
}
void setVisibility(boolean visible, WindowContainerTransaction wct) {
wct.reorder(mRootTaskInfo.token, visible /* onTop */);
}
void onSplitScreenListenerRegistered(SplitScreen.SplitScreenListener listener,
@SplitScreen.StageType int stage) {
for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
int taskId = mChildrenTaskInfo.keyAt(i);
listener.onTaskStageChanged(taskId, stage,
mChildrenTaskInfo.get(taskId).isVisible);
}
}
private void updateChildTaskSurface(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl leash, boolean firstAppeared) {
final Point taskPositionInParent = taskInfo.positionInParent;
mSyncQueue.runInSync(t -> {
t.setWindowCrop(leash, null);
t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y);
if (firstAppeared && !ENABLE_SHELL_TRANSITIONS) {
t.setAlpha(leash, 1f);
t.setMatrix(leash, 1, 0, 0, 1);
t.show(leash);
}
});
}
private void sendStatusChanged() {
mCallbacks.onStatusChanged(mRootTaskInfo.isVisible, mChildrenTaskInfo.size() > 0);
}
@Override
@CallSuper
public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
final String childPrefix = innerPrefix + " ";
pw.println(prefix + this);
}
}

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.stagesplit;
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);
}
}
}