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