Merge "Copy current split codes to stagesplit package" into sc-v2-dev
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user