From 7bf62d76e8c0aa5668df90cdb6a6b3ebe34e3439 Mon Sep 17 00:00:00 2001 From: Jerry Chang Date: Mon, 21 Jun 2021 06:16:49 +0000 Subject: [PATCH] Add border to indicate side stage split region Add a view to draw border of side stage root task. Bug: 189839391 Test: manual checks side stage border. Change-Id: Ie0606338d6907f92c80f11c7d86dca6bf525e45d --- .../Shell/res/layout/split_outline.xml | 26 ++++ .../wm/shell/common/split/SplitLayout.java | 5 +- .../wm/shell/splitscreen/OutlineManager.java | 125 ++++++++++++++++++ .../wm/shell/splitscreen/OutlineRoot.java | 62 +++++++++ .../wm/shell/splitscreen/OutlineView.java | 76 +++++++++++ .../wm/shell/splitscreen/SideStage.java | 32 ++++- .../shell/splitscreen/StageCoordinator.java | 1 + .../wm/shell/splitscreen/SideStageTests.java | 7 +- .../splitscreen/SplitTransitionTests.java | 2 +- 9 files changed, 328 insertions(+), 8 deletions(-) create mode 100644 libs/WindowManager/Shell/res/layout/split_outline.xml create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineManager.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineRoot.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineView.java diff --git a/libs/WindowManager/Shell/res/layout/split_outline.xml b/libs/WindowManager/Shell/res/layout/split_outline.xml new file mode 100644 index 0000000000000..4e2a77f213a00 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/split_outline.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index d844dd87438ce..a79807bac76c2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -311,9 +311,8 @@ public final class SplitLayout { return context.getSystemService(WindowManager.class) .getMaximumWindowMetrics() .getWindowInsets() - .getInsets(WindowInsets.Type.navigationBars() - | WindowInsets.Type.statusBars() - | WindowInsets.Type.displayCutout()).toRect(); + .getInsets(WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()) + .toRect(); } private static boolean isLandscape(Rect bounds) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineManager.java new file mode 100644 index 0000000000000..5d5a6e50341af --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineManager.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.splitscreen; + +import static android.view.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.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.LayoutInflater; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; +import android.view.WindowlessWindowManager; + +import com.android.wm.shell.R; + +import java.util.function.Supplier; + +/** + * 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 int mOutlineColor; + private final Rect mOutlineBounds = new Rect(); + private final Rect mTmpBounds = new Rect(); + private final Supplier mOutlineSurfaceSupplier; + private SurfaceControlViewHost mViewHost; + + /** + * Constructs {@link #OutlineManager} with indicated outline color for the provided root + * surface. + */ + OutlineManager(Context context, Configuration configuration, + Supplier outlineSurfaceSupplier, int color) { + super(configuration, null /* rootSurface */, null /* hostInputToken */); + mContext = context.createDisplayContext(context.getDisplay()); + mOutlineSurfaceSupplier = outlineSurfaceSupplier; + mOutlineColor = color; + } + + @Override + protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { + b.setParent(mOutlineSurfaceSupplier.get()); + } + + boolean updateOutlineBounds(Rect rootBounds) { + computeOutlineBounds(mContext, rootBounds, mTmpBounds); + if (mOutlineBounds.equals(mTmpBounds)) { + return false; + } + mOutlineBounds.set(mTmpBounds); + + if (mViewHost == null) { + mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this); + } + if (mViewHost.getView() == null) { + final OutlineRoot rootView = (OutlineRoot) LayoutInflater.from(mContext) + .inflate(R.layout.split_outline, null); + rootView.updateOutlineBounds(mOutlineBounds, mOutlineColor); + + final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + rootBounds.width(), rootBounds.height(), + TYPE_APPLICATION_OVERLAY, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, + PixelFormat.TRANSLUCENT); + 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(rootView, lp); + } else { + ((OutlineRoot) mViewHost.getView()).updateOutlineBounds(mOutlineBounds, mOutlineColor); + final WindowManager.LayoutParams lp = + (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams(); + lp.width = rootBounds.width(); + lp.height = rootBounds.height(); + mViewHost.relayout(lp); + } + + return true; + } + + private static void computeOutlineBounds(Context context, Rect rootBounds, Rect outBounds) { + computeDisplayStableBounds(context, outBounds); + outBounds.intersect(rootBounds); + // Offset the coordinate from screen based to surface based. + outBounds.offset(-rootBounds.left, -rootBounds.top); + } + + private static void computeDisplayStableBounds(Context context, Rect outBounds) { + final WindowMetrics windowMetrics = + context.getSystemService(WindowManager.class).getMaximumWindowMetrics(); + outBounds.set(windowMetrics.getBounds()); + outBounds.inset(windowMetrics.getWindowInsets().getInsets( + WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout())); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineRoot.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineRoot.java new file mode 100644 index 0000000000000..71d48eeca71df --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineRoot.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.splitscreen; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.R; + +/** Root layout for holding split outline. */ +public class OutlineRoot extends FrameLayout { + public OutlineRoot(@NonNull Context context) { + super(context); + } + + public OutlineRoot(@NonNull Context context, + @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public OutlineRoot(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public OutlineRoot(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + private OutlineView mOutlineView; + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mOutlineView = findViewById(R.id.split_outline); + } + + void updateOutlineBounds(Rect bounds, int color) { + mOutlineView.updateOutlineBounds(bounds, color); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineView.java new file mode 100644 index 0000000000000..ea66180e3dd22 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineView.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.splitscreen; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.Region; +import android.util.AttributeSet; +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 Rect mBounds = new Rect(); + + public OutlineView(@NonNull Context context) { + super(context); + } + + public OutlineView(@NonNull Context context, + @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public OutlineView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public OutlineView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(getResources() + .getDimension(R.dimen.accessibility_focus_highlight_stroke_width)); + } + + void updateOutlineBounds(Rect bounds, int color) { + if (mBounds.equals(bounds) && mPaint.getColor() == color) return; + mBounds.set(bounds); + mPaint.setColor(color); + } + + @Override + protected void onDraw(Canvas canvas) { + if (mBounds.isEmpty()) return; + final Path path = new Region(mBounds).getBoundaryPath(); + canvas.drawPath(path, mPaint); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java index 82f95a4f32ea5..a0bdcc3edd5f1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java @@ -16,8 +16,12 @@ package com.android.wm.shell.splitscreen; +import android.annotation.CallSuper; import android.app.ActivityManager; +import android.content.Context; +import android.graphics.Color; import android.graphics.Rect; +import android.view.SurfaceControl; import android.view.SurfaceSession; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -28,15 +32,19 @@ 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 { private static final String TAG = SideStage.class.getSimpleName(); + private final Context mContext; + private OutlineManager mOutlineManager; - SideStage(ShellTaskOrganizer taskOrganizer, int displayId, + SideStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId, StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, SurfaceSession surfaceSession) { super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession); + mContext = context; } void addTask(ActivityManager.RunningTaskInfo task, Rect rootBounds, @@ -69,4 +77,26 @@ class SideStage extends StageTaskListener { wct.reparent(task.token, newParent, false /* onTop */); return true; } + + @Override + @CallSuper + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + super.onTaskAppeared(taskInfo, leash); + if (mRootTaskInfo != null && mRootTaskInfo.taskId == taskInfo.taskId) { + mOutlineManager = new OutlineManager(mContext, mRootTaskInfo.configuration, + () -> mRootLeash, + Color.YELLOW); + } + } + + @Override + @CallSuper + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + super.onTaskInfoChanged(taskInfo); + if (mRootTaskInfo != null && mRootTaskInfo.taskId == taskInfo.taskId + && mRootTaskInfo.isRunning) { + mOutlineManager.updateOutlineBounds( + mRootTaskInfo.configuration.windowConfiguration.getBounds()); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 467f8c71a1893..c6aacb1ab501f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -152,6 +152,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSyncQueue, mSurfaceSession); mSideStage = new SideStage( + mContext, mTaskOrganizer, mDisplayId, mSideStageListener, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java index 56a005642ce22..69ead3ac9cf95 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java @@ -33,6 +33,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.SyncTransactionQueue; @@ -46,7 +47,7 @@ import org.mockito.Spy; /** Tests for {@link SideStage} */ @SmallTest @RunWith(AndroidJUnit4.class) -public class SideStageTests { +public class SideStageTests extends ShellTestCase { @Mock private ShellTaskOrganizer mTaskOrganizer; @Mock private StageTaskListener.StageListenerCallbacks mCallbacks; @Mock private SyncTransactionQueue mSyncQueue; @@ -60,8 +61,8 @@ public class SideStageTests { public void setup() { MockitoAnnotations.initMocks(this); mRootTask = new TestRunningTaskInfoBuilder().build(); - mSideStage = new SideStage(mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, mSyncQueue, - mSurfaceSession); + mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, + mSyncQueue, mSurfaceSession); mSideStage.onTaskAppeared(mRootTask, mRootLeash); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index 5d9d50fd796e6..b6da8681d8503 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -102,7 +102,7 @@ public class SplitTransitionTests extends ShellTestCase { mMainStage = new MainStage(mTaskOrganizer, DEFAULT_DISPLAY, mock( StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession); mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface()); - mSideStage = new SideStage(mTaskOrganizer, DEFAULT_DISPLAY, mock( + mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock( StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession); mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface()); mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,